|
46 | 46 | * as REAL columns beside `number`. `summary` is a roll-up persisted as a |
47 | 47 | * numeric column. |
48 | 48 | * - **temporal** = `CALENDAR_DATE_TYPES` ∪ `INSTANT_TYPES` ∪ `CLOCK_TIME_TYPES`: |
49 | | - * `date`, `datetime`, `time`. `time` is a native TIME column on every SQL |
50 | | - * dialect the driver emits DDL for, and `AnalyticsResult.fields[].type` |
51 | | - * already describes `min` / `max` over it as temporal (#15768), so it sits |
52 | | - * beside the two members the ruling named. |
| 49 | + * `date`, `datetime`, `time`. The ruling named the first two; `time` is the |
| 50 | + * third temporal class and takes the same treatment: its stored form is a |
| 51 | + * dialect question exactly like the other two (native TIME on Postgres and |
| 52 | + * MySQL `TIME(3)`, canonical `HH:MM:SS[.fff]` TEXT on SQLite — #3994), the |
| 53 | + * canonical form orders chronologically on every one of them, and |
| 54 | + * `AnalyticsResult.fields[].type` already describes `min` / `max` over it as |
| 55 | + * temporal (#15768, `TEMPORAL_SOURCE_FIELD_TYPES`). So it sits beside the |
| 56 | + * two members the ruling named. |
53 | 57 | * - **everything else** — the text family, booleans, option types, references, |
54 | 58 | * files, structured JSON, `vector`, and the computed `formula` / |
55 | | - * `autonumber` — is refused for `sum` / `avg` / `min` / `max`. `formula` |
56 | | - * carries a declared `returnType`, but it is VIRTUAL in SQL storage (no |
57 | | - * column is emitted), so no arithmetic aggregate can be lowered to it |
58 | | - * whatever that type says; `autonumber` is a formatted string. Booleans are |
59 | | - * the divergence class exactly: one dialect sums 0/1, another has no |
60 | | - * `sum(boolean)` at all. |
| 59 | + * `autonumber` — is refused for `sum` / `avg` / `min` / `max`, the ruling's |
| 60 | + * "every other pair: refused". `formula` carries a declared `returnType`, |
| 61 | + * but it is VIRTUAL in SQL storage (no column is emitted), so no arithmetic |
| 62 | + * aggregate can be lowered to it whatever that type says; `autonumber` is a |
| 63 | + * formatted string. |
| 64 | + * |
| 65 | + * Two rows the ruling's default covers are recorded here as OVERRIDES of |
| 66 | + * existing opinions, not as settled ground — the row stands as ruled, the |
| 67 | + * text says only what this tree can defend: |
| 68 | + * |
| 69 | + * - **Booleans** (`boolean`, `toggle`) are refused for the four arithmetic / |
| 70 | + * order aggregates by the ruling's default, yet the runtime already ANSWERS |
| 71 | + * them: maintainer ruling #11152 pins that booleans aggregate as numbers on |
| 72 | + * every face with no per-aggregate exception (`AGGREGATION_CASES` in |
| 73 | + * `aggregation-conformance.ts`: `sum(flag)=3`, `avg(flag)=0.5`, |
| 74 | + * `min(flag)=0`, `max(flag)=1`, six backends), and `driver-sql` casts a |
| 75 | + * boolean aggregand to `int` on Postgres to make that hold (#11635). So the |
| 76 | + * refusal is NOT grounded in backend divergence — the backends agree. Whether |
| 77 | + * booleans belong in these rows is a collision between two rulings (batch |
| 78 | + * #59 and #11152) and is referred to the maintainer as its own decision; the |
| 79 | + * row is left exactly as batch #59 stated it until that decision lands. |
| 80 | + * - **The string classes** (`STRING_VALUE_TYPES`, `SINGLE_OPTION_TYPES`, |
| 81 | + * `REFERENCE_VALUE_TYPES`, `autonumber`) are refused for `min` / `max` here, |
| 82 | + * while `service-analytics`' `measureResultType` (#15768, |
| 83 | + * `STRING_SOURCE_FIELD_TYPES`) already types `min` / `max` over them as a |
| 84 | + * supported `'string'` result. The refusal is defensible — the ORDER of |
| 85 | + * strings is collation-dependent, so two backends can return two different |
| 86 | + * "smallest" values — but it overrides that existing opinion, and is |
| 87 | + * recorded as such rather than presented as agreement. |
61 | 88 | * |
62 | 89 | * ## Relation to `isIncoherentAggregate` |
63 | 90 | * |
@@ -126,13 +153,18 @@ export const AGGREGATE_FIELD_TYPE_COMPATIBILITY: Readonly<Record<AggregationFunc |
126 | 153 | * both consumer legs call, so one pair cannot be accepted at authoring and |
127 | 154 | * refused at compile time. |
128 | 155 | * |
129 | | - * Fail-closed on vocabulary: a value outside `AggregationFunction` or outside |
130 | | - * `FieldType` answers `false`. The parameters are typed as `string` because |
131 | | - * the lint leg judges metadata BEFORE it is parsed; that is a convenience of |
132 | | - * the signature, not a tolerance — off-vocabulary input is refused, never |
133 | | - * mapped. |
| 156 | + * Fail-closed on vocabulary AND on shape: a value outside `AggregationFunction` |
| 157 | + * or outside `FieldType` answers `false`, and so does anything that is not a |
| 158 | + * string at all. The second half is load-bearing for a refusal gate: the lint |
| 159 | + * leg judges metadata BEFORE it is parsed, so the value it hands in may be an |
| 160 | + * array or an object, and a property-key lookup alone would coerce |
| 161 | + * `['count']` or `{ toString: () => 'sum' }` to a member spelling and let the |
| 162 | + * pair through. The `string` parameter types are a convenience of the |
| 163 | + * signature, not a tolerance — off-vocabulary or off-shape input is refused, |
| 164 | + * never mapped. |
134 | 165 | */ |
135 | 166 | export function isAggregateCompatibleWithFieldType(aggregate: string, fieldType: string): boolean { |
| 167 | + if (typeof aggregate !== 'string' || typeof fieldType !== 'string') return false; |
136 | 168 | if (!Object.prototype.hasOwnProperty.call(AGGREGATE_FIELD_TYPE_COMPATIBILITY, aggregate)) return false; |
137 | 169 | const row: readonly string[] = AGGREGATE_FIELD_TYPE_COMPATIBILITY[aggregate as AggregationFunction]; |
138 | 170 | return row.includes(fieldType); |
|
0 commit comments