├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── src └── quack_extension.c └── test └── sql └── quack.test /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: {} 9 | 10 | jobs: 11 | build: 12 | name: Build and test 13 | runs-on: ubuntu-24.04 14 | timeout-minutes: 10 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: mlugg/setup-zig@v1 18 | with: 19 | version: "0.14.0" 20 | - uses: astral-sh/setup-uv@v5 21 | with: 22 | enable-cache: false 23 | - run: zig build -Dinstall-headers --verbose --summary new 24 | - run: zig build test -Dplatform=linux_amd64_gcc4 --summary new 25 | - run: tree -ash zig-out 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-cache 3 | zig-out 4 | 5 | repo 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Mathias Lafeldt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What the Duck? It's Zig! ⚡️ 2 | 3 | The infamous [DuckDB quack extension](https://duckdb.org/community_extensions/extensions/quack.html) rewritten in C and built with Zig. 4 | 5 | Proof that you can develop DuckDB extensions without drowning in boilerplate. 6 | 7 | ## Building the Extension 8 | 9 | Install [Zig](https://ziglang.org) and [uv](https://docs.astral.sh/uv/). That's it. No other dependencies are required. 10 | 11 | Now experience the power and simplicity of the [Zig Build System](https://ziglang.org/learn/build-system/) with these commands: 12 | 13 | ```shell 14 | # Build the extension for all supported DuckDB versions and platforms (Linux, macOS, Windows) 15 | zig build 16 | 17 | # Build for a list of DuckDB versions 18 | zig build -Dduckdb-version=1.1.3 -Dduckdb-version=1.2.0 19 | 20 | # Build for a list of platforms 21 | zig build -Dplatform=linux_arm64 -Dplatform=osx_arm64 -Dplatform=windows_arm64 22 | 23 | # Build for a specific DuckDB version and platform 24 | zig build -Dduckdb-version=1.1.3 -Dplatform=linux_amd64 25 | 26 | # Optimize for performance 27 | zig build --release=fast 28 | 29 | # Optimize for binary size 30 | zig build --release=small 31 | 32 | # Also install DuckDB C headers for development 33 | zig build -Dinstall-headers 34 | ``` 35 | 36 | The build output in `zig-out` will look like this: 37 | 38 | ``` 39 | ❯ tree zig-out 40 | zig-out 41 | ├── v1.1.0 42 | │ ├── linux_amd64 43 | │ │ └── quack.duckdb_extension 44 | │ ├── linux_amd64_gcc4 45 | │ │ └── quack.duckdb_extension 46 | │ ├── linux_arm64 47 | │ │ └── quack.duckdb_extension 48 | │ ├── linux_arm64_gcc4 49 | │ │ └── quack.duckdb_extension 50 | │ ├── osx_amd64 51 | │ │ └── quack.duckdb_extension 52 | │ ├── osx_arm64 53 | │ │ └── quack.duckdb_extension 54 | │ ├── windows_amd64 55 | │ │ └── quack.duckdb_extension 56 | │ └── windows_arm64 57 | │ └── quack.duckdb_extension 58 | ├── v1.1.1 59 | │ ├── linux_amd64 60 | │ │ └── quack.duckdb_extension 61 | │ ├── linux_amd64_gcc4 62 | │ │ └── quack.duckdb_extension 63 | │ ├── linux_arm64 64 | │ │ └── quack.duckdb_extension 65 | │ ├── linux_arm64_gcc4 66 | │ │ └── quack.duckdb_extension 67 | │ ├── osx_amd64 68 | │ │ └── quack.duckdb_extension 69 | │ ├── osx_arm64 70 | │ │ └── quack.duckdb_extension 71 | │ ├── windows_amd64 72 | │ │ └── quack.duckdb_extension 73 | │ └── windows_arm64 74 | │ └── quack.duckdb_extension 75 | ├── ... 76 | ``` 77 | 78 | See `zig build --help` for more options. 79 | 80 | ## Testing 81 | 82 | Run the [SQL logic tests](https://duckdb.org/docs/dev/sqllogictest/intro.html) with `zig build test`. 83 | 84 | ``` 85 | ❯ zig build test --summary new 86 | [0/1] test/sql/quack.test 87 | SUCCESS 88 | [0/1] test/sql/quack.test 89 | SUCCESS 90 | [0/1] test/sql/quack.test 91 | SUCCESS 92 | [0/1] test/sql/quack.test 93 | SUCCESS 94 | [0/1] test/sql/quack.test 95 | SUCCESS 96 | Build Summary: 16/16 steps succeeded 97 | test success 98 | ├─ sqllogictest v1.1.0 osx_arm64 success 91ms MaxRSS:43M 99 | ├─ sqllogictest v1.1.1 osx_arm64 success 92ms MaxRSS:44M 100 | ├─ sqllogictest v1.1.2 osx_arm64 success 92ms MaxRSS:43M 101 | ├─ sqllogictest v1.1.3 osx_arm64 success 93ms MaxRSS:44M 102 | └─ sqllogictest v1.2.0 osx_arm64 success 103ms MaxRSS:45M 103 | ``` 104 | 105 | You can also pass `-Dduckdb-version` to test against a specific DuckDB version, or use `-Dplatform` to select a different native platform, e.g. `linux_amd64_gcc4` instead of `linux_amd64`. 106 | 107 | ## Using the Extension 108 | 109 | ``` 110 | ❯ duckdb -unsigned 111 | v1.2.0 5f5512b827 112 | Enter ".help" for usage hints. 113 | 🟡◗ LOAD 'zig-out/v1.2.0/osx_arm64/quack.duckdb_extension'; 114 | 🟡◗ SELECT quack('Zig'); 115 | ┌──────────────┐ 116 | │ quack('Zig') │ 117 | │ varchar │ 118 | ├──────────────┤ 119 | │ Quack Zig 🐥 │ 120 | └──────────────┘ 121 | ``` 122 | 123 | ## Creating an Extension Repository 124 | 125 | You can easily create your own [extension repository](https://duckdb.org/docs/extensions/working_with_extensions.html#creating-a-custom-repository). In fact, `zig build` already does this for you by default! However, you might also want to write files to a different directory and compress them. Here's how: 126 | 127 | ```shell 128 | rm -rf repo 129 | 130 | zig build --prefix repo --release=fast 131 | 132 | gzip repo/*/*/*.duckdb_extension 133 | ``` 134 | 135 | This will generate a repository that is ready to be uploaded to S3 with a tool like [rclone](https://rclone.org). 136 | 137 | ## License 138 | 139 | Licensed under the [MIT License](LICENSE). 140 | 141 | Feel free to use this code as a starting point for your own DuckDB extensions. 🐤 142 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Build = std.Build; 3 | 4 | pub fn build(b: *Build) void { 5 | const optimize = b.standardOptimizeOption(.{}); 6 | const duckdb_versions = b.option([]const DuckDBVersion, "duckdb-version", "DuckDB version(s) to build for (default: all)") orelse DuckDBVersion.all; 7 | const platforms = b.option([]const Platform, "platform", "DuckDB platform(s) to build for (default: all)") orelse Platform.all; 8 | const install_headers = b.option(bool, "install-headers", "Install DuckDB C headers") orelse false; 9 | const flat = b.option(bool, "flat", "Install files without DuckDB version prefix") orelse false; 10 | 11 | if (flat and duckdb_versions.len > 1) { 12 | std.zig.fatal("-Dflat requires passing a specific DuckDB version", .{}); 13 | } 14 | 15 | const test_step = b.step("test", "Run SQL logic tests"); 16 | const check_step = b.step("check", "Check if extension compiles"); 17 | 18 | const metadata_script = b.dependency("extension_ci_tools", .{}).path("scripts/append_extension_metadata.py"); 19 | const sqllogictest = b.dependency("sqllogictest", .{}).path(""); 20 | 21 | const ext_version = detectGitVersion(b) catch "n/a"; 22 | 23 | for (duckdb_versions) |duckdb_version| { 24 | const version_string = duckdb_version.toString(b); 25 | const duckdb_headers = duckdb_version.headers(b); 26 | 27 | for (platforms) |platform| { 28 | const platform_string = platform.toString(); 29 | const target = platform.target(b); 30 | 31 | const ext = b.addSharedLibrary(.{ 32 | .name = "quack", 33 | .target = target, 34 | .optimize = optimize, 35 | }); 36 | ext.addCSourceFiles(.{ 37 | .files = &.{ 38 | "quack_extension.c", 39 | }, 40 | .root = b.path("src"), 41 | .flags = &cflags, 42 | }); 43 | ext.addIncludePath(duckdb_headers); 44 | ext.linkLibC(); 45 | ext.root_module.addCMacro("DUCKDB_EXTENSION_NAME", ext.name); 46 | ext.root_module.addCMacro("DUCKDB_BUILD_LOADABLE_EXTENSION", "1"); 47 | 48 | const filename = b.fmt("{s}.duckdb_extension", .{ext.name}); 49 | ext.install_name = b.fmt("@rpath/{s}", .{filename}); // macOS only 50 | 51 | const ext_path = path: { 52 | const cmd = Build.Step.Run.create(b, b.fmt("metadata {s} {s}", .{ version_string, platform_string })); 53 | cmd.addArgs(&.{ "uv", "run", "--python=3.12" }); 54 | cmd.addFileArg(metadata_script); 55 | cmd.addArgs(&.{ "--extension-name", ext.name }); 56 | cmd.addArgs(&.{ "--extension-version", ext_version }); 57 | cmd.addArgs(&.{ "--duckdb-platform", platform_string }); 58 | cmd.addArgs(&.{ "--duckdb-version", duckdb_version.extensionAPIVersion() }); 59 | cmd.addArg("--library-file"); 60 | cmd.addArtifactArg(ext); 61 | cmd.addArg("--out-file"); 62 | break :path cmd.addOutputFileArg(filename); 63 | }; 64 | 65 | { 66 | const install_file = b.addInstallFileWithDir(ext_path, .{ 67 | .custom = if (flat) platform_string else b.fmt("{s}/{s}", .{ version_string, platform_string }), 68 | }, filename); 69 | install_file.step.name = b.fmt("install {s} {s}", .{ version_string, platform_string }); 70 | b.getInstallStep().dependOn(&install_file.step); 71 | } 72 | 73 | if (install_headers) { 74 | const header_dirs = [_]Build.LazyPath{ 75 | duckdb_headers, 76 | // Add more header directories here 77 | }; 78 | for (header_dirs) |dir| { 79 | b.getInstallStep().dependOn(&b.addInstallDirectory(.{ 80 | .source_dir = dir, 81 | .include_extensions = &.{"h"}, 82 | .install_dir = if (flat) .header else .{ .custom = b.fmt("{s}/include", .{version_string}) }, 83 | .install_subdir = "", 84 | }).step); 85 | } 86 | } 87 | 88 | // Run tests on native platform 89 | if (b.graph.host.result.os.tag == target.result.os.tag and 90 | b.graph.host.result.cpu.arch == target.result.cpu.arch) 91 | { 92 | const cmd = Build.Step.Run.create(b, b.fmt("sqllogictest {s} {s}", .{ version_string, platform_string })); 93 | cmd.addArgs(&.{ "uv", "run", "--python=3.12", "--with" }); 94 | cmd.addFileArg(sqllogictest); 95 | cmd.addArgs(&.{ "--with", b.fmt("duckdb=={s}", .{@tagName(duckdb_version)}) }); 96 | cmd.addArgs(&.{ "python3", "-m", "duckdb_sqllogictest" }); 97 | cmd.addArgs(&.{ "--test-dir", "test" }); 98 | cmd.addArg("--external-extension"); 99 | cmd.addFileArg(ext_path); 100 | 101 | test_step.dependOn(&cmd.step); 102 | } 103 | 104 | check_step.dependOn(&ext.step); 105 | } 106 | } 107 | } 108 | 109 | const DuckDBVersion = enum { 110 | @"1.1.0", // First version with C API support 111 | @"1.1.1", 112 | @"1.1.2", 113 | @"1.1.3", 114 | @"1.2.0", 115 | @"1.2.1", 116 | @"1.2.2", 117 | 118 | const all = std.enums.values(@This()); 119 | 120 | fn toString(self: @This(), b: *Build) []const u8 { 121 | return b.fmt("v{s}", .{@tagName(self)}); 122 | } 123 | 124 | fn headers(self: @This(), b: *Build) Build.LazyPath { 125 | return switch (self) { 126 | .@"1.1.0", .@"1.1.1", .@"1.1.2", .@"1.1.3" => b.dependency("libduckdb_1_1_3", .{}).path(""), 127 | .@"1.2.0", .@"1.2.1", .@"1.2.2" => b.dependency("libduckdb_1_2_2", .{}).path(""), 128 | }; 129 | } 130 | 131 | fn extensionAPIVersion(self: @This()) [:0]const u8 { 132 | return switch (self) { 133 | .@"1.1.0", .@"1.1.1", .@"1.1.2", .@"1.1.3" => "v0.0.1", 134 | .@"1.2.0", .@"1.2.1", .@"1.2.2" => "v1.2.0", 135 | }; 136 | } 137 | }; 138 | 139 | const Platform = enum { 140 | linux_amd64, // Node.js packages, etc. 141 | linux_amd64_gcc4, // Python packages, CLI, etc. 142 | linux_arm64, 143 | linux_arm64_gcc4, 144 | osx_amd64, 145 | osx_arm64, 146 | windows_amd64, 147 | windows_arm64, 148 | 149 | const all = std.enums.values(@This()); 150 | 151 | fn toString(self: @This()) [:0]const u8 { 152 | return @tagName(self); 153 | } 154 | 155 | fn target(self: @This(), b: *Build) Build.ResolvedTarget { 156 | const manylinux2014_glibc_version = std.SemanticVersion{ .major = 2, .minor = 17, .patch = 0 }; 157 | 158 | return b.resolveTargetQuery(switch (self) { 159 | .linux_amd64 => .{ 160 | .os_tag = .linux, 161 | .cpu_arch = .x86_64, 162 | .abi = .gnu, 163 | }, 164 | .linux_amd64_gcc4 => .{ 165 | .os_tag = .linux, 166 | .cpu_arch = .x86_64, 167 | .abi = .gnu, 168 | .glibc_version = manylinux2014_glibc_version, 169 | }, 170 | .linux_arm64 => .{ 171 | .os_tag = .linux, 172 | .cpu_arch = .aarch64, 173 | .abi = .gnu, 174 | }, 175 | .linux_arm64_gcc4 => .{ 176 | .os_tag = .linux, 177 | .cpu_arch = .aarch64, 178 | .abi = .gnu, 179 | .glibc_version = manylinux2014_glibc_version, 180 | }, 181 | .osx_amd64 => .{ 182 | .os_tag = .macos, 183 | .cpu_arch = .x86_64, 184 | .abi = .none, 185 | }, 186 | .osx_arm64 => .{ 187 | .os_tag = .macos, 188 | .cpu_arch = .aarch64, 189 | .abi = .none, 190 | }, 191 | .windows_amd64 => .{ 192 | .os_tag = .windows, 193 | .cpu_arch = .x86_64, 194 | .abi = .gnu, // TODO: Switch to msvc? 195 | }, 196 | .windows_arm64 => .{ 197 | .os_tag = .windows, 198 | .cpu_arch = .aarch64, 199 | .abi = .gnu, // TODO: Switch to msvc? 200 | }, 201 | }); 202 | } 203 | }; 204 | 205 | fn detectGitVersion(b: *std.Build) ![]const u8 { 206 | var code: u8 = 0; 207 | const git_describe = try b.runAllowFail(&[_][]const u8{ 208 | "git", 209 | "-C", 210 | b.build_root.path orelse ".", 211 | "describe", 212 | "--tags", 213 | "--match", 214 | "v[0-9]*", 215 | "--always", 216 | }, &code, .Ignore); 217 | 218 | return std.mem.trim(u8, git_describe, " \n\r"); 219 | } 220 | 221 | const cflags = [_][]const u8{ 222 | "-Wall", 223 | "-Wextra", 224 | "-Werror", 225 | "-fvisibility=hidden", // Avoid symbol clashes 226 | }; 227 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .duckdb_quack, 3 | .version = "0.0.0", // unused 4 | .fingerprint = 0xcbb43e7e9a2d5711, 5 | .minimum_zig_version = "0.14.0", 6 | .dependencies = .{ 7 | .libduckdb_1_1_3 = .{ 8 | .url = "https://github.com/duckdb/duckdb/releases/download/v1.1.3/libduckdb-src.zip", 9 | .hash = "N-V-__8AAHDTTgGBVUssp-p_sCRi3afsQcOcpC5rjmY7Njbu", 10 | }, 11 | .libduckdb_1_2_2 = .{ 12 | .url = "https://github.com/duckdb/duckdb/releases/download/v1.2.2/libduckdb-src.zip", 13 | .hash = "N-V-__8AAIQgbgH4rGGT9XHi32ZCAebK5JlqHFsFywouo3RU", 14 | }, 15 | .extension_ci_tools = .{ 16 | .url = "https://github.com/duckdb/extension-ci-tools/archive/v1.2.0.zip", 17 | .hash = "N-V-__8AAH1NAQB5lKS2XDrbSFDS8owjice1pR1UGZQP0Yn7", 18 | }, 19 | // TODO: Switch to Python package once it's available 20 | .sqllogictest = .{ 21 | .url = "https://github.com/duckdb/duckdb-sqllogictest-python/archive/4db6a82.zip", 22 | .hash = "N-V-__8AACGWAQAeZ-w0omkwYvd00zlnhs3220wqgo91ByN2", 23 | }, 24 | }, 25 | .paths = .{""}, 26 | } 27 | -------------------------------------------------------------------------------- /src/quack_extension.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include // memcpy 3 | 4 | DUCKDB_EXTENSION_EXTERN 5 | 6 | // Workaround for missing struct tag in DUCKDB_EXTENSION_ENTRYPOINT (DuckDB 1.1.x) 7 | typedef struct duckdb_extension_access duckdb_extension_access; 8 | 9 | #if DUCKDB_EXTENSION_API_VERSION_MAJOR >= 1 10 | #define EXTENSION_RETURN(result) return (result) 11 | #else 12 | #define EXTENSION_RETURN(result) return 13 | #endif 14 | 15 | #define QUACK_PREFIX "Quack " 16 | #define QUACK_SUFFIX " 🐥" 17 | 18 | static void quack_function(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output) { 19 | const size_t prefix_len = strlen(QUACK_PREFIX); 20 | const size_t suffix_len = strlen(QUACK_SUFFIX); 21 | 22 | duckdb_vector input_vector = duckdb_data_chunk_get_vector(input, 0); 23 | duckdb_string_t *input_data = (duckdb_string_t *)duckdb_vector_get_data(input_vector); 24 | uint64_t *input_mask = duckdb_vector_get_validity(input_vector); 25 | 26 | duckdb_vector_ensure_validity_writable(output); 27 | uint64_t *result_mask = duckdb_vector_get_validity(output); 28 | 29 | idx_t num_rows = duckdb_data_chunk_get_size(input); 30 | for (idx_t row = 0; row < num_rows; row++) { 31 | if (!duckdb_validity_row_is_valid(input_mask, row)) { 32 | // name is NULL -> set result to NULL 33 | duckdb_validity_set_row_invalid(result_mask, row); 34 | continue; 35 | } 36 | 37 | duckdb_string_t name = input_data[row]; 38 | const char *name_str = duckdb_string_t_data(&name); 39 | size_t name_len = duckdb_string_t_length(name); 40 | 41 | size_t res_len = prefix_len + name_len + suffix_len; 42 | char *res = duckdb_malloc(res_len); 43 | if (res == NULL) { 44 | duckdb_scalar_function_set_error(info, "Failed to allocate memory for result"); 45 | return; 46 | } 47 | 48 | memcpy(res, QUACK_PREFIX, prefix_len); 49 | memcpy(res + prefix_len, name_str, name_len); 50 | memcpy(res + prefix_len + name_len, QUACK_SUFFIX, suffix_len); 51 | 52 | duckdb_vector_assign_string_element_len(output, row, res, res_len); 53 | duckdb_free(res); 54 | } 55 | } 56 | 57 | DUCKDB_EXTENSION_ENTRYPOINT(duckdb_connection conn, duckdb_extension_info info, duckdb_extension_access *access) { 58 | duckdb_scalar_function func = duckdb_create_scalar_function(); 59 | duckdb_scalar_function_set_name(func, "quack"); 60 | 61 | duckdb_logical_type typ = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR); 62 | duckdb_scalar_function_add_parameter(func, typ); 63 | duckdb_scalar_function_set_return_type(func, typ); 64 | duckdb_destroy_logical_type(&typ); 65 | 66 | duckdb_scalar_function_set_function(func, quack_function); 67 | 68 | if (duckdb_register_scalar_function(conn, func) == DuckDBError) { 69 | access->set_error(info, "Failed to register scalar function"); 70 | EXTENSION_RETURN(false); 71 | } 72 | 73 | duckdb_destroy_scalar_function(&func); 74 | EXTENSION_RETURN(true); 75 | } 76 | -------------------------------------------------------------------------------- /test/sql/quack.test: -------------------------------------------------------------------------------- 1 | # name: test/sql/quack.test 2 | # description: Test quack extension 3 | # group: [quack] 4 | 5 | # Before we load the extension, this will fail 6 | statement error 7 | SELECT quack('Zig') 8 | ---- 9 | Catalog Error: Scalar Function with name quack does not exist! 10 | 11 | # Require statement will ensure this test is run with this extension loaded 12 | require quack 13 | 14 | # Enable query verification 15 | statement ok 16 | PRAGMA enable_verification 17 | 18 | # Confirm the extension works 19 | query I 20 | SELECT quack('Zig') 21 | ---- 22 | Quack Zig 🐥 23 | 24 | query I 25 | SELECT quack('||| Arena is a multiplayer-focused first-person shooter released in 1999') 26 | ---- 27 | Quack ||| Arena is a multiplayer-focused first-person shooter released in 1999 🐥 28 | --------------------------------------------------------------------------------