test(driver-sql): pin what keeps a dotted key's bound literal off the wire, per dialect (#8931 Q3) - #9108
Merged
os-zhuang merged 1 commit intoAug 16, 2026
Conversation
… wire, per dialect (#8931) Issue #8931 question 3 asked for one separable half: strip the caller's bound literal from the Postgres dotted-WHERE route's message and send the full text to the server log instead (the #7929 shape). Measured live on PostgreSQL 16.13, the premise does not hold. On Postgres the bound literal is never in that message to begin with: knex positions pg bindings to `$1` before it formats the failing statement into the error, and its formatter substitutes only `?`, so zero bindings are inlined. Sqlite and mysql keep `?`, so they DO inline it -- which is the leak #7929/#8790 really redacted. No behaviour changed. The dotted route's `code` and `status` are untouched on every dialect; questions 1 and 2 (envelope, and which envelope) stay unruled. What is added is the content axis `DOTTED_STATUS_QUO` lacked, so an incidental property of a dependency stops being an unstated assumption: - `literalWithheldBy` per cell -- a real redaction (sqlite, mysql) vs never inlined (postgres); the two are different in kind and only one is a fix. - `identifies` per cell, so "no literal" cannot pass against an emptied message. - absence asserted on `message`, `stack` and every own string property (an unenveloped pg error reaches the caller carrying a dozen fields). - a POSITIVE CONTROL per cell: the redacting dialects must show the literal in the server log; postgres must show a `$n` placeholder where it would be. - a mechanism pin on knex's `positionBindings`, so a dependency upgrade that moves this turns the pg cell red instead of silent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
os-zhuang
marked this pull request as ready for review
August 16, 2026 13:19
os-zhuang
deleted the
claude/issue-8931-postgres-dotted-literal-redaction
branch
August 16, 2026 13:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8931 — question 3 only, per the maintainer authorisation 「同意 a」 (comment
5306729666). Questions 1 and 2 remain open and unruled, so this must not close the card.⭐ The premise did not survive re-measurement
Question 3 asked for one separable, verdict-free half: strip the caller's bound literal from the Postgres dotted-WHERE route's message, and send the full text to the server log instead (the #7929 shape).
Measured live on PostgreSQL 16.13, there is no bound literal in that message to strip.
The card's before/after table is dated to
origin/main@13d78642d; it was re-measured here rather than inherited, onorigin/main@5cd31f61e, with a real PG 16.13 provisioned in-container.{'title.x': v}code/status42P01/undefined— matches the recorded status quoselect * from "…" where "title"."x" = $1 - missing FROM-clause entry for table "title"messagestackdetail,hint,where,internalQuery, … all checked)Measured across 7 filter shapes (
eq,$in,$like,$gte,$ne,$between, dotted-inside-$or) × both halves (find,count). All 14 cells: literal absent,$nplaceholder present.Why — the mechanism, and it is not ours
Inside knex 3.3.0 (
lib/execution/internal/query-executioner.js):enrichQueryObjectrunsclient.positionBindings(sql)— the pg dialect rewrites every?to$1,$2, …err.message = formatQuery(sql, bindings) + ' - ' + nativeMessage.formatQuerysubstitutes?and nothing else. On pg no?survives step 1, so it inlines zero bindings.Sqlite and mysql leave
?standing, so they do inline the value — which is the disclosure #7929 and #8790 really did have to redact. Verified directly against the real library:positionBindings('… = ?')returns… = $1onpg, and is the identity onbetter-sqlite3andmysql2.Both branches of that catch are literal-free on pg — the
compileSqlOnError === falsebranch uses the already-positioned SQL too.What this PR does instead
No behaviour change. Zero production files touched. The dotted route's
code,statusand classification come out exactly as they went in, on every dialect — questions 1 and 2 stay the maintainer's.What it adds is the axis
DOTTED_STATUS_QUOdid not have, which the dispatch pre-authorised. Postgres' immunity is incidental, undocumented, and lives inside a dependency — the three properties that make something worth pinning rather than assuming:literalWithheldByper cell, because the cells agree on the outcome for reasons that differ in kind: sqlite/mysql = a real redaction (the driver-sql: one unresolvable WHERE column, two answers —find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 refusal took an inlined literal back out); postgres = never inlined (nothing redacted anything). Collapsing those two into "no literal reaches the caller" is what let a false premise stand.identifiesper cell — substrings the caller-visible message must still carry, so "no literal" cannot pass against an emptied message.message,stack, and every own string property — an unenveloped pg error reaches the caller as the driver's own object carrying a dozen fields, so the message is not the only way out.$nplaceholder where the value would be.positionBindings, so a dependency upgrade that moves this turns the pg cell red instead of silent. If knex ever positions bindings after formatting, question 3 becomes a real fix and this goes red the same day.Verification
All at
f6150697f, the final commit.pnpm --filter @objectstack/driver-sql test— 102 files passed, 1973 passed / 33 skipped, with a live PG 16.13 provisioned CI-style (timezone=Asia/Shanghai,TZ=America/New_York).pnpm --filter @objectstack/driver-sql typecheck— clean.scripts/pm/dispatch-gates.mjsagainst the actual changed path, all PASS:check:nul-bytes,check:test-source-alias,check:type-source-resolution,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:error-code-casing,check:error-status-conformance.Reverse verification — direction predicted before running, both legs landed as predicted:
literalWithheldByto the redaction mechanism1 failed / 46 passed— "the dialect text should have reached the server log with the literal intact"identifies1 failed / 46 passed— "the caller must still be told 'zz-not-in-any-message'"Ran under the shared verification lock throughout.
The Postgres dotted route still answers a raw
42P01with nostatus, carrying the compiled statement's shape (table, quoted reference,$n). That is a disclosure of internal query structure and an ADR-0112 envelope gap — but closing it means deciding which envelope, which is question 2, and whether the driver can envelope it without forming an opinion about the key, which is question 1. Both are unruled and both are the maintainer's. No predicate was widened;isUnresolvableColumnErroris untouched, so the #8371 fence stands.skip-changeset: test-only, nothing user-visible changed — there is no caller-visible message change to describe, precisely because the premise died.Generated by Claude Code