Skip to content

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

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

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

Conversation

@egarcia74

@egarcia74 egarcia74 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

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
Contributor 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).

… name escapes

Three review findings on the recovery scans, each mutation-tested:

- Single-quoted strings are now BLANKED by the mask instead of preserved:
  routine names never live in single quotes, and preserved dynamic SQL
  (EXEC(N'CREATE PROC [dbo].[Fake] ...')) fabricated a routine node
  whenever an unrelated parse error armed the whole-file scan.
  Double-quoted and bracket-delimited identifiers stay verbatim — they
  are exactly the delimited names the recovery regex must see.

- An UNCLOSED block comment now masks to end-of-file (matching SQL
  semantics). Requiring the closing */ left everything after an
  unterminated /* unmasked, and an unterminated comment is exactly the
  kind of error that arms recovery, so DDL inside it fabricated nodes.

- The double-quoted name atom in _ROUTINE_RECOVERY_RX and the mask now
  consume "" escapes, so CREATE PROCEDURE "dbo"."a""b" recovers its
  full name instead of truncating at the first quote — the "" twin of
  the ]] bracket escape. (For a statement the grammar CAN parse,
  tree-sitter-sql itself truncates object_reference at the escape; that
  upstream grammar defect is out of scope here.)

Also converts the ]] regression test's hard tree_sitter_sql import to
pytest.importorskip — on this branch the grammar is still an optional
extra, so a default install's suite must skip, not error.

@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 T-SQL routine recovery to extract_sql in graphify/extractors/sql.py by sharing a single _ROUTINE_RECOVERY_RX between the walk-time ERROR scan and the whole-file has_error fallback, extending it to accept bracket-delimited names (with ]] escapes), PROC/PROC EDURE, and CREATE OR ALTER. Introduces _mask_sql_comments and applies it at both recovery sites so commented-out or single-quoted (dynamic SQL) DDL can't fabricate routine nodes. Adds tests covering bracketed procedures, CREATE OR ALTER, escaped brackets, cross-site name agreement, and comment/literal masking.

Worth a look

  • Routine recovery still scans inside multi-line single-quoted stringsgraphify/extractors/sql.py:59 · 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 — 287 functions depend on the 125 functions this change touches.

Health — this change adds coupling hotspots:

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

Verification — 287 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: 136 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).

… masked file

Independent review of 3e44cbe found the regex mask had hit its
complexity ceiling, with one regression and one surviving hole:

- REGRESSION: a /* inside a MULTI-LINE single-quoted string (ordinary
  T-SQL dynamic SQL) was not recognized as a literal by the single-line
  string atom, so the new unclosed-comment-to-EOF rule blanked the rest
  of the file — hiding every routine below. The mask is now a linear
  character scanner: the in-string /* is consumed as blanked,
  line-scoped string content and never opens a comment.
- Nested /* */ (SQL Server and PostgreSQL both nest) ended at the first
  */, so DDL commented out inside a nested comment fabricated a node.
  The scanner tracks nesting depth.

The scanner emits exactly one output character per input character, so
the offset/line invariant holds by construction.

The per-ERROR-node recovery scan is removed as unsound, not just
redundant: tree-sitter's lexer does not nest /* */ either, so an ERROR
fragment can begin MID-comment with no opener in sight, and no
fragment-level mask can know that. Any ERROR node makes root.has_error
true, so the whole-file masked scan already recovers everything the
per-node scan could, from the same shared regex, with id dedupe.

Also narrows the fabrication claim to what the mask models (MySQL
double-quoted strings and dollar-quoted bodies still rely on the
has_error gate alone) and updates the comments that overclaimed.
…est rationale

Verification review of 5c285bb surfaced one regression and two
overclaiming comments:

- A bracket/double-quote span that reaches its closer ACROSS a comment
  opener ('SELECT [Col FROM t -- CREATE PROC [dbo]' closes on [dbo]'s
  bracket) was preserved verbatim, shielding the commented-out DDL
  inside it from the mask — as was a truly unterminated opener, whose
  span ran to end-of-line. Both shapes are now abandoned: the delimiter
  is emitted alone and the line rescanned, so the comment fires. The
  trade — a genuine identifier containing '--' or '/*' ([a--b]) is now
  conservatively blanked past the opener, losing that routine's
  recovery, never fabricating one — is stated at the branch.
- The line-scoping rationale claimed single-quoted dynamic SQL was
  handled; only the same-line form is. Multi-line dynamic SQL
  (SET @SQL = N'<newline>CREATE PROC ...') fabricates from its
  continuation lines and is now documented as a known hole beside the
  unmodelled quoting dialects, with the reason it is not closed (a
  string heuristic risks swallowing real DDL in a recovery-only path).
- The nesting comment now names the dialects that do NOT nest /* */
  (MySQL, Oracle), where depth tracking over-blanks — losing, never
  fabricating, which is the accepted direction.
Fuzz verification of 1d56f17 found the abandon-and-rescan rule
re-paired single quotes: a quote inside an abandoned span became a live
opener, shifted pairing could swallow a genuine dynamic-SQL string's
closing quote, and EXEC(N'CREATE PROC ...') was exposed again — the
exact fabrication 3e44cbe closed, reachable through a one-line prefix
(SELECT [Col's -- x] ; EXEC(N'...')).

A distrusted span (unterminated, or closing across a -- or /*) is
irreducibly ambiguous — identifier data vs stray delimiter — and any
rule that picks one reading exposes text another reading blanks. So
blank the union of every reading: the rest of the span's line is
blanked outright, and a raw /* on it with no later */ on the same line
carries forward as nesting-aware comment state (some reading may have
left it open; a real */ below still closes it). Under-blanking is what
fabricates and is now structurally impossible for these spans;
over-blanking loses at most routines on a line that was already broken.

Adds a deterministic 20k-case fuzz pinning the structural invariants
(one char per char, newlines preserved, blank-or-verbatim, idempotence)
so the next masking defect class trips a property, not a hand-written
shape.
…ial fuzz

Differential fuzzing of c786d61 (1M cases) found the union stopped one
step short: where a carried block comment closed MID-line, the rest of
that line was emitted verbatim — but under the reading where the carry
never opened, that whole line can be a comment or a string
('-- note */ CREATE PROC ...' fabricated from a line that BEGINS with
--). The distrusted-span blank is now a loop (_blank_tail_and_carry):
blank to end-of-line, carry any raw unclosed /* to its nesting-aware
close, and where that close lands mid-line apply the same rule to the
remainder of that line, until a line ends carry-free. Content on a
carry-close line is conservatively lost; the next line resumes
normally.

Lands the defect class as a test, not just shapes: a frozen copy of
this revision's mask is embedded in the test file and a 20k-case
differential fuzz asserts the live mask never exposes a character the
frozen baseline blanks — the monotonicity property that found every
masking defect in this series (it fails on c786d61; the structural
fuzz alone did not).
Review sign-off notes: the subset claim is now scoped to exclude the
irreducible */* token-split divergence (a fifth absolute claim was not
going to fare better than the first four), and the differential-fuzz
test says how to refresh its frozen baseline on a deliberate exposure
change instead of being deleted.

@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

Adds _mask_sql_comments (a linear offset-preserving scanner that blanks single-quoted strings and nesting-aware comments while preserving delimited identifiers) and _ROUTINE_RECOVERY_RX, and routes both SQL routine-recovery sites (walk-time ERROR scan and whole-file fallback) through them so quoted/bracket-delimited CREATE FUNCTION/PROC[EDURE] names recover consistently and DDL hidden in comments or strings no longer fabricates nodes. Extends recovery to handle CREATE OR ALTER, PROC shorthand, and doubled-bracket (]]) escapes. Adds the corresponding SQL multilang tests plus rationale entries.

Worth a look

  • Dollar-quoted SQL text can fabricate routines when recovery is armedgraphify/extractors/sql.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Routine recovery matches DDL keywords inside delimited identifiersgraphify/extractors/sql.py:31 · 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 — 298 functions depend on the 136 functions this change touches.

Health — this change adds coupling hotspots:

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

Verification — 298 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: 147 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).

The Graphify review bot flagged (and a repro confirmed) that DDL
keywords occurring INSIDE a preserved delimited identifier fabricate a
routine: 'SELECT 1 AS [CREATE PROCEDURE dbo.usp_Phantom pending]' in a
file with an unrelated parse error minted dbo.usp_Phantom(). The mask
must preserve delimited identifiers verbatim — they carry the
recoverable names — but the recovery regex was not span-aware, so it
matched keywords inside them.

The scanner (_scan_sql) now also returns the [start, end) of every
preserved identifier span, and the whole-file recovery scan skips any
match whose CREATE keyword starts inside one: a genuine statement's
NAME may be a delimited identifier; its CREATE never is.
_mask_sql_comments stays as the masked-text-only wrapper, so the
frozen-baseline and structural fuzz tests apply unchanged (blanking
behavior is untouched). Mutation-tested: removing the skip fails the
new regression test.
@egarcia74

Copy link
Copy Markdown
Contributor Author

Both advisory findings from the latest Graphify review triaged:

  • "Routine recovery matches DDL keywords inside delimited identifiers" — confirmed real and fixed in 8c8148c. Reproduced first: SELECT 1 AS [CREATE PROCEDURE dbo.usp_Phantom pending]; in a file with an unrelated parse error minted dbo.usp_Phantom(). The mask must preserve delimited identifiers verbatim (they carry the recoverable names), but the recovery regex wasn't span-aware. The scanner now also reports the [start, end) of every preserved identifier span, and the whole-file scan skips any match whose CREATE starts inside one — a genuine statement's name may be a delimited identifier; its CREATE never is. Pinned by test_sql_ddl_keywords_inside_delimited_identifiers_do_not_fabricate (fails with the skip removed); masking behavior is unchanged, so the frozen-baseline differential fuzz and structural fuzz apply as-is.

  • "Dollar-quoted SQL text can fabricate routines when recovery is armed" — true, and already a documented, deliberate limitation. PostgreSQL dollar-quoting (and MySQL default-mode double-quoted strings, and multi-line single-quoted dynamic SQL) are named as unmodelled in the comment block on _scan_sql, with the rationale: modelling them in a recovery-only path that runs exclusively on files that already failed to parse risks one unclosed delimiter swallowing real DDL — a silent loss — whereas the residual fabrication requires DDL-shaped text inside such quoting in an already-broken file, and remains behind the root.has_error gate. Happy to revisit if maintainers prefer the other side of that trade.

Validation: full suite 6 failed, 4843 passed, 28 skipped (the 6 are the same environmental Ollama failures as 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

Reworks SQL routine recovery in graphify/extractors/sql.py around a shared _ROUTINE_RECOVERY_RX and a new _scan_sql character scanner that masks comments and single-quoted strings while preserving delimited identifiers, so the walk-time ERROR scan and whole-file fallback agree on captured names and stop fabricating routine nodes from commented-out or dynamic SQL. Extends the regex to handle quoted/bracket-delimited names, CREATE OR ALTER, PROC shorthand, and doubled-delimiter escapes. Adds extensive tests/test_multilang.py coverage for quoted PL/pgSQL routines, bracket-named T-SQL procedures, escaped delimiters, comment/literal masking, and recovery-site name agreement.

Worth a look

  • Routine recovery matches CREATE inside bare identifiersgraphify/extractors/sql.py:33 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Routine recovery scans PostgreSQL dollar-quoted stringsgraphify/extractors/sql.py:665 · 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 — 302 functions depend on the 140 functions this change touches.

Health — this change adds coupling hotspots:

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

Verification — 302 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: 151 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).

The Graphify review bot flagged (and a repro confirmed) that the
recovery regex matched CREATE inside a bare identifier: 'SELECT
AUTOCREATE PROCEDURE x FROM t;' in an error-bearing file minted a
phantom routine x(). Delimited identifiers are span-skipped at the
scan site, but a bare word has no span, so the regex itself must
refuse — \bCREATE closes it. Mutation-tested: removing the boundary
fails the new regression test.
@egarcia74

Copy link
Copy Markdown
Contributor Author

Latest Graphify-review findings triaged:

  • "Routine recovery matches CREATE inside bare identifiers" — confirmed real and fixed in f3d0248. Reproduced: SELECT AUTOCREATE PROCEDURE x FROM t; in an error-bearing file minted a phantom x() — the regex had no leading word boundary, and while delimited identifiers are span-skipped at the scan site, a bare word has no span. \bCREATE closes it; regression test test_sql_create_inside_a_bare_word_does_not_fabricate fails with the boundary removed.
  • "Routine recovery scans PostgreSQL dollar-quoted strings" — same finding as the previous run, answered above: a documented, deliberate limitation of the recovery-only path (see the comment block on _scan_sql), behind the root.has_error gate; happy to model dollar-quoting if maintainers prefer that side of the swallow-risk trade.

Validation: full suite 6 failed, 4844 passed, 28 skipped (same environmental Ollama failures as 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 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 comment/string masking layer to the SQL routine-recovery path: introduces _scan_sql (with helpers _blank, _blank_tail_and_carry) to blank single-quoted strings and nesting-aware block/line comments while preserving delimited identifiers, plus a shared _ROUTINE_RECOVERY_RX used by both recovery sites. Extends the regex to recover quoted PL/pgSQL, CREATE OR ALTER, PROC, and bracket-delimited T-SQL routines, and makes the scan skip matches inside preserved identifier spans. Adds multilang tests covering dynamic-SQL/comment non-fabrication, delimited-name escapes, PL/pgSQL survival, and mask fuzz/invariants.

Worth a look

  • Recovery scan reads names from masked_src, blanking delimited identifier contentgraphify/extractors/sql.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Dollar-quoted SQL strings are scanned as executable DDLgraphify/extractors/sql.py:79 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Routine recovery scans PostgreSQL dollar-quoted strings as DDLgraphify/extractors/sql.py:672 · 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 — 304 functions depend on the 142 functions this change touches.

Health — this change adds coupling hotspots:

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

Verification — 304 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: 153 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).

@egarcia74

Copy link
Copy Markdown
Contributor Author

Triage of the latest Graphify-review run (on f3d0248), for whoever reviews this next:

  • "Recovery scan reads names from masked_src, blanking delimited identifier content" — not correct. A captured name can only be verbatim source text: the name atoms match either [\w$]+ (word characters — blanked positions are spaces, which cannot participate) or delimited forms that require their real delimiters, which blanking removes. A name therefore either comes from a preserved identifier span (copied verbatim, ""/]] escapes included) or is not captured at all — a distrusted span like [a--b] is wholly blanked and yields no match (the documented conservative loss), never a mangled name. Pinned by the exact-label assertions in the suite ([dbo].[a]]b](), "dbo"."a""b"()) and by the 391-file corpus validation (28/28 procedures recovered with verbatim bracketed names).
  • The two dollar-quoting findings are the same documented limitation answered in the two comments above (deliberate trade in a recovery-only path behind the root.has_error gate; see the comment block on _scan_sql).

The review bot re-derives findings on every push, so the dollar-quoting advisory recurs each run. To keep this thread readable I'll stop replying per-run here: every distinct finding to date is either fixed with a named, mutation-tested commit (four were real — thanks, genuinely useful) or answered with rationale above. Awaiting human review.

@egarcia74

Copy link
Copy Markdown
Contributor Author

Superseded by #3166 — the same change squashed to a single commit and rebased onto current v8 (0.9.51) so it cherry-picks cleanly, with the defect now tracked in #3164. The review history stays here: each Graphify-review finding on this PR was reproduced and either fixed or refuted in the thread above, and the final scanner is what #3166 carries. Closing this one.

@egarcia74 egarcia74 closed this Aug 28, 2026
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