Native の高レベル API は、AkkEngine の上にある C++ 向けの薄い層です。別のストレージエンジンを作るのではなく、決定的なテーブル接頭辞を作り、構造体を BinPack でエンコードし、行 ID のメタデータを管理し、永続化はバイト列指向のエンジンへ委譲します。
実行時の部品
Section titled “実行時の部品”| 部品 | 役割 |
|---|---|
AkkaraDB | engine::AkkEngine を所有し、table<&T::id>() とスキーマ登録を提供します。 |
PackedTable<PrimaryKeyPtr> | 型付きの操作を、エンジンキー、値、スキャン、メタデータ書き込みへ変換します。 |
BinPack | 主キー、エンティティ、インデックス値、参照、optional 値、map、入れ子の構造体をエンコード / デコードします。 |
AKKARADB_ENTITY | C++ 構造体の RefTraits とクエリ用フィールドを登録します。 |
Ref<T> | キー、行 ID、読み込み済みエンティティを保持し、テーブル接続情報を通して解決します。 |
Schema | テーブルホルダを登録し、フックによる外部キー動作を設定します。 |
AkkaraDB::open() は高レベル用のオプションから engine::AkkEngineOptions を作ります。ラッパーはエンジンを隠しません。db->engine() から下層のエンジンに触れられるため、PackedTable が公開していない低レベル操作も必要に応じて使えます。
| 層 | そこで行うこと |
|---|---|
| アプリケーションの C++ | AkkaraDB、PackedTable、クエリ式、参照、結合、スキーマフックを呼びます。 |
| Native 高レベル API | 構造体をエンコードし、テーブル / インデックス / メタデータのキーを作り、行 ID の mapping を管理し、フックを実行します。 |
AkkEngine | バイト列、WAL レコード、MemTable、SSTable、Blob、VersionLog のデータ、必要なら API サーバーやクラスタ部品を扱います。 |
高レベル API の永続性、コンパクション、Blob、VersionLog、API サーバー、クラスタの挙動は、設定された AkkEngine から引き継がれます。
テーブル名前空間
Section titled “テーブル名前空間”各テーブル名は 8 バイトの接頭辞に変換されます。行は次の形で保存されます。
[tablePrefix][encodedPrimaryKey]テーブルは、テーブル名から派生したメタデータ接頭辞も所有します。
| 接頭辞の用途 | 使い道 |
|---|---|
| テーブル行 | エンコード済みのエンティティ値。 |
idx:<field> | 登録済みフィールドのセカンダリインデックス項目。 |
pk2row | 主キーから安定した行 ID への対応表。 |
row2pk | 安定した行 ID から現在の主キーへの対応表。 |
nextrow | 次の行 ID を割り当てるためのカウンタ。 |
テーブル名は物理的なストレージレイアウトの一部です。テーブル名を変えると、API は別のキー範囲を見ることになります。
主キーのエンコード
Section titled “主キーのエンコード”主キー型は、PackedTable に渡したメンバポインタから推論されます。
auto users = db->table<&User::id>("users");数値キーは、辞書順のバイト比較が数値順と一致するようにエンコードされます。signed integer は sign bit を反転してから big-endian bytes にし、unsigned integer は big-endian で書きます。それ以外の対応済み値は BinPack のエンコードを使います。エンコード済み主キーは、行キーではテーブル接頭辞に、行 ID メタデータではメタデータ接頭辞に追加されます。
インデックス対象フィールドのエンコードも、順序付きフィールド型では同じ考え方です。整数と浮動小数点値は並び順を保つバイト列へ変換され、Immutable<T> は包んでいる値、Ref<T> は参照先の行 ID をインデックス化します。それ以外のフィールド値は BinPack を使います。
書き込み経路
Section titled “書き込み経路”put(entity) は、下層エンジンへの書き込みの前後で次の処理を行います。
- 一時バッファをリセットする。
entity.*PrimaryKeyPtrから主キーを取り出す。- テーブル行キーを作る。
- フックやインデックスが必要なら以前のエンティティを読む。
- 置換の場合、更新フックと変更不可フィールドの検査を実行する。
Ref<T>フィールドを接続し、dirty ref を保存する。- 登録済みの外部キーを検証する。
- エンティティを
BinPackでエンコードする。 AkkEngineにキー / 値を書き込む。- 安定した行 ID を割り当て、または再利用する。
- 行 ID のメタデータを更新する。
- セカンダリインデックス項目を再構築する。
これは 1 つの論理的なテーブル書き込みの周辺で、複数のエンジン操作を行います。高レベル API は AkkEngine の上に複数行トランザクション管理を追加しません。
読み取り経路
Section titled “読み取り経路”get(pk) は同じ名前空間付きキーを作り、AkkEngine から値のバイト列を読みます。getInto(pk, out) は既存オブジェクトに直接デコードします。
デコード後、テーブルは参照の接続情報を取り付け、immutable field を seal します。これにより、読み込まれた Ref<T> フィールドはテーブルスキーマ経由で解決でき、読み込まれた Immutable<T> フィールドは意図しない変更を拒否できます。
scanAll() はテーブル接頭辞の開始 / 終了範囲を作り、エンジンのスキャン結果を走査します。返ってきたキーはテーブル接頭辞と一致するか確認され、主キー部分は Entry::id に、値のバイト列は Entry::value にデコードされます。
scan(startPk, endPk) はエンコード済み主キーから正確なキー範囲を作ります。数値キーは並び順を保つため、数値範囲スキャンは自然な数値順になります。
セカンダリインデックス
Section titled “セカンダリインデックス”index<&Field>() はフィールド用の IndexDef を登録し、Index<FieldPtr> オブジェクトを返します。
インデックス項目には、候補となる主キーのバイト列を見つけるための情報が保存されます。Index::find(value) は、エンコード済みフィールド値のインデックス接頭辞をスキャンし、インデックスキーから主キーのバイト列を取り出し、主キーで現在のエンティティを読みます。
| 操作 | インデックスの保守 |
|---|---|
put() による挿入 | エンティティを書き込んだ後にインデックス項目を書きます。 |
put() による置換 | 古いインデックス項目を消してから新しい項目を書きます。 |
remove() | 行メタデータを消す前に古いインデックス項目を消します。 |
updatePrimaryKey() | 新しいインデックス項目を書き、古い主キー項目を消します。 |
インデックスの自動バックフィルはありません。既存行をインデックスに反映したい場合は、アプリケーション側で行を書き直します。
クエリモデル
Section titled “クエリモデル”C++ のクエリ API は、演算子のオーバーロードとフィールドプロキシで式の型を組み立てます。
profiles.query([](auto profile) { return profile.email == "a@example.test" && profile.age >= 18;});AKKARADB_QUERYABLE はエンティティ用の akkaradbQueryProxy() オーバーロードを作ります。query() は述語にそのプロキシを渡し、返ってきた式ツリーを QueryView に保持します。
対応している式は次の通りです。
| 式 | 内部の形 |
|---|---|
==, !=, <, <=, >, >= | Compare<Op, L, R> |
&&, ` | |
in, notIn | リスト内に含まれるかどうかの比較。 |
startsWith, contains, like | 文字列向けの比較。 |
isNull, isNotNull | optional / null の確認。 |
field<&Nested::x>() | 入れ子フィールドへのパス。 |
get(key) / mapGet(key) | map からの値取得式。 |
QueryView::begin() は PackedTable に QueryPlan を作らせます。計画は次のどちらかを選びます。
| 取得元 | 挙動 |
|---|---|
TABLE | テーブルのキー範囲をスキャンし、各行に式を評価します。 |
INDEX | 1 つ以上のインデックス範囲をスキャンし、候補行を主キーで読み、必要なら重複排除してから完全な式を評価します。 |
計画器は、登録済みインデックスを使える述語を探します。返される行は常に完全な式で絞り込まれるため、インデックスは候補集合を狭める役割です。
計画器は AND 式の中を再帰的に探し、使えるインデックス付き述語を score で選びます。等価範囲、数値の順序付き範囲、複数の IN 範囲、optional フィールドの null 範囲、startsWith や単純な like("prefix%") 向けの prefix index 範囲、または !=、notIn、isNotNull、contains のようなフィールドインデックス全体の scan を作れます。OR は両辺が index 化できる場合だけ、複数の index 範囲を union して候補主キーを重複除去します。片側でも index 化できない場合は、取りこぼしを避けるため table scan に fallback します。
行 ID メタデータ
Section titled “行 ID メタデータ”各テーブルは、見えている主キーとは別に安定した行 ID を持ちます。
| メタデータ | 目的 |
|---|---|
pk2row | 主キーから安定した行 ID を解決します。 |
row2pk | 行 ID から現在の主キーを解決します。 |
nextrow | 次の行 ID を割り当てます。 |
updatePrimaryKey() は行 ID を保ったまま対応表を書き換えます。これにより Ref<T> は主キー変更をまたいで解決できます。
Ref<T> は、既知の主キー、既知の行 ID、読み込み済みの値、dirty な値を保持できます。解決は RefBinding<T> を通して遅延実行されます。
| 状態 | 意味 |
|---|---|
| キーが分かっている | id() をテーブル検索なしで返せます。 |
| 行 ID が分かっている | 行 ID メタデータ経由で解決できます。 |
| 読み込み済み | operator-> と operator* がエンティティを返せます。 |
| dirty | owner の書き込み時に参照先エンティティを保存する必要があります。 |
| 接続済み | 解決用のテーブル接続情報があります。 |
テーブルがエンティティをデコードすると、参照の接続情報を取り付けます。テーブルがエンティティを書き込むと、owner をエンコードする前に dirty ref を保存します。
結合ヘルパーは型付きの読み取りビューであり、専用の結合エンジンではありません。
| 結合形式 | 実行の形 |
|---|---|
join<&RefField>(right) | 左テーブルをスキャンし、右テーブルを行 ID で検索して参照を解決します。 |
join<&LeftField, &RightPk>(right) | 左テーブルをスキャンし、右テーブルを主キーで検索します。 |
join<&LeftField, &RightField>(right) | 左テーブルをスキャンし、一致するフィールドを探すために右テーブルをスキャンします。 |
各結合行は、左側の Entry とデコード済みの右側エンティティを持ちます。where()、first()、any()、count()、toVector() はビュー上で評価されます。
スキーマフック
Section titled “スキーマフック”AkkaraDB::Schema は C++ のエンティティ型をキーにしてテーブルホルダを保持します。また、登録済みテーブル間で Ref<T> フィールドを解決するための接続情報も提供します。
foreignKey() は参照元テーブルまたは参照先テーブルにフックを追加して動作します。
| フック対象 | 目的 |
|---|---|
参照元の put() | 存在しない参照先への参照を拒否します。 |
参照先の remove() | 削除時に Restrict、Cascade、SetNull を適用します。 |
| 参照先の主キー更新 | キー移動時に Restrict、Cascade、SetNull を適用します。 |
外部キー動作は、参照している行を見つけるために参照元テーブルをスキャンします。大きいテーブルで高速な参照検査が必要な場合は、明示的なインデックスやアプリケーション側の検索構造を用意します。
一貫性の境界
Section titled “一貫性の境界”高レベル API はエンジンの上にある薄い層です。1 つの PackedTable 操作でも、エンティティ、行 ID メタデータ、インデックスキーを複数回書く場合があります。スキーマ動作は複数行を書く場合があります。結合はスキャンと検索で現在の状態を読みます。
複数行にまたがる all-or-nothing な不変条件が必要な場合は、テーブル API の上に明示的な調整層を作るか、不変条件を 1 つの保存値の中へ閉じ込める設計にします。
バックアップと移行
Section titled “バックアップと移行”バックアップでは、テーブル行とテーブルメタデータを一緒に保持する必要があります。
| データ | 理由 |
|---|---|
| エンジンファイル | 実際の保存行、WAL / SST / Blob の状態、低レベルのメタデータを含みます。 |
| 行 ID の対応表 | Ref<T>、getByRowId()、updatePrimaryKey() の安定性に必要です。 |
| インデックス項目 | Index::find() とインデックス付きクエリ計画に必要です。 |
| VersionLog のデータ | エンジンの過去時点読み取りやロールバックを使う設定なら必要です。 |
移行用の道具では、テーブル名、主キー型、インデックス対象フィールド、構造体のエンコードをストレージレイアウトの決定として扱います。
BinPack は集約型フィールドを宣言順でエンコードするため、フィールドの追加、削除、並べ替え、型変更は自動互換なスキーマ変更ではありません。明示的な書き換え、またはバージョン付きエンティティ形式として設計します。
低レベル API に降りる場合
Section titled “低レベル API に降りる場合”バイト列のレイアウトを完全に制御したい場合、独自の通信方式やサーバー層を作る場合、構造体以外のエンコードを使う場合、大量データのストレージを直接修復する場合、エンジン内部を直接検証する場合は engine::AkkEngine を使います。型付き構造体、テーブル単位のインデックス、クエリ式、参照、スキーマフックが主な価値なら高レベル API を使います。