Claude-skill-registry DuckDB-Extensions
Building DuckDB loadable extensions with CMake, FetchContent, and proper metadata handling
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/duckdb-extensions" ~/.claude/skills/majiayu000-claude-skill-registry-duckdb-extensions && rm -rf "$T"
skills/data/duckdb-extensions/SKILL.mdDuckDB Extensions
This skill covers building DuckDB loadable extensions (
.duckdb_extension files) using CMake and FetchContent. Extensions must include proper metadata to be loaded by DuckDB.
Instructions
When helping users build DuckDB extensions, follow these guidelines:
-
Version Matching: The extension MUST be built against the exact same DuckDB version that will load it. Use FetchContent with a specific git tag (e.g.,
).v1.2.1 -
Metadata is Required: DuckDB extensions need metadata appended via the
script. Without this, DuckDB will reject the extension with "not a DuckDB extension" error.append_metadata.cmake -
Static Linking: For portable extensions, link against
with visibility hidden and exclude-libs flags.duckdb_static -
Table Function State: In DuckDB 1.1+,
is const during execution. Usebind_data
for mutable state accessed viaGlobalTableFunctionState
.input.global_state -
PIC Required: Static libraries linked into the shared extension must be built with
.POSITION_INDEPENDENT_CODE ON
Examples
Example 1: Platform Mismatch Error
User: I get "built for platform 'linux_amd64', but we can only load extensions built for platform 'linux_amd64_gcc4'" Claude: This means your system DuckDB was built with the gcc4 ABI. Options: 1. Use the DuckDB binary from your extension build (in `_deps/duckdb-build/duckdb`) 2. Or match your build platform to the system DuckDB The built-from-source DuckDB will be at: `build/release/_deps/duckdb-build/duckdb`
Example 2: "Not a DuckDB extension" Error
User: DuckDB says "The file is not a DuckDB extension. The metadata at the end of the file is invalid" Claude: The extension is missing required metadata. Add this POST_BUILD command: add_custom_command( TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -DABI_TYPE=CPP -DEXTENSION=$<TARGET_FILE:${TARGET_NAME}> -DPLATFORM_FILE=${duckdb_BINARY_DIR}/duckdb_platform_out -DVERSION_FIELD=v1.2.1 -DEXTENSION_VERSION=v1.2.1 -DNULL_FILE=${duckdb_SOURCE_DIR}/scripts/null.txt -P ${duckdb_SOURCE_DIR}/scripts/append_metadata.cmake ) add_dependencies(${TARGET_NAME} duckdb_platform)
Example 3: Const bind_data Error
User: I get "increment of member in read-only object" when trying to modify bind_data Claude: In newer DuckDB, bind_data is const during execution. Move mutable state to GlobalTableFunctionState: struct MyBindData : public TableFunctionData { // Immutable data set during bind std::vector<std::string> items; }; struct MyState : public GlobalTableFunctionState { // Mutable state for execution idx_t offset = 0; }; Then register an init_global function: func.init_global = [](ClientContext &ctx, TableFunctionInitInput &input) { return make_uniq<MyState>(); }; Access both in execute: auto &bind = input.bind_data->Cast<MyBindData>(); auto &state = input.global_state->Cast<MyState>();
Reference Implementation Details
CMakeLists.txt Template
Purpose: Complete CMake setup for a DuckDB extension with FetchContent
cmake_minimum_required(VERSION 3.21) set(TARGET_NAME myextension) set(EXTENSION_NAME ${TARGET_NAME}_extension) set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension) project(${TARGET_NAME}) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) # Extension sources set(EXTENSION_SOURCES src/myextension.cpp src/table_functions.cpp ) # Fetch DuckDB - use exact version tag FetchContent_Declare( duckdb GIT_REPOSITORY https://github.com/duckdb/duckdb.git GIT_TAG v1.2.1 ) FetchContent_MakeAvailable(duckdb) include_directories(${duckdb_SOURCE_DIR}/src/include) # Build loadable extension add_library(${LOADABLE_EXTENSION_NAME} SHARED ${EXTENSION_SOURCES}) # Required for static linking set_target_properties(${LOADABLE_EXTENSION_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(${LOADABLE_EXTENSION_NAME} PRIVATE duckdb_static -Wl,--gc-sections -Wl,--exclude-libs,ALL ) target_include_directories(${LOADABLE_EXTENSION_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/src/include ) target_compile_definitions(${LOADABLE_EXTENSION_NAME} PUBLIC -DDUCKDB_BUILD_LOADABLE_EXTENSION) set_target_properties(${LOADABLE_EXTENSION_NAME} PROPERTIES OUTPUT_NAME ${TARGET_NAME} PREFIX "" SUFFIX ".duckdb_extension" ) # Version must match FetchContent tag set(DUCKDB_VERSION_NORMALIZED "v1.2.1") set(NULL_FILE ${duckdb_SOURCE_DIR}/scripts/null.txt) # Add metadata (REQUIRED for DuckDB to load the extension) add_custom_command( TARGET ${LOADABLE_EXTENSION_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -DABI_TYPE=CPP -DEXTENSION=$<TARGET_FILE:${LOADABLE_EXTENSION_NAME}> -DPLATFORM_FILE=${duckdb_BINARY_DIR}/duckdb_platform_out -DVERSION_FIELD=${DUCKDB_VERSION_NORMALIZED} -DEXTENSION_VERSION=${DUCKDB_VERSION_NORMALIZED} -DNULL_FILE=${NULL_FILE} -P ${duckdb_SOURCE_DIR}/scripts/append_metadata.cmake ) add_dependencies(${LOADABLE_EXTENSION_NAME} duckdb_platform)
Key Points:
providesFetchContent_MakeAvailable(duckdb)
andduckdb_SOURCE_DIRduckdb_BINARY_DIR- The
target generatesduckdb_platform
file needed for metadataduckdb_platform_out - Version string MUST include the
prefix (e.g.,v
notv1.2.1
)1.2.1
Table Function Pattern (DuckDB 1.1+)
Purpose: Correct pattern for table functions with mutable execution state
#include "duckdb.hpp" #include "duckdb/function/table_function.hpp" #include "duckdb/main/extension_util.hpp" namespace duckdb { // Immutable bind data struct MyTableBindData : public TableFunctionData { std::vector<std::string> items; }; // Mutable execution state struct MyTableState : public GlobalTableFunctionState { idx_t offset = 0; }; static unique_ptr<FunctionData> MyTableBind( ClientContext &context, TableFunctionBindInput &input, vector<LogicalType> &return_types, vector<string> &names) { return_types.push_back(LogicalType::VARCHAR); names.push_back("item"); auto result = make_uniq<MyTableBindData>(); result->items = {"one", "two", "three"}; return std::move(result); } static unique_ptr<GlobalTableFunctionState> MyTableInitGlobal( ClientContext &context, TableFunctionInitInput &input) { return make_uniq<MyTableState>(); } static void MyTableExecute( ClientContext &context, TableFunctionInput &input, DataChunk &output) { auto &bind_data = input.bind_data->Cast<MyTableBindData>(); auto &state = input.global_state->Cast<MyTableState>(); idx_t count = 0; while (state.offset < bind_data.items.size() && count < STANDARD_VECTOR_SIZE) { output.SetValue(0, count, Value(bind_data.items[state.offset])); state.offset++; count++; } output.SetCardinality(count); } void RegisterMyTableFunction(DatabaseInstance &db) { TableFunction func("my_table", {}, MyTableExecute, MyTableBind); func.init_global = MyTableInitGlobal; ExtensionUtil::RegisterFunction(db, func); } } // namespace duckdb
Extension Entry Point
Purpose: Required entry point functions for DuckDB to initialize the extension
#define DUCKDB_EXTENSION_MAIN #include "duckdb.hpp" #include "duckdb/main/extension_util.hpp" namespace duckdb { void RegisterMyTableFunction(DatabaseInstance &db); static void LoadInternal(DatabaseInstance &instance) { RegisterMyTableFunction(instance); } void MyextensionExtensionLoad(DuckDB &db) { LoadInternal(*db.instance); } std::string MyextensionExtensionVersion() { return "v1.0.0"; } } // namespace duckdb extern "C" { DUCKDB_EXTENSION_API void myextension_init(duckdb::DatabaseInstance &db) { duckdb::LoadInternal(db); } DUCKDB_EXTENSION_API const char *myextension_version() { return duckdb::MyextensionExtensionVersion().c_str(); } }
Key Points:
- Function names in
block must match extension name:extern "C"
and{name}_init{name}_version
macro ensures proper symbol visibilityDUCKDB_EXTENSION_API
Loading Extensions
-- Load unsigned extension (development) LOAD '/path/to/myextension.duckdb_extension'; -- Or start DuckDB with -unsigned flag -- duckdb -unsigned
Troubleshooting
Version Mismatch Error
Error: "built specifically for DuckDB version 'X' and can only be loaded with that version"
Cause: Extension metadata version doesn't match running DuckDB
Solution: Ensure
DUCKDB_VERSION_NORMALIZED in CMake matches the DuckDB tag AND includes the v prefix
PIC/Relocation Error
Error: "relocation R_X86_64_PC32 against symbol... recompile with -fPIC"
Cause: Static library being linked into shared library without PIC
Solution: Add to any static library targets:
set_target_properties(mylib PROPERTIES POSITION_INDEPENDENT_CODE ON)
Missing duckdb_platform Dependency
Error: "duckdb_platform_out" file not found during metadata append
Solution: Ensure
add_dependencies(${TARGET_NAME} duckdb_platform) is present
Deployment & Configuration
Extension Installation Path
DuckDB looks for extensions in:
~/.duckdb/extensions/v{version}/{platform}/
For example:
~/.duckdb/extensions/v1.2.1/linux_amd64/myextension.duckdb_extension
Copy your built extension there for automatic discovery:
cp build/release/myextension.duckdb_extension ~/.duckdb/extensions/v1.2.1/linux_amd64/
Auto-Loading with ~/.duckdbrc
Create
~/.duckdbrc to automatically load extensions and configure settings on startup:
-- Load extension (by name if installed in extensions path) LOAD 'myextension'; -- Custom settings MUST come AFTER the LOAD SET myextension_host = 'server.example.com'; SET myextension_port = 50051;
Important: Settings registered by an extension are only available AFTER the extension loads. Put
LOAD first, then SET.
Wrapper Script for Unsigned Extensions
During development, unsigned extensions require the
-unsigned flag. Create a wrapper script to avoid forgetting:
#!/bin/bash # ~/.local/bin/duckdb - wrapper for unsigned extension support exec /path/to/actual/duckdb -unsigned "$@"
Make it executable and ensure
~/.local/bin is in your PATH before the actual duckdb location.
Registering Custom Settings
Extensions can register custom settings that users can configure:
#include "duckdb/main/config.hpp" static void LoadInternal(DatabaseInstance &instance) { auto &config = DBConfig::GetConfig(instance); // Register string setting with default config.AddExtensionOption("myextension_host", "Server hostname", LogicalType::VARCHAR, Value("localhost")); // Register integer setting config.AddExtensionOption("myextension_port", "Server port", LogicalType::INTEGER, Value(50051)); // Register functions... }
Access settings in your code:
Value host_value, port_value; if (context.TryGetCurrentSetting("myextension_host", host_value)) { std::string host = host_value.GetValue<std::string>(); }
Best Practices Summary
- Always use exact version tags with FetchContent, never
ormainmaster - Version string must include
prefix to match DuckDB's formatv - Test with the DuckDB binary from the same build before system DuckDB
- Use
for any mutable state during table function executionGlobalTableFunctionState - Set
for all static libraries linked into the extensionPOSITION_INDEPENDENT_CODE ON - Install extensions to
for auto-discovery~/.duckdb/extensions/v{version}/{platform}/ - In
, always.duckdbrc
beforeLOAD
for extension settingsSET