Files
delphi-database/app/database/schema_updates.py
2025-08-15 17:19:51 -05:00

149 lines
4.6 KiB
Python

"""
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]] = {
# 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