🔍

Rustで自作データベースを作る その27: verifyのdetailsを追加して、mismatchの中身を限定的に確認する

に公開

今日出勤するとき土砂降りで大変でした。

さて、前回は restore 後の self-check / verify を導入して、

  • verify_table_import
  • verify_database_import
  • db-cli verify-table
  • db-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 --details
  • verify-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