From 350aa9f016e4ab8ae19205ba83ca425b9804d8c4 Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Tue, 4 Aug 2026 12:51:46 -0700 Subject: [PATCH] fix(db): keep migration versions unique --- crates/corpus-core/src/db.rs | 36 +++++++++++++++++++ ...tions.sql => 0012_merlin_observations.sql} | 0 2 files changed, 36 insertions(+) rename migrations/{0010_merlin_observations.sql => 0012_merlin_observations.sql} (100%) diff --git a/crates/corpus-core/src/db.rs b/crates/corpus-core/src/db.rs index 582d5f4..ae706c0 100644 --- a/crates/corpus-core/src/db.rs +++ b/crates/corpus-core/src/db.rs @@ -5,6 +5,9 @@ //! small pool (max 8) suitable for a single-node server; production //! deployments can raise this via a future config surface. //! +//! Each migration filename must have a unique numeric prefix because SQLx +//! persists that prefix as the primary-keyed migration version. +//! //! Call [`migrate`] once at process start before serving traffic. use crate::error::Result; @@ -25,3 +28,36 @@ pub async fn migrate(pool: &PgPool) -> Result<()> { sqlx::migrate!("../../migrations").run(pool).await?; Ok(()) } + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, fs, path::Path}; + + #[test] + fn migration_versions_are_unique() { + let migrations_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../migrations"); + let mut versions = BTreeMap::new(); + + for entry in fs::read_dir(&migrations_dir).expect("read migrations directory") { + let path = entry.expect("read migration directory entry").path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("sql") { + continue; + } + + let name = path + .file_name() + .and_then(|name| name.to_str()) + .expect("migration filename is valid UTF-8") + .to_owned(); + let (version, _) = name + .split_once('_') + .expect("migration filename has a numeric prefix"); + let version: i64 = version.parse().expect("migration prefix is numeric"); + + assert!( + versions.insert(version, name).is_none(), + "duplicate migration version {version}" + ); + } + } +} diff --git a/migrations/0010_merlin_observations.sql b/migrations/0012_merlin_observations.sql similarity index 100% rename from migrations/0010_merlin_observations.sql rename to migrations/0012_merlin_observations.sql