Skip to content

Commit 6d728b8

Browse files
fix(driver-sql): the five refusal log lines take the shared driver-fault cut, moved to core (#21414)
Fixes #21385 Clause-②: no Security handling: this body, the commits, the changesets and the test names carry classes and positions only. Every pin plants a synthetic sentinel and asserts it is ABSENT. No measured log line is copied anywhere, and the local probe printed booleans only. ## What this changes This implements the maintainer's ruling on #21385 (comment 5950942037, letter A): one cutter for every log face. The cutter's home is `@objectstack/types`, per the claim's amendment 1 (5954587444) and triage's pointer (5954318919). See "Patch round 1" below. 1. **The redaction module moves to `@objectstack/types`.** `packages/objectql/src/driver-fault-redaction.ts` becomes `packages/types/src/driver-fault-redaction.ts`. Against `main`, that is the PR's one git rename (`R092`). The cut is the same code: the same split, the same structural cut at the separator, the same value templates and the same property rules. - The module's one import, `looksLikeInternalErrorLeak`, is now the package-local `./error-leak.js`. `error-leak.ts` and its frozen list are not edited. - The types entry exports four names, listed by name: `redactBoundStatement`, `redactStatementFromMessage`, `redactPropagatedDriverFault` and the `DriverFaultOrigin` type. - The template table and its load-time guard stay package-internal. The guard's eight cases moved verbatim beside it, to `packages/types/src/driver-fault-redaction.test.ts`. - Why `types`: it is the lowest package every face of this family can reach. `driver-sql`, `objectql` and `core` all depend on it, and the family's fourth position, `operatorFacingErrorText`, lives inside it (#21418). The module header says so. - There is no copy of the cut, no edit to `error-leak.ts` or `driver-error-classification.ts`, and no row added to the frozen predicate list. `packages/core` is not in the diff. 2. **One widening, on the log face.** `redactStatementFromMessage` takes an optional second argument: the same `{ statementSent: true }` that `redactPropagatedDriverFault` already took. Without it, the answer is unchanged. 3. **objectql calls the moved code.** `engine.ts` and `lifecycle/lifecycle-service.ts` now import from `@objectstack/types`, with the same arguments as before. Five objectql test files repoint their import. None of the moved names was on objectql's entries. 4. **driver-sql's five lines call it.** In `sql-driver.ts`, each refusal line writes the dialect's text through `redactStatementFromMessage(text, { statementSent: true })`, imported from `@objectstack/types`. The lines are: - the unresolvable WHERE column (`INVALID_FILTER`), on `find` and on `count`; - the read terminal (`DATABASE_ERROR`); - the raw-statement terminal (`DATABASE_ERROR`), which no longer writes the sent statement as a separate field; - the unresolvable groupBy / aggregation column (`INVALID_FIELD`); - the unresolvable listed-distinct column (`INVALID_FIELD`). `statementSent` holds by construction: each line writes a fault raised by a statement this driver sent, which is the same knowledge the engine's raw door passes. The read terminal's cut sits where its text is computed, so the two debug lines that demote it inside a scope take the cut too (see Acceptance notes). 5. **`driver-turso`'s remote transport is covered by the same line.** Its refusals reach the base class's raw terminal. Its transport error carries no statement (the bare shape), so the line now writes the engine's diagnostic and nothing of the statement it was sent. 6. **Two envelope sentences are corrected.** The read terminal's and the raw terminal's composed `DATABASE_ERROR` messages said the statement was written to the server log. After this change that is not true, so each now says the diagnostic was written, with the statement and its bound values cut. Code, status, `cause` and the withheld text are unchanged. See Acceptance notes, deviation 1. ## Measured: sentinel on each line, before and after Probe: drive each line through the public driver method on better-sqlite3, a live PostgreSQL 16 and a live MySQL 8.0.46, with one synthetic sentinel. An extractor printed only whether the captured line held the sentinel, and on which field. BEFORE was measured at `ecb6ca0258` (base), and AFTER at `0cbd86d55e`, with the same extractor. The committed pins re-measure AFTER at the final head, `992e940fff`. | line | drive | sqlite before → after | pg before → after | mysql before → after | |---|---|---|---|---| | WHERE column | `find` | present → absent | absent → absent | present → absent | | WHERE column | `count` | present → absent | absent → absent | present → absent | | read terminal | missing table (sqlite, mysql); 22P02 value (pg) | present → absent | present → absent | present → absent | | raw terminal | bound value | present (dialect field) → absent | present (dialect field) → absent | present (dialect field) → absent | | raw terminal | value spelled inline | present (statement field and dialect field) → absent | same → absent | same → absent | | groupBy / aggregation column | — | present → absent | absent → absent | present → absent | | listed-distinct column | — | present → absent | absent → absent | present → absent | After the change, on every row: the lead (code and class of fault, object and column) is kept, the dialect's own diagnostic is kept, and the cut's marker stands where the statement was. On the pg read and raw rows, the 22P02 value slot reads as the value marker. The raw line no longer carries a `statement:` field. The `driver-turso` remote transport, with a sentinel spelled inline in the sent statement: the raw terminal was handed it in the statement and not in the transport's bare error, and the line carries none of it. No marker appears there, because the bare error has no statement to cut. ## Zone 2 hypotheses - **H1 confirmed, with one amendment.** All five lines were driven on all three dialects. At base they sit at `sql-driver.ts` `:10087`, `:10215`, `:10284`, `:10960` and `:11253`. The amendment: on PostgreSQL, the three unresolvable-column lines never carried the sentinel. pg positions bindings as `$n` before knex formats the message, and its column diagnostic names identifiers only. Those three pg cells are recorded in the pin as non-regression cells, not as non-vacuous ones. The pg read and raw terminals did carry it, through the 22P02 diagnostic and through the raw statement's own text. - **H2 superseded in patch round 1.** Round 0 confirmed that `packages/core` could hold the module. Amendment 1 moved it to `packages/types`, where it also fits. Measured, the package takes a new runtime module: - its `tsup` entry is `src/index.ts` (edge-safe; the module uses no `node:` builtin); - its `tsconfig` includes `src/**/*`, and `--listFiles` shows both the module and its test; - `check-dts-emitted` runs in its build, and `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:published-files` all pass; - no gate refused it, and none was edited. The WHOLE module moved, not only the cutter. The two faces and the property rules are thin wrappers over the module's private split, so objectql no longer has a wrapper of its own. - **H3 confirmed.** Each line keeps its lead, plus `(CODE)` on the read and raw lines, and the dialect's diagnostic. It loses the statement and any template-owned value. The raw terminal's separately logged `statement:` field carried an inline sentinel on all three dialects at base, and it is no longer written. - **H4 partly falsified.** Every envelope's code, status, `cause` and withheld text are unchanged. But two composed messages described the log half ("the statement … written to the server log"), and this change made them false. They were corrected; see deviation 1. The engine's existing pins for the two earlier positions (the cards behind PR #21335 and PR #21384) stay green: - the full objectql suite, which includes `driver-fault-boundary-redaction.test.ts` and `driver-fault-redaction-residue.test.ts`; - the dogfood suite, which includes `raw-statement-fault-redaction.test.ts`; - the plugin-auth carrier pin, 15/15 on three dialects. ## Pins - **New: `packages/drivers/driver-sql/src/sql-driver-21385-refusal-log-line-redaction.test.ts`, 46 cases.** It runs on every cell of the driver axis: SQLite always, live PostgreSQL and MySQL when their URLs are set. - Per cell, each of the seven drives has two cases: - a SENTINEL case: the text handed to the line carried the sentinel as measured, the written line does not carry it, and the marker is present; - a CONTROL case: the envelope's code and status, the lead, the diagnostic, the dialect code and the named object and column. - Each cell also has one success control. - One case pins the deferred-DDL debug line. - Non-vacuity is read off what each protected refusal method was HANDED, through a recording subclass. Every assertion on a line is a boolean, so a red names the line and never prints it. - **Flipped: nine existing pins in eight files.** Each of these pinned the retired design, in which the statement was in the log line: - in `driver-sql`: `sql-driver-11455-…`, `-11541-…`, `-16019-…`, `-17639-…`, `-17857-…`, `sql-driver-backend-fault-envelope` (two cases) and `sql-driver-unresolvable-where-column-refusal` (the positive control); - in `driver-turso` (patch round 1): `turso-driver-16019-remote-raw-statement-fault-envelope`. Each now asserts the diagnostic and that the statement is absent. The `driver-sql` ones also assert the marker. The positive controls, and the turso pin, read the sentinel's presence on what the refusal was handed (or on the envelope's `cause`) instead of on the log. ## Reverse verification **The five calls (round 0).** The change was committed first (head `7445ed5cda`, `sql-driver.ts` blob `ede93583a0f1`). Only the five calls were reverted: the four identical call sites and the read terminal's one. This used two nested `scripts/ablation-replace.mjs` legs: anchor x4 → x0 (blob `ede93583a0f1` → `e4243feaf00e`), then anchor x1 → x0 (blob `e4243feaf00e` → `a8ea87a6014b`). The driver-sql pin loads `./sql-driver.js` from source, so no build sits between the mutation and the run. - **Mutated: 22 red / 24 green of 46.** - Red on "the sentinel reached the line": sqlite 7/7, mysql 7/7, pg 3 (read, raw bound, raw inline), and the debug-line case. That is 18. - Red on "says a statement was cut": the 4 pg cells that were never handed the sentinel (WHERE find and count, aggregate, distinct). - Green: all 21 CONTROL cases and the 3 success controls. - **Restored.** Proven by the tool twice and by a script trap once: blob == HEAD, and `git diff HEAD` is empty. **The turso flip (patch round 1, at `992e940fff`).** Only the `driver-sql` change this pin depends on was reverted: the raw terminal's dropped `statement:` field was put back. `driver-turso` reads `driver-sql` from its dist, so each leg was rebuilt and its dist checked before the run. - The first attempt was a no-op. The replacement text contained the anchor, so `ablation-replace.mjs` refused it (anchor x1 → x1) and ran nothing. The anchor was changed and the leg re-run. - **Mutate.** Anchor x1 → x0, blob `ae07547bcd68` → `025ec6208050`. The on-disk marker count was 1. `driver-sql` was rebuilt, and `ablation-dist-preflight.mjs` found the marker present in 2 built files. - The turso pin went 1 red / 3 green; the red is "the sentinel reached the server log". - The driver-sql raw pins went 7 red / 46 green: the six raw-terminal sentinel cases on three cells, and the `16019` raw-line pin. - **Restore.** Proven by the tool and by a trap: blob == HEAD `ae07547bcd68`, and `git diff HEAD` is empty. After a rebuild, the preflight with `--absent` found the marker absent from all 6 built files. The turso pin went back to 4/4, and the driver-sql raw pins to 53/53. ## Verification Final head: `992e940fff`. Live servers: PostgreSQL 16 and MySQL 8.0.46, throwaway instances that were stopped by recorded PID and removed. `TZ=America/New_York` was set, as the live CI job sets it. - `@objectstack/types`: `test` 23 files / 696 tests passed; `test:repo` 1 file / 7 tests passed; `typecheck` exit 0. - `@objectstack/core` (the module left it): `test` 76 files / 2156 tests passed; `typecheck` exit 0. - `@objectstack/objectql`: `test` 365 files / 7379 tests passed; `test:repo` 1 file / 5 tests passed; `typecheck` exit 0, with `check:test-typecheck` OK. - `@objectstack/driver-sql`, full suite (`vitest run --maxWorkers=2`), with both live URLs: 227 files passed; 5472 tests passed and 1 skipped; 0 sentinel occurrences in the run log. `typecheck`: exit 0. - `@objectstack/driver-turso`: `test` 88 files passed; 2365 tests passed and 33 skipped. `typecheck`: exit 0. - `@objectstack/driver-sqlite-wasm`: `test` 36 files / 675 tests passed; `typecheck` exit 0. - The `Test Core (1/6)` packages that CI never reached: - `spec`: `test` 600 files, 17606 passed + 1 todo; `test:repo` 48 files / 849 passed; - `service-settings` 33 / 591; - `cloud-connection` 31 / 401; - `service-messaging` 46 / 507; - `example-showcase` 31 / 394; - `plugin-pinyin-search` 2 / 21; - `connector-mcp` 3 / 23; - `knowledge-memory` 1 / 8. All exited 0. - Dogfood, against freshly built dists (`turbo run build --filter=@objectstack/dogfood... …`, 63/63), with both live URLs: `test` 171 files passed and 1 skipped; 1402 tests passed and 3 skipped. - `typecheck` exit 0, after `pnpm install --frozen-lockfile`. The first attempt failed on a missing link for `@objectstack/trigger-api`: the `main` merge had added that dependency, and this worktree's install predated the merge. - The plugin-auth carrier pin (`driver-fault-auth-log-carriers.test.ts`), on three dialects: 15/15. - The runtime files that read these lines (`seed-tenancy-autonumber-split`, `expected-read-refusal-noise.channel-asymmetry`, `metadata-list-ambient-vs-bare-transaction`, `first-boot-migration-gate-read`): 4 files, 22 tests passed. - Gates: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 70 at `992e940fff`. All 70 ran with exit 0, and `--ran` reconciles 70 derived / 70 run / 0 NOT-MEASURED / 0 UNRUN. - `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit 3) for 8 packages without a dist. After those were built, it exited 0. - Also run: `pnpm check:live-db-isolation` PASS, and a control-byte self-scan of the 26 touched files, with no hit. - ESLint, narrowed to the 23 touched TS files, at `992e940fff`: - the population read from `eslint.config.mjs` is `packages/**/*.{ts,tsx,mts,cts}`; - `--format json` gives 23 files, 0 errors and 0 warnings; - invariance: the config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move an untouched file's verdict. - The repo-wide `pnpm lint` is CI's. ## Patch round 1 - **Why.** CI on `7445ed5cda` went red in `Test Core (1/6)`, on the `driver-turso` pin that asserted the statement on the raw terminal's line. That pin is this PR's, and it is flipped here. The shard's first attempt had failed earlier, in an unrelated `spec` hook timeout (#21421). Separately, the seat moved the cutter's home to `@objectstack/types` (amendment 1, 5954587444), so that #21418 can call it from `operatorFacingErrorText`. - **What moved.** The module, its guard test and the four named exports moved from `packages/core` to `packages/types`, and core's export block was removed. Every importer was repointed, with the module-path strings and comments that named core. The core `minor` changeset became a types `minor` one, and the driver-sql and objectql changesets now name `@objectstack/types`. Net against `main`: one rename, `objectql` → `types`; `git diff origin/main...HEAD -- packages/core` is empty. - **The sweep for other pins of the retired line.** - Command: `git grep -n -E` over the test files of all 18 packages that depend on `@objectstack/driver-sql`. The pattern covered the five lines' leads, the `[sql-driver]` prefix, `refused a raw statement` / `refused a read on` / `could not be resolved on`, `kept server-side`, `statement: `, and an `includes('DATABASE_ERROR' | 'INVALID_FILTER' | 'INVALID_FIELD')` filter. - Result: 35 hit lines in 28 files. Read one by one, exactly one is a pin of the retired content, the `driver-turso` pin above. - The rest are prose, unrelated words, the #7929 withheld-filter line, or line consumers that key on the lead and the diagnostic. The four runtime consumers among them were run, and are green. - No other lane's package needed an edit. - **Does `dispatch-gates --commands` derive the suites of packages that depend on a changed package?** Measured: no. - It maps a file surface to `check:*` gate families. Of its 70 commands at `992e940fff`, none is a package test suite: the only `--filter` command is `spec`'s `check:duration-unit-keys`. - So it derives neither the changed packages' own suites nor their dependents'. That is why round 0's local runs never reached `driver-turso`. ## Acceptance notes - **Deviation 1: two caller-facing envelope messages changed by one sentence each.** The dispatch said no caller-facing envelope changes. The read terminal's and the raw terminal's composed messages stated that the statement was written to the server log, and this change makes that false. The agent definition requires a released string that a change makes false to be corrected in that change, and it wins on a conflict. So each sentence now states what is written: the diagnostic, with the statement and its bound values cut. - The disclosure, code, status and `cause` are unchanged. - `operatorFacingErrorText`'s copy matches only the leading "refused to run a raw statement" fragment, which is kept; the producer pin `sql-driver-16657-…` is green. - Six hand-built fixture copies in five test files still carry the old second sentence, in `metadata`, `metadata-protocol`, `rest`, `types` and `objectql`. They stay green because the matcher keys only on the leading fragment. They are left as they are: four of the files sit outside this claim's surface, and editing only the fifth would split the copies. - **Bounded in-place extension: the read terminal's two debug lines.** The read terminal computes its dialect text once and writes it on the warn line or on one of two debug lines (a pre-DDL question, or a table whose DDL the driver deferred). The cut sits where the text is computed, so all three take it. It is the same defect class and the same mechanical call, in the claimed file, with the same gate family. It is pinned on the deferred-DDL debug line. - **The turso pin keeps no marker assertion.** The remote transport's error is bare (no statement in front of the diagnostic), so the cut has nothing to cut and adds no marker. The pin asserts the diagnostic, the class of fault, no sentinel and no `statement:` field. Non-vacuity is read off the sent statement the raw terminal was handed. - **The process timezone.** Three live-matrix files require the process zone to differ from UTC. Local runs set `TZ=America/New_York`, as CI's live job does. ## Out of scope (reported, not edited) - **`operatorFacingErrorText` (`@objectstack/types`) hands the raw path's `cause` text back whole.** Measured on SQLite with a synthetic sentinel bound into a raw statement: the envelope's message carries none, and the helper's answer carries it. This is #21418's (the family's fourth position), which can now call the cutter from the same package. Its docblock also says the driver writes the statement one line earlier, which is no longer true. - **Stale prose outside this surface.** These no longer match the driver's lines: - `packages/qa/dogfood/test/raw-statement-fault-redaction.test.ts` (its header says the driver's raw line writes the statement); - `sql-driver-diagnostic-value-probe.test.ts`'s rationale for not importing the redactor (the module paths in its failure messages are updated here). --- _Generated by [Claude Code](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3a6d92f commit 6d728b8

26 files changed

Lines changed: 859 additions & 201 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
---
4+
5+
fix(driver-sql): the driver's own refusal log lines no longer write the statement or the values bound into it
6+
7+
Clause-②: no
8+
9+
Five warning lines wrote the dialect's message to the server log as it came back. That message opens with the statement, with its bound values inlined on SQLite and MySQL, and on PostgreSQL a value-bearing diagnostic carries the value itself. The lines are the read terminal, the raw-statement terminal, and the refusals for a WHERE, a groupBy or aggregation, and a listed-distinct column the backend could not resolve. Each line now writes the dialect's text through the driver-fault redaction in `@objectstack/types`, the cut the engine applies at its boundary.
10+
11+
- **What stays on each line.** Its code, the class of fault it reports, the object and column it names, the dialect's error code where the line printed one, and the dialect's own diagnostic.
12+
- **What goes.** The statement and the values bound or inlined into it, replaced by `[statement and bound values redacted]`, and the value slot of each diagnostic the redaction's templates own, replaced by `[value redacted]`. The raw-statement line no longer writes the statement it was sent, which also holds for `@objectstack/driver-turso`'s remote transport, whose refusals reach the same line. The two debug lines the read terminal writes inside a pre-DDL question, or for a table whose DDL the driver deferred, take the same cut.
13+
- **The envelopes.** The code, status, `cause` and withheld text of every refusal are unchanged. Two composed messages, the read terminal's `DATABASE_ERROR` and the raw-statement terminal's, said the statement was written to the server log; they now say the diagnostic was written with the statement and its bound values cut.
14+
- **What changes for an operator.** A log reader that took the statement or a bound value from these lines now finds the marker where the dialect's text carried them, and nothing where the raw-statement line wrote the sent statement on its own. The diagnostic, the codes and the named object and column are where they were.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
refactor(objectql): the engine takes its driver-fault redaction from `@objectstack/types`
6+
7+
Clause-②: no
8+
9+
The redaction the engine applies to its write-path log lines, at its boundary, at the raw-statement door and in the lifecycle sweep now lives in `@objectstack/types`, so `@objectstack/driver-sql` calls the same cut. The engine calls it as before, with the same arguments, and its answers are unchanged. None of the moved names was exported from `@objectstack/objectql`'s entries, so its public surface does not move.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/types': minor
3+
---
4+
5+
feat(types): the driver-fault redaction is exported from types, so a driver's own log lines take the same cut the engine applies
6+
7+
Clause-②: no
8+
9+
- **New exports.** `redactBoundStatement`, `redactStatementFromMessage`, `redactPropagatedDriverFault` and the `DriverFaultOrigin` type are exported from `@objectstack/types`, by name. They moved here from `@objectstack/objectql`, which never exported them from its entries. The cut is unchanged by the move: the same split, the same structural cut at the separator, the same value templates and the same property rules.
10+
- **Why here.** `@objectstack/driver-sql`, `@objectstack/objectql` and `@objectstack/core` all depend on this package, and `operatorFacingErrorText` lives in it, so this is the lowest package all of them can import the cut from. The module imports only this package's own leak predicate, which is unchanged.
11+
- **One widening, on the log face.** `redactStatementFromMessage` takes an optional second argument, `{ statementSent: true }`. With it the cut runs without asking the shared leak predicate, as `redactPropagatedDriverFault` already did with the same flag. Without it the function answers exactly as before.
12+
- **Why minor.** The package gains four exported names, and `redactStatementFromMessage` gains the optional parameter above. No existing export of `@objectstack/types` changes.

‎packages/drivers/driver-sql/src/schema-drift.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1290,7 +1290,7 @@ export function diffManagedTable(args: {
12901290
// never revisits an existing column, so a deployment upgrading into that
12911291
// release gets no change AND no diagnostic. The server keeps refusing the
12921292
// same write, and the refusal is a poor substitute for a report: the live
1293-
// probe behind `objectql`'s `driver-fault-redaction.ts` measured Postgres's
1293+
// probe behind `types`' `driver-fault-redaction.ts` measured Postgres's
12941294
// `22001` as identifier-only and naming the TYPE rather than the column
12951295
// (`value too long for type character varying(255)`), MySQL's `1406` as
12961296
// `Data too long for column 'label' at row 1`. Meanwhile every

‎packages/drivers/driver-sql/src/schema-drift.unbounded-text-column.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
* never revisits an existing one — so a deployment upgrading into that release
2929
* gets no change AND no diagnostic, while the server keeps refusing the same
3030
* write. That refusal is a poor substitute for a report: the live probe behind
31-
* `objectql`'s `driver-fault-redaction.ts` measured Postgres's `22001` as
31+
* `types`' `driver-fault-redaction.ts` measured Postgres's `22001` as
3232
* identifier-only and naming the TYPE rather than the column (`value too long
3333
* for type character varying(255)`).
3434
*

‎packages/drivers/driver-sql/src/sql-driver-11455-aggregate-fault-envelope.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,19 @@ describe(`[#11455] driver-sql — aggregate() takes the backend-fault envelope (
241241
// THE LOG — a withholding, not a deletion
242242
// ───────────────────────────────────────────────────────────────
243243

244-
it('writes the full dialect text to the SERVER LOG, statement included', async () => {
244+
it('writes the dialect diagnostic to the SERVER LOG, the statement cut', async () => {
245245
const { err, logged } = await withLog(driver, () => driver.aggregate(MISSING_TABLE, COUNT_ALL));
246246
expect(err.code).toBe('DATABASE_ERROR');
247247
const line = logged.find((l) => l.includes(MISSING_TABLE));
248248
expect(line, 'an operator must still be able to read what the backend said').toBeDefined();
249-
expect(String(line)).toMatch(/\bselect\b/i);
249+
// [#21385, maintainer ruling 2026-10-02] A redaction, not a deletion, and
250+
// since this ruling a cut one: the dialect's diagnostic still reaches the
251+
// log for an operator, the statement and its bound values do not (a server
252+
// log leaves the data's trust boundary). The cut's marker says one stood
253+
// there. The sentinel pins for this line live in
254+
// `sql-driver-21385-refusal-log-line-redaction.test.ts`.
255+
expect(String(line)).not.toMatch(/\bselect\b/i);
256+
expect(String(line)).toContain('[statement and bound values redacted]');
250257
expect(String(line)).toContain('DATABASE_ERROR');
251258
});
252259

‎packages/drivers/driver-sql/src/sql-driver-11541-aggregate-unresolvable-column-refusal.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,12 @@ describe(`[#11541] driver-sql — aggregate() attributes an unresolvable column
274274
// The withholding half: the dialect's own words reach the operator.
275275
const line = logged.find((l) => l.includes('INVALID_FIELD') && l.includes('nosuchcol'));
276276
expect(line, 'the dialect message must reach the server log').toBeDefined();
277-
expect(String(line), 'the log carries the compiled statement').toMatch(/\bselect\b/i);
277+
// [#21385, maintainer ruling 2026-10-02] …cut: the compiled statement and
278+
// the literal bound into it no longer reach the log (a server log leaves
279+
// the data's trust boundary); the cut's marker says a statement stood there.
280+
expect(String(line), 'the log no longer carries the compiled statement').not.toMatch(/\bselect\b/i);
281+
expect(String(line).includes(SECRET_LITERAL), 'the bound literal reached the log').toBe(false);
282+
expect(String(line)).toContain('[statement and bound values redacted]');
278283
});
279284

280285
// ───────────────────────────────────────────────────────────────

‎packages/drivers/driver-sql/src/sql-driver-16019-raw-statement-fault-envelope.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,20 @@ describe('[#16019] SqlDriver.execute() declares a backend refusal as DATABASE_ER
139139
expect(looksLikeInternalErrorLeak('no such column: bogus_dim')).toBe(true);
140140
});
141141

142-
it('writes the statement and the dialect message to the server log — after this change, the only copy', async () => {
142+
it("writes the dialect's diagnostic to the server log, with the statement cut", async () => {
143143
await faultOf(() => driver.execute(TRANSLATE_SQL));
144144

145145
const line = driver.warned.find((m) => m.includes('DATABASE_ERROR'));
146146
expect(line).toBeDefined();
147147
expect(line).toContain('(SQLITE_ERROR)');
148-
expect(line).toContain(TRANSLATE_SQL);
149148
expect(line).toContain('no such function: translate');
149+
// [#21385, maintainer ruling 2026-10-02] The line used to write the
150+
// statement too, sent and compiled. Both carry the values a raw statement
151+
// binds or spells inline, and a server log leaves the data's trust
152+
// boundary, so neither is written: the cut's marker stands in their place.
153+
expect(line).not.toContain(TRANSLATE_SQL);
154+
expect(line).not.toContain('statement: ');
155+
expect(line).toContain('[statement and bound values redacted]');
150156
});
151157

152158
it('a missing table on the raw path stays classifiable through `cause` (isMissingTableError)', async () => {

‎packages/drivers/driver-sql/src/sql-driver-17639-distinct-fault-envelope.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,19 @@ describe(`[#17639] driver-sql — distinct() takes the backend-fault envelope ($
245245
// THE LOG — a withholding, not a deletion
246246
// ───────────────────────────────────────────────────────────────
247247

248-
it('writes the full dialect text to the SERVER LOG, statement included', async () => {
248+
it('writes the dialect diagnostic to the SERVER LOG, the statement cut', async () => {
249249
const { err, logged } = await withLog(driver, () => driver.distinct(MISSING_TABLE, 'title'));
250250
expect(err.code).toBe('DATABASE_ERROR');
251251
const line = logged.find((l) => l.includes(MISSING_TABLE));
252252
expect(line, 'an operator must still be able to read what the backend said').toBeDefined();
253-
expect(String(line)).toMatch(/\bselect\b/i);
253+
// [#21385, maintainer ruling 2026-10-02] A redaction, not a deletion, and
254+
// since this ruling a cut one: the dialect's diagnostic still reaches the
255+
// log for an operator, the statement and its bound values do not (a server
256+
// log leaves the data's trust boundary). The cut's marker says one stood
257+
// there. The sentinel pins for this line live in
258+
// `sql-driver-21385-refusal-log-line-redaction.test.ts`.
259+
expect(String(line)).not.toMatch(/\bselect\b/i);
260+
expect(String(line)).toContain('[statement and bound values redacted]');
254261
expect(String(line)).toContain('DATABASE_ERROR');
255262
});
256263

‎packages/drivers/driver-sql/src/sql-driver-17857-distinct-unresolvable-column-refusal.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,19 @@ describe(`[#17857] driver-sql — distinct() attributes an unresolvable column (
251251
expect(err.code).not.toBe('DATABASE_ERROR');
252252
});
253253

254-
it('arm 1: writes the full dialect text to the SERVER LOG, statement included', async () => {
254+
it('arm 1: writes the dialect diagnostic to the SERVER LOG, the statement cut', async () => {
255255
const { err, logged } = await withLog(driver, () => driver.distinct(TABLE, MISSING_COLUMN));
256256
expect(err.code).toBe('INVALID_FIELD');
257257
const line = logged.find((l) => l.includes(MISSING_COLUMN));
258258
expect(line, 'an operator must still be able to read what the backend said').toBeDefined();
259-
expect(String(line), 'the withheld statement is in the log').toMatch(/\bselect\b/i);
259+
// [#21385, maintainer ruling 2026-10-02] A redaction, not a deletion, and
260+
// since this ruling a cut one: the dialect's diagnostic still reaches the
261+
// log for an operator, the statement and its bound values do not (a server
262+
// log leaves the data's trust boundary). The cut's marker says one stood
263+
// there. The sentinel pins for this line live in
264+
// `sql-driver-21385-refusal-log-line-redaction.test.ts`.
265+
expect(String(line), 'the withheld statement is cut from the log').not.toMatch(/\bselect\b/i);
266+
expect(String(line), 'the cut says a statement stood there').toContain('[statement and bound values redacted]');
260267
expect(String(line), 'the log line names the envelope it produced').toContain('INVALID_FIELD');
261268
});
262269

0 commit comments

Comments
 (0)