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
fix(objectql)!: one key for the empty group bucket — real null, on both aggregation paths (#3839) (#3848)
`engine.aggregate` has two implementations of one feature and picks between
them per query: it pushes the aggregate down as SQL when the driver advertises
every requested granularity and the reference timezone is UTC, otherwise it
fetches rows and buckets them in JS. The two disagreed about how to spell
"empty" — SQL NULL against the in-memory literal `'(null)'` — so the same
dataset produced a different bucket key type on SQLite+UTC+`month` than on
`week`, a non-UTC timezone, or driver-rest / driver-memory / a remote Turso.
The measures were always right; only the key's type and literal differed, which
is why it went unnoticed. It was never date-specific either — a plain
`groupBy: ['stage']` over a NULL column diverged the same way.
Consumers are written against `null`: they check `== null` and supply their own
empty label. The sentinel defeated every one of them, leaking a raw English
debug string into the UI and compiling a drill on the empty bucket to
`field = '(null)'`, which matches nothing. The comment justifying the string
cited the client `useReportData` hook, removed with ADR-0021 — and the literal
never appeared in it.
- `applyInMemoryAggregation` / `bucketDateValue` key the empty bucket as `null`;
`bucketDateValue` returns `string | null`. Null and unparseable instants still
share one bucket, because SQL cannot tell them apart either.
- The internal composite bucket id is JSON-encoded, so the empty bucket stays
distinct from a row whose value is the literal string `"null"`. Same in
service-analytics' cross-object rebucketing.
- `bucketKeyToCalendarRange` accepts `string | null` — the empty bucket has no
calendar span, so a drill on it opens the unscoped superset as before.
- The driver output contract in spec now states the rule.
Gates: `checkDateBucketParity`'s fixture deliberately had no null instant
because the divergence would have failed it for a reason it was not about; it
has one now. Two fixes made that meaningful — the check folded labels through
`String(value)`, which turns SQL NULL into `'null'` and could compare equal to a
sentinel string, and it compared label sets with `JSON.stringify`, which is
sensitive to key insertion order that the two paths legitimately differ on (a
correct driver could be flagged, with an empty diff message). A new dogfood
check covers the non-date half against real drivers.
Both gates were confirmed to fail with the sentinel restored.
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments