Skip to content

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 into
mainfrom
claude/issue-15330-list-view-grouping-platform-pin
Sep 4, 2026
Merged

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
os-litant merged 2 commits into
mainfrom
claude/issue-15330-list-view-grouping-platform-pin

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

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/** or packages/qa/dogfood/test/**. packages/rest/test/ does not exist — this package puts its tests in src/**/*.test.ts (179 files), and tsconfig.test.json is wired to exactly that glob. So the choice was between packages/rest/src/ and dogfood, and packages/rest/src/ wins on the stated criterion — an existing suite already boots the door this card needs: rest-data-create-address-unknown-key.test.ts stands up ObjectQL + a sqlite SqlDriver + ObjectStackProtocolImplementation + RestServer and 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:

  • Deliverable 1compileListViewGroupQuery through POST /api/v1/data/:object/query: five header rows reading count 86/61/31/7/1 in both row orders, the per-group summaries on the same row, a two-level groupBy answering nine leaves that fold to the outer counts, the depth: 1 outer level, the empty group carried as a real null key (type asserted, not just the value), and one derived summary — count_filled of a nullable field via deriveColumnSummary, with count_empty / percent_filled / percent_empty beside it, all read off ONE count(notes) node.
  • Deliverable 2compileListViewGroupRowsQuery paging inside one group: 86 rows page as [50, 36] with 86 distinct ids, every row inside the group, total reporting the group size on every page. The empty group's rows page through the compiled $null predicate.
  • Both tiers, and they are compared to each other. ObjectQL.aggregate forks on typeof drv.aggregate === 'function' between driver-sql's native GROUP BY and applyInMemoryAggregation. The fork is driven by shadowing driver.aggregate with 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 through String() 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

  • The fork is counted, not assumed: driver.aggregate calls and driver.find calls 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.
  • §2 replays the DEFECT the ruling outlawed on this same door — grouping the first page gives 86/14 contiguous and 31/31/30/7/1 interleaved. Neither is the data.
  • §3 ablates the compiled body in place: drop groupBy and the door answers ONE row counting 186; drop both nodes and it answers ungrouped records with no count column.
  • §7 moves every header number with the view filter (86/61/31/7/1 becomes 28/20/10/2, one group gone entirely).
  • §8 pages the group OVERLAPPING as its own control: two pages summing to 100 rows carry only 80 distinct ids, so distinctness is shown to discriminate from the counts beside it.
  • §9 posts bodies the door must refuse, asserting the ADR-0112 pair — code AND status — 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 HEAD empty.

Ablation B (cross-package, through dist/). This suite resolves @objectstack/objectql through its exports, i.e. dist/, with no vitest alias — so the mutation was rebuilt and proven in the artifact. Dropped the null exclusion from applyInMemoryAggregation's count(field) arm, confirmed 1 injected marker and 0 remaining original hits on disk, rebuilt @objectstack/objectql, and scripts/ablation-dist-preflight.mjs confirmed 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, --absent preflight 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: sum over a column that is NULL in every row of a group. driver-sql answers null; the in-memory tier and the spec reducer answer 0. avg / min / max agree, count_distinct agrees, and a group with only SOME nulls agrees — which is why aggregation-conformance.ts has 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.findData slices records by limit over that order, so a limited header query selects a tier-dependent subset of groups. The pin asserts the slice ARITHMETIC (total stays the whole group set, hasMore follows) and pins neither face's order.

PM claims measured

claim verdict
Blocked-by: #14556 satisfied; the star barrel hides the symbols, the API-surface manifest answers holds
REST door drifted to rest-server.ts:8027 holds — located by text
Protocol drifted to protocol.ts:10343 / :10351 holds — located by text
client.data.query() at client/src/index.ts:5321 holds exactly — that anchor did not drift
RPC face at data-engine.zod.ts:720 holds exactly
Engine dispatch at objectql/src/engine.ts:13323 FALSIFIED — the fork is at :13493; :13323 is inside rejectCredentialAggregation's doc block
Fixture: 186 rows, 86/61/31/7/1, count(field) is the non-null count holds — re-derived from the generator, not retyped
Gate union of 43 commands FALSIFIED for this diff — re-derived at this tree: 39 families, all run
Route the pin to packages/rest/test/** or dogfood partly falsifiedpackages/rest/test/ does not exist

Verification

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 at 48db2cfd4a2: 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 run179 files, 3056 tests, all green (the new file is 33 of them).
  • pnpm --filter @objectstack/rest typecheck — green, including check: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-bytes green, plus a direct control-byte scan of the new file.

Changeset

None, and skip-changeset applied. The rule is the act, not the commit type: a changeset is owed when a published package's public surface widens. This PR adds one src/**/*.test.ts file to @objectstack/rest, whose files field publishes dist only and whose build config excludes **/*.test.ts from 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

…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
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 85a245988ec754caca94aefa06ab17db32d5bb1dpackageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 4, 2026
@os-litant os-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l tests labels Sep 4, 2026 — with Claude
@os-litant
os-litant marked this pull request as ready for review September 4, 2026 21:29
@os-litant
os-litant enabled auto-merge September 4, 2026 21:29
@os-litant
os-litant added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 66086d0 Sep 4, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-15330-list-view-grouping-platform-pin branch September 4, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants