""" ScanLook Database Migration System Simple migration system that tracks and applies database changes. Each migration has a version number and an up() function. Usage: from migrations import run_migrations run_migrations() # Call on app startup """ import sqlite3 import os DB_PATH = os.path.join(os.path.dirname(__file__), 'database', 'scanlook.db') def get_db(): """Get database connection""" conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def init_migrations_table(): """Create the migrations tracking table if it doesn't exist""" conn = get_db() conn.execute(''' CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() def get_applied_migrations(): """Get list of already-applied migration versions""" conn = get_db() try: rows = conn.execute('SELECT version FROM schema_migrations ORDER BY version').fetchall() return [row['version'] for row in rows] except: return [] finally: conn.close() def record_migration(version, name): """Record that a migration was applied""" conn = get_db() conn.execute('INSERT INTO schema_migrations (version, name) VALUES (?, ?)', [version, name]) conn.commit() conn.close() def column_exists(table, column): """Check if a column exists in a table""" conn = get_db() cursor = conn.execute(f'PRAGMA table_info({table})') columns = [row[1] for row in cursor.fetchall()] conn.close() return column in columns def table_exists(table): """Check if a table exists""" conn = get_db() cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [table]) exists = cursor.fetchone() is not None conn.close() return exists # ============================================ # MIGRATIONS # ============================================ # Add new migrations to this list. # Each migration is a tuple: (version, name, up_function) # # RULES: # - Never modify an existing migration # - Always add new migrations at the end with the next version number # - Check if changes are needed before applying (idempotent) # ============================================ def migration_001_add_modules_tables(): """Add Modules and UserModules tables""" conn = get_db() if not table_exists('Modules'): conn.execute(''' CREATE TABLE Modules ( module_id INTEGER PRIMARY KEY AUTOINCREMENT, module_name TEXT NOT NULL, module_key TEXT UNIQUE NOT NULL, description TEXT, icon TEXT, is_active INTEGER DEFAULT 1, display_order INTEGER DEFAULT 0 ) ''') print(" Created Modules table") if not table_exists('UserModules'): conn.execute(''' CREATE TABLE UserModules ( user_module_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, module_id INTEGER NOT NULL, granted_by INTEGER, granted_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES Users(user_id), FOREIGN KEY (module_id) REFERENCES Modules(module_id), FOREIGN KEY (granted_by) REFERENCES Users(user_id), UNIQUE(user_id, module_id) ) ''') print(" Created UserModules table") conn.commit() conn.close() def migration_002_add_usermodules_granted_columns(): """Add granted_by and granted_timestamp to UserModules if missing""" conn = get_db() if table_exists('UserModules'): if not column_exists('UserModules', 'granted_by'): conn.execute('ALTER TABLE UserModules ADD COLUMN granted_by INTEGER') print(" Added granted_by column to UserModules") if not column_exists('UserModules', 'granted_timestamp'): conn.execute('ALTER TABLE UserModules ADD COLUMN granted_timestamp DATETIME') print(" Added granted_timestamp column to UserModules") conn.commit() conn.close() def migration_003_add_default_modules(): """Add default modules if they don't exist""" conn = get_db() # Check if modules exist existing = conn.execute('SELECT COUNT(*) as cnt FROM Modules').fetchone() if existing['cnt'] == 0: conn.execute(''' INSERT INTO Modules (module_name, module_key, description, icon, is_active, display_order) VALUES ('Inventory Counts', 'counting', 'Cycle counts and physical inventory', 'fa-clipboard-check', 1, 1) ''') conn.execute(''' INSERT INTO Modules (module_name, module_key, description, icon, is_active, display_order) VALUES ('Consumption Sheets', 'cons_sheets', 'Production consumption tracking', 'fa-clipboard-list', 1, 2) ''') print(" Added default modules") conn.commit() conn.close() def migration_004_assign_modules_to_admins(): """Auto-assign all modules to owner and admin users""" conn = get_db() # Get admin users admins = conn.execute('SELECT user_id FROM Users WHERE role IN ("owner", "admin")').fetchall() modules = conn.execute('SELECT module_id FROM Modules').fetchall() for user in admins: for module in modules: try: conn.execute(''' INSERT INTO UserModules (user_id, module_id) VALUES (?, ?) ''', [user['user_id'], module['module_id']]) except sqlite3.IntegrityError: pass # Already assigned conn.commit() conn.close() print(" Assigned modules to admin users") def migration_005_add_cons_process_fields_duplicate_key(): """Add is_duplicate_key column to cons_process_fields if missing""" conn = get_db() if table_exists('cons_process_fields'): if not column_exists('cons_process_fields', 'is_duplicate_key'): conn.execute('ALTER TABLE cons_process_fields ADD COLUMN is_duplicate_key INTEGER DEFAULT 0') print(" Added is_duplicate_key column to cons_process_fields") conn.commit() conn.close() # List of all migrations in order MIGRATIONS = [ (1, 'add_modules_tables', migration_001_add_modules_tables), (2, 'add_usermodules_granted_columns', migration_002_add_usermodules_granted_columns), (3, 'add_default_modules', migration_003_add_default_modules), (4, 'assign_modules_to_admins', migration_004_assign_modules_to_admins), (5, 'add_cons_process_fields_duplicate_key', migration_005_add_cons_process_fields_duplicate_key), ] def run_migrations(): """Run all pending migrations""" print("šŸ”„ Checking database migrations...") # Make sure migrations table exists init_migrations_table() # Get already-applied migrations applied = get_applied_migrations() # Run pending migrations pending = [(v, n, f) for v, n, f in MIGRATIONS if v not in applied] if not pending: print("āœ… Database is up to date") return print(f"šŸ“¦ Running {len(pending)} migration(s)...") for version, name, func in pending: print(f"\n Migration {version}: {name}") try: func() record_migration(version, name) print(f" āœ… Migration {version} complete") except Exception as e: print(f" āŒ Migration {version} failed: {e}") raise print("\nāœ… All migrations complete") if __name__ == '__main__': run_migrations()