From 1a65f77307cf639f70ce64b0cf7a6ec4f8d60cf1 Mon Sep 17 00:00:00 2001 From: Eduardo Garcia-Prieto Date: Sun, 23 Aug 2026 00:13:51 +1000 Subject: [PATCH 1/4] feat(sql): make tree-sitter-sql a core dependency A default install (uv tool install graphifyy / pipx install graphifyy) shipped without SQL parsing: tree-sitter-sql lived behind the [sql] extra, so every .sql file silently contributed nothing until the user found the extra or ran pipx inject (#1745). The grammar ships prebuilt abi3 wheels for every supported platform from 0.3.9, so the reason tree-sitter-dm stays optional does not apply. - promote tree-sitter-sql>=0.3.9,<0.4 into [project.dependencies]; keep the [sql] extra as an alias so existing install scripts resolve - drop .sql from _EXTRA_FOR_EXTENSION: a missing grammar now means a broken install, so the #1745 warning must not point at an extra - reword the extractor's missing-grammar error to say the install is incomplete and name the direct repair Bumps version to 0.9.49. Co-Authored-By: Claude Fable 5 --- graphify/extract.py | 4 +++- graphify/extractors/sql.py | 4 +++- pyproject.toml | 14 ++++++++++++-- uv.lock | 6 ++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index ffc6153f82..9e6880388c 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -5073,7 +5073,9 @@ def add_existing_edge(edge: dict) -> None: # rather than falling back like Pascal does. Used by the #1745 warning in # extract() to tell the user which extra restores the language. _EXTRA_FOR_EXTENSION = { - ".sql": "sql", + # .sql is deliberately absent: tree-sitter-sql became a core dependency in + # 0.9.49, so a missing grammar means a broken install, not a missing extra + # — the extractor's own error names the direct pip install that repairs it. ".tf": "terraform", ".tfvars": "terraform", ".hcl": "terraform", diff --git a/graphify/extractors/sql.py b/graphify/extractors/sql.py index a5dc18c368..0f440f1b49 100644 --- a/graphify/extractors/sql.py +++ b/graphify/extractors/sql.py @@ -41,7 +41,9 @@ def extract_sql(path: Path, content: str | bytes | None = None) -> dict: # and surface the real exception in the latter case. if importlib.util.find_spec("tree_sitter_sql") is None: return {"nodes": [], "edges": [], - "error": "tree_sitter_sql not installed. Run: pip install tree-sitter-sql"} + "error": ("tree_sitter_sql not installed. It is a core dependency " + "since 0.9.49, so this install is incomplete — reinstall " + "graphifyy or run: pip install tree-sitter-sql")} return {"nodes": [], "edges": [], "error": f"tree_sitter_sql is installed but failed to load: {e}"} diff --git a/pyproject.toml b/pyproject.toml index 15ea9dd57c..4dfcf5d642 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.9.48" +version = "0.9.49" description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = "Apache-2.0" @@ -41,6 +41,13 @@ dependencies = [ "tree-sitter-fortran>=0.6,<0.8", "tree-sitter-bash>=0.23,<0.27", "tree-sitter-json>=0.23,<0.26", + # Core since 0.9.49 (was the [sql] extra): .sql files are a mainstream + # corpus language and the extra-gated grammar made a default install + # silently skip them (#1745). Ships prebuilt abi3 wheels for every + # supported platform (win/macOS/Linux glibc+musl, x86_64+arm64) from + # 0.3.9, so no C toolchain is needed — the reason tree-sitter-dm stays + # optional does not apply here. + "tree-sitter-sql>=0.3.9,<0.4", ] [project.urls] @@ -74,7 +81,10 @@ anthropic = ["anthropic"] gemini = ["openai", "tiktoken"] openai = ["openai", "tiktoken"] chinese = ["jieba"] -sql = ["tree-sitter-sql"] +# tree-sitter-sql moved to core dependencies in 0.9.49. The extra is kept as +# an alias so existing `graphifyy[sql]` install commands and scripts keep +# resolving; it adds nothing beyond the core install. +sql = ["tree-sitter-sql>=0.3.9,<0.4"] # extract_pascal() uses tree-sitter-pascal for AST-quality extraction (more # accurate calls/inherits edges) and falls back to a regex extractor when it is # absent (#781), so this stays optional. Unlike tree-sitter-dm below, it ships diff --git a/uv.lock b/uv.lock index 881314a5b5..64da1f74d9 100644 --- a/uv.lock +++ b/uv.lock @@ -1090,7 +1090,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.48" +version = "0.9.49" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1120,6 +1120,7 @@ dependencies = [ { name = "tree-sitter-ruby" }, { name = "tree-sitter-rust" }, { name = "tree-sitter-scala" }, + { name = "tree-sitter-sql" }, { name = "tree-sitter-swift" }, { name = "tree-sitter-typescript" }, { name = "tree-sitter-verilog" }, @@ -1334,8 +1335,9 @@ requires-dist = [ { name = "tree-sitter-ruby", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-rust", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-scala", specifier = ">=0.23,<0.27" }, + { name = "tree-sitter-sql", specifier = ">=0.3.9,<0.4" }, { name = "tree-sitter-sql", marker = "extra == 'all'" }, - { name = "tree-sitter-sql", marker = "extra == 'sql'" }, + { name = "tree-sitter-sql", marker = "extra == 'sql'", specifier = ">=0.3.9,<0.4" }, { name = "tree-sitter-swift", specifier = ">=0.7,<0.9" }, { name = "tree-sitter-typescript", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-verilog", specifier = ">=1.0,<2.0" }, From 2bf1b62cf46c3b7cac4c5ef6df4f7b8d69c2b1e0 Mon Sep 17 00:00:00 2001 From: Eduardo Garcia-Prieto Date: Sun, 23 Aug 2026 00:14:06 +1000 Subject: [PATCH 2/4] test(sql): pin SQL parsing as a core capability - new test_sql_core_dependency.py: tree-sitter-sql must sit in [project.dependencies] with an upper bound, .sql must not map to an optional extra, and a table/view/procedure corpus must produce structural nodes plus contains/references/reads_from edges - convert every importorskip("tree_sitter_sql") to a hard import: the grammar is core now, so its absence must fail the suite, not skip it - update the #1745 warning test: the dev environment now ships the grammar, so genuine absence is simulated by blanking find_spec, and the message must say "core dependency" instead of hinting at graphifyy[sql] Each packaging guard was mutation-tested (dep removed from pyproject, .sql re-added to the extras map) and failed as required. Co-Authored-By: Claude Fable 5 --- tests/test_extract.py | 26 +++++--- tests/test_multilang.py | 42 ++++++------- tests/test_sql_core_dependency.py | 100 ++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 tests/test_sql_core_dependency.py diff --git a/tests/test_extract.py b/tests/test_extract.py index c9790e4ab5..5c58c8dcd4 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -3610,12 +3610,21 @@ def test_extract_no_warning_when_all_code_has_extractors(tmp_path, capsys): assert "no AST extractor" not in err -def test_extract_warns_when_sql_extra_missing(tmp_path, capsys, monkeypatch): +def test_extract_warns_when_sql_grammar_missing(tmp_path, capsys, monkeypatch): # #1745: .sql HAS a dispatch entry, so the #1689 warning can't fire, and # extract_sql returns an "error" result when tree-sitter-sql is absent, so - # the #1666 warning skips it too. The files must not vanish silently: - # extract() surfaces them with the [sql] extra named. + # the #1666 warning skips it too. The files must not vanish silently. + # Since 0.9.49 the grammar is a core dependency, so absence means a broken + # install: the warning must name the direct repair, not a [sql] extra. + # The dev environment now ships the grammar, so simulate genuine absence + # by failing the import AND blanking find_spec for this one module. + import importlib.util as _ilu monkeypatch.setitem(sys.modules, "tree_sitter_sql", None) # import -> ImportError + _real_find_spec = _ilu.find_spec + monkeypatch.setattr( + _ilu, "find_spec", + lambda name, *a, **k: None if name == "tree_sitter_sql" else _real_find_spec(name, *a, **k), + ) s1 = tmp_path / "schema.sql"; s1.write_text("CREATE TABLE users (id INT);\n") s2 = tmp_path / "views.sql"; s2.write_text("CREATE VIEW v AS SELECT * FROM users;\n") py = tmp_path / "main.py"; py.write_text("def main():\n return 1\n") @@ -3625,7 +3634,8 @@ def test_extract_warns_when_sql_extra_missing(tmp_path, capsys, monkeypatch): assert "2 .sql file(s)" in err assert "tree_sitter_sql not installed" in err - assert 'graphifyy[sql]' in err + assert "core dependency" in err, "message must say the install is broken, not point at an extra" + assert "graphifyy[sql]" not in err, ".sql must not be hinted as an optional extra any more" assert "#1745" in err # the Python file still extracts normally labels = [n.get("label") for n in result["nodes"]] @@ -3639,7 +3649,7 @@ def test_extract_warns_when_sql_extra_missing(tmp_path, capsys, monkeypatch): def test_extract_failed_sources_empty_when_sql_installed(tmp_path): """#2543: successful extracts do not appear in failed_sources.""" - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip s = tmp_path / "schema.sql"; s.write_text("CREATE TABLE users (id INT);\n") py = tmp_path / "main.py"; py.write_text("def main():\n return 1\n") result = extract([s, py], cache_root=tmp_path) @@ -3647,7 +3657,7 @@ def test_extract_failed_sources_empty_when_sql_installed(tmp_path): def test_extract_no_missing_dep_warning_when_sql_installed(tmp_path, capsys): - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip s = tmp_path / "schema.sql"; s.write_text("CREATE TABLE users (id INT);\n") extract([s], cache_root=tmp_path) err = capsys.readouterr().err @@ -3661,7 +3671,7 @@ def test_extract_sql_reports_load_failure_not_missing(tmp_path, monkeypatch): # `pip install` — but surface the real load exception instead. import builtins from graphify.extractors.sql import extract_sql - pytest.importorskip("tree_sitter_sql") # find_spec must see it as installed + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip # find_spec must see it as installed _orig_import = builtins.__import__ @@ -3682,7 +3692,7 @@ def test_extract_warns_sql_grammar_failed_to_load(tmp_path, capsys, monkeypatch) # grammar with the real cause and WITHOUT the misleading "install the extra" # hint, so the files are neither silently dropped nor sent to a no-op fix. import builtins - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip _orig_import = builtins.__import__ diff --git a/tests/test_multilang.py b/tests/test_multilang.py index cb390eebcc..028cda4cb1 100644 --- a/tests/test_multilang.py +++ b/tests/test_multilang.py @@ -450,39 +450,39 @@ def test_cache_miss_after_file_change(tmp_path): # ── SQL ─────────────────────────────────────────────────────────────────────── -def _extract_sql_or_skip(fixture: str = "sample.sql"): - pytest.importorskip("tree_sitter_sql") +def _extract_sql_required(fixture: str = "sample.sql"): + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip return extract_sql(FIXTURES / fixture) def test_sql_finds_tables(): - r = _extract_sql_or_skip() + r = _extract_sql_required() labels = [n["label"] for n in r["nodes"]] assert any("users" in l for l in labels) assert any("organizations" in l for l in labels) def test_sql_finds_view(): - r = _extract_sql_or_skip() + r = _extract_sql_required() labels = [n["label"] for n in r["nodes"]] assert any("active_users" in l for l in labels) def test_sql_finds_function(): - r = _extract_sql_or_skip() + r = _extract_sql_required() labels = [n["label"] for n in r["nodes"]] assert any("get_user" in l for l in labels) def test_sql_emits_foreign_key_edge(): - r = _extract_sql_or_skip() + r = _extract_sql_required() relations = {e["relation"] for e in r["edges"]} assert "references" in relations def test_sql_emits_reads_from_edge(): - r = _extract_sql_or_skip() + r = _extract_sql_required() relations = {e["relation"] for e in r["edges"]} assert "reads_from" in relations def test_sql_no_dangling_edges(): - r = _extract_sql_or_skip() + r = _extract_sql_required() node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: assert e["source"] in node_ids, f"dangling source: {e['source']}" @@ -495,7 +495,7 @@ def test_sql_cte_is_not_read_as_a_table(): with a same-named node from another language. The real table in the same FROM/JOIN must still resolve. """ - r = _extract_sql_or_skip("sample_cte.sql") + r = _extract_sql_required("sample_cte.sql") labels = [n["label"] for n in r["nodes"]] assert "levels" not in labels, "CTE name leaked into the graph as a table node" @@ -509,7 +509,7 @@ def test_sql_cte_is_not_read_as_a_table(): def test_sql_column_list_cte_is_not_read_as_a_table(tmp_path): """#2577: `WITH levels(a, b) AS (...)` — the name precedes a column list.""" - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip p = tmp_path / "schema.sql" p.write_text( "CREATE TABLE users (id INT, role TEXT);\n" @@ -527,7 +527,7 @@ def test_sql_cte_shadows_same_named_table_within_its_statement(tmp_path): """#2577: inside the declaring statement the CTE shadows a real same-named table (SQL scoping), so v1's FROM binds to the CTE and emits nothing; v2 has no CTE in scope and reads the real table. Exactly one deterministic edge.""" - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip p = tmp_path / "schema.sql" p.write_text( "CREATE TABLE levels (role TEXT);\n" @@ -546,7 +546,7 @@ def test_sql_subquery_cte_does_not_suppress_outer_real_table(tmp_path): """#2577 refinement: a WITH inside a subquery is scoped to that subquery only. A statement-wide pre-collect would also swallow the OUTER reference to the real `t2`, dropping a true edge — per-subtree scoping keeps it.""" - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip p = tmp_path / "schema.sql" p.write_text( "CREATE TABLE t2 (id INT);\n" @@ -567,7 +567,7 @@ def test_sql_cte_never_binds_to_cross_language_symbol(tmp_path): so _rewire_unique_stub_nodes bound it to a same-named symbol from ANOTHER language (schema_v_roles -> ui_levels). With the CTE excluded, no reads_from edge may target a TypeScript node.""" - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip sql = tmp_path / "schema.sql" sql.write_text( "CREATE TABLE users (id INT, role TEXT);\n" @@ -594,7 +594,7 @@ def test_sql_cross_file_fk_resolves_and_never_leaks_scan_path(tmp_path): minted a node-less id under the referencing file's own stem, which with absolute inputs leaked the machine path AND could never match the m1 definition, so prisma-style cross-migration FKs dangled.""" - pytest.importorskip("tree_sitter_sql") + import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip from graphify.ids import make_id m1 = tmp_path / "prisma" / "migrations" / "m1" @@ -647,7 +647,7 @@ def test_sql_cross_file_fk_resolves_and_never_leaks_scan_path(tmp_path): def test_sql_alter_table_fk_edge(): """ALTER TABLE ... FOREIGN KEY ... REFERENCES produces a references edge.""" - r = _extract_sql_or_skip("sample_alter_fk.sql") + r = _extract_sql_required("sample_alter_fk.sql") fk_edges = [e for e in r["edges"] if e["relation"] == "references"] assert len(fk_edges) >= 1 node_ids = {n["id"] for n in r["nodes"]} @@ -657,14 +657,14 @@ def test_sql_alter_table_fk_edge(): def test_sql_schema_qualified_names(): """Schema-qualified table names (Schema.Table) are preserved.""" - r = _extract_sql_or_skip("sample_schema_qualified.sql") + r = _extract_sql_required("sample_schema_qualified.sql") labels = [n["label"] for n in r["nodes"]] assert any("Sales.Customer" in l for l in labels) assert any("Sales.SalesOrder" in l for l in labels) def test_sql_schema_qualified_alter_fk(): """ALTER TABLE with schema-qualified names produces correct edges.""" - r = _extract_sql_or_skip("sample_schema_qualified.sql") + r = _extract_sql_required("sample_schema_qualified.sql") fk_edges = [e for e in r["edges"] if e["relation"] == "references"] assert len(fk_edges) >= 1 node_ids = {n["id"] for n in r["nodes"]} @@ -675,7 +675,7 @@ def test_sql_schema_qualified_alter_fk(): def test_sql_plpgsql_functions_survive_parse_errors(): """PL/pgSQL bodies make tree-sitter-sql emit ERROR nodes; the functions must still be extracted (#1910), without cascading into later statements.""" - r = _extract_sql_or_skip("sample_plpgsql.sql") + r = _extract_sql_required("sample_plpgsql.sql") labels = [n["label"] for n in r["nodes"]] # Both PL/pgSQL functions extracted, schema-qualified name kept whole assert "exposed.important_function()" in labels @@ -694,7 +694,7 @@ def test_sql_plpgsql_functions_survive_parse_errors(): def test_sql_plpgsql_clean_function_not_double_emitted(): """A cleanly-parsed LANGUAGE sql function in the same file is emitted once.""" - r = _extract_sql_or_skip("sample_plpgsql.sql") + r = _extract_sql_required("sample_plpgsql.sql") labels = [n["label"] for n in r["nodes"]] assert labels.count("plain_sql_fn()") == 1 # And nothing else is duplicated either @@ -712,7 +712,7 @@ def test_sql_quoted_plpgsql_routines_are_recovered(): with an *unquoted* name recovered fine, which is why the drop looked like it depended only on the body statement. """ - r = _extract_sql_or_skip("sample_plpgsql_quoted.sql") + r = _extract_sql_required("sample_plpgsql_quoted.sql") labels = [n["label"] for n in r["nodes"]] for name in ( "raise_exception_fn", @@ -727,7 +727,7 @@ def test_sql_quoted_plpgsql_routines_are_recovered(): def test_sql_quoted_plpgsql_file_stays_clean(): """The #2180 recovery must not add junk, duplicates, or drop the tables.""" - r = _extract_sql_or_skip("sample_plpgsql_quoted.sql") + r = _extract_sql_required("sample_plpgsql_quoted.sql") labels = [n["label"] for n in r["nodes"]] # Tables before and after the unparseable routines still extract. assert any("accounts" in l for l in labels) diff --git a/tests/test_sql_core_dependency.py b/tests/test_sql_core_dependency.py new file mode 100644 index 0000000000..ad97baba83 --- /dev/null +++ b/tests/test_sql_core_dependency.py @@ -0,0 +1,100 @@ +"""SQL parsing is a core capability, not an optional extra (0.9.49). + +tree-sitter-sql used to live behind the [sql] extra, so a default +`uv tool install graphifyy` / `pipx install graphifyy` silently skipped every +.sql file in the corpus: the extractor bailed with an error, the #1745 warning +was the only signal, and until #2543 the failure was even stamped into the +incremental manifest. These tests pin the fix at the packaging layer so a +regression (the dependency sliding back into an extra) fails the suite rather +than resurfacing as a field report. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +from graphify import extract as extractmod +from graphify.extract import extract + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _pyproject() -> dict: + with open(REPO_ROOT / "pyproject.toml", "rb") as fh: + return tomllib.load(fh) + + +def test_tree_sitter_sql_is_a_core_dependency(): + """The grammar must be in [project.dependencies], not only in an extra.""" + deps = _pyproject()["project"]["dependencies"] + sql_deps = [d for d in deps if d.replace("_", "-").startswith("tree-sitter-sql")] + assert sql_deps, ( + "tree-sitter-sql is missing from [project.dependencies]; a default " + "install would silently skip every .sql file again (#1745)" + ) + + +def test_tree_sitter_sql_core_pin_stays_inside_supported_tree_sitter_range(): + """The core pin must carry an upper bound like every other grammar pin.""" + deps = _pyproject()["project"]["dependencies"] + (pin,) = [d for d in deps if d.replace("_", "-").startswith("tree-sitter-sql")] + assert "<" in pin, f"tree-sitter-sql core pin has no upper bound: {pin!r}" + + +def test_sql_extension_is_not_mapped_to_an_optional_extra(): + """#1745's hint map must not send users to a now-redundant [sql] extra.""" + assert ".sql" not in extractmod._EXTRA_FOR_EXTENSION, ( + "_EXTRA_FOR_EXTENSION still maps .sql to an extra; the 'install " + "graphifyy[sql]' hint is wrong now that the grammar is core" + ) + + +def test_tree_sitter_sql_imports_in_this_environment(): + """The dev environment itself must satisfy the core dependency.""" + import tree_sitter_sql # noqa: F401 + + +def test_sql_corpus_produces_structural_nodes_and_edges(tmp_path): + """Regression: a table/view/procedure corpus yields real structure. + + Guards the end-to-end path (dispatch -> tree-sitter parse -> node/edge + emission), not just the packaging declaration: representative DDL must + produce object nodes, `contains` edges from the file, a foreign-key + `references` edge, and `reads_from` edges from the view and procedure. + """ + schema = tmp_path / "schema.sql" + schema.write_text( + "CREATE TABLE organizations (\n" + " id INT PRIMARY KEY,\n" + " name TEXT NOT NULL\n" + ");\n" + "CREATE TABLE users (\n" + " id INT PRIMARY KEY,\n" + " org_id INT REFERENCES organizations(id)\n" + ");\n" + "CREATE VIEW active_users AS\n" + " SELECT * FROM users WHERE active = 1;\n" + "CREATE PROCEDURE prune_users()\n" + "BEGIN\n" + " DELETE FROM users WHERE id IN (SELECT id FROM active_users);\n" + "END;\n" + ) + + r = extract([schema]) + labels = {n["label"] for n in r["nodes"]} + assert {"organizations", "users", "active_users"} <= labels, labels + assert any(l.startswith("prune_users") for l in labels), labels + + relations = {e["relation"] for e in r["edges"]} + assert "contains" in relations, "file node must contain the SQL objects" + assert "references" in relations, "users.org_id FK must emit a references edge" + assert "reads_from" in relations, "view/procedure bodies must emit reads_from edges" + + node_ids = {n["id"] for n in r["nodes"]} + for e in r["edges"]: + assert e["source"] in node_ids, f"dangling edge source: {e['source']}" From a8cf54c1bc9b5533383a37b09ba0fccbdd61a400 Mon Sep 17 00:00:00 2001 From: Eduardo Garcia-Prieto Date: Sun, 23 Aug 2026 00:14:06 +1000 Subject: [PATCH 3/4] docs: SQL extraction is included by default since 0.9.49 Remove the sql row from the optional-extras table, add an upgrade note (existing extra/pipx-inject users just run the normal upgrade), and add the 0.9.49 changelog entry. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 ++++ README.md | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6930b4e76..8d67ef89a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## 0.9.49 (unreleased) + +- Feature: SQL schema extraction now works in every default install — `tree-sitter-sql` moved from the optional `[sql]` extra into the core dependencies (it ships prebuilt wheels for every supported platform, so the install stays toolchain-free). Previously a plain `uv tool install graphifyy` / `pipx install graphifyy` silently contributed nothing for `.sql` files until the user found the extra or ran `pipx inject`. The `sql` extra remains as a harmless alias so existing install scripts keep resolving, and the missing-grammar error now says the install is incomplete instead of pointing at an extra. Existing installs pick this up with the normal `uv tool upgrade graphifyy` / `pipx upgrade graphifyy`. + ## 0.9.48 (2026-08-20) - Fix: a control character in a node label or id no longer aborts the whole export; the GraphML and Obsidian exporters scrub only the characters those formats forbid (tab, newline, and non-ASCII letters are preserved), and `graph.json` and its byte-identity round-trip are untouched (#2897, thanks @abhay-codes07). diff --git a/README.md b/README.md index 0c14d207c9..fbcb764b53 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,6 @@ Codex users also need `multi_agent = true` under `[features]` in `~/.codex/confi | `anthropic` | Anthropic Claude API (`--backend claude`, uses `ANTHROPIC_API_KEY`) | `uv tool install "graphifyy[anthropic]"` | | `bedrock` | AWS Bedrock (uses IAM, no API key) | `uv tool install "graphifyy[bedrock]"` | | `azure` | Azure OpenAI Service (`--backend azure`, uses `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT`) | `uv tool install "graphifyy[openai]"` | -| `sql` | SQL schema extraction | `uv tool install "graphifyy[sql]"` | | `postgres` | Live PostgreSQL introspection (`--postgres DSN`) | `uv tool install "graphifyy[postgres]"` | | `dm` | BYOND DreamMaker `.dm`/`.dme` AST extraction (may need a C compiler + `python3-dev` if no wheel matches your platform) | `uv tool install "graphifyy[dm]"` | | `terraform` | Terraform / HCL `.tf`/`.tfvars`/`.hcl` AST extraction | `uv tool install "graphifyy[terraform]"` | @@ -272,6 +271,8 @@ Codex users also need `multi_agent = true` under `[features]` in `~/.codex/confi | `chinese` | Chinese query segmentation (jieba) | `uv tool install "graphifyy[chinese]"` | | `all` | Everything above | `uv tool install "graphifyy[all]"` | +> **SQL is no longer an extra.** Since 0.9.49, `.sql` schema extraction is included in every default install (`tree-sitter-sql` is a core dependency). If you previously installed the `sql` extra or ran `pipx inject graphifyy tree-sitter-sql`, the normal upgrade (`uv tool upgrade graphifyy` or `pipx upgrade graphifyy`) is all you need — the `sql` extra remains as a harmless alias for older install scripts. + --- From 3207a51a8633673c5052bdcc43301f3cf5ea3888 Mon Sep 17 00:00:00 2001 From: Eduardo Garcia-Prieto Date: Sun, 23 Aug 2026 13:18:36 +1000 Subject: [PATCH 4/4] chore: leave the version bump and changelog entry to the release process Upstream cuts versions and batches changelog entries in maintainer commits (see 'chore: bump to 0.9.48'), so a contributor PR should not claim the next version number. Comments and messages now describe the grammar as a core dependency without naming a release. --- CHANGELOG.md | 4 ---- README.md | 2 +- graphify/extract.py | 4 ++-- graphify/extractors/sql.py | 4 ++-- pyproject.toml | 6 +++--- tests/test_extract.py | 10 +++++----- tests/test_multilang.py | 12 ++++++------ tests/test_sql_core_dependency.py | 2 +- uv.lock | 2 +- 9 files changed, 21 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d67ef89a8..b6930b4e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,6 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) -## 0.9.49 (unreleased) - -- Feature: SQL schema extraction now works in every default install — `tree-sitter-sql` moved from the optional `[sql]` extra into the core dependencies (it ships prebuilt wheels for every supported platform, so the install stays toolchain-free). Previously a plain `uv tool install graphifyy` / `pipx install graphifyy` silently contributed nothing for `.sql` files until the user found the extra or ran `pipx inject`. The `sql` extra remains as a harmless alias so existing install scripts keep resolving, and the missing-grammar error now says the install is incomplete instead of pointing at an extra. Existing installs pick this up with the normal `uv tool upgrade graphifyy` / `pipx upgrade graphifyy`. - ## 0.9.48 (2026-08-20) - Fix: a control character in a node label or id no longer aborts the whole export; the GraphML and Obsidian exporters scrub only the characters those formats forbid (tab, newline, and non-ASCII letters are preserved), and `graph.json` and its byte-identity round-trip are untouched (#2897, thanks @abhay-codes07). diff --git a/README.md b/README.md index fbcb764b53..7c6f824db3 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ Codex users also need `multi_agent = true` under `[features]` in `~/.codex/confi | `chinese` | Chinese query segmentation (jieba) | `uv tool install "graphifyy[chinese]"` | | `all` | Everything above | `uv tool install "graphifyy[all]"` | -> **SQL is no longer an extra.** Since 0.9.49, `.sql` schema extraction is included in every default install (`tree-sitter-sql` is a core dependency). If you previously installed the `sql` extra or ran `pipx inject graphifyy tree-sitter-sql`, the normal upgrade (`uv tool upgrade graphifyy` or `pipx upgrade graphifyy`) is all you need — the `sql` extra remains as a harmless alias for older install scripts. +> **SQL is no longer an extra.** `.sql` schema extraction is included in every default install (`tree-sitter-sql` is a core dependency). If you previously installed the `sql` extra or ran `pipx inject graphifyy tree-sitter-sql`, the normal upgrade (`uv tool upgrade graphifyy` or `pipx upgrade graphifyy`) is all you need — the `sql` extra remains as a harmless alias for older install scripts. diff --git a/graphify/extract.py b/graphify/extract.py index 9e6880388c..a896476568 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -5073,8 +5073,8 @@ def add_existing_edge(edge: dict) -> None: # rather than falling back like Pascal does. Used by the #1745 warning in # extract() to tell the user which extra restores the language. _EXTRA_FOR_EXTENSION = { - # .sql is deliberately absent: tree-sitter-sql became a core dependency in - # 0.9.49, so a missing grammar means a broken install, not a missing extra + # .sql is deliberately absent: tree-sitter-sql is a core dependency, so a + # missing grammar means a broken install, not a missing extra # — the extractor's own error names the direct pip install that repairs it. ".tf": "terraform", ".tfvars": "terraform", diff --git a/graphify/extractors/sql.py b/graphify/extractors/sql.py index 0f440f1b49..3c0a947044 100644 --- a/graphify/extractors/sql.py +++ b/graphify/extractors/sql.py @@ -41,8 +41,8 @@ def extract_sql(path: Path, content: str | bytes | None = None) -> dict: # and surface the real exception in the latter case. if importlib.util.find_spec("tree_sitter_sql") is None: return {"nodes": [], "edges": [], - "error": ("tree_sitter_sql not installed. It is a core dependency " - "since 0.9.49, so this install is incomplete — reinstall " + "error": ("tree_sitter_sql not installed. It is a core dependency, " + "so this install is incomplete — reinstall " "graphifyy or run: pip install tree-sitter-sql")} return {"nodes": [], "edges": [], "error": f"tree_sitter_sql is installed but failed to load: {e}"} diff --git a/pyproject.toml b/pyproject.toml index 4dfcf5d642..29406fcfae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.9.49" +version = "0.9.48" description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = "Apache-2.0" @@ -41,7 +41,7 @@ dependencies = [ "tree-sitter-fortran>=0.6,<0.8", "tree-sitter-bash>=0.23,<0.27", "tree-sitter-json>=0.23,<0.26", - # Core since 0.9.49 (was the [sql] extra): .sql files are a mainstream + # Core dependency (was the [sql] extra): .sql files are a mainstream # corpus language and the extra-gated grammar made a default install # silently skip them (#1745). Ships prebuilt abi3 wheels for every # supported platform (win/macOS/Linux glibc+musl, x86_64+arm64) from @@ -81,7 +81,7 @@ anthropic = ["anthropic"] gemini = ["openai", "tiktoken"] openai = ["openai", "tiktoken"] chinese = ["jieba"] -# tree-sitter-sql moved to core dependencies in 0.9.49. The extra is kept as +# tree-sitter-sql is now a core dependency. The extra is kept as # an alias so existing `graphifyy[sql]` install commands and scripts keep # resolving; it adds nothing beyond the core install. sql = ["tree-sitter-sql>=0.3.9,<0.4"] diff --git a/tests/test_extract.py b/tests/test_extract.py index 5c58c8dcd4..b8adbdb6b4 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -3614,7 +3614,7 @@ def test_extract_warns_when_sql_grammar_missing(tmp_path, capsys, monkeypatch): # #1745: .sql HAS a dispatch entry, so the #1689 warning can't fire, and # extract_sql returns an "error" result when tree-sitter-sql is absent, so # the #1666 warning skips it too. The files must not vanish silently. - # Since 0.9.49 the grammar is a core dependency, so absence means a broken + # The grammar is a core dependency, so absence means a broken # install: the warning must name the direct repair, not a [sql] extra. # The dev environment now ships the grammar, so simulate genuine absence # by failing the import AND blanking find_spec for this one module. @@ -3649,7 +3649,7 @@ def test_extract_warns_when_sql_grammar_missing(tmp_path, capsys, monkeypatch): def test_extract_failed_sources_empty_when_sql_installed(tmp_path): """#2543: successful extracts do not appear in failed_sources.""" - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip s = tmp_path / "schema.sql"; s.write_text("CREATE TABLE users (id INT);\n") py = tmp_path / "main.py"; py.write_text("def main():\n return 1\n") result = extract([s, py], cache_root=tmp_path) @@ -3657,7 +3657,7 @@ def test_extract_failed_sources_empty_when_sql_installed(tmp_path): def test_extract_no_missing_dep_warning_when_sql_installed(tmp_path, capsys): - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip s = tmp_path / "schema.sql"; s.write_text("CREATE TABLE users (id INT);\n") extract([s], cache_root=tmp_path) err = capsys.readouterr().err @@ -3671,7 +3671,7 @@ def test_extract_sql_reports_load_failure_not_missing(tmp_path, monkeypatch): # `pip install` — but surface the real load exception instead. import builtins from graphify.extractors.sql import extract_sql - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip # find_spec must see it as installed + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip # find_spec must see it as installed _orig_import = builtins.__import__ @@ -3692,7 +3692,7 @@ def test_extract_warns_sql_grammar_failed_to_load(tmp_path, capsys, monkeypatch) # grammar with the real cause and WITHOUT the misleading "install the extra" # hint, so the files are neither silently dropped nor sent to a no-op fix. import builtins - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip _orig_import = builtins.__import__ diff --git a/tests/test_multilang.py b/tests/test_multilang.py index 028cda4cb1..f51adec36b 100644 --- a/tests/test_multilang.py +++ b/tests/test_multilang.py @@ -451,7 +451,7 @@ def test_cache_miss_after_file_change(tmp_path): # ── SQL ─────────────────────────────────────────────────────────────────────── def _extract_sql_required(fixture: str = "sample.sql"): - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip return extract_sql(FIXTURES / fixture) @@ -509,7 +509,7 @@ def test_sql_cte_is_not_read_as_a_table(): def test_sql_column_list_cte_is_not_read_as_a_table(tmp_path): """#2577: `WITH levels(a, b) AS (...)` — the name precedes a column list.""" - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip p = tmp_path / "schema.sql" p.write_text( "CREATE TABLE users (id INT, role TEXT);\n" @@ -527,7 +527,7 @@ def test_sql_cte_shadows_same_named_table_within_its_statement(tmp_path): """#2577: inside the declaring statement the CTE shadows a real same-named table (SQL scoping), so v1's FROM binds to the CTE and emits nothing; v2 has no CTE in scope and reads the real table. Exactly one deterministic edge.""" - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip p = tmp_path / "schema.sql" p.write_text( "CREATE TABLE levels (role TEXT);\n" @@ -546,7 +546,7 @@ def test_sql_subquery_cte_does_not_suppress_outer_real_table(tmp_path): """#2577 refinement: a WITH inside a subquery is scoped to that subquery only. A statement-wide pre-collect would also swallow the OUTER reference to the real `t2`, dropping a true edge — per-subtree scoping keeps it.""" - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip p = tmp_path / "schema.sql" p.write_text( "CREATE TABLE t2 (id INT);\n" @@ -567,7 +567,7 @@ def test_sql_cte_never_binds_to_cross_language_symbol(tmp_path): so _rewire_unique_stub_nodes bound it to a same-named symbol from ANOTHER language (schema_v_roles -> ui_levels). With the CTE excluded, no reads_from edge may target a TypeScript node.""" - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip sql = tmp_path / "schema.sql" sql.write_text( "CREATE TABLE users (id INT, role TEXT);\n" @@ -594,7 +594,7 @@ def test_sql_cross_file_fk_resolves_and_never_leaks_scan_path(tmp_path): minted a node-less id under the referencing file's own stem, which with absolute inputs leaked the machine path AND could never match the m1 definition, so prisma-style cross-migration FKs dangled.""" - import tree_sitter_sql # noqa: F401 — core dep since 0.9.49; absence must FAIL, not skip + import tree_sitter_sql # noqa: F401 — core dependency; absence must FAIL, not skip from graphify.ids import make_id m1 = tmp_path / "prisma" / "migrations" / "m1" diff --git a/tests/test_sql_core_dependency.py b/tests/test_sql_core_dependency.py index ad97baba83..03dff2b38a 100644 --- a/tests/test_sql_core_dependency.py +++ b/tests/test_sql_core_dependency.py @@ -1,4 +1,4 @@ -"""SQL parsing is a core capability, not an optional extra (0.9.49). +"""SQL parsing is a core capability, not an optional extra. tree-sitter-sql used to live behind the [sql] extra, so a default `uv tool install graphifyy` / `pipx install graphifyy` silently skipped every diff --git a/uv.lock b/uv.lock index 64da1f74d9..5275514aef 100644 --- a/uv.lock +++ b/uv.lock @@ -1090,7 +1090,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.49" +version = "0.9.48" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },