Skip to content

SQL Lineage Parser

Tollkeeper's lineage parser extracts source and sink tables from SQL statements using sqlglot. This powers automatic dependency detection in the Airflow integration, but you can use it standalone.

Setup

Install the sqlglot extra:

pip install tollkeeper[sqlglot]

Basic usage

from tollkeeper import extract_lineage

result = extract_lineage("""
    INSERT INTO analytics.fact_orders
    SELECT o.*, c.name
    FROM raw.orders o
    JOIN raw.customers c ON o.customer_id = c.id
""")

result.sources  # frozenset({'raw.orders', 'raw.customers'})
result.sinks    # frozenset({'analytics.fact_orders'})

Supported statements

Statement Sources Sinks
SELECT ... FROM a JOIN b {a, b} {}
INSERT INTO t SELECT ... FROM a {a} {t}
CREATE TABLE t AS SELECT ... FROM a {a} {t}
MERGE INTO t USING s ON ... {s} {t}

Multi-statement SQL is supported. Sources and sinks are accumulated across all statements.

CTE handling

Common Table Expressions are excluded from sources. Only the real tables they reference are reported:

result = extract_lineage("""
    WITH enriched AS (
        SELECT * FROM raw.events
    )
    INSERT INTO analytics.sessions
    SELECT * FROM enriched
""")

result.sources  # frozenset({'raw.events'}), not {'enriched'}
result.sinks    # frozenset({'analytics.sessions'})

Fully qualified names

Catalog, schema, and table names are preserved:

result = extract_lineage("SELECT * FROM my_catalog.my_schema.my_table")
result.sources  # frozenset({'my_catalog.my_schema.my_table'})

Dialect support

Pass a sqlglot dialect for engine-specific SQL syntax:

result = extract_lineage(
    "SELECT * FROM `project.dataset.table`",
    dialect="bigquery",
)

See sqlglot dialects for the full list.

Error handling

# Empty SQL
extract_lineage("")        # raises ValueError("SQL string is empty")

# Unparseable SQL
extract_lineage("NOT SQL") # raises ValueError("Failed to parse SQL: ...")

# Jinja templates (common in Airflow)
extract_lineage("SELECT * FROM {{ params.table }}")
# raises ValueError("SQL contains Jinja template expressions; provide explicit sources/sinks")

Jinja-templated SQL cannot be statically parsed. In the Airflow integration, provide sources and sinks explicitly when your SQL uses templates.

Use with Airflow task groups

The parser is used automatically by TollkeeperTaskGroup to wire up sensor dependencies:

from airflow_tollkeeper import TollkeeperTaskGroup

group = TollkeeperTaskGroup(
    table="fact_orders",
    sql="INSERT INTO fact_orders SELECT * FROM dim_customers JOIN raw_events ...",
    backend=backend,
    signal_store=store,
)
# Sensors are created for dim_customers and raw_events automatically

If the SQL contains Jinja or the parser can't extract what you need, pass sources explicitly:

group = TollkeeperTaskGroup(
    table="fact_orders",
    sql="{{ params.insert_sql }}",
    sources=["dim_customers", "raw_events"],
    backend=backend,
    signal_store=store,
)