Skip to content

feat(sql): link SQL tables to the code that queries them (#2884) - #2891

Open
rajarshidattapy wants to merge 4 commits into
Graphify-Labs:v8from
rajarshidattapy:feat/sql-table-code-links
Open

feat(sql): link SQL tables to the code that queries them (#2884)#2891
rajarshidattapy wants to merge 4 commits into
Graphify-Labs:v8from
rajarshidattapy:feat/sql-table-code-links

Conversation

@rajarshidattapy

Copy link
Copy Markdown
Contributor

Fixes #2884.

The gap

tree-sitter-sql extracts table declarations correctly and the host-language grammars extract 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, 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, with db/ 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.py registers into resolver_registry (gated on .sql being 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.sql and 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 contains edge from a .sql file, which is exactly how the SQL extractor anchors what it defines. The sourceless reference stubs it mints for tables defined in another file carry no contains edge 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:

table naive matcher strict matcher
events 157 files 27
users 98 78
listings 95 32
total edges 1,489 921

All 130 phantom events edges were JavaScript variables named events. Table names like users, events, sales, notifications are 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:

_SQL_KEYWORDS = r"(?:FROM|JOIN|INTO|UPDATE|REFERENCES|TABLE(?:\s+IF\s+NOT\s+EXISTS)?)"

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_linked pins this: a file full of const events = […] — including the word SELECT in a comment — gets no edge, while the genuine FROM volunteer_assignments in a sibling file still does.

On the calls guardrail

references/extraction-spec.md is right and unchanged: calls edges stay within one language. references is 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=1 skips 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

Tests

tests/test_sql_table_references.py — the issue's volunteer_assignments scenario end to end (two routes, two declaration sites, four edges), the degree-1-orphan regression, the events-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 (missing tree_sitter_hcl, and Windows-specific install/symlink/FIFO tests).

…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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 defaultgraphify/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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 referencesgraphify/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).

@safishamsi

Copy link
Copy Markdown
Collaborator

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:

  1. False edges from comments and non-SQL strings. The matcher scans the raw file bytes, so a table name after a keyword mints an edge even when it is not a real query. All of these currently produce a phantom references edge:

    • // SELECT foo FROM events (a comment)
    • const s = "the FROM users keyword"; (a plain string)
    • """Docs: you can JOIN users with assets here.""" (a docstring)
      This is the same phantom-edge class the issue itself calls out. Suggested direction: strip line/block comments before scanning (cheap per-language heuristics for //, #, /* */, --), and ideally require the keyword+table to sit inside a string/template literal rather than free code. The existing negative test only covers the bare-identifier case; please add the three cases above and make them pass.
  2. Schema-qualified tables never link. _declared_tables rejects any label that is not a bare identifier, but the SQL extractor stores labels verbatim, so CREATE TABLE public.users has label public.users and is filtered out — a Postgres/T-SQL corpus gets zero edges. The extractor already has _norm_ident for this; reusing it in the resolver (match on the normalized/bare name, and handle the schema-qualified reference form in code) would fix it. A test with a public.users schema plus SELECT ... FROM users and FROM public.users would lock it in.

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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 restorationgraphify/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 delimitergraphify/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 namegraphify/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).

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.

SQL tables are never linked to the application code that queries them — the whole data layer lands as degree-1 orphans

2 participants