fix(pg_introspect): read FKs from pg_constraint so read-only roles get 'references' edges (#1746)#1761
Conversation
…t 'references' edges (Graphify-Labs#1746) information_schema.referential_constraints only shows constraints where the current user has WRITE access to the referencing table (owner or a privilege other than SELECT — per the PostgreSQL docs). A read-only introspection role therefore gets zero FK rows, while tables, views and routines all still appear (SELECT is enough for those views) — so --postgres built a graph with contains/reads_from edges but not a single 'references' edge, exactly as reported. Query pg_catalog.pg_constraint (contype = 'f') instead, which is not privilege-filtered. This also fixes a latent second bug: constraint names in PostgreSQL are only unique per table, so the old name-based key_column_usage joins cross-matched same-named constraints on sibling tables (verified live: two tables sharing a 'fk_owner' constraint produced columns = {user_id,user_id,user_id,user_id}). pg_constraint keys by oid, and conkey/confkey unnest WITH ORDINALITY preserves composite-FK column order. Verified against a live PostgreSQL 16 with a SELECT-only role: referential_constraints returned 0 FK rows, the new query returned all 3 (single-column, composite, and same-named sibling constraints), and introspect_postgres emitted all 3 references edges end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Merged on v8, authored to you. The root cause is exactly right: |
Fixes #1746.
Root cause
introspect_postgresreads foreign keys frominformation_schema.referential_constraints— but per the PostgreSQL docs:A read-only introspection role — exactly what hosted providers like Supabase hand out, and what
pg_introspect's ownREAD ONLYtransaction encourages — therefore gets zero FK rows, while tables/views/routines all still appear (SELECT is enough for those views). The graph then builds "successfully" withcontains/reads_fromedges but not a singlereferencesedge, matching the reported counts (459 contains + 183 reads_from, 0 references). This is also why file-based extraction of the same schema's migrations does producereferencesedges — the gap is specific to the live-introspection path.Reproduced live on PostgreSQL 16 with a SELECT-only role:
referential_constraints→ 0 rows;pg_catalog.pg_constraint(contype='f') → all 3.Fix
Query
pg_catalog.pg_constraintinstead — system catalogs are not privilege-filtered. The query returns the same 7-column shape, so DDL generation is untouched.This also fixes a latent second bug: PostgreSQL constraint names are only unique per table, and the old name-based
key_column_usagejoins cross-matched same-named constraints on sibling tables. Verified live: two tables each with a constraint namedfk_ownerproducedcolumns = {user_id,user_id,user_id,user_id}(malformed FK DDL) even for a privileged user.pg_constraintkeys by oid, andunnest(conkey) WITH ORDINALITYpreserves composite-FK column order.Verification
fk_ownerconstraints on two sibling tables, introspected as a SELECT-only role: all 3referencesedges emitted end-to-end, composite FK produces exactly one edge, bothfk_ownerFKs attributed to the correct tables.referential_constraintsand still yields thereferencesedge; the test mock now records executed queries.tests/test_pg_introspect.py7/7, plustest_extract.py+test_multilang.py(172 total) green; ruff clean.🤖 Generated with Claude Code