SQL orchestration with templating, semantic layer, and table/column/value lineage
Project description
unwind
SQL orchestration with Jinja templating, a semantic layer, and lineage at the table, column, and value level. An alternative to Dbt and SQLMesh, focused on deterministic value-lineage backed by a SQLGlot AST and a DuckDB execution engine.
A project is a directory of .sql and .py files. SQL models give full
table/column/value lineage via the SQLGlot AST. Python models are opaque to
lineage but participate in the DAG like any other node — typical use cases
are sources (loading data from Oracle / Postgres / Parquet) and sinks
(exporting a final table elsewhere). The two kinds compose freely.
Status: alpha — table, column, and deterministic value lineage all work end-to-end, plus a web UI and an optional LLM investigator (pydantic-ai, multi-provider).
Install
The core install (duckdb, jinja2, sqlglot) is enough to load a project,
plan its DAG, run it, and compute table / column / value lineage in Python.
Anything that talks to the outside world — the web UI, the LLM investigator,
the source connectors, the SQL-stored-in-DB loader — lives behind an
optional extra so you only install what you actually use.
| Extra | What it enables | Pulls in |
|---|---|---|
web |
Project.show() (FastAPI/Uvicorn DAG explorer) |
fastapi, uvicorn[standard] |
llm |
Project.get_investigator() (multi-provider via pydantic-ai) |
pydantic-ai |
db |
unwind.load_from_db() + connectors.sqlalchemy(...) |
sqlalchemy |
connectors |
unwind.connectors.parquet/oracle/... for Python models |
pyarrow, oracledb |
all |
Everything above, no decisions required | sum of the four extras |
Pick what you need:
pip install unwind-sql # core only
pip install "unwind-sql[web]" # add the web UI
pip install "unwind-sql[web,llm]" # web UI + LLM investigator
pip install "unwind-sql[connectors]" # Python-model loaders (parquet, oracle, …)
pip install "unwind-sql[all]" # everything — recommended for trying it out
For local development on this repo:
uv sync --all-extras # equivalent to the `[all]` extra
Run the example
The bundled example computes net margin per order over 5 raw/ref tables, with a Jinja macro and a final aggregation by warehouse:
cd example/
uv run python generate_data.py # one-time: write the 5 parquet sources
uv run python main.py
The script:
- Loads
models/(mixed.sql+.py) and runs the full DAG on DuckDB. - Prints the table & column lineage of
int_tax_costs.local_tax_fee. - Traces
local_tax_feefororder_id="ORD-7892"back to the raw values that contributed (raw_orders.gross_sales = 500.0,ref_local_taxes.tax_pct = 0.20, …). - If
OPENAI_API_KEYis set, asks an LLM to explain the trace in plain language and flag suspicious values (usespydantic-ai, swap providers by passingllm_provider="anthropic"etc.). - Opens a browser tab on the web UI (Cytoscape.js DAG + per-column
lineage tree). Press
Ctrl+Cto stop.
raw_orders in the example is a Python model (example/models/raw_orders.py)
backed by a one-line load_data() helper. Switch its backend without touching
any SQL:
UNWIND_SOURCE_MODE=parquet uv run python main.py # default
UNWIND_SOURCE_MODE=oracle uv run python main.py # needs oracledb + ORACLE_DSN
See example/main.py and example/README.md for the model walkthrough.
Python models
A file in models/ whose Python module defines a top-level callable
model(context) is recognised as a node of the DAG. Anything else in
models/*.py is imported as a plain helper module — so from helpers import load_data just works.
# models/raw_orders.py
from helpers import load_data
GROUP = "costs"
MATERIALIZED = "view" # "table" (default) or "view"
DEPENDS_ON = () # tuple of upstream model names
def model(context):
# context.duckdb (live connection), context.variables (Jinja vars),
# context.project_root (the path passed to unwind.load)
return load_data("raw_orders") # pyarrow.Table, DataFrame, str (SQL), or None
The return value is registered into DuckDB (zero-copy for Arrow). For a
sink, do the work via context.duckdb and return None.
unwind.connectors ships tiny helpers (parquet, oracle, sqlalchemy) so
your load_data() reduces to a few lines of branching.
Test
uv run pytest
uv run ruff check
uv run ty check
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
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 unwind_sql-0.2.0.tar.gz.
File metadata
- Download URL: unwind_sql-0.2.0.tar.gz
- Upload date:
- Size: 615.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c22bb22cdbdf2d5010ea5d4d46710115c2bc4841219c3c9009505d55030c50e
|
|
| MD5 |
d87d05ffd42fb89f5246cb0902fd70ad
|
|
| BLAKE2b-256 |
e4b9bb4be607cdc7fa6cb7351337fc6d6db7fff84f4c0e68965a9f4f606b033c
|
File details
Details for the file unwind_sql-0.2.0-py3-none-any.whl.
File metadata
- Download URL: unwind_sql-0.2.0-py3-none-any.whl
- Upload date:
- Size: 627.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ec7aa2140ff10e490d2cf5f06ccbc1595566f0795a1e7492b0d54984a40f1f3
|
|
| MD5 |
c7e61cb9c91a958405279ab705293f41
|
|
| BLAKE2b-256 |
47ac508cecf25b073a652a12a1d16a68f9450c0355dd4dc051edf761b8aee1ef
|