DuckDBのC++ClientAPI
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
DuckDBのC++ Client APIを調査していく。
基本的な使用方法は、公式サイトで
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
インメモリDB作成
you must first initialize a DuckDB instance using its constructor. DuckDB() takes as parameter the database file to read and write from. The special value nullptr can be used to create an in-memory database.
https://duckdb.org/docs/api/cpp#startup--shutdown
duckdb/duckdb.hpp
をインクルードした上で、 duckdb.DuckDBコンストラクタにstd::nullptr_t型の定数であるnullptr
を渡すことで、インメモリDBを作成できる。
auto db = duckDB(nullptr);
The DuckDB constructor may throw exceptions, for example if the database file is not usable.
コンストラクタは例外を吐くようなので、try-catchはしっかりと。
duckdb.DuckDB
クラスの実体は、duckdb/database.hpp
で宣言されている。
実装はduckdb/main/database.cpp
。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
DB接続
duckdb.DuckDB
インスタンスを、duckdb.Connection
クラスのコンストラクタに渡すことで接続できる。
auto conn = duckdb.Connection(db);
以下マルチスレッドで扱うための公式サイトの説明
you can create one or many Connection instances using the Connection() constructor. While connections should be thread-safe, they will be locked during querying. It is therefore recommended that each thread uses its own connection if you are in a multithreaded environment.
メインスレッドで1つのduckdb.DuckDB
インスタンスを作成。
各スレッドにduckdb.DuckDB
インスタンスを渡して、duckdb.Connection
インスタンスを作成すれば良さそう。
duckdb.Connection
クラスの実体は、duckdb/connection.hpp
で宣言されている。
実装は、duckdb/main/connection.cpp
。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
問い合わせAPI
Query
duckdb.Connection.Query
は、3つのオーバーロードが提供されている。
DUCKDB_API unique_ptr<MaterializedQueryResult> Query(const string &query);
//! Issues a query to the database and materializes the result (if necessary). Always returns a
//! MaterializedQueryResult.
DUCKDB_API unique_ptr<MaterializedQueryResult> Query(unique_ptr<SQLStatement> statement);
// prepared statements
template <typename... ARGS>
unique_ptr<QueryResult> Query(const string &query, ARGS... args) {
vector<Value> values;
return QueryParamsRecursive(query, values, args...);
}
- 一つ目は、SQL文字列を渡す一番シンプルなケース
- 二つ目は、
duckdb.Paser
クラスによってパースされた結果を渡すケース-
duckdb.Paser
クラスによってパースされた結果は、duckdb.SQLStatement
のコレクションとして返される。 - これを一つずつ回す用途と思われる
-
Connection
クラスのExtractStatements
にSQL文字列を渡すことでも同様にduckdb.SQLStatement
のコレクションを取得することができるっぽい。
-
- 三つ目はSQL文字列+プレースホルダのパラメータを渡すケース
一つ目と二つ目の戻り値の型はduckdb.MaterializedQueryResult
。
実体はduckdb/main/materialized_query_result.hpp
で宣言されている。
実装は、duckdb/main/materialized_query_result.cpp
。
Fetch
メソッドのコメントを読むに、
Fetches a DataChunk from the query result.
This will consume the result (i.e. the result can only be scanned once with this function)
1回のFetch
コールですべての結果を収集する模様。だからMaterializeとついているのだろう。
三つ目の戻り値の型はduckdb.QueryResult
。
実体はduckdb/main/query_result.hpp
で宣言されている。
実装は、duckdb/main/query_result.cpp
。
Fetch
メソッドのコメントを読むに、
Fetches a DataChunk of normalized (flat) vectors from the query result.
Returns nullptr if there are no more results to fetch.
ストリーミングに結果を収集する模様。
メソッド名は異なるが、sendQuery
はSQL文字列のみを渡してduckdb.QueryResult
を返す同一のパターン。
Queryが送出する例外
(把握している範囲のみ)
- duckdb::ParserException
-
SQL
のパースに失敗した際に送出される例外
-
Prepare
SQL文字列を渡すケースと、duckdb.SQLStatement
をわたす2つのオーバーロードが提供されている。
その名の通りduckdb.PreparedStatement
を返すメソッド。
duckdb.PreparedStatement
の実体は、duckdb/prepared_statement.hpp
で宣言されている。
実装は、duckdb/main/prepared_statement.cpp
。
duckdb.PreparedStatement
のExecute
メソッドにプレースホルダのパラメータを渡すことでduckdb.QueryResult
を返す。
duckdb.PreparedStatement
のExecute
メソッドは3つのオーバーロードが提供されている。
- 一つ目は、位置パラメータのコレクションを
std::vecror
を派生したduckdb::vector
に詰めて渡すケース。2番目の引数でduckdb.MaterializedQueryResult
かduckdb.QueryResult
かをいずれかの振る舞いを返すかを選択できそう。 - 二つ目は、名前付きパラメータのコレクションを
duckdb::case_insensitive_map_t
に詰めて渡すケース。2番目の引数でduckdb.MaterializedQueryResult
かduckdb.QueryResult
かをいずれかの振る舞いを返すかを選択できそう。 - 三つ目は、パラメータを可変長引数で渡すケース。内部で
duckdb::vector
に詰めているため位置パラメータの簡易ケースに見える。
Prepareが送出する例外
(把握している範囲のみ)
- duckdb.ParameterNotResolvedException
- パラメータの型が解決できなかった場合に投げられる。
- おもに
select list
でプレースホルダに型キャストを指定しなかった場合に見られる。- 大半は解決不能で例外を投げるが、文字列型との連結(
||
)であれば、なぜか解決してくれる(リテラル除く)- 🙆♀️
$s || Foo.s
- 🙆♀️
$s1 || $s2::text
- 🙅♀️
$s || 'abc'
- 🙆♀️
- 大半は解決不能で例外を投げるが、文字列型との連結(
PendingQuery
SQL文字列を渡すケースと、duckdb::SQLStatement
をわたす2つのオーバーロードが提供されている。
2番目の引数で、duckdb::MaterializedQueryResult
かduckdb::QueryResult
かをいずれかの振る舞いを返すかを選択できそう。
戻り値のduckdb::PendingQueryResult
のExecute
メソッドはduckdb::QueryResult
を返す。
正直、PreparedStatementとどう差別化しているのかよくわからなかった。
PendingQueryに関する追記
PendingQuery
はエラーとなるクエリであっても例外を送出せず、エラー状態の戻り値を返す。
duckdb::PendingQueryResult::GetErrorOpject()
でduckdb::ErrorData
を取得し、
ついでduckdb::ErrorData::ExtraInfo()
でエラーの詳細情報が取得できる。
使い所さんとしては、別リレーションの依存を持つ場合に、順序を逆にしてCREATE TABLE
するとエラーとなるが、このときどの依存先リレーションがなかったかがエラーの詳細情報から読み取れる。
auto& pending = conn.PendingQuery(content);
if (pending.HasError()) {
auto& err = pending.GetErrorObject().ExtraInfo();
for (auto[key, value]: err) {
// CREATE TABLEの場合、以下の情報が取得できる。nameが依存先リレーション
// key: name, value: B
// key: candidates, value: bit
// key: type, value: Type
// key: error_subtype, value: MISSING_ENTRY
std::cout << std::format("key: {}, value: {}", key, value) << std::endl;
}
ほか
明示的な切断メソッドは提供さててなさそう。
インスタンスををスタックメモリに作れば、メソッド抜けた際にRAII
で自動開放。
ヒープメモリにnewで作成した場合は、deleteするまで長期生存な運用方法なのだろう。きっと。
また、トランザクションの開始とか色々メソッドがあるが、それらは必要に応じておいおい。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
スキーマ投入
以下の説明を見た感じ、SELECT
同様、素直にクエリ投げるだけで良さそう。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
DESCRIBE文を眺めるだけのソース
#include <duckdb.hpp>
#include <duckdb/parser/query_node/list.hpp>
#include <duckdb/parser/tableref/list.hpp>
#define MAGIC_ENUM_RANGE_MAX (std::numeric_limits<uint8_t>::max())
#include <magic_enum/magic_enum.hpp>
#include <iostream>
extern "C" {
auto ParseDescribeStmt() -> void {
auto sql = "describe select $a::int, xyz, 123 from Foo";
auto db = duckdb::DuckDB(nullptr);
auto conn = duckdb::Connection(db);
auto stmts = conn.ExtractStatements(sql);
auto& stmt = stmts[0];
std::cout << std::format("stmt/type: {}", magic_enum::enum_name(stmt->type)) << std::endl;
duckdb::SelectStatement& select_stmt = stmt->Cast<duckdb::SelectStatement>();
std::cout << std::format("node/type: {} ({})", magic_enum::enum_name(select_stmt.node->type), select_stmt.node->type == duckdb::QueryNodeType::SELECT_NODE) << std::endl;
std::cout << std::format("node/type#2: {}", duckdb::QueryNodeType::SELECT_NODE == duckdb::SelectNode::TYPE) << std::endl;
auto& root_node = select_stmt.node->Cast<duckdb::SelectNode>();
std::cout << std::format("select-node/list: {}", root_node.select_list.size()) << std::endl;
std::cout << std::format("table-ref/type: {}", magic_enum::enum_name(root_node.from_table->type)) << std::endl;
auto& table_ref = root_node.from_table->Cast<duckdb::ShowRef>();
std::cout << std::format("showref/type: {} name: {}, node?: {}",
magic_enum::enum_name(table_ref.show_type),
table_ref.table_name == "" ? "<show query>" : table_ref.table_name,
(bool)table_ref.query
) << std::endl;
std::cout << std::format("query: {}", table_ref.ToString()) << std::endl;
}
}
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
SELECT文にDESCRIBE文をバイパス手術するだけのコード
TEST_CASE("Prepend describe") {
auto sql = std::string("select $a::int, xyz, 123 from Foo");
auto db = duckdb::DuckDB(nullptr);
auto conn = duckdb::Connection(db);
auto stmts = conn.ExtractStatements(sql);
auto& stmt = stmts[0];
// ここで名前パラメータを位置パラメータに変換する。
// 付け替え内容は省略
auto& select_stmt = stmt->Cast<duckdb::SelectStatement>();
auto& original_node = select_stmt.node;
auto describe = new duckdb::ShowRef();
describe->show_type = duckdb::ShowType::DESCRIBE;
describe->query = std::move(original_node);
auto describe_node = new duckdb::SelectNode();
describe_node->from_table.reset(describe);
// ↓これがないと実行時エラー。理由は後述の注意点
describe_node->select_list.push_back(duckdb::StarExpression().Copy());
select_stmt.node.reset(describe_node);
// 記録されたパラメータを改ざんする。理由は後述の注意点2
{
// パラメータ名を$1とした場合
// DESCRIBEする上でパラメータの値は関係なく、
// 同じパラメータに差し替えてしまっても問題ない
select_stmt.named_param_map = {{"1", 0}};
// 件数もお忘れなく
select_stmt.n_param = 1;
}
}
catchorg/Catch2
(https://github.com/catchorg/Catch2)で確認
注意点
上記コードにコメントとして書いた
describe_node->select_list.push_back(duckdb::StarExpression().Copy());
がないまま、このPrepare
を行うと実行時エラーとなる。
なくても、文字列化すれば期待通りの結果となるため、原因の追求に手間取った。
加工した結果と文字列化後に再パースした結果を比較したところ、select list
に差異があった。
具体的には、文字列化後に再パースした結果にはselect list
として*
が入っていた。
これはDESCRIBE
が実質SELECT文
であるため、形だけでもselect list
が必要なためと思われる。
以下、なんとかノード情報が取得できないか思案し、duckdb json plugin
のJsonSerializer
を使って文字列化した記録。
// stmt は加工したduckdb::SQLStatement
{
auto alc = duckdb_yyjson::yyjson_alc_dyn_new();
auto doc_1 = duckdb_yyjson::yyjson_mut_doc_new(alc);
auto ser_1 = duckdb::JsonSerializer(doc_1, false, false, false);
stmt.Serialize(ser_1);
duckdb_yyjson::yyjson_mut_doc_set_root(doc_1, ser_1.GetRootObject());
std::cout << "<<<< 加工したStatement >>>>" << std::endl;
std::cout << duckdb_yyjson::yyjson_mut_write(doc_1, 0, nullptr) << std::endl;
}
// 文字列かした後に再度パースしたステートメントを比較対象に
{
auto parser = duckdb::Parser();
auto stmts = parser.ParseQuery(stmt.ToString());
auto& stmt_2 = stmts[0];
auto doc_2 = duckdb_yyjson::yyjson_mut_doc_new(alc);
auto ser_2 = duckdb::JsonSerializer(doc_2, false, false, false);
stmt_2->Cast<duckdb::SelectStatement>().Serialize(ser_2);
duckdb_yyjson::yyjson_mut_doc_set_root(doc_2, ser_2.GetRootObject());
std::cout << "<<<< 文字列化したあと再パースしたStatement >>>>" << std::endl;
std::cout << duckdb_yyjson::yyjson_mut_write(doc_2, 0, nullptr) << std::endl;
}
注意点2
duckdb
はパース結果に元のクエリのプレースホルダ情報を記録しており(n_param
とnamed_param_map
フィールド)、duckdb::PreparedStatement
を構築する際に引き継がれる。
初めから位置パラメータであれば問題ないが、名前パラメータの場合には構文木の内容を位置パラメータに差し替える必要があり[1]、加えてこの記録しているプレースホルダ情報も改ざんする必要がある。
これは、duckdb::PreparedStatement
を実行(Execute
)する際に引き継がれたプレースホルダ情報と渡されたパラメータの付け合わせを行なっているからである。
-
1.0.0において、duckdb_cliでも名前パラメータを含むクエリをDESCRIBEするとエラーとなる。位置パラメータであれば実行可能。 ↩︎
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
duckdb::QueryResult
問い合わせ結果を保持する型。
duckdb::Connection.Query
が返すduckdb::MaterializedQueryResult
はこの型のサブクラス。
この型を返すメソッドは例外を投げないことに注意 プレースホルダにパラメータ渡さなかったら例外投げてきやがったわ。
- 親クラスの
duckdb::BaseQueryResult
のHasError
メソッドで成功したかどうかが確認できる。 - エラー情報は、
GetErrorType
やGetError
で取得できる。 - エラーの場合に例外送出したい場合は、
ThrowError
メソッドを呼ぶ。
列名の取得
QueryResult::ColumnName
メソッドに列のインデックス(0-base)を渡す。
列数の取得は、さらに親クラスであるduckdb::BaseQueryResult
のColumnCount
メソッドを呼ぶ。
データの取得
QueryResult::Fetch
メソッドを呼ぶことでカラムナなデータチャンクを取得できる。
が、行単位で扱いたい場合にはちと不便。
QueryResult::begin
やQueryResult::end
メソッドが返すQueryResult::QueryResultIterator
が行単位で取得できるとコメントに書かれている。
The row-based query result iterator. Invoking the
内部でFetch
メソッドも呼び出しているため、たぶんこれが一番楽だと思います。
使い方は一般的なイテレーターと同じ。
イテレータをデリファレンスした際の型はduckdb::QueryResult::QueryResultRow
。
QueryResultRow.GetValue
メソッドにインデックス(0-base)を渡すことで値を取得できる。
戻り値の型はテンプレート引数のため、多分auto
は使えない。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
プレースホルダを持つSQL
duckdb_cli
で以下のクエリを投げた場合、問題なく結果が表示される。
DESCRIBE (SELECT CAST($1 AS INTEGER) AS a, 123, CAST($1 AS VARCHAR) AS c)
同じことをduckdb::Connection
のQuery
メソッドや、duckdb::PreparedStatement
のExecute
メソッドに対してパラメータなしで実行すると、Unknown exception
[1]というメッセージの例外が投げられて死ぬ。
直接実行するduckdb::Connection
のQuery
メソッドではどうすることもできない[2]が、
duckdb::PreparedStatement
であれば、すべてのプレースホルダにNULL
を渡すことで回避できる。
問題はパラメータの個数。
これは、PreparedStatement
のGetStatementProperties
メソッドを呼び、duckdb::StatementProperties
を取得する。
この型のparameter_count
にパラメータの個数が保持されている。
あとは以下のように記述すればよい。
auto sql = "...";
auto stmt = conn.Prepare(sql);
auto param_count = stmt->GetStatementProperties().parameter_count;
// パラメータ数分だけ、duckdb::Valueのデフォルトコンストラクタで埋める
// duckdb::Valueのデフォルトコンストラクタは、引数に`LogicalType::SQLNULL`を渡してくれる
auto params = duckdb::vector<duckdb::Value>(param_count);
auto result = stmt->Execute(params);
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
Statementのダンプ
duckdb::SQLStatement
の各サブクラス(例えばduckdb::SelectStatement
)はSerialize
メソッドを持つ
void Serialize(Serializer &serializer) const;
Serializer
はインターフェースであり、以下のサブクラスが提供されている。
- BinarySerializer (src/include/duckdb/common/serializer/binary_serializer.hpp)
- JsonSerializer (extension/json/include/json_serializer.hpp)
ここでは、JsonSerializer
の使い方を紹介する。
JsonSerializer
を使うにあたり、いくつかのパッケージに依存しているため、あわせてインクルードパスに含める。
- yyjson (third_party/yyjson)
- fmt (third_party/fmt)
- re2 (third_party/re2)
JsonSerializer
や依存パッケージに対するヘッダファイルは、duckdb
のビルド成果物には含まれないため、duckdb
のソースレポジトリをクローンしておく必要がある。
実装はlibduckdb
からリンク可能のため実装のソースビルドは任意で。
以下コード例
template <typename T>
auto serializeStatement(duckdb_yyjson::yyjson_mut_doc *doc, T& stmt) -> std::string {
auto ser = duckdb::JsonSerializer(doc, false, false, false);
stmt.Serialize(ser);
duckdb_yyjson::yyjson_mut_doc_set_root(doc, ser.GetRootObject());
auto flag = duckdb_yyjson::YYJSON_WRITE_PRETTY;
return std::move(std::string(duckdb_yyjson::yyjson_mut_write(doc, flag, nullptr)));
}
上記のヘルパ関数を使用例
#include <iostream>
#include <duckdb/parser/parser.hpp>
#include <duckdb/parser/statement/list.hpp>
#include "json_serializer.hpp"
#include "yyjson.hpp"
auto main() -> void {
duckdb::Parser parser;
parser.ParseQuery("select 123, 'anc'");
auto stmts = parser.ParseQuery;
auto doc = duckdb_yyjson::yyjson_mut_doc_new(nullptr);
std::cout << ::serializeStatement(doc, stmts[0]->Cast<duckdb::SelectStatement>()) << std::endl;
duckdb_yyjson::yyjson_mut_doc_free(doc);
return 0;
}
Serialize
メソッドは、SQLStatement
のサブクラスだけではなくduckdb::SelectNode
やduckdb::parsedExpresseion
などのサブクラスも持っているため、子要素のみのダンプも同様の方法で行える。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
Binderによる型解決
(情報が精査できていないため、分かった範囲だけ列挙)
-
duckdb::Binder::CreateBinder
にduckdb::ClientContext
を渡してインスタンス化-
ClientContext
はduckdb::Connection
のパブリックフィールド
-
-
Bind
メソッドは引数としてduckdb::SQLStatement
を渡して型解決結果(duckdb::BoundStatement
)を返す -
Bind
メソッドは事前にトランザクションを開始しておく必要がある。-
Connection::BeginTransaction
orClientContext::RunFunctionInTransaction
-
- プレースホルダを含むクエリに対して
Bind
メソッド実行前にBinder
のperameters
フィールドを指定する必要あり- 指定し忘れると
duckdb::ExpressionBinder::BindExpression
で実行時例外が送出されるduckdb::InternalException
- 型解決だけが目的なのであれば、あくまで初期値なので空のままでもOK
- 逆に何らかの初期値を与えてしまうと、定数として認識されてしまう
- プレースホルダがなければ任意
-
duckdb::BoundParameterMap
(duckdb/planner/bound_parameter_map.hpp)の引数はduckdb::case_insensitive_map_t<duckdb::BoundParameterData>
-
case_insensitive_map_t
はただのstd::unordered_map
-
BoundParameterData
はduckdb::Value
(duckdb/common/types/value.hpp)を渡してインスタンス化-
Value
は実行器に渡されるパラメータそのもの - 判明していれば、
duckdb::LogicalType
を渡して型だけでインスタンス化、値はあとで再バインドなこともたぶんできる
-
-
- 指定し忘れると
-
Binder
は規定値としてトップレベルでのNULL
をINTEGER
に変換する- この制御は
can_contain_nulls
フィールドをtrue
にすることで止めれる -
private
フィールドだがSetCanContainNulls
メソッドで変更可能
- この制御は
-
select-list
にプレースホルダを置く場合、明示的に型キャスト(例:$1::int
)しないと、例外が送出されるduckdb::ParameterNotResolvedException
-
Bind
結果のduckdb::BoundStatement
はselect-list
の名前、型、論理プランを保持している- 論理プラン上でも、パラメータ自体の型は解決しないが、外側に型キャストが足されるので、それを見ればパラメータの型を推論できる
- テーブル列も適切に型が解決される
-
DESCRIBE
しなくても名称と型は取得可能- エイリアス名も取れる
- カタログ情報を追えば、
Nullability
も追跡できそう(な気がする)
-
-
select-list
の論理プランとfrom
の論理プランとは関連づけられない-
リレーションに別名(例:FROM Foo t1
)をつければselect-list
の論理プランに付与されるので、絞り込みは可能付与しないのなら全リレーションを辿ることになる
-
-
binding
フィールドにリレーションと列のインデックスが記録される- インデックス使ってカタログをルックアップできる?
- スター式は展開される
- 論理プランを走査する目的で
duckdb::LogicalOperatorVisitor
が提供されている- 論理プランの最適化の実施が本来の目的
- サブクラスを作ることで、プランの目的のノードをトラップできる
- 式ノードであればノードの具象型ごとにメソッドが用意されている (
VisitReplace
メソッド) - オペレータノードを直接トラップすることはできないが、
VisitOperator
メソッドをオーバーライドすれば近いことはできそう- リレーションの列名やエイリアス名は取得できるが、エイリアスのない定数の場合は空文字列
- 別途解決する手段を講じる必要がある (
ToString
で取れる値をエイリアスにする等)
- 別途解決する手段を講じる必要がある (
- 同じ列名がいてもお構いなしに同じ名前となる
- サフィックスを付与する等の手段を講じる必要がある
- ユーザ定義型(
ENUM
,STRUCT
等)は展開されユーザ定義名が落とされる-
LogicalType
のtype_info_
フィールドのエイリアスは空いてそう-
CREATE TYPE X AS XX
でユーザ定義名に別名をつけても別の方として認識される- エイリアス名が埋まることはなさそう
- スキーマ投入時に、ユーザ定義名をエイリアスに入れてしまえば論理プランまで引き継がれる
-
-
- リレーションの列名やエイリアス名は取得できるが、エイリアスのない定数の場合は空文字列
- 式ノードであればノードの具象型ごとにメソッドが用意されている (
-
BindContext
がもつBinder
コレクションを辿ればテーブルエントリまで届く- がしかし一番外側の
SELECT
句しか拾えずサブクエリだと手詰まり -
TableRef
のBind
結果はBoundTableRef
- 実テーブルの
BoundTableRef
であるBoundBaseTableRef
はテーブルエントリの参照を持つ サブクエリの場合BoundSubqueryRef
となり、subquery
フィールド(BoundQueryNode
)からBoundTableRef
を再帰的にたどれば、やがてBoundBaseTableRef
まで届く- 確かに届きはするがPlannerが発行する
table_index
とは異なるため、突き合わせできない。
- 実テーブルの
- がしかし一番外側の
- 2024-08-07追記:
BoundTableRef
をわざわざ取得しなくても、構成されたプラン内にいるLogicalGet
から取得できる。-
LogicalGet::function::get_bind_info
コールバックを経由して、duckdb::BindInfo`を取得する -
BindInfo::table
でカタログに届く- これはメインの
FROM
句であろうがサブクエリのFROM
句であろうが関係なく拾える。 - ただし表関数はどうなるかは未調査
- これはメインの
-
-
order by
を持たない場合、duckdb::LogicalProjection
がオペレータのルートにくる -
order by
を持つ場合、duckdb::LogicalOrderBy
がオペレータのルートに、その子オペレータにduckdb::LogicalProjection
が入る -
group by
に含めた列をselect-list
に置いた場合、列のbinding.table_index
が置き換わる。- 置き換わる値は
duckdb::LogicalAggregate
のgroup_index
の値 - 置き換えの際の
column_index
は0から付番する
- 置き換わる値は
-
group by
の際の集計をselect-list
に置く場合、table_index
はaggregate_index
の値に置き換えられる-
aggregate`の対象は
expressions`- 集約関数の場合、
duckdb::BoundAggregateExpression
- 集約関数の場合、
- 置き換えの際の
column_index
は0から付番する -
expressions
にはgroups
は含まれていないことに注意-
column_index
もgroups
とは独立して付番する
-
- 他に
groupings_index
も置き換えに使われそうな値だがSQL
をどう記述した時に現れるかは未調査(予想)groupings_index
はgrouping sets
句で現れそう・・・-
logical_aggregate.hpp
のソースコメントにGROUPING
関数の呼び出しで必要になるって書かれてた- 公式ドキュメントに
grouping
関数の使用例は記載ないが、困った時のポスグレドキュメント- https://www.postgresql.jp/document/16/html/functions-aggregate.html#FUNCTIONS-GROUPING-TABLE
-
table_index
としてgroupings_index
がgrouping_functions
ベクタのインデックスがそのままcolumn_index
として現れる
- 公式ドキュメントに
- 置き換えの元の内容は、同じく
duckdb::LogicalAggregate
のgroups
がduckdb::BoundColumnRefExpression
のベクタとして保持している
-
気になる人向け用プランのダンプ (クッソ長いので注意)
** SQL: select xyz, 123, $2::date as "Cast($time as DATE)", id + $3::bigint as xid from Foo where kind = $1
** Dump SQL Statement (JSON)
{
"node": {
"type": "SELECT_NODE",
"modifiers": [],
"cte_map": {
"map": []
},
"select_list": [
{
"class": "COLUMN_REF",
"type": "COLUMN_REF",
"alias": "",
"query_location": 7,
"column_names": [
"xyz"
]
},
{
"class": "CONSTANT",
"type": "VALUE_CONSTANT",
"alias": "",
"query_location": 12,
"value": {
"type": {
"id": "INTEGER",
"type_info": null
},
"is_null": false,
"value": 123
}
},
{
"class": "CAST",
"type": "OPERATOR_CAST",
"alias": "Cast($time as DATE)",
"query_location": 19,
"child": {
"class": "PARAMETER",
"type": "VALUE_PARAMETER",
"alias": "",
"query_location": 18446744073709551615,
"identifier": "2"
},
"cast_type": {
"id": "DATE",
"type_info": null
},
"try_cast": false
},
{
"class": "FUNCTION",
"type": "FUNCTION",
"alias": "xid",
"query_location": 55,
"function_name": "+",
"schema": "",
"children": [
{
"class": "COLUMN_REF",
"type": "COLUMN_REF",
"alias": "",
"query_location": 52,
"column_names": [
"id"
]
},
{
"class": "CAST",
"type": "OPERATOR_CAST",
"alias": "",
"query_location": 59,
"child": {
"class": "PARAMETER",
"type": "VALUE_PARAMETER",
"alias": "",
"query_location": 18446744073709551615,
"identifier": "3"
},
"cast_type": {
"id": "BIGINT",
"type_info": null
},
"try_cast": false
}
],
"filter": null,
"order_bys": {
"type": "ORDER_MODIFIER",
"orders": []
},
"distinct": false,
"is_operator": true,
"export_state": false,
"catalog": ""
}
],
"from_table": {
"type": "BASE_TABLE",
"alias": "",
"sample": null,
"query_location": 80,
"schema_name": "",
"table_name": "Foo",
"column_name_alias": [],
"catalog_name": ""
},
"where_clause": {
"class": "COMPARISON",
"type": "COMPARE_EQUAL",
"alias": "",
"query_location": 95,
"left": {
"class": "COLUMN_REF",
"type": "COLUMN_REF",
"alias": "",
"query_location": 90,
"column_names": [
"kind"
]
},
"right": {
"class": "PARAMETER",
"type": "VALUE_PARAMETER",
"alias": "",
"query_location": 18446744073709551615,
"identifier": "1"
}
},
"group_expressions": [],
"group_sets": [],
"aggregate_handling": "STANDARD_HANDLING",
"having": null,
"sample": null,
"qualify": null
}
}
[binder] parameters: true
** Dump BOUND Statement
**** types[0]
{
"id": "VARCHAR",
"type_info": null
}
**** types[1]
{
"id": "INTEGER",
"type_info": null
}
**** types[2]
{
"id": "DATE",
"type_info": null
}
**** types[3]
{
"id": "BIGINT",
"type_info": null
}
**** names[0] xyz
**** names[1] 123
**** names[2] Cast($time as DATE)
**** names[3] xid
** Dump BOUND Logical plan (LOGICAL_PROJECTION)
{
"type": "LOGICAL_PROJECTION",
"children": [
{
"type": "LOGICAL_FILTER",
"children": [
{
"type": "LOGICAL_GET",
"children": [],
"table_index": 0,
"returned_types": [
{
"id": "INTEGER",
"type_info": null
},
{
"id": "VARCHAR",
"type_info": null
},
{
"id": "INTEGER",
"type_info": null
}
],
"names": [
"id",
"xyz",
"kind"
],
"column_ids": [
2,
1,
0
],
"projection_ids": [],
"table_filters": {
"filters": []
},
"name": "seq_scan",
"arguments": [],
"original_arguments": [],
"has_serialize": true,
"function_data": {
"catalog": "memory",
"schema": "main",
"table": "Foo",
"is_index_scan": false,
"is_create_index": false,
"result_ids": []
},
"projected_input": []
}
],
"expressions": [
{
"expression_class": "BOUND_COMPARISON",
"type": "COMPARE_EQUAL",
"alias": "",
"query_location": 95,
"left": {
"expression_class": "BOUND_COLUMN_REF",
"type": "BOUND_COLUMN_REF",
"alias": "kind",
"query_location": 90,
"return_type": {
"id": "INTEGER",
"type_info": null
},
"binding": {
"table_index": 0,
"column_index": 0
},
"depth": 0
},
"right": {
"expression_class": "BOUND_CAST",
"type": "OPERATOR_CAST",
"alias": "",
"query_location": 18446744073709551615,
"child": {
"expression_class": "BOUND_CONSTANT",
"type": "VALUE_CONSTANT",
"alias": "",
"query_location": 18446744073709551615,
"value": {
"type": {
"id": "NULL",
"type_info": null
},
"is_null": true
}
},
"return_type": {
"id": "INTEGER",
"type_info": null
},
"try_cast": false
}
}
],
"projection_map": []
}
],
"table_index": 1,
"expressions": [
{
"expression_class": "BOUND_COLUMN_REF",
"type": "BOUND_COLUMN_REF",
"alias": "xyz",
"query_location": 7,
"return_type": {
"id": "VARCHAR",
"type_info": null
},
"binding": {
"table_index": 0,
"column_index": 1
},
"depth": 0
},
{
"expression_class": "BOUND_CONSTANT",
"type": "VALUE_CONSTANT",
"alias": "",
"query_location": 12,
"value": {
"type": {
"id": "INTEGER",
"type_info": null
},
"is_null": false,
"value": 123
}
},
{
"expression_class": "BOUND_PARAMETER",
"type": "VALUE_PARAMETER",
"alias": "Cast($time as DATE)",
"query_location": 19,
"identifier": "2",
"return_type": {
"id": "DATE",
"type_info": null
},
"parameter_data": {
"value": {
"type": {
"id": "NULL",
"type_info": null
},
"is_null": true
},
"return_type": {
"id": "DATE",
"type_info": null
}
}
},
{
"expression_class": "BOUND_FUNCTION",
"type": "BOUND_FUNCTION",
"alias": "xid",
"query_location": 55,
"return_type": {
"id": "BIGINT",
"type_info": null
},
"children": [
{
"expression_class": "BOUND_CAST",
"type": "OPERATOR_CAST",
"alias": "",
"query_location": 52,
"child": {
"expression_class": "BOUND_COLUMN_REF",
"type": "BOUND_COLUMN_REF",
"alias": "id",
"query_location": 52,
"return_type": {
"id": "INTEGER",
"type_info": null
},
"binding": {
"table_index": 0,
"column_index": 2
},
"depth": 0
},
"return_type": {
"id": "BIGINT",
"type_info": null
},
"try_cast": false
},
{
"expression_class": "BOUND_PARAMETER",
"type": "VALUE_PARAMETER",
"alias": "",
"query_location": 59,
"identifier": "3",
"return_type": {
"id": "BIGINT",
"type_info": null
},
"parameter_data": {
"value": {
"type": {
"id": "NULL",
"type_info": null
},
"is_null": true
},
"return_type": {
"id": "BIGINT",
"type_info": null
}
}
}
],
"name": "+",
"arguments": [
{
"id": "BIGINT",
"type_info": null
},
{
"id": "BIGINT",
"type_info": null
}
],
"original_arguments": [],
"has_serialize": false,
"is_operator": true
}
]
}
** Dump BOUND Parameters
params[0] key: 1
**** Param types[0]
{
"id": "NULL",
"type_info": null
}
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
ユーザ定義情報の取得
duckdb::Catalog::GetEntry
メソッドで取得できる
- メソッド実行前にトランザクションを開始する必要がある
- カタログ名不明(
INVALID_CATALOG
)、スキーマ名不明(INVALID_SCHEMA
)で引き渡せば、全カタログから探し出してくれる
conn.context->RunFunctionInTransaction(
[&]{
auto& entry = duckdb::Catalog::GetEntry<duckdb::TypeCatalogEntry>(
*conn.context,
INVALID_CATALOG, INVALID_SCHEMA, "Visibility"
);
}
);
定義名が分かっていれば上記の方法で対処できるが、全てのユーザ定義情報を取得するためには全てのユーザ定義名が必要となる。
もっと簡易的に取得する方法はないかと探すと、function/table/system/duckdb_types.cpp
のDuckDBTypesInit
でそれっぽいことをやっている。
抜粋:
unique_ptr<GlobalTableFunctionState> DuckDBTypesInit(ClientContext &context, TableFunctionInitInput &input) {
auto result = make_uniq<DuckDBTypesData>();
auto schemas = Catalog::GetAllSchemas(context);
for (auto &schema : schemas) {
schema.get().Scan(context, CatalogType::TYPE_ENTRY,
[&](CatalogEntry &entry) { result->entries.push_back(entry.Cast<TypeCatalogEntry>()); });
};
return std::move(result);
}
-
Catalog::GetAllSchemas
の戻り値の型はreference<SchemaCatalogEntry>
-
reference
はstd::reference_wrapper
の別名
SchemaCatalogEntry::Scan
を呼んで適当なコンテナに詰めてあげればよい。
またこの方法だけでは、システムで定義されたすべての型を拾ってしまう。
ユーザ定義型に限定したい場合、CatalogEntry
のinternal
フィールドがfalse
のものに絞り込めばいい。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
CatalogEntryの取り扱い
CREATE TABLE
を行うと、duckdb::CatalogEntry
の形で格納される。
このオブジェクトは、
-
duckdb::Catalog
のGetEntry
メソッドで取得できる -
duckdb::Binder
でduckdb::TableRef
のバインドを実行した際、実テーブルであれば内部で保持する
Catalog
を取り扱う際以下の注意点がある。
- 取得はトランザクションスコープ内で行う
-
Catalog
から取得できるCatalogEntry
への列定義や制約情報のアクセスもまたトランザクションスコープ内で行う
トランザクションスコープ外でアクセスしてもエラーにはならないが破棄されるのか中身が空っぽになる。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
JoinプランのNullabilityの推論
select *
from a
outer join b
inner join c
なSQLの場合
"type": "LOGICAL_PROJECTION",
"type": "LOGICAL_FILTER",
"type": "LOGICAL_COMPARISON_JOIN",
"type": "LOGICAL_COMPARISON_JOIN",
"type": "LOGICAL_GET",
"type": "LOGICAL_FILTER",
"type": "LOGICAL_GET",
"type": "LOGICAL_GET",
ような逆ポーランド記法っぽい構造でプランが構築される。
このプランを以下のスタックマシンを実現するようにduckdb::LogicalOperatorVisitor
で走査すれば、各リレーションのNullabilityを推論できそう・・・。お願いできて。
LOGICAL_JOIN_C: # inner join c
init list # 結果リストの初期化
push dummy # 最後のdrop対策
push inner right
push inner left
walkin c[0] # 左リレーションに入る
LOGICAL_JOIN_B: # outer join b
push outer right
push outer left
walkin b[0] # 左リレーションに入る
LOGICAL_GET_A: # a
pop # (outer left) <- from a
eval # 結果リストを探索 -> ないからNotNullに決定
add list # (a, NotNull) を結果リストに入れる
ret
LOGICAL_JOIN_B:
walkin b[1] # 右リレーションに入る
LOGICAL_GET_B: # b
pop # (outer right) <- b
eval # outer rightなのでNullに決定
add list # (b, Null) をリストに入れる
ret
LOGICAL_JOIN_B:
drop # (inner left)を捨てる
ret
LOGICAL_JOIN_C:
walkin c[1] # 右リレーションに入る
LOGICAL_GET_C: # c
pop # (inner right) <- c
eval # inner rightなので相手の列で決める / 全てNotNull -> NotNull else Null
add list # (c, ?) を結果リストに入れる
ret
LOGICAL_JOIN_C:
drop # (dummy)を捨てる
ret
その後・・・
なんやかんやあって、この方法じゃ解決しきれなかった。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
Create文の論理プラン (論理オペレータ)
duckdb::LogicalOperatorType
を眺めると、以下のものが見つかる。
- LOGICAL_CREATE_TABLE
- 具象型は
duckdb::LogicalCreateTable
-
info
フィールド(duckdb::BoundCreateTableInfo
型)に詳細情報を保持
- 具象型は
- LOGICAL_CREATE_INDEX
- 具象型は
duckdb::LogicalCreateIndex
-
info
フィールド(duckdb::CreateIndexInfo
)に詳細情報を保持
- 具象型は
- LOGICAL_CREATE_SEQUENCE
- 具象型は
duckdb::LogicalCreate
-
info
フィールド(duckdb::CreateInfo
)に詳細情報を保持しているが基底クラスのためキャストが必要- 具象型は
duckdb::CreateSequenceInfo
- 具象型は
- 具象型は
- LOGICAL_CREATE_VIEW
- 具象型は
duckdb::LogicalCreate
-
info
フィールド(duckdb::CreateInfo
)に詳細情報を保持しているが基底クラスのためキャストが必要- 具象型は
duckdb::CreateViewInfo
- 具象型は
- 具象型は
- LOGICAL_CREATE_TYPE
- 具象型は
duckdb::LogicalCreate
-
info
フィールド(duckdb::CreateInfo
)に詳細情報を保持しているが基底クラスのためキャストが必要- 具象型は
duckdb::CreateTypeInfo
- 具象型は
- 具象型は
- LOGICAL_CREATE_SCHEMA (未調査)
- LOGICAL_CREATE_MACRO (未調査)
ENUMユーザ定義型のメンバ情報の取得
ENUMユーザ定義型のメンバ情報は、duckdb::CreateTypeInfo
の奥深くに眠っている。
またその値もopaque
ポインタな配列であるため、キャストして戻してやる必要がありやや面倒。
duckdb::unique_ptr<duckdb::LogicalOperator>& op = ...; // (snip)
// 論理オペレータの型キャスト
auto& op_create = op->Cast<duckdb::LogicalCreate>();
// TypeInfoの取得
auto type_info = op_create.type->Cast<duckdb::CreateTypeInfo>();
// typeフィールド (duckdb::LogicalType)が型情報の総本山
// LogicalTypeの型種別の判定
bool b = info.type.Contains(duckdb::LogicalTypeId::ENUM);
// 拡張区域(ExtraTypeInfo)に進行する
auto ext_info = type_info.type.AuxInfo();
// ExtraTypeInfoはLogicalTypeごとに異なる派生型を持つ
auto enum_ext_info = ext_info->Cast<duckdb::EnumTypeInfo>();
// EnumTypeInfo::values_insert_order (protected)が目的地
// だがduckdb::Venctorという名の実質opaqueポインタ
// duckdb::FlatVectorを使ってキャストする (string_t*を返す)
auto members = duckdb::FlatVector::GetData<duckdb::string_t>(enum_ext_info.GetValuesInsertOrder());
// 要素数の取得
auto member_size = enum_ext_info.GetDictSize();
// member_sizeとmembersを使ってループを回す
for (const auto *it = members; it != members + member_size; ++it) {
// (snip)
}
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
ユーザ定義型のバインド
CREATE TYPE 〜
でユーザ定義型を作り、その型をテーブルやキャストに使うケース。
このケースでは、ユーザ定義型そのものがバインドされるのではなく、ENUMやSTRUSTなどの実際の型がバインドされる。
これの何が問題かというとユーザ定義型名が捨てられるということ。
そもそもduckdbの論理型は種別(INTEGER等)は保持しているが型名を保持していないため。
型名はカタログが保持しているから。
バインド結果でユーザ定義型の型名が知りたければ悪あがきが必要。
悪あがき
例えばENUM
の場合、同じ値のセットを持っていても別のユーザ定義型として作成した場合は別のインスタンスとして扱われる。
そのことを利用して、CREATE TYPE
直後にユーザ定義のカタログをフェッチし、論理型に名前を書き込んでしまえばいい(悪い顔
// トランザクションスコープないで実行すること
auto schemas = duckdb::Catalog::GetAllSchemas(*conn.context);
for (auto &schema: schemas) {
schema.get().Scan(*conn.context, duckdb::CatalogType::TYPE_ENTRY, [&](duckdb::CatalogEntry &entry) {
if (! entry.internal) {
auto& type_entry = entry.Cast<duckdb::TypeCatalogEntry>();
// カタログが持つ定義名を論理型のエイリアスとして設定する
// なおprimitive型の場合、カタログをバイパスするためユーザ定義名を取得されない
// ただし型キャストでエイリアスな型を明示すれば、カタログを通るため取得可能
type_entry.user_type.SetAlias(type_entry.name);
// ↓の方法では、`CREATE TYPE U AS INTEGER`のように
// 組み込み型のエイリアスを作成した場合に`GetAuxInfoShrPtr()`がnullptrを返す
// type_entry.user_type.GetAuxInfoShrPtr()->alias = type_entry.name;
}
});
};
また、SELECT
文で`ENUM('a','b')と匿名でキャストしても同様。
![ktz_alias](https://zenn-dev.github.io/default-avatars/dark/k.png)
CTEのハンドリング
パースされた構文木において、CTE
はduckdb::QueryNode
のcte_map
フィールドに保持される。
Derived Table
のようにfrom_table
にぶら下がるわけではない。
と・・・単純にはいかないのが世の常で、
-
WITH v AS (...) ...
とすると、QueryNodetype
はSELECT_NODE
-
WITH v AS NOT MATERIALIZED (...) ...
とすると、QueryNodetype
はSELECT_NODE
-
WITH v AS MATERIALIZED (...) ...とすると、
QueryNodetypeは
CTE_NODE`
となる。
また、それぞれのケースのduckdb::CTEMaterialize
の値は
-
WITH v AS (...) ...
の場合、CTE_MATERIALIZE_DEFAULT
-
WITH v AS NOT MATERIALIZED (...) ...
の場合もCTE_MATERIALIZE_DEFAULT
-
WITH v AS MATERIALIZED (...) ...
の場合のみ、CTE_MATERIALIZE_ALWAYS
となる。
CTE_MATERIALIZE_DEFAULT
は、構文木をToString()する際にNOT MATERIALIZED
を取り除いてしまう困りもの。
CTE定義のアクセス
cte_map
はduckdb::InsertionOrderPreservingMap
をラップしておりcte_map.map
でアクセス可能
-
duckdb::InsertionOrderPreservingMap
はstd::unordered_map
ライクなAPI
を持つ- 実際
std::unordered_map
をラップしたもの - マップのキーが
CTE
名、値がCTE
の定義内容 - 値の型は
duckdb::unique_ptr<duckdb::CommonTableExpressionInfo>
でquery
フィールドでduckdb::SelectStatement
にアクセス可能
- 実際
duckdb::InsertionOrderPreservingMap
はKeyとValueの組のイテレータを返すbegin
とend
が提供されているため、以下のようにすれば走査できる。
// nodeの型はduckdb::QueryNode
for (auto& [key, value]: node.cte_map.map) {
// ...
}