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

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Triage: needs-user-decision + domain:spec + target:v17 (labels and this comment land as a pair).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling (2026-08-09): REMOVE. AggregationNode.distinct is retired per ADR-0049. pm:queue.

    Rationale (three-axis): count_distinct already covers the only spelling with measured demand, and SUM(DISTINCT …) / AVG(DISTINCT …) are near-universally a modelling mistake — no business pull for the ENFORCE leg. Long-term, "same query, two numbers, chosen by which backend answered" is the worst divergence class — a plausible wrong number instead of a refusal — and must not ship declared-but-unenforced. And for AI authors the flag is a perfect trap: it reads as supported, silently changes aggregates only on the in-memory fallback, and nothing surfaces the divergence.

    Implementation notes from the card, adopted: the key sits on the request surface (QueryAST builder output / POST /data/:object/query body), so no stored-metadata conversion is needed (the #4286 note applies verbatim); removal costs the in-memory fallback's collectValues flag and nothing else. The out-of-scope note stands — sum(*)-with-no-field dying as a raw dialect error is a separate gap, not to be smuggled into this PR.

    Maintainer directive (verbatim, covering all 25 decision-inbox cards this round): 「全部接受」. Recorded by PM session session_01LGRN2cSRfggfX9B2L83bQc. Veto window open — comment to overturn.


    Generated by Claude Code

  3. self-assigned this
    on Aug 9, 2026
  4. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 (seat restart; maintainer acceleration directive 2026-08-09 ~06:50Z)
    Session: session_01PiRUoQkTSBBmpyXBY3cVn2
    Branch: claude/issue-6815-aggregation-distinct-retired
    Worktree: objectstack-issue-6815
    Domain: domain:spec
    File surface: packages/spec/src/data/query.zod.ts (retire AggregationNode.distinct per the 2026-08-09 REMOVE ruling), packages/spec/src/migrations/registry.ts (D3 entry + RETIRED_KEYS registration; ⛔ NO D2 — request surface, the #4286 note applies verbatim), packages/objectql/src/in-memory-aggregation.ts (drop the collectValues distinct flag), client SDK builder sweep (packages/client — remove any producer, the #6866 precedent), pins/tests, spec generated trees. (Stop on breach; explain in the report.)
    Serial constraints cleared: same-round siblings #4697 (automation/) and #6704 (api/export.zod.ts) are source-disjoint; all three collide on spec GENERATED artifacts, and this card additionally touches migrations/registry.ts — the current hottest table (#6866 took three re-merge laps) — so landing is serialized by this seat. #5499-frozen drivers (memory/mongodb): no changes needed there — removal makes every face uniform by construction.
    Container assessment: M, mode:subagent shared container.


    Generated by Claude Code

  5. added a commit that references this issue on Aug 9, 2026
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