From c9acf7816a741106356c937a28cdf7b62a3c9b4e Mon Sep 17 00:00:00 2001 From: amtulifra Date: Sun, 23 Aug 2026 03:26:53 +0530 Subject: [PATCH] fix(sql): walk into BEGIN;...COMMIT; transaction blocks (#2953) tree-sitter-sql wraps a transaction in its own top-level transaction node instead of statement, so the extractor's root dispatch loop never reached it. Every table, view, function, and trigger defined inside a transaction was silently dropped with no error, and any REFERENCES pointing at one of those tables fell back to a sourceless stub instead of the real node. Route the transaction node through the existing generic walk, which already recurses into whatever children it finds. --- graphify/extractors/sql.py | 9 ++++++++- tests/test_multilang.py | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/graphify/extractors/sql.py b/graphify/extractors/sql.py index a5dc18c368..71565510ec 100644 --- a/graphify/extractors/sql.py +++ b/graphify/extractors/sql.py @@ -398,7 +398,14 @@ def _collect_defined_names(node) -> None: if stmt.type == "statement": for child in stmt.children: walk(child) - elif stmt.type in ("fb_proc_or_trigger", "set_term", "declare_external_function", "ERROR"): + elif stmt.type in ("fb_proc_or_trigger", "set_term", "declare_external_function", + "ERROR", "transaction"): + # `transaction` wraps BEGIN/START TRANSACTION ... COMMIT/ROLLBACK + # blocks in its own top-level node (#2953), so statements inside + # never reach the `stmt.type == "statement"` branch above and were + # silently dropped. walk() has no special case for "transaction" + # either, so it falls through to its generic per-child recursion, + # which reaches the nested `statement` nodes normally. walk(stmt) # Global regex fallback: catch any REFERENCES missed due to ERROR nodes in the parse tree diff --git a/tests/test_multilang.py b/tests/test_multilang.py index cb390eebcc..8e96ea69ca 100644 --- a/tests/test_multilang.py +++ b/tests/test_multilang.py @@ -487,6 +487,28 @@ def test_sql_no_dangling_edges(): for e in r["edges"]: assert e["source"] in node_ids, f"dangling source: {e['source']}" +def test_sql_transaction_block_is_not_dropped(tmp_path): + """#2953: statements wrapped in BEGIN; ... COMMIT; land under a top-level + `transaction` node, not `statement`, and were silently skipped entirely.""" + pytest.importorskip("tree_sitter_sql") + p = tmp_path / "con_transaccion.sql" + p.write_text( + "BEGIN;\n" + "CREATE TABLE gamma (id INT PRIMARY KEY);\n" + "CREATE TABLE delta (id INT PRIMARY KEY, gamma_id INT REFERENCES gamma(id));\n" + "COMMIT;\n" + ) + r = extract_sql(p) + labels = [n["label"] for n in r["nodes"]] + assert "gamma" in labels + assert "delta" in labels + # Both tables get real source info, not a sourceless _ref_stub + by_label = {n["label"]: n for n in r["nodes"]} + assert by_label["gamma"]["source_location"] == "L2" + assert by_label["delta"]["source_location"] == "L3" + fk_edges = [e for e in r["edges"] if e["relation"] == "references"] + assert len(fk_edges) == 1 + def test_sql_cte_is_not_read_as_a_table(): """#2577: a name bound by WITH ... AS (...) is scoped to its statement, not a table.