Zuckdb.zig
A DuckDB driver for Zig
Install / Use
/learn @karlseguin/Zuckdb.zigREADME
Zig driver for DuckDB.
Quick Example
const db = try zuckdb.DB.init(allocator, "/tmp/db.duck", .{});
defer db.deinit();
var conn = try db.conn();
defer conn.deinit();
// for insert/update/delete returns the # changed rows
// returns 0 for other statements
_ = try conn.exec("create table users(id int)", .{});
var rows = try conn.query("select * from users", .{});
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});
}
Any non-primitive value that you get from the row are valid only until the next call to next or deinit.
Install
This library is tested with DuckDB 1.4.3. You can either link to an existing libduckdb on your system, or have zuckdb download and build DuckDB for you (this will take time.)
- Add zuckdb as a dependency in your
build.zig.zon:
zig fetch --save git+https://github.com/karlseguin/zuckdb.zig#master
Link to libduckdb
-
Download the libduckdb from the <a href="https://duckdb.org/docs/installation/index.html?version=latest&environment=cplusplus&installer=binary">DuckDB download page</a>.
-
Place the
duckdb.hfile and thelibduckdb.so(linux) orlibduckdb.dylib(mac) in your project'slibfolder. -
Add this in
build.zig:
const zuckdb = b.dependency("zuckdb", .{
.target = target,
.optimize = optimize,
}).module("zuckdb");
// Your app's program
const exe = b.addExecutable(.{
.name = "run",
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/main.zig"),
});
// include the zuckdb module
exe.root_module.addImport("zuckdb", zuckdb);
// link to libduckdb
exe.linkSystemLibrary("duckdb");
// tell the linker where to find libduckdb.so (linux) or libduckdb.dylib (macos)
exe.addLibraryPath(b.path("lib/"));
Automatically fetch and buikd DuckDB
- Add this in
build.zig:
const zuckdb = b.dependency("zuckdb", .{
.target = target,
.optimize = optimize,
.system_libduckdb = false,
.debug_duckdb = false, // optional, compile DuckDB with DUCKDB_DEBUG_STACKTRACE or not
}).module("zuckdb");
// Your app's program
const exe = b.addExecutable(.{
.name = "run",
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/main.zig"),
});
// include the zuckdb module
exe.root_module.addImport("zuckdb", zuckdb);
Static Linking
It's also possible to statically link DuckDB. In order to do this, you must build DuckDB yourself, in order to compile it using Zig C++ and using the bundle-library target
git clone -b 1.4.3 --single-branch https://github.com/duckdb/duckdb.git
cd duckdb
export CXX="zig c++"
DUCKDB_EXTENSIONS='json' make bundle-library
When this finished (it will take several minutes), you can copy build/release/libduckdb_bundle.a and src/include/duckdb.h to your project's lib folder. Rename libduckdb_bundle.a to libduckdb.a.
Finally, Add the following to your build.zig:
exe.linkSystemLibrary("duckdb");
exe.linkSystemLibrary("stdc++");
exe.addLibraryPath(b.path("lib/"));
DB
The DB is used to initialize the database, open connections and, optionally, create a connection pool.
init
Creates or opens the database.
// can use the special path ":memory:" for an in-memory database
const db = try DB.init(allocator, "/tmp/db.duckdb", .{});
defer db.deinit();
The 3rd parameter is for options. The available options, with their default, are:
access_mode- Sets theaccess_modeDuckDB configuration. Defaults to.automatic. Valid options are:.automatic,.read_onlyor.read_write.enable_external_access- Sets theenable_external_accessDuckDB configuration. Defaults totrue.
initWithErr
Same as init, but takes a 4th output parameter. On open failure, the output parameter will be set to the error message. This parameter must be freed if set.
var open_err: ?[]u8 = null;
const db = DB.initWithErr(allocator, "/does/not/exist", .{}, &open_err) catch |err| {
if (err == error.OpenDB) {
defer allocator.free(open_err.?);
std.debug.print("DB open: {}", .{open_err.?});
}
return err;
};
deinit
Closes the database.
conn
Returns a new connection object.
var conn = try db.conn();
defer conn.deinit();
...
pool
Initializes a pool of connections to the DB.
var pool = try db.pool(.{.size = 2});
// the pool owns the `db`, so pool.deinit will call `db.deinit`.
defer pool.deinit();
var conn = try pool.acquire();
defer pool.release(conn);
The pool method takes an options parameter:
size: usize- The number of connections to keep in the pool. Defaults to5timeout: u64- The time, in milliseconds, to wait for a connetion to be available when callingpool.acquire(). Defaults to10_000.on_connection: ?*const fn(conn: *Conn) anyerror!void- The function to call when the pool first establishes the connection. Defaults tonull.on_first_connection: ?*const fn(conn: *Conn) anyerror!void- The function to call on the first connection opened by the pool. Defaults tonull.
Conn
query
Use conn.query(sql, args) !Rows to query the database and return a zuckdb.Rows which can be iterated. You must call deinit on the returned rows.
var rows = try conn.query("select * from users where power > $1", .{9000});
defer rows.deinit();
while (try rows.next()) |row| {
// ...
}
exec
conn.exec(sql, args) !usize is a wrapper around query which returns the number of affected rows for insert, updates or deletes.
row
conn.row(sql, args) !?OwningRow is a wrapper around query which returns a single optional row. You must call deinit on the returned row:
var row = (try conn.query("select * from users where id = $1", .{22})) orelse return null;;
defer row.deinit();
// ...
begin/commit/rollback
The conn.begin(), conn.commit() and conn.rollback() calls are wrappers around exec, e.g.: conn.exec("begin", .{}).
prepare
conn.prepare(sql, opts) !Stmt prepares the given SQL and returns a zuckdb.Stmt. For one-off queries, you should prefer using query, exec or row which wrap prepare and then call stmt.bind(values) and finally stmt.execute(). Getting an explicit Stmt is useful when executing the same statement multiple times with different values.
Values for opts are:
auto_release: bool- This defaults to and should usually be kept asfalse. Whentrue, the statement is automatically discarded (deinit) after the result of its first execution is complete. If you're going to set this totrue, you might as well useconn.exec,conn.queryorconn.rowinstead of getting an explicit statement.
err
If a method of conn returns error.DuckDBError, conn.err will be set:
var rows = conn.query("....", .{}) catch |err| {
if (err == error.DuckDBError) {
if (conn.err) |derr| {
std.log.err("DuckDB {s}\n", .{derr});
}
}
return err;
}
In the above snippet, it's possible to skip the if (err == error.DuckDBError)check, but in that case conn.err could be set from some previous command (conn.err is always reset when acquired from the pool).
release
conn.release() will release the connection back to the pool. This does nothing if the connection did not come from the pool (i.e. pool.acquire()). This is the same as calling pool.release(conn).
Rows
The rows returned from conn.query exposes the following methods:
count()- the number of rows in the resultchanged()- the number of updated/deleted/inserted rowscolumnName(i: usize)- the column name at positioniin a resultdeinit()- must be called to free resources associated with the resultnext() !?Row- returns the next row
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
get
Row exposes a get(T, index) T function. This function trusts you! If you ask for an <code>i32</code> the library will crash if the column is not an <code>int4</code>. Similarly, if the value can be null, you must use the optional type, e.g. <code>?i32</code>.
The supported types for get, are:
[]u8,[]const u8i8i16i32i64i128u8u16u32u64f32f64boolzuckdb.Datezuckdb.Timezuckdb.Intervalzuckdb.UUIDzudkdb.Enum
Optional version of the above are all supported and must be used if it's possible the value is null.
String values and enums are only valid until the next call to next() or deinit. You must dupe the values if you want them to outlive the row.
list
Row exposes a list method which behaves similar to get but returns a zuckdb.List(T).
const row = (try conn.row("select [1, 32, 99, null, -4]::int[]", .{})) orelse unreachable;
defer row.deinit();
const list = row.list(?i32, 0).?;
try t.expectEqual(5, list.len);
try t.expectEqual(1, list.get(0).?);
try t.expectEqual(32, list.get(1).?);
try t.expectEqual(99, list.get(2).?);
try t.expectEqual(null, list.get(3));
try t.expectEqual(-4, list.get(4).?);
list() always returns a nullable, i.e. ?zuckdb.List(T). Besides the len field, get is used on the provided list to return a value at a specific index. row.list(T, col).get(idx) works with any of the types supported by row.get(col).
a List(T) also has a alloc(allocator: Allocator) ![]T method. This will allocate a []T and fill it with the list values. It is the caller's responsibility to free the returned slice.
Alternatively, fill(into: []T) void can be us
