├── .clang-format ├── .gitignore ├── .gitmodules ├── .tool-versions ├── .vscode └── settings.json ├── Justfile ├── LICENSE ├── README.md ├── assets ├── dark-duck.svg └── light-duck.svg ├── build.zig ├── build.zig.zon ├── duckdb.build.zig ├── lib └── .gitkeep └── src ├── bridge.cpp ├── duckdbext.zig ├── include └── bridge.hpp └── main.zig /.clang-format: -------------------------------------------------------------------------------- 1 | # https://github.com/duckdb/duckdb/blob/main/.clang-format 2 | --- 3 | BasedOnStyle: LLVM 4 | TabWidth: 4 5 | IndentWidth: 4 6 | ColumnLimit: 120 7 | AllowShortFunctionsOnASingleLine: false 8 | --- 9 | UseTab: ForIndentation 10 | DerivePointerAlignment: false 11 | PointerAlignment: Right 12 | AlignConsecutiveMacros: true 13 | AlignTrailingComments: true 14 | AllowAllArgumentsOnNextLine: true 15 | AllowAllConstructorInitializersOnNextLine: true 16 | AllowAllParametersOfDeclarationOnNextLine: true 17 | AlignAfterOpenBracket: Align 18 | SpaceBeforeCpp11BracedList: true 19 | SpaceBeforeCtorInitializerColon: true 20 | SpaceBeforeInheritanceColon: true 21 | SpacesInAngles: false 22 | SpacesInCStyleCastParentheses: false 23 | SpacesInConditionalStatement: false 24 | AllowShortLambdasOnASingleLine: Inline 25 | AllowShortLoopsOnASingleLine: false 26 | AlwaysBreakTemplateDeclarations: Yes 27 | IncludeBlocks: Regroup 28 | Language: Cpp 29 | AccessModifierOffset: -4 30 | --- 31 | Language: Java 32 | SpaceAfterCStyleCast: true 33 | --- 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "duckdb"] 2 | path = duckdb 3 | url = https://github.com/duckdb/duckdb 4 | branch = main 5 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | zig 0.12.0 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.toml": "toml", 4 | "__config": "cpp", 5 | "__availability": "cpp", 6 | "condition_variable": "cpp", 7 | "memory": "cpp", 8 | "ostream": "cpp", 9 | "regex": "cpp", 10 | "string": "cpp", 11 | "__locale": "cpp", 12 | "bitset": "cpp", 13 | "filesystem": "cpp", 14 | "iomanip": "cpp", 15 | "istream": "cpp", 16 | "locale": "cpp", 17 | "sstream": "cpp" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | ddb_version := "1.0.0" 2 | 3 | update-ddb: 4 | @cd duckdb && git checkout v{{ddb_version}} 5 | 6 | bootstrap: 7 | # git submodule add -b main --name duckdb https://github.com/duckdb/duckdb 8 | @git clone -b v{{ddb_version}} --single-branch https://github.com/duckdb/duckdb.git 9 | @git submodule update --init --recursive 10 | 11 | fetch-libduckdb: 12 | @curl -sL --output lib/duckdb.zip https://github.com/duckdb/duckdb/releases/download/v{{ddb_version}}/libduckdb-osx-universal.zip 13 | @unzip -o lib/duckdb.zip -d lib 14 | 15 | build: #fetch-libduckdb 16 | @zig build -freference-trace 17 | 18 | test: build 19 | @duckdb -unsigned -s "LOAD './zig-out/lib/quack.duckdb_extension'; FROM quack(times = 5)" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
4 | 🐥 a DuckDB extension library for zig 5 |
6 | 7 |
8 |
9 |
10 |
15 | 16 | --- 17 | 18 | The zig analog to the [c++ extension template](https://github.com/duckdb/extension-template) 19 | 20 | \- softprops 2024 21 | -------------------------------------------------------------------------------- /assets/dark-duck.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/light-duck.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const duckdb = @import("duckdb.build.zig"); 3 | 4 | // Although this function looks imperative, note that its job is to 5 | // declaratively construct a build graph that will be executed by an external 6 | // runner. 7 | pub fn build(b: *std.Build) void { 8 | // Standard target options allows the person running `zig build` to choose 9 | // what target to build for. Here we do not override the defaults, which 10 | // means any target is allowed, and the default is native. Other options 11 | // for restricting supported target set are available. 12 | const target = b.standardTargetOptions(.{}); 13 | 14 | // Standard optimization options allow the person running `zig build` to select 15 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 16 | // set a preferred release mode, allowing the user to decide how to optimize. 17 | const optimize = b.standardOptimizeOption(.{}); 18 | 19 | _ = b.addModule("duckdb_ext", .{ 20 | .root_source_file = b.path("src/duckdbext.zig"), 21 | }); 22 | 23 | const name = "quack"; 24 | const lib = b.addSharedLibrary(.{ 25 | .name = name, 26 | // In this case the main source file is merely a path, however, in more 27 | // complicated build scripts, this could be a generated file. 28 | .root_source_file = b.path("src/main.zig"), 29 | .target = target, 30 | .optimize = optimize, 31 | }); 32 | 33 | // duckdb headers 34 | lib.addIncludePath(b.path("duckdb/src/include")); 35 | lib.addIncludePath(b.path("duckdb/third_party/re2")); 36 | 37 | // our c bridge 38 | lib.addIncludePath(b.path("src/include")); 39 | // our c++ bridge 40 | lib.addCSourceFile(.{ .file = b.path("src/bridge.cpp") }); 41 | 42 | lib.linkLibC(); 43 | // https://github.com/ziglang/zig/blob/e1ca6946bee3acf9cbdf6e5ea30fa2d55304365d/build.zig#L369-L373 44 | lib.linkSystemLibrary("c++"); 45 | 46 | lib.linkSystemLibrary("duckdb"); 47 | lib.addLibraryPath(b.path("lib")); 48 | 49 | // resolve memory zls issue 50 | //lib.addIncludePath(b.path("/usr/share")); 51 | 52 | // This declares intent for the library to be installed into the standard 53 | // location when the user invokes the "install" step (the default step when 54 | // running `zig build`). 55 | //b.installArtifact(lib); 56 | 57 | //b.getInstallStep().dependOn(&b.addInstallArtifact( 58 | // lib, 59 | // .{ 60 | // .dest_sub_path = name ++ ".duckdb_extension", 61 | // }, 62 | // ).step); 63 | b.getInstallStep().dependOn(&duckdb.appendMetadata( 64 | b, 65 | b.addInstallArtifact( 66 | lib, 67 | .{ 68 | .dest_sub_path = name ++ ".duckdb_extension", 69 | }, 70 | ), 71 | .{ .platform = "osx_arm64" }, 72 | ).step); 73 | 74 | // Creates a step for unit testing. This only builds the test executable 75 | // but does not run it. 76 | const main_tests = b.addTest(.{ 77 | .root_source_file = .{ .path = "src/main.zig" }, 78 | .target = target, 79 | .optimize = optimize, 80 | }); 81 | 82 | // duckdb headers 83 | main_tests.addIncludePath(b.path("duckdb/src/include")); 84 | main_tests.addIncludePath(b.path("duckdb/third_party/re2")); 85 | 86 | // our c bridge 87 | main_tests.addIncludePath(b.path("src/include")); 88 | 89 | // our c++ bridge 90 | main_tests.addCSourceFile(.{ .file = b.path("src/bridge.cpp") }); 91 | 92 | main_tests.linkLibC(); 93 | // https://github.com/ziglang/zig/blob/e1ca6946bee3acf9cbdf6e5ea30fa2d55304365d/build.zig#L369-L373 94 | main_tests.linkSystemLibrary("c++"); 95 | 96 | main_tests.linkSystemLibrary("duckdb"); 97 | main_tests.addLibraryPath(b.path("lib")); 98 | 99 | const run_main_tests = b.addRunArtifact(main_tests); 100 | 101 | // This creates a build step. It will be visible in the `zig build --help` menu, 102 | // and can be selected like this: `zig build test` 103 | // This will evaluate the `test` step rather than the default, which is "install". 104 | const test_step = b.step("test", "Run library tests"); 105 | test_step.dependOn(&run_main_tests.step); 106 | } 107 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "duckdb_ext", 3 | .version = "0.1.0", 4 | .min_zig_version = "0.12.0", 5 | .paths = .{""}, 6 | } 7 | -------------------------------------------------------------------------------- /duckdb.build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Appends DuckDb extension metadata to library artifact 4 | pub fn appendMetadata( 5 | owner: *std.Build, 6 | installArtifact: *std.Build.Step.InstallArtifact, 7 | options: AppendMetadata.Options, 8 | ) *AppendMetadata { 9 | var append = AppendMetadata.create( 10 | owner, 11 | installArtifact, 12 | options, 13 | ); 14 | append.step.dependOn(&installArtifact.step); 15 | return append; 16 | } 17 | 18 | pub const AppendMetadata = struct { 19 | step: std.Build.Step, 20 | installArtifact: *std.Build.Step.InstallArtifact, 21 | options: Options, 22 | 23 | pub const Options = struct { 24 | duckDbVersion: []const u8 = "v1.0.0", 25 | platform: []const u8, 26 | extVersion: ?[]const u8 = null, 27 | }; 28 | 29 | pub fn create(owner: *std.Build, installArtifact: *std.Build.Step.InstallArtifact, options: Options) *AppendMetadata { 30 | const self = owner.allocator.create(AppendMetadata) catch @panic("OOM"); 31 | self.* = .{ 32 | .options = options, 33 | .installArtifact = installArtifact, 34 | .step = std.Build.Step.init(.{ 35 | .id = .custom, 36 | .name = "append-metadata", 37 | .owner = owner, 38 | .makeFn = make, 39 | }), 40 | }; 41 | return self; 42 | } 43 | 44 | fn make(step: *std.Build.Step, _: *std.Progress.Node) !void { 45 | const self: *AppendMetadata = @fieldParentPtr("step", step); 46 | const path = self.installArtifact.artifact.installed_path.?; 47 | var payload = std.mem.zeroes([512]u8); 48 | const segments = [_][]const u8{ 49 | "", "", "", "", 50 | self.options.extVersion orelse self.options.duckDbVersion, self.options.duckDbVersion, self.options.platform, "4", 51 | }; 52 | for (segments, 0..) |segment, i| { 53 | const start = 32 * i; 54 | const end = start + segments[i].len; 55 | @memcpy(payload[start..end], segment); 56 | } 57 | var file = try std.fs.cwd().openFile(path, .{ .mode = .read_write }); 58 | try file.seekTo(try file.getEndPos()); 59 | try file.writer().writeAll(&payload); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softprops/zig-duckdb-ext/5bc3a3c11e9ab001077d2afd2342ec7a1b727547/lib/.gitkeep -------------------------------------------------------------------------------- /src/bridge.cpp: -------------------------------------------------------------------------------- 1 | #define DUCKDB_EXTENSION_MAIN 2 | 3 | #include "include/bridge.hpp" 4 | 5 | #include "duckdb.hpp" 6 | 7 | extern "C" { 8 | 9 | // implemented in zig 10 | void quack_init_zig(void *db); 11 | 12 | // implemented in zig 13 | const char *quack_version_zig(void); 14 | 15 | // called by duckdb cli using the convention {extension_name}_init(db) 16 | DUCKDB_EXTENSION_API void quack_init(duckdb::DatabaseInstance &db) { 17 | quack_init_zig((void *)&db); 18 | } 19 | 20 | // called by duckdb cli using the convention {extension_name}_version() 21 | DUCKDB_EXTENSION_API const char *quack_version() { 22 | return quack_version_zig(); 23 | } 24 | }; 25 | 26 | #ifndef DUCKDB_EXTENSION_MAIN 27 | #error DUCKDB_EXTENSION_MAIN not defined 28 | #endif 29 | 30 | void duckdb::QuackExtension::Load(DuckDB &db) { 31 | DuckDB *ptr = &db; 32 | quack_init_zig((void *)ptr); 33 | } 34 | 35 | std::string duckdb::QuackExtension::Name() { 36 | return "quack"; 37 | } -------------------------------------------------------------------------------- /src/duckdbext.zig: -------------------------------------------------------------------------------- 1 | //! A library for building [DuckDB Extensions](https://duckdb.org/docs/extensions/overview) with zig 2 | const std = @import("std"); 3 | 4 | // https://duckdb.org/docs/api/c/api.html 5 | const c = @cImport(@cInclude("duckdb.h")); 6 | 7 | /// A simple DB interface intended for use with testing extensions 8 | /// see https://duckdb.org/docs/connect/overview.html#in-memory-database for more info on in memory databases 9 | pub const DB = struct { 10 | allocator: ?std.mem.Allocator, 11 | ptr: *c.duckdb_database, 12 | 13 | /// wrap db provided by duckdb runtime 14 | pub fn provided(ptr: *c.duckdb_database) @This() { 15 | return .{ .ptr = ptr, .allocator = null }; 16 | } 17 | 18 | pub fn memory(allocator: std.mem.Allocator) !@This() { 19 | const config = try allocator.create(c.duckdb_config); 20 | defer allocator.destroy(config); 21 | if (c.duckdb_create_config(config) == c.DuckDBError) { 22 | return error.ConfigError; 23 | } 24 | 25 | const db = try allocator.create(c.duckdb_database); 26 | errdefer allocator.destroy(db); 27 | var open_err: [*c]u8 = undefined; 28 | if (c.duckdb_open_ext(":memory:", db, config.*, &open_err) == c.DuckDBError) { 29 | std.debug.print("error opening db: {s}\n", .{open_err}); 30 | defer c.duckdb_free(open_err); 31 | return error.OpenError; 32 | } 33 | 34 | return .{ .ptr = db, .allocator = allocator }; 35 | } 36 | 37 | /// extensions will be passed in a database as anyopaque value 38 | /// use this to faciliate that. 39 | pub fn toOpaque(self: *@This()) *anyopaque { 40 | return @as(*anyopaque, @ptrCast(self.ptr)); 41 | } 42 | 43 | pub fn deinit(self: *@This()) void { 44 | if (self.allocator) |alloc| { 45 | c.duckdb_close(self.ptr); 46 | alloc.destroy(self.ptr); 47 | } 48 | } 49 | }; 50 | 51 | /// Logical types describe the possible types which may be used in parameters 52 | /// in DuckDB 53 | pub const LogicalType = enum(c.enum_DUCKDB_TYPE) { 54 | bool = c.DUCKDB_TYPE_BOOLEAN, 55 | tinyint = c.DUCKDB_TYPE_TINYINT, 56 | smallint = c.DUCKDB_TYPE_SMALLINT, 57 | int = c.DUCKDB_TYPE_INTEGER, 58 | bigint = c.DUCKDB_TYPE_BIGINT, 59 | utinyint = c.DUCKDB_TYPE_UTINYINT, 60 | usmallint = c.DUCKDB_TYPE_USMALLINT, 61 | uint = c.DUCKDB_TYPE_UINTEGER, 62 | ubitint = c.DUCKDB_TYPE_UBIGINT, 63 | float = c.DUCKDB_TYPE_FLOAT, 64 | double = c.DUCKDB_TYPE_DOUBLE, 65 | timestamp = c.DUCKDB_TYPE_TIMESTAMP, 66 | date = c.DUCKDB_TYPE_DATE, 67 | time = c.DUCKDB_TYPE_TIME, 68 | interval = c.DUCKDB_TYPE_INTERVAL, 69 | hugeint = c.DUCKDB_TYPE_HUGEINT, 70 | varchar = c.DUCKDB_TYPE_VARCHAR, 71 | blog = c.DUCKDB_TYPE_BLOB, 72 | decimal = c.DUCKDB_TYPE_DECIMAL, 73 | timestamp_s = c.DUCKDB_TYPE_TIMESTAMP_S, 74 | timestamp_ms = c.DUCKDB_TYPE_TIMESTAMP_MS, 75 | timestamp_ns = c.DUCKDB_TYPE_TIMESTAMP_NS, 76 | @"enum" = c.DUCKDB_TYPE_ENUM, 77 | list = c.DUCKDB_TYPE_LIST, 78 | @"struct" = c.DUCKDB_TYPE_STRUCT, 79 | map = c.DUCKDB_TYPE_MAP, 80 | uuid = c.DUCKDB_TYPE_UUID, 81 | @"union" = c.DUCKDB_TYPE_UNION, 82 | bit = c.DUCKDB_TYPE_BIT, 83 | 84 | const Ref = struct { 85 | ptr: c.duckdb_logical_type, 86 | fn deinit(self: *@This()) void { 87 | c.duckdb_destroy_logical_type(&self.ptr); 88 | } 89 | }; 90 | 91 | fn from(tpe: c.duckdb_type) LogicalType { 92 | return @enumFromInt(tpe); 93 | } 94 | 95 | fn toInternal(self: @This()) Ref { 96 | // Creates a duckdb_logical_type from a standard primitive type. 97 | // todo: The resulting type should be destroyed with duckdb_destroy_logical_type. 98 | // todo: This should not be used with DUCKDB_TYPE_DECIMAL. 99 | return .{ .ptr = c.duckdb_create_logical_type(@intFromEnum(self)) }; 100 | } 101 | }; 102 | 103 | pub const Value = struct { 104 | val: c.duckdb_value, 105 | fn init(val: c.duckdb_value) @This() { 106 | return .{ .val = val }; 107 | } 108 | 109 | // todo: This must be destroyed with duckdb_free. 110 | /// Obtains a string representation of the given value 111 | pub fn toString(self: *@This()) [*:0]u8 { 112 | return c.duckdb_get_varchar(self.val); 113 | } 114 | 115 | /// Obtains an int64 of the given value. 116 | pub fn toI64(self: *@This()) ?i64 { 117 | return c.duckdb_get_int64(self.val); 118 | } 119 | 120 | pub fn deinit(self: *@This()) void { 121 | c.duckdb_destroy_value(&self.val); 122 | } 123 | }; 124 | 125 | pub const InitInfo = struct { 126 | ptr: c.duckdb_init_info, 127 | fn init(info: c.duckdb_init_info) @This() { 128 | return .{ .ptr = info }; 129 | } 130 | 131 | pub fn setErr(self: *@This(), err: [*:0]const u8) void { 132 | c.duckdb_init_set_error(self.ptr, err); 133 | } 134 | }; 135 | 136 | pub const BindInfo = struct { 137 | ptr: c.duckdb_bind_info, 138 | fn init(info: c.duckdb_bind_info) @This() { 139 | return .{ .ptr = info }; 140 | } 141 | 142 | /// Adds a result column to the output of the table function. 143 | pub fn addResultColumn( 144 | self: *@This(), 145 | name: [*:0]const u8, 146 | lType: LogicalType, 147 | ) void { 148 | var typeRef = lType.toInternal(); 149 | defer typeRef.deinit(); 150 | c.duckdb_bind_add_result_column( 151 | self.ptr, 152 | name, 153 | typeRef.ptr, 154 | ); 155 | } 156 | 157 | pub fn setCardinality( 158 | self: *@This(), 159 | cardinality: u64, 160 | exact: bool, 161 | ) void { 162 | c.duckdb_bind_set_cardinality(self.ptr, cardinality, exact); 163 | } 164 | 165 | /// Get an indexed parameter's value if any exists at this index for this binding 166 | pub fn getParameter( 167 | self: *@This(), 168 | idx: u64, 169 | ) ?Value { 170 | const param = c.duckdb_bind_get_parameter(self.ptr, idx); 171 | if (param == null) { 172 | return null; 173 | } 174 | return Value.init(param); 175 | } 176 | 177 | /// Retrieves the parameter value at the given index. 178 | /// The result must be destroyed with Value.deinit(). 179 | pub fn getNamedParameter( 180 | self: @This(), 181 | name: [*:0]const u8, 182 | ) ?Value { 183 | const param = c.duckdb_bind_get_named_parameter(self.ptr, name); 184 | if (param == null) { 185 | return null; 186 | } 187 | return Value.init(param); 188 | } 189 | 190 | /// Retrieves the number of regular (non-named) parameters to the function 191 | pub fn getParameterCount(self: @This()) u64 { 192 | return c.duckdb_bind_get_parameter_count(self.ptr); 193 | } 194 | 195 | pub fn setErr(self: *@This(), err: [*:0]const u8) void { 196 | c.duckdb_bind_set_error(self.ptr, err); 197 | } 198 | }; 199 | 200 | pub const DataChunk = struct { 201 | ptr: c.duckdb_data_chunk, 202 | fn init(data: c.duckdb_data_chunk) @This() { 203 | return .{ .ptr = data }; 204 | } 205 | 206 | pub fn setSize( 207 | self: *@This(), 208 | size: u64, 209 | ) void { 210 | c.duckdb_data_chunk_set_size( 211 | self.ptr, 212 | size, 213 | ); 214 | } 215 | 216 | pub fn vector( 217 | self: *@This(), 218 | colIndex: u64, 219 | ) Vector { 220 | return Vector.init( 221 | c.duckdb_data_chunk_get_vector( 222 | self.ptr, 223 | colIndex, 224 | ), 225 | ); 226 | } 227 | }; 228 | 229 | pub const Vector = struct { 230 | ptr: c.duckdb_vector, 231 | fn init(vec: c.duckdb_vector) @This() { 232 | return .{ .ptr = vec }; 233 | } 234 | 235 | /// Assigns a string element in the vector at the specified location. 236 | /// Note: the provided string should be null terminiated 237 | pub fn assignStringElement( 238 | self: @This(), 239 | index: u64, 240 | value: []const u8, 241 | ) void { 242 | c.duckdb_vector_assign_string_element( 243 | self.ptr, 244 | index, 245 | std.mem.sliceTo(value, 0).ptr, 246 | ); 247 | } 248 | }; 249 | 250 | /// An established connection to a duckdb database 251 | pub const Connection = struct { 252 | allocator: std.mem.Allocator, 253 | ptr: *c.duckdb_connection, 254 | 255 | /// Returns an an active duckdb database connection 256 | pub fn init( 257 | allocator: std.mem.Allocator, 258 | db: DB, 259 | ) !@This() { 260 | const con: *c.duckdb_connection = try allocator.create( 261 | c.duckdb_connection, 262 | ); 263 | // if we fail to connect to the db, make sure we release the connection memory 264 | errdefer allocator.destroy(con); 265 | if (c.duckdb_connect( 266 | db.ptr.*, 267 | con, 268 | ) == c.DuckDBError) { 269 | std.debug.print("error connecting to duckdb\n", .{}); 270 | return error.ConnectionError; 271 | } 272 | return .{ .ptr = con, .allocator = allocator }; 273 | } 274 | 275 | pub fn deinit(self: *@This()) void { 276 | self.allocator.destroy(self.ptr); 277 | } 278 | 279 | /// Registers a new table function with this connection. Returns false if this registration attempt 280 | /// fails 281 | pub fn registerTableFunction(self: *@This(), func: TableFunctionRef) bool { 282 | if (c.duckdb_register_table_function(self.ptr.*, func.ptr) == c.DuckDBError) { 283 | std.debug.print("error registering duckdb table func\n", .{}); 284 | return false; 285 | } 286 | return true; 287 | } 288 | }; 289 | 290 | fn DeinitData(comptime T: type) type { 291 | return struct { 292 | fn deinit(data: ?*anyopaque) callconv(.C) void { 293 | if (std.meta.hasFn(T, "deinit")) { 294 | var typed: *T = @ptrCast(@alignCast(data)); 295 | typed.deinit(); 296 | } 297 | c.duckdb_free(data); 298 | } 299 | }; 300 | } 301 | 302 | /// Returns the DuckDB version this build is linked to 303 | pub fn duckdbVersion() [*:0]const u8 { 304 | return c.duckdb_library_version(); 305 | } 306 | 307 | /// A tuple type used to declare TableFunction named parameters 308 | pub const NamedParameter = struct { [*:0]const u8, LogicalType }; 309 | 310 | pub const TableFunctionRef = struct { 311 | ptr: c.duckdb_table_function, 312 | }; 313 | 314 | /// A TableFunction type can generate new table function instances via the `create()` fn which can then be 315 | /// registered with a DuckDB connection for use 316 | pub fn TableFunction( 317 | /// an instance of this type will be allocated when initFunc is invoked. 318 | /// 319 | /// if a method of type deinit is defined on this type it will be called by the duckdb runtime 320 | comptime IData: type, 321 | /// an instance of this type will be allocated when bindFunc is invoked 322 | /// 323 | /// if a method of type deinit is defined on this type it will be called by the duckdb runtime 324 | comptime BData: type, 325 | /// intialization function impl 326 | initFunc: fn (*InitInfo, *IData) anyerror!void, 327 | /// bind function impl 328 | bindFunc: fn (*BindInfo, *BData) anyerror!void, 329 | /// excecution function impl 330 | funcFunc: fn (*DataChunk, *IData, *BData) anyerror!void, 331 | ) type { 332 | return struct { 333 | name: [*:0]const u8, 334 | parameters: []const LogicalType = &[_]LogicalType{}, 335 | named_parameters: []const NamedParameter = &[_]NamedParameter{}, 336 | supports_projection_pushdown: bool = false, 337 | 338 | /// creates a new underlying table function reference 339 | pub fn create(self: *@This()) TableFunctionRef { 340 | const ptr = c.duckdb_create_table_function(); 341 | c.duckdb_table_function_set_name(ptr, self.name); 342 | c.duckdb_table_function_supports_projection_pushdown(ptr, self.supports_projection_pushdown); 343 | c.duckdb_table_function_set_bind(ptr, bind); 344 | c.duckdb_table_function_set_init(ptr, init); 345 | c.duckdb_table_function_set_function(ptr, func); 346 | for (self.parameters) |p| { 347 | var typeRef = p.toInternal(); 348 | defer typeRef.deinit(); 349 | c.duckdb_table_function_add_parameter(ptr, typeRef.ptr); 350 | } 351 | for (self.named_parameters) |p| { 352 | var typeRef = p[1].toInternal(); 353 | defer typeRef.deinit(); 354 | c.duckdb_table_function_add_named_parameter(ptr, p[0], typeRef.ptr); 355 | } 356 | return .{ .ptr = ptr }; 357 | } 358 | 359 | // c apis 360 | 361 | fn init(info: c.duckdb_init_info) callconv(.C) void { 362 | std.log.debug("init called...", .{}); 363 | var initInfo = InitInfo.init(info); 364 | const data: *IData = @ptrCast(@alignCast(c.duckdb_malloc(@sizeOf(IData)))); 365 | initFunc(&initInfo, data) catch |e| { 366 | std.debug.print("error initializing {any}", .{e}); 367 | initInfo.setErr("error initing data"); 368 | }; 369 | 370 | c.duckdb_init_set_init_data(info, data, DeinitData(IData).deinit); 371 | } 372 | 373 | fn bind(info: c.duckdb_bind_info) callconv(.C) void { 374 | std.log.debug("bind called...", .{}); 375 | var bindInfo = BindInfo.init(info); 376 | const data: *BData = @ptrCast(@alignCast(c.duckdb_malloc(@sizeOf(BData)))); 377 | 378 | const result = bindFunc(&bindInfo, data); 379 | 380 | c.duckdb_bind_set_bind_data(info, data, DeinitData(BData).deinit); 381 | result catch { 382 | bindInfo.setErr("error binding data"); 383 | }; 384 | } 385 | 386 | fn func(info: c.duckdb_function_info, output: c.duckdb_data_chunk) callconv(.C) void { 387 | std.log.debug("func called...", .{}); 388 | const initData: *IData = @ptrCast(@alignCast(c.duckdb_function_get_init_data(info))); 389 | const bindData: *BData = @ptrCast(@alignCast(c.duckdb_function_get_bind_data(info))); 390 | var dataChunk = DataChunk.init(output); 391 | funcFunc(&dataChunk, initData, bindData) catch |e| { 392 | std.debug.print("error applying func {any}", .{e}); 393 | }; 394 | } 395 | }; 396 | } 397 | -------------------------------------------------------------------------------- /src/include/bridge.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "duckdb.hpp" 5 | 6 | extern "C" { 7 | DUCKDB_EXTENSION_API char const *quack_version(); 8 | DUCKDB_EXTENSION_API void quack_init(duckdb::DatabaseInstance &db); 9 | } 10 | 11 | namespace duckdb { 12 | class QuackExtension : public Extension { 13 | public: 14 | void Load(DuckDB &db) override; 15 | std::string Name() override; 16 | }; 17 | } // namespace duckdb -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const duckdbext = @import("duckdbext.zig"); 3 | 4 | const InitData = struct { 5 | done: bool, 6 | }; 7 | 8 | const BindData = struct { 9 | times: usize, 10 | }; 11 | 12 | /// called by c++ bridge when loading ext 13 | export fn quack_version_zig() [*:0]const u8 { 14 | return duckdbext.duckdbVersion(); 15 | } 16 | 17 | test quack_version_zig { 18 | try std.testing.expectEqualStrings( 19 | "v1.0.0", 20 | std.mem.sliceTo(quack_version_zig(), 0), 21 | ); 22 | } 23 | 24 | /// called by c++ bridge when loading ext 25 | export fn quack_init_zig(db: *anyopaque) void { 26 | std.log.debug("initializing ext...", .{}); 27 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 28 | defer _ = gpa.deinit(); 29 | const allocator = gpa.allocator(); 30 | 31 | var conn = duckdbext.Connection.init( 32 | allocator, 33 | duckdbext.DB.provided(@ptrCast(@alignCast(db))), 34 | ) catch |e| { 35 | std.debug.print( 36 | "error connecting to duckdb {any}\n", 37 | .{e}, 38 | ); 39 | @panic("error connecting to duckdb"); 40 | }; 41 | defer conn.deinit(); 42 | 43 | quack_init(&conn); 44 | } 45 | 46 | // split for test injection 47 | fn quack_init(conn: *duckdbext.Connection) void { 48 | var table_func = duckdbext.TableFunction( 49 | InitData, 50 | BindData, 51 | init, 52 | bind, 53 | func, 54 | ){ 55 | .name = "quack", 56 | .named_parameters = &[_]duckdbext.NamedParameter{ 57 | .{ "times", .int }, 58 | }, 59 | }; 60 | 61 | if (!conn.registerTableFunction(table_func.create())) { 62 | std.debug.print("error registering duckdb table func\n", .{}); 63 | return; 64 | } 65 | } 66 | 67 | test quack_init { 68 | const allocator = std.testing.allocator; 69 | 70 | var db = try duckdbext.DB.memory(allocator); 71 | defer db.deinit(); 72 | 73 | var conn = duckdbext.Connection.init( 74 | allocator, 75 | db, 76 | ) catch |e| { 77 | std.debug.print( 78 | "error connecting to duckdb {any}\n", 79 | .{e}, 80 | ); 81 | @panic("error connecting to duckdb"); 82 | }; 83 | defer conn.deinit(); 84 | 85 | quack_init(&conn); 86 | // todo exec test query 87 | } 88 | 89 | // impls 90 | 91 | fn bind( 92 | info: *duckdbext.BindInfo, 93 | data: *BindData, 94 | ) !void { 95 | info.addResultColumn("quacks", .varchar); 96 | 97 | var times = info.getNamedParameter("times") orelse { 98 | info.setErr("times parameter required"); 99 | return; 100 | }; 101 | defer times.deinit(); 102 | data.times = try std.fmt.parseInt( 103 | usize, 104 | std.mem.sliceTo(times.toString(), 0), 105 | 10, 106 | ); 107 | } 108 | 109 | fn init( 110 | _: *duckdbext.InitInfo, 111 | data: *InitData, 112 | ) !void { 113 | data.done = false; 114 | } 115 | 116 | fn func( 117 | chunk: *duckdbext.DataChunk, 118 | initData: *InitData, 119 | bindData: *BindData, 120 | ) !void { 121 | if (initData.done) { 122 | chunk.setSize(0); 123 | return; 124 | } 125 | 126 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 127 | defer _ = gpa.deinit(); 128 | const allocator = gpa.allocator(); 129 | 130 | initData.done = true; 131 | const repeated = try repeat(allocator, " 🐥", bindData.times); 132 | defer allocator.free(repeated); 133 | 134 | for (0..10) |i| { 135 | chunk.vector(i).assignStringElement(0, repeated); 136 | } 137 | chunk.setSize(10); 138 | } 139 | 140 | fn repeat(allocator: std.mem.Allocator, str: []const u8, times: usize) ![:0]const u8 { 141 | const repeated = try allocator.allocSentinel(u8, str.len * times, 0); 142 | var i: usize = 0; 143 | while (i < str.len * times) : (i += 1) { 144 | repeated[i] = str[i % str.len]; 145 | } 146 | return repeated; 147 | } 148 | --------------------------------------------------------------------------------