Rustで自作データベースを作る その27: verifyのdetailsを追加して、mismatchの中身を限定的に確認する
今日出勤するとき土砂降りで大変でした。
さて、前回は restore 後の self-check / verify を導入して、
verify_table_importverify_database_importdb-cli verify-tabledb-cli verify-db
を追加し、import 結果が期待どおりかを read-only に確認できるようにしました。
ここまでで、restore の流れはかなり整ってきています。
- export する
- preflight する
- import する
- execution report を見る
- verify する
ただ、verify を入れると次に自然な欲求が出てきます。
mismatch なのは分かった。では、何がズレているのかをもう少し知りたい
たとえば今ほしい情報は、いきなり full diff ではありません。
- どの primary key が不足しているのか
- どの primary key が余剰なのか
- mismatch の中身を少しだけ見たい
そこで今回は、verify の detail mode を追加します。
今回のテーマはこれです。
full diff には進まず、まずは PK 差分だけを限定的に見える化する
なぜ full diff ではなく detail mode なのか
ここでいきなり full diff engine に行くこともできます。
でも、それはかなり重いです。
- row ごとの差分表示
- 列ごとの差分表示
- 巨大 table での paging
- 値の pretty print
- mismatch のグルーピング
このあたりは、やり始めると一気に別フェーズになります。
一方で、今ほしいのはもっと小さい確認です。
mismatch が起きた時に、どの PK が足りないのか / 多いのかを知りたい
この 1 段だけでも、restore 後の調査はかなり進みます。
だから今回は、
- default verify は軽いまま
- 必要な時だけ
--detailsで掘る
という方針にします。
今回の全体像
まず構造を整理すると、verify はこうなります。
verify
├─ summary mode (default)
└─ detail mode (--details)
役割はこうです。
summary mode
- schema が一致するか
- row count が一致するか
- PK 集合が一致するか
- status が success / mismatch / failed のどれか
detail mode
- 上記に加えて
- missing PK
- unexpected PK
を限定的に返す
つまり detail mode は、summary mode の上に載る補助情報です。
今回追加する details の内容
最小なら、detail mode で返したいのはこれで十分です。
PrimaryKeyMismatchDetails
-------------------------
missing_primary_keys
unexpected_primary_keys
missing_count
unexpected_count
truncated
ここでの意味は単純です。
-
missing_primary_keys
export JSON にはあるが target に無い PK -
unexpected_primary_keys
target にはあるが export JSON には無い PK
この 2 つが見えるだけで、かなり調査しやすくなります。
今回も source of truth は export JSON
verify の detail mode でも、基準は変わりません。
export JSON
|
| expected
v
compare
^
|
target DB
つまり details も、
- export 側の PK 集合
- target 側の PK 集合
の差分から作ります。
この一貫性が大事です。
default verify は軽いままにする
ここがとても重要です。
もし details を常に集めるようにすると、
- mismatch でなくても余計な情報を作る
- 出力が重くなる
- 大きい table で扱いづらい
- summary verify の良さが消える
だから今回は、details は opt-in にします。
db-cli verify-table --db ./target-db --in users.json
db-cli verify-table --db ./target-db --in users.json --details
意味は明確です。
- 普段は summary
- 必要な時だけ detail
この切り方がかなり良いです。
detail mode でも full row diff はまだやらない
ここを曖昧にしないのが大事です。
今回 details で返すのは PK 差分だけ です。
つまり、まだやらないのは次です。
- row 内容の value diff
- 列ごとの差分
- 同じ PK で値だけ違うケースの詳細表示
- full mismatch dump
今回はそこへ行きません。
理由は簡単で、
PK 差分だけでも mismatch のかなり大きな部分が見える からです。
detail mode の report 形
たとえば TableVerifyReport に optional な details を持たせるのが自然です。
pub struct TableVerifyReport {
pub table_name: String,
pub expected_row_count: usize,
pub actual_row_count: usize,
pub schema_matches: bool,
pub row_count_matches: bool,
pub pk_set_matches: bool,
pub status: VerifyStatus,
pub mismatch_summary: Option<String>,
pub details: Option<PrimaryKeyMismatchDetails>,
}
この構造の良いところは、
- default では
details = None -
--detailsの時だけ埋まる
という opt-in が型で表現できることです。
件数制限を最初から入れておく
ここも重要です。
巨大な mismatch がある時に、PK を全部返すと report が膨らみすぎます。
たとえば、
- missing 10,000 件
- unexpected 8,000 件
みたいなケースで、全部出すのはつらいです。
だから今回は最初から上限を入れます。
たとえば、
- missing 最大 20 件
- unexpected 最大 20 件
で十分です。
そして report にこう入れます。
missing_count = 130
unexpected_count = 45
truncated = true
これなら、
- 件数の全体像
- 一部サンプル
- 打ち切りの事実
が全部分かります。
verify-table --details の text 出力イメージ
text では、たとえばこういう形が自然です。
status: mismatch
table: users
schema_matches: yes
row_count_matches: no
expected_rows: 120
actual_rows: 119
pk_set_matches: no
mismatch: row count and primary key set differ
missing_primary_keys (2):
- {"type":"uint64","value":5}
- {"type":"uint64","value":9}
unexpected_primary_keys (1):
- {"type":"uint64","value":200}
これだけでかなり useful です。
truncation がある時の見せ方
巨大 mismatch では、truncate をちゃんと見せる必要があります。
たとえばこうです。
missing_primary_keys: showing 20 of 130 (truncated)
unexpected_primary_keys: showing 20 of 45 (truncated)
この表示があると、ユーザーは
- 「全部は出ていない」
- 「でもどんなズレかの傾向は分かる」
という理解ができます。
ここを silent truncate にしないのが大事です。
JSON 出力では details をそのまま返せる
JSON だとさらに扱いやすいです。
{
"status": "mismatch",
"table_name": "users",
"expected_row_count": 120,
"actual_row_count": 119,
"schema_matches": true,
"row_count_matches": false,
"pk_set_matches": false,
"mismatch_summary": "row count and primary key set differ",
"details": {
"missing_primary_keys": [
{"type":"uint64","value":5},
{"type":"uint64","value":9}
],
"unexpected_primary_keys": [
{"type":"uint64","value":200}
],
"missing_count": 2,
"unexpected_count": 1,
"truncated": false
}
}
CI や別ツールから使うなら、かなり便利です。
compare path は二重実装しない方がいい
ここも設計上かなり大事です。
summary verify では、すでに PK 集合 mismatch を判定しています。
なら detail mode は、その差分計算を再利用すべきです。
つまり構造はこうです。
PK 集合比較
├─ summary: matches / mismatch
└─ details: missing / unexpected の一部抜粋
こうしておくと、summary と details の判定がズレにくいです。
これはかなり重要です。
details があると mismatch と failure の違いもさらに分かりやすい
前回、status は 3 段でした。
- success
- mismatch
- failed
detail mode が入ると、この違いがさらに見やすくなります。
mismatch
比較自体は完了している。だから details を返せる
failed
比較自体が完了していない。だから details は無理に返さない
たとえば dirty target は failed です。
status: failed
failure_stage: precondition
error: target database is dirty
ここでは missing/unexpected PK を出す必要はありません。
verify_table_import の内部イメージ
概念的にはこうなります。
pub fn verify_table_import_with_options(
&self,
input: impl Read,
options: VerifyOptions,
) -> Result<TableVerifyReport, VerifyError> {
let export = parse_table_export(input)?;
validate_table_export(&export)?;
self.ensure_database_is_clean()?;
let actual = self.describe_table(&export.table.name)?
.ok_or_else(|| VerifyError::missing_table(&export.table.name))?;
let expected_pk = collect_export_pk_set(&export)?;
let actual_pk = collect_actual_pk_set(&export.table.name, self)?;
let missing = expected_pk.difference(&actual_pk);
let unexpected = actual_pk.difference(&expected_pk);
let details = if options.include_details {
Some(build_pk_mismatch_details(missing, unexpected, options.limit))
} else {
None
};
Ok(TableVerifyReport {
table_name: export.table.name.clone(),
expected_row_count: export.rows.len(),
actual_row_count: actual_pk.len(),
schema_matches: compare_schema(&export.table.schema, &actual.schema),
row_count_matches: export.rows.len() == actual_pk.len(),
pk_set_matches: missing.is_empty() && unexpected.is_empty(),
status: ...,
mismatch_summary: ...,
details,
})
}
本質は、差分集合をどう summary と details へ分けるかです。
verify-db --details も自然に載る
database verify でも同じです。
- per-table で details を持つ
- 全体 report ではそれを束ねる
たとえば text 出力はこうです。
status: mismatch
tables: 2
expected_rows: 420
actual_rows: 419
- users: success
- orders: mismatch
orders missing_primary_keys (1):
- {"type":"uint64","value":101}
かなり実用的です。
end-to-end のサンプルはかなり映える
今回の記事で見せやすいのは、わざと mismatch を作って details を見る流れです。
まず source を作ります。
let mut source = MultiTableDb::open("./source-db")?;
source.create_table(users_schema())?;
{
let mut users = source.open_table("users")?;
users.insert(user_row(1, "alice"))?;
users.insert(user_row(2, "bob"))?;
users.insert(user_row(3, "carol"))?;
}
export します。
db-cli export-table users --db ./source-db --out users.json
target に import したあと、何らかの理由で 1 行欠けた状態を想定します。
そのうえで verify します。
db-cli verify-table --db ./target-db --in users.json --details
出力イメージ:
status: mismatch
table: users
schema_matches: yes
row_count_matches: no
expected_rows: 3
actual_rows: 2
pk_set_matches: no
mismatch: row count and primary key set differ
missing_primary_keys (1):
- {"type":"uint64","value":3}
かなり分かりやすいです。
verify の流れがさらに良くなる
ここまで来ると restore workflow はかなり綺麗です。
export
|
v
preflight
|
v
import
|
v
execution report
|
v
verify
|
v
verify --details
段階的に深く見られます。
- まず軽く確認
- 実行
- 結果確認
- 必要なら mismatch をもう少し掘る
この段差の付け方がかなり良いです。
今回まだやらないこと
ここは明確に書いた方が記事として締まります。
今回 やらない のは、たとえば次です。
- full row diff
- value-level diff
- auto repair
- checksum tree
- overwrite / merge import
- online verification
- interactive mismatch explorer
なぜ今はやらないのか。
それは、今回必要なのが
mismatch の最初の掘り下げ
だからです。
つまり今欲しいのは、
- missing PK が分かる
- unexpected PK が分かる
- でも report は暴れすぎない
- default verify は軽いまま
という最小契約です。
今回の責務分離
今回の detail mode で見えてくる責務分離は、かなりきれいです。
summary verify
└─ 一致判定の要約
detail verify
└─ PK 差分の追加観測
CLI
└─ --details の有無で表示を切り替える
compare core
└─ 同じ差分計算を summary と details で共有する
mutation path
└─ いっさい触らない
特に大事なのは、
- detail mode が full diff の代わりをしようとしない
- summary と details が同じ比較基盤を使う
- default verify の軽さを壊さない
という点です。
所感
今回のタスクは、かなり「実際に使う時の気持ちよさ」に効く回だと思います。
verify があるだけでも良いです。
でも mismatch が出た時に、次に欲しいのは結局これです。
で、何が足りないの?
そこに対して、full diff へ一気に行かず、
まず PK 差分だけを opt-in で見せるのはかなり筋が良いです。
- 情報量は増える
- でも責務は増やしすぎない
- mismatch 調査が一段楽になる
かなりバランスが良いと思います。
まとめ
今回は、verify の detail mode を導入して、
verify-table --detailsverify-db --details- missing / unexpected primary key の限定表示
- truncation 付き detail report
を追加し、mismatch の中身を少しだけ深掘りできるようにするタスクでした。
要するに、T-026 で作った minimal verify の次に、
full diff に行く前の一段深い観測を足す回です。
これで自作DBは、単に export/import/verify できるだけでなく、
ズレた時にそのズレを少し掘って確認できる小さな DB システム にさらに近づきます。
次は、この基盤の上に row value diff ではなく、まずは schema mismatch の detail 表示 を足すのが自然なテーマになります。
PK 差分が見えるようになったので、次は「schema が違う時にどこが違うか」を限定的に見せる方向へ進めると、さらに調査しやすくなりそうです。
次回もぜひご覧ください!
Discussion