From 3f3d4e3ee24c15d3cc6925de286d883e3eb04801 Mon Sep 17 00:00:00 2001 From: Vasili Pascal Date: Fri, 16 Oct 2026 21:46:39 +0300 Subject: [PATCH] Give casbin_admin_template_update its own migration version 20261016120000 was used twice: baked_snapshot_server_type (2b9eb5fb, in dev since #291 and applied on dev) and casbin_admin_template_update (adb17792). sqlx keys _sqlx_migrations by version, so every fresh database failed with a duplicate key on _sqlx_migrations_pkey and the integration tests could not start the test app; a database that already applied one of them refuses the other at boot. casbin_admin_template_update moves to 20261016191200 (its commit time). It only inserts Casbin rules with ON CONFLICT DO NOTHING, so applying it under the new version is safe wherever it already ran. Regression test (red on dev): migration_boot_contract every_migration_version_is_used_once. migration_boot_contract 11 passed; admin_jwt, admin_notifications_bell, agent_command_flow (failing in CI) 21 passed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012TKZy6f5shPzM9WLdswH6f --- ...200_casbin_admin_template_update.down.sql} | 0 ...91200_casbin_admin_template_update.up.sql} | 0 tests/migration_boot_contract.rs | 39 ++++++++++++++++--- 3 files changed, 33 insertions(+), 6 deletions(-) rename migrations/{20261016120000_casbin_admin_template_update.down.sql => 20261016191200_casbin_admin_template_update.down.sql} (100%) rename migrations/{20261016120000_casbin_admin_template_update.up.sql => 20261016191200_casbin_admin_template_update.up.sql} (100%) diff --git a/migrations/20261016120000_casbin_admin_template_update.down.sql b/migrations/20261016191200_casbin_admin_template_update.down.sql similarity index 100% rename from migrations/20261016120000_casbin_admin_template_update.down.sql rename to migrations/20261016191200_casbin_admin_template_update.down.sql diff --git a/migrations/20261016120000_casbin_admin_template_update.up.sql b/migrations/20261016191200_casbin_admin_template_update.up.sql similarity index 100% rename from migrations/20261016120000_casbin_admin_template_update.up.sql rename to migrations/20261016191200_casbin_admin_template_update.up.sql diff --git a/tests/migration_boot_contract.rs b/tests/migration_boot_contract.rs index b5548f37..a9e038c0 100644 --- a/tests/migration_boot_contract.rs +++ b/tests/migration_boot_contract.rs @@ -53,11 +53,7 @@ fn stub_sqlx(bin_dir: &Path, log: &Path) -> PathBuf { } /// Run the entrypoint with the given command, returning (exit code, stub log). -fn run_entrypoint( - workdir: &Path, - env: &[(&str, &str)], - args: &[&str], -) -> (Option, String) { +fn run_entrypoint(workdir: &Path, env: &[(&str, &str)], args: &[&str]) -> (Option, String) { let bin_dir = workdir.join("bin"); let log = workdir.join("sqlx-invocations.log"); stub_sqlx(&bin_dir, &log); @@ -110,7 +106,8 @@ fn write_database_config_with_password(dir: &Path, password: &str) { } fn temp_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("stacker-entrypoint-{name}-{}", std::process::id())); + let dir = + std::env::temp_dir().join(format!("stacker-entrypoint-{name}-{}", std::process::id())); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).expect("temp dir"); dir @@ -299,3 +296,33 @@ fn compose_files_do_not_boot_around_the_migration_entrypoint() { } } } + +/// sqlx keys `_sqlx_migrations` by version: two migrations with one version +/// make every fresh database fail with a duplicate key on +/// `_sqlx_migrations_pkey`, and a database that applied one of them refuses the +/// other at boot. 20261016120000 was used twice (baked_snapshot_server_type and +/// casbin_admin_template_update). +#[test] +fn every_migration_version_is_used_once() { + let mut owners: std::collections::BTreeMap> = + Default::default(); + for entry in std::fs::read_dir(manifest_path("migrations")).expect("migrations dir") { + let name = entry.unwrap().file_name().to_string_lossy().to_string(); + let Some(stem) = name + .strip_suffix(".up.sql") + .or_else(|| name.strip_suffix(".down.sql")) + else { + continue; + }; + let (version, description) = stem.split_once('_').unwrap_or((stem, "")); + owners + .entry(version.to_string()) + .or_default() + .insert(description.to_string()); + } + let duplicates: Vec<_> = owners.iter().filter(|(_, d)| d.len() > 1).collect(); + assert!( + duplicates.is_empty(), + "migration versions used more than once: {duplicates:?}" + ); +}