Ubik
SQL

Continuous views

A live GROUP BY with no window is a materialized view, republishing the groups that changed as an upsert changelog keyed by the group tuple, refreshed on --emit-every.

A windowed query emits a group once, when the watermark passes the window's end. A query with no window emits differently: a live --follow run whose GROUP BY has no window function is a materialized view. It holds one running value per group and, at each refresh, republishes the groups that changed since the last one.

ubik --from kafka://broker/orders --follow --emit-every '2 seconds' -f json \
  "SELECT merchant, sum(amount) AS revenue, count(*) AS n
   FROM orders GROUP BY merchant"

Produce three orders to the topic:

{"merchant":"acme","amount":250}
{"merchant":"globex","amount":42}
{"merchant":"acme","amount":980}

At the next refresh the view emits one row per group:

{"merchant":"acme","revenue":1230,"n":2}
{"merchant":"globex","revenue":42,"n":1}

Now produce two more, one for the existing acme and one for a new initech:

{"merchant":"acme","amount":70}
{"merchant":"initech","amount":300}

The next refresh republishes only the groups that changed:

{"merchant":"acme","revenue":1300,"n":3}
{"merchant":"initech","revenue":300,"n":1}

acme is re-emitted with its new running total, initech appears, and globex is not re-emitted because nothing about it changed. The output is an upsert changelog keyed by the group tuple (here merchant), so a consumer reading latest-per-key always holds the true current aggregate. Point --to kafka://broker/topic at a sink to write that changelog to Kafka; it is the same keyed-upsert contract a windowed changelog uses, keyed by window and group. See Kafka source and sink.

Refresh cadence

--emit-every sets the refresh interval, default 2 seconds.

FlagTypeDefaultMeaning
--broker-timeoutstringfail a Kafka replay with BROKER_LOST after this much sustained MID-RUN broker unreachability (default '5 minutes'; '0 seconds' waits forever); a broker unreachable at startup always fails fast, ~5s, nothing is invested yet
--checkpointstringwrite exactly-once checkpoints to this path (a sibling <path>.lock file guards it against a second engine; it stays behind, leave it)
--checkpoint-everyint0checkpoint every N chunks (~2048 rows each) and keep running (requires --checkpoint)
--dimensionstringArray[]lookup-join build side, name=file://path.ndjson or name=kafka://broker/topic (repeatable)
--emit-everystringrefresh cadence of a continuous view. A live --follow run whose GROUP BY has no window is a materialized view: at each refresh it republishes the groups that changed, as an upsert changelog keyed by the group tuple, so a consumer's latest-per-key is always the true aggregate. Default '2 seconds'. A view that has caught up refreshes immediately whatever this says, so '0 seconds' leaves only the caught-up and pre-checkpoint refreshes, never no emission
--emit-modestringhow the Kafka sink emits each closed window: 'transactional' (default) wraps each window in a Kafka transaction so a read_committed consumer sees it atomically, or 'idempotent' drops the transaction for lower per-window latency, keeping exactly-once through the (window, group) upsert key and a durability barrier that flushes and verifies before advancing. Pick transactional when a downstream needs window atomicity, idempotent for a latest-per-key or changelog consumer that wants the latency (Kafka sink only)
--followboolfalsetail the topic live instead of replaying to its current head and stopping (Kafka only); Ctrl-C to stop. Window semantics differ: a replay closes every remaining window when the topic ends, follow closes windows only as the event-time watermark advances, so a partition with no fresh events holds every window open (list/logs show 'wm held by'). A one-shot validation can emit rows a follow never will
-f, --formatstringresult output format: table|json|csv. Default follows the destination: the aligned table at a terminal, raw JSON on a pipe. The flag forces one either way (csv to a file, json at a TTY, table down a pipe)
--fromstringsource, e.g. kafka://broker/topic
--idle-partition-timeoutstringstop counting a source partition in the coordinated watermark once the broker proves it is at the end of its log and it has stayed quiet that long (e.g. '60 seconds'), so one permanently idle partition stops holding every window open. A partition that still holds unread records is never excluded, whatever the engine is doing. Empty (the default) and '0 seconds' disable it; a run without it is byte-identical to one built before the feature existed (windowed queries, Kafka source)
--pipelinestringdurable auto-resume pipeline: checkpoint to this path and resume it automatically if it already exists, so ONE command serves first run and restart (for systemd/k8s: one ExecStart + Restart=on-failure gives kill-9 recovery). Needs --follow (live) or --checkpoint-every (periodic). The auto form of --checkpoint/--resume; don't combine them
--progress-intervalstringheartbeat a JSON progress line on stderr every interval: rows in/out and ev/s (per-run counters, reset on restart), watermark, checkpoint seq/age (null until one commits), late-dropped/spilled counters, Kafka lag (null until first measured), plus windows closed / rows folded on a windowed query (default '10 seconds', or 500ms when a terminal is watching; '0 seconds' disables). A run shorter than one interval (a quick one-shot) emits none; the final stats line carries the run's totals and is its summary
--resumestringrestore state and offsets from a checkpoint, then continue; re-supply --from and the original SQL (a mismatch is refused)
--schemastringlocal Avro schema (.avsc) for a raw-Avro topic with no registry
--schema-registrystringConfluent Schema Registry URL for an Avro topic, e.g. https://sr.example.com
--stop-afterint0checkpoint after N chunks (~2048 rows each) and stop (requires --checkpoint)
--tostringsink for the aggregation changelog, e.g. kafka://broker/topic (default: stdout)
-v, --versionboolfalseprint the ubik CLI and engine versions and exit
--viewstringrunning-view density at a terminal: 'simple' (one status line) or 'extended' (adds the stats box). Press v while a run is live to switch; that choice becomes your default, this flag forces one run without changing it. Piped output is unaffected, it stays raw JSON
--watermark-delaystringbounded out-of-orderness: close a window at end + interval (e.g. '5 seconds'), so a late row within the grace still counts (windowed queries)

A view that has caught up to the source refreshes immediately, whatever the interval says, so a burst is reflected without waiting out the clock. Setting --emit-every 0 seconds leaves only those caught-up refreshes and the one taken before each checkpoint, never no emission.

Exactly-once

A materialized view checkpoints like any other run: the running per-group state and the source offsets are in the checkpoint, so a kill -9 resumes the view with each group's aggregate intact and no double counting. Run it as a supervised pipeline for automatic restart, or with --pipeline for the one-command durable form. See exactly-once.

Windowed or continuous

Both run the same aggregates over the same stream; the difference is when a group is emitted.

  • A windowed query (GROUP BY ..., TUMBLE/HOP/SESSION(...)) emits each window once, when the watermark passes its end. The result is a stream of finished windows. See windows.
  • A continuous view (GROUP BY with no window) never finishes a group: it keeps one running value per key and republishes it as it changes, on the --emit-every cadence.

On this page