fix: make migrations resilient to orphaned scans_new tables
All checks were successful
Deploy / deploy (push) Successful in 12s

- Add _recover_orphaned_new_tables() called at DB init startup: if scans_new
  exists but scans doesn't (from a previously aborted migration), rename it back
- Drop VIEW recent_scans/active_scans before dropping the scans table in both
  _migrate_add_pause_cancel_status and _migrate_add_reverse_scan_support, so
  executescript can cleanly recreate them
- Add DROP TABLE IF EXISTS scans_new / scheduled_scans_new at the start of each
  table-recreation migration to prevent 'table scans_new already exists' errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 18:18:22 +01:00
parent 77d2a46264
commit 4f4f7e86d1

View File

@@ -227,6 +227,28 @@ def _migrate_add_timing_columns_to_scans(conn, verbose=True):
print(" ✅ Migration complete: started_at and completed_at columns added to scans")
def _recover_orphaned_new_tables(conn, verbose=True):
"""
Recovery: if a previous migration left behind scans_new or scheduled_scans_new
(e.g. after a crash between DROP TABLE scans and RENAME), restore them.
"""
tables = [r[0] for r in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
).fetchall()]
if 'scans_new' in tables and 'scans' not in tables:
if verbose:
print(" 🔧 Recovering: renaming orphaned scans_new → scans")
conn.execute("ALTER TABLE scans_new RENAME TO scans")
conn.commit()
if 'scheduled_scans_new' in tables and 'scheduled_scans' not in tables:
if verbose:
print(" 🔧 Recovering: renaming orphaned scheduled_scans_new → scheduled_scans")
conn.execute("ALTER TABLE scheduled_scans_new RENAME TO scheduled_scans")
conn.commit()
def _migrate_add_pause_cancel_status(conn, verbose=True):
"""
Migration: Extend status CHECK constraint to include 'paused' and 'cancelled'.
@@ -248,6 +270,11 @@ def _migrate_add_pause_cancel_status(conn, verbose=True):
# SQLite doesn't support ALTER TABLE MODIFY COLUMN, so recreate the table.
# Use PRAGMA foreign_keys = OFF to avoid FK errors during the swap.
conn.execute("PRAGMA foreign_keys = OFF")
# Drop views that reference scans so they can be cleanly recreated by executescript.
conn.execute("DROP VIEW IF EXISTS recent_scans")
conn.execute("DROP VIEW IF EXISTS active_scans")
# Drop any leftover _new table from a previously aborted migration.
conn.execute("DROP TABLE IF EXISTS scans_new")
# Drop triggers that reference scans (they are recreated by executescript below).
conn.execute("DROP TRIGGER IF EXISTS update_scans_timestamp")
conn.execute("DROP TRIGGER IF EXISTS update_scan_flight_count_insert")
@@ -318,6 +345,9 @@ def _migrate_add_reverse_scan_support(conn, verbose=True):
if verbose:
print(" 🔄 Migrating scans table: relaxing origin constraint, adding scan_mode…")
conn.execute("PRAGMA foreign_keys = OFF")
conn.execute("DROP VIEW IF EXISTS recent_scans")
conn.execute("DROP VIEW IF EXISTS active_scans")
conn.execute("DROP TABLE IF EXISTS scans_new")
conn.execute("DROP TRIGGER IF EXISTS update_scans_timestamp")
conn.execute("DROP TRIGGER IF EXISTS update_scan_flight_count_insert")
conn.execute("DROP TRIGGER IF EXISTS update_scan_flight_count_update")
@@ -413,6 +443,7 @@ def _migrate_add_reverse_scan_support(conn, verbose=True):
if sched_cols and 'scan_mode' not in sched_cols:
if verbose:
print(" 🔄 Migrating scheduled_scans table: relaxing origin constraint, adding scan_mode…")
conn.execute("DROP TABLE IF EXISTS scheduled_scans_new")
conn.execute("DROP TRIGGER IF EXISTS update_scheduled_scans_timestamp")
conn.execute("""
CREATE TABLE scheduled_scans_new (
@@ -508,6 +539,9 @@ def initialize_database(db_path=None, verbose=True):
else:
print(" No existing tables found")
# Recover any orphaned _new tables left by previously aborted migrations
_recover_orphaned_new_tables(conn, verbose)
# Apply migrations before running schema
_migrate_relax_country_constraint(conn, verbose)
_migrate_add_routes_unique_index(conn, verbose)