Skip to content

Commit 80f1dcd

Browse files
os-zhuangclaude
andauthored
fix(driver-sql): correlate PG introspectForeignKeys on the constraint and the key ordinal (#11396)
* fix(driver-sql): correlate PG introspectForeignKeys on the constraint and the key ordinal Rewrites the Postgres arm of `SqlDriver.introspectForeignKeys` onto `pg_constraint`, replacing a three-view `information_schema` join that was wrong in two independent ways (both measured on live PostgreSQL 16.13): 1. `ccu.table_schema = tc.table_schema` demanded parent and child share a schema. `constraint_column_usage` describes the REFERENCED side of a foreign key, so its `table_schema` is the parent's — a cross-schema target contributed zero rows and the table reported having no foreign keys at all. 2. The kcu/ccu join carried no ordinal correlation, so an N-column key came back as the N x N cartesian product (a 2-column key measured as 4 rows). `constraint_column_usage` exposes no ordinal column, so defect 2 has nothing to correlate on inside `information_schema`; correlating on `tc.constraint_schema` (the spelling `introspectUniqueConstraints` carries) was measured to fix defect 1 and leave defect 2 at 4 rows. `pg_constraint` carries both facts on one row: `conkey`/`confkey` are parallel `smallint[]`s in key order, so `unnest(...) WITH ORDINALITY` — the shape `introspectPrimaryKeys` already uses for `indkey` — pairs child column with parent column by construction and pins the key order. `IntrospectedForeignKey`'s shape is unchanged; its doc comment now states the ordered-sibling-rows contract the ORDER BY establishes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y * chore(driver-sql): changeset for the PG introspectForeignKeys join-correlation fix Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8d237b4 commit 80f1dcd

3 files changed

Lines changed: 488 additions & 33 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/driver-sql": patch
3+
---
4+
5+
**Fix:** `introspectForeignKeys`' Postgres arm no longer drops a cross-schema foreign key, nor returns a composite one as a cartesian product (#11324).
6+
7+
The arm joined three `information_schema` views, and the correlations were wrong in two independent ways. Both were measured on live PostgreSQL 16.13, against the query as it stood after #11201, so neither was caused by nor repaired by that change.
8+
9+
**A foreign key whose target lived in another schema vanished.** The join carried `ccu.table_schema = tc.table_schema`, which demands parent and child sit in the same schema. For a FOREIGN KEY constraint, `constraint_column_usage` describes the *referenced* side — that is exactly why the projection aliases it `referenced_table` — so its `table_schema` is the **parent's**, not the constraint's. A cross-schema reference therefore contributed **zero rows**, and the table reported having no foreign keys at all. That is the #7332 failure mode through a different door and it has no `onFailure` to consult, because nothing failed: `[]` does not read downstream as "I could not see it", it reads as *this table has no foreign keys*, and federated-object codegen, the persisted `external_catalog` (ADR-0015) and schema-drift comparison all act on it. Cross-schema references are the normal shape for the federated remotes ADR-0015 points this driver at.
10+
11+
**A composite foreign key came back as the cartesian product of its columns.** The `kcu``ccu` join carried no ordinal correlation at all, so an N-column key yielded N x N rows pairing every child column with every parent column. Measured, a 2-column key `(x, y) references p (a, b)` returned **four** records — `x -> a`, `x -> b`, `y -> a`, `y -> b` — where the answer is `x -> a`, `y -> b`. Because `IntrospectedForeignKey` is a flat per-column record, the two phantom pairs are indistinguishable from the real ones to every consumer: a wrong-shaped answer that type-checks.
12+
13+
**The whole query moves to `pg_constraint` rather than the join predicate being patched.** `constraint_column_usage` exposes no ordinal column at all — measured, its seven columns are the catalog/schema/name triples for the table and the constraint plus `column_name` — so the composite half has nothing to correlate on inside `information_schema`. The conservative half-fix was tried and measured: correlating `ccu` on `tc.constraint_schema`, the spelling `introspectUniqueConstraints` already carries, repairs the cross-schema case and leaves the composite case at four rows. `pg_constraint` carries both facts on one row — `conkey` and `confkey` are parallel `smallint[]`s in key order — so unnesting them *together* pairs child column with parent column by construction, and `unnest(...) WITH ORDINALITY` keeps the key position the old join threw away. That is the shape `introspectPrimaryKeys` already uses for `indkey` (#11101 / #11162), and dropping to the catalog matches what that arm and `introspectIndexes` already do.
14+
15+
**No interface change.** `IntrospectedForeignKey` keeps its flat per-column shape and gains no ordinal field. A composite key is expressed as **ordered sibling rows** — contiguous, in declared key order, each pairing its own child column with its own parent column — which `ORDER BY con.conname, con.oid, k.ord` now pins and the type's docblock now states. Measured on a key declared out of column sequence, `foreign key (second_col, first_col)`, the result is key order rather than column order. An ordinal field was considered and rejected: it would let a wrong `ORDER BY` keep shipping wrong rows that merely *describe* their wrongness, where the pairing is a fact the query itself has to get right.
16+
17+
Schema scoping is unchanged in meaning: `ns.nspname = ANY (current_schemas(false))` is #11201's `tc.table_schema = ANY (…)` expressed over the catalog, so a same-named table in a schema `search_path` never reaches still contributes nothing. An unknown table name still yields an empty list rather than a throw, so the #7332 `onFailure` contract is untouched.

0 commit comments

Comments
 (0)