高レベルのクエリ API には 2 つの実行経路があります。
通常の C++ API は、演算子オーバーロードとフィールドプロキシから型付きの式オブジェクトを作ります。この経路では [](auto row) { ... } のようなラムダ式を使い、コンパイラプラグインは不要です。
Native ビルドでは、任意で akkara-query Clang プラグインによる補助的な書き換え処理を使えます。この経路では、対応している型付きラムダ式、たとえば [](const Profile& row) { ... } がコンパイル時にバイトコード記述子へ変換されます。可能な場合は、エンコード済みの行バイト列を直接評価します。
クエリで使うフィールドは AKKARADB_ENTITY または AKKARADB_QUERYABLE で登録します。
struct Profile { uint64_t id; std::string email; std::string name; uint32_t age;};
AKKARADB_ENTITY(Profile, id, email, name, age);等価比較、IN、数値範囲に使うフィールドは indexed<&Field>() で登録します。startsWith() や prefix 型の like() を高速化したい文字列フィールドには、別途 prefixIndexed<&Field>() を登録します。
profiles.indexed<&Profile::email>() .indexed<&Profile::age>() .indexed<&Profile::name>() .prefixIndexed<&Profile::email>();基本的なクエリ
Section titled “基本的なクエリ”auto adults = profiles .query([](auto profile) { return profile.age >= 18; }) .limit(10) .toVector();query() は遅延評価されるビューを返します。反復するか終端メソッドを呼ぶまで評価されません。
| メソッド | 結果 |
|---|---|
where(fn) | 条件を論理 AND で追加します。 |
limit(n) | 一致した行数を制限します。 |
first() | std::optional<Entry> を返します。 |
any() | bool を返します。 |
count() | 一致した行数を数えます。 |
toVector() | 一致した entry を実体化します。 |
クエリ結果の順序は安定した API 契約ではありません。順序が必要な場合は、実体化した後にアプリケーション側で sort してください。
対応している式
Section titled “対応している式”| 式 | 例 |
|---|---|
| 等価比較と大小比較 | profile.age >= 18 |
| 論理演算子 | profile.age >= 18 && profile.name != "Bob" |
| リスト包含 | profile.name.in({"Alice", "Carol"}) |
| リスト非包含 | profile.age.notIn(std::vector<uint32_t>{17, 41}) |
| 文字列 prefix / search | profile.email.startsWith("a@"), profile.email.contains("@") |
| SQL 風 pattern | profile.email.like("a@%") |
| optional check | profile.nickname.isNull() |
| nested field | user.address.template field<&Address::city>() == "Tokyo" |
| map lookup | user.tags.get("tier") == std::optional<std::string>{"gold"} |
nested field は dependent template call になるため、template field<...>() と書きます。
Native バイトコードへの書き換え
Section titled “Native バイトコードへの書き換え”akkara-query Clang プラグインを使うと、型付きエンティティラムダ式を akkaradb::query::bytecode::CompiledQueryDescriptor<T> に書き換えられます。
auto adults = profiles.query([](const Profile& profile) { return profile.age >= 18 && profile.name == "Alice";});生成される記述子はバイトコード、定数、フィールド束縛、計画ヒント、所有しているキャプチャを保持します。PackedTable::query(descriptor) はバイトコードクエリビューを作ります。参照フィールドがすべて生データ読み取り機能を持つ場合はエンコード済みの行バイト列を直接評価し、そうでない場合はエンティティを復号して同じバイトコード VM を実行します。
where() もバイトコードとして合成できる場合は書き換えられます。
auto view = profiles.query([](const Profile& profile) { return profile.age >= 18;});
auto bobby = view.where([](const Profile& profile) { return profile.name == "Bobby";});profiles.query(...).where(...) の直接連結と、変数に入れた書き換え済みクエリビューの where(...) の両方が対象です。対応できない where ラムダ式は、通常の復号後フィルターとして残ります。対応できない query ラムダ式は、必要に応じて生成された HostCallBool 記述子に包まれます。
実用上の目安
Section titled “実用上の目安”通常の高レベル式 DSL では [](auto row) を使います。Clang プラグインで Native バイトコードへ書き換えたい場合は [](const Entity& row) を使います。
index は table を開いた直後に登録します。完全一致、IN、否定 membership、数値範囲には indexed<&Field>() を追加します。頻繁な startsWith() や単純な like("prefix%") には prefixIndexed<&Field>() を追加します。
map や nested field の条件は表現力のために使えますが、それだけで index の起点になるとは考えない方が安全です。必要なら top-level の indexed predicate と組み合わせます。
クエリラムダ式の境界
Section titled “クエリラムダ式の境界”通常の query() に渡すラムダ式は、実エンティティではなくクエリプロキシを受け取ります。戻り値はプロキシフィールドと対応演算子から作られたクエリ式である必要があります。
auto adults = profiles.query([](auto profile) { return profile.age >= 18 && profile.email.endsWith(".test");});ラムダ式自体は普通の C++ なので、ローカル定数や補助関数は使えます。境界になるのは戻り値です。補助関数を使う場合も、その補助関数はプロキシフィールド上のクエリ式を返す必要があります。
バイトコードへの書き換え経路では境界が少し違います。型付きエンティティラムダ式を受け取り、戻り値の C++ 式を変換しようとします。組み込みの比較、論理演算、算術、対応する文字列操作、キャプチャ、見えている静的カスタム opcode 登録は変換できます。未対応の行依存コードは、query(...) ではホスト呼び出しへのフォールバック、where(...) では復号後フィルターとして残ります。
インデックス計画
Section titled “インデックス計画”プランナーは候補集合を狭められる登録済みインデックスを探します。返す前には必ず完全なクエリ式を評価するため、インデックス計画は走査コストを変えるだけで、クエリの意味は変えません。
| 条件 | 計画 |
|---|---|
field == literal | 通常 field index の等価 range。 |
数値 field >/>=/</<= literal | bool 以外の arithmetic field では通常 field index の ordered range。 |
field.in(values) | 変換できる値ごとに等価 range を作り、候補 primary key を dedupe。 |
field != literal, notIn, isNotNull | 通常フィールドインデックス全体を走査し、式で絞り込みます。 |
isNull | 通常 field index 上で encoded empty optional の等価 range。 |
startsWith("prefix") | prefixIndexed<&Field>() があれば prefix-index range。なければ indexed<&Field>() の full field-index scan。 |
like("prefix%") | 単純な末尾 % pattern かつ prefixIndexed<&Field>() があれば prefix-index range。なければ通常 field-index scan。 |
like("exact") | 通常 field index の等価 range。 |
contains | 通常フィールドインデックス全体を走査し、式で絞り込みます。 |
AND | 両側を見て、score が高い usable index plan を選びます。 |
OR | 両側が index plan を作れる場合は range union を scan して primary key を dedupe。片側でも index 化できない場合は table scan。 |
AND の中に複数の usable index plan がある場合、planner は単純な score で scan source を選びます。
| 計画種別 | 相対 score |
|---|---|
| Equality | 100 |
| String prefix index | 95 |
IN equality ranges | 90 |
Optional isNull equality | 85 |
| Ordered numeric range | 80 |
OR union | 70 |
| Full field-index scan | 10 |
この score は heuristic です。table cardinality や index selectivity を見る cost model ではありません。
リテラル変換
Section titled “リテラル変換”index range は literal を field type に変換できる場合だけ作られます。
| ケース | 挙動 |
|---|---|
std::string field と string-like literal | std::string_view 経由で変換します。 |
| arithmetic field と arithmetic literal | 値が field type に収まる場合だけ変換します。負数は unsigned field に変換されません。 |
| constructible / convertible な field type | construction または assignment conversion を使います。 |
| 変換できない literal | その predicate は index plan の起点になりません。 |
IN values | 変換できる値ごとに等価 range を作り、変換できない値は skip します。 |
候補行を読み込んだ後には、完全なクエリ式が必ず評価されます。リテラル変換はインデックス範囲を作れるかどうかだけを決めます。
LIKE の意味
Section titled “LIKE の意味”like() は SQL 風 wildcard matching を行います。
| pattern token | 意味 |
|---|---|
% | 0 文字以上に一致します。 |
_ | ちょうど 1 文字に一致します。 |
| その他の文字 | その文字自身に一致します。 |
% や _ を wildcard ではない通常文字として扱うための escape syntax は、現時点では documented API としてありません。
prefix index を使えるのは like("abc%") のような単純な末尾 % pattern だけです。like("%abc")、like("a_c%")、like("a%b%")、like("abc_") は正しく評価されますが、prefix-index range optimization の対象にはなりません。
バイトコード実行時の詳細
Section titled “バイトコード実行時の詳細”バイトコード VM はフィールド読み込み、定数、比較、!、対応する文字列条件、利用者定義 opcode、ホスト呼び出しへのフォールバック、数値 +、-、*、/、%、JumpIfFalse / JumpIfTrue による短絡評価 && / || をサポートします。
BytecodeQueryView::where(descriptor) は既存の記述子と新しい記述子を論理 AND で合成し、走査用に一つの準備済みバイトコードプログラムを作ります。合成記述子はホスト呼び出しとカスタム opcode のキャプチャポインタを呼び出し・束縛ごとに保持するため、独立したキャプチャを持つ記述子同士も、一つの記述子全体で共有するキャプチャポインタなしに合成できます。
利用者定義 opcode の ID 範囲は 0x8000..0xFFFF です。CustomOpcodeRegistry::registerOpcode(...) は不正または重複したメタデータを拒否し、ログを出して、レジストリを変更せずに処理を続けます。AKKARADB_QUERY_OPCODE(...) による静的変換は、その登録が翻訳単位内で query より前に見えている場合に働きます。
バイトコードは Ref<T> をローカルの生の行フィールドとして辿りません。post.author->name == "Alice" のような条件式はテーブル境界をまたぎます。query(...) ではプラグインが通常の C++ 遅延解決を保つために HostCallBool 記述子を出し、where(...) では合成可能なバイトコードにできない限り復号後フィルターとして残します。
プランナーのトレードオフ
Section titled “プランナーのトレードオフ”AND は片側の index だけを使っても、候補行に残りの predicate を適用すれば正しく評価できます。OR は片側だけを使うと unindexed branch に一致する行を取り逃がすため、両側が indexable な場合だけ union plan になります。
OR union plan は常に小さいとは限りません。!=、notIn、isNotNull、contains のような branch は full field-index scan を作ることがあります。union plan は正しいですが、多くの index entry を読む場合があります。
prefix index は通常 field index とは別の index です。string-like field 専用で、findBy<&Field>() には使われず、等価検索や数値範囲用の通常 index を置き換えるものでもありません。対象 string field の write / delete / update では追加 maintenance が発生します。
通常 index も prefix index も、既存行へ自動 backfill されません。table を開いた直後、対象行を書き込む前に登録してください。