Skip to content

The #8682 write-path redaction keeps a caller's VALUE in the log on MySQL — Duplicate entry 'acme@example.com' for key … survives the cut, measured through the shipped function #8823

Description

@qq9340100

Blocked-by: #8739

Filed unassigned. Surfaced while correcting the dialect claim on #8739 — it is the residual that claim was used to size, so it is filed with the evidence rather than left as a remark in a report. Duplicate-searched first (keyword + file path over open issues): no existing card.

What was measured

redactStatementFromMessage (packages/objectql/src/driver-fault-redaction.ts, landed by PR #8737 for #8682) keeps everything after the LAST - in a knex-shaped driver message — the database's own diagnostic — and replaces the bound statement. Driven directly through the shipped function on the current main worktree, four dialect-shaped messages in, kept tail out:

mysql unknown column => "Unknown column 'zzz' in 'field list' [statement and bound values redacted]"
mysql duplicate entry => "Duplicate entry 'acme@example.com' for key 'crm_account.email' [statement and bound values redacted]"
sqlite unique violation => "UNIQUE constraint failed: crm_account.email [statement and bound values redacted]"
pg unique violation => "duplicate key value violates unique constraint \"crm_account_email_key\" [statement and bound values redacted]"

Three of the four keep an IDENTIFIER — a column, a table, a constraint name. The MySQL duplicate-entry row keeps a caller-supplied value, because that is what mysql2 puts in the diagnostic itself rather than in the statement. The cut is working exactly as designed; the design's premise is that the tail names identifiers, and on this one family and this one dialect it does not.

The MySQL phrasing is not invented here. It is recorded verbatim in this repo already: packages/types/src/unique-violation.ts (ER_DUP_ENTRY: Duplicate entry 'acme@example.com' for key 'idx_email_unique') and pinned as a fixture in packages/types/src/unbacked-conflict-target.test.ts.

Why the dialect matters, and why this is not a rerun of #8739

PR #8737 sized this residual explicitly, and its stated reason was that error-leak.ts "records that this repo runs only SQLite/libsql and Postgres". #8739 established that reason was false. What the measurement above adds is that the reason was also load-bearing for this family: on the two dialects the PR named, the kept tail carries no caller value; on MySQL it does. The conclusion held for the canary the PR actually measured (unknown-column, which names an identifier on MySQL too) and does not extend to the unique-violation path.

The asymmetry is worth stating precisely, because it is not obvious:

  • Postgres does NOT leak here, but not because of the cut. Its value lives in the DETAIL: line, which node-postgres keeps on error.detail — a field Logger does not serialize, since it writes only message and stack. Different mechanism, same outcome.
  • SQLite does not leak because its diagnostic names table.column and stops.
  • MySQL leaks because the value is in the native message.

Not claimed

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions