You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
driver-sql: introspectPrimaryKeys / ForeignKeys / UniqueConstraints still swallow every error and report ABSENCE — the #7332 ruling was applied to introspectIndexes only #11161
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.
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 radius — introspectSchema 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:
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.
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.tswrap their entire body in a barecatchand return an empty collection:introspectForeignKeys(// silently ignore introspection errors) andintrospectUniqueConstraintscarry 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
introspectIndexesand the ruling is still in the source, in its own words:and at its one
'partial'-shaped call site:introspectIndexestherefore 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
primaryKeysthan for indexesprimaryKeysis consumed as an addressing / upsert-conflict-target key: federated-object codegen, the persistedexternal_catalogunder 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 rewrittenpg_indexquery 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 raisesundefined_table, which thiscatchconverts 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: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 radius —
introspectSchemacalls 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:
introspectIndexesswallows every error and returns a partial index list, which the drift differ then reports as missing indexes #7332 contract verbatim —onFailure?: 'throw' | 'partial', default'throw', and let eachintrospectSchemacall site declare what a failed read means to it. Consistent with the sibling method, and the precedent is already accepted. Most work.[]but log aterrorwith 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.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.