huskies: merge 1087 story Pipeline+Status split — Step D: migrate CRDT storage to (Pipeline, Status) and remove the Stage enum
This commit is contained in:
@@ -69,6 +69,13 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Story 1087: before running the migration that splits `stage` into
|
||||
// (`pipeline`, `status`), take a timestamped side-car copy of the live DB
|
||||
// so the pre-split state is recoverable. Skip the copy when the file does
|
||||
// not yet exist (fresh installs) or when the split-stage migration has
|
||||
// already been applied (subsequent restarts).
|
||||
backup_pre_pipeline_status(db_path).await;
|
||||
|
||||
let options = SqliteConnectOptions::new()
|
||||
.filename(db_path)
|
||||
.create_if_missing(true);
|
||||
@@ -147,6 +154,63 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Story 1087: file name of the split-stage migration. The version prefix is
|
||||
/// the same `i64` sqlx assigns to that migration on `installed_on` rows in
|
||||
/// `_sqlx_migrations`.
|
||||
const SPLIT_STAGE_MIGRATION_VERSION: i64 = 20260515000000;
|
||||
|
||||
/// Story 1087: take a timestamped side-car copy of `pipeline.db` if and only if
|
||||
/// the split-stage migration has not yet been applied. This is the AC1 backup
|
||||
/// — `pipeline.db.pre-pipeline-status.<unix-ts>.bak` next to the live file.
|
||||
///
|
||||
/// Failures are logged but never propagated: a missing backup must not block
|
||||
/// the server from starting (a corrupt source file or a read-only directory
|
||||
/// will be surfaced by the migration step itself).
|
||||
pub(crate) async fn backup_pre_pipeline_status(db_path: &Path) {
|
||||
if !db_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cheap pre-check: open the DB read-only and see whether the split-stage
|
||||
// migration version is recorded in `_sqlx_migrations`. If it is, the
|
||||
// backup has already been taken on a previous start and there is nothing
|
||||
// to do.
|
||||
let options = SqliteConnectOptions::new()
|
||||
.filename(db_path)
|
||||
.read_only(true)
|
||||
.create_if_missing(false);
|
||||
|
||||
let probe = SqlitePool::connect_with(options).await;
|
||||
if let Ok(pool) = probe {
|
||||
let already_split: Result<Option<(i64,)>, _> =
|
||||
sqlx::query_as("SELECT version FROM _sqlx_migrations WHERE version = ?1 LIMIT 1")
|
||||
.bind(SPLIT_STAGE_MIGRATION_VERSION)
|
||||
.fetch_optional(&pool)
|
||||
.await;
|
||||
pool.close().await;
|
||||
if let Ok(Some(_)) = already_split {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let ts = chrono::Utc::now().timestamp();
|
||||
let mut backup = db_path.as_os_str().to_owned();
|
||||
backup.push(format!(".pre-pipeline-status.{ts}.bak"));
|
||||
let backup_path = std::path::PathBuf::from(backup);
|
||||
|
||||
match tokio::fs::copy(db_path, &backup_path).await {
|
||||
Ok(_) => slog!(
|
||||
"[db] Wrote pre-pipeline-status backup of {} to {}",
|
||||
db_path.display(),
|
||||
backup_path.display(),
|
||||
),
|
||||
Err(e) => slog!(
|
||||
"[db] Failed to write pre-pipeline-status backup of {}: {e}",
|
||||
db_path.display(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare the live `_sqlx_migrations` table against the compiled-in migration
|
||||
/// set and return any rows whose version is not known to this binary.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user