duckdbのextensionづくりメモ

どうやって実装し始めるのか1㍉も分からないので、参考になりそうなことを記録していくスレ
すべての開始地点はこいつをクローンすることから
c-apiを使って実装する場合は、こっちっぽい
-
extension
名の設定-
LOAD/INSTALL
コマンドのことを考えると、名前にハイフンは含めないほうがよさそう
-
python3 ./scripts/bootstrap-template.py <extension名>
- 該当バージョンのduckdbをチェックアウト(
cd duckdb && git checkout <VERSION>
)- バージョンタグをメタデータとして
extension
に埋め込む -
extension
をロードさせる際に、埋め込まれたバージョンタグを検証- 古い場合はロードできない
- バージョンタグをメタデータとして
ローカルでの確認
-
duckdb_cli -unsigned
で起動 -
LOAD
/path/to/build/release/extension/<EXTENSION_NAME>/<EXTENSION_NAME>.duckdb_extension`- デバッグビルドなら、
/path/to/build/debug/〜
- インストールとロードを一括で行ってくれる
- デバッグビルドなら、
duckdb-wasmでのローカルでの確認
署名チェックのバイパス
署名をしていない機能拡張を扱うため、署名チェックをバイパスする必要がある。
通常ならSET allow_unsigned_extensions = true
を発行する必要があるが、duckdb-wasm
の場合は接続後に変更しちゃダメと怒られる。
かわりにDuckDBConfig
を介して指定する方法が提供されている。
署名チェックをバイパス手順
-
duckdb-wasm
をいつも通りインスタンス化する -
AsyncDuckDB.open
関数にDuckDBConfig.allowUnsignedExtensions = true
で実行する
機能拡張のロード
機能拡張はhttp
でアクセスできるパスでなければならない。
パス解決のため、SET custom_extension_repository = <レポジトリのURL>
で変更する
-
extension_directory
でいけるかは未確認
viteでデバッグ起動した手元の環境(duckdb-wasm 1.29.0
)では、以下のパスからのロードを求められた。
http://localhost:5173/v1.1.1/wasm_eh/<EXTENSION_NAME>.duckdb_extension.wasm
なので、public/v1.1.1/wasm_eh/<EXTENSION_NAME>.duckdb_extension.wasm
に機能拡張を配置した。
加えて他の機能拡張に依存する場合は、そっちを先にロードしておかないと、シンボルエラーで死ぬ。
Error: IO Error: Extension "http://localhost:5173/v1.1.1/wasm_eh/extract_ast.duckdb_extension.wasm" could not be loaded: Could not load dynamic lib: extract_ast
Error: bad export type for '_ZTVN6duckdb14JsonSerializerE': undefined
実行例
const db: AsyncDuckDB = ...
await db.open({allowUnsignedExtensions: true})
const conn = await db.connect()
// json機能拡張に依存している場合
await conn.query(`LOAD json`)
// レポジトリの変更
await conn.query(`SET custom_extension_repository = '${import.meta.resolve!('/').slice(0, -1)}'`)
// 自身の機能拡張の有効化
await conn.query(`INSTALL some_extension; LOAD some_extension`)

duckdb-wasm用のextensionをビルドするための構成例
-
duckdb-wam
のレポジトリをclone (全てのサブモジュール含めて) -
submodules/duckdb/extension
フォルダに、extension
のレポジトリをclone -
duckdb-waqsm本体のビルド
- WASM_LOADABLE_EXTENSIONS=1 GEN=ninja ./scripts/wasm_build_lib.sh relsize eh
- BUILD_TYPEは
dev
,debug
,relsize
,relperf
の4つ - PLATFORM_TYPE
- mvp: minimum viable product
- eh: exception handling
- coi: cross origin isolation
- BUILD_TYPEは
- extensionは共有ライブラリとしてビルドされる
- WASM_LOADABLE_EXTENSIONS=1 GEN=ninja ./scripts/wasm_build_lib.sh relsize eh
-
community extensionのビルド
-
emcc build/relsize/eh/third_party/duckdb/src/duckdb_ep-build/extension/${{ github.event.repository.name }}/${{ env.extension-name }}.duckdb_extension -o ../${{ env.extension-name }}.extension.wasm -sSIDE_MODULE
-
ここは今と変わってる可能性あり -
静的ライブラリをwasmに変換する呼び出し
- 現在の呼び出しの際の引数が少し変わっているので注意が必要
-
emcc $<TARGET_FILE:${TARGET_NAME}> \
-o $<TARGET_FILE:${TARGET_NAME}>.wasm \
-O3 -sSIDE_MODULE=2 \
-sEXPORTED_FUNCTIONS="${EXPORTED_FUNCTIONS}" \
${WASM_THREAD_FLAGS} \
${DUCKDB_EXTENSION_${EXTENSION_NAME_UPPERCASE}_LINKED_LIBS}
-
TARGET_NAME
は同じ -
EXPORTED_FUNCTIONS
は上記の通り -
WASM_THREAD_FLAGS
はPLATFORM_TYPE = coi
の場合に指定する-pthread -sSHARED_MEMORY=1
-
XXXX_LINKED_LIBS
は追加でリンクする静的ライブラリか?- 現状そこまで指定しているextension見つからない

C-ABIでextensionを書く
現在、C言語でもextensionが書けるようになっている。

extensionのエントリーポイント
duckdbのエンジンが、extensionから<EXTENSION名>_init
関数を呼び出してインストールする。
extern "C" {
// xxxxをextension名と一致させる
DUCKDB_EXTENSION_API void xxxx_init(duckdb::DatabaseInstance &db) {
// (snip)
}
}
あとどこで呼ばれているか分からないが、<EXTENSION名>_version
も必要っぽい
DUCKDB_EXTENSION_API const char *xxxx_version() {
// (snip)
}
↑
cpp
のABIでextension
を作成する場合、xxxx_init
と共にwasm
からエクスポートさせる
CABIの場合、xxxx_init_c_api
のみをエクスポートさせるだけで十分のため不要っぽい。(CMakeLists.txtより)
if (${ABI_TYPE} STREQUAL "CPP") set(EXPORTED_FUNCTIONS "_${NAME}_init,_${NAME}_version") elseif (${ABI_TYPE} STREQUAL "C_STRUCT") set(EXPORTED_FUNCTIONS "_${NAME}_init_c_api") endif()
DUCKDB_EXTENSION_API
は関数をシンボルとしてエクスポートさせるおまじない。
extension
のLOAD/INSTALL
はextension_helper.hpp
のExtensionHelper
クラスが担っている。
ExtensionHelper::InstallExtension
関数の中で、動的にエントリポイントを解決している。

extennsin周りをコードリーディングした人の記録

拡張ポイント
duckdb::ParserExtension
duckdb/parser/parser_extension.hpp
パースできない構文にぶつかった時に移譲される。
メンバ
- parse_function_t parse_function
- typedef ParserExtensionParseResult (*parse_function_t)(ParserExtensionInfo *info, const string &query)
- クエリを受け取りパース結果を返す関数を渡す
- plan_function_t plan_function
- typedef ParserExtensionPlanResult (*plan_function_t)(ParserExtensionInfo *info, ClientContext &context, unique_ptr<ParserExtensionParseData> parse_data)
-
extension
で表関数を提供したい場合に、その論理プランの構築を行う関数を渡す -
表関数の追加はReplacement scan
で事足りるのかもhttps://duckdb.org/docs/api/c/replacement_scans-
Replacement scan
はSELECT * FROM 'a.csv'
をSELECT * FROM rewad_csv_auto('a.csv')
として実行するような目的のもの - 新たに表関数を追加する場合に使用するものではない
- shared_ptr<ParserExtensionInfo> parser_info
-
plan_function
に渡す追加情報をここに定義する。 - 一種のユーザデータ
-
duckdb::OperatorExtension
duckdb/planner/operator_extension.hpp
- 構文木から論理プランを構築する過程で必要に応じて移譲
-
duckdb::Planner::CreatePlan
で失敗した際に移譲される- planner/planner.cpp#L62
メンバ
- bind_function_t Bind
- typedef BoundStatement (*bind_function_t)(ClientContext &context, Binder &binder, OperatorExtensionInfo *info, SQLStatement &statement);
- shared_ptr<OperatorExtensionInfo> operator_info
Bind
関数にパース結果と追加情報のinfo
を渡して、duckdb::BoundStatement
を作成させる関数を渡す。
- GetNameメソッド
- Deserializeメソッド
上記の関数が純粋仮装関数として宣言されている。
duckdb::OptimizerExtension
duckdb/optimizer/optimizer_extension.hpp
論理プランの最適化の過程で必要に応じて移譲
メンバ
-
optimize_function_t optimize_function
-
shared_ptr<OptimizerExtensionInfo> optimizer_info
-
最適化は、
PreparedStatement
を作成する際、構築された論理プランの後処理として行われる。- client_context.cpp#L354
-
optimize_function
に論理プランと追加情報のoptimizer_info
を渡して、論理プランを改変する関数を渡す- 戻り値はなく、論理プランをダイレクトに改変する
duckdb::StorageExtension
duckdb/storage/storage_extension.hpp
外部データベースのアタッチとかに関与してるっぽい。
メンバ
- attach_function_t attach
- typedef unique_ptr<Catalog> (*attach_function_t)(StorageExtensionInfo *storage_info, ClientContext &context, AttachedDatabase &db, const string &name, AttachInfo &info, AccessMode access_mode)
- create_transaction_manager_t create_transaction_manager;
- typedef unique_ptr<TransactionManager> (*create_transaction_manager_t)(StorageExtensionInfo *storage_info, AttachedDatabase &db, Catalog &catalog)
- shared_ptr<StorageExtensionInfo> storage_info;
attached_database.cpp
のAttachedDatabase::AttachedDatabase
メソッドから呼ばれる。
まずattach
を呼び、次いでcreate_transaction_manager
追加情報のstorage_info
などを渡してトランザクションマネージャを構築する。
拡張ポイントの追加
duckdb::DBConfig
に以下のメンバフィールドが用意されている。
- vector<ParserExtension> parser_extensions
- vector<OptimizerExtension> optimizer_extensions
- vector<unique_ptr<OperatorExtension>> operator_extensions
- case_insensitive_map_t<duckdb::unique_ptr<StorageExtension>> storage_extensions
データベースの初期化時に、突っ込むときめ細やかな制御が可能になるのかな?
スカラ関数の追加
論理プラン構築の際、関数名と引数の型で、関数カタログを引く。
そのため、カタログに追加しておけば、多分事足りる。

duckcb-wasm
用のextension
はduckdb/extension-ci-tools
見るのがよさそう。
以下の入力を指定した上で
# Inputs
EXT_NAME : Upper case string describing the name of the out-of-tree extension
EXT_CONFIG : Path to the extension config file specifying how to build the extension
EXT_FLAGS : Extra CMake flags to pass to the build
EXT_RELEASE_FLAGS : Extra CMake flags to pass to the release build
EXT_DEBUG_FLAGS : Extra CMake flags to pass to the debug build
SKIP_TESTS : Replaces all test targets with a NOP step
make <PLATFORM>
でビルドしてるように見える。
例えばwasm-eh
でビルドするのなら
make wasm_eh
-
EXT_NAME
とEXT_CONFIG
はextention-template
で指定済み - 他のオプションは未使用
なのでオプションの指定は不要そう。

サンプルのビルド
extension-template
として用意されたサンプルのビルドの注意点
- cmakeはversion 3.30以降が必要
homebrew
でemscripten
をインストールした場合、パスが解決できずエラーになる。
以下の環境変数を設定すると先に進む。
- export EMSDK=$(brew -prefix emscripten)
- export EMSCRIPTEN_ROOT=$EMSDK/libexec
extension-ci-tools/makefiles/duckdb_extension.MakefileのVCPKG_EMSDK_FLAGSのパスがhomebrew
でインストールしたemscripten
とは合致しない。
以下のように変更した。
# 変更前
# VCPKG_EMSDK_FLAGS=-DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$(EMSDK)/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake
# 変更後
VCPKG_EMSDK_FLAGS=-DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$(EMSDK)/libexec/cmake/Modules/Platform/Emscripten.cmake
サンプルはOpenSSL
を使用しているため、README
に従ってvcpkg
を導入し、
vcpkg add port openssl
でプロジェクトに追加した。
明示的にopenssl
を追加する必要があるかどうかはよくわかってない。
vcpkg.json
への追加は必須。install
までは不要。

ビルドしたextensionの確認
README
にも書かれているように、release/duckdb
にextension
が組み込み済みでビルドされる。
README
ではmake
でのビルドを指示しているが、全ターゲットのビルドでクッソ時間がかかるのでmake release
だけにしておくのが得策だと思う。
-
bootstrap-template.py
でextension名
変更すると、関数名も合わせて変更される。-
select quack('Jane')
が動かなくて焦った。
-

Extension type

表関数の追加
TableFunction
duckdb::TableFunction
型をインスタンス化し、duckdb::ExtractAstFunction
でラップして、カタログに追加する流れ。
TableFunction
のコンストラクタは以下の引数を受け取る。
- name (必須)
- 関数名
- arguments (必須)
- 引数の型
- function (必須)
- 実際に実行する関数を渡す。
- 引数の型によるオーバーロードを実現したい場合は、
nullptr
を渡し、AddFunction
メソッドで後で追加する。
- bind (オプショナル)
- 構文木の要素から論理プランを作成する際に呼ばれる関数を渡す。
論理プランを作ることがPreparedStatement
の最大の目的なので、nullptr
を渡すケースってなんなんだろう?
-
return_types
引数に戻り値の各列の型を追記して型解決 -
names
引数に戻り値の列名を追記 - 引数などの文脈的な情報は、
duckdb::TableFunctionData
のサブクラスを用意し値を詰めて返す
- 構文木の要素から論理プランを作成する際に呼ばれる関数を渡す。
- init_global (オプショナル)
- 関数を実行前の初期化フェーズで呼ばれる関数を渡す。
- マルチスレッドで関数を実行する場合、スレッド間で共有すべき情報をここで初期化する
- オプショナル引数とか
- 共有すべき情報が特になければ
nullptr
を渡す。
- init_local (オプショナル)
- 関数を実行前の初期化フェーズで呼ばれる関数を渡す。
- 各スレッドごとの状態はここで初期化する。
- シングルスレッドで回すのであれば、
nullptr
を渡す。
TableFunctionData
duckdb::FunctionData
のサブクラスで、実行する関数のバインド情報を保持する目的の型。
この型のライフサイクルはPreparedStatement
のライフサイクルと一致する。
GlobalTableFunctionState
実行器(duckdb::Executor
)の最初で初期化され、実行終了後破棄される。
Scanner
function
引数として渡した関数が呼び出される際に、入力として
- duckdb::FunctionData
- duckdb::GlobalTableFunctionState
- duckdb::LocalTableFunctionState
が渡される。
引数は、このいずれかで保持させておかなければならない(たいていFunctionData
)。
Scan
結果はoutput
引数に突っ込む。
合わせて、output.Setcardinarity
メソッドも呼んで、投入件数を更新する。
- これしないと、0件扱いにされる。

表関数まわりの実行器
-
TableFunction::function
にコールバックが割り当てられている場合、第一に選択される。 -
TableFunction::function
未割り当てで、TableFunction::in_out_function
にコールバックが割り当てられている場合、そっちが選択される- 加えて、
TableFunction::in_out_function_final
も割り当てられている場合-
TableFunction::in_out_function_final
を実行 -
TableFunction::in_out_function
を実行 - 1, 2で件数0なら、再度
TableFunction::in_out_function_final
を実行
-
- 加えて、
-
Scanner
関数はデータが消化し切ったと認識されるまで何度も呼ばれる-
output
引数が0件のまま呼び出しを抜ければ、終了と判断してくれる- そのために進捗を
GlobalTableFunctionState
で管理する必要がある
- そのために進捗を
-