Skip to content

fix(sql): recover T-SQL bracket-named, CREATE OR ALTER, and PROC routines - #2960

Open
egarcia74 wants to merge 4 commits into
Graphify-Labs:v8from
egarcia74:fix/tsql-proc-recovery
Open

fix(sql): recover T-SQL bracket-named, CREATE OR ALTER, and PROC routines#2960
egarcia74 wants to merge 4 commits into
Graphify-Labs:v8from
egarcia74:fix/tsql-proc-recovery

Conversation

@egarcia74

@egarcia74 egarcia74 commented Aug 23, 2026

Copy link
Copy Markdown

Problem

T-SQL stored procedures and functions are silently dropped from the graph. On a real 391-file T-SQL data-migration corpus (SQL Server DACPAC project: staging/model/transform layers, [Schema].[Object] naming throughout), 0 of 26 stored procedures produced a node — while tables and views extracted fine, so the gap is invisible unless you go counting.

Two independent defects stack:

  1. The grammar can't parse the T-SQL routine shape at all. CREATE PROCEDURE ... AS BEGIN ... END never parses structurally in tree-sitter-sql (there is no create_procedure production for the AS BEGIN body idiom), so ERROR-node regex recovery is these objects' only path into the graph.
  2. The recovery regex doesn't speak T-SQL. It accepted only unquoted or double-quoted names — no bracket-delimited names (CREATE PROCEDURE [dbo].[usp_Load]), no CREATE OR ALTER, no PROC shorthand. On this corpus, every routine uses brackets, so recovery recovered nothing.

What this PR does

All changes are in graphify/extractors/sql.py:

  • One shared pattern, _ROUTINE_RECOVERY_RX, hoisted to module level and used by both recovery sites (the walk-time ERROR-node scan and the whole-file has_error fallback). The two sites had drifted; the drifted second site is what minted a phantom dbo() node for a mixed-delimiter name (CREATE PROCEDURE dbo.[usp_Mixed]) — the schema captured as if it were the routine. Sharing the pattern fixes that and prevents re-drift.
  • Bracket-delimited identifiers, including T-SQL's ]] escape for a literal ] inside a name ([dbo].[a]]b] recovers as one routine, fully consumed, not truncated at the escape).
  • CREATE OR ALTER alongside the existing OR REPLACE, and the PROC shorthand alongside PROCEDURE.
  • Comment masking in the whole-file fallback: the raw-text scan now runs over _mask_sql_comments(src) (offset-preserving blanking of -- and /* */), so commented-out DDL in a file with an unrelated parse error can no longer fabricate routine nodes.

Recovered routines are name-only nodes (no body reads_from edges), matching the existing PL/pgSQL recovery behavior — a named node with a contains edge beats an invisible object.

Result on the real corpus

391 .sql files, before → after: 1 → 28 routine nodes, including every bracket-named [Audit]/[Utils]/[Monitoring] procedure; table and view counts unchanged; no phantom schema nodes.

Tests

Added in tests/test_multilang.py, each verified to fail with the fix reverted:

  • bracket-delimited procedure recovers under its full [dbo].[usp_X] name;
  • CREATE OR ALTER PROCEDURE recovers;
  • PROC shorthand recovers;
  • mixed-delimiter name (dbo.[usp_Mixed]) yields exactly one node — both recovery sites agree on the captured name (the phantom-dbo() regression);
  • ]] escape inside a bracket-delimited name is consumed, capturing [dbo].[a]]b]() as one routine;
  • commented-out DDL in an ERROR-bearing file does not fabricate a routine node.

Validation

  • Full suite: 6 failed, 4836 passed, 28 skipped — the 6 (test_ollama.py, test_ollama_retry_cap.py) fail identically on unmodified v8 in this environment (local Ollama config), none touch SQL.
  • ruff check graphify/ tests/: all checks passed.

Note

Independent of my companion PR #2959 making tree-sitter-sql a core dependency; both touch tests/test_multilang.py, and a local merge of the two branches resolves trivially (the combined branch produced the 1 → 28 measurement above and runs in production).

egarcia74 and others added 3 commits August 23, 2026 00:44
T-SQL's AS BEGIN...END body idiom never parses structurally (the grammar
has no create_procedure form for it), so recovery from ERROR nodes is the
only path such routines have into the graph — and the recovery pattern
matched only bare or double-quoted names and only OR REPLACE. In a T-SQL
codebase that brackets every identifier ([dbo].[usp_X], a common house
standard) and uses CREATE OR ALTER, every stored procedure silently
vanished: 0/26 recovered in the reporting corpus.

- accept bracket-delimited name parts and OR ALTER, mirroring what
  fb_proc_or_trigger already does for Firebird
- hoist the pattern into a shared _ROUTINE_RECOVERY_RX used by both
  recovery sites (walk-time ERROR scan and whole-file has_error
  fallback): when the two drifted, a mixed-delimiter name
  (dbo.[usp_Mixed]) was captured differently by each, minting a second
  phantom node named after the schema that id-dedupe could not catch

Recovered routines stay name-only nodes (no body reads_from edges),
matching the existing PL/pgSQL recovery. Each new guard was
mutation-tested: dropping the bracket alternative, dropping OR ALTER,
and re-introducing the pattern drift each fail exactly their test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ames

[a]]b] names the identifier a]b; stopping at the first ] truncated the
recovered routine to [dbo].[a] — a phantom that could collide with a
genuinely named [dbo].[a]. The bracketed-part alternative now consumes
]] before treating a lone ] as the closing delimiter, in both name-part
positions of the shared _ROUTINE_RECOVERY_RX. Mutation-tested: reverting
the escape handling fails the new regression test.

Addresses the CodeRabbit P2 review finding on PR #2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream batches changelog entries in maintainer release commits;
the entry's content moves to the PR description.

@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

Extends SQL routine recovery to handle T-SQL dialect: adds bracket-delimited identifiers ([dbo].[usp_Load], with ]] escaping), CREATE OR ALTER, and the PROC shorthand, and consolidates the previously-duplicated recovery regex into a shared _ROUTINE_RECOVERY_RX so the walk-time ERROR scan and whole-file fallback can't drift and double-emit. Adds _mask_sql_comments (offset-preserving) around the whole-file scan so commented-out DDL can't fabricate routine nodes when an unrelated parse error arms recovery. Covers all of this with new test_sql_* cases.

Worth a look

  • Comment masking treats -- inside string literals as a real commentgraphify/extractors/sql.py:37 · 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 — 279 functions depend on the 117 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 119 callees
  • new: extract_sql() — 15 callers, 9 callees
  • new: walk() — 1 callers, 8 callees
  • new: test_poisoned_manifest_is_healed() — 0 callers, 6 callees

Verification — 279 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: 128 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify extract\_sql.

The verifier did not have enough to check extract\_sql, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 4 more finding(s) on lines outside this diff (see the check run).

…ask the ERROR-node scan

The recovery mask read '-- note' inside a string literal as a line
comment (blanking to end-of-line) and a /* inside a string as a block
comment opener (blanking through the next real */), so real DDL sharing
the span could be hidden. The mask now preserves single-quoted strings
(with '' escapes), double-quoted identifiers, and bracket-delimited
identifiers (with ]] escapes) before blanking comments. Literal patterns
are deliberately single-line: the mask only runs on files that already
failed to parse, where an unclosed quote is likely, and a multi-line
match would let one unclosed delimiter swallow real DDL below it.

The walk-time ERROR-node scan now masks too: an ERROR blob whose byte
span covers commented-out DDL fabricated a routine node from it exactly
as the whole-file scan once did (reproduced: a -- CREATE PROC line
sandwiched between broken segments).

Both guards are mutation-tested: a literal-blind mask fails the new
unit pin; an unmasked ERROR scan fails the extended fabrication test.
@egarcia74

Copy link
Copy Markdown
Author

Addressed the Graphify-review finding (-- inside string literals treated as a comment opener) in b6d4dfc, and it turned out to be half of a symmetric pair:

  • The mask is now literal-aware: single-quoted strings (with '' escapes), double-quoted identifiers, and bracket-delimited identifiers (with ]] escapes) are preserved before comments are blanked, so '-- note' / 'open /* here' can no longer hide DDL sharing the span. Literal patterns are deliberately single-line — this mask only runs on files that already failed to parse, where an unclosed quote is likely, and a multi-line match would let one unclosed delimiter swallow real DDL below it (a miss is visible; a swallow is silent).
  • The walk-time ERROR-node scan now masks too: while verifying the fix I found the fabrication risk the whole-file scan was cured of was still live at the other recovery site — an ERROR blob whose byte span covers commented-out DDL minted a routine node from it (reproduced with a -- CREATE PROC ... line sandwiched between broken segments).

Both directions are mutation-tested: a literal-blind mask fails the new unit pin (test_mask_sql_comments_preserves_literals_and_blanks_comments), and an unmasked ERROR scan fails the extended fabrication test. Full suite after the change: 6 failed, 4838 passed, 28 skipped — same 6 environmental Ollama failures as on unmodified v8; ruff check clean.

@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 2 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Extends SQL routine recovery in extract_sql to handle T-SQL bracket-delimited identifiers ([dbo].[usp_Load], with ]] escapes), CREATE OR ALTER, and the PROC shorthand, and consolidates the walk-time ERROR scan and whole-file fallback onto a single shared _ROUTINE_RECOVERY_RX so the two sites can no longer capture divergent names and double-emit nodes. Adds _mask_sql_comments to blank comment spans (offset-preserving, literals kept verbatim) at both recovery sites so commented-out DDL can't fabricate routine nodes. Adds tests covering the bracketed/OR ALTER/PROC forms, escaped-bracket names, cross-site name agreement, and comment masking.

Worth a look

  • Routine recovery still matches DDL text inside string literalsgraphify/extractors/sql.py:510 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Optional tree_sitter_sql dependency is imported instead of skippedtests/test_multilang.py:536 · 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 — 283 functions depend on the 121 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 119 callees
  • new: extract_sql() — 16 callers, 9 callees
  • new: walk() — 1 callers, 9 callees
  • new: test_poisoned_manifest_is_healed() — 0 callers, 6 callees

Verification — 283 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: 132 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify extract\_sql.

The verifier did not have enough to check extract\_sql, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 4 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.

1 participant