Uh oh!
There was an error while loading. Please reload this page.
fix(driver-turso): escape the groupBy alias on the remote transport instead of gating it (#14235) - #14480
Conversation
…nstead of gating it (#14235) The second output-name position of `RemoteTransport.aggregate`. #14113 moved the aggregation alias to `aliasIdentifierSql` and left this one gated because it carried a landed #6401 pin asserting the refusal; #13714 had already routed both of driver-sql's output-name positions through escaping. An output-column key is a NAME — quoted and escaped; a column reference is grammar — validated. So `field` keeps `assertSafeIdentifier` and `outKey` no longer has it. The #6401 pin is rewritten in place on the same input to assert what the transport now emits, plus dotted/spaced round-trip cases and two executing cases against the SQLite-backed libsql stub. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… alias (#14235) The input the rewritten #6401 pin carries has no aggregation `field`, so the emission is `count(*)`, not `count("stage")`; and the one-statement assertion now counts SELECTs rather than probing for a quote that the escape never produces. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…14235) 4 failed / 14 passed of 18, case for case as predicted; every failure throws inside the call rather than failing a comparison, and the `field`-position control stays green. Notes why no `dist/` leg applies and how the mutation and the restore were each proved on disk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…rso-groupby-alias-escape
…#14287 left (#14235) Both controls were hand-offs addressed to this card in their own comments — #14113's "this control records the state it was left in", #14287's "the day that card lands, this is the test that has to be rewritten deliberately rather than found red by surprise". PM ruling on the stop condition (comment 5506404777) amended this card's file surface to include them. Each file has exactly one case rewritten, on the same input, to assert the quoted emission instead of the refusal; the envelope file also records in its `## The accept set is UNTOUCHED` paragraph that the groupBy OUT KEY position moved. Every other refusal position — object, field, groupBy FIELD, aggregation alias, DDL — still asserts INVALID_REQUEST / 400, the accept-set describe that drives the groupBy FIELD is byte-identical, and `unsafeIdentifierError` / `UNSAFE_IDENTIFIER_CODE` are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
📓 Docs Drift Check1 anchor(s) derived from 1 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 — 6 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 22ea82c33374c3c9ba99719c1df84f2c600b60a3 && git checkout 22ea82c33374c3c9ba99719c1df84f2c600b60a3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e286e248866c40d2db662f69aa0ba6e71b4b096 ffabb9991c5eefbb6020632a829dcc39725debc2 && git checkout -B drift-repro 9e286e248866c40d2db662f69aa0ba6e71b4b096 && git merge --no-ff ffabb9991c5eefbb6020632a829dcc39725debc2
node scripts/docs-audit/affected-docs.mjs --json 9e286e248866c40d2db662f69aa0ba6e71b4b096 |
os-musk
commented
Sep 2, 2026
Landing provenance (engine execution seat,
Generated by Claude Code |
Fixes#14235
RemoteTransport.aggregateemits a caller-supplied output NAME in exactly two positions. #13714 routed BOTH ofdriver-sql's throughSqlDriver.aliasIdentifierSql; #14113 moved this transport's aggregation alias to the same escaping and deliberately left the groupBy alias gated, because that position carried a landed #6401 pin asserting the refusal. This card is the recorded, non-silent reversal of that pin.An output-column key is a NAME — quoted and escaped. A column REFERENCE is grammar — validated. So
fieldkeepsassertSafeIdentifierandoutKeyno longer has it:GroupByNodeSchema.aliasis the same class of key asAggregationNodeSchema.alias; the in-memory face projectsg.alias ?? g.fieldverbatim and the MongoDB and SQL faces agree, so this face was the last one refusing names the contract admits —'Region Name','deal.stage_bucket'— with an opaque 500 out ofmapDataErrorbefore #14287 and a 400 since.outKey === fieldstill emits the alias-less"field", byte-identical.The file surface was amended mid-round, and why
The dispatch bound this card to two files and predicted that no test outside
remote-transport-groupby-node.test.tspinned the groupBy alias refusal. That prediction was wrong, and the round stopped on it rather than widening its own scope: two more tests pinned it, each left behind by a sibling card as a hand-off addressed to this one.remote-transport-aggregation-alias-quoting.test.ts(drivers(turso): RemoteTransport refuses a cube-qualified aggregation alias, so every analytics cube query on a remote datasource is an un-enveloped 500 #14113): "Filed separately rather than patched inline; this control records the state it was left in."remote-transport-unsafe-identifier-envelope.test.ts(drivers(turso):assertSafeIdentifierstill throws a bare Error with no code/status on the field, object, groupBy and DDL positions — an unsafe identifier on the remote transport is still an opaque 500 #14287): "the day that card lands, this is the test that has to be rewritten deliberately rather than found red by surprise."The PM ruled option A on the card and amended the surface to include both. Each file has exactly one case rewritten, on the same input, from a refusal assertion to the quoted emission; everything else in both files is byte-identical.
⛔ What did NOT move:
unsafeIdentifierErrorandUNSAFE_IDENTIFIER_CODEare untouched, and every other refusal position —object, the aggregationfield, the groupBy FIELD, the DDL table / column / index names — still assertsINVALID_REQUEST/ 400. The envelope file'sdescribe('the accept set is UNTOUCHED …')block drives the groupBy FIELD position and is byte-identical; its header paragraph gains one sentence recording that the OUT KEY position moved here. #14287's envelope ruling is not re-decided by this PR — this card decided which POSITIONS the gate governs, not what the gate answers when it fires.Clause-② self-reading:
noNo contract move. Nothing in
packages/specchanges;GroupByNodeSchema.aliasalready declares this key and already admits these names. What widens is the driver's accept set, toward the contract the other three faces already implement — declared = enforced, restored. #14287's landed header says of this position "it MOVES the accept set", which is that same driver-level reading, not a spec one.Evidence — every figure below measured at
ffabb9991Package suite green.
pnpm --filter @objectstack/driver-turso test—Test Files 42 passed (42) · Tests 1146 passed (1146). Both previously-red controls pass on the new truth.Typecheck.
pnpm --filter @objectstack/driver-turso typecheckexit 0, and the tests are really in the program rather than assumed to be:tsc --noEmit --listFilesnames all three edited test files exactly once each.Ablation — direction predicted in the suite header BEFORE the run. Mutation: restore the two pre-change lines at the groupBy select site. Proved on disk by grep counts on both the injected and the removed text plus
git hash-object(mutatede3ee37e6dvs HEAD blob7de7f70ff). Result:Tests 4 failed | 14 passed (18)— the two capture cases and both executing cases fail by THROWING inside the call (unsafe identifier rejected: "bucket"; DROP TABLE deal; --","Region Name","deal.stage_bucket"), never on a comparison, while thefield-position control, the two byte-identical-emission cases, the string-form control, all five date-bucket refusals and both parity cases stay GREEN. Restore proved by the blob hash matching HEAD again plus an emptygit diff HEAD; the script restores under a trap on EXIT INT TERM with an absolute path. Nodist/leg applies, and that is measured: the suite imports the mutated unit through a relative in-package specifier vitest resolves tosrc/, and the package'svitest.config.tsdeclares no alias.Escaped, not merely emitted. The security-relevant claim is pinned against a real SQLite-backed libsql stub, not only on a captured string: an alias of
bucket"; DROP TABLE deal; --comes back as a COLUMN NAME and the table still holds all three rows.Driver conformance ledger, before the first edit and after the last commit — identical:
Gate union. The 36 families
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderives for this changeset, re-derived after the last commit and run atffabb9991: 34 exit 0. The other two exit 3 = NOT MEASURED by their own text, both environmental and neither a finding:check-test-completeness("There is no local log to hand it, so the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix.") andpm/check-half-states("it is no reading at all"). Exit codes captured after redirect, never through a pipe. Pluspnpm lintover the whole repo — exit 0 — andpnpm check:error-status-conformance— exit 0, "every derivable runtime status is documented, and every documented status is reachable."Changeset:
@objectstack/driver-tursopatch. Branch mergedorigin/main(never rebased, never force-pushed).🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code