Skip to content

driver-sql: introspectPrimaryKeys / ForeignKeys / UniqueConstraints still swallow every error and report ABSENCE — the #7332 ruling was applied to introspectIndexes only #11161

Description

@os-zhuang

Split out of #11101, which repaired the PG/MySQL key ordering and was explicitly forbidden from touching this. Filed with the evidence that card's work produced.

What

Three sibling introspection methods in packages/drivers/driver-sql/src/sql-driver.ts wrap their entire body in a bare catch and return an empty collection:

protectedasyncintrospectPrimaryKeys(tableName: string): Promise<string[]>{constprimaryKeys: string[]=[];try{/* … per-dialect query … */}catch{// silently ignore}returnprimaryKeys;// ← [] is indistinguishable from "this table has no primary key"}

introspectForeignKeys (// silently ignore introspection errors) and introspectUniqueConstraints carry the same shape.

Why this is a finding and not a style note

The repo has already ruled on exactly this question, one method over.#7332 fixed introspectIndexes and the ruling is still in the source, in its own words:

introspectIndexes(tableName,opts: {/** * What a failed read means to THIS caller (#7332). `'throw'` (the * default) surfaces it; `'partial'` returns whatever was read before the * failure — correct only where a short read is self-correcting. */onFailure?: 'throw'|'partial';}={})

and at its one 'partial'-shaped call site:

}catch{// #7332's direction, for the same reason: a failed read is not evidence// of an absent index, and this caller's whole output is a refusal.returnnull;}

introspectIndexes therefore throws by default, and a caller that wants a short read must ask for it by name. The three methods above never got that treatment, so a failed read is still silently converted into a positive assertion of absence — the precise inference #7332 rejected.

Why it matters more for primaryKeys than for indexes

primaryKeys is consumed as an addressing / upsert-conflict-target key: federated-object codegen, the persisted external_catalog under ADR-0015, and schema-drift comparison against a declared key. [] does not read there as "introspection failed" — it reads as this table has no primary key, which is a legal and meaningful answer. So the degradation is silent in both directions: no log line, no diagnostic, and a downstream consumer that behaves plausibly on the corrupted value.

Measured evidence from #11101

This is the concrete hazard the #11101 work ran into, and it is why that card's test had to be written the unusual way it was.

While developing the Postgres rewrite (unnest(i.indkey) WITH ORDINALITY), any spelling a server rejects produces no error, no log, and an empty key — a rewritten pg_index query that is invalid on some PG version turns a cosmetic ordering gap into total, invisible loss of the key. Verified against a live PostgreSQL 16.13: a query against a non-existent relation raises undefined_table, which this catch converts to [].

That is also why #11101's pin asserts the exact ordered array rather than "does not throw" or set membership — under this catch, a does-not-throw test stays green over total key loss. From that test file:

An EMPTY result usually means the dialect arm's query was rejected by
the server and swallowed by the method's `catch { }` — read the query, not this fixture.

Every test in the repo that touches these three methods has to be written defensively around this catch. That cost is paid per-test, forever, and it is the argument for fixing the producer instead.

Decision needed (not folded into #11101 on purpose)

Swallow-to-throw is an error-contract change with its own blast radiusintrospectSchema calls all four methods per table in a loop, so making these throw changes what a partially-readable database does to a whole-schema introspection. That deserves its own ruling rather than a rider on an ordering fix.

Options, roughly in the shape #7332 already established:

  • A. Adopt the driver-sql: introspectIndexes swallows every error and returns a partial index list, which the drift differ then reports as missing indexes #7332 contract verbatimonFailure?: 'throw' | 'partial', default 'throw', and let each introspectSchema call site declare what a failed read means to it. Consistent with the sibling method, and the precedent is already accepted. Most work.
  • B. Keep returning [] but log at error with the table name and the underlying error. Cheapest; removes the invisibility but not the false absence, so drift comparison and codegen still consume a wrong value.
  • C. Distinguish "read failed" from "no key" in the return type (e.g. string[] | null), forcing every consumer to handle it. Cleanest signal, widest call-site churn.

A is the recommendation on consistency grounds alone: the question was answered next door and the answer is quoted in the same file.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions