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:
L4XB authored and safishamsi committed 2026-09-10 14:51:54 +01:00
1 parent 355eefd391
commit d2abe4ed0c
2 files changed
+61

No files matched your search

+26
View File
@@ -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
+35
View File
@@ -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.