DLens
GPS for your data: trace any dbt column to its source, see what breaks before you change it, with the file and line for every step.
DLens reads a dbt project, parses the compiled SQL with sqlglot against the real warehouse schema, and builds a column-level lineage graph. Every edge records how a column is derived (rename, transformation, aggregation, ...), the expression, and where in the model file it lives.
Status
v0.1.0: lineage CLI. It works on DuckDB dbt projects (dbt-core 1.12, dbt-duckdb 1.11), Python 3.12 and 3.13. The Q&A agent and the benchmark are not built yet: see the roadmap.
Quickstart
pip install dlens-lineage
git clone --depth 1 https://github.com/harshith769/dlens.git # for the example projects
cd dlens
dlens ingest corpora/jaffle_shop # runs dbt, shows what DLens will see
dlens trace customers.customer_lifetime_value -p corpora/jaffle_shop
dlens impact stg_orders.order_id -p corpora/jaffle_shop
dlens report -p corpora/jaffle_shop # parse quality per model
Use your own project the same way: dlens ingest path/to/dbt_project (DuckDB profile in the project
directory). ingest runs dbt, so it needs the project's seeds and sources to be buildable. The first
run takes about 10 seconds on jaffle_shop; after that the graph is cached in
target/dlens_graph.json and rebuilt when the manifest, your SQL, or the DLens version changes
(--rebuild forces it). Column ids are model.column.
Example output
The commands below ran on corpora/synthetic_shop, a
15-model project built to contain traps (renames, CASE, multi-hop joins, aggregates). Paths are
relative to the project; long lines are real, not wrapped.
dlens ingest corpora/synthetic_shop
models: 15
seeds: 6
sources: 0
exposures: 0
columns: 132
unmapped relations: 0
dlens trace fct_orders.revenue_finance -p corpora/synthetic_shop
fct_orders.revenue_finance
├─ int_customer_order_history.items_subtotal [TRANSFORMATION] int_customer_order_history.items_subtotal - int_customer_or… models/marts/fct_orders.sql:13-13
│ └─ int_order_financials.items_subtotal [IDENTITY] int_order_financials.items_subtotal AS items_subtotal models/intermediate/int_customer_order_history.sql:2-2
│ └─ int_orders_enriched.items_subtotal [IDENTITY] orders.items_subtotal AS items_subtotal models/intermediate/int_order_financials.sql:23-23
│ └─ int_order_items_enriched.line_amount [AGGREGATION] COALESCE(item_agg.items_subtotal, 0) AS items_subtotal <- S… models/intermediate/int_orders_enriched.sql:24-24
│ ├─ stg_order_items.quantity [TRANSFORMATION] items.quantity * items.unit_price AS line_amount models/intermediate/int_order_items_enriched.sql:8-8
│ │ └─ raw_order_items.quantity [IDENTITY] raw_order_items.quantity AS quantity models/staging/stg_order_items.sql:5-5
│ └─ stg_order_items.unit_price [TRANSFORMATION] items.quantity * items.unit_price AS line_amount models/intermediate/int_order_items_enriched.sql:8-8
│ └─ raw_order_items.unit_price [IDENTITY] raw_order_items.unit_price AS unit_price models/staging/stg_order_items.sql:6-6
└─ int_customer_order_history.refund_amount [TRANSFORMATION] int_customer_order_history.items_subtotal - int_customer_or… models/marts/fct_orders.sql:13-13
└─ int_order_financials.refund_amount [IDENTITY] int_order_financials.refund_amount AS refund_amount models/intermediate/int_customer_order_history.sql:2-2
└─ stg_refunds.refund_amt [AGGREGATION] COALESCE(refunds_per_order.refund_amount, 0) AS refund_amou… models/intermediate/int_order_financials.sql:26-26
└─ raw_refunds.refund_amt [IDENTITY] raw_refunds.refund_amt AS refund_amt models/staging/stg_refunds.sql:4-4
3 path(s), deepest 6 hop(s), 3 origin column(s)
dlens impact raw_orders.tax_usd -p corpora/synthetic_shop
raw_orders.tax_usd
└─ stg_orders.tax_amount [RENAME] raw_orders.tax_usd AS tax_amount models/staging/stg_orders.sql:6-6
└─ int_orders_enriched.order_tax [RENAME] orders.tax_amount AS order_tax models/intermediate/int_orders_enriched.sql:22-22
├─ int_order_financials.order_total [TRANSFORMATION] orders.items_subtotal + orders.order_tax AS order_total models/intermediate/int_order_financials.sql:25-25
│ └─ int_customer_order_history.order_total [IDENTITY] int_order_financials.order_total AS order_total models/intermediate/int_customer_order_history.sql:2-2
│ └─ fct_orders.order_total [IDENTITY] int_customer_order_history.order_total AS order_total models/marts/fct_orders.sql:10-10
└─ int_order_financials.sales_tax [RENAME] orders.order_tax AS sales_tax models/intermediate/int_order_financials.sql:24-24
└─ int_customer_order_history.sales_tax [IDENTITY] int_order_financials.sales_tax AS sales_tax models/intermediate/int_customer_order_history.sql:2-2
├─ fct_orders.revenue_marketing [TRANSFORMATION] int_customer_order_history.items_subtotal + int_customer_or… models/marts/fct_orders.sql:14-14
│ └─ fct_daily_revenue.revenue_marketing [AGGREGATION] SUM(fct_orders.revenue_marketing) AS revenue_marketing models/marts/fct_daily_revenue.sql:5-5
└─ fct_orders.sales_tax [IDENTITY] int_customer_order_history.sales_tax AS sales_tax models/marts/fct_orders.sql:9-9
└─ dim_customers.lifetime_value [AGGREGATION] COALESCE(order_agg.lifetime_value, 0) AS lifetime_value <- … models/marts/dim_customers.sql:21-21
11 affected column(s), 7 model(s)
depth 1: 1 column(s)
depth 2: 1 column(s)
depth 3: 2 column(s)
depth 4: 2 column(s)
depth 5: 3 column(s)
depth 6: 2 column(s)
models: dim_customers, fct_daily_revenue, fct_orders, int_customer_order_history, int_order_financials, int_orders_enriched, stg_orders
exposures: none
dlens report -p corpora/synthetic_shop
Parse quality per model:
dim_customers FULL
fct_daily_revenue FULL
fct_orders FULL
fct_payment_events FULL
int_customer_order_history FULL
int_order_financials FULL
int_order_items_enriched FULL
int_orders_enriched FULL
int_payment_events FULL
stg_customers FULL
stg_order_items FULL
stg_orders FULL
stg_payments FULL
stg_products FULL
stg_refunds FULL
Models: 15 FULL, 0 TABLE_ONLY, 0 FAILED
Edges: 115 (AGGREGATION 14, IDENTITY 58, RENAME 22, TRANSFORMATION 21)
Model-level citations: 0
Low-confidence edges: 0
Deferred indirect (window keys, v0.3): 2
Constant columns (no edges): 0
Support matrix
DLens is tested construct by construct against golden files. The table, including what is partial, unsupported and untested, is in the construct support matrix.
How it works
dbt project ──dbt build --empty + docs generate──▶ manifest.json + catalog.json + target/compiled/*.sql
│
catalog columns ──▶ sqlglot schema (every parse is schema-aware)
│
compiled SQL (one model) ──sqlglot lineage──▶ column edges + kind + file:lines
│
LineageGraph (NetworkX) ◀─┘──▶ target/dlens_graph.json (cache)
│
dlens trace · dlens impact · dlens report
Design notes for each part are in docs/explain/.
Roadmap
- v0.1 (this release): lineage CLI.
- v0.2: a Q&A agent that answers lineage questions by calling these graph tools; every claim carries a file/line citation checked by a validator.
- v0.3: hybrid retrieval (graph + RAG) and window/join-key (indirect) edges.
- v1.0: a published benchmark against vector-only RAG, with design-authored gold labels.
Limitations
- DuckDB only. Other warehouses may work through sqlglot dialects but are untested.
- Python 3.12 and 3.13 only; dbt-core is pinned to 1.12.x and dbt-duckdb to 1.11.x.
ingestruns dbt in the project directory (it writestarget/andlogs/).- Only value dependencies are edges. Join, filter, GROUP BY and window keys are not edges yet (window keys are recorded as deferred).
- Some constructs are partial or unsupported;
SELECT *over a join with duplicate column names is untested. See the support matrix. - Jinja is handled by dbt, not by DLens: DLens only parses the compiled SQL. If dbt cannot build
the project,
dlens ingestfails with dbt's error. - Diff (comparing the lineage of two columns, e.g. two revenue definitions) is planned for v1.x; comparing two versions of a project is out of scope.
- Pre-1.0: the CLI output and the Python API may change between minor versions.
Credits
sqlglot does the SQL parsing and scope analysis. dbt and dbt-duckdb produce the manifest, catalog and compiled SQL. jaffle_shop is vendored as an example corpus. dbt-colibri is prior art for dbt column lineage and the comparison point for the v1.0 benchmark.
Contributing
See CONTRIBUTING.md. Changes are listed in CHANGELOG.md.
Licence
Apache-2.0. See LICENSE. The vendored jaffle_shop keeps its own licence in
corpora/jaffle_shop/LICENSE.
Metadata
Release files for dlens-lineage 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dlens_lineage-0.1.0.tar.gz | 38.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dlens_lineage-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 73.7 kB
Release files / dlens_lineage-0.1.0.tar.gz
| Download URL | dlens_lineage-0.1.0.tar.gz |
|---|---|
| Size | 38.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b61be9915c4cfbfb452292b64f645e82be6a08801d9d2f828af0bf5232917c23
|
|
BLAKE2b-256 checksum How to use checksums |
3f8e71790e001e52a8f42071f2387eff49b46ab985d69d038632f0561ec80ec1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / dlens_lineage-0.1.0-py3-none-any.whl
| Download URL | dlens_lineage-0.1.0-py3-none-any.whl |
|---|---|
| Size | 34.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aaceafbfeaa15b7888878db6c499b99a183bc5e4af945229d2f5c410cae713bf
|
|
BLAKE2b-256 checksum How to use checksums |
1f94389584802a22d28d17051ba561231f02070032c855e430f95e04ae38fc8a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log