test(rest): pin the compiled list-view group-header and row-page queries through POST /data/:object/query, on driver-sql and the in-memory tier - #15548
Merged
os-litant merged 2 commits intoSep 4, 2026
Conversation
…ies through POST /data/:object/query The spec half of server-side list-view grouping (#14556) compiles a grouped list view into an aggregate header query and a per-group row page, pinned only against a reducer local to its own test file. This adds the platform half: the COMPILED queries are posted through the existing door and answered by the real chain (RestServer -> protocol.findData -> engine.aggregate -> sqlite SqlDriver), on BOTH faces `ObjectQL.aggregate` forks between -- driver-sql's native GROUP BY and `applyInMemoryAggregation` -- with the fork driven by shadowing `driver.aggregate` over one driver instance and one seeded database, and asserted by call counters rather than assumed. Pins the card's fixture (186 rows, five units 86/61/31/7/1) in both row orders, a two-level groupBy, the `depth: 1` outer level, the empty group keyed null with its `$null` row page, one derived summary (count_filled of a nullable field), the view filter reaching the header numbers, and 86 rows paging as [50, 36] with 86 distinct ids. No route, no wire shape and no behaviour changes: this is a pin. Refs #15330 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…st-view-grouping-platform-pin
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 4, 2026
os-litant
marked this pull request as ready for review
September 4, 2026 21:29
os-litant
enabled auto-merge
September 4, 2026 21:29
os-litant
deleted the
claude/issue-15330-list-view-grouping-platform-pin
branch
September 4, 2026 21:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15330
The platform half of maintainer ruling A on objectui#7189 (2026-09-02, 「7189 A 其他同意」): grouping on a list view is server-side, the group set and every header number are properties of the query, and rows inside a group are paged. The spec half landed with #14556 / PR #15284.
This PR adds one test file and changes no behaviour, no route and no wire shape. The door already existed.
Where the pin lives, and why
packages/rest/src/list-view-grouping-query-door.test.ts.The dispatch offered
packages/rest/test/**orpackages/qa/dogfood/test/**.packages/rest/test/does not exist — this package puts its tests insrc/**/*.test.ts(179 files), andtsconfig.test.jsonis wired to exactly that glob. So the choice was betweenpackages/rest/src/and dogfood, andpackages/rest/src/wins on the stated criterion — an existing suite already boots the door this card needs:rest-data-create-address-unknown-key.test.tsstands upObjectQL+ a sqliteSqlDriver+ObjectStackProtocolImplementation+RestServerand calls the real route handler. This file reuses that harness verbatim. The dogfood parity suites nearby (empty-group-bucket-parity,group-key-read-shape-parity) sit at the DRIVER seam and never cross the REST door, which is the half this card exists to measure.What is pinned
The compiled queries are posted — never a hand-written body, so the pin goes red when the compiler drifts:
compileListViewGroupQuerythroughPOST /api/v1/data/:object/query: five header rows readingcount86/61/31/7/1 in both row orders, the per-group summaries on the same row, a two-levelgroupByanswering nine leaves that fold to the outer counts, thedepth: 1outer level, the empty group carried as a realnullkey (type asserted, not just the value), and one derived summary —count_filledof a nullable field viaderiveColumnSummary, withcount_empty/percent_filled/percent_emptybeside it, all read off ONEcount(notes)node.compileListViewGroupRowsQuerypaging inside one group: 86 rows page as[50, 36]with 86 distinct ids, every row inside the group,totalreporting the group size on every page. The empty group's rows page through the compiled$nullpredicate.ObjectQL.aggregateforks ontypeof drv.aggregate === 'function'between driver-sql's nativeGROUP BYandapplyInMemoryAggregation. The fork is driven by shadowingdriver.aggregatewith an own property for the duration of a tier, over the same driver instance and the same seeded database — nothing else moves. Header sets are then compared canonicalised and type-carrying: folding throughString()is what hid the group-key read-shape divergence, and would make the comparison pass against the defect it exists to catch.Anti-vacuity — every claim has an arm that must come out differently
driver.aggregatecalls anddriver.findcalls receiving an aggregation-bearing AST are tallied, and every tiered case asserts which one moved. A file that silently ran one tier twice would report perfect agreement while measuring one face.groupByand the door answers ONE row counting 186; drop both nodes and it answers ungrouped records with nocountcolumn.codeANDstatus— never a bare throw.Reverse verification, run and recorded
Ablation A (in-file). Neutered the tier fork so both tiers push down. 14 of 33 red, naming
tier in-memory must reach applyInMemoryAggregation via driver.find. Restored; blob hash back to the HEAD blob,git diff HEADempty.Ablation B (cross-package, through
dist/). This suite resolves@objectstack/objectqlthrough itsexports, i.e.dist/, with no vitest alias — so the mutation was rebuilt and proven in the artifact. Dropped the null exclusion fromapplyInMemoryAggregation'scount(field)arm, confirmed 1 injected marker and 0 remaining original hits on disk, rebuilt@objectstack/objectql, andscripts/ablation-dist-preflight.mjsconfirmed the marker live in 4 built files. Result, in the predicted direction: 4 of 33 red — the three cross-tier agreement cases and the in-memory derived-summary case — while the driver-sql arm stayed green. Restored, rebuilt,--absentpreflight confirms the marker gone from all 14 built files and the whole tree clean against HEAD.Face disagreement found — reported, not repaired
On this card's fixture the two faces agree completely: every value and every runtime type, matching the spec reducer.
Outside it, one divergence:
sumover a column that is NULL in every row of a group. driver-sql answersnull; the in-memory tier and the spec reducer answer0.avg/min/maxagree,count_distinctagrees, and a group with only SOME nulls agrees — which is whyaggregation-conformance.tshas never met this cell (its numeric aggregands are non-null by declaration).Per deliverable 3 this was reported on #14556 first (comment 5546049145) and filed as #15546 for triage. It is deliberately not pinned here in either direction — pinning today's divergence would cement it — and no face was repaired in this PR.
Second observation, confirming a known limit rather than a new one: the group SEQUENCE is not a cross-tier contract, and
protocol.findDataslicesrecordsbylimitover that order, so a limited header query selects a tier-dependent subset of groups. The pin asserts the slice ARITHMETIC (totalstays the whole group set,hasMorefollows) and pins neither face's order.PM claims measured
Blocked-by: #14556satisfied; the star barrel hides the symbols, the API-surface manifest answersrest-server.ts:8027protocol.ts:10343/:10351client.data.query()atclient/src/index.ts:5321data-engine.zod.ts:720objectql/src/engine.ts:13323:13493;:13323is insiderejectCredentialAggregation's doc blockcount(field)is the non-null countpackages/rest/test/**or dogfoodpackages/rest/test/does not existVerification
Gate union re-derived from the delivered diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(the tool reads its own change set) and run in full at48db2cfd4a2: 39/39 green, matching the tool's own Reconciliation line — 28 by path, 6 by change kind, 7 declared whole-tree, 2 reached both ways. Two of them first answered exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads,check:type-check-debt), which is NOT MEASURED rather than red; the full workspace closure was built and both then ran green, the latter re-measuring 13 ledger entries with none above its recorded number.Also at
48db2cfd4a2:pnpm --filter @objectstack/rest exec vitest run— 179 files, 3056 tests, all green (the new file is 33 of them).pnpm --filter @objectstack/rest typecheck— green, includingcheck:test-typecheck: this package's test layer is at ZERO debt, so a new test file carrying any tsc error would be red with no entry to widen.0 file(s) / 0 error(s).pnpm lint— repo-wide, green. Run in full rather than narrowed, so no narrowing needs justifying.pnpm check:nul-bytesgreen, plus a direct control-byte scan of the new file.Changeset
None, and
skip-changesetapplied. The rule is the act, not the commit type: a changeset is owed when a published package's public surface widens. This PR adds onesrc/**/*.test.tsfile to@objectstack/rest, whosefilesfield publishesdistonly and whose build config excludes**/*.test.tsfrom the artifact. Nothing is published, nothing widens, no exported symbol and no accepted key or value moves.🤖 Generated with Claude Code
https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Generated by Claude Code