merge-request: checkpoint restartable migration

This commit is contained in:
2026-08-15 21:26:02 +09:00
parent eaaf2f6dcc
commit 0b8924eda7
2 changed files with 120 additions and 7 deletions
+43 -7
View File
@@ -677,7 +677,28 @@ impl SqliteMergeRequestStore {
}
pub fn migrate(conn: &Connection) -> Result<()> {
conn.busy_timeout(Duration::from_secs(5)).map_err(db)?;
conn.pragma_update(None, "foreign_keys", "ON").map_err(db)?;
// Acquire the writer lock before reading either the version or table layout so
// concurrent store initialization cannot act on a stale migration decision.
conn.execute_batch("BEGIN IMMEDIATE").map_err(db)?;
let result = migrate_transaction(conn);
match result {
Ok(()) => match conn.execute_batch("COMMIT") {
Ok(()) => Ok(()),
Err(error) => {
let _ = conn.execute_batch("ROLLBACK");
Err(db(error))
}
},
Err(error) => {
let _ = conn.execute_batch("ROLLBACK");
Err(error)
}
}
}
fn migrate_transaction(conn: &Connection) -> Result<()> {
conn.execute_batch("CREATE TABLE IF NOT EXISTS merge_request_schema_migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);").map_err(db)?;
let version: i64 = conn
.query_row(
@@ -1014,26 +1035,41 @@ fn archive_incompatible_legacy_tables(conn: &Connection, version: i64) -> Result
"merge_request_revisions",
"merge_requests",
];
conn.pragma_update(None, "foreign_keys", "OFF")
.map_err(db)?;
for table in tables {
if !table_exists(conn, table)? {
continue;
}
let archive = format!("legacy_v6_{table}");
if table_exists(conn, &archive)? {
conn.pragma_update(None, "foreign_keys", "ON").map_err(db)?;
return Err(MergeRequestError::Database(format!(
"legacy archive table {archive} already exists"
)));
// The retired non-transactional migration could archive a table, recreate
// its empty replacement, and then fail. Resume that exact state without
// ever choosing between two populated copies.
if !table_is_empty(conn, table)? {
return Err(MergeRequestError::Database(format!(
"legacy archive table {archive} already exists while {table} still contains data"
)));
}
conn.execute_batch(&format!("DROP TABLE {table};"))
.map_err(db)?;
continue;
}
conn.execute_batch(&format!("ALTER TABLE {table} RENAME TO {archive};"))
.map_err(db)?;
}
conn.pragma_update(None, "foreign_keys", "ON").map_err(db)?;
Ok(())
}
fn table_is_empty(conn: &Connection, table: &str) -> Result<bool> {
let has_row: i64 = conn
.query_row(
&format!("SELECT EXISTS(SELECT 1 FROM {table} LIMIT 1)"),
[],
|row| row.get(0),
)
.map_err(db)?;
Ok(has_row == 0)
}
fn table_has_columns(conn: &Connection, table: &str, required: &[&str]) -> Result<bool> {
if !table_exists(conn, table)? {
return Ok(false);