From 5f5cb276909590ed845f5671743795fd69ae4ca9 Mon Sep 17 00:00:00 2001 From: Alex Gorevski Date: Tue, 17 Feb 2026 20:50:08 -0800 Subject: [PATCH] fix(cron): handle ALTER TABLE race condition in schema migration Problem: add_column_if_missing() checks PRAGMA table_info for column existence, then issues ALTER TABLE ADD COLUMN if not found. When two concurrent processes both pass the check before either executes the ALTER, the second process fails with a 'duplicate column name' error. Fix: Catch the 'duplicate column name' SQLite error after the ALTER TABLE and treat it as a benign no-op. Also explicitly drop statement/rows handles before ALTER to release locks. Ref: #710 (Item 8) --- src/cron/store.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/cron/store.rs b/src/cron/store.rs index 013ed55..9c68d05 100644 --- a/src/cron/store.rs +++ b/src/cron/store.rs @@ -443,13 +443,25 @@ fn add_column_if_missing(conn: &Connection, name: &str, sql_type: &str) -> Resul return Ok(()); } } + // Drop the statement/rows before executing ALTER to release any locks + drop(rows); + drop(stmt); - conn.execute( + // Tolerate "duplicate column name" errors to handle the race where + // another process adds the column between our PRAGMA check and ALTER. + match conn.execute( &format!("ALTER TABLE cron_jobs ADD COLUMN {name} {sql_type}"), [], - ) - .with_context(|| format!("Failed to add cron_jobs.{name}"))?; - Ok(()) + ) { + Ok(_) => Ok(()), + Err(rusqlite::Error::SqliteFailure(err, Some(ref msg))) + if msg.contains("duplicate column name") => + { + tracing::debug!("Column cron_jobs.{name} already exists (concurrent migration): {err}"); + Ok(()) + } + Err(e) => Err(e).with_context(|| format!("Failed to add cron_jobs.{name}")), + } } fn with_connection(config: &Config, f: impl FnOnce(&Connection) -> Result) -> Result {