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 ./example2 result rowsubik_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.
| Field | CLI equivalent |
|---|---|
from, sql | required: the source and the query |
to | --to: a Kafka sink instead of pulling via ubik_next |
checkpoint, resume, checkpoint_every | the exactly-once options |
follow | --follow: tail live instead of replaying to the head |
watermark_delay, idle_partition_timeout | the watermark bounds |
schema, schema_registry | the Avro format options |
workers, state_memory_mb, spill_dir | the resource knobs (UBIK_WORKERS, etc.) |
progress_interval | drives 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.
| Call | Returns |
|---|---|
ubik_last_error | the {code, message, hint} triple, identical to the CLI's |
ubik_stats | final late_dropped and spilled_blocks |
ubik_progress | the live snapshot (rows, watermark, checkpoint age, Kafka lag) |
ubik_license | the license state, so the host renders the notice in its own idiom |
ubik_checkpoint | take 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.