Skip to content

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 into
mainfrom
claude/issue-8931-postgres-dotted-literal-redaction
Aug 16, 2026
Merged

test(driver-sql): pin what keeps a dotted key's bound literal off the wire, per dialect (#8931 Q3)#9108
os-zhuang merged 1 commit into
mainfrom
claude/issue-8931-postgres-dotted-literal-redaction

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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, on origin/main @ 5cd31f61e, with a real PG 16.13 provisioned in-container.

what Postgres, dotted key {'title.x': v}
code / status 42P01 / undefined — matches the recorded status quo
caller-visible message select * from "…" where "title"."x" = $1 - missing FROM-clause entry for table "title"
bound literal in message absent
bound literal in stack absent
bound literal in any own property absent (detail, 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, $n placeholder present.

Why — the mechanism, and it is not ours

Inside knex 3.3.0 (lib/execution/internal/query-executioner.js):

  1. enrichQueryObject runs client.positionBindings(sql) — the pg dialect rewrites every ? to $1, $2, …
  2. Only then does the failure path build err.message = formatQuery(sql, bindings) + ' - ' + nativeMessage.
  3. formatQuery substitutes ? 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 … = $1 on pg, and is the identity on better-sqlite3 and mysql2.

Both branches of that catch are literal-free on pg — the compileSqlOnError === false branch uses the already-positioned SQL too.

What this PR does instead

No behaviour change. Zero production files touched. The dotted route's code, status and 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_QUO did 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:

  • literalWithheldBy per 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 [] while count() 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.
  • identifies per cell — substrings the caller-visible message must still carry, 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 as the driver's own object carrying a dozen fields, so the message is not the only way out.
  • A POSITIVE CONTROL per cell: the redacting dialects must show the literal in the server log (proving the text really could have carried it); postgres must show a $n placeholder where the value would be.
  • A mechanism pin on knex's 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 test102 files passed, 1973 passed / 33 skipped, with a live PG 16.13 provisioned CI-style (timezone=Asia/Shanghai, TZ=America/New_York).
  • Same suite with no live server (Test Core config): 99 passed, 1734 passed / 57 skipped.
  • pnpm --filter @objectstack/driver-sql typecheck — clean.
  • Gates re-derived with scripts/pm/dispatch-gates.mjs against 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:

leg sabotage expected observed
relabel pg's literalWithheldBy to the redaction mechanism claim pg is redacted red on the pg positive control only 1 failed / 46 passed"the dialect text should have reached the server log with the literal intact"
add an absent needle to pg's identifies empty-message trivially-true guard red on the pg disclosure pin 1 failed / 46 passed"the caller must still be told 'zz-not-in-any-message'"

Ran under the shared verification lock throughout.

⚠️ What is still open, and is deliberately not touched here

The Postgres dotted route still answers a raw 42P01 with no status, 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; isUnresolvableColumnError is 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

… 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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@os-zhuang os-zhuang added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed tests labels Aug 16, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 16, 2026 13:19
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit a01ca7f Aug 16, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8931-postgres-dotted-literal-redaction branch August 16, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants