Skip to content

fix(pg_introspect): emit FK DDL before function stubs so unparseable routines can't drop references edges (#1854)#1927

Open
sekmur wants to merge 1 commit into
Graphify-Labs:v8from
sekmur:fix/pg-introspect-fk-order-1854
Open

fix(pg_introspect): emit FK DDL before function stubs so unparseable routines can't drop references edges (#1854)#1927
sekmur wants to merge 1 commit into
Graphify-Labs:v8from
sekmur:fix/pg-introspect-fk-order-1854

Conversation

@sekmur

@sekmur sekmur commented Jul 15, 2026

Copy link
Copy Markdown

Problem

The dropped-FK half of #1854 (closed as fixed-by-#1746, but reproducible on a fresh 0.9.16 — graphify/pg_introspect.py and graphify/extractors/sql.py are byte-identical from v0.9.14 through v0.9.16).

introspect_postgres() reconstructs one synthetic DDL document in the order tables → views → functions → FKs and hands it to extract_sql() in a single tree-sitter parse. A C-language routine's routine_definition is just the C symbol name, so its stub:

CREATE FUNCTION "public"."levenshtein"() RETURNS void AS $gfx$ levenshtein $gfx$ LANGUAGE c;

is unparseable, and tree-sitter's error recovery consumes the statements after it. With the FK ALTER TABLEs emitted last, they're what gets eaten — so any DB with a common extension installed (uuid-ossp, pgcrypto, pg_trgm, fuzzystrmatch, unaccent, PostGIS, …) silently loses its references edges. This is distinct from #1746 (FK reading privileges): the failure is downstream of a correct catalog read, in the parse.

Fix

Emit the FK block before the routine DDL — a pure block move. FK statements only reference tables, which are emitted first, so the order is always safe; an unparseable stub can now only damage what follows it (the remaining stubs, which contribute no edges from their bodies anyway).

Measured on a real DB (35 tables / 41 FKs / 41 extension routines from fuzzystrmatch + pg_trgm): 1/41 references edges before, 41/41 after.

Test

test_pg_introspect_fk_edges_survive_unparseable_function_stubs — 7 tables, 6 FKs, one LANGUAGE c routine + one plpgsql routine, asserts all 6 references edges survive. Fails on current v8 (0 or partial edges, statement-mix-dependent), passes with the fix. The existing baseline test never caught this because its mock routines only use SQL/PLPGSQL languages, never C.

Longer term this path may not need a parse at all — introspect_postgres() already holds tables and FK pairs as structured rows from pg_catalog, so emitting nodes/edges directly (instead of round-tripping through synthetic DDL) would make this failure class impossible. This PR is the minimal ordering fix; happy to discuss the direct-emission approach separately.

🤖 Generated with Claude Code

…routines can't drop references edges (Graphify-Labs#1854)

A C-language routine's body from information_schema.routines is just the C
symbol name, so its reconstructed stub (CREATE FUNCTION ... AS $gfx$ name
$gfx$ LANGUAGE c) is unparseable by tree-sitter-sql, and the parser's error
recovery consumes the statements that follow. With FK ALTER TABLEs emitted
last, every references edge was silently lost on any DB with a common
extension installed (uuid-ossp, pgcrypto, pg_trgm, fuzzystrmatch, ...).

Emit the FK block before the routine DDL. FK statements only reference
tables, which are emitted first, so the order is always safe. On a real DB
with 35 tables / 41 FKs / 41 extension routines: 41/41 references edges vs
1/41 before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant