diff --git a/graphify/pg_introspect.py b/graphify/pg_introspect.py index d183c051..09a702db 100644 --- a/graphify/pg_introspect.py +++ b/graphify/pg_introspect.py @@ -154,4 +154,12 @@ def introspect_postgres(dsn: str | None = None) -> dict: # Pass virtual path and in-memory DDL content to extract_sql result = extract_sql(virtual_path, content=ddl_string) + # extract_sql() reports a missing grammar as an 'error' key with empty + # nodes/edges; surface it instead of letting the CLI print + # "PostgreSQL: 0 nodes" as if the database were empty. + if result.get("error"): + raise ImportError( + f"{result['error']} (required by --postgres; " + "install with: pip install 'graphifyy[postgres]')" + ) return result \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 237ed434..eb9ca1d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,10 @@ svg = ["matplotlib", "numpy>=2.0; python_version >= '3.13'"] leiden = ["graspologic; python_version < '3.13'"] office = ["python-docx", "openpyxl"] google = ["openpyxl"] -postgres = ["psycopg[binary]"] +# --postgres reconstructs DDL and feeds it to extract_sql(), which needs the +# tree-sitter-sql grammar; without it introspection silently returns 0 nodes, +# so the postgres extra must carry the grammar too. +postgres = ["psycopg[binary]", "tree-sitter-sql"] video = ["faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9"] kimi = ["openai", "tiktoken"] ollama = ["openai"] diff --git a/tests/test_pg_introspect.py b/tests/test_pg_introspect.py index e29e4374..39eb6812 100644 --- a/tests/test_pg_introspect.py +++ b/tests/test_pg_introspect.py @@ -367,6 +367,26 @@ def test_pg_introspect_import_error(): assert "graphifyy[postgres]" in str(exc_info.value) +def test_pg_introspect_grammar_error_surfaces(): + """If extract_sql reports an error (e.g. tree-sitter-sql missing), the + introspection must raise instead of returning an empty graph — otherwise + the CLI prints "PostgreSQL: 0 nodes" as if the database were empty.""" + mock_psycopg = _make_mock_psycopg( + [("public", "users", "BASE TABLE")], [], [], [] + ) + with patch.dict("sys.modules", {"psycopg": mock_psycopg}): + with patch( + "graphify.pg_introspect.extract_sql", + return_value={ + "nodes": [], + "edges": [], + "error": "tree_sitter_sql not installed. Run: pip install tree-sitter-sql", + }, + ): + with pytest.raises(ImportError, match="tree_sitter_sql not installed"): + introspect_postgres("postgresql://myhost/mydb") + + def test_pg_introspect_uri_forward_slashes(): """Assert that the virtual path in postgresql introspection output uses forward slashes on all platforms.""" mock_psycopg = _make_mock_psycopg([], [], [], [], host="some-host", dbname="some-db")