Skip to content

driver-sql (PG): introspectForeignKeys' JOIN correlations are wrong in two ways — a cross-schema FK target vanishes, and a composite FK is returned as a cartesian product #11324

Description

@os-zhuang

Found while working #11201 (which schema-scoped this query's WHERE, not its JOINs). Not fixed there: the bounded in-place-fix exemption did not apply — these are join-correlation defects, a different class from the unscoped-read defect that card owns, and each needs its own fixture. #11201 is a separate subject and is not addressed by this issue.

Both facts below were measured on a live PostgreSQL 16.13, against the query as it stands after#11201's fix — so neither is caused by, nor repaired by, that change.

What

packages/drivers/driver-sql/src/sql-driver.ts, SqlDriver.introspectForeignKeys, Postgres arm. The query joins:

FROMinformation_schema.table_constraintsAS tc
JOINinformation_schema.key_column_usageAS kcu
ONtc.constraint_name=kcu.constraint_nameANDtc.table_schema=kcu.table_schemaJOINinformation_schema.constraint_column_usageAS ccu
ONccu.constraint_name=tc.constraint_nameANDccu.table_schema=tc.table_schema

For a FOREIGN KEY constraint, constraint_column_usage describes the referenced (parent) side — that is exactly why the projection aliases it ccu.table_name AS referenced_table. Its table_schema is therefore the parent's schema, not the constraint's. Two consequences:

1. A foreign key whose target lives in another schema is dropped entirely

ccu.table_schema = tc.table_schema demands parent and child sit in the same schema. When they do not, the constraint contributes zero rows and disappears from the answer.

Measured, search_path = os11201_probe:

createtableos11201_probe_far.remote_parent (id varchar(64) primary key);
createtablecross_child (
id varchar(64) primary key, p varchar(64),
constraint fk_cross foreign key (p) referencesos11201_probe_far.remote_parent(id));

→ the arm's query returns 0 rows for cross_child. The table has a declared foreign key and introspectForeignKeys reports it has none.

This is the #7332 failure mode arriving through a different door: [] 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 that. Cross-schema references are the normal shape for the federated remotes ADR-0015 points this driver at.

The sibling arm already spells the correct correlation: introspectUniqueConstraints joins ccu on tc.constraint_schema = ccu.constraint_schema — the constraint's schema, which is invariant to where the parent lives.

2. A composite foreign key comes back as the cartesian product of its columns

The kcuccu join carries no ordinal_position correlation, so an N-column foreign key yields N x N rows pairing every child column with every parent column.

Measured:

createtablecomp_parent (a varchar(64), b varchar(64), primary key (a, b));
createtablecomp_child (
id varchar(64) primary key, x varchar(64), y varchar(64),
constraint fk_comp foreign key (x, y) references comp_parent(a, b));

4 rows returned for a 2-column key:

x -> comp_parent.a
x -> comp_parent.b
y -> comp_parent.a
y -> comp_parent.b

The correct answer is x -> a, y -> b. IntrospectedForeignKey is a flat per-column record, so the two phantom pairs are indistinguishable from real ones to every consumer.

This is the same class the primary-key arm already paid for twice: #11101 (row order unspecified because the key ordinal was neither selected nor ordered on) and #10997 (SQLite composite key truncated). The FK arm never got that pass.

Suggested shape

One rewrite likely closes both, and the in-tree precedent is introspectPrimaryKeys: drop information_schema for pg_constraint, whose conkey / confkey are parallel int2vectors, and join them by shared ordinality — the unnest(...) WITH ORDINALITY shape #11101/#11162 already established in this file. That yields the ordered, correctly-paired column list directly, carries the parent's schema without constraining it, and removes the information_schema privilege-view layer these correlations were fighting.

Alternatively, and more conservatively: correlate ccu on constraint_schema (as introspectUniqueConstraints does) and add the kcu.ordinal_position ↔ referenced-ordinal correlation. Note constraint_column_usage exposes no ordinal, so the composite half probably cannot be fixed inside information_schema at all — which is the argument for the pg_constraint rewrite.

Scope note

Whether IntrospectedForeignKey needs a shape change is not settled here and should not be assumed: the flat per-column record can express a composite key correctly as ordered sibling rows, but nothing currently pins their order. That interface is being edited by #11270, so a fix touching its declaration needs to be sequenced against that PR rather than racing it.

Not measured here

Whether any in-tree consumer currently hits either shape. Both were reproduced directly against the driver's own query on a live server; neither depends on a consumer to be wrong.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions