//! Applied migrations are immutable: once a migration ships, its bytes are frozen. //! //! `sqlx::migrate!` records each file's checksum in `_sqlx_migrations` when it runs //! and refuses to boot if a previously-applied file no longer hashes the same. The //! hash covers raw file bytes, so comments count. Editing a shipped migration //! therefore bricks every install that already ran it, while leaving fresh installs //! and CI perfectly green: they have no recorded checksum to contradict. That //! asymmetry is why this needs a test rather than review attention (see 534a7f6, //! a comment-only edit to 047 that crashed every existing install at launch). //! //! Checksums come from sqlx's own `Migrator`, so they track sqlx's definition of the //! hash rather than a reimplementation that could drift from it. //! //! Adding a migration: run the regenerate command below and commit the new line. //! Changing an existing line means changing a shipped migration, which is the thing //! this test exists to stop. Add a new migration instead. //! //! Regenerate: `UPDATE_MIGRATION_CHECKSUMS=1 cargo test -p goingson-db-sqlite` use std::collections::BTreeMap; use std::fmt::Write as _; use std::path::PathBuf; /// Environment variable that rewrites the manifest instead of asserting against it. const UPDATE_VAR: &str = "UPDATE_MIGRATION_CHECKSUMS"; fn manifest_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../migrations/sqlite/CHECKSUMS") } /// Checksums of the migrations currently on disk, keyed by version. fn current_checksums() -> BTreeMap { sqlx::migrate!("../../migrations/sqlite") .iter() .map(|m| { let hex = m.checksum.iter().fold(String::new(), |mut acc, b| { let _ = write!(acc, "{b:02x}"); acc }); (m.version, hex) }) .collect() } fn render(checksums: &BTreeMap) -> String { let mut out = String::from( "# sha384 of each migration file, as computed by sqlx.\n\ # Regenerate: UPDATE_MIGRATION_CHECKSUMS=1 cargo test -p goingson-db-sqlite\n\ # A changed line means a shipped migration was edited. See migration_checksum_tests.rs.\n", ); for (version, checksum) in checksums { let _ = writeln!(out, "{version:03} {checksum}"); } out } fn parse(raw: &str) -> BTreeMap { raw.lines() .map(str::trim) .filter(|line| !line.is_empty() && !line.starts_with('#')) .map(|line| { let (version, checksum) = line .split_once(char::is_whitespace) .unwrap_or_else(|| panic!("malformed CHECKSUMS line: {line:?}")); let version = version .trim_start_matches('0') .parse() .unwrap_or_else(|e| panic!("bad version in {line:?}: {e}")); (version, checksum.trim().to_string()) }) .collect() } #[test] fn shipped_migrations_are_unmodified() { let current = current_checksums(); let path = manifest_path(); if std::env::var_os(UPDATE_VAR).is_some() { std::fs::write(&path, render(¤t)).expect("write CHECKSUMS"); return; } let raw = std::fs::read_to_string(&path).expect("read CHECKSUMS"); let expected = parse(&raw); // Checked before the whole-map comparison: a modified migration is the case that // bricks installs, and it reads as an ordinary diff in the full assert output. let modified: Vec = current .iter() .filter(|(version, checksum)| { expected .get(*version) .is_some_and(|pinned| pinned != *checksum) }) .map(|(version, _)| *version) .collect(); assert!( modified.is_empty(), "migration(s) {modified:?} were edited after shipping.\n\ Every install that already ran them will abort at launch with \ \"migration N was previously applied but has been modified\".\n\ Revert the edit and put the change in a new migration. If these \ migrations have genuinely never shipped, regenerate with \ {UPDATE_VAR}=1." ); assert_eq!( expected, current, "migration set changed. New migrations are expected; regenerate with \ {UPDATE_VAR}=1 and commit the result. A missing version means a \ migration file was deleted, which breaks installs that ran it." ); }