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

Closed

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)

CREATE TABLE covered (k1 text NOT NULL, k2 text NOT NULL, payload text);
CREATE UNIQUE INDEX covered_pk ON covered (k2, k1) INCLUDE (payload);
ALTER TABLE 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.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 22, 2026
  2. added theissue type on Aug 23, 2026
  3. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue, domain:engine, Bug — see the family-anchor comment on #11161 (driver-sql introspection trio; prefer one claim covering all three, region-declared against in-flight #11067/#11176).


    Generated by Claude Code

  4. added a commit that references this issue on Aug 23, 2026
    9c71640
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 23, 2026
  6. self-assigned this
    on Aug 23, 2026
  7. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Claim (late — PM bookkeeping correction) · session session_01RfyXxZ2WPjcjhuXpiQQc3y · branch claude/issue-11161-introspection-trio

    This card was dispatched as part of the introspection trio (#11161 / #11162 / #11163, one file region in packages/drivers/driver-sql/src/sql-driver.ts, one commit per issue). The work is in PR #11203, which carries Fixes #11162.

    The label pair was not written at dispatch time: the card stayed pm:queue + unassigned for the duration of the run, which advertised it to the lane as dispatchable while an open PR already covered it. Corrected now to pm:dispatched + assignee. No work state changed — only the card's honesty about it.

    For the record of what the PR measured on this card specifically (live PG 16.13, fixture (k1, k2, payload) with CREATE UNIQUE INDEX ... (k2, k1) INCLUDE (payload) promoted via ADD CONSTRAINT ... PRIMARY KEY USING INDEX): catalog reports indkey = '2 1 3', indnatts = 3, indnkeyatts = 2; before the fix the introspected key was k2, k1, payload, after it is k2, k1. Reverse verification took 2 reds on this card's legs.


    Generated by Claude Code

  8. added a commit that references this issue on Aug 23, 2026
    9cc1940
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions