|
| 1 | +--- |
| 2 | +'@objectstack/lint': minor |
| 3 | +--- |
| 4 | + |
| 5 | +Refuse a dataset measure whose `aggregate` the field's declared type cannot carry, at authoring time |
| 6 | + |
| 7 | +A dataset measure pairs an `aggregate` with a `field`, and |
| 8 | +`AGGREGATE_FIELD_TYPE_COMPATIBILITY` (`@objectstack/spec`) declares which of those pairs every |
| 9 | +backend answers the same way. Nothing in the authoring path read that table, so `avg` over a |
| 10 | +`datetime` field validated clean and shipped: one SQL family coerces the column's canonical UTC |
| 11 | +text and returns a plausible number (the average *year*), another has no such function and fails at |
| 12 | +query time — the answer decided by the deployment rather than by the document. The analytics service |
| 13 | +refuses the pair when a query is built (`400 DATASET_INVALID`); this is the same verdict, from the |
| 14 | +same table, at the door the author is standing in front of. |
| 15 | + |
| 16 | +New rule `measure-aggregate-field-type-refused`, gating (`error`), on `os validate` / `os build` / |
| 17 | +`os lint`. It resolves the field's declared type on the object graph lint already indexes — including |
| 18 | +a dotted `relationship.field` path, whose leaf type the compile leg cannot see — and refuses the |
| 19 | +pair when `isAggregateCompatibleWithFieldType` says no. The message names the aggregate, the field, |
| 20 | +its declared type and the accepted set, and the hint names the aggregates that type *does* accept, |
| 21 | +both computed from the table rather than restated. It stays silent wherever the type cannot be |
| 22 | +resolved (an object this stack does not define, a field path that resolves to nothing, an untyped |
| 23 | +field, an aggregate outside the closed `AggregationFunction` vocabulary) rather than guessing. |
| 24 | + |
| 25 | +**BREAKING**: metadata that passed `os validate` / `os build` / `os lint` before can now fail. Every |
| 26 | +pair this refuses is one the analytics service already refuses at query time, so nothing that |
| 27 | +*worked* stops working — but a build that did not fail now does. |
| 28 | + |
| 29 | +Migration, per refused pair — FROM the aggregate the field's type cannot carry, TO one it accepts: |
| 30 | + |
| 31 | +- `avg` / `sum` over a `date` / `datetime` / `time` field → `min` / `max`, which return a real |
| 32 | + instant of the field's own type, or `count` / `count_distinct`. A DURATION is not recoverable from |
| 33 | + an aggregate over instants: store it as a number (a computed "days open" field) and aggregate that. |
| 34 | +- `sum` over a `percent` field → `avg`. A rate does not add; the total routinely exceeds 100%. |
| 35 | +- `min` / `max` over the string, option, reference, file, structured-JSON or `formula` classes → |
| 36 | + `count` / `count_distinct` for "how many distinct values", or a SORT on the record list for "the |
| 37 | + first / last record". String order is collation-dependent, so two backends answer two different |
| 38 | + "smallest" values for one document. |
| 39 | +- Any other refused pair → read the row for your aggregate in |
| 40 | + `AGGREGATE_FIELD_TYPE_COMPATIBILITY`; the refusal message prints it. |
| 41 | + |
| 42 | +A `derived` measure whose `of` names a refused measure is fixed by fixing that measure, not the |
| 43 | +`derived` one. A `date` / `datetime` / `text` field used as a DIMENSION — grouping, bucketing, |
| 44 | +filtering — is untouched: this is about aggregation only. |
| 45 | + |
| 46 | +Clause-②: yes (narrowing) |
| 47 | + |
| 48 | +<!-- adr-0087: not-required (already-registered dataset-measure-aggregate-field-type-refused, dataset-measure-selecting-aggregate-field-type-refused) The two entries register this exact surface — dataset measure `aggregate` x `field` pairs the table refuses — with the prescription above; #16099 likewise declared not-required against the first id when it widened the same leg. This change adds no surface of its own: it is the second consumer of that one table, at the authoring door. --> |
0 commit comments