SQL code graph analyzer and lineage tracer
Project description
sql-code-graph
No backward-compatibility guarantees. APIs, CLI flags, and graph schema may change between releases without a deprecation period. Pin to an exact version in production. Re-indexing is always the migration path.
Dialect support. sqlcg is built on sqlglot, so in theory it can work for any dialect sqlglot parses. In practice we've only tested it against a production Snowflake warehouse (~1,400 SQL files) — and getting that one dialect working properly was already challenging. Other dialects (
bigquery,postgres,ansi,tsql,dbt) may well work, but their lineage hasn't been validated, so expect rough edges. We'd gladly collaborate to get another dialect integrated — open an issue with a minimal, anonymised corpus we can test against.
SQL lineage and dependency analysis as an MCP server for Claude Code.
Indexes a directory of .sql files into a graph database and exposes lineage
queries as MCP tools — so Claude can answer questions like "what tables does
this view depend on?" or "where is orders.customer_id derived from?"
without reading every file.
Quick start
Choose one:
Permanent install (recommended):
uv tool install sql-code-graph # Fast, managed, no isolation needed
sqlcg install # Register MCP server in Claude Code
One-shot try (cold cache warning):
uvx sql-code-graph # First run is slow (downloads deps)
# Subsequent runs use cache, ~1s startup
Restart Claude Code, then inside your project ask:
Index my SQL files at ./sql --dialect snowflake
That's it. The MCP tools are now available to Claude in every conversation for that project.
Workflow (3 steps)
- Initialize:
sqlcg db init - Index:
sqlcg index ./sql --dialect snowflake - Keep fresh:
sqlcg git install-hooks(auto-reindex on branch switch), orsqlcg watch ./sqlto re-index automatically as you edit files (both optional)
Full setup (recommended)
# 1. Install (this project uses uv — never pip)
uv tool install sql-code-graph # global tool (recommended)
# — or, from a source checkout:
# git clone https://github.com/Warhorze/sql-code-graph
# cd sql-code-graph && uv sync # then run via `uv run sqlcg …`
# 2. Register with Claude Code (writes ~/.claude.json)
sqlcg install
# 3. Restart Claude Code
# 4. Index your SQL repo
# Only git-tracked files are indexed — build artefacts, node_modules,
# and .venv are ignored automatically.
sqlcg db init
sqlcg index ./sql --dialect snowflake # snowflake is the only tested dialect
# 5. (Optional) Keep the graph fresh on branch switches
cd /your/sql/repo
sqlcg git install-hooks
Step 5 installs a post-checkout git hook that re-indexes automatically
whenever you switch branches. Without it the graph may be stale after a
git checkout until you re-run sqlcg index manually.
Dialect config
To avoid passing --dialect every time, create .sqlcg.toml in your repo root:
[sqlcg]
dialect = "snowflake" # the only tested dialect
The git hook and sqlcg index --dialect auto both read this file.
Add to your project CLAUDE.md (recommended)
Adding a short note to your project's CLAUDE.md helps Claude know the tools
are available and when to use them:
## SQL lineage
This project uses sql-code-graph. MCP tools are available:
- `db_info` — check graph health and parse quality before running lineage queries
- `index_repo` — index or re-index a directory of SQL files
- `find_table_usages` — find all queries that read a table
- `trace_column_lineage` — trace where a column's value comes from
- `get_upstream_dependencies` / `get_downstream_dependencies` — dependency chains
- `search_sql_pattern` — full-text search across all indexed SQL
- `execute_sql` — raw read-only SQL query for advanced analysis
The MCP server works without this — Claude can discover the tools on its own — but the CLAUDE.md snippet ensures they get used proactively.
Parse quality
After indexing, sqlcg gain shows a parse quality breakdown that tells you how
much column-level lineage was extracted:
| Quality | Meaning | Tools affected |
|---|---|---|
FULL |
Column-level lineage extracted | All tools work |
TABLE_ONLY |
Table edges only — no column lineage | trace_column_lineage, get_*_dependencies return empty |
SCRIPTING_FALLBACK |
sqlglot fell back to raw command node | Partial table edges; column lineage unavailable |
FAILED |
File failed to parse entirely | File invisible to all queries |
Quality is shown per-file after sqlcg index and in sqlcg gain Section F.
list_dialects_and_repos() warns when scripting fallback exceeds 20% of queries.
What causes TABLE_ONLY? Mostly SELECT * — sqlglot can't trace column names through
a wildcard without knowing the source table's columns. See Resolving SELECT *
below for how sqlcg expands wildcards automatically from your DDL and CTAS bodies.
What causes SCRIPTING_FALLBACK? Snowflake $$ procedure bodies or BEGIN…END scripting
blocks. sqlglot parses the block as a raw Command node and extracts DML via tokenizer
fallback. Table edges are usually correct; column edges are not.
Check sqlcg db info for the parsing mode distribution across all indexed queries.
Resolving SELECT *
A SELECT * ETL produces TABLE_ONLY parse quality because sqlglot needs the source
table's column list to expand the wildcard into individual columns. sqlcg resolves this
automatically, with no extra setup — there is no CSV to export or command to run.
Wildcards are expanded from two sources harvested while indexing:
- DDL files —
CREATE TABLE/CREATE VIEWstatements give sqlcg the column list for any table they define. - Cross-file CTAS bodies — a
CREATE TABLE … AS SELECT(or CTE) in one file is used to resolveSELECT *against that table in another file.
So the only thing you need to do is index the DDL alongside your ETLs — point sqlcg index at a path that contains both (e.g. the repo root, or both ddl/ and etl/). The
more of your CREATE statements sqlcg can see, the more wildcards it resolves.
sqlcg index . --dialect snowflake # index DDL + ETLs together
After indexing, sqlcg db info shows non-zero STAR_EXPANSION lineage edges, and
trace_column_lineage returns results for queries that previously returned empty.
Note: an earlier
sqlcg load-schemacommand was removed in 2026-05 — it fed column names into the parse/qualify loop (hot path) and measured zero edge delta on a real warehouse. The newsqlcg catalog load(see below) is different: it writesHAS_COLUMNrows after indexing and never touches the parser.
INFORMATION_SCHEMA catalog enrichment
If your warehouse DDL lives in XML changelogs (Liquibase, Flyway) or other non-SQL
formats that sqlcg cannot parse, you can supply an INFORMATION_SCHEMA.COLUMNS export
to enrich edge health without re-parsing anything.
# Export from Snowflake (or any SQL client):
# SELECT table_catalog, table_schema, table_name, column_name, ordinal_position, data_type
# FROM information_schema.columns
# WHERE table_schema NOT IN ('INFORMATION_SCHEMA')
# ORDER BY table_schema, table_name, ordinal_position;
# → save as columns.csv
sqlcg catalog load columns.csv
sqlcg gain # check the delta
What it does:
- Reads
table_catalog(optional),table_schema,table_name,column_namecolumns. - Delimiter sniffed (
,or;); extra columns ignored; names case-folded to lowercase. - Creates
SqlColumnnodes andHAS_COLUMN(source='information_schema')edges. - DDL-sourced rows are never overwritten (
ddl > information_schema > usageprecedence). - Idempotent — running it twice produces no row growth.
- Catalog-sourced rows survive per-file reindex (
sqlcg reindex).
Persisting across full rebuilds: add to .sqlcg.toml:
[sqlcg.catalog]
path = "/path/to/columns.csv"
With this set, sqlcg index automatically re-applies the catalog at the end of each
full rebuild. If the file is missing, indexing succeeds with a warning.
What it does not do:
- It does not feed column names into the parse/qualify hot path —
SELECT *expansion uses only DDL files and cross-file CTAS bodies (unchanged). - It does not create new lineage edges — it only validates / invalidates existing ones.
MCP tools reference
| Tool | Description |
|---|---|
index_repo(repo_path, dialect) |
Index a directory of SQL files |
| Lineage & dependencies | |
trace_column_lineage(table_col) |
Trace a column's value upstream to its sources |
get_upstream_dependencies(table_col) |
Full upstream dependency chain |
get_downstream_dependencies(table_col) |
Full downstream dependency chain |
find_table_usages(table_name) |
Find all queries that read a table (3-signal: direct SELECT, SELECT *, or column-lineage CTE read) |
find_definition(table_qualified) |
Find where a table/view is defined |
| Change impact | |
get_change_scope(table_qualified) |
Blast radius of changing a table (impact + risk); gating_join_tables field adds row-reachability supplement (see caveats) |
diff_impact(changed_files) |
What a set of changed files affects downstream; gating_join_tables field adds row-reachability supplement (see caveats) |
get_backfill_order(table_qualified) |
Topological rebuild/backfill order |
scope_change(target) |
Synthesised change-scope summary for a target |
get_empty_propagation(tables) |
Downstream blast radius when named table(s) are empty — two views: value-derivation (primary) + row-reachability (supplement) |
get_pr_impact(base_ref) |
Detect producers a PR dropped + their blast radius; code-regression detection, not runtime monitoring |
| Analysis | |
get_hub_ranking(k) |
Top-k tables by downstream dependent count (hub/centrality) |
analyze_unused() |
Tables with no within-corpus consumers (dead-code candidates, heuristic); uses 3-signal "used" definition — direct SELECT, SELECT *, or CTE-derived column read |
| Search & meta | |
search_sql_pattern(query) |
Full-text search across indexed SQL |
list_dialects_and_repos() |
List indexed repos and dialects (catalogue) |
db_info() |
Graph health, node counts, parse quality breakdown, warnings, freshness (indexed SHA vs HEAD) |
execute_sql(query) |
Raw read-only SQL query against the graph |
submit_feedback(...) |
Report a false positive/negative to improve metrics |
Input format: lineage/dependency tools expect a schema-qualified column reference —
schema.table.column(e.g.ba.orders.customer_id), not a baretable.column. Each returned node carries bothname(the bare column) andtable(the owningschema.table), so results are navigable without a second lookup.
Provenance fields: lineage edges now carry
file,line, andexpression(where the lineage was derived from), aconfidenceof1.0for plainly-parsed facts (lower for inferred edges, with areason), and atable_kind(table/cte/derived/external) so CTE and derived aliases are distinguishable from real tables.
LLM agent tip: call
db_info()before lineage queries to check thatSqlColumn > 0andwarningsis empty. Ifparse_quality["scripting_block"]is high, column lineage will be limited for those files — use table-level tools (find_table_usages,get_*_dependencies) instead.
CLI reference
Full option reference: docs/cli.md
sqlcg install # register MCP server in Claude Code
sqlcg db init # initialise graph database
sqlcg index <path> --dialect snowflake # index SQL files (snowflake is the tested dialect)
sqlcg index <path> --dialect auto # read dialect from .sqlcg.toml
sqlcg index <path> --profile # index + print per-stage timing and slowest files
sqlcg index <path> --include-working-tree # also index uncommitted changes (marks graph dirty)
sqlcg reindex <path> --from <sha> --to <sha> # incremental resync of only changed files
sqlcg analyze unused # tables with no within-corpus consumers (3-signal: direct SELECT, SELECT *, or CTE-derived column read)
sqlcg analyze empty-impact <table>... # downstream blast radius when named table(s) are empty (two views)
sqlcg analyze pr-impact --base <ref> # detect producers a PR dropped + their blast radius (code-regression detection)
sqlcg analyze upstream/downstream # trace lineage from the CLI
sqlcg find table/column/pattern # search the graph
sqlcg watch <path> # watch a directory and re-index on SQL file changes
sqlcg viz # generate a self-contained graph-explorer HTML
sqlcg db info # graph stats + freshness (indexed SHA vs HEAD)
sqlcg git install-hooks # install post-checkout + post-merge resync hooks
sqlcg gain # show usage metrics
sqlcg report # generate FP/error report
sqlcg mcp best-practices # print the fact/heuristic boundary for the MCP tools
sqlcg mcp start # start MCP server manually
sqlcg mcp status # server status JSON — incl. running version + stale_by_version
sqlcg mcp stop # stop the running MCP server gracefully
sqlcg mcp restart # stop the server (client must respawn it)
sqlcg version # show installed version
Staying on the latest build
The installed package, the CLI, and the running MCP server all report the same
version (sqlcg.__version__). After upgrading, an editor may still be talking to an
old MCP process — sqlcg mcp status surfaces this directly: it reports the running
server's version and a stale_by_version flag that is true when the live server
differs from the installed build. Re-running sqlcg install stops the stale server so
your editor respawns it on the new build, so you never debug against an outdated server.
Reads while the server is running
DuckDB takes an exclusive lock on the database file, so while the MCP server is
live it holds that lock (other processes cannot open the file, even read-only).
CLI read commands (find, analyze, db info, list-repos,
gain) automatically route their query through the running server over its
control socket and return rows as usual — no flag, no config. When no server is
running they open the database directly, exactly as before. If the server is
mid-reindex the read waits for it to finish rather than failing with
"Database is locked".
Visualising the graph (viz)
sqlcg viz generates a self-contained graph-explorer HTML from the live
graph — every node, edge, the force-graph library, and the facet data are inlined
into a single file, so it opens by double-click in a browser with no server or
external resources.
sqlcg viz # writes table_graph.html in the current dir
sqlcg viz --out lineage.html # choose the output path
In the browser the filter composes schema ∩ kind ∩ tag (multi-select per facet) plus a job dropdown; an edge renders only when both endpoints are visible. Table, view, and temp nodes are on by default; CTE and derived nodes are off (toggleable).
Two optional CSV facets let you colour and slice the graph by your own
conventions — both are pattern,label[,color] with a required header, patterns
are case-insensitive fnmatch globs (*, ?) matched against the qualified
table name:
sqlcg viz --tags tags.csv # legend swatches: colour + filter the graph
sqlcg viz --jobs jobs.csv # populate the job dropdown (filter only)
Omit --tags to hide the tag legend; omit --jobs to leave the dropdown empty.
Supported dialects
sqlcg is built on sqlglot, so other dialects can be parsed in theory. Only Snowflake has been tested against a real corpus, though — the table reflects what's actually been exercised, not what sqlglot can do.
| Dialect | Status |
|---|---|
snowflake |
✅ Tested against a production DWH (~1,400 files) |
bigquery |
⚠️ Unproven — parses via sqlglot, lineage not validated |
postgres |
⚠️ Unproven — parses via sqlglot, lineage not validated |
ansi |
⚠️ Unproven — parses via sqlglot, lineage not validated |
tsql |
⚠️ Unproven — parses via sqlglot, lineage not validated |
dbt |
⚠️ Unproven — via optional extra, lineage not validated |
Want another dialect properly supported? We'd be glad to collaborate — open an issue with a minimal, anonymised corpus we can develop and test against. Getting Snowflake right was real work, so a representative corpus is what makes the difference.
Development
git clone https://github.com/Warhorze/sql-code-graph
cd sql-code-graph
uv sync --all-extras
uv run pytest tests/unit
Issues
Bug reports and feature requests: github.com/Warhorze/sql-code-graph/issues
Questions and discussion: github.com/Warhorze/sql-code-graph/discussions
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sql_code_graph-1.43.3-py3-none-any.whl.
File metadata
- Download URL: sql_code_graph-1.43.3-py3-none-any.whl
- Upload date:
- Size: 435.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8f9c1c4e5edc3dac70e9157fe387cbc61f0cb4168117d9cb5d6f8b22fff0bc9
|
|
| MD5 |
957b4c26b040db24ab495d3edff476c4
|
|
| BLAKE2b-256 |
775c250124668f5a6f6d823e04cc61fd46c9c5d6385d96f68ba5e1bd4fb1aca6
|
Provenance
The following attestation bundles were made for sql_code_graph-1.43.3-py3-none-any.whl:
Publisher:
release.yml on Warhorze/sql-code-graph
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sql_code_graph-1.43.3-py3-none-any.whl -
Subject digest:
b8f9c1c4e5edc3dac70e9157fe387cbc61f0cb4168117d9cb5d6f8b22fff0bc9 - Sigstore transparency entry: 1850870163
- Sigstore integration time:
-
Permalink:
Warhorze/sql-code-graph@08d9354f25c1b189a90026290f393f1cd012c381 -
Branch / Tag:
refs/tags/v1.43.3 - Owner: https://github.com/Warhorze
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@08d9354f25c1b189a90026290f393f1cd012c381 -
Trigger Event:
push
-
Statement type: