|
| 1 | +--- |
| 2 | +'@objectstack/service-analytics': patch |
| 3 | +--- |
| 4 | + |
| 5 | +Draft-preview analytics: `avg` answers the mean of the NON-NULL operands, and `null` when there are none — matching every live face |
| 6 | + |
| 7 | +A dataset measure `{ aggregate: 'avg', field: 'amount' }` compiles to the cube |
| 8 | +metric `{ type: 'avg', sql: 'amount' }`, and the draft-preview evaluator built |
| 9 | +its operand list with `rows.map((r) => Number(r[field]))`. `Number(null)` is `0` |
| 10 | +and `Number.isFinite` accepts it, so every NULL entered the average as a zero |
| 11 | +OPERAND and was counted in the divisor. `AVG(col)` is defined over non-null |
| 12 | +values in every SQL dialect, so a drafted chart showed a different number than |
| 13 | +the published one, silently — and where a group's column was NULL in every row |
| 14 | +the number it showed was `0`: a plausible-looking average that a reader cannot |
| 15 | +tell from one somebody measured. |
| 16 | + |
| 17 | +Measured on one dataset, one row set, two `AnalyticsService` instances differing |
| 18 | +only in `draftRowsResolver` (the live half being `NativeSQLStrategy`'s generated |
| 19 | +SQL on a real SQLite). Rows `{meals, null}` and `{meals, null}` answered |
| 20 | +`avg_amount` null live and `0` on preview; rows `{travel, 10}`, `{travel, 20}`, |
| 21 | +`{travel, null}` answered 15 live and 10 on preview. Both cells now answer the |
| 22 | +live number. |
| 23 | + |
| 24 | +The empty answer is READ from the platform's own ruling rather than restated |
| 25 | +here: `emptyGroupValueFor` (`@objectstack/spec/data`) returns the identity `0` |
| 26 | +where counting or summing nothing is a measured fact and `undefined` — spelled |
| 27 | +`null` on this wire — where there is nothing to answer. It is the same function |
| 28 | +`fillEmptyGroups`, `sql-driver` and `driver-turso` read, and the one #16203 cited |
| 29 | +when it moved `min`/`max` off the same idiom in this function. |
| 30 | + |
| 31 | +Unchanged, and pinned by the same differential: `sum` over a group with no values |
| 32 | +still answers the ruled identity `0`, `count` over one still answers `0` |
| 33 | +(#16218), `min`/`max` still answer `null` (#16203), and `avg` over a group that |
| 34 | +has values still answers its mean. `sum` and the numeric `default` arm keep their |
| 35 | +existing operand list — `0` is the additive identity, so the coercion never moved |
| 36 | +`sum`'s answer, and the `default` arm serves the custom-SQL metric types, which |
| 37 | +have no live standard to be moved towards. |
| 38 | + |
| 39 | +The `null` fires on an EMPTY group and never on an incoherent one. "No numeric |
| 40 | +operand" is two different situations: no row carried a value at all — the empty |
| 41 | +group the policy rules on — or rows carried values that do not read as numbers, |
| 42 | +such as a `date` column under `avg`. The second is an incoherent |
| 43 | +aggregate/field-type pair that #16099 owns and no layer refuses yet; it keeps the |
| 44 | +numeric identity it has always had, since the live face answers a different |
| 45 | +number again (SQLite's numeric affinity over a TEXT column) and a `null` there |
| 46 | +would invent a third answer. That boundary is pinned from both sides — by |
| 47 | +`preview-aggregate-operand-type.test.ts` (#16203) and by a control in the new |
| 48 | +differential. |
| 49 | + |
| 50 | +The live path is unchanged. |
| 51 | + |
| 52 | +Bumped `patch` rather than `minor`, on the same reasoning the sibling #16218 |
| 53 | +shipped under: the package's published surface is byte-unchanged — `src/index.ts` |
| 54 | +is not in this diff and does not re-export `preview-evaluator.ts` at all, and |
| 55 | +`aggregate()` is module-private — and the only user-visible effect is a drafted |
| 56 | +chart's number moving to the number the published chart already showed. A value |
| 57 | +correcting toward the live standard is a fix, not the backwards-compatible |
| 58 | +feature addition `minor` denotes. It is a real value change for a consumer |
| 59 | +reading the preview response (`0` becomes blank), which is why the card was filed |
| 60 | +separately rather than ridden along with #16203 — but the `0` it replaces was |
| 61 | +never a number the platform promised. |
0 commit comments