Open17

DuckDBのC++ClientAPI

ktz_aliasktz_alias

インメモリ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_aliasktz_alias

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_aliasktz_alias

問い合わせ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.PreparedStatementExecuteメソッドにプレースホルダのパラメータを渡すことでduckdb.QueryResultを返す。

duckdb.PreparedStatementExecuteメソッドは3つのオーバーロードが提供されている。

  • 一つ目は、位置パラメータのコレクションをstd::vecrorを派生したduckdb::vectorに詰めて渡すケース。2番目の引数でduckdb.MaterializedQueryResultduckdb.QueryResultかをいずれかの振る舞いを返すかを選択できそう。
  • 二つ目は、名前付きパラメータのコレクションをduckdb::case_insensitive_map_tに詰めて渡すケース。2番目の引数でduckdb.MaterializedQueryResultduckdb.QueryResultかをいずれかの振る舞いを返すかを選択できそう。
  • 三つ目は、パラメータを可変長引数で渡すケース。内部でduckdb::vectorに詰めているため位置パラメータの簡易ケースに見える。

Prepareが送出する例外

(把握している範囲のみ)

  • duckdb.ParameterNotResolvedException
    • パラメータの型が解決できなかった場合に投げられる。
    • おもにselect listでプレースホルダに型キャストを指定しなかった場合に見られる。
      • 大半は解決不能で例外を投げるが、文字列型との連結(||)であれば、なぜか解決してくれる(リテラル除く)
        • 🙆‍♀️ $s || Foo.s
        • 🙆‍♀️ $s1 || $s2::text
        • 🙅‍♀️ $s || 'abc'

PendingQuery

SQL文字列を渡すケースと、duckdb::SQLStatementをわたす2つのオーバーロードが提供されている。
2番目の引数で、duckdb::MaterializedQueryResultduckdb::QueryResultかをいずれかの振る舞いを返すかを選択できそう。
戻り値のduckdb::PendingQueryResultExecuteメソッドは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_aliasktz_alias

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_aliasktz_alias

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 pluginJsonSerializerを使って文字列化した記録。

        // 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_paramnamed_param_mapフィールド)、duckdb::PreparedStatementを構築する際に引き継がれる。

初めから位置パラメータであれば問題ないが、名前パラメータの場合には構文木の内容を位置パラメータに差し替える必要があり[1]、加えてこの記録しているプレースホルダ情報も改ざんする必要がある。

これは、duckdb::PreparedStatementを実行(Execute)する際に引き継がれたプレースホルダ情報と渡されたパラメータの付け合わせを行なっているからである。

脚注
  1. 1.0.0において、duckdb_cliでも名前パラメータを含むクエリをDESCRIBEするとエラーとなる。位置パラメータであれば実行可能。 ↩︎

ktz_aliasktz_alias

duckdb::QueryResult

問い合わせ結果を保持する型。
duckdb::Connection.Queryが返すduckdb::MaterializedQueryResultはこの型のサブクラス。

この型を返すメソッドは例外を投げないことに注意 プレースホルダにパラメータ渡さなかったら例外投げてきやがったわ。

  • 親クラスのduckdb::BaseQueryResultHasErrorメソッドで成功したかどうかが確認できる。
  • エラー情報は、GetErrorTypeGetErrorで取得できる。
  • エラーの場合に例外送出したい場合は、ThrowErrorメソッドを呼ぶ。

列名の取得

QueryResult::ColumnNameメソッドに列のインデックス(0-base)を渡す。
列数の取得は、さらに親クラスであるduckdb::BaseQueryResultColumnCountメソッドを呼ぶ。

データの取得

QueryResult::Fetchメソッドを呼ぶことでカラムナなデータチャンクを取得できる。
が、行単位で扱いたい場合にはちと不便。

QueryResult::beginQueryResult::endメソッドが返すQueryResult::QueryResultIteratorが行単位で取得できるとコメントに書かれている。

The row-based query result iterator. Invoking the

内部でFetchメソッドも呼び出しているため、たぶんこれが一番楽だと思います。
使い方は一般的なイテレーターと同じ。

イテレータをデリファレンスした際の型はduckdb::QueryResult::QueryResultRow
QueryResultRow.GetValueメソッドにインデックス(0-base)を渡すことで値を取得できる。
戻り値の型はテンプレート引数のため、多分autoは使えない。

ktz_aliasktz_alias

プレースホルダを持つSQL

duckdb_cliで以下のクエリを投げた場合、問題なく結果が表示される。

DESCRIBE (SELECT CAST($1 AS INTEGER) AS a, 123, CAST($1 AS VARCHAR) AS c)

同じことをduckdb::ConnectionQueryメソッドや、duckdb::PreparedStatementExecuteメソッドに対してパラメータなしで実行すると、Unknown exception[1]というメッセージの例外が投げられて死ぬ。

直接実行するduckdb::ConnectionQueryメソッドではどうすることもできない[2]が、
duckdb::PreparedStatementであれば、すべてのプレースホルダにNULLを渡すことで回避できる。

問題はパラメータの個数。
これは、PreparedStatementGetStatementPropertiesメソッドを呼び、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);
脚注
  1. もしかしたらcatch2がハンドリングできなかっただけかもしれない。 ↩︎

  2. パラメータの個数が既知であれば可変長引数版のQueryメソッドに対してnullptrなパラメータで埋めてあげればいけるかも(未確認) ↩︎

ktz_aliasktz_alias

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::SelectNodeduckdb::parsedExpresseionなどのサブクラスも持っているため、子要素のみのダンプも同様の方法で行える。

ktz_aliasktz_alias

Binderによる型解決

(情報が精査できていないため、分かった範囲だけ列挙)

  • duckdb::Binder::CreateBinderduckdb::ClientContextを渡してインスタンス化
    • ClientContextduckdb::Connectionのパブリックフィールド
  • Bindメソッドは引数としてduckdb::SQLStatementを渡して型解決結果(duckdb::BoundStatement)を返す
  • Bindメソッドは事前にトランザクションを開始しておく必要がある。
    • Connection::BeginTransaction or ClientContext::RunFunctionInTransaction
  • プレースホルダを含むクエリに対してBindメソッド実行前にBinderperametersフィールドを指定する必要あり
    • 指定し忘れると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
      • BoundParameterDataduckdb::Value (duckdb/common/types/value.hpp)を渡してインスタンス化
        • Valueは実行器に渡されるパラメータそのもの
        • 判明していれば、duckdb::LogicalTypeを渡して型だけでインスタンス化、値はあとで再バインドなこともたぶんできる
  • Binderは規定値としてトップレベルでのNULLINTEGERに変換する
    • この制御はcan_contain_nullsフィールドをtrueにすることで止めれる
    • privateフィールドだがSetCanContainNullsメソッドで変更可能
  • select-listにプレースホルダを置く場合、明示的に型キャスト(例: $1::int)しないと、例外が送出される
    • duckdb::ParameterNotResolvedException
  • Bind結果のduckdb::BoundStatementselect-listの名前、型、論理プランを保持している
    • 論理プラン上でも、パラメータ自体の型は解決しないが、外側に型キャストが足されるので、それを見ればパラメータの型を推論できる
    • テーブル列も適切に型が解決される
      • DESCRIBEしなくても名称と型は取得可能
        • エイリアス名も取れる
      • カタログ情報を追えば、Nullabilityも追跡できそう(な気がする)
    • select-listの論理プランとfromの論理プランとは関連づけられない
      • リレーションに別名(例: FROM Foo t1)をつければselect-listの論理プランに付与されるので、絞り込みは可能
        • 付与しないのなら全リレーションを辿ることになる
    • bindingフィールドにリレーションと列のインデックスが記録される
      • インデックス使ってカタログをルックアップできる?
    • スター式は展開される
    • 論理プランを走査する目的でduckdb::LogicalOperatorVisitorが提供されている
      • 論理プランの最適化の実施が本来の目的
      • サブクラスを作ることで、プランの目的のノードをトラップできる
        • 式ノードであればノードの具象型ごとにメソッドが用意されている (VisitReplaceメソッド)
        • オペレータノードを直接トラップすることはできないが、VisitOperatorメソッドをオーバーライドすれば近いことはできそう
          • リレーションの列名やエイリアス名は取得できるが、エイリアスのない定数の場合は空文字列
            • 別途解決する手段を講じる必要がある (ToStringで取れる値をエイリアスにする等)
          • 同じ列名がいてもお構いなしに同じ名前となる
            • サフィックスを付与する等の手段を講じる必要がある
          • ユーザ定義型(ENUM, STRUCT等)は展開されユーザ定義名が落とされる
            • LogicalTypetype_info_フィールドのエイリアスは空いてそう
              • CREATE TYPE X AS XXでユーザ定義名に別名をつけても別の方として認識される
                • エイリアス名が埋まることはなさそう
              • スキーマ投入時に、ユーザ定義名をエイリアスに入れてしまえば論理プランまで引き継がれる
      • BindContextがもつBinderコレクションを辿ればテーブルエントリまで届く
        • がしかし一番外側のSELECT句しか拾えずサブクエリだと手詰まり
        • TableRefBind結果は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::LogicalAggregategroup_indexの値
    • 置き換えの際のcolumn_indexは0から付番する
  • group byの際の集計をselect-listに置く場合、table_indexaggregate_indexの値に置き換えられる
    • aggregate`の対象はexpressions`
      • 集約関数の場合、duckdb::BoundAggregateExpression
    • 置き換えの際のcolumn_indexは0から付番する
    • expressionsにはgroupsは含まれていないことに注意
      • column_indexgroupsとは独立して付番する
    • 他にgroupings_indexも置き換えに使われそうな値だがSQLをどう記述した時に現れるかは未調査
      • (予想)groupings_indexgrouping sets句で現れそう・・・
      • logical_aggregate.hppのソースコメントにGROUPING関数の呼び出しで必要になるって書かれてた
    • 置き換えの元の内容は、同じくduckdb::LogicalAggregategroupsduckdb::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_aliasktz_alias

ユーザ定義情報の取得

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.cppDuckDBTypesInitでそれっぽいことをやっている。

抜粋:

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>
  • referencestd::reference_wrapperの別名

SchemaCatalogEntry::Scanを呼んで適当なコンテナに詰めてあげればよい。
またこの方法だけでは、システムで定義されたすべての型を拾ってしまう。

ユーザ定義型に限定したい場合、CatalogEntryinternalフィールドがfalseのものに絞り込めばいい。

ktz_aliasktz_alias

CatalogEntryの取り扱い

CREATE TABLEを行うと、duckdb::CatalogEntryの形で格納される。

このオブジェクトは、

  • duckdb::CatalogGetEntryメソッドで取得できる
  • duckdb::Binderduckdb::TableRefのバインドを実行した際、実テーブルであれば内部で保持する

Catalogを取り扱う際以下の注意点がある。

  • 取得はトランザクションスコープ内で行う
  • Catalogから取得できるCatalogEntryへの列定義や制約情報のアクセスもまたトランザクションスコープ内で行う

トランザクションスコープ外でアクセスしてもエラーにはならないが破棄されるのか中身が空っぽになる。

ktz_aliasktz_alias

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_aliasktz_alias

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_aliasktz_alias

ユーザ定義型のバインド

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_aliasktz_alias

CTEのハンドリング

パースされた構文木において、CTEduckdb::QueryNodecte_mapフィールドに保持される。
Derived Tableのようにfrom_tableにぶら下がるわけではない。

と・・・単純にはいかないのが世の常で、

  • WITH v AS (...) ...とすると、QueryNodetypeSELECT_NODE
  • WITH v AS NOT MATERIALIZED (...) ...とすると、QueryNodetypeSELECT_NODE
  • WITH v AS MATERIALIZED (...) ...とすると、QueryNodetypeCTE_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_mapduckdb::InsertionOrderPreservingMapをラップしておりcte_map.mapでアクセス可能

  • duckdb::InsertionOrderPreservingMapstd::unordered_mapライクなAPIを持つ
    • 実際std::unordered_mapをラップしたもの
    • マップのキーがCTE名、値がCTEの定義内容
    • 値の型はduckdb::unique_ptr<duckdb::CommonTableExpressionInfo>queryフィールドでduckdb::SelectStatementにアクセス可能

duckdb::InsertionOrderPreservingMapはKeyとValueの組のイテレータを返すbeginendが提供されているため、以下のようにすれば走査できる。

// nodeの型はduckdb::QueryNode
for (auto& [key, value]: node.cte_map.map) {
  // ...
}