Skip to content

Commit da0ec3c

Browse files
os-justinclaude
andcommitted
feat(spec): list-view grouping — the four *_filled / *_empty summaries derive from one COUNT(field) node; the existing query door; scalar keys; unknown-member refusal
Contract review conditions (seat comment on the card, 2026-09-04): - alias_collision also refuses a grouping field named `count` (checked up front, and the per-summary collision check now precedes the count early-return); pinned. - fold sentence: `count_distinct` does not fold across leaves — listed with `avg`; an outer-level `count_unique` needs the `depth` query; pinned (8 vs 4). - the "no aggregate route" premise corrected: both queries ride the existing `POST /data/:object/query` → `protocol.findData` → `engine.aggregate` door (answering `{ object, records, total, hasMore }`), `client.data.query()` and the RPC `method: 'aggregate'`; the platform half pins that door. - an unknown ColumnSummary value → `summary_unknown` (INVALID_QUERY / 400); `summary_unmapped` (NOT_IMPLEMENTED / 501) kept for a declared member with no counterpart (none today); both pinned. - group keys are scalar-valued: `group_key_not_scalar` (INVALID_QUERY) for an array/object key; per-instant date grouping and unbounded header cardinality recorded in the GroupingConfigSchema JSDoc. - fork (i) ruling implemented: count_filled / count_empty / percent_filled / percent_empty compile to ONE `{ function: 'count', field, alias: 'count_<field>' }` node (deduplicated, never the fieldless count) and `deriveColumnSummary(row, summary, field)` computes them on the header row (percent_filled 0 when count is 0; percent_empty = 1 − percent_filled); describes and the changeset updated; pinned on the fixture's nullable field in both row orders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
1 parent dd2685a commit da0ec3c

4 files changed

Lines changed: 459 additions & 140 deletions

File tree

.changeset/list-view-grouping-server-side-contract.md

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,31 @@ New on the `ui` entry, `view-grouping-query.ts`:
3333
→ the row page; `listViewGroupKeyPredicate` (the empty group is spelled with
3434
the `$null` predicate — the spelling the view filter dialect's `is_empty`
3535
lowers to).
36-
- `COLUMN_SUMMARY_AGGREGATION` — the `ColumnSummary``AggregationFunction`
37-
table, exhaustive by type: `count` → a fieldless `count` (`COUNT(*)`),
36+
- `COLUMN_SUMMARY_AGGREGATION` — the `ColumnSummary`aggregation table,
37+
exhaustive by type: `count` → a fieldless `count` (`COUNT(*)`),
3838
`count_unique``count_distinct`, `sum` / `avg` / `min` / `max` → the same
39-
name, `none` → nothing. `count_empty`, `count_filled`, `percent_empty` and
40-
`percent_filled` have no counterpart yet (`UNMAPPED_COLUMN_SUMMARIES`); a
41-
grouped view declaring one is refused loudly at compile time with
42-
`ListViewGroupQueryError` (`NOT_IMPLEMENTED` / 501, the summary's path) —
43-
their mapping is an open contract question on #14556, and nothing is dropped
44-
silently in the meantime.
39+
name, `none` → nothing; `count_filled` / `count_empty` / `percent_filled` /
40+
`percent_empty` map by derivation — one `{ function: 'count', field }` node
41+
(`COUNT(field)`, the non-null count, header column `count_<field>`), from
42+
which `deriveColumnSummary(row, summary, field)` computes all four on the
43+
header row (`count_filled` = `count_<field>`, `count_empty` = `count −
44+
count_<field>`, `percent_filled` = `count_<field> / count`, 0 when the count
45+
is 0, `percent_empty` = `1 − percent_filled`). Server-side "empty" is `null`
46+
on every face; the footer's client-side reading of `''` / `[]` as empty is
47+
the renderer's to converge. A future member with no counterpart is refused
48+
loudly at compile time (`ListViewGroupQueryError`, `NOT_IMPLEMENTED` / 501,
49+
the summary's path — `UNMAPPED_COLUMN_SUMMARIES`, empty today); a value that
50+
is no member at all is `INVALID_QUERY` / 400.
4551
- Result-column naming on a header row: each grouped field under its own name
46-
(raw stored value, `null` for the empty group), `count`, and each summary
47-
under `<function>_<field>` (`columnSummaryAlias`).
52+
(raw stored value, `null` for the empty group; group keys are scalar), `count`,
53+
and each summary under `<function>_<field>` (`columnSummaryAlias`).
4854

4955
`GroupingConfigSchema` / `GroupingFieldSchema` / `ColumnSummarySchema` now say
50-
this in their docs. Nothing changes in what parses: no key is added, removed
51-
or re-shaped. `minor` because a new exported helper and a declared contract
52-
semantics ship; not breaking — the page-scoped behaviour was never declared.
53-
The route that carries the header query to the grid is the platform half of
54-
#14556; the grid consuming it is objectui#7189.
56+
this in their docs, with the shape's recorded limits (a date grouping field
57+
groups per distinct stored instant; header cardinality is unbounded). Nothing
58+
changes in what parses: no key is added, removed or re-shaped. `minor` because
59+
a new exported helper and a declared contract semantics ship; not breaking —
60+
the page-scoped behaviour was never declared. Both queries ride the existing
61+
`POST /data/:object/query` door (`protocol.findData``engine.aggregate`,
62+
answering `{ object, records, total, hasMore }`); the grid consuming the header
63+
rows is objectui#7189.

0 commit comments

Comments
 (0)