Skip to main content

sqlleaf

Trace your data

sqlleaf is a tool to help you to understand how your data flows across your data systems by producing detailed, column-level lineage across any SQL query and dialect.

It extends beyond traditional lineage tools by mapping relationships across all kinds of expressions, such as columns, functions, literals and variables.

Example

Add some SQL queries to a file:

-- File: example.sql
CREATE TABLE users (name VARCHAR(50), birthday DATE);
CREATE TABLE logins (name VARCHAR(50));

INSERT INTO users (name, birthday)
SELECT LOWER(name) AS name, CURRENT_TIMESTAMP AS birthday
FROM logins;

Add some Python code:

sql = open("example.sql").read()

import sqlleaf
lineage = sqlleaf.Lineage()
lineage.generate(sql=sql, dialect="postgres")
lineage.print_paths()

View the lineage printed to the console:

column[logins.name] -> function[LOWER] -> column[users.name]
function[CURRENT_TIMESTAMP] -> column[users.birthday]

Installation

Available on PyPI:

pip3 install sqlleaf

Applications

sqlleaf allows you to build a wide range of new applications on top of SQL databases, such as:

  • data tracing and tagging systems, such as personally identifiable information tracking
  • lineage-based SQL validators
  • auditing systems and historical analysis
  • identification of unused columns and tables
  • support for database migration correctness
  • impact analysis of changes to queries

Features

Types of queries

sqlleaf traces lineage across a wide range of DML queries, such as INSERT, UPDATE, MERGE, CREATE TABLE AS, COPY, UNLOAD, CTEs, and many more.

It parses over 30 dialects, extending functionality from the underlying SQL transpiler sqlglot.

Here are some basic queries as examples:

-- Basic CTAS
CREATE TABLE dst AS SELECT * FROM src;

-- Basic UPDATE
UPDATE dst t SET first_name = 'john', age = s.age FROM src s;

-- An INSERT with a CTE (Common Table Expression)
WITH my_cte AS (SELECT 1 AS num) INSERT INTO dst (age) SELECT c.num FROM my_cte c;

Support for dynamic SQL inside PL/pgSQL

You can trace lineage across variables and complex, dynamically constructed statements. This works by analyzing the procedure's inner statements, substituting any arguments and variables, and transforming the statements into the desired queries.

For example, given a query to calculate the row count of an arbitrary table:

-- Define a PL/pgSQL procedure in Postgres
CREATE PROCEDURE get_maximum_id(schema_name TEXT, table_name TEXT)
AS $$
BEGIN
    EXECUTE format('INSERT INTO target (my_value) SELECT MAX(id) FROM %I.%I', schema_name, table_name);
END;
$$ LANGUAGE plpgsql;

-- Call it
CALL get_maximum_id('public', 'users');

sqlleaf extracts and transforms this into the statement:

INSERT INTO target (my_value) SELECT MAX(id) FROM public.users;

and then generates its lineage:

column[public.users.id] -> function[MAX] -> column[target.my_value]

User-defined functions, procedures and types

Given a user-defined function or a procedure, sqlleaf will extract the statement's inner queries so that they can be referenced by the calling query:

-- Create the function
CREATE FUNCTION hello() RETURNS TEXT AS $$
    SELECT 'Hello';
$$ LANGUAGE SQL;

-- Insert its value into a new table
CREATE TABLE dst (my_name VARCHAR);
INSERT INTO dst (my_name) SELECT hello();

This will perform inline transformations to produce a new query:

INSERT INTO dst (my_name) SELECT (SELECT 'hello') AS name;

with lineage: literal['Hello'] -> column[dst.name]

Pivot, Unpivot

Complex transformations such as pivots, unpivots, laterals and table functions are supported.

-- UNPIVOT example
INSERT INTO dst
SELECT name, amount
FROM src
UNPIVOT (
  amount FOR name IN (john_total AS 'john')
);

produces lineage:
literal["john"] -> unpivot[] -> column[target.name]
column[source.john_total] -> unpivot[] -> column[target.amount]

Customization

You can bring your own logic for processing SQL expressions and their nodes.

For example, let's say you only want to include certain functions inside the lineage graph, such as SUM(). You would do this by defining a user-defined hook and that excludes everything except SUM():

import sqlleaf
lineage = sqlleaf.Lineage()

from sqlleaf.lineage import Hook
from sqlleaf.models.node import FunctionNode

# Register a hook that only collects certain functions
hook = Hook(name="only_sum", kind=FunctionNode, func=lambda n: n.name == "SUM")
lineage.register_hooks(hooks=[hook])

When you run lineage.generate(), the only functions that are included as nodes are those named SUM.

Searching the lineage

To map the lineage into a graph, sqlleaf is built on top of the popular library networkx. If you need to search the graph, you can use any of network'x graph traversal functions. There are currently no dedicated search APIs for sqlleaf. These will be developed in an upcoming major release.

There are various functions for printing the lineage: print_paths(), print_tree(), and print_json().

Examples

Below are examples of SQL queries and their corresponding lineages.

1. Simple INSERT

INSERT INTO dst (name) VALUES (3);

literal[3] -> column[dst.name]

2. JSONPath selectors

INSERT INTO dst SELECT jsonblob ->> 'fruits' -> 'apple' AS name FROM src;

column[src.name] → jsonpath[.fruits.apple] → column[dst.name]

3. Using variables via SET

SET b = 'src';
CREATE TABLE dst AS SELECT col1 FROM IDENTIFIER($b);

column[SRC.COL1] → column[DST.COL1]

4. COPY queries

COPY dst TO STDOUT;

column[dst.col1] → stream[stdout]
column[dst.col2] → stream[stdout]

COPY dst TO PROGRAM 'gzip > /tmp/data.csv.gz';

column[dst.col1] → program[gzip]
column[dst.col2] → program[gzip]

5. UNLOAD query

UNLOAD ('SELECT name FROM src') TO 's3://dst-bucket/name-prefix' IAM_ROLE '...';

column[src.name] → column[name path=s3://dst-bucket/name-prefix]

6. Sequences

CREATE SEQUENCE my_serial START 101;
INSERT INTO dst (age) SELECT nextval('my_serial') as age;

sequence[my_serial] → column[dst.age]

7. An UPDATE with inner CTEs containing UPDATEs that return their old and new columns

WITH first_cte AS (
    UPDATE fruit.raw
    SET name = 'pear'
    RETURNING age, old.age as old_age, new.age as new_age
),
second_cte AS (
    UPDATE fruit.raw AS r
    SET name = 'tomato'
    RETURNING *, OLD.*, NEW.*
)
UPDATE fruit.processed
SET age = first_cte.age
FROM first_cte;

column[fruit.raw.age] -> column[first_cte.age] -> column[fruit.processed.age]
literal["pear"] -> column[fruit.raw.name]
literal["tomato"] -> column[fruit.raw.name

Limitations

  • Many of the dialects are incomplete. Postgres and PL/pgSQL have been mapped out extensively, but most of the dialects do not have their own specific features parsed into graphs yet. Dialects that do have features are Athena, Redshift, MySQL and Snowflake; the others have not been implemented at all yet.

  • sqlleaf does not support data deletion. Statements like DELETE, TRUNCATE or DROP TABLE currently have no effect. They will in the future.

Metadata

Release files for sqlleaf 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sqlleaf 0.0.3
File Size Uploaded Provenance
sqlleaf-0.0.3.tar.gz 128.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sqlleaf 0.0.3
File Interpreter ABI Platform
sqlleaf-0.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 303.6 kB

Release files / sqlleaf-0.0.3.tar.gz

Download URL sqlleaf-0.0.3.tar.gz
Size 128.9 kB
Tags Source
SHA-256 checksum
How to use checksums
afa9f7a820d84b2969175d51136e9aac71214fecb3575ca0092be2c5f55e7352
BLAKE2b-256 checksum
How to use checksums
6de8bdb9a531c8c6fbbd793c88f59423c0cbc6c58e34816ce5164b01cdf4e123
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / sqlleaf-0.0.3-py3-none-any.whl

Download URL sqlleaf-0.0.3-py3-none-any.whl
Size 174.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3909a0bc48cc40b285c1574a14eb188c71d4b3efb8bd4ac45c1b50d0e3c55884
BLAKE2b-256 checksum
How to use checksums
da27e59253603aa7614d3588dc402a9af5a13ca8918e9f6f60a639a9a2ca3ff4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for sqlleaf 0.0.3

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page