Skip to content

Commit 934b70a

Browse files
committed
chore(changeset): the analytics row wildcard is count-only (breaking minor, narrowing)
Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
1 parent b74aada commit 934b70a

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: the analytics row wildcard `'*'` is admitted only where a `count` consumes it — a cube or dataset measure over `'*'` under any other aggregate, and a cube dimension over `'*'`, are refused at parse (#21409)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner, the `(narrowing)` arm above and the ADR-0087 disposition below,
12+
never by the level).
13+
14+
`'*'` is the row wildcard: what a `count` aggregates (`COUNT(*)`), reading no
15+
field value. It is now admitted in exactly one place, a measure that counts:
16+
17+
- `MetricSchema.sql` — a cube measure's `sql` — admits `'*'` under
18+
`type: 'count'` only; under any other `type` it is refused at `sql`
19+
(code `custom`).
20+
- `DatasetMeasureSchema.field` — an ADR-0021 dataset measure's `field` — admits
21+
`'*'` under `aggregate: 'count'` only; under any other aggregate, or on a
22+
measure with no aggregate (a `derived` one), it is refused at `field`
23+
(code `custom`). A count may still omit `field`.
24+
- `DimensionSchema.sql` — a cube dimension's `sql` — never admits `'*'`
25+
(code `invalid_format`): it takes the column path without the wildcard arm,
26+
the pattern a dataset dimension's `field` already takes.
27+
28+
Each refusal names the slot and the aggregate the author wrote, and prescribes
29+
the two ways out: a `count`, or a column. A column or a relationship path parses
30+
byte-identically to before on every slot, and so does a `count` over `'*'`.
31+
32+
Why: no aggregate but `count` has a column to read over `'*'`, and a dimension
33+
has no aggregate at all, yet the contract admitted the wildcard on any measure
34+
and on a cube dimension, and the analytics strategies sent it to the database as
35+
written. Measured at `POST /api/v1/analytics/dataset/query` over a real SQLite
36+
driver, on the native-SQL and the ObjectQL strategy alike: a dataset measure
37+
aggregating `'*'` under `sum`, `avg`, `min`, `max` or `count_distinct` answered
38+
`500 DATABASE_ERROR`. A dataset measure compiles to the cube measure it names
39+
verbatim, so the same reading covers an authored cube measure. Such a member
40+
never produced an answer, so no working document changes meaning: the failure
41+
moves from the query to the authoring parse. The two measure slots ask ONE
42+
shared predicate; the rule is cross-field (the slot and its aggregate), so it is
43+
a refinement, which the published JSON Schema cannot carry — both sites are
44+
declared in `dropped-refinements.baseline.json`. The dimension half is a
45+
`pattern`, so `json-schema/**` states it.
46+
47+
## FROM → TO
48+
49+
```
50+
FROM defineDataset({ name: 'deal_metrics', label: 'Deal Metrics', object: 'deal',
51+
dimensions: [{ name: 'stage', field: 'stage' }],
52+
measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] })
53+
-> parsed; every query selecting `deals` answered 500 DATABASE_ERROR
54+
TO -> ZodError at measures.0.field (custom):
55+
`measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. …
56+
57+
measures: [{ name: 'deals', aggregate: 'count' }] // a row count
58+
measures: [{ name: 'deal_value', aggregate: 'sum', field: 'amount' }] // an aggregate of a column
59+
60+
FROM defineCube({ name: 'deals', sql: 'deal',
61+
measures: { total: { label: 'Total', type: 'sum', sql: '*' } },
62+
dimensions: { everything: { label: 'All', type: 'string', sql: '*' } } })
63+
TO -> refused at measures.total.sql (custom) and dimensions.everything.sql (invalid_format)
64+
65+
measures: { total: { label: 'Total', type: 'sum', sql: 'amount' } },
66+
dimensions: { stage: { label: 'Stage', type: 'string', sql: 'stage' } }
67+
```
68+
69+
**The one-line fix:** parse each cube and dataset; every refusal at `…sql` /
70+
`…field` naming `'*'` is one member to change — declare a `count` to count rows,
71+
or name the column the measure aggregates (a dimension names the column it
72+
groups by). On a `derived` dataset measure, delete `field`: nothing read it.
73+
There is no mechanical rewrite, so `os migrate meta` lists nothing for it.
74+
75+
**What a stored document meets.** A metadata read still serves it as stored,
76+
with the refusal on its read diagnostics (`_diagnostics`), and a re-save through
77+
the metadata write door is refused at the slot. `POST
78+
/api/v1/analytics/dataset/query` parses every dataset it is handed, inline or
79+
saved, so a stored dataset carrying such a measure answers `400
80+
VALIDATION_FAILED` at `measures.N.field` on every query — including a query that
81+
selects only its other measures, which used to answer — until the member is
82+
fixed: it fails closed. An authored cube reaches the analytics runtime through
83+
the stack definition, whose parse refuses it.
84+
85+
## The kit
86+
87+
- **Schema.** `data/analytics-column-reference.ts` (not published API) declares
88+
the predicate `rowWildcardOutsideCount` and its refusal once; `MetricSchema`
89+
and `DatasetMeasureSchema` call both from a refinement, and
90+
`DimensionSchema.sql` takes `ANALYTICS_COLUMN_PATH`. No export, key or enum
91+
member changes, so the api-surface, authorable-surface and JSON-schema
92+
manifest ratchets are unchanged.
93+
- **ADR-0087.** D3 entry `analytics-row-wildcard-outside-count-refused`. No D2
94+
conversion: rewriting to `count` would change the figure the author asked for,
95+
and only the author can name the column. No `RETIRED_KEYS_BY_MAJOR` row.
96+
- **Dropped refinements.** `data/Metric` and `ui/DatasetMeasure` gain their root
97+
site, and every published schema embedding them gains the embedded site.
98+
- **Liveness.** `analytics_cube` `measures.sql` / `dimensions.sql` and `dataset`
99+
`measures.field` stay `live`, re-verified, their notes re-pointed here.
100+
- **Docs.** The `ui/dataset` reference page is regenerated.
101+
- **Runtime.** Unchanged.
102+
103+
## Reach, measured
104+
105+
- This repository: no example, platform object, doc, skill, script or test
106+
fixture authors `'*'` outside a `count` at the three slots (`git grep` of every
107+
`field` / `sql` value spelled `'*'`, 173 hits, each read in its enclosing
108+
object: 154 under a `count`, the rest QueryAST aggregations, comments and
109+
strategy-level literals). One spec pin admitted `'*'` on a cube dimension; it
110+
now pins the refusal.
111+
- objectui at the pinned `.objectui-sha`: zero `field` / `sql` values spelled
112+
`'*'` (lit controls: 51 `aggregate: 'sum'`, 438 `field: 'amount'`).
113+
- Out-of-repo authored metadata: NOT MEASURED.
114+
115+
<!-- adr-0087: registered analytics-row-wildcard-outside-count-refused -->

0 commit comments

Comments
 (0)