Skip to content

driver-sql (PG): introspectPrimaryKeys reports a covering primary key's INCLUDE'd columns as key members — indkey is read whole, indnkeyatts is ignored #11162

Description

@os-zhuang

Found while working #11101 (which repaired the ordering of the PG/MySQL key). This is a membership defect in the same method — a different defect class, so it was deliberately not folded in. Measured on a live PostgreSQL 16.13.

What

packages/drivers/driver-sql/src/sql-driver.ts, SqlDriver.introspectPrimaryKeys, Postgres arm. The query reads all of pg_index.indkey. For a covering primary key, indkey holds the key columns and the INCLUDEd payload columns; indnkeyatts is the count of the leading entries that are actually key columns, and it is not consulted.

Postgres reaches this via CREATE UNIQUE INDEX … INCLUDE (…) promoted with ALTER TABLE … ADD CONSTRAINT … PRIMARY KEY USING INDEX.

Measured (PostgreSQL 16.13)

CREATETABLEcovered (k1 textNOT NULL, k2 textNOT NULL, payload text);
CREATEUNIQUE INDEXcovered_pkON covered (k2, k1) INCLUDE (payload);
ALTERTABLE covered ADD CONSTRAINT covered_pkey PRIMARY KEY USING INDEX covered_pk;
 indkey | indnatts | indnkeyatts
--------+----------+-------------
2 1 3 | 3 | 2

The declared key is (k2, k1). payload (attnum 3) is a payload column, not a key column — indnkeyatts = 2 says so.

Both the pre-#11101 query and the post-#11101 query report it as a key member:

--- OLD query (membership test, a.attnum = ANY(i.indkey)) ---
k1
k2
payload
--- NEW query (#11101, joins the ordinality of indkey) ---
k2
k1
payload

#11101 neither introduced nor worsened this — the two arms agree on membership and differ only in order, which is why it was left alone there. The payload entry is wrong in both.

Why it matters

primaryKeys is consumed as an addressing / upsert-conflict-target key (federated-object codegen, the persisted external_catalog under ADR-0015, schema-drift comparison). A key with an extra member is a different key: an upsert conflict target naming a non-key column does not match the constraint, and drift comparison against a correctly-declared (k2, k1) reports a phantom unexpected_key_member:payload.

Likely shape of the fix

Bound the join to the key attributes: WHERE k.ord <= i.indnkeyatts (or unnest(i.indkey[0:i.indnkeyatts-1]) — note int2vector is 0-based). indnkeyatts exists in PG 11+; below that it equals indnatts and the bound is a no-op.

Worth checking the MySQL and SQLite arms for the analogous question before fixing, so all three dialects keep the single answer #11101 just established. MySQL has no covering-index concept for a PRIMARY KEY, so it is likely PG-only.

Not measured here

Whether any in-tree consumer currently introspects a covering primary key. The repo's own schema sync does not create them; this reaches a remote table under federation (ADR-0015), where the DDL is not ours.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions