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
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.queryDatasetcall, one dataset, one set of rows, run twice — once onInMemoryDriver, once onSqliteWasmDriver— against@objectstack/*17.1.0 as pinned by hotcrm.The measure under test is an average over a boolean column:
Rows: 5
crm_caserecords — 4 closed (3 withis_sla_violated: false, 1true), 1 open withtrue.SQLite's numbers are the arithmetically correct ones (2/5 unfiltered, 1/4 over closed cases). driver-memory returns
nullfor 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:
countmeasures carrying boolean predicates (filter: { is_closed: true, is_sla_violated: false }) agree exactly across both drivers (closed_count4,sla_met_count3) — so boolean filtering is fine on both.derivedratio built on those counts computes identically on both (0.75).Why it is worth recording
nulland 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'sOpen Cases by Prioritytable 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
nulland cannot fail in the direction that matters. That is how I hit it — an assertion written against the violation rate passed on SQLite and readnullon memory.What I did not determine
I did not read the driver or
objectqlaggregate 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 thannull.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.tsandtest/sla-compliance-gauge.test.tsinobjectstack-ai/hotcrmboth build the two-driver harness this used (ObjectQL.create+AnalyticsServicewith theexecuteAggregatebridge the plugin wires at boot); adding anavgmeasure overis_sla_violatedto either reproduces it.Generated by Claude Code