Skip to content

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

Description

@claude

Found while implementing #6401 (GroupByNode.alias on the SQL faces). Adjacent to it, not created by it. Filed unassigned, recording not claiming — driver-mongodb is inside the #5499 investment freeze.

The finding

GroupByNodeSchema declares a UNION: a bare field name, or a structured { field, dateGranularity?, alias? } node. driver-mongodb can only take the first half.

packages/drivers/driver-mongodb/src/mongodb-aggregation.ts:

groupBy?: string[];                       // :38 — half of the declared union
...
if (opts.groupBy && opts.groupBy.length > 0) {
  for (const field of opts.groupBy) {
    groupId[field] = `$${field}`;         // :66-69
  }
}
...
for (const field of opts.groupBy) {
  project[field] = `$_id.${field}`;       // :85-88 — mirrored in $project
}

A structured node is an object in that loop. groupId[field] stringifies it, so the $group._id key becomes the literal "[object Object]" and its value the literal "$[object Object]" — a field path that matches nothing. The $project stage mirrors the same key.

So the aggregation does not refuse and does not throw: it returns rows grouped by a nonexistent field path, under a column named [object Object].

Why tsc never saw it

packages/drivers/driver-mongodb/src/mongodb-driver.ts:512 passes the value through an any cast:

groupBy: (query as any).groupBy,

so the declared GroupByNode[] union never meets that string[] annotation. The annotation is a restatement of the protocol that drifted from it — the same shape #6212 closed for driver-turso's remote transport, which read groupBy as string[] and died on "[object Object]" as an unsafe identifier. That one at least failed loudly; this one answers.

Scope note — this is NOT the alias divergence

#6401 converged the three SQL faces onto alias ?? field for the projected column. driver-mongodb is not a fourth face of that divergence: the alias is unreachable here rather than ignored, because the whole structured half of the union is. Fixing alias alone would not help — the node has to be destructured first.

Recorded as a measured DEBT row in scripts/check-driver-conformance.mjs (the driver-mongodb × AGGREGATION_CASES cell) by #6401, so the conformance matrix carries the verdict rather than an omission.

Relationship to #6814

#6814 is the count_distinct disagreement for the same frozen pair — a wrong NUMBER from a lowering that exists. This is a different defect: a declared SHAPE that has no lowering at all. Same package, same freeze, separate cells; filing separately so the freeze is lifted against a measured list rather than one line item.

Verdict

Read from the source; not executed — this package has no server-free aggregation suite for the cell, which is part of the debt (the real-mongod suites are opt-in since #5517, so whatever closes this needs a server-free half like mongodb-filter-logic-translation.test.ts has).

Not fixed here: #5499 freezes the package, and #6401's ruling kept mechanical alignment out of the enforce change on principle — a frozen driver gets an honest row or a mechanical alignment, never a flip.

Related: #6401, #6814, #6212, #5499, ADR-0049.


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    Triage state repair: + finding (the title already declared it; the label was missing, which left the card invisible to the queue view AND the findings round — the half-labelled state the sweep disjuncts exist to catch).

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


    Generated by Claude Code

  2. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    Findings triage round: held (finding stays) — first formal grading; the earlier comment was a label-state repair.

    Premises re-verified on origin/main @ 3e8e669: mongodb-aggregation.ts:38 still declares groupBy?: string[] (half of the declared union) and mongodb-driver.ts:512 still passes the value through (query as any).groupBy, so a structured GroupByNode still stringifies into a "[object Object]" $group._id. The hold reason is unchanged: #5499 (investment freeze) is still open and standing, and the defect is carried as a measured DEBT row in the conformance matrix (driver-mongodb × AGGREGATION_CASES, per #6401) — the freeze will be lifted against a measured list, which is exactly what this card is a line of.

    Restart conditions: #5499 lifts, or the maintainer green-lights the driver-mongodb AGGREGATION_CASES cell — the closing PR needs the structured-node destructure first (alias alone is unreachable), a server-free suite half per #5517, and the ledger-row deletion in the same PR.

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


    Generated by Claude Code

  3. self-assigned this
    on Aug 11, 2026
  4. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 2 (drivers seat, post-unfreeze batch 1)
    Session: session_013ZU38vNzLXCscgdFZjKd53
    Branch: claude/issue-6850-mongodb-structured-groupby
    Worktree: objectstack-issue-6850
    Domain: domain:drivers
    File surface: packages/drivers/driver-mongodb/src/mongodb-aggregation.ts, packages/drivers/driver-mongodb/src/mongodb-driver.ts (the :512 as any pass-through), a NEW server-free aggregation suite in the same package, scripts/check-driver-conformance.mjs (the driver-mongodb × AGGREGATION_CASES DEBT row), .changeset/* (stop on breach; explain in the report)
    Container & model: M, mode:cloud, model: opus
    Serial constraints cleared: file-disjoint from the #5346/#5376 dispatch riding in the same batch (filter faces vs aggregation faces — no shared file); hot-file sql-driver.ts untouched; no other in-flight claims in the lane.

    Authorization: maintainer unfroze driver-mongodb 2026-08-11 (verbatim on #5499, comment 5249019855). The findings-round grading on this card named its restart condition as "#5499 lifts" — it has. This card rides with the mongodb half of #6814 (same file, same new suite, same conformance cell): PR carries Fixes #6850 + Part of #6814 (the driver-memory half of #6814 stays frozen).

    Premises re-verified by the PM on origin/main @ 76d74ecb4: mongodb-aggregation.ts:38 still groupBy?: string[], :142 still $addToSet: fieldRef ?? null, mongodb-driver.ts:512 still (query as any).groupBy. Per the grading comment: the closing PR needs the structured-node destructure first (alias alone is unreachable), a server-free suite half per #5517, and the ledger-row deletion in the same PR.


    Generated by Claude Code

  5. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    os-dev report — #6850 + the driver-mongodb half of #6814

    Branch claude/issue-6850-mongodb-structured-groupby pushed; draft PR #7550 open (Fixes #6850, Part of #6814 — deliberately NOT Fixes #6814, whose driver-memory half stays frozen).

    Premises: all three held, and the card under-counted

    Re-verified on origin/main @ 76d74ecb4 before the first edit:

    Premise Verdict
    mongodb-aggregation.ts:38 declares groupBy?: string[] held
    :66-69 / :85-88 stringify a structured node held
    :142 lowers count_distinct via $addToSet: fieldRef ?? null held
    mongodb-driver.ts:512 carries (query as any).groupBy held
    check-driver-conformance.mjs DEBT row, spelled as described held (at :446-486 on this tree)

    One premise the cards did not carry: count(col) ignored field entirely and emitted { $sum: 1 } for both spellings, so count(stage) answered 6 where AGGREGATION_CASES says 4 — the number count(*) already has. Found by executing the case-set rather than reading the file, and it is mandatory for the cell: neither issue names it. This is the case-set's own stated purpose ("three different numbers over one column") catching the collapse it was built for.

    What landed

    1. normalizeGroupBy reads the declared union first (the destructure the grading comment required — alias alone was unreachable). $group._id keys on alias ?? field with the FIELD as value; the bare-string emission is byte-identical and pinned.
    2. count_distinct sizing excludes null, in postProcessAggregation.
    3. count(col) counts non-null values via $cond/$ifNull; count(*) unchanged.
    4. dateGranularity → refused, NOT_IMPLEMENTED / 501, ADR-0112 envelope, message mirroring driver-sql/driver-turso sentence for sentence. A malformed entry → INVALID_QUERY / 400. Both refusal tests assert code and status, not toThrow() alone.
    5. mongodb-driver.ts spells the declared type. Probed that this is a real guard: assigning DriverQuery['groupBy'] to string[] now fails tsc against the full union.
    6. New server-free suite mongodb-aggregation-translation.test.ts; the driver-mongodb × AGGREGATION_CASES DEBT row deleted in the same commit.

    Route choices (rejecting the card's own sketch, with reasons)

    • $ne: null before the $addToSet — rejected as unsound, not merely inferior: as a $match it drops the row from the whole pipeline, so a count(*) or sum() sharing that pipeline silently loses the null rows. Correct only for a single-aggregation pipeline, which the builder cannot assume.
    • $size of $setDifference against [null] — sound, and better on wire size; needs a $project stage the builder does not emit without groupBy, i.e. a pipeline-shape change nothing here can execute. Recorded in-code as the right move once the cell has a live half.
    • dateGranularity — measured what the SQL faces lower it to before deciding, per the suggestion. A faithful lowering must emit the engine's bucket LABELS ('2026-01', '2026-Q1', ISO '2026-W03'), publish supports.queryDateGranularity, and be held to date-bucket-parity.test.ts — a card of its own. Refusal chosen; silently ignoring a declared key was never an option.

    Gates (local)

    driver-mongodb tests 266 passed / 143 skipped (opt-in real-mongod halves) · driver-mongodb typecheck clean · spec typecheck clean · check:driver-conformance OK — 37 covered, 3 DEBT, 0 exempt (cell flipped to ok; driver-memory's row stands) · eslint on all changed files clean · build closure clean. CI: in_progress — not polled; the PM owns convergence.

    Honest bound on the coverage this cell now claims

    The suite holds the LOWERING to the table. It does not answer "does MongoDB agree?" — $cond / $ifNull / $addToSet are modelled from documentation, not observed, because this environment cannot fetch a mongod binary (proxy 403), and a suite nobody has executed is a claim rather than a check. That bound is written into the suite header, the AGGREGATION_CASES enrolment list and the ledger note rather than left to be discovered.

    packages/drivers/driver-memory untouched. The dead string_agg arm untouched.


    Generated by Claude Code

  6. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #7550, MERGED at f067930 (record of the step-7 review that preceded the ready-flip).

    • Path surface: 6 files — the four claimed (aggregation source, driver cast, new server-free suite, conformance ledger) + changeset + one UNDECLARED file, packages/spec/src/data/aggregation-conformance.ts, verified comment-only by this seat (DEBT rows struck through, enrolled list updated, freeze paragraph corrected). Cross-seat declaration posted to the spec-surface seat per the UnknownFilterTokenError 漏诊:含非 word 字符的类占位符串({TODAY()}、{current-user-id}、{30 days ago})绕过诊断,原样下发按字面串比较(17.0.0-rc.2) #5586/fix(rest,objectql): the import dry run asks the engine for its verdict instead of predicting it (#4633) #6532 precedent ([PM seat] domain:spec-surface — 🔀 merged into #6017 #6298 comment 5249310176).
    • Beyond-the-cards find: count(col) ignored field and emitted { $sum: 1 } (6 where the standard says 4) — caught by EXECUTING the case-set, named by neither card, fixed as mandatory for the cell. The case-set doing what it was built for.
    • Two PM-sketch rejections reviewed and upheld: $ne:null pre-$addToSet rejected as unsound (a $match drops null rows from the whole shared pipeline); dateGranularity refused loudly (NOT_IMPLEMENTED/501, ADR-0112 envelope) rather than implemented or silently dropped — a faithful $dateTrunc lowering is measured as a card of its own (bucket labels + capability record + date-bucket-parity enrollment).
    • Gate rule honored: suite + DEBT-row deletion in the same PR; coverage bound (lowering-only, no live mongod — proxy blocks the binary) stated in the suite header, enrolment list and ledger note rather than left to be discovered.
    • CI: 26/26 completed; ESLint and TypeScript Type Check both completed: success read individually before the flip.

    Part of #6814 behaved as intended — that card stays open and returns to pm:on-hold for its driver-memory half.


    Generated by Claude Code

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