コンテンツにスキップ

高レベル API のアーキテクチャ

Native の高レベル API は、AkkEngine の上にある C++ 向けの薄い層です。別のストレージエンジンを作るのではなく、決定的なテーブル接頭辞を作り、構造体を BinPack でエンコードし、行 ID のメタデータを管理し、永続化はバイト列指向のエンジンへ委譲します。

部品役割
AkkaraDBengine::AkkEngine を所有し、table<&T::id>() とスキーマ登録を提供します。
PackedTable<PrimaryKeyPtr>型付きの操作を、エンジンキー、値、スキャン、メタデータ書き込みへ変換します。
BinPack主キー、エンティティ、インデックス値、参照、optional 値、map、入れ子の構造体をエンコード / デコードします。
AKKARADB_ENTITYC++ 構造体の RefTraits とクエリ用フィールドを登録します。
Ref<T>キー、行 ID、読み込み済みエンティティを保持し、テーブル接続情報を通して解決します。
Schemaテーブルホルダを登録し、フックによる外部キー動作を設定します。

AkkaraDB::open() は高レベル用のオプションから engine::AkkEngineOptions を作ります。ラッパーはエンジンを隠しません。db->engine() から下層のエンジンに触れられるため、PackedTable が公開していない低レベル操作も必要に応じて使えます。

そこで行うこと
アプリケーションの C++AkkaraDBPackedTable、クエリ式、参照、結合、スキーマフックを呼びます。
Native 高レベル API構造体をエンコードし、テーブル / インデックス / メタデータのキーを作り、行 ID の mapping を管理し、フックを実行します。
AkkEngineバイト列、WAL レコード、MemTable、SSTable、Blob、VersionLog のデータ、必要なら API サーバーやクラスタ部品を扱います。

高レベル API の永続性、コンパクション、Blob、VersionLog、API サーバー、クラスタの挙動は、設定された AkkEngine から引き継がれます。

各テーブル名は 8 バイトの接頭辞に変換されます。行は次の形で保存されます。

[tablePrefix][encodedPrimaryKey]

テーブルは、テーブル名から派生したメタデータ接頭辞も所有します。

接頭辞の用途使い道
テーブル行エンコード済みのエンティティ値。
idx:<field>登録済みフィールドのセカンダリインデックス項目。
pk2row主キーから安定した行 ID への対応表。
row2pk安定した行 ID から現在の主キーへの対応表。
nextrow次の行 ID を割り当てるためのカウンタ。

テーブル名は物理的なストレージレイアウトの一部です。テーブル名を変えると、API は別のキー範囲を見ることになります。

主キー型は、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 を使います。

put(entity) は、下層エンジンへの書き込みの前後で次の処理を行います。

  1. 一時バッファをリセットする。
  2. entity.*PrimaryKeyPtr から主キーを取り出す。
  3. テーブル行キーを作る。
  4. フックやインデックスが必要なら以前のエンティティを読む。
  5. 置換の場合、更新フックと変更不可フィールドの検査を実行する。
  6. Ref<T> フィールドを接続し、dirty ref を保存する。
  7. 登録済みの外部キーを検証する。
  8. エンティティを BinPack でエンコードする。
  9. AkkEngine にキー / 値を書き込む。
  10. 安定した行 ID を割り当て、または再利用する。
  11. 行 ID のメタデータを更新する。
  12. セカンダリインデックス項目を再構築する。

これは 1 つの論理的なテーブル書き込みの周辺で、複数のエンジン操作を行います。高レベル API は AkkEngine の上に複数行トランザクション管理を追加しません。

get(pk) は同じ名前空間付きキーを作り、AkkEngine から値のバイト列を読みます。getInto(pk, out) は既存オブジェクトに直接デコードします。

デコード後、テーブルは参照の接続情報を取り付け、immutable field を seal します。これにより、読み込まれた Ref<T> フィールドはテーブルスキーマ経由で解決でき、読み込まれた Immutable<T> フィールドは意図しない変更を拒否できます。

scanAll() はテーブル接頭辞の開始 / 終了範囲を作り、エンジンのスキャン結果を走査します。返ってきたキーはテーブル接頭辞と一致するか確認され、主キー部分は Entry::id に、値のバイト列は Entry::value にデコードされます。

scan(startPk, endPk) はエンコード済み主キーから正確なキー範囲を作ります。数値キーは並び順を保つため、数値範囲スキャンは自然な数値順になります。

index<&Field>() はフィールド用の IndexDef を登録し、Index<FieldPtr> オブジェクトを返します。

インデックス項目には、候補となる主キーのバイト列を見つけるための情報が保存されます。Index::find(value) は、エンコード済みフィールド値のインデックス接頭辞をスキャンし、インデックスキーから主キーのバイト列を取り出し、主キーで現在のエンティティを読みます。

操作インデックスの保守
put() による挿入エンティティを書き込んだ後にインデックス項目を書きます。
put() による置換古いインデックス項目を消してから新しい項目を書きます。
remove()行メタデータを消す前に古いインデックス項目を消します。
updatePrimaryKey()新しいインデックス項目を書き、古い主キー項目を消します。

インデックスの自動バックフィルはありません。既存行をインデックスに反映したい場合は、アプリケーション側で行を書き直します。

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, isNotNulloptional / null の確認。
field<&Nested::x>()入れ子フィールドへのパス。
get(key) / mapGet(key)map からの値取得式。

QueryView::begin()PackedTableQueryPlan を作らせます。計画は次のどちらかを選びます。

取得元挙動
TABLEテーブルのキー範囲をスキャンし、各行に式を評価します。
INDEX1 つ以上のインデックス範囲をスキャンし、候補行を主キーで読み、必要なら重複排除してから完全な式を評価します。

計画器は、登録済みインデックスを使える述語を探します。返される行は常に完全な式で絞り込まれるため、インデックスは候補集合を狭める役割です。

計画器は AND 式の中を再帰的に探し、使えるインデックス付き述語を score で選びます。等価範囲、数値の順序付き範囲、複数の IN 範囲、optional フィールドの null 範囲、startsWith や単純な like("prefix%") 向けの prefix index 範囲、または !=notInisNotNullcontains のようなフィールドインデックス全体の scan を作れます。OR は両辺が index 化できる場合だけ、複数の index 範囲を union して候補主キーを重複除去します。片側でも index 化できない場合は、取りこぼしを避けるため table scan に fallback します。

各テーブルは、見えている主キーとは別に安定した行 ID を持ちます。

メタデータ目的
pk2row主キーから安定した行 ID を解決します。
row2pk行 ID から現在の主キーを解決します。
nextrow次の行 ID を割り当てます。

updatePrimaryKey() は行 ID を保ったまま対応表を書き換えます。これにより Ref<T> は主キー変更をまたいで解決できます。

Ref<T> は、既知の主キー、既知の行 ID、読み込み済みの値、dirty な値を保持できます。解決は RefBinding<T> を通して遅延実行されます。

状態意味
キーが分かっているid() をテーブル検索なしで返せます。
行 ID が分かっている行 ID メタデータ経由で解決できます。
読み込み済みoperator->operator* がエンティティを返せます。
dirtyowner の書き込み時に参照先エンティティを保存する必要があります。
接続済み解決用のテーブル接続情報があります。

テーブルがエンティティをデコードすると、参照の接続情報を取り付けます。テーブルがエンティティを書き込むと、owner をエンコードする前に dirty ref を保存します。

結合ヘルパーは型付きの読み取りビューであり、専用の結合エンジンではありません。

結合形式実行の形
join<&RefField>(right)左テーブルをスキャンし、右テーブルを行 ID で検索して参照を解決します。
join<&LeftField, &RightPk>(right)左テーブルをスキャンし、右テーブルを主キーで検索します。
join<&LeftField, &RightField>(right)左テーブルをスキャンし、一致するフィールドを探すために右テーブルをスキャンします。

各結合行は、左側の Entry とデコード済みの右側エンティティを持ちます。where()first()any()count()toVector() はビュー上で評価されます。

AkkaraDB::Schema は C++ のエンティティ型をキーにしてテーブルホルダを保持します。また、登録済みテーブル間で Ref<T> フィールドを解決するための接続情報も提供します。

foreignKey() は参照元テーブルまたは参照先テーブルにフックを追加して動作します。

フック対象目的
参照元の put()存在しない参照先への参照を拒否します。
参照先の remove()削除時に RestrictCascadeSetNull を適用します。
参照先の主キー更新キー移動時に RestrictCascadeSetNull を適用します。

外部キー動作は、参照している行を見つけるために参照元テーブルをスキャンします。大きいテーブルで高速な参照検査が必要な場合は、明示的なインデックスやアプリケーション側の検索構造を用意します。

高レベル API はエンジンの上にある薄い層です。1 つの PackedTable 操作でも、エンティティ、行 ID メタデータ、インデックスキーを複数回書く場合があります。スキーマ動作は複数行を書く場合があります。結合はスキャンと検索で現在の状態を読みます。

複数行にまたがる all-or-nothing な不変条件が必要な場合は、テーブル API の上に明示的な調整層を作るか、不変条件を 1 つの保存値の中へ閉じ込める設計にします。

バックアップでは、テーブル行とテーブルメタデータを一緒に保持する必要があります。

データ理由
エンジンファイル実際の保存行、WAL / SST / Blob の状態、低レベルのメタデータを含みます。
行 ID の対応表Ref<T>getByRowId()updatePrimaryKey() の安定性に必要です。
インデックス項目Index::find() とインデックス付きクエリ計画に必要です。
VersionLog のデータエンジンの過去時点読み取りやロールバックを使う設定なら必要です。

移行用の道具では、テーブル名、主キー型、インデックス対象フィールド、構造体のエンコードをストレージレイアウトの決定として扱います。

BinPack は集約型フィールドを宣言順でエンコードするため、フィールドの追加、削除、並べ替え、型変更は自動互換なスキーマ変更ではありません。明示的な書き換え、またはバージョン付きエンティティ形式として設計します。

バイト列のレイアウトを完全に制御したい場合、独自の通信方式やサーバー層を作る場合、構造体以外のエンコードを使う場合、大量データのストレージを直接修復する場合、エンジン内部を直接検証する場合は engine::AkkEngine を使います。型付き構造体、テーブル単位のインデックス、クエリ式、参照、スキーマフックが主な価値なら高レベル API を使います。