GestionTablets/scripts/migrate_database.py

446 lines
15 KiB
Python
Raw Normal View History

#!/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 <db_file>] [--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()