_sqlx_migrations のレコードに入る checksum を自前で計算するスクリプト

2025/01/29に公開


こいつを突破したい

はじめに

sqlx-cli でのマイグレーションを管理している _sqlx_migrations テーブルを確認すると、こんなテーブル構造をしています。


_sqlx_migrations のテーブル構造

5 番目のカラムに checksum がありますね。
ここには 6ACD14A420A076E4F59F8D11061CF2ABE392DDE34B32A62E83E10B6C0160C9CC3B98D2381E7465C9EB37E1CC72D8EB69 といった値が入っています。

このチェックサムの値がどのように計算されているかを調査したので、メモがてら残してます。

なんで調査したか

DB を sqlx-cli でマイグレーションしたあと、そのマイグレーションに使った SQL をちょっとだけ編集したくなりました。テーブル構造の変更みたいなものではなく、コメント追加とかほんとに些細な修正です。
マイグレーションファイル名は変えてないので、sqlx migrate run ではこれがスルーされればいいなと思っていました。

でも sqlx-cli は賢いので、「error: migration XXXXXXXXXXXXXX was previously applied but has been modified」 というエラーを出します。正攻法ではないですが、これをどうにか回避したくなったというわけです…

結論

file_path を書き換えてこのスクリプトを実行すれば、対象マイグレーションファイルのチェックサムが出力されます。その値で該当レコードを更新すればエラーを突破できました。

use sha2::{Digest, Sha384};
use std::fmt::Write;
use std::fs;

/// 10 進数のバイト列を 16 進数文字列に変換
/// @see https://github.com/launchbadge/sqlx/blob/546ec960a955959a649dda29180c3de52be0e181/sqlx-cli/src/migrate.rs#L187
fn short_checksum(checksum: &[u8]) -> String {
    let mut s = String::with_capacity(checksum.len() * 2);
    for b in checksum {
        write!(&mut s, "{b:02x?}").expect("should not fail to write to str");
    }
    s.to_uppercase()
}

fn main() -> std::io::Result<()> {
    // ファイルパス指定
    let file_path = "migrations/XXXXXXXXXXXXXX_init_tables.up.sql";

    // ファイル読み込み
    // @see https://github.com/launchbadge/sqlx/blob/546ec960a955959a649dda29180c3de52be0e181/sqlx-core/src/migrate/source.rs#L118
    let sql = fs::read_to_string(file_path)?;

    // チェックサムの計算
    // @see https://github.com/launchbadge/sqlx/blob/546ec960a955959a649dda29180c3de52be0e181/sqlx-core/src/migrate/migration.rs#L25
    let checksum = Vec::from(Sha384::digest(sql.as_bytes()).as_slice());
    let checksum_row = short_checksum(&checksum);

    // 出力
    println!("checksum: {}", checksum_row);

    Ok(())
}

調査

エラーの発生箇所

エラー「error: migration XXXXXXXXXXXXXX was previously applied but has been modified」を出している場所は下記でした。migrations 配下のファイルから計算したチェックサムと _sqlx_migrations のレコードにあるチェックサムをそれぞれ比較して、一致しなければ MigrateError::VersionMismatch を返しています。

https://github.com/launchbadge/sqlx/blob/546ec960a955959a649dda29180c3de52be0e181/sqlx-cli/src/migrate.rs#L324-L326
https://github.com/launchbadge/sqlx/blob/546ec960a955959a649dda29180c3de52be0e181/sqlx-core/src/migrate/error.rs#L18-L19

つまりチェックサムを一致させればこれを回避できるわけですね。
というわけで、書き換えたマイグレーションファイルのチェックサムを計算して、その値で _sqlx_migrations のレコードを更新すれば大丈夫そうです。

チェックサムの計算箇所

レコードの元になっていそうな構造体 Migrationnew() するときに、チェックサムを計算する処理がありました。これを真似ればよさそうです。
https://github.com/launchbadge/sqlx/blob/546ec960a955959a649dda29180c3de52be0e181/sqlx-core/src/migrate/migration.rs#L25

もう少しコードを追うと、入力はマイグレーションファイルを読み込んだ String だということもわかったので、以下のようにして実行してみます。

    // ファイルパス指定
    let file_path = "migrations/20241210054743_init_tables.up.sql";

    // ファイル読み込み
    let sql = fs::read_to_string(file_path)?;

    // チェックサムの計算
    let checksum = Vec::from(Sha384::digest(sql.as_bytes()).as_slice());

    // 出力
    println!("{:?}", checksum);
    // Output: [106, 160, 20, 164, 32, 190, 118, 244, 227, 143, 141, 17, 6, 28, 242, 151, 227, 146, 221, 227, 75, 18, 166, 46, 141, 225, 11, 108, 1, 96, 201, 204, 59, 54, 210, 56, 30, 116, 55, 211, 235, 55, 225, 204, 124, 216, 225, 105]

ハッシュ値のバイト列である Vec<u8> の配列が出力されました。最初に例示したような String 型にしたいところです。

チェックサムの変換箇所

10 進数のバイト列を 16 進数文字列に変換する箇所は下記にありました。

https://github.com/launchbadge/sqlx/blob/546ec960a955959a649dda29180c3de52be0e181/sqlx-cli/src/migrate.rs#L187-L193

配列をひとつずつ走査して、{b:02x?} の部分で変換した文字列を s に詰めていっています。これを真似すると欲しかったあの文字列 6ACD14A420A... が出力されました。めでたし〜

おわりに

いったん誤魔化すことができたのでよかったです。

sqlx くんこれくらい許してよ!とも思いましたが、マイグレーション済みのファイルをいじるのがそもそもよくないよなと反省しました。懺悔!

レバテック開発部

Discussion