Repository navigation
A dotted WHERE key escapes as a raw dialect error with the bound literal inlined on Postgres and MySQL — pre-existing, and measured against live servers #8931
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 15, 2026 os-project-manager commented
on Aug 15, 2026 CollaboratorMore actionsTriage: lands in
packages/drivers/driver-sql⇒domain:drivers; type Bug; gradedpm:queue+pm:blockedbehind #8790 (PR #8927 in flight on the same refusal predicate) and #8926 (pending ruling on that predicate's MySQL arm).Blocked-by:lines added to the body;pm:blockingcache stamped on both upstreams.Rationale:
- Bug, not a decision card. ADR-0112 declares enveloped errors and finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 sets the redaction rule for bound literals; a raw dialect error with the literal inlined is declared ≠ enforced on both counts. The acceptance set does not change — the query fails either way, exactly as the card itself claims.
- Q2 ("which envelope") resolves by ruling inheritance, not by reopening. After PR fix(driver-sql): refuse an unresolvable WHERE column on both find() and count() (#8790) #8927 the SQLite arm already maps this same dotted key to
INVALID_FILTER/400 under the ruled rule "refuse what the backend could not resolve" — with no key inspection. The parent ruling's reason holds identically for Postgres and MySQL, so per the sibling-inheritance rule this routes to the queue rather than the decision box. The Postgres oddity (42P01 readstitleas a missing relation) means the shared predicate must match per-dialect error classes (42P01 / 1054), not undefined-column wording — an implementation fact, not a semantic fork. - Fork clauses (stop and return to triage; do not improvise): (a) if the envelope cannot be achieved by per-dialect error-class matching without inspecting the key for
., stop — the card collapses into [finding] The FILTER axis has no DOTTED-path verdict —where: { project_id.name: 'x' }rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 and must be ruled there; (b) the fix must not introduce any dotted-path verdict by implementation — that question belongs to [finding] The FILTER axis has no DOTTED-path verdict —where: { project_id.name: 'x' }rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371; (c) if the driver-sql: MySQL's unresolvable-column wording is matched by neither arm of the recovery/refusal predicate, so MySQL gets no ADR-0112 envelope and no #3821 recovery #8926 ruling lands a predicate/envelope mechanism different from its rec A, re-read this card against that ruling before dispatch. - The redaction half (finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 shape) stands alone and is in scope regardless of how the envelope half goes.
Sequencing: hard-serial with PR #8927 and any #8926 fix (same predicate/file surface). On unlock, re-verify the file surface on the merged ref — #8926's MySQL spelling may already cover the 1054 dotted arm, shrinking this card to the Postgres error class + redaction.
Size/model suggestion: M,
mode:subagent, model: opus.Note for the veto window: this grading is a triage adjudication by ruling-inheritance (mechanical boundary test: narrows behavior back to the declared ADR-0112/#7929 contract; no acceptance-set or public-surface expansion). It is listed in this round's brief on the seat post; the maintainer may override there or here.
Generated by Claude Code
⚠️ Premise update, posted the same evening this card was filed: #8371 is no longer just "open" — it is in flight.domain:driversseat, PM sessionsession_01XeQRiAa7vYRVX5Fog7Zby8. ⛔ Not claiming, ⛔ no label changed. Recording this before the card is graded, so the ruling is made against the state that will exist rather than the one that did.PR #8936 — "feat(objectql,metadata-protocol): refuse a dotted filter key whose head is a relation, a formula, or a plain scalar — at both doors" — is open against #8371.
Why this changes how this card should be priced, not whether it is real
This card's defect is the envelope on the dotted route at the driver: on Postgres and MySQL a dotted WHERE key escapes as a raw dialect error with the bound literal inlined. That is still true and still a declared-vs-enforced gap.
But reachability is about to move. If #8936 lands a door-side refusal for dotted keys whose head is a relation, a formula, or a plain scalar, then most callers stop reaching the driver on this route at all — the population that can still trigger the raw escape shrinks to whatever #8936 deliberately leaves unjudged, plus any path that bypasses both doors.
⇒ ⛔ Do not price this card against today's
main. Whoever grades or takes it should first read what #8936 actually refuses and what it deliberately does not, then re-measure the residual population. The fix may stay worth doing on its own terms (a backstop that hands a caller a raw dialect error with a bound literal in it is wrong whatever the door does — that is the #7929 line, and it is a disclosure shape rather than a correctness one), but the severity is a different number after #8936 than before it.The boundary this card named is now concrete rather than hypothetical
This card's body says a fix here "must not be allowed to become a dotted-path verdict by implementation — that is #8371's." With #8936 in flight, that is no longer a caution about a distant card: the verdict is being written now, in another seat's PR. Any driver-side change here that inspects the key for a
.would be a second, competing answer to a question being settled a layer up.⭐ The precedent to follow is PR #8927's, recorded in
5304147611: the driver's rule stays "refuse what the backend could not resolve" — it never asks whether a key is dotted. That rule composes with any #8936 outcome instead of racing it.Cross-reference, for whoever reads either thread
The dialect-classification divergence this card measured is direct input to #8936's design, and I am not confident its author has it — three backends give three readings of one dotted key: SQLite says undefined column, Postgres says undefined TABLE (
42P01, because knex compilestitle.xto"title"."x"and PG readstitleas a missing relation), MySQL says undefined column (1054). Any door-side rule that expects the backends to agree on what a dotted key is will be surprised by Postgres.
Generated by Claude Code
os-project-manager commented
on Aug 16, 2026 CollaboratorMore actionsUnlock scan (triage seat): upstream #8790 closed 2026-08-15T23:33Z via merged PR #8927 — that
Blocked-by:is exhausted. #8926 is still open in the decision inbox, so this card stayspm:blocked.Repricing note for the eventual dispatch (do not skip): PR #8927 changed the exact predicate surface this card measures —
find()/count()behaviour on unresolvable WHERE columns in driver-sql. Re-verify this card's measured behaviour on the merged ref before pricing or dispatching; the merge that closed #8790 is the kind most likely to have moved (or partially fixed) what this card describes. Triage seat, sessionsession_01JeRt9LgqbAygZftG9KDusR.
Generated by Claude Code
#8936 has MERGED — the re-pricing this card was waiting on is now possible, and owed before it is graded.
domain:driversseat, PM sessionsession_01XeQRiAa7vYRVX5Fog7Zby8. ⛔ Not claiming, ⛔ no label changed.Measured on
origin/main@a8189aef4:a8189aef4 feat(objectql,metadata-protocol): refuse a dotted filter key whose head is a relation, a formula, or a plain scalar — at both doors (#8371) (#8936) + packages/spec/src/migrations/entries/semantic/18.engine-dotted-filter-refused.tsSo #8371 is settled and landed, which discharges the ⛔ this card carried — "a fix here must not be allowed to become a dotted-path verdict by implementation". The verdict now exists, written where it belongs, and nothing at the driver needs to mint one.
What still has to be measured before this card is graded
My earlier note said ⛔ do not price this against the then-current
main. That is now actionable rather than a caution:#8936 refuses a dotted key whose head is a relation, a formula, or a plain scalar. The residual population for this card is whatever still reaches the driver on the dotted route after that door — i.e. the heads #8936 deliberately does not judge, plus any path that bypasses both doors.
⇒ Whoever takes this must read what #8936 actually refuses and what it deliberately leaves alone, then re-measure. The defect itself is unchanged and still real — a raw dialect error carrying a bound literal to the caller is an ADR-0112 gap on its own terms, and #8936 is an authoring/query door rather than a driver envelope — but the severity is a different number now, and it may be small enough to change the disposition.
Unchanged and still the load-bearing fact
The dialect-classification divergence stands, and it is what makes a driver-side "just match the error text" fix wrong on Postgres:
dialect how the backend classifies {'title.x': v}SQLite undefined column Postgres undefined TABLE ( 42P01) — knex compilestitle.xto"title"."x", so PG readstitleas a missing relationMySQL undefined column ( 1054)⭐ Worth handing to #8936's author too, if it is not already known there: any rule that expects the three backends to agree on what a dotted key is will be surprised by Postgres.
⚠️ Provenance unchanged: the before/after table on this card is the #8790 dev's measurement against live servers, tied toorigin/main@13d78642d. It is dated evidence, and #8936 landing is exactly the kind of change that dates it further.
Generated by Claude Code
Unblock scan — one of two dependencies discharged; card stays blocked. From the
domain:driversseat (#6020), PM sessionsession_01NTKPDRoynY8i3HmdSFUxFj. ⛔ Not a claim, ⛔ no label changed.Blocked-by:state today verdict #8790 CLOSED completed, PR #8927 MERGED ✅ discharged #8926 OPEN, needs-user-decisionwith the maintainer⛔ still blocking ⇒
pm:blockedis still the honest label and is deliberately left in place. Recording the half-discharge so the next unblock scan does not re-derive it, and so nobody reads "still blocked" as "nothing has moved".Re-verification owed at unlock, ⛔ not now: when #8926 is ruled, this card's file surface must be re-verified on the merged ref before dispatch. That is not a formality here — PR #8927 has landed since this card's evidence was taken, and the card itself says so:
The before/after table is the #8790 dev's measurement against live servers, re-stated here rather than re-run by this seat. Treat it as dated evidence tied to
origin/main@13d78642d.origin/mainis now atb537855. The Postgres42P01/ MySQL1054readings are dated evidence about a tree that has moved, and the ruling on #8926 will likely reshape the very predicate this card extends. ⇒ re-measure before implementing; do not inherit the table.Also flagged upward this round: with #8790 gone, this card is the only queued work in the lane, and #8926 is the single ruling gating it — noted on #8926 itself, where the triage comment's "nothing is gated on this ruling" had gone stale.
Generated by Claude Code
⚠️ Scope change incoming — this card narrows to Postgres-only when PR #9061 mergesdomain:driversseat, sessionsession_01NTKPDRoynY8i3HmdSFUxFj. ⛔ Not a claim, ⛔ no label changed — the card stayspm:blocked. Recording now so the next dev does not re-measure a cell that has already been closed.#8926 was ruled option A by the maintainer and its PR (#9061) is reviewed ACCEPT and awaiting CI. It adds MySQL's
ER_BAD_FIELD_ERRORwording to the sharedisUnresolvableColumnErrorpredicate.MySQL spells a dotted key with the same sentence as a plain missing column (
Unknown column 'title.x' in 'where clause'), so widening the predicate catches the dotted route on MySQL as a side effect of the wording — the key is never inspected for a dot. Measured on a live MySQL 8.0.46 in that PR:dialect this card's evidence ( 13d78642d)after #9061 MySQL raw 1054, bound literal inlinedINVALID_FILTER/ 400, enveloped ✅Postgres raw 42P01, bound literal inlinedunchanged — still raw ⇒ The MySQL half of this card is closed by #9061, not by anything done here. What remains is the Postgres half, and it remains for the reason this card already identified as the hard part: Postgres is the odd one out. knex compiles
{'title.x': v}to"title"."x", so PG reports a missing relation (42P01, "missing FROM-clause entry for table") — a verdict that a column-wording predicate structurally cannot see. Widening the shared predicate does not reach it, and the same trick will not work twice.What that changes for whoever takes this
- ⛔ Do not re-run the MySQL cells. They are measured and fixed; re-measuring them will show green and could be misread as "the card is stale".
- The card's three framing questions (envelope-without-judging / which envelope / redaction as the separable cheap half) now apply to one dialect, which makes the "can it be enveloped without forming an opinion about the key?" question sharper, not easier — on Postgres there is no column-shaped signal to key on at all.
⚠️ Its before/after table is still dated evidence tied toorigin/main@13d78642d. Re-measure on the merged ref at unlock; ⛔ do not inherit it.
Blocking status unchanged:
Blocked-by: #8790(discharged) ·Blocked-by: #8926(still open until #9061 merges — then this card is free).
Generated by Claude Code
🔓 UNBLOCKED — both dependencies discharged.
pm:blockedremoved, back topm:queue.domain:driversseat (#6020), sessionsession_01NTKPDRoynY8i3HmdSFUxFj. ⛔ Not a claim; unassigned and open to whoever takes it.Blocked-by:state evidence #8790 discharged PR #8927 merged ( 716ac9bf8)#8926 discharged PR #9061 merged ( 9c4d096f3), verified by artifact onorigin/mainwith a positive control⚠️ Read this before doing anything: this card is now smaller than its body saysPer the unlock rule, the two least trustworthy things at this exact moment are the card's own claims and the PM's summary of them. So, measured rather than asserted:
#9061 closed the MySQL half. MySQL spells a dotted key with the same
ER_BAD_FIELD_ERRORsentence as a plain missing column, so widening the shared predicate caught the dotted route on MySQL as a consequence of the wording — the key is never inspected for a dot, so it is ⛔ not a dotted-path verdict and does not trespass on #8371.dialect this card's evidence ( 13d78642d)on maintodayMySQL raw 1054, bound literal inlinedINVALID_FILTER/ 400, enveloped ✅ closedPostgres raw 42P01, bound literal inlinedunchanged — still raw ← this is the whole card now ⛔ Do not re-run the MySQL cells. They will come back green and could be misread as "the card is stale". It is not stale; it is narrower.
What remains is the harder half, and the same trick will not work twice
Postgres is the odd one out, exactly as this card first identified: knex compiles
{'title.x': v}to"title"."x", so PG reports a missing relation (42P01, "missing FROM-clause entry for table"). A column-wording predicate structurally cannot see it. WideningisUnresolvableColumnErroragain does not reach this.So the card's three questions now apply to one dialect, and question 1 gets sharper, not easier: on Postgres there is no column-shaped signal to key on at all, which is precisely what makes "can this be enveloped without forming an opinion about the key?" a real question rather than a formality.
⚠️ If it cannot be done without inspecting the key, the card's own body says it collapses into #8371 and should be ruled there — that remains the honest exit, not a failure.⭐ Question 3 is untouched by any of this and may still be the cheap, separable win: stripping the bound literal from the caller-visible message needs no verdict at all and is the half with a security shape.
Owed at dispatch, ⛔ not now
The before/after table is dated evidence tied to
origin/main@13d78642d.mainhas moved a long way since, including #8927 and #9061 into this very predicate. Re-measure on the merged ref before implementing — ⛔ do not inherit that table.
Generated by Claude Code
os-project-manager commented
on Aug 16, 2026 CollaboratorMore actionsUnlock scan (triage seat): both
Blocked-by:lines are now discharged — #8790 closed earlier, #8926 closed 08:24Z via merged PR #9061. This card is fully dispatchable for the drivers lane.⚠️ Re-priced on the merged ref — the upstream merge absorbed half the card. #9061 widenedisUnresolvableColumnErrorto MySQL'sUnknown columnwordings, and MySQL classifies a dotted key as an undefined column (1054) — so on MySQL the dotted key now takes the refusal envelope. This is stated onorigin/mainitself:sql-driver-unresolvable-where-column-refusal.test.tsdocuments "Since #8926 MySQL sits in the same cell as SQLite" for the dotted key. What remains is exactly the odd dialect the card starred:- Postgres:
42P01undefined-TABLE (missing FROM-clause entry for table "title") — a shape the predicate does not match and never has; still a raw dialect error with the bound literal inlined. The card's questions 1–3 (envelope-without-judging, which envelope, separable redaction) now apply to this one dialect. - The redaction half (finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 shape) remains fully live for Postgres and is still the cheap standalone piece.
Dispatch must re-measure both dialects on the merged ref rather than trusting the card's
13d78642dtable — the MySQL row of that table is known-stale by construction. The #8371 fence stands: ⛔ no key-inspection for dots; refuse what the backend could not resolve.Triage: this comment is from the triage seat (scheduled run) — no claim.
Generated by Claude Code
- Postgres:
13 remaining items
os-project-manager commented
on Aug 17, 2026 CollaboratorMore actionsClaim — PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza, branchclaude/issue-8931-backend-fault-envelope.Dispatching on the 03:33Z maintainer ruling: Option C — a generic backend-fault envelope at driver-sql's terminal catch-all. The PR carries
Fixes #8931; its merge closes the card (questions 1+2 were the last open scope, question 3's premise having been falsified and pinned by PR #9108).⛔ The three fences from the ruling, restated because each is a way to get this wrong:
- No filter verdict. The envelope asserts only "the backend rejected this statement" — ⛔ not
INVALID_FILTER. On Postgres42P01is ambiguous between a dotted key and a genuinely missing relation and cannot honestly support one. - No new per-error-class predicate. Terminal catch-all only;
isUnresolvableColumnErrorandisMissingTableErrorstay untouched. A second recognizer on42P01is the split-predicate shape driver-sql: MySQL's unresolvable-column wording is matched by neither arm of the recovery/refusal predicate, so MySQL gets no ADR-0112 envelope and no #3821 recovery #8926 refused. - No statement shape in the caller-visible message — no physical table names, quoted references, or
$nplaceholders. Full dialect text + compiled statement go to the server log through the existingredactStatementFromMessage(2 consumers — reuse it, do not mint a second vocabulary).
Also in scope: update
DOTTED_STATUS_QUOand PR #9108's mechanism/message-content pins in the same PR. The Postgres row's cells change deliberately — record the reason alongside.
Generated by Claude Code
- No filter verdict. The envelope asserts only "the backend rejected this statement" — ⛔ not
- added a commit that references this issue
on Aug 17, 2026 os-project-manager commented
on Aug 17, 2026 CollaboratorMore actions{ "issue": 8931, "status": "done", "branch": "claude/issue-8931-backend-fault-envelope", "pr": "https://github.com/objectstack-ai/objectstack/pull/9273", "premise_still_valid": true, "summary": "Implemented the 2026-08-17 ruling (option C) as a terminal catch-all at driver-sql's two read exits (the else of findRows' recovery ladder, and count's tail): any dialect error the existing classification does not claim now leaves as a generic backend-fault envelope, DATABASE_ERROR / 500, composed and carrying no dialect text. All three fences hold and were verified live on PostgreSQL 16.13: no filter verdict (the envelope is never INVALID_FILTER and says nothing about the filter — measured, a dotted key and a table that was never created both raise 42P01, differing only in prose, and isMissingTableError returns true for BOTH); no new per-error-class predicate (isUnresolvableColumnError and isMissingTableError are byte-untouched, no predicate learns 42P01, the key is still never inspected for a dot); no statement shape in the caller-visible message (no table names, no quoted references, no $n). The original dialect error is kept as a NON-ENUMERABLE cause — load-bearing, because isMissingTableError follows cause and 13 non-test call sites use it to tell a benign unprovisioned table from a failure that must stay loud; verified live that isMissingTableError(wrapped) is still true on pg and sqlite for both halves, so the accept set is unchanged. Pins updated in the same PR: DOTTED_STATUS_QUO's pg row moves to DATABASE_ERROR/500 with the reason recorded beside it, the enveloped boolean becomes a real status, and #9108's literalWithheldBy axis is preserved rather than collapsed — the knex never-inlined fact moves to a new dialectTextInlinesLiteral axis asserted against the SERVER LOG, which is where the dialect text still lives, and the knex positionBindings mechanism pin stays with its consumer restated. One neighbouring disclosure closes as a side effect and is named in the PR body: Postgres inlines the caller's rejected VALUE in its own 22P02 diagnostic, downstream of parameterisation, where no statement cut can remove it.", "tests": "All at final commit fc3d82286 (tree clean; the gate union was re-run at that head after the last commit). LIVE MEASUREMENT: provisioned a real PostgreSQL 16.13 in-container (initdb + pg_ctl, port 54931, timezone=Asia/Shanghai with process TZ=America/New_York, the three-way zone skew the matrix's own non-vacuity guard demands) — same version as the card's evidence; torn down afterwards. No live MySQL was provisioned, so its cells are declared un-run (declareDialectCell), never silently skipped. (1) pnpm --filter @objectstack/driver-sql test with live PG: 'Test Files 103 passed | 1 skipped (104), Tests 1998 passed | 34 skipped (2032)'. (2) pnpm --filter @objectstack/driver-sql typecheck: clean. (3) DOWNSTREAM sweep — the PREFIX direction (packages that CONSUME driver-sql), each run and read: driver-turso 1003, driver-sqlite-wasm 395, rest 2011, runtime 2464, dogfood 780, service-analytics 1722, service-automation 967, service-datasource 478, service-messaging 242, cli 1367, plugin-security 1279, plugin-auth 1261, plugin-sharing 617, plugin-approvals 496, plugin-reports 70, trigger-record-change 78 — all green. That sweep CAUGHT one real consequence: runtime's #7842 ambient-vs-bare transaction test pinned the raw knex text 'Timeout acquiring a connection' reaching the caller; it is updated in this PR to assert the envelope AND read the same string off cause, so its discrimination is re-homed, not weakened. (4) GATES re-derived with node scripts/pm/dispatch-gates.mjs against the ACTUAL changed paths (not a recalled list); all PASS: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-cross-package-test-inputs, docs-audit/check-affected-docs; plus the convention-triggered check:query-options-erasure, check:type-check-coverage, check:type-check-debt (ratchet, run after a full turbo build), check:engine-double-contract, check:where-matcher; plus the implicated check:nul-bytes, check:error-code-casing, check:error-status-conformance, check:driver-conformance, check:route-envelope, check:driver-memory-census, check:durability-log-level, check:partof-closing-keyword. The derivation ADDED beyond the prompt's lead: changeset-gate-self-tests, objectui-changeset, cross-package-test-inputs, adr-0087-registration, changeset-no-major, empty-changeset, affected-docs. (5) BOUNDARY MEASUREMENT: ran the real mapDataError over the exact recorded driver errors, before and after. The card's own route is UNCHANGED on the wire (500 DATABASE_ERROR both ways — REST already derived that verdict by sniffing the message; it is now declared by the producer). One route DOES change and is flagged in the PR body and in open_questions: a read on a registered object whose table was never created goes 404 OBJECT_NOT_FOUND -> 500 DATABASE_ERROR. (6) REVERSE VERIFICATION, direction predicted before each run, all three as predicted: (a) revert findRows' catch-all to 'throw error' -> '19 failed | 53 passed', including 'find() and count() answer an unclassified fault the SAME way' going red because count's exit was still wrapped — a genuine discriminator, not a blanket red; (b) drop the cause -> '5 failed | 67 passed', exactly the two cause pins on both cells plus the 22P02 pin that reads cause.code; (c) make the cause enumerable -> '2 failed | 70 passed', exactly the serialisation pins. The fix was COMMITTED before all three legs; each restored with git checkout <branch> -- <path> and the tree verified clean after. (7) DIST DISCIPLINE, and it caught a false green: runtime resolves @objectstack/driver-sql through exports -> dist/ with no vitest alias, so the first cause-ablation leg against it came back GREEN off a stale artifact. Redone properly: mutate -> pnpm --filter @objectstack/driver-sql build -> node scripts/ablation-dist-preflight.mjs @objectstack/driver-sql \"defineProperty(err, 'cause'\" --absent (marker absent from all 6 built files) -> run, which then went red as predicted ('expected undefined to match /Timeout acquiring a connection/i'). Restore leg: source restored AND dist rebuilt, with the preflight re-run in the present direction on a marker that actually appears in executable output ('The database refused to run this query for object') to prove the mutation is gone from the artifact. All build/test runs wrapped in flock -E 99 -w 540 /tmp/os-heavy-verify.lock; heavy commands ran with NODE_OPTIONS=--max-old-space-size=4096. Changeset present (@objectstack/driver-sql patch) — the change is user-visible, so no skip-changeset label applies.", "open_questions": [ { "question": "A read against a REGISTERED object whose table was never provisioned now answers 500 DATABASE_ERROR where it previously answered 404 OBJECT_NOT_FOUND. Measured at the real mapDataError with the exact driver errors; no test pinned it end-to-end (the unknown-object heuristic suites feed synthetic strings straight to the mapper and are unaffected). It is a direct consequence of the ruled mechanism — the dotted-key 42P01 and the missing-relation 42P01 are indistinguishable without exactly the per-error-class predicate fence 2 forbids. Is that acceptable, or does it need its own follow-up?", "options": [ "A: accept as shipped. The 404's body said \"Object 'x' is not registered\", which is FALSE in that state — the object IS registered, its table is not provisioned — and 500 DATABASE_ERROR is what DATA_STORE_FAULT was built to say for a store condition that may clear.", "B: restore the 404 by teaching mapDataError to consult error.cause for the missing-relation phrasing. Keeps today's wire answer, costs an edit in packages/rest outside this card's surface, and re-introduces message sniffing over a wrapped error.", "C: file it as a separate card and decide there, leaving this PR as ruled." ], "recommendation": "A. Real business need: no deployment was measured as depending on the 404, and the sentence it shipped was untrue in exactly the state that produces it. Long-term soundness: B would put the boundary back in the business of re-deriving a verdict from prose that the producer now declares — the drift ADR-0112 exists to remove — and it would need its own tripwire. AI-agent error-resistance: a declared 500 DATABASE_ERROR tells an agent 'the server could not serve this, retry/escalate'; a 404 saying the object is not registered actively misleads it into deleting or re-declaring a registration that is fine. If the maintainer disagrees, C is the clean route — it is a REST-boundary question, not a driver one." }, { "question": "The ruling said to reuse `redactStatementFromMessage`; this PR does not call it, for two measured reasons (recorded in the PR body). Does the maintainer want it reused anyway, which would require moving it out of @objectstack/objectql?", "options": [ "A: keep the composed message as shipped. No second redaction vocabulary is minted (there is nothing to redact — the caller message carries no dialect text at all), and the server-log line is the dialect's text verbatim, matching unresolvableFilterColumnRefusal one method over.", "B: move redactStatementFromMessage to @objectstack/types (where looksLikeInternalErrorLeak already lives) and call it from the driver. A cross-package refactor with its own blast radius, and it would still fail the disclosure clause." ], "recommendation": "A, and the second reason is the decisive one rather than the layering: `redactStatementFromMessage` cuts at the last ' - ' and KEEPS the dialect's diagnostic tail. On this card's own route that tail is `missing FROM-clause entry for table \"title\"` — a physical table name in a quoted reference, which fence 3 forbids — and on the 22P02 route it is `invalid input syntax for type integer: \"...\"`, i.e. it would ship the caller's value. Reusing it would defeat the disclosure clause it was cited to serve. The layering point (driver-sql sits below the engine; no driver depends on objectql) is the second, independent reason." }, { "question": "No ADR-0087 semantic-migration entry was added, unlike #8790's driver-side change. Correct?", "options": [ "A: correct as shipped. SemanticMigration entries are structured TODOs for METADATA SOURCE FILES (`replacement` is 'the canonical replacement the author should move to'); nothing authorable changes here and there is no source for a consumer to migrate. check:adr-0087-registration passes and the changeset carries the not-required marker with that reasoning.", "B: add one anyway, on the #8790 precedent, since a consumer that pattern-matched the raw dialect code on a failing read has to change." ], "recommendation": "A. #8790 changed what find() RETURNS (an empty list became a throw) and its `replacement` names a real authoring action ('name a column the object actually has'). This card changes only the SHAPE of an error that was already a failure, with no authoring action available to take — a migration TODO would have an empty `replacement`. The consumer-side note (read error.cause) belongs in the changeset, where it is, rather than in a metadata migration chain." } ], "out_of_scope_findings": [] }
Generated by Claude Code
- added a commit that references this issue
on Aug 24, 2026 - added a commit that references this issue
on Aug 24, 2026 objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsDirector note, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn, 2026-10-02T11:05Z. ⛔ Not a reopening.The server-log half of this card's 2026-08 design is amended by the maintainer's ruling on #21385 (batch #269 item 1, A). Caller-supplied bound values no longer go to the server log at any layer. driver-sql's five "kept server-side" WARN lines call the same redaction core as the engine boundary (#21274), and keep the error code, class and dialect diagnostic. The caller-envelope half here is unchanged.
Generated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Filed unassigned and ungraded by the
domain:driversseat (PM sessionsession_01XeQRiAa7vYRVX5Fog7Zby8) from a fork the #8790 dev reported and deliberately did not resolve. For triage to grade.⛔ Pre-existing. NOT introduced by PR #8927 — that is the load-bearing fact, and it is measured rather than argued (below).
The defect
A dotted WHERE key (
{'title.x': v}) ondriver-sqlescapes as the dialect's own raw error, with no ADR-0112 envelope and the bound literal inlined in the message — the same disclosure shape #7929 redacted elsewhere, and the same shape #8790 has just removed fromcount()on the plain-column route.Measured on live servers, before and after PR #8927
The #8790 dev stood up PostgreSQL 16.13 and MySQL 8.0.46 locally and ran
origin/main@13d78642dand the #8790 branch side by side out of two worktrees. This is observation, not classifier-reading:[]+ rawSQLITE_ERRORINVALID_FILTER/ 40042P01, both halves42P01, both halves — identical1054, both halves1054, both halves — identical⇒ Postgres and MySQL are byte-identical before and after. #8927 changed the SQLite arm only, as a consequence of the ruled plain-column refusal, and its PM answer (
5304147611) records why that is the ruling applied literally rather than an extension.⭐ The fact that makes this its own card: three backends, three readings of one key
{'title.x': v}no such column: title.x)42P01) — knex compiles it to"title"."x", so PG readstitleas a missing relation1054)Postgres is the odd one: the error is
missing FROM-clause entry for table "title", so any predicate keyed on the undefined-column wording structurally cannot see it. That is why a fix here is not "add one more error string".⛔ Why this is not folded into a neighbouring card
Unknown column 'x' in 'where clause'matching neither arm of the predicate). Different route.where: { project_id.name: 'x' }rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 is the FILTER-axis dotted-path verdict — the authoring-side question of whether dotted paths should be judged at all. Different layer.where: { project_id.name: 'x' }rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 rules: even if dotted paths stay deliberately unjudged, a raw dialect error carrying a bound literal to the caller is an ADR-0112 gap on its own terms..".What a fix has to decide
where: { project_id.name: 'x' }rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 and should be ruled there instead.INVALID_FILTERwould assert a verdict about the filter. A transport/dialect-fault envelope asserts only "the backend rejected this statement". These are different claims and the choice is the card.Not claimed
find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790's ruling and PM answer5304147611.find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 dev's measurement against live servers, re-stated here rather than re-run by this seat. Treat it as dated evidence tied toorigin/main@13d78642d.Refs: #8790 (the plain-column refusal that measured this) · PR #8927 · #8926 (MySQL plain-column predicate gap) · #8371 (dotted-path verdict) · #7929 (the bound-literal disclosure shape).
Blocked-by: #8790
Blocked-by: #8926
(Triage 2026-08-15: dependency lines added — the fix extends the same recovery/refusal predicate PR #8927 is landing and #8926's pending ruling will shape. Rationale in the triage comment below.)