Skip to content

[spec/drivers] AggregationNode.distinct is honoured by the in-memory fallback and ignored by every SQL face — one query, two numbers (ADR-0049) #6815

Description

@os-zhuang

Filed unassigned from the #6409 lane. Found while lowering count_distinct; adjacent to it, not created by it.

The finding

AggregationNodeSchema (packages/spec/src/data/query.zod.ts) declares a per-aggregation flag:

distinct: z.boolean().optional().describe('Apply DISTINCT before aggregation'),

Exactly one consumer reads it. packages/objectql/src/in-memory-aggregation.ts:

const values = collectValues(rows, field, !!agg.distinct);

Nothing else in the repo does — measured by grepping every consumer of aggregations[]:

Face Reads agg.distinct?
objectql in-memory fallback yes — deduplicates before applying the function
driver-sql SqlDriver.aggregate no
driver-turso RemoteTransport.aggregate no
driver-mongodb buildAggregationStage no
driver-memory computeAggregate no
service-analytics AGGREGATE_SQL no

So { function: 'sum', field: 'amount', distinct: true } returns a deduplicated sum when the engine falls back in memory and an ordinary sum on every SQL datasource. Same query, two numbers, chosen by which backend answered — the divergence class #6203 and #5907 each closed on the aggregate axis, still open on this key. And unlike those, the wrong answer here is a plausible number rather than a refusal, so nothing surfaces it.

The key was NOT covered by #4286, which swept the request surface: that issue dispositioned QueryAST.distinct (the query-level one, since retired) and AggregationNode.filter (marked [EXPERIMENTAL — not enforced]). AggregationNode.distinct is neither — it is declared plainly, has a real consumer, and reads as supported.

Why it is worth a decision now rather than later

#6409 lowered count_distinct on the SQL family, which changes what this key looks like to an author. count_distinct is now a working, portable, deduplicated count; distinct: true beside it is the affordance that reads as "…and the same for sum/avg". One of the two now works everywhere and the other works in one place, which is a sharper false affordance than it was yesterday.

Two legs, per ADR-0049

ENFORCE. SELECT SUM(DISTINCT col) is standard SQL and supported by SQLite, PostgreSQL and MySQL, so the lowering exists and is portable — the same argument that kept count_distinct declared at #6188. driver-sql's and driver-turso's lowering tables already carry a distinct flag as of #6409, so the emitters have the shape; what they lack is reading it off the NODE as well as off the function. driver-memory/driver-mongodb are #5499-frozen, which is the usual constraint on this leg.

REMOVE. count_distinct covers the only spelling anyone has measured demand for, SUM(DISTINCT …) / AVG(DISTINCT …) are near-universally a modelling mistake, and the key has no stored-metadata authoring surface — QueryAST is the client SDK builder's output and the POST /data/:object/query body, so there is no conversion to write (the #4286 note on the request surface applies verbatim). Removal costs the in-memory fallback its collectValues flag and nothing else.

No recommendation offered — this is the maintainer's call, in the same shape as #6188's.

What is NOT in scope of this issue

count(distinct *) and friends: count_distinct with no field is refused with INVALID_QUERY / 400 as of #6409, on both SQL faces. Separately, sum/avg/min/max written with no field still emit sum(*) and die as a dialect syntax error with no ADR-0112 envelope — a real but distinct gap, and one #6409 deliberately did not widen its refusal surface to cover.

Refs: #6409, #6188, #4286, ADR-0049, #5499.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions