Handles

Three opaque handle types. Callers never see the Rust layout behind them; they move them only through the functions below.

EngineHandle
A connection. Created by engine_open or engine_branch; freed by engine_close.
EngineResult
A buffered query result (all rows materialised). Produced by engine_query; freed by engine_result_free.
EngineStmt
A prepared statement with a row cursor. Produced by engine_prepare; freed by engine_finalize.

Status codes

Every non-constructor function returns an EngineStatus. The seven codes are exhaustive.

CodeValueMeaning
ENGINE_OK0Success.
ENGINE_ERR_SQL1Parse / plan / type error.
ENGINE_ERR_CONSTRAINT2Unique / foreign-key / check violation.
ENGINE_ERR_CONFLICT3Serialization / write conflict — retryable.
ENGINE_ERR_STORAGE4Backend I/O, CAS rejected, or S3 fault.
ENGINE_ERR_TXN5Illegal state-machine transition (e.g. DDL inside a transaction).
ENGINE_ERR_MISUSE6Null handle, use-after-free, or bad argument.
ENGINE_ERR_INTERNAL7A bug — a caught panic. The engine stays defined; never UB.

Lifecycle

EngineHandle* engine_open(const char* url)
Open a connection. The NUL-terminated url selects the storage backend by scheme (file://./local.db, s3://bucket/mydb?region=…). Returns NULL on failure — the caller then has no handle to query.
void engine_close(EngineHandle* h)
Close and free a connection. Idempotent on NULL.

One-shot execution

EngineStatus engine_exec(EngineHandle* h, const char* sql)
Run DDL/DML with no result set. The affected row count is read afterward with engine_changes.
EngineStatus engine_query(EngineHandle* h, const char* sql, EngineResult** out)
Run a query. On ENGINE_OK, *out receives an owned EngineResult; otherwise *out is NULL. Values are NUL-terminated text; a SQL NULL is reported as a NULL pointer. Free the result with engine_result_free.

Prepared statements

Prepare once, bind typed parameters, and step the cursor row by row. Reuse with engine_reset (bindings are kept).

EngineStatus engine_prepare(EngineHandle* h, const char* sql, EngineStmt** out)
Compile sql into a statement. On ENGINE_OK, *out receives the owned EngineStmt.
EngineStatus engine_bind(EngineStmt* s, int idx, const char* value)
Bind a parameter by 1-based positional idx. value is a NUL-terminated typed literal (see Parameter encoding).
EngineStatus engine_step(EngineStmt* s, int* done)
Advance the cursor. On ENGINE_OK, *done == 0 means a row is current (read its columns with engine_column_value); *done == 1 means there are no more rows.
EngineStatus engine_finalize(EngineStmt* s)
Free the statement.
EngineStatus engine_reset(EngineStmt* s)
Re-execute from the start, keeping the current bindings.

Transactions

EngineStatus engine_begin(EngineHandle* h)
Start an explicit transaction.
EngineStatus engine_commit(EngineHandle* h)
Commit. Blocks until the WAL is durable before returning.
EngineStatus engine_rollback(EngineHandle* h)
Discard the transaction's pending changes.

Branching

EngineHandle* engine_branch(EngineHandle* h, const char* name)
Create a copy-on-write branch off the database h is connected to, at its current committed LSN, and return a new connection handle bound to that branch. The branch shares the base's immutable history but writes in isolation — neither the base nor any sibling sees a branch's writes. The returned handle is owned by the caller and freed with engine_close. Returns NULL on failure (e.g. inside an active transaction, or branch-of-branch); the reason is available via engine_last_error(h).

Result and row access

Read a buffered EngineResult by row and column. Returned const char* pointers are borrowed and valid until engine_result_free(r).

int engine_result_rows(const EngineResult* r)
Number of rows in the result.
int engine_result_cols(const EngineResult* r)
Number of columns.
const char* engine_result_colname(const EngineResult* r, int col)
Borrowed column name for col.
const char* engine_result_value(const EngineResult* r, int row, int col)
Borrowed cell value at (row, col). NULL for a SQL NULL cell. Valid until engine_result_free(r).

Statement cursor access

Read the column metadata and the current row of a stepping EngineStmt.

int engine_column_count(const EngineStmt* s)
Number of columns the statement produces.
const char* engine_column_name(const EngineStmt* s, int col)
Borrowed name of column col.
const char* engine_column_value(const EngineStmt* s, int col)
Borrowed value of column col for the current row. NULL for a SQL NULL cell. Valid only until the next engine_step / engine_reset / engine_finalize on s.

Copy borrowed pointers before they expire

A cursor value pointer is invalidated by the next step/reset/finalize; a result value pointer is invalidated by engine_result_free. Copy any value you need to keep out before the owning object advances or is freed.

Errors and metadata

const char* engine_last_error(EngineHandle* h)
Borrowed, per-handle C string describing the last error on h. Valid until the next call on h. Empty string if there is none.
long long engine_changes(EngineHandle* h)
Rows affected by the last statement.
long long engine_last_lsn(EngineHandle* h)
Commit LSN of the last commit.
int engine_abi_version(void)
The ABI version this library exports — currently 3. Check it against ENGINE_ABI_VERSION at load time.

Freeing

void engine_result_free(EngineResult* r)
Free a query result. Idempotent on NULL.

Statements are freed by engine_finalize; handles by engine_close.

Parameter encoding

engine_bind takes each parameter as a NUL-terminated typed literal: a one-character tag followed by the encoded value. This is how the string-only ABI carries a typed argument.

TagTypeExample
iIntegeri42
fFloatf3.5
sTextshello
bBytes (base64)b<base64>
nNULLn
vVectorv[1,2,3] or v1,2,3

Safety contract

  • MUST assume no Rust panic crosses the boundary — a caught panic becomes ENGINE_ERR_INTERNAL and the handle stays defined and queryable.
  • MUST treat a null or invalid handle as ENGINE_ERR_MISUSE — null handles, use-after-free, and bad arguments all map to this code rather than undefined behaviour.
  • MUST read every value as NUL-terminated text; a SQL NULL is a NULL pointer, distinct from the empty string.
  • MUST copy a borrowed const char* out before the owning object advances or is freed — cursor values expire on the next step/reset/finalize; result values on engine_result_free; error strings on the next call to the handle.
  • MUST NOT use a single handle concurrently from multiple threads. Distinct handles are thread-safe with respect to one another; one handle is not.

Minimal C example

Open, run a one-shot statement, query, then free and close. All < / > below are literal C source.

#include <stdio.h>
#include "engine.h"

int main(void) {
    EngineHandle* h = engine_open("file://./demo.db");
    if (!h) return 1;

    engine_exec(h, "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT)");
    engine_exec(h, "INSERT INTO notes (id, title) VALUES (1, 'hello')");

    EngineResult* r = NULL;
    if (engine_query(h, "SELECT id, title FROM notes ORDER BY id", &r) == ENGINE_OK) {
        int rows = engine_result_rows(r);
        int cols = engine_result_cols(r);
        for (int i = 0; i < rows; i++) {
            for (int c = 0; c < cols; c++) {
                const char* v = engine_result_value(r, i, c);
                printf("%s%s", v ? v : "NULL", c + 1 < cols ? "\t" : "\n");
            }
        }
        engine_result_free(r);
    } else {
        printf("query failed: %s\n", engine_last_error(h));
    }

    engine_close(h);
    return 0;
}

From Bun you rarely touch this directly

The @twilldb/bun client wraps every function here over bun:ffi, encodes the typed-literal parameters, and pins EXPECTED_ABI_VERSION to catch a stale binary. See Connect from Bun for the ergonomic typed API.

Related

Twill DB documentation · Licensed under AGPL-3.0. · Author