Interlace 2.0 — out now

SQL and Python are the same kind of node

Not a Python escape hatch bolted onto a SQL tool. A .py model and a .sql model are interchangeable in one DAG — either can depend on the other, and the planner does not care which you wrote.

$ pip install interlaced

published as interlaced; the import name and CLI are interlace

A Python model in the hot path

Ten models over 25 million rows, from the benchmark project. Click any node to see its source — user_ltv.py sits between two SQL models, and nothing around it acknowledges the difference.

examples/benchmark · 25M rows 13 nodes · 12 edges

elsewhere in the same graph

Python models/user_ltv.py
from interlace import model

@model(depends_on=["by_user"], strategy="merge", key=["user_id"])
def user_ltv(by_user):
    for batch in by_user.reader():
        score = pc.add(pc.multiply(batch.column("spend"), 0.1), batch.column("events"))
        yield pa.RecordBatch.from_arrays(
            [batch.column("user_id"), batch.column("spend"), pc.round(score, 2)],
            names=["user_id", "spend", "ltv"],
        )

Python, in the hot path. Its upstream is SQL. It is a generator, so 100k rows stream through Arrow with bounded memory — and merge is the same keyed upsert a SQL model gets.

Two axes, not one

materialise decides where a result lands and who owns it. strategy decides how it is written. They compose — and every combination above comes from one real project.

owned

virtual · view · ephemeral

Interlace builds an immutable fingerprinted snapshot and serves it through an environment view. That ownership is what makes rebuild-skip, sandboxes, atomic promotion, rollback and gc possible.

terminal

table · file

A destination Interlace does not own. It delivers into an external table or overwrites a file, evolves the target additively, and never drops it — so grants, indexes and RLS survive. Environment-gated to production by default.

A breaking change cannot apply to a terminal target: there is no old version to serve during the build and no atomic cutover. That constraint is why the two planes exist.

One run, every plane

A single command builds owned snapshots, delivers into external tables and writes files — each with its own strategy, in dependency order. This is the benchmark: 25M synthetic events fanned out through every strategy, start to finish in seconds.

Terminal examples/benchmark
$ interlace run

Build results

 Model             Output    Strategy              Engine    Depends on             Rows    Time
 ───────────────────────────────────────────────────────────────────────────────────────────────
 events            virtual   replace               default      +25,000,000   3.85s
 daily_revenue     virtual   incremental_by_time   default   events                  +29   0.28s
 daily_feed        table     append                default   daily_revenue           +29   0.30s
 revenue_report    file      replace               default   daily_revenue           +29   0.07s
 by_day            virtual   replace               default   enriched                +30   0.08s
 by_device         virtual   replace               default   enriched                 +4   0.11s
 by_product        virtual   replace               default   enriched            +15,000   0.21s
 by_user           virtual   replace               default   enriched           +100,000   0.45s
 product_catalog   virtual   full_merge            default   by_product          +15,000   0.34s
 top_products      view      replace               default   by_product                   0.36s
 user_history      virtual   scd                   default   by_user            +100,000   0.09s
 user_ltv          virtual   merge                 default   by_user            +100,000   0.15s

Checks: 2/2 passed
Ran 12 model(s) (12 task(s)); promoted 13 to 'prod'.

Preview every change

interlace plan classifies each model as breaking, non-breaking or forward-only before anything is built.

Reuse instead of rebuild

Column-level impact analysis proves when a downstream output is unchanged, so its existing table is reused.

Checks gate promotion

An error-severity check failure blocks the apply before the environment view ever moves.

One process, not a stack

interlace serve runs the web UI, HTTP API, scheduler and stream ingestion together. There is no separate orchestrator to deploy and no broker to operate.

Durable streams

POST an event and it is fsynced before the 200, deduplicated by idempotency key, and materialised exactly once — the watermark commits in the same warehouse transaction as the data.

Free sandboxes

An environment is a set of views over fingerprinted tables, so a dev environment reuses production’s for free. Promotion is an atomic view swap; rollback is the same move backwards.

Built-in scheduling

Cron and interval triggers over a durable run queue with leases, retries and cooperative cancellation. No Airflow, no broker, no second deployment.

Pin models to engines

DuckDB/DuckLake by default, with Postgres, Snowflake, BigQuery, Redshift, MotherDuck and Spark behind an engine: pin. Cross-engine dependencies move as Arrow, or over a federated ATTACH.

Ready to simplify your data pipelines?

Get started with Interlace in minutes. Install, define your first model, and run.