#!/usr/bin/env python3 """ Database Migration Script This script migrates the existing SQLite database to the new schema that includes the students table and relationships. Usage: python migrate_database.py [--database ] [--dry-run] Examples: python migrate_database.py python migrate_database.py --database tablets.db python migrate_database.py --dry-run """ import argparse import os import sqlite3 import sys from datetime import datetime DEFAULT_DATABASE = 'tablets.db' def parse_arguments(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description='Migrate database to new schema with students table' ) parser.add_argument( '--database', '-d', default=DEFAULT_DATABASE, help=f'Path to SQLite database file (default: {DEFAULT_DATABASE})' ) parser.add_argument( '--dry-run', action='store_true', help='Show migration steps without executing' ) parser.add_argument( '--verbose', '-v', action='store_true', help='Show detailed migration information' ) return parser.parse_args() def backup_database(database_path: str) -> str: """ Create a backup of the database before migration. Args: database_path: Path to the database file Returns: Path to the backup file """ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_path = f"{database_path}.backup_{timestamp}" if os.path.exists(database_path): import shutil shutil.copy2(database_path, backup_path) print(f"Created backup: {backup_path}") return backup_path def check_table_exists(conn: sqlite3.Connection, table_name: str) -> bool: """Check if a table exists in the database.""" cursor = conn.cursor() cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,) ) return cursor.fetchone() is not None def check_column_exists(conn: sqlite3.Connection, table_name: str, column_name: str) -> bool: """Check if a column exists in a table.""" cursor = conn.cursor() cursor.execute(f"PRAGMA table_info({table_name})") columns = [col[1] for col in cursor.fetchall()] return column_name in columns def create_students_table(conn: sqlite3.Connection) -> bool: """Create the students table.""" try: cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_number INTEGER, cial_code TEXT UNIQUE NOT NULL, nif_nie_passport TEXT UNIQUE, registration_number INTEGER, file_number TEXT, first_name TEXT NOT NULL, last_name TEXT NOT NULL, full_name TEXT NOT NULL, birth_date TEXT, gender TEXT CHECK(gender IN ('M', 'F', 'O')), study_group TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_identification UNIQUE (cial_code, nif_nie_passport) ) ''') # Create indexes cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_students_cial ON students(cial_code) ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_students_nif ON students(nif_nie_passport) ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_students_name ON students(last_name, first_name) ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_students_birth_date ON students(birth_date) ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_students_gender ON students(gender) ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_students_group ON students(study_group) ''') conn.commit() print("✓ Created students table") return True except sqlite3.Error as e: print(f"✗ Error creating students table: {e}") conn.rollback() return False def add_student_relationship_to_tablets(conn: sqlite3.Connection) -> bool: """Add assigned_to_student column to tablets table.""" try: cursor = conn.cursor() if not check_column_exists(conn, 'tablets', 'assigned_to_student'): cursor.execute(''' ALTER TABLE tablets ADD COLUMN assigned_to_student INTEGER ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_tablets_student ON tablets(assigned_to_student) ''') conn.commit() print("✓ Added assigned_to_student column to tablets table") else: print("✓ assigned_to_student column already exists in tablets table") return True except sqlite3.Error as e: print(f"✗ Error adding assigned_to_student to tablets: {e}") conn.rollback() return False def add_student_relationship_to_non_loanable_devices(conn: sqlite3.Connection) -> bool: """Add assigned_to_student column to non_loanable_devices table.""" try: cursor = conn.cursor() if not check_column_exists(conn, 'non_loanable_devices', 'assigned_to_student'): cursor.execute(''' ALTER TABLE non_loanable_devices ADD COLUMN assigned_to_student INTEGER ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_non_loanable_student ON non_loanable_devices(assigned_to_student) ''') conn.commit() print("✓ Added assigned_to_student column to non_loanable_devices table") else: print("✓ assigned_to_student column already exists in non_loanable_devices table") return True except sqlite3.Error as e: print(f"✗ Error adding assigned_to_student to non_loanable_devices: {e}") conn.rollback() return False def add_student_relationship_to_loans(conn: sqlite3.Connection) -> bool: """Add student_id column to loans table.""" try: cursor = conn.cursor() if not check_column_exists(conn, 'loans', 'student_id'): cursor.execute(''' ALTER TABLE loans ADD COLUMN student_id INTEGER ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_loans_student ON loans(student_id) ''') conn.commit() print("✓ Added student_id column to loans table") else: print("✓ student_id column already exists in loans table") return True except sqlite3.Error as e: print(f"✗ Error adding student_id to loans: {e}") conn.rollback() return False def add_foreign_keys(conn: sqlite3.Connection) -> bool: """Add foreign key constraints to tables.""" try: cursor = conn.cursor() # For tablets table if check_column_exists(conn, 'tablets', 'assigned_to_student'): # SQLite doesn't support adding foreign keys with ALTER TABLE easily # We'll rely on application-level enforcement print("✓ Foreign key for tablets.assigned_to_student will be enforced at application level") # For non_loanable_devices table if check_column_exists(conn, 'non_loanable_devices', 'assigned_to_student'): print("✓ Foreign key for non_loanable_devices.assigned_to_student will be enforced at application level") # For loans table if check_column_exists(conn, 'loans', 'student_id'): print("✓ Foreign key for loans.student_id will be enforced at application level") conn.commit() return True except sqlite3.Error as e: print(f"✗ Error adding foreign keys: {e}") conn.rollback() return False def add_timestamp_columns(conn: sqlite3.Connection) -> bool: """Add created_at and updated_at columns to existing tables if they don't exist.""" try: cursor = conn.cursor() tables_to_update = ['tablets', 'users', 'loans', 'non_loanable_devices'] for table in tables_to_update: if not check_column_exists(conn, table, 'created_at'): cursor.execute(f''' ALTER TABLE {table} ADD COLUMN created_at TEXT ''') # Update existing rows cursor.execute(f''' UPDATE {table} SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL ''') print(f"✓ Added created_at column to {table} table") if not check_column_exists(conn, table, 'updated_at'): cursor.execute(f''' ALTER TABLE {table} ADD COLUMN updated_at TEXT ''') # Update existing rows cursor.execute(f''' UPDATE {table} SET updated_at = CURRENT_TIMESTAMP WHERE updated_at IS NULL ''') print(f"✓ Added updated_at column to {table} table") conn.commit() return True except sqlite3.Error as e: print(f"✗ Error adding timestamp columns: {e}") conn.rollback() return False def migrate_database(database_path: str, dry_run: bool = False, verbose: bool = False) -> bool: """ Perform the database migration. Args: database_path: Path to the database file dry_run: If True, show steps without executing verbose: If True, show detailed information Returns: True if migration succeeded, False otherwise """ if dry_run: print("[DRY RUN] Migration steps:") else: print("Starting database migration...") # Create backup backup_path = backup_database(database_path) try: conn = sqlite3.connect(database_path) conn.execute("PRAGMA foreign_keys = ON") if dry_run: print(" 1. Would create students table") print(" 2. Would add assigned_to_student to tablets table") print(" 3. Would add assigned_to_student to non_loanable_devices table") print(" 4. Would add student_id to loans table") print(" 5. Would add timestamp columns to existing tables") print(" 6. Would create indexes") else: print("\nRunning migration steps:") # Step 1: Create students table if not create_students_table(conn): return False # Step 2: Add student relationship to tablets if not add_student_relationship_to_tablets(conn): return False # Step 3: Add student relationship to non_loanable_devices if not add_student_relationship_to_non_loanable_devices(conn): return False # Step 4: Add student relationship to loans if not add_student_relationship_to_loans(conn): return False # Step 5: Add timestamp columns if not add_timestamp_columns(conn): return False # Step 6: Add foreign keys (enforced at application level) if not add_foreign_keys(conn): return False conn.close() if dry_run: print("\n[DRY RUN] No changes were made to the database.") else: print("\n✓ Database migration completed successfully!") return True except Exception as e: print(f"\n✗ Migration failed: {e}") if not dry_run and 'conn' in locals(): conn.rollback() conn.close() return False def verify_migration(database_path: str) -> bool: """Verify that the migration was successful.""" try: conn = sqlite3.connect(database_path) cursor = conn.cursor() print("\nVerifying migration:") # Check students table exists if check_table_exists(conn, 'students'): print("✓ students table exists") cursor.execute("SELECT COUNT(*) FROM students") count = cursor.fetchone()[0] print(f" - Contains {count} students") else: print("✗ students table missing") return False # Check columns in tablets if check_column_exists(conn, 'tablets', 'assigned_to_student'): print("✓ tablets.assigned_to_student column exists") else: print("✗ tablets.assigned_to_student column missing") return False # Check columns in non_loanable_devices if check_column_exists(conn, 'non_loanable_devices', 'assigned_to_student'): print("✓ non_loanable_devices.assigned_to_student column exists") else: print("✗ non_loanable_devices.assigned_to_student column missing") return False # Check columns in loans if check_column_exists(conn, 'loans', 'student_id'): print("✓ loans.student_id column exists") else: print("✗ loans.student_id column missing") return False # Check indexes cursor.execute("SELECT name FROM sqlite_master WHERE type='index'") indexes = [row[0] for row in cursor.fetchall()] required_indexes = [ 'idx_students_cial', 'idx_students_nif', 'idx_students_name', 'idx_students_birth_date', 'idx_students_gender', 'idx_students_group', 'idx_tablets_student', 'idx_non_loanable_student', 'idx_loans_student' ] for idx in required_indexes: if idx in indexes: print(f"✓ Index {idx} exists") else: print(f"✗ Index {idx} missing") return False conn.close() print("\n✓ All migration verifications passed!") return True except Exception as e: print(f"✗ Verification failed: {e}") return False def main(): """Main entry point.""" args = parse_arguments() print(f"Database: {args.database}") print(f"Dry run: {args.dry_run}") print() # Perform migration success = migrate_database( database_path=args.database, dry_run=args.dry_run, verbose=args.verbose ) if success and not args.dry_run: # Verify migration verify_migration(args.database) # Exit with appropriate code sys.exit(0 if success else 1) if __name__ == '__main__': main()