Repository navigation
fix(spec,objectql): name the aggregated column at having, PostgreSQL only at where - #20577
Conversation
…reSQL only where a driver binds The number-comparand refusal said "a declared number field" at `having`, where the column is an aggregation alias or a groupBy projection with no declared FieldType of its own; it now says "a numeric aggregated column" there. The not-a-number/boolean/date clauses named PostgreSQL's server error at every position; they now do so only at `where`, the one position a driver actually binds — `having` and the per-aggregation `filter` are evaluated by the engine itself, on every driver, before any read. The per-aggregation `filter` genuinely reads a declared field (it narrows raw rows before aggregation), so its "declared ... field" wording is unchanged; only its PostgreSQL clause moves. `NumberComparandRefusalSite` gains two optional fields the engine door already knew: `aggregated` and `boundByDriver`, both defaulting to the old `where` behaviour so an existing site renders byte-for-byte unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
📓 Docs Drift Check14 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a82c9809c57509943abe2bb30a373d7fd67b04bc && git checkout a82c9809c57509943abe2bb30a373d7fd67b04bc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f572a7eb3c272a5be85e3ec64d2516c944c3b243 ec7ba10ea3d81bbb5300209fadce68cd85e7f033 && git checkout -B drift-repro f572a7eb3c272a5be85e3ec64d2516c944c3b243 && git merge --no-ff ec7ba10ea3d81bbb5300209fadce68cd85e7f033
node scripts/docs-audit/affected-docs.mjs --json f572a7eb3c272a5be85e3ec64d2516c944c3b243 |
Contract reviewServed-tier: Read: card #20510 (body, triage grade 5879512564, the seat's scope note 5882219550, the claim 5882482745, the os-dev-report 5883674027); PR #20545's at-tier record 5882199114 (its #20510 paragraph); PR #20577 (body, its one comment, the 6-file list, the net diff against the merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20510
Clause-②: no
What changed
Two false phrases in the number-comparand refusal, at two positions:
havingsaid "a declared number field". The column athaving(
total, an aggregation alias or a groupBy projection) has no declaredFieldTypeof its own — the door hands the verdict a synthesized{ type: 'number' }for whatever the engine classes numeric(
aggregatedRowColumnClasses). It now says "a numeric aggregated column".not-a-number/boolean/dateclauses named PostgreSQL's servererror at every position. Only
where(both spellings) ever reaches alive driver bind;
havingand the per-aggregationfilterare evaluatedby the engine itself, in JS, before any row is read, on every driver
alike — no driver ever sees the comparand there. The clause now appears
only where the driver actually binds it.
Measured, and the card's own claim corrected: the per-aggregation
filter's column is a real declared field, not an aggregated one — thatposition narrows the object's RAW source rows before aggregation runs,
against the object's declared field map (confirmed by both the door's own
header and the existing pin at
aggregations[1].filter.f_number.$gt, whichalready reads
'f_number' compares a declared number field). So only itsPostgreSQL clause moves; its "declared … field" wording is unchanged.
Before / after
where… compares a declared number field against "abc" …, which is not a number: it has no numeric reading, and backends answer it differently (PostgreSQL with a server error).filter… compares a declared number field against "abc" …: it has no numeric reading, and backends answer it differently (PostgreSQL with a server error).… compares a declared number field against "abc" …: it has no numeric reading.having… compares a declared number field against "abc" at having.total.$gt, which is not a number: it has no numeric reading, and backends answer it differently (PostgreSQL with a server error).… compares a numeric aggregated column against "abc" at having.total.$gt, which is not a number: it has no numeric reading.The accept set is unchanged — no refusal added or removed, words only.
Mechanism
NumberComparandRefusalSite(@objectstack/spec) gains two optional fields,both defaulting to the pre-existing
wherebehaviour so an existing site (orone built before this change) renders identically:
aggregated?: boolean— the column is an aggregated-row column, not adeclared field. Only
havingsets it.boundByDriver?: boolean— this position reaches a live driver bind. Onlywheresets ittrue; default istruefor backward compatibility.The engine door (
@objectstack/objectql) passes both at its three callsites — it already knew which position it was building a refusal for; no
second rule, and no driver-level change.
Acceptance notes
{ $field }-adjacent structural filter shape(
where { amount: { "a": 1 } }) was already flagged by PR fix(spec)!: a boolean, a Date or an array compared against a number field is refused like a non-numeric string (#20502) #20545's at-tierreview as a class (a) finding for the filing seat to pick up; this PR does
not touch it.
packages/rest'sdata-number-comparand-door.test.tsasserts no phrase athaving/ the per-aggregationfilter(only status/path), and itswherephrase pin is unaffected — left untouched, per the claimed file surface.
Tests (all green, fresh builds)
@objectstack/specbuild, thenvitest run filter-number-comparand-declared-type.test.ts— 47/47 passed, including new cases for
aggregated/boundByDriverand a byte-for-byte pin that the pre-existing
wheremessage is unchanged;pnpm typecheck— clean (no new errors).@objectstack/objectqlbuild, thenvitest run engine-number-comparand-declared-type-door.test.ts engine-aggregate-having-temporal-door.test.ts— 85/85 passed;pnpm typecheck— clean.@objectstack/restbuild, thenvitest run data-number-comparand-door.test.ts— 4 passed, 8 skipped (PostgreSQL/MySQL cells skipped as before; no code
change was needed there).
check:api-surface,check:dual-source-exports,check:entry-nameability,check:exported-any,check:lean-entry-closure— all green against thefresh build (the widened
NumberComparandRefusalSitetype doesn't move thename-level api-surface snapshot).
dispatch-gates.mjs, 86 commands): all green except 3that need the WHOLE workspace built (
check:doc-formula-expressions,check:dual-build-cjs-loads,check:type-check-debt --re-measure) — outof this diff's local scope, left to CI.
Full command list and exit codes are in the report comment on the issue.
Generated by Claude Code