├── .gitignore ├── test └── sql │ └── quack.test ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── src └── quack_extension.c └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-cache 3 | zig-out 4 | 5 | repo 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | - name: Install Zig 18 | run: | 19 | ZIG_VERSION=$(awk -F\" '/minimum_zig_version/ {print $2}' $GITHUB_WORKSPACE/build.zig.zon) 20 | echo "Using Zig version $ZIG_VERSION" 21 | mkdir -p $HOME/.zig 22 | curl -fSs "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz" | tar -xJ -C $HOME/.zig 23 | echo "$HOME/.zig/zig-x86_64-linux-${ZIG_VERSION}" >> $GITHUB_PATH 24 | echo "ZIG_LOCAL_CACHE_DIR=$HOME/.cache/zig" >> $GITHUB_ENV 25 | - uses: astral-sh/setup-uv@v5 26 | with: 27 | enable-cache: false 28 | - run: zig build -Dinstall-headers --verbose --summary new 29 | - run: zig build test --summary new 30 | - run: tree -ash zig-out 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/quack_extension.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include // memcpy 3 | 4 | DUCKDB_EXTENSION_EXTERN 5 | 6 | #define QUACK_PREFIX "Quack " 7 | #define QUACK_SUFFIX " 🐥" 8 | 9 | static void quack_function(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output) { 10 | const size_t prefix_len = strlen(QUACK_PREFIX); 11 | const size_t suffix_len = strlen(QUACK_SUFFIX); 12 | 13 | duckdb_vector input_vector = duckdb_data_chunk_get_vector(input, 0); 14 | duckdb_string_t *input_data = (duckdb_string_t *)duckdb_vector_get_data(input_vector); 15 | uint64_t *input_mask = duckdb_vector_get_validity(input_vector); 16 | 17 | duckdb_vector_ensure_validity_writable(output); 18 | uint64_t *result_mask = duckdb_vector_get_validity(output); 19 | 20 | idx_t num_rows = duckdb_data_chunk_get_size(input); 21 | for (idx_t row = 0; row < num_rows; row++) { 22 | if (!duckdb_validity_row_is_valid(input_mask, row)) { 23 | // name is NULL -> set result to NULL 24 | duckdb_validity_set_row_invalid(result_mask, row); 25 | continue; 26 | } 27 | 28 | duckdb_string_t name = input_data[row]; 29 | const char *name_str = duckdb_string_t_data(&name); 30 | size_t name_len = duckdb_string_t_length(name); 31 | 32 | size_t res_len = prefix_len + name_len + suffix_len; 33 | char *res = duckdb_malloc(res_len); 34 | if (res == NULL) { 35 | duckdb_scalar_function_set_error(info, "Failed to allocate memory for result"); 36 | return; 37 | } 38 | 39 | memcpy(res, QUACK_PREFIX, prefix_len); 40 | memcpy(res + prefix_len, name_str, name_len); 41 | memcpy(res + prefix_len + name_len, QUACK_SUFFIX, suffix_len); 42 | 43 | duckdb_vector_assign_string_element_len(output, row, res, res_len); 44 | duckdb_free(res); 45 | } 46 | } 47 | 48 | DUCKDB_EXTENSION_ENTRYPOINT(duckdb_connection conn, duckdb_extension_info info, struct duckdb_extension_access *access) { 49 | duckdb_scalar_function func = duckdb_create_scalar_function(); 50 | duckdb_scalar_function_set_name(func, "quack"); 51 | 52 | duckdb_logical_type typ = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR); 53 | duckdb_scalar_function_add_parameter(func, typ); 54 | duckdb_scalar_function_set_return_type(func, typ); 55 | duckdb_destroy_logical_type(&typ); 56 | 57 | duckdb_scalar_function_set_function(func, quack_function); 58 | 59 | if (duckdb_register_scalar_function(conn, func) == DuckDBError) { 60 | access->set_error(info, "Failed to register scalar function"); 61 | return false; 62 | } 63 | 64 | duckdb_destroy_scalar_function(&func); 65 | return true; 66 | } 67 | -------------------------------------------------------------------------------- /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.3.2 -Dduckdb-version=1.4.1 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.4.1 -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.3.0 42 | │ ├── linux_amd64 43 | │ │ └── quack.duckdb_extension 44 | │ ├── linux_arm64 45 | │ │ └── quack.duckdb_extension 46 | │ ├── osx_amd64 47 | │ │ └── quack.duckdb_extension 48 | │ ├── osx_arm64 49 | │ │ └── quack.duckdb_extension 50 | │ ├── windows_amd64 51 | │ │ └── quack.duckdb_extension 52 | │ └── windows_arm64 53 | │ └── quack.duckdb_extension 54 | ├── v1.3.1 55 | │ ├── linux_amd64 56 | │ │ └── quack.duckdb_extension 57 | │ ├── linux_arm64 58 | │ │ └── quack.duckdb_extension 59 | │ ├── osx_amd64 60 | │ │ └── quack.duckdb_extension 61 | │ ├── osx_arm64 62 | │ │ └── quack.duckdb_extension 63 | │ ├── windows_amd64 64 | │ │ └── quack.duckdb_extension 65 | │ └── windows_arm64 66 | │ └── quack.duckdb_extension 67 | ├── ... 68 | ``` 69 | 70 | See `zig build --help` for more options. 71 | 72 | ## Testing 73 | 74 | Run [SQL logic tests](https://duckdb.org/docs/dev/sqllogictest/intro.html) with `zig build test`. 75 | 76 | ``` 77 | ❯ zig build test --summary new 78 | [1/1] test/sql/quack.test 79 | SUCCESS 80 | [1/1] test/sql/quack.test 81 | SUCCESS 82 | [1/1] test/sql/quack.test 83 | SUCCESS 84 | [1/1] test/sql/quack.test 85 | SUCCESS 86 | [1/1] test/sql/quack.test 87 | SUCCESS 88 | 89 | Build Summary: 16/16 steps succeeded 90 | test success 91 | ├─ sqllogictest v1.3.0 osx_arm64 success 106ms MaxRSS:48M 92 | ├─ sqllogictest v1.3.1 osx_arm64 success 108ms MaxRSS:49M 93 | ├─ sqllogictest v1.3.2 osx_arm64 success 106ms MaxRSS:48M 94 | ├─ sqllogictest v1.4.0 osx_arm64 success 120ms MaxRSS:50M 95 | └─ sqllogictest v1.4.1 osx_arm64 success 130ms MaxRSS:50M 96 | ``` 97 | 98 | You can also pass `-Dduckdb-version` to test against a specific DuckDB version, or use `-Dplatform` to select a different platform. 99 | 100 | ## Using the Extension 101 | 102 | ``` 103 | ❯ duckdb -unsigned 104 | DuckDB v1.4.0 (Andium) b8a06e4a22 105 | Enter ".help" for usage hints. 106 | 🟡◗ LOAD 'zig-out/v1.4.1/osx_arm64/quack.duckdb_extension'; 107 | 🟡◗ SELECT quack('Zig'); 108 | ┌──────────────┐ 109 | │ quack('Zig') │ 110 | │ varchar │ 111 | ├──────────────┤ 112 | │ Quack Zig 🐥 │ 113 | └──────────────┘ 114 | ``` 115 | 116 | ## Creating an Extension Repository 117 | 118 | 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: 119 | 120 | ```shell 121 | rm -rf repo 122 | 123 | zig build --prefix repo --release=fast 124 | 125 | gzip repo/*/*/*.duckdb_extension 126 | ``` 127 | 128 | This will generate a repository that is ready to be uploaded to S3/R2/etc. with a tool like [rclone](https://rclone.org). 129 | 130 | ## License 131 | 132 | Licensed under the [MIT License](LICENSE). 133 | 134 | Feel free to use this code as a starting point for your own DuckDB extensions. 🐤 135 | --------------------------------------------------------------------------------