Skip to content

refact(remote): normalize identifiers through PgIdentifier, fold the baseline - #234

Merged
veksen merged 1 commit into
mainfrom
refact-drift-column-check
Aug 14, 2026
Merged

refact(remote): normalize identifiers through PgIdentifier, fold the baseline#234
veksen merged 1 commit into
mainfrom
refact-drift-column-check

Conversation

@veksen

@veksen veksen commented Aug 14, 2026

Copy link
Copy Markdown
Member

Goal

Keep the statistics-refresh trigger cheap to read, so the next person changing it can see what each comparison is for.

Follow-up cleanup on #233, which added the column check. Quality only, no new capability.

What

Nothing observable changes for ordinary schemas. A snapshot that covers a table's columns still reports no drift, and a migration that adds one still earns a re-dump.

One case changes. Two columns whose names differ only in how many double quotes they hold, such as we"ird and we""ird, keyed to the same entry, so the second read as covered when it was not. They are now distinct.

How

Read src/remote/stats-drift.ts. remote.ts is two lines, a signature and a comment.

Identifiers normalize through PgIdentifier. unquote took String(name), which is the emit path and re-escapes, then undid however many levels of escaping it found. PgIdentifier.unquoted() is the accessor for reading a name rather than emitting one, and its doc comment says to use it instead of unpicking toString(). It leaves exactly one level of quote_ident doubling, so it pairs with a single replaceAll. Core does the same, for the same asymmetry, in sql/foreign-keys.ts. Undoing one level per side is also what keeps we"ird and we""ird apart, which collapsing to a fixed point cannot.

StatsBaseline folds into one map. It held tables, reltuples and columns, all keyed by the same table key, all written in the same loop, with identical key sets by construction and nothing enforcing it. It is now one Map<TableKey, {reltuples, columns}>. Both new ?. reads already had their undefined guard on the next line.

The three Shape Drift checks share a verdict builder. They built the same object three times, differing in a noun phrase. uncoveredColumns returns the list rather than a formatted sentence, which gives it the same shape as the two table scans, and the order reads as one chain.

Two comments contradicted the code. The module doc said every comparison runs in one direction, which the dropped-table check does not; it is now scoped to columns, with the asymmetry explained. The remote.ts call site said Shape Drift reads columns and indexes, and it reads only columns.

The uncoveredColumns doc now records that partitioned tables get no column check: SCHEMA_DUMP_SQL filters relispartition = false and relkind in ('r','m'), so neither a partitioned parent nor its partitions reach the comparison. Tracked in Query-Doctor/Site#4008.

Net 35 lines removed, one test added.

Tests

npm run typecheck clean. Full suite 43 files, 455 tests, all passing.

One test added: two names differing only in quote count stay distinct.

Three tests in this area are mutation-checked. Removing the single un-doubling fails the embedded-quote case, reversing the check order fails the table-before-column case, and collapsing quotes to a fixed point fails the new distinctness case.

Out of scope, both needing a core release. tableKey and unquote are duplicated in core's optimizer/synthesize-reltuples.ts, where the copy strips one level and so mis-keys a table named we"ird, tracked in Query-Doctor/Site#4009. table() is a third copy of a fixture that apply-statistics.test.ts and seed-stats-baseline.test.ts already share, and belongs in test-utils.ts.

…baseline

The column check hand-rolled its identifier unquoting: String(name), then a
fixed-point loop undoing however many levels of quote escaping that had
applied. PgIdentifier.unquoted() is the accessor for reading a name rather
than emitting one, and its doc says so. Core already pairs it with a single
un-doubling in sql/foreign-keys.ts, for this exact asymmetry.

Undoing exactly one level per side is also more correct than the fixed
point, which folded `we""ird` and `we"ird` into the same key and reported
the second as covered. Test added for the distinction.

StatsBaseline held three structures keyed by the same table key, whose key
sets were identical by construction but not by type. One Map of a record
deletes a field rather than adding a third.

The three Shape Drift checks built the same verdict literal three times,
differing in a noun phrase. They now share one builder, and the check order
reads as one chain.

The module doc claimed every comparison runs in one direction, which the
dropped-table check contradicts. Scoped to columns, with the asymmetry
named. The call-site comment in remote.ts claimed Shape Drift reads indexes,
which it does not.

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query Doctor — 6 successful checks

 Cost regression — No query went up more than 5%
 Untested data access — No changed data-access file without a test
 New query — No new queries
 New query with index recommendation — No new query ships an index recommendation
 Schema drift — No schema changes
 High-value nudge — No index or rewrite past the threshold


More details via MCP → get_ci_run({ runId: "019ffe0b-cb4c-74f6-a140-e2ca38fe1cab" }) · view run · docs
3 queries read against main on assumed statistics of 10,000,000 rows per table. Sync production stats for costs measured against your real data.

@veksen
veksen merged commit e04d1a4 into main Aug 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant