├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── main.zig └── test.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Beachglass Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-duckdb 2 | 3 | A thin wrapper for duckdb in zig. 4 | 5 | The current implementation uses the dynamic library (libduckdb) released by 6 | [duckdb](https://github.com/duckdb/duckdb). 7 | 8 | This has only been used and tested on Linux. 9 | 10 | Please sanitize your sql before passing to this library as this is subject to 11 | sql injection attack if you are using strings passed in directly from users. 12 | A future version will likely expose prepared statement so the variables will 13 | be sanitized propertly. 14 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) !void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | const duckdb = b.dependency("duckdb", .{}); 19 | 20 | _ = b.addModule("duck", .{ 21 | .source_file = .{ .path = "src/main.zig" }, 22 | }); 23 | 24 | // (re-)add modules from libduckdb 25 | _ = b.addModule("libduckdb.include", .{ 26 | .source_file = .{ .path = duckdb.builder.pathFromRoot( 27 | duckdb.module("libduckdb.include").source_file.path, 28 | ) }, 29 | }); 30 | 31 | _ = b.addModule("libduckdb.library", .{ 32 | .source_file = .{ .path = duckdb.builder.pathFromRoot( 33 | duckdb.module("libduckdb.library").source_file.path, 34 | ) }, 35 | }); 36 | 37 | _ = b.addModule("libduckdb.h", .{ 38 | .source_file = .{ .path = duckdb.builder.pathFromRoot( 39 | duckdb.module("libduckdb.h").source_file.path, 40 | ) }, 41 | }); 42 | 43 | _ = b.addModule("libduckdb.so", .{ 44 | .source_file = .{ .path = duckdb.builder.pathFromRoot( 45 | duckdb.module("libduckdb.so").source_file.path, 46 | ) }, 47 | }); 48 | 49 | // _ = b.installLibFile(duckdb.builder.pathFromRoot( 50 | // duckdb.module("libduckdb.so").source_file.path, 51 | // ), "libduckdb.so"); 52 | 53 | const lib = b.addStaticLibrary(.{ 54 | .name = "duck", 55 | // In this case the main source file is merely a path, however, in more 56 | // complicated build scripts, this could be a generated file. 57 | .root_source_file = .{ .path = "src/main.zig" }, 58 | .target = target, 59 | .optimize = optimize, 60 | }); 61 | 62 | // This declares intent for the library to be installed into the standard 63 | // location when the user invokes the "install" step (the default step when 64 | // running `zig build`). 65 | b.installArtifact(lib); 66 | 67 | // Creates a step for unit testing. This only builds the test executable 68 | // but does not run it. 69 | const unit_tests = b.addTest(.{ 70 | .root_source_file = .{ .path = "src/test.zig" }, 71 | .target = target, 72 | .optimize = optimize, 73 | }); 74 | unit_tests.step.dependOn(b.getInstallStep()); 75 | unit_tests.linkLibC(); 76 | unit_tests.addLibraryPath(.{ .path = duckdb.builder.pathFromRoot( 77 | duckdb.module("libduckdb.library").source_file.path, 78 | ) }); 79 | unit_tests.addIncludePath(.{ .path = duckdb.builder.pathFromRoot( 80 | duckdb.module("libduckdb.include").source_file.path, 81 | ) }); 82 | unit_tests.linkSystemLibraryName("duckdb"); 83 | 84 | const run_unit_tests = b.addRunArtifact(unit_tests); 85 | run_unit_tests.setEnvironmentVariable("LD_LIBRARY_PATH", duckdb.builder.pathFromRoot( 86 | duckdb.module("libduckdb.library").source_file.path, 87 | )); 88 | 89 | // Similar to creating the run step earlier, this exposes a `test` step to 90 | // the `zig build --help` menu, providing a way for the user to request 91 | // running the unit tests. 92 | const test_step = b.step("test", "Run unit tests"); 93 | test_step.dependOn(&run_unit_tests.step); 94 | } 95 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "duck", 3 | .version = "0.0.7", 4 | .paths = .{"."}, 5 | .dependencies = .{ 6 | .duckdb = .{ 7 | .url = "https://github.com/beachglasslabs/libduckdb/archive/refs/tags/v0.9.1.6.tar.gz", 8 | .hash = "12201d053f086ca5ef37eb970298c7beada0ab7dd8857b1712e8cd523f64c119466f", 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const duckdb = @cImport(@cInclude("duckdb.h")); 3 | 4 | db: duckdb.duckdb_database, 5 | conn: duckdb.duckdb_connection, 6 | 7 | pub const duckdb_result = duckdb.duckdb_result; 8 | 9 | pub const Self = @This(); 10 | 11 | pub fn init(db_path: ?[]const u8) !Self { 12 | var self = Self{ 13 | .db = undefined, 14 | .conn = undefined, 15 | }; 16 | 17 | if (db_path) |db_file| { 18 | if (duckdb.duckdb_open(db_file.ptr, &self.db) == duckdb.DuckDBError) { 19 | std.log.err("duckdb: error opening db {s}\n", .{db_file}); 20 | return error.DuckDBError; 21 | } else { 22 | std.log.info("duckdb: db opened {s}\n", .{db_file}); 23 | } 24 | } else { 25 | if (duckdb.duckdb_open(null, &self.db) == duckdb.DuckDBError) { 26 | std.log.err("duckdb: error opening in-memory db\n", .{}); 27 | return error.DuckDBError; 28 | } else { 29 | std.log.info("duckdb: opened in-memory db\n", .{}); 30 | } 31 | } 32 | if (duckdb.duckdb_connect(self.db, &self.conn) == duckdb.DuckDBError) { 33 | std.log.err("duckdb: error connecting to db\n", .{}); 34 | return error.DuckDBError; 35 | } else { 36 | std.log.info("duckdb: db connected\n", .{}); 37 | } 38 | 39 | return self; 40 | } 41 | 42 | pub fn deinit(self: *Self) void { 43 | duckdb.duckdb_disconnect(&self.conn); 44 | duckdb.duckdb_close(&self.db); 45 | } 46 | 47 | // TODO sanitize string 48 | pub fn query(self: *const Self, query_str: []const u8) !void { 49 | var result = try self.queryResult(query_str); 50 | defer self.freeResult(&result); 51 | } 52 | 53 | // TODO sanitize string 54 | pub fn queryResult(self: *const Self, query_str: []const u8) !duckdb.duckdb_result { 55 | var result: duckdb.duckdb_result = undefined; 56 | std.log.debug("duckdb: query sql {s}\n", .{query_str}); 57 | if (duckdb.duckdb_query(self.conn, query_str.ptr, &result) == duckdb.DuckDBError) { 58 | std.log.err("duckdb: query error {s}\n", .{duckdb.duckdb_result_error(&result)}); 59 | return error.DuckDBError; 60 | } 61 | return result; 62 | } 63 | 64 | pub fn freeResult(_: *const Self, result: *duckdb.duckdb_result) void { 65 | defer duckdb.duckdb_destroy_result(result); 66 | } 67 | 68 | pub fn countRows(_: *const Self, result: *duckdb.duckdb_result) usize { 69 | return duckdb.duckdb_row_count(result); 70 | } 71 | 72 | pub fn countCols(_: *const Self, result: *duckdb.duckdb_result) usize { 73 | return duckdb.duckdb_column_count(result); 74 | } 75 | 76 | pub fn value(_: *const Self, allocator: std.mem.Allocator, result: *duckdb.duckdb_result, row: usize, col: usize) ![]const u8 { 77 | var val = duckdb.duckdb_value_varchar(result, col, row); 78 | defer duckdb.duckdb_free(val); 79 | return try allocator.dupe(u8, std.mem.span(val)); 80 | } 81 | 82 | pub fn boolean(_: *const Self, result: *duckdb.duckdb_result, row: usize, col: usize) bool { 83 | return duckdb.duckdb_value_boolean(result, col, row); 84 | } 85 | 86 | pub fn isNull(_: *const Self, result: *duckdb.duckdb_result, row: usize, col: usize) bool { 87 | return duckdb.duckdb_value_is_null(result, col, row); 88 | } 89 | 90 | pub fn optional(self: *const Self, allocator: std.mem.Allocator, result: *duckdb.duckdb_result, row: usize, col: usize) !?[]const u8 { 91 | if (self.isNull(result, col, row)) { 92 | return null; 93 | } else { 94 | return try self.value(allocator, result, col, row); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const DuckDb = @import("main.zig"); 4 | 5 | test "boolean" { 6 | var duck = try DuckDb.init(null); 7 | defer duck.deinit(); 8 | 9 | try duck.query("CREATE TABLE test_bool_table (test_bool BOOL);"); 10 | var true_sql = try std.fmt.allocPrintZ(std.testing.allocator, "INSERT INTO test_bool_table (SELECT '{}') RETURNING test_bool;", .{true}); 11 | defer std.testing.allocator.free(true_sql); 12 | var result = try duck.queryResult(true_sql); 13 | defer duck.freeResult(&result); 14 | try std.testing.expect(duck.countRows(&result) == 1); 15 | var true_result = duck.boolean(&result, 0, 0); 16 | try std.testing.expect(true_result == true); 17 | var false_sql = try std.fmt.allocPrintZ(std.testing.allocator, "INSERT INTO test_bool_table (SELECT {}) RETURNING test_bool;", .{false}); 18 | defer std.testing.allocator.free(false_sql); 19 | var result2 = try duck.queryResult(false_sql); 20 | defer duck.freeResult(&result2); 21 | try std.testing.expect(duck.countRows(&result2) == 1); 22 | var false_result = duck.boolean(&result2, 0, 0); 23 | try std.testing.expect(false_result == false); 24 | } 25 | 26 | test "optional" { 27 | var duck = try DuckDb.init(null); 28 | defer duck.deinit(); 29 | 30 | var opt_val: ?[]const u8 = "dog"; 31 | try duck.query("CREATE TABLE test_optional_table (test_optional varchar(32));"); 32 | var str_sql = try std.fmt.allocPrintZ(std.testing.allocator, "INSERT INTO test_optional_table(test_optional) VALUES ('{?s}') RETURNING test_optional;", .{opt_val}); 33 | defer std.testing.allocator.free(str_sql); 34 | var result = try duck.queryResult(str_sql); 35 | defer duck.freeResult(&result); 36 | try std.testing.expect(duck.countRows(&result) == 1); 37 | var str_result = try duck.optional(std.testing.allocator, &result, 0, 0); 38 | defer if (str_result) |str| std.testing.allocator.free(str); 39 | if (str_result) |str| { 40 | try std.testing.expect(std.mem.eql(u8, str, opt_val.?)); 41 | } 42 | opt_val = null; 43 | var null_sql = try std.fmt.allocPrintZ(std.testing.allocator, "INSERT INTO test_optional_table (test_optional) VALUES ({?s}) RETURNING test_optional;", .{opt_val}); 44 | defer std.testing.allocator.free(null_sql); 45 | var result2 = try duck.queryResult(null_sql); 46 | defer duck.freeResult(&result2); 47 | try std.testing.expect(duck.countRows(&result2) == 1); 48 | var null_result = try duck.optional(std.testing.allocator, &result2, 0, 0); 49 | try std.testing.expectEqual(null_result, opt_val); 50 | } 51 | --------------------------------------------------------------------------------