Skip to content

The draft-preview evaluator answers 0 for an avg over a group with no numeric values — emptyGroupValueFor rules that null, and the live path answers null #16219

Description

@os-warren

Found while working #16203. ⛔ Not that card's defect class (that one is the operand's TYPE) and ⛔ deliberately left unchanged there; the min/max half of this same idiom moved with that card because min/max were its subject.

The divergence, driven

packages/services/service-analytics/src/preview-evaluator.ts, aggregate():

case 'avg': return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;

The : 0 is the whole defect. emptyGroupValueFor (packages/spec/src/data/aggregation-policy.ts) is the platform's own ruling on this question and says the opposite in prose that reads like it was written for this line:

Counting no rows is 0 and summing them is 0: those are measured facts, not missing data. Averaging, minimising or maximising no rows is undefined — there is nothing to average — and must stay null rather than be flattened to a zero that reads as a real measurement.

Measured on one dataset, one row set, two AnalyticsService instances differing only in draftRowsResolver (the live half is NativeSQLStrategy's SQL on a real SQLite through sql.js, seeded from the same rows). The meals group's amount is NULL in every row:

LIVE     [{"category":"meals","avg_amount":null,"sum_amount":0, ...}]
PREVIEW  [{"category":"meals","avg_amount":0,   "sum_amount":0, ...}]

sum agrees at 0 — that is the identity, and fillEmptyGroups writes it deliberately. avg does not: the preview reports a measurement nobody made, and 0 is a plausible-looking average that a reader cannot tell from a real one.

Scope note

The same : 0 idiom stood on the min/max arms until #16203, which replaced it with null on those two and cites the same policy function. avg was left untouched there on purpose: it was outside that card's statement, and moving it is a value change on a published wire surface that deserves its own review rather than a rider.

Dedup

Searched (search_issues, repo-scoped; repo-scoped REST answers 403 for this session). The zero-match is a reading, not a silent zeroing — the same query returned #15546, #11065, #11151 and #6815, four adjacent aggregate-divergence cards known to exist. ⛔ Not a duplicate of #15546: that is sum (where 0 IS the ruled identity) across driver-sql and the objectql in-memory tier — a different aggregate on different faces. The relationship is worth reading together, though: both are "which face decides what an empty group means".

Related

#16203 · #15546 · #4708 (the card fillEmptyGroups and emptyGroupValueFor came from).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions