Skip to content

driver-mongodb: native dateGranularity lowering ($dateTrunc → engine bucket labels) + publish supports.queryDateGranularity #7580

Closed

Description

@huangyiirene

Provenance (maintainer direct-route, 2026-08-11)

Filed and queued on the maintainer's instruction in the drivers PM session chat (session session_013ZU38vNzLXCscgdFZjKd53), verbatim:

mongodb dateGranularity 立卡排进队列

Routing label applied under the 2026-08-10 direct-route standing authorization (maintainer-commanded cards route directly; natural-inflow triage remains the triage seat's). driver-mongodb is unfrozen per #5499 comment 5249019855.

Background — what PR #7550 measured and deliberately did not do

PR #7550 (Fixes #6850 / Part of #6814) gave driver-mongodb a real structured-GroupByNode lowering, and chose to refuse a dateGranularity-bearing node (NOT_IMPLEMENTED / 501, ADR-0112 envelope) rather than implement or silently drop it. Its PR body records the measurement that sized the native lowering as a card of its own — this card.

The standard already exists — this card joins it, it does not invent it

  • Capability record: supports.queryDateGranularity is a per-granularity record ({ day, week, month, quarter, year }). packages/objectql/src/engine.ts (~:9295-9301, drifts — locate by content) pushes a bucketed groupBy down only for granularities the driver advertises; otherwise the engine buckets in memory (in-memory-aggregation.ts). So today mongodb aggregations still bucket correctly — one layer up, without index/server-side benefit.
  • Precedent + parity suites: driver-sql publishes the capability and is held to sql-driver-date-bucket.test.ts (parity), sql-driver-date-bucket-storage.test.ts (storage forms), sql-driver-aggregate-datetime-window.test.ts / -temporal-output.test.ts. The engine's timezone semantics are pinned in engine-aggregate-timezone.test.ts.
  • Labels are the contract: buckets must come back as the ENGINE's label spellings (e.g. 2026-01, quarter and ISO-week forms) — byte-parity with what the in-memory fallback emits, or the two paths answer the same query differently depending on a capability bit.

Scope

  1. Lower a dateGranularity-bearing groupBy node natively ($dateTrunc or equivalent expression), emitting the engine's bucket labels.
  2. Storage forms per ADR-0053: mongodb stores datetime as BSON Date but date/time as text — measure which granularity × field-kind cells a native lowering can honestly serve, and only advertise those.
  3. Publish supports.queryDateGranularity for exactly the cells implemented — declared = enforced; an advertised granularity the lowering mishandles is worse than the current honest refusal.
  4. Keep the fix(driver-mongodb): take a structured GroupByNode, and answer count/count_distinct like every other backend #7550 refusal as local defense for direct callers with unadvertised granularities (the engine will not push those down once the capability record is honest).
  5. Parity coverage in the shape of the driver-sql suites, server-free per driver-mongodb 测试:两个套件并发下载 MongoDB 二进制,mongodb-memory-server 的 rename 竞态让全绿测试 exit 1(队列 flaky) #5517 (the in-process evaluator from mongodb-aggregation-translation.test.ts — note its stated bound: it holds the LOWERING to the table; live-mongod agreement stays out of reach in this fleet, and any modelled $dateTrunc semantics must carry the same written bound).

Acceptance

  • For every advertised granularity × field-kind cell: pushdown result labels are byte-identical to the engine's in-memory fallback on the same rows (parity cases, both spellings of the node).
  • Unadvertised cells keep refusing loudly at the builder (existing tests stay green or are consciously updated).
  • Timezone semantics match engine-aggregate-timezone.test.ts's pinned behavior — measure before implementing; if a faithful timezone treatment is not achievable server-free, narrow the advertised set and record why rather than approximating.

Refs: PR #7550 (measurement + refusal), #6850 / #6814 (the cell this grew from), #5499 (unfreeze), aggregation-conformance.ts:51 (capability note), ADR-0053 (storage forms), ADR-0112 (refusal envelope).

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 11, 2026
  2. self-assigned this
    on Aug 11, 2026
  3. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    CollaboratorAuthor

    Claim: PM loop round 4 (drivers seat)
    Session: session_013ZU38vNzLXCscgdFZjKd53
    Branch: claude/issue-7580-mongodb-date-granularity
    Worktree: objectstack-issue-7580
    Domain: domain:drivers
    File surface: packages/drivers/driver-mongodb/src/mongodb-aggregation.ts (the refuseDateBucketedGroupBy region + a granularity lowering), packages/drivers/driver-mongodb/src/mongodb-driver.ts (the supports record), packages/drivers/driver-mongodb/src/mongodb-aggregation-translation.test.ts and/or a new parity suite in the same package, .changeset/* (stop on breach; explain in the report)
    Container & model: M, mode:cloud, model: opus
    Serial constraints cleared: in-flight #7177 (PR #7579) edits findone/filter/temporal TEST files — disjoint from this card's aggregation surface. #7550 (same aggregation files) MERGED at f067930; this card builds on its post-merge state. Hot-file sql-driver.ts untouched (driver-sql suites are read as precedent only — ⛔ not edited).

    Authorization: maintainer direct-route instruction 2026-08-11 (verbatim in the card body's Provenance section); driver-mongodb unfrozen per #5499 comment 5249019855.


    Generated by Claude Code

  4. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    CollaboratorAuthor

    os-dev report — #7580 · draft PR #7590 · branch claude/issue-7580-mongodb-date-granularity · worktree objectstack-issue-7580 · base origin/main @ 34c01a5

    Premises, verified against origin/main before implementing

    All five PM anchors held; none was dead.

    Anchor Verdict
    Pushdown gate at engine.ts:9295-9301 HOLDS. granularityCaps?.[g.dateGranularity] === true gates drv.aggregate; otherwise applyInMemoryAggregation. Current mongodb behaviour is correct one layer up — this card moves work, it does not fix an answer.
    Labels are the engine's (in-memory-aggregation.ts#bucketDateValue) HOLDS. '2024' / '2024-Q1' / '2024-01' / '2024-01-15' / '2025-W01'; null, missing and unparseable share one bucket (#3839).
    ADR-0053 storage forms split HOLDS. datetime→BSON Date, date→YYYY-MM-DD TEXT, time→HH:MM:SS TEXT (mongodb-temporal.ts canon table).
    #7550 refusal at mongodb-aggregation.ts:62, pins at …translation.test.ts:378-417 HOLDS. Both found and consciously rewritten.
    Timezone semantics (engine-aggregate-timezone.test.ts) HOLDS, and dissolves the risk the card flagged. tzRequiresInMemory forces in-memory for any non-UTC zone + date bucket, and the driver AST carries no timezone key at all. UTC-only is not a narrowing — it is the only thing the lowering could mean. No advertised set needed narrowing on timezone grounds.

    Two premises measured directly (both decided a design choice):

    • new Date('14:30:00') → Invalid Date, so a Field.time column buckets to the empty bucket on the engine path. The lowering matches it via onError: null → the time cell is served faithfully-degenerately and needs no refusal (a refusal would have broken a query the engine answers today).
    • String(999) = '999' vs %Y = '0999' — a shared, pre-existing property of every SQL face too. Recorded, not narrowed over.

    What was implemented

    Native lowering via $dateToString, not $dateTrunc (route declined with reasons: label-not-instant contract, MongoDB 3.6 floor instead of 5.0, and fewest unobserved operators — the governing constraint under #5517). $convert … onError/onNull: null makes the read total across all three ADR-0053 storage forms + null/missing/junk. quarter derived by $switch over the zero-padded %m string so no numeric→string formatting is assumed; null propagation falls out of $concat, so no $cond guard.

    Advertised: all five (day, week, month, quarter, year) — week: true where driver-sql/SQLite is false, because %G/%V give MongoDB both halves of the ISO-8601 week date. Declared = enforced is a mechanism: the capability record and the builder's refusal read one exported constant, and the suite asserts identity, not equality.

    Gates

    Gate Status
    pnpm --filter @objectstack/driver-mongodb test ✅ 341 passed, 143 skipped (was 266)
    pnpm --filter @objectstack/driver-mongodb typecheck ✅ clean
    pnpm check:driver-conformance ✅ OK — 37 covered, 3 DEBT, 0 exempt (unchanged)
    eslint --no-inline-config on all changed files ✅ clean
    build closure --filter @objectstack/driver-mongodb... ✅ clean (now incl. objectql)
    CI on #7590 in_progress — PM owns convergence

    @objectstack/objectql tests not run: the objectql fallback's shared cases were not touched (read-only).

    Reverse-verification — every break executed, each had to fail

    Injected defect Result
    %G-W%V → %Y-W%V 4 failed (both week cells + both year-boundary pins)
    $convert removed 14 failed (every cell, all three storage forms)
    quarter boundary off by one month 3 failed (both quarter cells + Q3 pin)
    capability record as a second, value-equal literal 1 failed (identity assertion)

    Declared file-surface breach — one, reported as required

    The claim named the surface as driver-mongodb source + .changeset/*. Two additions go past it, both inside the driver package:

    1. packages/drivers/driver-mongodb/package.json — @objectstack/objectql added as a devDependency. Measured: objectql depends on no driver, so the edge is acyclic, and a devDependency never enters the published package. It buys the acceptance criterion literally — parity against the real applyInMemoryAggregation rather than the hand-copied bucketDateValue the three SQL bucket suites carry and whose drift their own ⚠️ Keep in sync comments admit they cannot detect. turbo's test → ^build already orders it. Revertable in isolation to the hand-copy precedent if the PM prefers the thinner dependency graph.
    2. mongodb-pipeline-evaluator.testkit.ts — fix(driver-mongodb): take a structured GroupByNode, and answer count/count_distinct like every other backend #7550's evaluator extracted verbatim (only date operators added) so both suites share one strict reader; a second, laxer copy is the easiest way to bless a broken lowering.

    packages/objectql, driver-memory, driver-sql, packages/verify, packages/qa/dogfood and content/docs/releases/ untouched.

    Open questions for the PM

    1. checkDateBucketParity enrolment stays out of reach. It drives a LIVE driver through create/find/aggregate, so driver-mongodb cannot join packages/qa/dogfood/test/date-bucket-parity-conformance.test.ts server-free. A follow-up card could add a server-free adapter there, or enrol mongodb the day the opt-in real-mongod half becomes runnable. Not filed — PM's call whether it is a card.
    2. The whole parity claim is documentation-derived (driver-mongodb 测试:两个套件并发下载 MongoDB 二进制,mongodb-memory-server 的 rename 竞态让全绿测试 exit 1(队列 flaky) #5517, proxy 403 on the mongod binary). The bound is written in three places (suite header, MongoDBDriver.supports doc, beside the lowering), exactly as fix(driver-mongodb): take a structured GroupByNode, and answer count/count_distinct like every other backend #7550 did. The one thing a live mongod would settle: whether $convert's string→date arm accepts precisely the ISO-8601 set modelled here.
    3. Residual divergence, asserted as a live difference rather than hidden: legacy non-canonical text in a temporal column ('2024-01-15 10:00:00' with a space) buckets in memory and empties in the pushdown. Not a form this driver writes (coerceTemporalValue canonicalises), and the identical exposure exists on every SQL face. Advertised anyway — flagging in case the PM reads that as a cell rather than a data condition.
    4. No engine-side defect found, so nothing was filed against packages/objectql.

    Generated by Claude Code

  5. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    CollaboratorAuthor

    ACCEPT — PR #7590 (review of record; flipping ready + queueing now).

    • Path surface: 9 files, all inside packages/drivers/driver-mongodb + changeset + the 3-line lockfile companion. The one declared breach — @objectstack/objectql as a devDependency — is ACCEPTED: acyclic (measured), dev-only (never enters the published package), and it buys the acceptance criterion literally — parity against the REAL applyInMemoryAggregation instead of a hand-copy whose own comments admit drift is undetectable. Validate Package Dependencies gate agrees. The shared-evaluator testkit extraction is likewise accepted (one strict reader beats two drifting copies).
    • Route choice reviewed and upheld: $dateToString over the PM-suggested $dateTrunc, for the governing reason under driver-mongodb 测试:两个套件并发下载 MongoDB 二进制,mongodb-memory-server 的 rename 竞态让全绿测试 exit 1(队列 flaky) #5517 — fewest unobserved operators — plus the label-not-instant contract and the lower server floor. The dispatch prompt's route was optional by construction; this is the third time this batch a dev's measured route beat the PM's sketch.
    • Declared = enforced held as a MECHANISM: capability record and builder refusal read one exported constant, suite asserts identity. All five granularities advertised, week: true justified by %G/%V (ISO both halves) where the SQL faces lack it. Timezone risk dissolved by measurement (engine forces in-memory for non-UTC; UTC-only is the lowering's only possible meaning).
    • Reverse-verification: four injected defects, each failed the suite (week format, $convert removal, quarter boundary, duplicate-literal capability record). This is what makes a documentation-derived parity claim worth something.
    • Open-question dispositions (PM's calls, none maintainer-level): (1) dogfood checkDateBucketParity enrollment needs a server-free adapter or a runnable mongod — recorded on the seat card as a candidate follow-up for triage, not filed today; (2) the documentation-derived bound is stated in three places — accepted as the honest ceiling of this fleet; (3) the legacy non-canonical-text divergence is a data condition shared by every SQL face, asserted as a live difference in the suite — accepted as recorded, not a cell.
    • CI: 26/26 completed, ESLint and TypeScript Type Check completed: success read individually.

    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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions