Skip to content

[drivers] driver-memory / driver-mongodb disagree with the SQL family on count_distinct — the two AGGREGATION_CASES cells #6409 left open #6814

Description

@os-zhuang

Filed unassigned from the #6409 lane (the enforce half of #6188). Recording, not claiming — both packages are inside the #5499 investment freeze and #6409's ruling put them explicitly out of scope. This issue exists so the two open cells of the new AGGREGATION_CASES matrix column have a measured verdict behind them instead of an omission.

Context

#6409 lowered count_distinct to COUNT(DISTINCT x) on the SQL family and introduced AGGREGATION_CASES / AGGREGATION_ROWS (@objectstack/spec/data) — the shared standard the aggregate lowerings are now held to. Three of the five drivers were enrolled by that PR (driver-sql, driver-turso REMOTE, driver-sqlite-wasm). The other two carry DEBT rows in scripts/check-driver-conformance.mjs pointing here.

The standard's answer for count_distinct is distinct NON-NULL values of the target column — what COUNT(DISTINCT col) computes on SQLite, PostgreSQL and MySQL alike, and what objectql's in-memory fallback (in-memory-aggregation.ts, new Set(values.filter(v => v != null)).size) and service-analytics's AGGREGATE_SQL already give.

What each package answers today

Read from the source on origin/main @ 6de592c; not executed against the fixture, since neither cell has a suite (that is the debt).

1. driver-memory — the DATA face answers null, silently

MemoryDriver.computeAggregate (packages/drivers/driver-memory/src/memory-driver.ts) has arms for count, sum, avg, min, max and then:

default:
returnnull;

There is no count_distinct arm. So aggregate() with { function: 'count_distinct', field: 'stage', alias: 'n' } resolves with { n: null } — no error, no log, no refusal. Against AGGREGATION_ROWS the standard says 2.

This is the worse half of the pair: a wrong ANSWER rather than a wrong number. It is also the default:-arm shape #4157 and the aggregation-lockstep guard in service-analytics exist to prevent one layer up, arriving here through a different door.

Note the package is partial in the way #6409's ruling described: its ANALYTICS face (memory-analytics.ts) does implement count_distinct ($addToSet, then sized). It is only the data-driver aggregate() path that has no arm — so this package answers one declared function two ways depending on which face you enter, the divergence class #5374 fixed for $contains in the same package.

2. driver-mongodb — counts NULL as a distinct value

mongodb-aggregation.ts lowers count_distinct to { $addToSet: fieldRef ?? null }, and postProcessAggregation takes the array's .length. $addToSet adds an explicit null to the set, so a column with rows won, won, lost, null, won, null sizes to 3 where the standard says 2.

$addToSet on a MISSING field adds nothing, so the divergence only shows for an explicitly-null value — which is exactly what AGGREGATION_ROWS seeds and exactly what a nullable column produces in practice.

Same file still carries a string_agg arm and a string_agg branch in postProcessAggregation; string_agg was retired from AggregationFunction at #6188, so that code is no longer reachable through a spec-valid request. Not a defect — noted so it is not rediscovered as one — and untouched for the same freeze reason.

Disposition

Deliberately none proposed. Both packages are #5499-frozen, so this is a decision about the freeze, not about the code: the fix on driver-memory is a count_distinct arm beside its neighbours, and on driver-mongodb a $ne: null filter before the $addToSet (or sizing a $setDifference against [null]). Neither is difficult; both are inside the boundary.

Clearing a cell means writing the suite that runs AGGREGATION_CASES and deleting the ledger row in the same PR — the gate fails either half alone. driver-mongodb's real-mongod suites are opt-in since #5517, so whatever clears its cell needs a server-free half in the shape of mongodb-filter-logic-translation.test.ts.

Refs: #6409 (the lane that measured this), #6188 (the split ruling), #5499 (the freeze), #6682 / #6520 (the same pair's other two open cells).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions