Skip to content

Built-in connectors (blackhole, changelog, print, collect, queryable_state)

Row-channel sinks compiled into the engine itself - no client library, no CLINK_WITH_* knob, always available when CLINK_BUILD_SQL=ON. They are selected by connector name in CREATE TABLE ... WITH (connector='...') and take no further option. blackhole, changelog and print are registered by clink::sql::install() (src/sql/install.cpp); collect is registered by the embedded engine and only works there.

blackhole, changelog and print natively accept changelog streams (retracting GROUP BY, Top-N, outer joins), which append-only sinks reject; collect is append-only in v1.

blackhole

Counts then drops every row (the runner's records_in metric still tallies them). For benchmarks where sink I/O must not distort throughput, and for driving a job whose output is irrelevant.

CREATE TABLE sink (bidder BIGINT, bid_count BIGINT) WITH (connector='blackhole');

changelog

Nets a changelog stream (insert / delete / update_before / update_after) into its final relation by full-row multiplicity - no primary key needed - and writes the survivors on flush. For keyless changelog outputs, e.g. a retracting aggregate joined to another stream.

print

One JSON line per record to stdout, flushed per record. The demo and debugging sink, and what a bare SELECT compiles onto under clink run (see embedded execution). Append rows print as plain JSON objects; changelog rows keep their meaning: a non-insert kind prefixes the line and the __row_kind marker itself is stripped from the printed object.

{"usr":"ada","total":10}
-U {"usr":"ada","total":10}
+U {"usr":"ada","total":42}

Lines are written with a single fwrite each, so concurrent subtasks at parallelism above 1 interleave whole lines, never fragments. Row order across subtasks is not defined, same as any parallel sink.

queryable_state (source)

State-as-table: a bounded source that snapshots another job's LIVE queryable state through the Coordinator's JSON scan route and emits one row per key - the served value document's fields become the row's columns. One job's running aggregates are another job's table, with no sink round-trip. SQL aggregate_row (unbounded GROUP BY, in-memory path) exposes its state automatically, so any GROUP BY job is readable this way while it runs.

CREATE TABLE live_totals (usr TEXT, total BIGINT)
WITH (connector='queryable_state', format='json',
      coordinator_port='8081', job_id='1');

SELECT usr, total FROM live_totals;   -- the CURRENT aggregate values

Options: coordinator_port (required - the Coordinator's HTTP port), job_id (required), coordinator_host (default 127.0.0.1), role (default the generic subtask role), slot (default agg), limit (default 100000, the route's cap), batch_size (default 256). The snapshot is taken once, at the first produce; truncated results are cut at limit. Delivery is a bounded read of a moving target - consistent per subtask scan, not a global point-in-time snapshot.

collect (embedded only)

Delivers the table's rows to the HOST PROCESS as typed Arrow record batches - the results surface of the embedded engine and libclink. Read it in C++ via EmbeddedEngine::collect_reader(table) or from any language through clink_collect_stream() (the Arrow C stream interface; zero-copy into pyarrow, DuckDB, polars). One consumer per table; reads block until data, end after the producing job finishes, and cancel when the engine closes. Append-only in v1: a retracting (changelog) SELECT is rejected at bind. Embedded-only by design - on a cluster there is no host process to deliver to, and the submit fails loudly. See embedded execution.

CREATE TABLE results (user_id BIGINT, total BIGINT) WITH (connector='collect');
INSERT INTO results SELECT user_id, SUM(amount) FROM orders GROUP BY user_id;

(An unbounded GROUP BY emits the running total per input row - the last batch row per key is the final answer, the same convention as the file sink.)