Skip to content

avg over a boolean field returns null on driver-memory and a number on sqlite — same rows, same dataset, same executor #11065

Description

@os-sam

Measured while working objectstack-ai/hotcrm#1213. Filed as a finding, unassigned — I am not proposing a fix, and the severity call belongs to triage.

What was measured

One AnalyticsService.queryDataset call, one dataset, one set of rows, run twice — once on InMemoryDriver, once on SqliteWasmDriver — against @objectstack/*17.1.0 as pinned by hotcrm.

The measure under test is an average over a boolean column:

{name: 'avg_sla_violated',label: 'SLA Violation Rate',aggregate: 'avg',field: 'is_sla_violated',format: '0.0%'}

Rows: 5 crm_case records — 4 closed (3 with is_sla_violated: false, 1 true), 1 open with true.

[memory] unfiltered {"avg_sla_violated":null, "closed_count":4,"sla_met_count":3}
[memory] closed-filter {"avg_sla_violated":null, "closed_count":4,"sla_met_count":3}
[sqlite] unfiltered {"avg_sla_violated":0.4, "closed_count":4,"sla_met_count":3}
[sqlite] closed-filter {"avg_sla_violated":0.25, "closed_count":4,"sla_met_count":3}

SQLite's numbers are the arithmetically correct ones (2/5 unfiltered, 1/4 over closed cases). driver-memory returns null for the same question.

Two controls in the same rows show the divergence is specific to averaging a boolean, not to booleans generally and not to the rows:

  • count measures carrying boolean predicates (filter: { is_closed: true, is_sla_violated: false }) agree exactly across both drivers (closed_count 4, sla_met_count 3) — so boolean filtering is fine on both.
  • A derived ratio built on those counts computes identically on both (0.75).

Why it is worth recording

null and a number are not two spellings of one answer. A dashboard tile bound to such a measure renders a value on one driver and a blank on the other, with no error on either path — and the blank is indistinguishable from "no matching rows". hotcrm ships exactly this shape: its Service dashboard's Open Cases by Priority table has an SLA Violation Rate column bound to this measure, which renders in the app (SQLite) and would be blank against driver-memory.

It also silently weakens tests: a suite that pins a rate measure on the in-memory driver asserts against null and cannot fail in the direction that matters. That is how I hit it — an assertion written against the violation rate passed on SQLite and read null on memory.

What I did not determine

I did not read the driver or objectql aggregate source, so I am not claiming where the divergence lives (driver-memory's aggregate implementation, the ObjectQL aggregate bridge, or a deliberate refusal to average a non-numeric column). If it is deliberate, the two faces still disagree, and a refusal would be more useful than null.

Related but distinct cells of the same "drivers disagree about an aggregate" family, both closed: #6814 (count_distinct), #6815 (AggregationNode.distinct).

Reproduction

test/knowledge-deflection.test.ts and test/sla-compliance-gauge.test.ts in objectstack-ai/hotcrm both build the two-driver harness this used (ObjectQL.create + AnalyticsService with the executeAggregate bridge the plugin wires at boot); adding an avg measure over is_sla_violated to either reproduces it.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions