mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 17:09:17 +00:00
fix(sql): emit index nodes for CREATE [UNIQUE] INDEX statements
Adds an AST branch for create_index in the SQL extractor, emitting an index node with an 'indexes' edge to its table (cross-file stub when the table is defined elsewhere), mirroring the existing trigger handling. (#3470)
This commit is contained in:
1 parent
355eefd391
commit
d2abe4ed0c
2 files changed
+61
No files matched your search
@@ -486,6 +486,32 @@ def extract_sql(path: Path, content: str | bytes | None = None) -> dict:
|
||||
tbl_nid = table_nids.get(_norm_ident(tbl_name)) or _ref_stub(tbl_name)
|
||||
_add_edge(trig_nid, tbl_nid, "triggers", line)
|
||||
|
||||
elif t == "create_index":
|
||||
# CREATE [UNIQUE] INDEX [CONCURRENTLY] [IF NOT EXISTS] <name>
|
||||
# ON <table> (...). Unlike CREATE POLICY (#3401) the grammar
|
||||
# parses this statement fine; the walk simply never dispatched on
|
||||
# it, so every index was silently dropped (#3467). The name is the
|
||||
# identifier (or a quoted literal) before ON; the table is the
|
||||
# object_reference after it. An unnamed index (`CREATE INDEX ON
|
||||
# t (c)`) has nothing to name a node after and is skipped.
|
||||
index_name: str | None = None
|
||||
index_table: str | None = None
|
||||
after_on = False
|
||||
for c in node.children:
|
||||
if c.type == "keyword_on":
|
||||
after_on = True
|
||||
elif not after_on and index_name is None and c.type in ("identifier", "literal"):
|
||||
index_name = _read(c).strip('"`')
|
||||
elif after_on and index_table is None and c.type == "object_reference":
|
||||
index_table = _read(c)
|
||||
if index_name:
|
||||
index_nid = _make_id(stem, index_name)
|
||||
_add_node(index_nid, index_name, line)
|
||||
if index_table:
|
||||
index_tbl_nid = (table_nids.get(_norm_ident(index_table))
|
||||
or _ref_stub(index_table))
|
||||
_add_edge(index_nid, index_tbl_nid, "indexes", line)
|
||||
|
||||
# NOTE: there is deliberately NO recovery scan on individual ERROR
|
||||
# nodes. Any ERROR node anywhere makes root.has_error true, so the
|
||||
# whole-file masked scan below this walk already recovers everything
|
||||
|
||||
@@ -531,6 +531,41 @@ 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_create_index_emits_index_node_linked_to_its_table(tmp_path):
|
||||
"""#3467: CREATE [UNIQUE] INDEX parsed fine but the walk never dispatched
|
||||
on create_index, so every index was silently dropped."""
|
||||
pytest.importorskip("tree_sitter_sql")
|
||||
p = tmp_path / "schema.sql"
|
||||
p.write_text(
|
||||
"CREATE TABLE public.profiles (id uuid PRIMARY KEY, owner uuid NOT NULL);\n"
|
||||
"CREATE INDEX profiles_owner_idx ON public.profiles (owner);\n"
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS profiles_id_uniq ON public.profiles (id);\n"
|
||||
"CREATE INDEX CONCURRENTLY orders_customer_idx ON public.orders (customer_id);\n"
|
||||
'CREATE INDEX "quoted idx" ON public.profiles (owner);\n'
|
||||
"CREATE INDEX ON public.profiles (owner);\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract_sql(p)
|
||||
by_label = {n["label"]: n for n in r["nodes"]}
|
||||
for name in ("profiles_owner_idx", "profiles_id_uniq", "orders_customer_idx", "quoted idx"):
|
||||
assert name in by_label, name
|
||||
assert by_label[name]["source_file"] == str(p)
|
||||
edges = {(e["source"], e["relation"], e["target"]) for e in r["edges"]}
|
||||
profiles = by_label["public.profiles"]["id"]
|
||||
assert (by_label["profiles_owner_idx"]["id"], "indexes", profiles) in edges
|
||||
assert (by_label["profiles_id_uniq"]["id"], "indexes", profiles) in edges
|
||||
assert (by_label["quoted idx"]["id"], "indexes", profiles) in edges
|
||||
# An index on a table defined in another file links to a sourceless stub,
|
||||
# the same way a trigger does (#2324).
|
||||
orders = by_label["public.orders"]
|
||||
assert orders["source_file"] == ""
|
||||
assert (by_label["orders_customer_idx"]["id"], "indexes", orders["id"]) in edges
|
||||
# The unnamed index is skipped and nothing dangles.
|
||||
node_ids = {n["id"] for n in r["nodes"]}
|
||||
assert all(e["source"] in node_ids and e["target"] in node_ids for e in r["edges"])
|
||||
assert sum(1 for e in r["edges"] if e["relation"] == "indexes") == 4
|
||||
|
||||
|
||||
def test_sql_tsql_bracketed_procedure_is_recovered(tmp_path):
|
||||
"""T-SQL CREATE PROCEDURE [Schema].[Name] ... AS BEGIN...END.
|
||||
|
||||
|
||||
Reference in new issue
Block a user