| name | DuckDB-Extensions |
| description | Building DuckDB loadable extensions with CMake, FetchContent, and proper metadata handling |
DuckDB 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
append_metadata.cmakescript. Without this, DuckDB will reject the extension with "not a DuckDB extension" error.Static Linking: For portable extensions, link against
duckdb_staticwith visibility hidden and exclude-libs flags.Table Function State: In DuckDB 1.1+,
bind_datais const during execution. UseGlobalTableFunctionStatefor mutable state accessed viainput.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:
FetchContent_MakeAvailable(duckdb)providesduckdb_SOURCE_DIRandduckdb_BINARY_DIR- The
duckdb_platformtarget generatesduckdb_platform_outfile needed for metadata - Version string MUST include the
vprefix (e.g.,v1.2.1not1.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
extern "C"block must match extension name:{name}_initand{name}_version DUCKDB_EXTENSION_APImacro ensures proper symbol visibility
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
mainormaster - Version string must include
vprefix to match DuckDB's format - Test with the DuckDB binary from the same build before system DuckDB
- Use
GlobalTableFunctionStatefor any mutable state during table function execution - Set
POSITION_INDEPENDENT_CODE ONfor all static libraries linked into the extension - Install extensions to
~/.duckdb/extensions/v{version}/{platform}/for auto-discovery - In
.duckdbrc, alwaysLOADbeforeSETfor extension settings