From 8e8a7c3808f1098172bb4212fb393c81ba5a33d0 Mon Sep 17 00:00:00 2001 From: Deepjyoti-Sarmah Date: Thu, 22 Oct 2026 04:12:32 +0530 Subject: [PATCH] test: one node per SQL table across files A table no file defines, read from N files, becomes N per-file stub nodes, and a schema-qualified read never binds to its definition in another file. A SQL read of `Orders` also binds to a Python class Orders. Guards cover same-file binding and different schemas. (cherry picked from commit 1dfc710497a753cc663a2cefc3ece724414be784) --- tests/test_sql_cross_file_tables.py | 95 +++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/test_sql_cross_file_tables.py diff --git a/tests/test_sql_cross_file_tables.py b/tests/test_sql_cross_file_tables.py new file mode 100644 index 000000000..9f1394e83 --- /dev/null +++ b/tests/test_sql_cross_file_tables.py @@ -0,0 +1,95 @@ +"""A table referenced from several files is one node, bound to its definition when one exists.""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +pytest.importorskip("tree_sitter_sql") + +from graphify.extract import extract # noqa: E402 + + +def _project(tmp_path: Path, files: dict[str, str]) -> dict: + paths = [] + for rel, text in files.items(): + p = tmp_path / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text) + paths.append(p) + return extract(paths, root=tmp_path) + + +def _norm(label: str) -> str: + return label.replace("[", "").replace("]", "").lower() + + +def _ids(result: dict, name: str) -> set[str]: + return {n["id"] for n in result["nodes"] if _norm(n["label"]) == name} + + +def _read_targets(result: dict, view: str) -> set[str]: + src = _ids(result, view) + return {e["target"] for e in result["edges"] if e["source"] in src and e["relation"] == "reads_from"} + + +VIEWS = { + "v/a.sql": "CREATE VIEW dbo.VA AS SELECT id FROM {t};\n", + "v/b.sql": "CREATE VIEW dbo.VB AS SELECT id FROM {t};\n", +} + + +# bug +@pytest.mark.parametrize("table", ["dbo.Orders", "Orders"]) +def test_undefined_table_from_two_files_is_one_node(tmp_path, table): + r = _project(tmp_path, {k: v.format(t=table) for k, v in VIEWS.items()}) + assert len(_ids(r, table.lower())) == 1 + assert _read_targets(r, "dbo.va") == _read_targets(r, "dbo.vb") + + +# bug +@pytest.mark.parametrize("table", ["dbo.Orders", "Orders"]) +def test_reads_bind_to_table_defined_in_another_file(tmp_path, table): + files = {"t/tables.sql": f"CREATE TABLE {table} (id INT);\n"} + files.update({k: v.format(t=table) for k, v in VIEWS.items()}) + r = _project(tmp_path, files) + defined = _ids(r, table.lower()) + assert len(defined) == 1 + assert _read_targets(r, "dbo.va") == defined + assert _read_targets(r, "dbo.vb") == defined + + +# bug +def test_bracket_and_plain_names_across_files_are_one_table(tmp_path): + r = _project(tmp_path, { + "t/tables.sql": "CREATE TABLE [dbo].[Orders] (id INT);\n", + "v/a.sql": "CREATE VIEW dbo.VA AS SELECT id FROM dbo.Orders;\n", + }) + assert _read_targets(r, "dbo.va") == _ids(r, "dbo.orders") + assert len(_ids(r, "dbo.orders")) == 1 + + +# guard +def test_same_file_definition_still_binds(tmp_path): + r = _project(tmp_path, {"a.sql": "CREATE TABLE dbo.Orders (id INT);\nCREATE VIEW dbo.VA AS SELECT id FROM dbo.Orders;\n"}) + assert _read_targets(r, "dbo.va") == _ids(r, "dbo.orders") + + +# guard +def test_different_schemas_stay_different_tables(tmp_path): + r = _project(tmp_path, { + "v/a.sql": "CREATE VIEW dbo.VA AS SELECT id FROM dbo.Orders;\n", + "v/b.sql": "CREATE VIEW dbo.VB AS SELECT id FROM sales.Orders;\n", + }) + assert _read_targets(r, "dbo.va").isdisjoint(_read_targets(r, "dbo.vb")) + + +# guard +def test_sql_read_does_not_bind_to_python_class(tmp_path): + r = _project(tmp_path, { + "app/models.py": "class Orders:\n pass\n", + "v/a.sql": "CREATE VIEW dbo.VA AS SELECT id FROM Orders;\n", + }) + py = {n["id"] for n in r["nodes"] if n.get("source_file", "").endswith(".py") and n["label"] == "Orders"} + assert py + assert _read_targets(r, "dbo.va").isdisjoint(py)