feat(sql): link SQL tables to the code that queries them (#2884) - #2891
feat(sql): link SQL tables to the code that queries them (#2884)#2891rajarshidattapy wants to merge 4 commits into
Conversation
…bs#2884) tree-sitter-sql extracted table declarations correctly and the host-language grammars extracted the application code correctly, but nothing connected the two: a table declared in db/schema.sql had no edge to the .ts file whose query reads `SELECT … FROM that_table`. Every table landed connected only to its declaring file (contains, degree 1), so the data layer floated free of the application and the graph could not answer "what breaks if I change this table?" — the question a schema-heavy project most wants from it. There is no AST route to this: the table name lives inside a template literal the TypeScript grammar sees only as a string. So the new resolver is lexical. After per-file extraction it scans every non-SQL source for each declared table name and emits a `references` edge (file -> table), EXTRACTED, confidence_score 1.0, to EVERY declaration site — schema.sql and the migration that created the table both — so tracing a table also reaches its migration history. Precision is the whole difficulty, and the reporter measured the trap: matching a table name on a word boundary while testing for SQL context file-wide gave 1,489 edges, of which all 130 `events` edges were JavaScript variables named `events`. Table names like users, events, sales and notifications are extremely common identifiers. The table must therefore sit in a SQL keyword position IN THE SAME MATCH — FROM/JOIN/INTO/UPDATE/ REFERENCES/TABLE — which brought that corpus to 921 hand-checked-clean edges. Matching runs over whole file text, not line by line, so a table named on the second line of a multi-line template literal still matches, and backtick / double-quote / bracket quoting is handled for MySQL, standard SQL and T-SQL. `calls` edges still stay within one language, as extraction-spec.md requires. `references` is a different relation with different semantics: "this file's SQL names this table" is a checkable claim, not an inferred call. Scope, matching the issue: file-level only, raw SQL in string literals only. Set GRAPHIFY_NO_SQL_LINKS=1 to skip the pass — an ORM-based repo names tables via model classes and gets little from it.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Graphify review — findings
Adds an SQL-to-host-language linkage resolver (graphify/sql_resolution.py, registered via sql_table_references in extract.py) that emits references edges from non-SQL source files to tables they name in SQL keyword positions. The pass is lexical, gated behind a 2MB per-file scan cap and a SQL-keyword pre-filter, and requires the table name to sit directly after FROM/JOIN/INTO/etc. to avoid matching ordinary identifiers. Adds GRAPHIFY_NO_SQL_LINKS to skip it (documented in the README) plus tests covering the TypeScript-query-to-schema case.
Worth a look
- extract() now emits SQL reference edges by default —
graphify/extract.py:3864· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1558 functions depend on the 253 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 479 callers, 42 callees - new:
_rebuild_code()— 98 callers, 51 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_js()— 80 callers, 3 callees - new:
dispatch_command()— 2 callers, 117 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 22 more — each is listed as a finding
Verification — 1558 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1415 function(s) in the blast radius were not formally verified this run
· 30 more finding(s) on lines outside this diff (see the check run).
Review flagged that extract() emits the new SQL reference edges by default. Keeping it on is deliberate — a schema the graph cannot see is the problem the issue reports, and every other language resolver is on by default too — but the only way to turn it off was GRAPHIFY_NO_SQL_LINKS, an env var a user has to already know exists. The issue itself asked for a flag. `--no-sql-links` sets that env var, the same bridge --api-timeout and --max-workers already use for settings consumed deep inside extract(), where there is no CLI argument to thread. The env var on its own still works. Documented in the usage line, the README flag list and the env table. Two tests: the flag suppresses the edges end to end through the CLI, and the default still emits them.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds a lexical SQL-linking resolver (graphify/sql_resolution.py) that emits references edges from application files to the .sql-declared tables their query text names in a keyword position, and registers it under sql_table_references. Wires up a --no-sql-links flag / GRAPHIFY_NO_SQL_LINKS env var in cli.py to skip the pass, keyword-position matching to suppress false hits on common identifiers, and a 2 MB scan cap to skip bundles. Documents both in the README and adds table-reference tests.
Worth a look
- Quoted identifier prefixes are falsely linked as table references —
graphify/sql_resolution.py:51· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1643 functions depend on the 305 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 479 callers, 42 callees - new:
_rebuild_code()— 98 callers, 51 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_js()— 80 callers, 3 callees - new:
dispatch_command()— 2 callers, 117 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 24 more — each is listed as a finding
Verification — 1643 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1600 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
· 32 more finding(s) on lines outside this diff (see the check run).
|
Thanks for this @rajarshidattapy — the architecture is right (resolver-registry wiring, the whole-file keyword pre-filter, the scan-size cap, and a clean --no-sql-links / GRAPHIFY_NO_SQL_LINKS opt-out are all sound), and unmatched tables correctly produce no edge. Two things need to change before it can land, both reproducible today:
Happy to re-review once those two are addressed. |
…inks # Conflicts: # README.md # graphify/cli.py
…#2884) Two defects from the Graphify-Labs#2884 review, both reproducible: Comments and prose minted edges. The matcher scanned raw file bytes, so a table name after a SQL keyword linked whether or not it was a query: `// SELECT foo FROM events`, `const s = "the FROM users keyword"` and a docstring saying "you can JOIN users with assets" all produced a phantom `references` edge. The match now has to sit inside a string literal (everything else is blanked, offsets preserved so line numbers hold) with a statement head — SELECT/INSERT/UPDATE/... — within 500 chars behind it. Blanking rather than extracting keeps Python implicit concatenation working. Schema-qualified tables never linked. `_declared_tables` required a bare identifier label, but the extractor stores labels verbatim, so `CREATE TABLE public.users` was labelled `public.users` and filtered out — a Postgres or T-SQL corpus got zero edges. Declarations are now keyed by the bare name via the extractor's own `_norm_ident`, and the reference pattern accepts an optional schema qualifier, so `FROM users`, `FROM public.users` and `FROM "public"."users"` all resolve.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds a SQL-table-to-code linking pass (resolve_sql_table_references in new graphify/sql_resolution.py) registered as the sql_table_references language resolver, emitting references edges from any non-SQL source whose string literals name a declared table in a real SQL keyword position near a statement head. Wires up --no-sql-links / GRAPHIFY_NO_SQL_LINKS in cli.py to skip the pass (with README docs), defaulting on. Also touches numerous existing resolvers, rationale symbols, and adds test_quoted_table_names_match.
Worth a look
- --no-sql-links mutates process-global environment without restoration —
graphify/cli.py:3092· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Identifier boundary treats
$as a delimiter —graphify/sql_resolution.py:93· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Schema-qualified references link to every table with the same bare name —
graphify/sql_resolution.py:245· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1682 functions depend on the 318 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 483 callers, 42 callees - new:
_rebuild_code()— 98 callers, 50 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_js()— 80 callers, 3 callees - new:
dispatch_command()— 2 callers, 119 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 25 more — each is listed as a finding
Verification — 1682 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1635 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
· 33 more finding(s) on lines outside this diff (see the check run).
Fixes #2884.
The gap
tree-sitter-sqlextracts table declarations correctly and the host-language grammars extract the application code correctly — but nothing connected the two. A table declared indb/schema.sqlhad no edge to the.tsfile whose query readsSELECT … FROM that_table, so every table landed connected only to its declaring file (contains, degree 1). On the reporter's corpus that was 65 orphaned SQL symbol nodes, withdb/never bridging a single community despite being queried from ~100 route files.The practical cost: the graph could not answer "what breaks if I change this table?" — the question a schema-heavy project most wants from a knowledge graph.
There is no AST route to it. The table name lives inside a template literal that the TypeScript grammar sees only as a string, so this is a text-embedding problem. The new resolver is lexical by necessity.
What it does
graphify/sql_resolution.pyregisters intoresolver_registry(gated on.sqlbeing present in the corpus, like every other language resolver). After per-file extraction it scans every non-SQL source for each declared table name and emits:file --references--> table,EXTRACTED,confidence_score: 1.0,context: "sql_table"to every declaration site —
schema.sqland the migration that created the table — so tracing a table also reaches its migration history, as the issue asks.A declaration is the target of a
containsedge from a.sqlfile, which is exactly how the SQL extractor anchors what it defines. The sourceless reference stubs it mints for tables defined in another file carry nocontainsedge and are correctly excluded — a stub is not a declaration site.Precision — the trap the issue warns about
The reporter measured it, and the numbers are the reason this PR is shaped the way it is:
eventsuserslistingsAll 130 phantom
eventsedges were JavaScript variables namedevents. Table names likeusers,events,sales,notificationsare extremely common identifiers, so "does this file contain SQL anywhere, and does this name appear anywhere in it" is not good enough.The table must sit in a SQL keyword position in the same match:
Matching runs over the whole file text rather than line by line, so a table named on the second line of a multi-line template literal still matches. Backtick (MySQL), double-quote (standard SQL) and bracket (T-SQL) quoting are all handled, per #2712.
test_a_table_named_like_a_variable_is_not_falsely_linkedpins this: a file full ofconst events = […]— including the wordSELECTin a comment — gets no edge, while the genuineFROM volunteer_assignmentsin a sibling file still does.On the
callsguardrailreferences/extraction-spec.mdis right and unchanged:callsedges stay within one language.referencesis a different relation with different semantics — "this file's SQL names this table" is a factual, checkable claim, not an inferred call. That distinction is written into the edge-emitting code so it survives future edits.Cost and opt-out
Lexical and free — no LLM. Files are read once, capped at 2 MiB, behind a cheap "is there a SQL keyword at all" pre-filter that skips the per-table scan for the overwhelming majority of files.
GRAPHIFY_NO_SQL_LINKS=1skips the pass entirely, documented in the README env table: an ORM-based repo names tables via model classes and gets little from it. An env var rather than a CLI flag because resolvers have no access to CLI args, and this needed no new plumbing to be usable.Scope, matching the issue
.sqlfiles are excluded from the scan for that reason.Tests
tests/test_sql_table_references.py— the issue'svolunteer_assignmentsscenario end to end (two routes, two declaration sites, four edges), the degree-1-orphan regression, theevents-variable precision trap, dialect quoting across MySQL/Postgres/T-SQL, the opt-out, and the dead-schema side effect (a table with no SQL-position reference anywhere gets no edges, which makes this a dead-schema detector too). Four of the six fail without the resolver.Full suite: no new failures — the 22 that fail on this machine fail identically on a clean
v8(missingtree_sitter_hcl, and Windows-specific install/symlink/FIFO tests).