""" Database Schema Verification & Repair Module Checks existing database structure and adds missing tables/columns/data Safely handles both fresh databases and existing installations """ import pymysql import logging logger = logging.getLogger(__name__) class SchemaVerifier: """Verify and repair database schema structure""" def __init__(self, connection): self.conn = connection self.cursor = connection.cursor() self.changes_made = [] def verify_and_repair(self): """ Main verification and repair process Returns: tuple: (success: bool, changes_summary: str) """ try: logger.info("=" * 60) logger.info("Starting database schema verification...") logger.info("=" * 60) # 1. Verify tables self.verify_tables() # 2. Verify columns in existing tables self.verify_columns() # 3. Verify reference data (roles, etc) self.verify_reference_data() # Commit all changes self.conn.commit() # Generate summary summary = self.generate_summary() logger.info("=" * 60) logger.info("✓ Database schema verification complete") logger.info("=" * 60) return True, summary except Exception as e: logger.error(f"✗ Schema verification failed: {e}") self.conn.rollback() return False, f"Error: {str(e)}" def table_exists(self, table_name): """Check if table exists in database""" try: self.cursor.execute(f""" SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s """, (table_name,)) return self.cursor.fetchone()[0] > 0 except Exception as e: logger.error(f"Error checking if table {table_name} exists: {e}") return False def column_exists(self, table_name, column_name): """Check if column exists in table""" try: self.cursor.execute(f""" SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s """, (table_name, column_name)) return self.cursor.fetchone()[0] > 0 except Exception as e: logger.error(f"Error checking column {table_name}.{column_name}: {e}") return False def get_table_columns(self, table_name): """Get all columns for a table""" try: self.cursor.execute(f""" SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s """, (table_name,)) return {row[0]: row for row in self.cursor.fetchall()} except Exception as e: logger.error(f"Error getting columns for {table_name}: {e}") return {} def verify_tables(self): """Verify all required tables exist""" logger.info("Verifying tables...") # Define all required tables with their schemas tables_to_verify = { 'users': self.get_users_schema, 'user_credentials': self.get_user_credentials_schema, 'user_modules': self.get_user_modules_schema, 'user_permissions': self.get_user_permissions_schema, 'roles': self.get_roles_schema, 'worker_manager_bindings': self.get_worker_manager_bindings_schema, 'application_settings': self.get_application_settings_schema, 'audit_logs': self.get_audit_logs_schema, 'backup_schedules': self.get_backup_schedules_schema, 'order_for_labels': self.get_order_for_labels_schema, } for table_name, schema_getter in tables_to_verify.items(): if not self.table_exists(table_name): logger.info(f" ⚠ Table '{table_name}' missing - creating...") try: sql = schema_getter() self.cursor.execute(sql) self.changes_made.append(f"Created table '{table_name}'") logger.info(f" ✓ Created table '{table_name}'") except Exception as e: logger.error(f" ✗ Failed to create table '{table_name}': {e}") raise else: logger.info(f" ✓ Table '{table_name}' exists") def verify_columns(self): """Verify all columns exist in tables""" logger.info("Verifying table columns...") # Define required columns for each table columns_to_verify = { 'users': [ ('id', 'INT', 'NO', 'PRI', 'auto_increment'), ('username', 'VARCHAR(100)', 'NO', 'UNI', ''), ('email', 'VARCHAR(255)', 'YES', 'UNI', ''), ('full_name', 'VARCHAR(255)', 'YES', '', ''), ('role', 'VARCHAR(100)', 'YES', 'MUL', ''), ('is_active', 'TINYINT(1)', 'YES', '', ''), ('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'), ('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'), ], 'worker_manager_bindings': [ ('id', 'INT', 'NO', 'PRI', 'auto_increment'), ('manager_id', 'INT', 'NO', 'MUL', ''), ('worker_id', 'INT', 'NO', '', ''), ('warehouse_zone', 'VARCHAR(100)', 'YES', '', ''), ('is_active', 'TINYINT(1)', 'YES', '', ''), ('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'), ('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'), ], 'roles': [ ('id', 'INT', 'NO', 'PRI', 'auto_increment'), ('name', 'VARCHAR(100)', 'NO', 'UNI', ''), ('description', 'TEXT', 'YES', '', ''), ('level', 'INT', 'YES', '', ''), ('created_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'), ('updated_at', 'TIMESTAMP', 'YES', '', 'DEFAULT_GENERATED'), ], } for table_name, required_columns in columns_to_verify.items(): if not self.table_exists(table_name): continue logger.info(f" Checking columns in '{table_name}'...") existing_columns = self.get_table_columns(table_name) for col_name, col_type, nullable, key, extra in required_columns: if col_name not in existing_columns: logger.info(f" ⚠ Column '{col_name}' missing - adding...") try: self.add_column(table_name, col_name, col_type, nullable) self.changes_made.append(f"Added column '{table_name}.{col_name}'") logger.info(f" ✓ Added column '{col_name}' to '{table_name}'") except Exception as e: logger.error(f" ✗ Failed to add column: {e}") raise else: logger.info(f" ✓ Column '{col_name}' exists") def add_column(self, table_name, column_name, column_type, nullable='YES'): """Add a missing column to a table""" null_clause = 'NULL' if nullable == 'YES' else 'NOT NULL' sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type} {null_clause}" self.cursor.execute(sql) def verify_reference_data(self): """Verify and add missing reference data (roles, etc)""" logger.info("Verifying reference data...") if not self.table_exists('roles'): logger.info(" ⚠ Roles table doesn't exist, skipping reference data") return # Define all required roles required_roles = [ ('superadmin', 'Super Administrator - Full system access', 100), ('admin', 'Administrator - Administrative access', 90), ('manager', 'Manager - Quality - Full access to assigned modules', 70), ('warehouse_manager', 'Manager - Warehouse - Full warehouse module access', 75), ('worker', 'Worker - Quality - Limited access', 50), ('warehouse_worker', 'Worker - Warehouse - Input-only warehouse access', 35), ] logger.info(" Verifying roles...") for role_name, role_desc, role_level in required_roles: self.cursor.execute("SELECT id FROM roles WHERE name = %s", (role_name,)) if not self.cursor.fetchone(): try: self.cursor.execute( "INSERT INTO roles (name, description, level) VALUES (%s, %s, %s)", (role_name, role_desc, role_level) ) self.changes_made.append(f"Added role '{role_name}'") logger.info(f" ✓ Added role '{role_name}'") except Exception as e: logger.error(f" ✗ Failed to add role '{role_name}': {e}") else: logger.info(f" ✓ Role '{role_name}' exists") def generate_summary(self): """Generate a summary of changes made""" if not self.changes_made: summary = "✓ Database structure is correct - no changes needed" else: summary = f"✓ Database repair complete - {len(self.changes_made)} change(s) made:\n" for i, change in enumerate(self.changes_made, 1): summary += f" {i}. {change}\n" return summary # Schema definitions @staticmethod def get_users_schema(): return """ CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE, full_name VARCHAR(255), role VARCHAR(100), is_active TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_role (role), INDEX idx_username (username), FOREIGN KEY (role) REFERENCES roles(name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_user_credentials_schema(): return """ CREATE TABLE IF NOT EXISTS user_credentials ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_roles_schema(): return """ CREATE TABLE IF NOT EXISTS roles ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, description TEXT, level INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_user_modules_schema(): return """ CREATE TABLE IF NOT EXISTS user_modules ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, module_name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_module (user_id, module_name), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_user_permissions_schema(): return """ CREATE TABLE IF NOT EXISTS user_permissions ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, module_name VARCHAR(100) NOT NULL, section_name VARCHAR(100) NOT NULL, action_name VARCHAR(100) NOT NULL, granted TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY unique_permission (user_id, module_name, section_name, action_name), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_worker_manager_bindings_schema(): return """ CREATE TABLE IF NOT EXISTS worker_manager_bindings ( id INT AUTO_INCREMENT PRIMARY KEY, manager_id INT NOT NULL, worker_id INT NOT NULL, warehouse_zone VARCHAR(100), is_active TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY unique_binding (manager_id, worker_id), FOREIGN KEY (manager_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (worker_id) REFERENCES users(id) ON DELETE CASCADE, CHECK (manager_id != worker_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_application_settings_schema(): return """ CREATE TABLE IF NOT EXISTS application_settings ( id INT AUTO_INCREMENT PRIMARY KEY, setting_key VARCHAR(255) UNIQUE NOT NULL, setting_value LONGTEXT, setting_type VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_audit_logs_schema(): return """ CREATE TABLE IF NOT EXISTS audit_logs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, action VARCHAR(255) NOT NULL, entity_type VARCHAR(100), entity_id INT, details JSON, ip_address VARCHAR(45), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), INDEX idx_action (action), INDEX idx_created_at (created_at), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_backup_schedules_schema(): return """ CREATE TABLE IF NOT EXISTS backup_schedules ( id INT AUTO_INCREMENT PRIMARY KEY, schedule_name VARCHAR(255) NOT NULL, frequency VARCHAR(50) NOT NULL, last_backup TIMESTAMP, next_backup TIMESTAMP, retention_days INT DEFAULT 30, is_active TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ @staticmethod def get_order_for_labels_schema(): return """ CREATE TABLE IF NOT EXISTS order_for_labels ( id BIGINT AUTO_INCREMENT PRIMARY KEY, comanda_productie VARCHAR(50) NOT NULL, cod_articol VARCHAR(50), descr_com_prod TEXT, cantitate DECIMAL(10, 2), com_achiz_client VARCHAR(50), nr_linie_com_client VARCHAR(50), customer_name VARCHAR(255), customer_article_number VARCHAR(100), open_for_order TINYINT(1) DEFAULT 1, line_number INT, printed_labels TINYINT(1) DEFAULT 0, data_livrara DATE, dimensiune VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_comanda_productie (comanda_productie), INDEX idx_printed_labels (printed_labels), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """