Skip to content

fix(driver-mongodb): take a structured GroupByNode, and answer count/count_distinct like every other backend - #7550

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-6850-mongodb-structured-groupby
Aug 11, 2026
Merged

fix(driver-mongodb): take a structured GroupByNode, and answer count/count_distinct like every other backend#7550
huangyiirene merged 1 commit into
mainfrom
claude/issue-6850-mongodb-structured-groupby

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6850
Part of #6814 — the driver-mongodb half only. driver-memory stays under the #5499 freeze and keeps its AGGREGATION_CASES DEBT row, so #6814 must stay OPEN after this lands.

Authorized by the maintainer's 2026-08-11 partial unfreeze on #5499 (comment 5249019855), which is the restart condition both findings-round gradings named.

What was wrong — three divergences, all of which ANSWER

driver-mongodb is now enrolled in the shared AGGREGATION_CASES standard (@objectstack/spec/data). Clearing the cell fixed three things. None of them ever surfaced as an error, which is the point: each returned plausible rows.

1. A structured GroupByNode had no lowering at all (#6850).GroupByNodeSchema declares a union — a bare field name, or { field, dateGranularity?, alias? }. buildAggregationPipeline annotated groupBy as string[] and did groupId[field] = '$' + field. A structured node is an object in that loop, so the $group._id key became the literal "[object Object]" and its value the field path "$[object Object]", which matches nothing. MongoDBDriver.aggregate passed the value through (query as any).groupBy, which is why the declared union never met that annotation at tsc.

Both sides now spell GroupByNode[]. The _id keys on alias ?? field with the field as its value, so the projected column is renamed and the grouping does not move — the rule #6401 converged the three SQL faces onto, and the one in-memory-aggregation.ts has always applied. The bare-string spelling emits exactly what it emitted before (pinned).

Verified the type guard is now real rather than nominal: a probe assigning DriverQuery['groupBy'] to string[] fails tsc with the full union, so the next drift between the two is a compile error.

2. count_distinct counted NULL as a distinct value (#6814).$addToSet keeps an explicit null, so a nullable column sized one higher than COUNT(DISTINCT col) — 3 where the standard says 2.

3. count(col) counted ROWS, not values — named by neither card. Measured while writing the suite: the count arm ignored field and emitted { $sum: 1 } for both spellings, so count(stage) answered 6 — the number count(*) already has — where the standard says 4. This is the case-set doing exactly what its own note says it is for ("three different numbers over one column"). It now counts non-null values, with a missing field reading as null (the SQL reading: an absent value is NULL, there is no third state).

Decisions worth reviewing

dateGranularity is REFUSED, not implemented and not ignored.NOT_IMPLEMENTED / 501 in the ADR-0112 envelope — driver-sql's and driver-turso's refusal, first sentence for first sentence (#6212). The reasoning is measured, not assumed: MongoDBDriver.supports publishes no queryDateGranularity, so the engine buckets every granularity in memory and never pushes a bucketed node down here; the refusal fires only for a caller that reached the builder directly — who previously got the "[object Object]" grouping. A native $dateTrunc lowering is buildable and this is not a verdict that it cannot be: it has to emit the engine's bucket labels ('2026-01', '2026-Q1', ISO '2026-W03'), publish the capability record, and be held to date-bucket-parity.test.ts — a card of its own. Silently ignoring a declared key was the one option not available.

The count_distinct null exclusion is in postProcessAggregation, not in the pipeline. Both server-side spellings the finding sketched were measured against that and not taken:

  • $ne: null before the $addToSet — as a $match it drops the row from the whole pipeline, so a count(*) or sum() sharing it would silently lose the null rows too. It is correct only for a pipeline carrying nothing else, which this builder cannot assume.
  • $size of a $setDifference against [null] — sound, and it would size server-side instead of shipping the array. It needs a $project stage the builder does not emit when there is no groupBy, i.e. a pipeline shape change no suite here can execute. Worth doing when this cell gains a live half; the existing Array.isArray guard would let it fall through harmlessly.

Excluding it in post-processing is exact, needs no unobserved server semantics to be true, and is pinned directly.

The suite, and what it does not claim

mongodb-aggregation-translation.test.ts — the server-free half #5517 requires, mirroring mongodb-filter-logic-translation.test.ts. It drives the emitted pipeline through a strict in-process evaluator of the stages the builder emits; every stage, accumulator and expression it does not model is a thrown error, never a tolerated no-op. Its discrimination is proved rather than asserted: each of the three pre-fix lowerings is replayed through it and must FAIL the case it broke, including the "[object Object]"$group._id — executed, it collapses all six rows into one group under a column literally named [object Object], holding null.

It also runs every grouped case a second time through the plain structured spelling { field: 'region' } — the half of the union that no capability bit guards and that the engine pushes down to every driver.

What it deliberately does not answer: whether a real mongod agrees.$cond / $ifNull / $addToSet are modelled from the documentation, not observed — this environment cannot fetch a mongod binary (proxy 403), and a suite nobody has executed is a claim, not a check. That bound is written into the suite header, into the AGGREGATION_CASES enrolment list, and into the ledger note, because a green cell reads as more than it is.

Ledger and table

  • scripts/check-driver-conformance.mjs: the driver-mongodb × AGGREGATION_CASES DEBT row is deleted in this PR, with the suite that replaces it — the gate fails either half alone. driver-memory's row is untouched. The section note records that the deleted row was right about what it measured and under-counted what it had not (defect 3), which is the argument for the suite rather than against the ledger.
  • packages/spec/src/data/aggregation-conformance.ts: comment-only — driver-mongodb moves from the DEBT table to the enrolled list, with the bound above stated.

Gates run locally

GateResult
pnpm --filter @objectstack/driver-mongodb test266 passed, 143 skipped (the opt-in real-mongod halves)
pnpm --filter @objectstack/driver-mongodb typecheckclean
pnpm --filter @objectstack/spec typecheckclean
pnpm check:driver-conformance (+ --self-test)OK — 37 covered cells, 3 DEBT, 0 exempt
eslint --no-inline-config on every changed fileclean
build closure (--filter @objectstack/driver-mongodb...)clean

The rest of the lint farm is left to CI.

Deliberately untouched

🤖 Generated with Claude Code

https://claude.ai/code/session_01PeD85qA1JNLXodmYdnu4Zm


Generated by Claude Code

…count_distinct like every other backend (#6850, #6814)
`driver-mongodb` is now enrolled in the shared `AGGREGATION_CASES` standard.
Clearing that cell fixed three divergences, all of the kind that ANSWER rather
than fail — which is why none of them ever surfaced as an error:
1. #6850 — a structured `GroupByNode` had no lowering at all. `groupBy` was
annotated `string[]` and `groupId[field] = '$' + field` stringified the
object, so the `$group._id` key became the literal `"[object Object]"` and
its value a field path matching nothing: rows grouped by a nonexistent path,
under a column of that name. `MongoDBDriver.aggregate` passed the value
through an `any` cast, which is why the declared union never met that
annotation at `tsc`. Both sides now spell `GroupByNode[]`, and the `_id`
keys on `alias ?? field` with the FIELD as its value — the #6401 rule.
2. #6814 — `count_distinct` sized a `$addToSet` that keeps explicit nulls, so a
nullable column answered 3 where the standard says 2. The sizing now excludes
null, matching `COUNT(DISTINCT col)` and the in-memory fallback.
3. Measured here, named by neither card — `count(col)` ignored `field` and
emitted `{ $sum: 1 }`, so `count(stage)` came back 6 where the standard says
4. It now counts non-null values, a missing field reading as null.
A `dateGranularity` node is refused with NOT_IMPLEMENTED/501 in the ADR-0112
envelope rather than silently ignored — the `driver-sql` / `driver-turso`
refusal, first sentence for first sentence (#6212). A native `$dateTrunc`
lowering needs the engine's bucket labels, a published capability record and
`date-bucket-parity.test.ts`, so it is its own change. A `groupBy` entry that is
neither half of the union is refused with INVALID_QUERY/400.
The suite is server-free (the real-mongod halves are opt-in since #5517): it
drives the EMITTED pipeline through a strict in-process evaluator that refuses
every shape it does not model, and each pre-fix lowering is replayed through it
and must fail the case it broke. It holds the LOWERING to the table and does not
answer "does MongoDB agree?" — recorded as open rather than implied. The
`driver-mongodb` x `AGGREGATION_CASES` DEBT row goes with it, in this PR, per
the gate's rule.
driver-memory is untouched: it stays under the #5499 freeze and keeps its row,
so #6814 stays open for its half.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeD85qA1JNLXodmYdnu4Zm
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 11, 2026 5:09am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-mongodb, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-mongodb)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/driver-mongodb, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 11, 2026
@huangyiirene
huangyiirene marked this pull request as ready for review August 11, 2026 05:35
@huangyiirene
huangyiirene added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit f067930Aug 11, 2026
27 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-6850-mongodb-structured-groupby branch August 11, 2026 05:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding][drivers] driver-mongodb cannot take a structured GroupByNode at all — the object stringifies into a "[object Object]" $group._id

1 participant

@huangyiirene