Ubik
C

Embed in C

Link libubik, run streaming SQL in your own process, and pull results as Arrow through the stable C ABI in include/ubik.h.

The engine is a library. include/ubik.h is its C ABI: no C++ type and no exception crosses it, and every binding, the Python module included, links through it. The release tarball ships include/ubik.h and lib/libubik alongside the ubik binary.

Results cross as Arrow. Input (a host table for a join) and output (the result batches) are the Arrow C Data Interface, so they map zero-copy into pyarrow, polars, DuckDB or anything else that speaks Arrow.

The lifecycle

One run is ubik_open, ubik_start, a pull loop on ubik_next, then ubik_close.

#include <stdio.h>
#include <stdint.h>
#include "ubik.h"

/* The Arrow C Data Interface, the stable struct results arrive in. Reproduced
   here (public domain, from the Arrow spec) so the example needs no Arrow
   headers. */
struct ArrowArray {
	int64_t length, null_count, offset, n_buffers, n_children;
	const void **buffers;
	struct ArrowArray **children;
	struct ArrowArray *dictionary;
	void (*release)(struct ArrowArray *);
	void *private_data;
};

int main(void) {
	ubik_options opts = {0};
	opts.from = "file://sales.ndjson";
	opts.sql = "SELECT region, count(*) AS n, sum(amount) AS total "
	           "FROM sales GROUP BY region";

	ubik_engine *e = ubik_open(&opts);
	if (ubik_start(e) == UBIK_ERROR) {
		const ubik_error *err = ubik_last_error(e);
		fprintf(stderr, "%s: %s\n", err->code, err->message);
		ubik_close(e);
		return 1;
	}

	uint64_t rows = 0;
	struct ArrowArray out;
	ubik_status s;
	while ((s = ubik_next(e, &out)) == UBIK_OK) {
		rows += (uint64_t)out.length;
		out.release(&out);
	}

	printf("%llu result rows\n", (unsigned long long)rows);
	ubik_close(e);
	return s == UBIK_EOF ? 0 : 1;
}

Compile against the shipped library and run it over the aggregates fixture.

cc -Iinclude example.c -Llib -lubik -o example
LD_LIBRARY_PATH=lib ./example
2 result rows

ubik_next returns UBIK_OK with a batch, UBIK_EOF when a bounded replay is done, UBIK_IDLE when a follow run is caught up but still live, and UBIK_ERROR on failure. The embedder owns the loop.

Options

ubik_options is zero-initialised; a zero or NULL field takes the same default the CLI resolves. The fields mirror the CLI flags.

FieldCLI equivalent
from, sqlrequired: the source and the query
to--to: a Kafka sink instead of pulling via ubik_next
checkpoint, resume, checkpoint_everythe exactly-once options
follow--follow: tail live instead of replaying to the head
watermark_delay, idle_partition_timeoutthe watermark bounds
schema, schema_registrythe Avro format options
workers, state_memory_mb, spill_dirthe resource knobs (UBIK_WORKERS, etc.)
progress_intervaldrives ubik_progress

A host DataFrame becomes a join dimension with ubik_register_table(engine, name, stream) before ubik_start, taking an ArrowArrayStream.

Errors, stats and progress, typed

An in-process host reads state through typed calls, never by scraping a stderr line that a Jupyter kernel would swallow anyway.

CallReturns
ubik_last_errorthe {code, message, hint} triple, identical to the CLI's
ubik_statsfinal late_dropped and spilled_blocks
ubik_progressthe live snapshot (rows, watermark, checkpoint age, Kafka lag)
ubik_licensethe license state, so the host renders the notice in its own idiom
ubik_checkpointtake a durable checkpoint now, between pulls

Python

pip install ubik-sql embeds this same ABI, so import ubik gives the whole engine in-process with no C to write; see the Python quickstart. Reach for the C ABI when the host is not Python.

On this page