/zuckdb.zig

A DuckDB driver for Zig

Primary LanguageZigMIT LicenseMIT

Zig driver for DuckDB.

Quick Example

const db = switch (zuckdb.DB.init(allocator, "/tmp/db.duck", .{})) {
    .ok => |db| db,
    .err => |err| {
        std.debug.print("Failed to open DB: {s}\n", .{err.desc});
        err.deinit();
        return error.DBOpenFailure;
    }
}
defer db.deinit();

const conn = db.open();
defer conn.deinit();

{
    // exec takes a null terminated string and does not support parameters
    // it is micro-optimized for simple cases.
    try conn.exec("create table users(id int)");
}

{
    const rows = switch (conn.query("insert into users (id) values ($1)", .{1})) {
        ok => |rows| rows,
        err => |err| => {
            std.debug.print("failed to insert: {s}\n", .{err.desc});
            // Important to note that even the error needs to be deinit'd
            err.deinit();
            return error.Failure;
        },
    }
    defer rows.deinit();

    // Since this is an insert, only rows.changed() is useful:
    std.debug.print("inserted {d} row(s)\n", .{rows.rowsChanged()});
}


{
    const rows = switch (conn.query("select * from users", .{});) {
        ok => |rows| rows,
        err => |err| => {
            std.debug.print("failed to select: {s}\n", .{err.desc});
            err.deinit();
            return error.Failure;
        },
    }
    defer rows.deinit();
    while (try rows.next()) |row| {
        // get the 0th column of the current row
        const id = row.get(i32, 0);
        std.debug.print("The id is: {d}", .{id});
    }
}

When fetching data from a row, data is only valid until the next call to next or deinit:

// assume we have rows from a "select name from users";

defer rows.deinit();

// we'll collect the names in here
const names = std.ArrayList([]const u8).init(allocator);

while (rows.next()) |row| {
    // name is not null
    const name = row.get([]u8, 0).?; 

    // we need to dupe the string value to own it beyond this block
    try names.append(try allocator.dupe(u8, name));
}

Query and Exec

query and exec are the two primary functions used to run queries. The queryZ and execZ versions are optimized when the 1st parameter (the sql string) is known to be null-terminated (so you should favor using execZ or queryZ if you have a static SQL string). exec and execZ do not accept SQL parameters and only returns an !void (i.e. no detailed message, no rows), but will run slightly faster.

You'll notice that the library does not use idiomatic error sets, but rather Rust-like results. This was done so that the error message could be exposed. As a consequence, you'll also notice that deinit must be called on BOTH the ok and err values. There are two options to deal with this. In the above example, deinit is called in both cases:

const rows = switch(conn.query("...", .{})) {
    .ok => |rows| rows,
    .err => {
        std.debug.print("{s}\n", .{err.desc});
        // err result is freed here, _after_ err.desc is used
        err.deinit();
        return error.Fail;
    }
}
// ok result is freed here
defer rows.deinit()

Alternatively, deinit can be called on the result itself:

const result = conn.query("...", .{});
// CALLED HERE
defer result.deinit();

const rows = switch() {
    .ok => |rows| rows,
    .err => {
        std.debug.print("{s}\n", .{err.desc});
        return error.Fail;
    }
}

// should NOT call rows.deinit()

Rows and Row

The rows returned from the ok case of a query exposes the following methods:

  • count() - the number of rows in the result
  • changed() - the number of updated/deleted/inserted rows
  • columnName(i: usize) - the column name at position i in a result

The most important method on rows is next() which is used to iterate the results. next() is a typical Zig iterator and returns a ?Row which will be null when no more rows exist to be iterated.

Row exposes a get(T, index) ?T and list(T, index) ?[]T function.

The supported types for get, are:

  • []u8,
  • []const u8
  • i8
  • i16
  • i32
  • i64
  • i128
  • u8
  • u16
  • u32
  • u64
  • f32
  • f64
  • bool
  • zuckdb.Date
  • zuckdb.Time
  • zuckdb.Interval
  • zuckdb.UUID

There are a few important notes. First calling get with the wrong type will result in null being returned. Second, get(T, i) usually returns a ?T. The only exception to this is get([]u8, i) which returns a []const u8 - I just didn't want to type get([]const u8) all the time.

row.list(T, i) is used to fetch a list from duckdb. It returns a type that exposes a len and get(i) ?T function:

const tags = row.list([]u8, 3) orelse unreachable; // TODO handle null properly
for (0..tags.len) |i| {
    const tag = tags.get(i);
    // get returns a nullable, so above, tag could be null
}

Pool

The zuckdb.Pool is a thread-safe connection pool:

const db = DB.init(t.allocator, "/tmp/duckdb.zig.test", .{}).ok;
var pool = db.pool(.{
    .size = 2,
    .on_connection = &connInit,
    .on_first_connection = &poolInit,
}).ok
defer pool.deinit();

const conn = pool.acquire();
defer pool.release(conn);

The Pool takes ownership of the DB object, thus db.deinit does not need to be called. In the above code, the DB result and Pool result are dangerously unwrapped directly into ok - this will panic if either returns an .err.

The on_connection and on_first_connection are optional callbacks. They both have the same signature:

?*const fn(conn: Conn) anyerror!void

If both are specific, the first initialized connection will first be passed to on_first_connection and then to on_connection.

Query Optimizations

In very tight loops, performance might be improved by providing a stack-based state for the query logic to use. The query, queryZ, row, rowZ, queryCache and queryCacheZ functions all have a WithState alternative, e.g.: queryZWithState. These functions take 1 additional "query state" parameter:

var state = zuckdb.StaticState(2){};
var query_result = conn.queryCacheWithState(SQL, .{ARGS}, &state);
// use query_result normally

The value passed to zuckdb.StaticState is the number of columns returned by the query. The state must remain valid while the query is used.