You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e66da5c
Browse filesBrowse the repository at this point in the historyBrowse files
feat(service-analytics)!: refuse a deriving aggregate over a field type that cannot carry it, for every field type and not only the temporal class (#17559)
* feat(service-analytics): refuse a deriving aggregate over a field type that cannot carry it
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
* test(service-analytics): pin the non-temporal deriving-aggregate refusal and re-aim the boundary
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
* chore(changeset): declare the deriving-aggregate accept-set narrowing
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
* docs(changeset): state the FROM -> TO migration per refused shape
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
* docs(service-analytics): correct the third narrative that stated the gate's scope as temporal
The formula-return-type suite's header explained why `min`/`max` over a `formula`
field is still reachable by quoting the temporal-class early return this card
removes. The conclusion is unchanged — `min`/`max` are outside the gate's reach
either way — but the reason moved from the field class to the aggregate class.
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(service-analytics)!: a dataset measure applying `sum` or `avg` to a field whose declared type cannot carry it is refused at compile time, for every field type and not only the temporal class (#16099)
6
+
7
+
<!-- adr-0087: not-required (already-registered dataset-measure-aggregate-field-type-refused) the hand-migration prescription for this exact narrowing is already registered under protocol major 18 by #16778 — store the quantity as a numeric field and aggregate that — and this change widens which pairs reach it without changing what an affected author must do. ⚠️ That entry's `acceptanceCriteria` is scoped to a `date`/`datetime`/`time` field and says a field of any other class is "neither refused nor certified by this leg", which is no longer true at HEAD; widening that sentence is a `packages/spec` edit this card is fenced out of and is reported to the `domain:spec` seat rather than done here. -->
8
+
9
+
**BREAKING** — an accept-set narrowing on a published authoring surface, continuing the
10
+
one #16778 began. A dataset measure pairing `aggregate: 'sum'` with a `text` field (or
11
+
`avg` with a `select`, `json`, `lookup`, `formula`, … field) used to compile and reach
12
+
the backend; it is now refused by `compileDataset` with `DATASET_INVALID` / **400**
13
+
before any query is built. Shipped as `minor` under the repo's launch-window convention
14
+
for accept-set narrowings.
15
+
16
+
⛔ This changeset adds no rows to any table and restates none. The verdict is
17
+
`AGGREGATE_FIELD_TYPE_COMPATIBILITY`'s — the one table `@objectstack/spec` declared in
18
+
#16353 under the director ruling of decision batch #59 ("both legs, table in spec") —
19
+
read through `isAggregateCompatibleWithFieldType`.
20
+
21
+
## What was wrong
22
+
23
+
#16778 landed the compile leg SCOPED to temporal source fields, leaving "every other
24
+
non-temporal pair the table refuses" as a stated residual that had never been driven.
25
+
Driven on this card, through the real service door:
26
+
27
+
```
28
+
sum × text the table refuses the pair the compile leg does NOT throw SQL IS emitted
29
+
sweep 6 aggregates × 49 field types = 294 pairs; 155 refused by the table;
30
+
minus 6 temporal (#16778's) minus 42 `min`/`max` × the string classes;
31
+
residual 107 — and 107 of 107 were ACCEPTED by the compile leg
32
+
control avg × datetime / date / time → DATASET_INVALID / 400, no SQL emitted
33
+
```
34
+
35
+
The control is what makes that a reading of the tree rather than of a blind harness: the
36
+
same service, door and `sourceFieldMeta` hook sees the pairs #16778 enforces refused.
37
+
38
+
So `sum` over a `text` column reached whichever backend the object is bound to, and the
39
+
answer was a property of the dialect rather than of the data — the shape Prime Directive
40
+
#12 exists to remove, and the same shape #16778 closed for one field class.
41
+
42
+
## What it does now
43
+
44
+
-`compileDataset` judges a measure whose aggregate DERIVES a number (`sum` / `avg`)
45
+
against the table for **every** declared field type, and refuses an unaccepted pair
46
+
with `DATASET_INVALID` / **400** — naming the measure, the field, its declared type
47
+
and the accepted set read off the table. Nothing reaches the driver.
48
+
-`sum` × `percent` is refused at last: the row `analytics-service.ts` has called
49
+
"incoherent" in a comment since before the table existed. `avg` × `percent` is still
50
+
ACCEPTED by the same table, which is what makes it a row and not a class.
51
+
- The refusal's closing prescription is now chosen by the source field's class: the
52
+
temporal sentence #16778 measured is kept verbatim for temporal fields, and a
53
+
non-numeric field is pointed at `count` / `count_distinct`, which accept every type
54
+
because they read no arithmetic off a value.
55
+
- Unchanged: `derived` is covered by construction (a dataset carrying a refused base
56
+
measure never finishes compiling), and the three "cannot answer, do not block" tiers —
57
+
no `sourceFieldMeta`, an unresolvable field, a `relationship.field` path.
58
+
59
+
## ⚠️ Scope: the DERIVING aggregates. `min` / `max` are still not judged here
60
+
61
+
`min` / `max` SELECT one of the stored values; `sum` / `avg` DERIVE a number. This is the
62
+
line this package already draws — `measureResultType` branches on exactly that pair of
63
+
aggregates — and the defect is about a derived number, so the deriving aggregates are its
64
+
population.
65
+
66
+
The `min` / `max` rows stay with **#17513**, and that is measured rather than assumed.
67
+
Enforcing the residual whole was tried on this card: with `min` / `max` × the string
68
+
classes subtracted, **15** cases in `measure-result-type.test.ts` still went red, every
69
+
one of them on `min` × `json` — a pair the table refuses, in no ruling's scope, driven
70
+
end to end by the same shared fixture as the string rows. One dataset compiles every
71
+
measure in that fixture, so one refused pair reds the whole section. ⇒ `min` / `max` is
72
+
one question, and it is the table-amendment card's.
73
+
74
+
## Upgrading — FROM → TO
75
+
76
+
Nothing an author writes is removed or renamed: `DatasetMeasure.aggregate` and
77
+
`DatasetMeasure.field` keep their spellings and their types. What narrows is which PAIRS of
78
+
values are accepted. The one-line fix, per shape:
79
+
80
+
| FROM (compiled before, refused now) | TO |
81
+
|---|---|
82
+
|`{ aggregate: 'sum', field: <a text / select / lookup / user / autonumber field> }`|`{ aggregate: 'count_distinct', field: <the same field> }` — counting reads no arithmetic off the value |
83
+
| `{ aggregate: 'sum' | 'avg', field: <a json / file / location / vector / composite field> }` | store the quantity you meant as its own numeric field and aggregate that |
84
+
|`{ aggregate: 'sum', field: <a formula field> }`| aggregate the formula's numeric INPUT column; a `formula` is virtual in SQL storage, so no arithmetic aggregate can be lowered to it |
85
+
|`{ aggregate: 'sum', field: <a percent field> }`|`{ aggregate: 'avg', field: <the same field> }` — a rate averages, it does not add |
86
+
| `{ aggregate: 'sum' | 'avg', field: <a date / datetime / time field> }` | unchanged from #16778: use `min` / `max` for a real instant, or store a duration as a number and aggregate that |
87
+
88
+
`min` / `max` are **not** affected by this change at all, over any field type.
89
+
90
+
No shipped dataset in this repository declares a newly-refused pair — every one of the
91
+
eleven shipped dataset measures resolves to `number`, `currency`, `summary` or `progress`.
92
+
The refusal names the accepted set for the aggregate, read off the table.
0 commit comments