Skip to content

fix(pg-meta): order primary key columns by key position and exclude INCLUDE columns - #382

Open
Rjabov wants to merge 1 commit into
supabase:mainfrom
Rjabov:fix/pg-meta-primary-key-order
Open

fix(pg-meta): order primary key columns by key position and exclude INCLUDE columns#382
Rjabov wants to merge 1 commit into
supabase:mainfrom
Rjabov:fix/pg-meta-primary-key-order

Conversation

@Rjabov

@Rjabov Rjabov commented Aug 25, 2026

Copy link
Copy Markdown

Closes #381

What kind of change does this PR introduce?

Bug fix: data-integrity issue in list_tables (verbose) primary key output.

What is the current behavior?

primary_keys is sorted by each column's physical position in the table rather than its position in the key, and it includes the non-key INCLUDE payload columns of the backing index.

create table membership (
  org_id  int not null,          -- physically column 1
  user_id int not null,          -- physically column 2
  role    text,
  primary key (user_id, org_id)
);

create table inc (a int not null, b int not null, c int not null);
create unique index inc_pk_idx on inc (a) include (b);
alter table inc add constraint inc_pk primary key using index inc_pk_idx;
table declared reported
membership (user_id, org_id) ["org_id","user_id"]
inc (a) INCLUDE (b) ["a","b"]

Root cause in pg-meta/tables.sql: a.attnum = any (i.indkey) is a set-membership test, so each column's position within the key is discarded and the enclosing jsonb_agg has no order by. indkey also spans indnatts, which includes the INCLUDE payload, rather than indnkeyatts.

This is the same defect #317 fixed in the foreign key subquery immediately below, which walks columns with unnest(...) with ordinality and aggregates order by cols.ord. The primary key subquery was not updated at the time.

What is the new behavior?

  • indkey is unnested with ordinality and the aggregate is ordered by that ordinal, so columns come back in constraint-definition order - not attnum, not alphabetical.
  • The unnest is bounded by indnkeyatts, so INCLUDE payload columns are excluded.
  • The implicit comma joins become explicit joins, which is what makes the lateral unnest readable. Output shape is unchanged: to_jsonb(_pk) - 'ord' emits exactly the same keys as before.
  • A comment records why = any (indkey) is wrong, so it doesn't get "simplified" back.

Two regression tests, both using a key order that is deliberately the reverse of the physical column order so any regression to attnum ordering fails immediately - the same shape as the FK ordering test added in #317.

Verification

Windows 11, Node 22 LTS, pnpm 10:

pnpm run build                            -> success
CI=true pnpm vitest run --project unit    -> 212 passed | 5 failed (217)
pnpm typecheck                            -> clean
npx @biomejs/biome@1.9.4 ci .             -> Checked 89 files. No fixes applied.

Both new tests pass:

✓ tools > composite primary key preserves constraint-definition column order
✓ tools > primary key excludes non-key INCLUDE columns

The 5 failures are pre-existing and unrelated to this change. They are all normalizeFilename: edge-function.ts imports resolve from node:path, which is platform-dispatched, but strips a hardcoded POSIX prefix (/tmp/user_fn_.../), so on Windows the strip never matches and the full C:\tmp\... path is returned. 3 direct failures in edge-function.test.ts plus 2 downstream in server.test.ts (list edge functions, get edge function). I confirmed the identical 5 failures on clean upstream/main with nothing applied, so they are the baseline, not a regression. Happy to file that separately - node:path/posix looks like the one-line fix, and every caller already forces POSIX semantics with fileURLToPath(..., { windows: false }).

End to end against a real MCP client and server (createSupabaseMcpServer over StreamTransport, PostgreSQL 16.4 via PGlite), calling list_tables with verbose: true:

before   membership  declared (user_id, org_id)   ->  ["org_id","user_id"]
         triple      declared (r, q, p)           ->  ["p","q","r"]
         inc         declared (a) INCLUDE (b)     ->  ["a","b"]

after    membership                               ->  ["user_id","org_id"]
         triple                                   ->  ["r","q","p"]
         inc                                      ->  ["a"]

Single-column primary keys and the composite foreign key output from #317 are unchanged on both builds.

On whether this actually misleads a model

I tested it rather than asserting it, and the result is mostly negative, so it's worth stating plainly: across 18 runs (3 models × 3 questions × before/after, one replicate per cell) 17 answered correctly on both builds. Models generally did not trust primary_keys - 17 of 18 runs called execute_sql and read pg_index/pg_constraint directly. Two runs spontaneously flagged the output as wrong, e.g. "list_tables reports the primary key as ["a","b"], which is misleading: it lumps the INCLUDE column in with the real key column."

The one failure was a weaker model on "what's the primary key on membership?" against the unfixed build, which answered org_id, user_id. Notably its own verification query used information_schema.key_column_usage without order by ordinal_position, so it confirmed the wrong answer.

So the case for this fix is not "agents give wrong answers". It's that primary_keys contradicts the database, that models are paying for a catalog round-trip list_tables exists to save, and that a non-agent consumer of the published package has no fallback at all.

What I did not test

  • Only against PGlite (PostgreSQL 16.4) and the repo's test harness, not a live Supabase project. indnkeyatts requires PG 11+.
  • One replicate per cell in the model matrix, and those runners were agents with shell access, which lean toward verifying - a plain chat client may lean on list_tables more than these did.

@Rjabov
Rjabov requested a review from a team as a code owner August 25, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

list_tables reports primary key columns in attnum order, and includes non-key INCLUDE columns

1 participant