""" Lightweight, idempotent schema updates for SQLite. Adds newly introduced columns to existing tables when running on an already-initialized database. Safe to call multiple times. """ from typing import Dict from sqlalchemy.engine import Engine from sqlalchemy import text def _existing_columns(conn, table: str) -> set[str]: rows = conn.execute(text(f"PRAGMA table_info('{table}')")).fetchall() return {row[1] for row in rows} # name is column 2 def ensure_schema_updates(engine: Engine) -> None: """Ensure missing columns are added for backward-compatible updates.""" # Map of table -> {column: SQL type} updates: Dict[str, Dict[str, str]] = { # Billing batch history (lightweight persistence) "billing_batches": { "id": "INTEGER", "batch_id": "TEXT", "status": "TEXT", "total_files": "INTEGER", "successful_files": "INTEGER", "failed_files": "INTEGER", "started_at": "DATETIME", "updated_at": "DATETIME", "completed_at": "DATETIME", "processing_time_seconds": "FLOAT", "success_rate": "FLOAT", "error_message": "TEXT", }, "billing_batch_files": { "id": "INTEGER", "batch_id": "TEXT", "file_no": "TEXT", "status": "TEXT", "error_message": "TEXT", "filename": "TEXT", "size": "INTEGER", "started_at": "DATETIME", "completed_at": "DATETIME", }, # Forms "form_index": { "keyword": "TEXT", }, # Richer Life/Number tables (forms & pensions harmonized) "life_tables": { "le_aa": "FLOAT", "na_aa": "FLOAT", "le_am": "FLOAT", "na_am": "FLOAT", "le_af": "FLOAT", "na_af": "FLOAT", "le_wa": "FLOAT", "na_wa": "FLOAT", "le_wm": "FLOAT", "na_wm": "FLOAT", "le_wf": "FLOAT", "na_wf": "FLOAT", "le_ba": "FLOAT", "na_ba": "FLOAT", "le_bm": "FLOAT", "na_bm": "FLOAT", "le_bf": "FLOAT", "na_bf": "FLOAT", "le_ha": "FLOAT", "na_ha": "FLOAT", "le_hm": "FLOAT", "na_hm": "FLOAT", "le_hf": "FLOAT", "na_hf": "FLOAT", "table_year": "INTEGER", "table_type": "VARCHAR(45)", }, "number_tables": { "month": "INTEGER", "na_aa": "FLOAT", "na_am": "FLOAT", "na_af": "FLOAT", "na_wa": "FLOAT", "na_wm": "FLOAT", "na_wf": "FLOAT", "na_ba": "FLOAT", "na_bm": "FLOAT", "na_bf": "FLOAT", "na_ha": "FLOAT", "na_hm": "FLOAT", "na_hf": "FLOAT", "table_type": "VARCHAR(45)", "description": "TEXT", }, "form_list": { "status": "VARCHAR(45)", }, # Printers: add advanced legacy fields "printers": { "number": "INTEGER", "page_break": "VARCHAR(50)", "setup_st": "VARCHAR(200)", "reset_st": "VARCHAR(200)", "b_underline": "VARCHAR(100)", "e_underline": "VARCHAR(100)", "b_bold": "VARCHAR(100)", "e_bold": "VARCHAR(100)", "phone_book": "BOOLEAN", "rolodex_info": "BOOLEAN", "envelope": "BOOLEAN", "file_cabinet": "BOOLEAN", "accounts": "BOOLEAN", "statements": "BOOLEAN", "calendar": "BOOLEAN", }, # Pensions "pension_schedules": { "vests_on": "DATE", "vests_at": "FLOAT", "version": "VARCHAR(10)", }, "marriage_history": { "married_from": "DATE", "married_to": "DATE", "married_years": "FLOAT", "service_from": "DATE", "service_to": "DATE", "service_years": "FLOAT", "marital_percent": "FLOAT", "version": "VARCHAR(10)", }, "death_benefits": { "lump1": "FLOAT", "lump2": "FLOAT", "growth1": "FLOAT", "growth2": "FLOAT", "disc1": "FLOAT", "disc2": "FLOAT", "version": "VARCHAR(10)", }, "separation_agreements": { "version": "VARCHAR(10)", }, # QDROs: add explicit created_at and workflow fields if missing "qdros": { "created_at": "DATETIME", "approval_status": "VARCHAR(45)", "approved_date": "DATE", "filed_date": "DATE", }, # Users: add approver flag "users": { "is_approver": "BOOLEAN", }, } with engine.begin() as conn: for table, cols in updates.items(): try: existing = _existing_columns(conn, table) except Exception: # Table may not exist yet continue for col_name, col_type in cols.items(): if col_name not in existing: try: conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type}")) except Exception: # Ignore if not applicable (other engines) or race condition pass