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で管理する必要がある
- そのために進捗を
-