Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Applications of many models

A model reads another model with ref('<model>'), and Streamform plans the whole graph as one application. order-pipeline in the examples archive is the reference: three models in a chain, ending in a sink.

The example

-- models/clean_orders.sql
SELECT customer_id, amount
FROM source('orders')
WHERE customer_id IS NOT NULL
-- models/customer_metrics.sql
SELECT customer_id, SUM(amount) AS total_spend
FROM ref('clean_orders')
GROUP BY customer_id
-- models/big_customers.sql
SELECT customer_id, total_spend
FROM ref('customer_metrics')
WHERE total_spend > 100
# streamform.yml (models section)
models:
  customer_metrics:
    materialized: upsert
    key: [customer_id]
  big_customers:
    materialized: upsert
    key: [customer_id]
    sink:
      connector: kafka
      topic: big-customers
      format: json

streamform graph prints the application:

order-pipeline

orders                   source                kafka topic=orders
└─ clean_orders          append
   └─ customer_metrics   upsert(customer_id)
      └─ big_customers   upsert(customer_id)   → kafka topic=big-customers

--format dot emits Graphviz for dot -Tsvg.

What crosses a ref()

ref('m') binds to m’s Sink: its output columns and its changelog mode. That mode is what makes the last model interesting. customer_metrics emits updates, so big_customers is a filter over an updating input, and its changelog has inserts when a customer crosses 100, updates while they stay above it, and a delete when a refund takes them under. Its fixture asserts exactly that; see Testing with fixtures.

streamform explain big_customers shows the upstream model on the first operator:

big_customers

Filter
  from=customer_metrics
  predicate=(total_spend > 100)
  boundedness=Unbounded
  changelog=Upsert(customer_id)
  state=Stateless

explain with no model prints every model in dependency order.

What check catches

The dependency graph is validated before anything is planned, and each problem names the models involved:

  • a ref() to a model that does not exist (with a suggestion when a close name exists);
  • a model that references itself, or a cycle, listed in full;
  • a name used by both a source and a model, since the two share one namespace;
  • a plain table name in FROM, which would bypass the graph.

Fixtures across models

A fixture for big_customers gives events to orders, the source at the top of its chain, and asserts big_customers’ changelog after clean_orders and customer_metrics have run. You test the model you care about; Streamform runs its upstream closure.

One plan for the graph means one job for the graph. Each model is a CREATE TEMPORARY VIEW built on the view of the model it reads, so the chain runs inside a single Flink job with no topic between models; only a declared sink: writes out. A model with neither a sink nor a reader is planned but nothing on Flink runs it, and build warns.

Limits today

A model reads exactly one relation: no joins or unions yet. GROUP BY over an updating model is refused, because folding an update into an accumulator needs retractions the runtime does not produce yet. Both are on the roadmap.