Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Migrate ALL sheets from USLaP_Final_Data_Consolidated_Master_v3.xlsx | |
| into uslap_database_v3.db as indexed SQLite tables. | |
| Existing tables are preserved. New tables are created for sheets | |
| not yet in the DB. If a sheet's table already exists, it is REPLACED | |
| (dropped and recreated) to ensure full sync. | |
| """ | |
| import sqlite3 | |
| import openpyxl | |
| import os | |
| import re | |
| import shutil | |
| from datetime import datetime | |
| MASTER_XLSX = "/Users/mmsetubal/Documents/USLaP workplace/USLaP_Final_Data_Consolidated_Master_v3.xlsx" | |
| DB_PATH = "/Users/mmsetubal/Documents/USLaP workplace/Code_files/uslap_database_v3.db" | |
| # Tables that already exist and should NOT be touched by this migration | |
| # (they have their own schema and data) | |
| PRESERVE_TABLES = { | |
| 'umd_operations', 'child_schema', 'dp_register', 'att_terms', | |
| 'phonetic_reversal', 'session_index', 'protocol_corrections', | |
| 'scholar_warnings', 'cross_reference', 'sqlite_sequence' | |
| } | |
| def sanitize_table_name(sheet_name): | |
| """Convert sheet name to valid SQLite table name.""" | |
| name = sheet_name.strip() | |
| # Replace spaces, hyphens, special chars with underscore | |
| name = re.sub(r'[^a-zA-Z0-9_\u0400-\u04FF\u0600-\u06FF]', '_', name) | |
| # Remove leading/trailing underscores | |
| name = name.strip('_') | |
| # Ensure it doesn't start with a number | |
| if name and name[0].isdigit(): | |
| name = 'sheet_' + name | |
| return name.lower() | |
| def sanitize_column_name(col_name): | |
| """Convert column header to valid SQLite column name.""" | |
| if col_name is None: | |
| return None | |
| name = str(col_name).strip() | |
| name = re.sub(r'[^a-zA-Z0-9_\u0400-\u04FF\u0600-\u06FF]', '_', name) | |
| name = name.strip('_') | |
| if not name: | |
| return None | |
| if name[0].isdigit(): | |
| name = 'col_' + name | |
| return name.lower() | |
| def get_column_type(values): | |
| """Infer SQLite column type from sample values.""" | |
| has_int = False | |
| has_float = False | |
| has_text = False | |
| for v in values: | |
| if v is None: | |
| continue | |
| if isinstance(v, bool): | |
| has_text = True | |
| elif isinstance(v, int): | |
| has_int = True | |
| elif isinstance(v, float): | |
| has_float = True | |
| else: | |
| has_text = True | |
| if has_text: | |
| return 'TEXT' | |
| if has_float: | |
| return 'REAL' | |
| if has_int: | |
| return 'INTEGER' | |
| return 'TEXT' | |
| def migrate(): | |
| # Backup DB first | |
| backup_dir = os.path.dirname(DB_PATH) | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| backup_path = os.path.join(backup_dir, f'backups/uslap_db_v3_backup_{timestamp}.db') | |
| os.makedirs(os.path.dirname(backup_path), exist_ok=True) | |
| shutil.copy2(DB_PATH, backup_path) | |
| print(f"Backup created: {backup_path}") | |
| # Load workbook (read_only for speed, data_only to get values not formulas) | |
| print(f"Loading {MASTER_XLSX}...") | |
| wb = openpyxl.load_workbook(MASTER_XLSX, read_only=True, data_only=True) | |
| print(f"Sheets found: {len(wb.sheetnames)}") | |
| for s in wb.sheetnames: | |
| print(f" - {s}") | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.execute("PRAGMA foreign_keys = ON") | |
| cursor = conn.cursor() | |
| migrated = [] | |
| skipped = [] | |
| errors = [] | |
| for sheet_name in wb.sheetnames: | |
| table_name = sanitize_table_name(sheet_name) | |
| # Check if this would collide with a preserved table | |
| if table_name in PRESERVE_TABLES: | |
| # Use a prefixed name to avoid collision | |
| table_name = 'xlsx_' + table_name | |
| print(f" Sheet '{sheet_name}' -> table '{table_name}' (prefixed to avoid collision)") | |
| print(f"\nProcessing: '{sheet_name}' -> '{table_name}'") | |
| ws = wb[sheet_name] | |
| rows = list(ws.iter_rows(values_only=True)) | |
| if not rows: | |
| print(f" SKIP: empty sheet") | |
| skipped.append(sheet_name) | |
| continue | |
| # First row = headers | |
| raw_headers = rows[0] | |
| headers = [] | |
| seen = {} | |
| for i, h in enumerate(raw_headers): | |
| col_name = sanitize_column_name(h) | |
| if col_name is None: | |
| col_name = f'col_{i}' | |
| # Handle duplicates | |
| if col_name in seen: | |
| seen[col_name] += 1 | |
| col_name = f'{col_name}_{seen[col_name]}' | |
| else: | |
| seen[col_name] = 0 | |
| headers.append(col_name) | |
| data_rows = rows[1:] | |
| if not data_rows: | |
| print(f" SKIP: headers only, no data") | |
| skipped.append(sheet_name) | |
| continue | |
| # Filter out completely empty rows | |
| data_rows = [r for r in data_rows if any(v is not None for v in r)] | |
| if not data_rows: | |
| print(f" SKIP: all rows empty") | |
| skipped.append(sheet_name) | |
| continue | |
| # Ensure all rows have same column count as headers | |
| n_cols = len(headers) | |
| clean_rows = [] | |
| for r in data_rows: | |
| row = list(r) | |
| if len(row) < n_cols: | |
| row.extend([None] * (n_cols - len(row))) | |
| elif len(row) > n_cols: | |
| row = row[:n_cols] | |
| clean_rows.append(tuple(row)) | |
| # Infer column types | |
| col_types = [] | |
| for i in range(n_cols): | |
| sample = [r[i] for r in clean_rows[:100]] | |
| col_types.append(get_column_type(sample)) | |
| # Build CREATE TABLE | |
| col_defs = [] | |
| for h, t in zip(headers, col_types): | |
| col_defs.append(f'"{h}" {t}') | |
| try: | |
| # Drop if exists (full sync) | |
| cursor.execute(f'DROP TABLE IF EXISTS "{table_name}"') | |
| create_sql = f'CREATE TABLE "{table_name}" ({", ".join(col_defs)})' | |
| cursor.execute(create_sql) | |
| # Insert data | |
| placeholders = ', '.join(['?' for _ in headers]) | |
| insert_sql = f'INSERT INTO "{table_name}" VALUES ({placeholders})' | |
| # Convert all values to strings for TEXT columns to avoid type issues | |
| final_rows = [] | |
| for r in clean_rows: | |
| row = [] | |
| for i, v in enumerate(r): | |
| if v is not None and col_types[i] == 'TEXT': | |
| row.append(str(v)) | |
| else: | |
| row.append(v) | |
| final_rows.append(tuple(row)) | |
| cursor.executemany(insert_sql, final_rows) | |
| # Create indexes on common key columns | |
| key_columns = ['entry_id', 'root_id', 'en_term', 'score', 'network_id', | |
| 'shift_id', 'dp_id', 'allah_name_id', 'root_letters', | |
| 'запись_id', 'ru_term', 'корень_id'] | |
| for kc in key_columns: | |
| if kc in headers: | |
| idx_name = f'idx_{table_name}_{kc}' | |
| try: | |
| cursor.execute(f'CREATE INDEX IF NOT EXISTS "{idx_name}" ON "{table_name}"("{kc}")') | |
| except: | |
| pass # Skip if index creation fails | |
| conn.commit() | |
| print(f" OK: {len(final_rows)} rows, {n_cols} columns") | |
| migrated.append((sheet_name, table_name, len(final_rows), n_cols)) | |
| except Exception as e: | |
| errors.append((sheet_name, str(e))) | |
| print(f" ERROR: {e}") | |
| conn.rollback() | |
| wb.close() | |
| # Print summary | |
| print("\n" + "="*60) | |
| print("MIGRATION SUMMARY") | |
| print("="*60) | |
| print(f"\nMigrated: {len(migrated)} sheets") | |
| for sheet, table, rows, cols in migrated: | |
| print(f" {sheet:40s} -> {table:35s} ({rows} rows, {cols} cols)") | |
| print(f"\nSkipped: {len(skipped)} sheets") | |
| for s in skipped: | |
| print(f" {s}") | |
| print(f"\nErrors: {len(errors)} sheets") | |
| for s, e in errors: | |
| print(f" {s}: {e}") | |
| # Show final table list | |
| cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") | |
| all_tables = cursor.fetchall() | |
| print(f"\nTotal tables in DB: {len(all_tables)}") | |
| for t in all_tables: | |
| cursor.execute(f'SELECT COUNT(*) FROM "{t[0]}"') | |
| count = cursor.fetchone()[0] | |
| print(f" {t[0]:40s} {count:>6d} rows") | |
| conn.close() | |
| print(f"\nDone. DB: {DB_PATH}") | |
| if __name__ == '__main__': | |
| migrate() | |