""" Migration idempotente per sandbox. Base.metadata.create_all() crea solo tabelle mancanti, non colonne aggiuntive. Qui gestiamo ALTER TABLE ADD COLUMN IF NOT EXISTS per l'evoluzione dello schema. Ogni migration รจ una stringa SQL che puo essere eseguita piu volte senza errori. """ from sqlalchemy import text import logging log = logging.getLogger("rendicontazione-api.migrations") MIGRATIONS = [ # 2026-04-18: colonne file upload su remission_invoice """ ALTER TABLE gepafin_rendic.remission_invoice ADD COLUMN IF NOT EXISTS storage_path varchar(1024), ADD COLUMN IF NOT EXISTS mime varchar(128), ADD COLUMN IF NOT EXISTS size_bytes bigint, ADD COLUMN IF NOT EXISTS sha256 varchar(64), ADD COLUMN IF NOT EXISTS uploaded_by integer, ADD COLUMN IF NOT EXISTS uploaded_at timestamptz; """, # 2026-04-18: colonne file upload su remission_ula_employee """ ALTER TABLE gepafin_rendic.remission_ula_employee ADD COLUMN IF NOT EXISTS storage_path varchar(1024), ADD COLUMN IF NOT EXISTS mime varchar(128), ADD COLUMN IF NOT EXISTS size_bytes bigint, ADD COLUMN IF NOT EXISTS sha256 varchar(64), ADD COLUMN IF NOT EXISTS uploaded_by integer, ADD COLUMN IF NOT EXISTS uploaded_at timestamptz; """, # 2026-04-18: colonne file upload su remission_document """ ALTER TABLE gepafin_rendic.remission_document ADD COLUMN IF NOT EXISTS storage_path varchar(1024), ADD COLUMN IF NOT EXISTS mime varchar(128), ADD COLUMN IF NOT EXISTS size_bytes bigint, ADD COLUMN IF NOT EXISTS sha256 varchar(64), ADD COLUMN IF NOT EXISTS uploaded_by integer; """, # 2026-04-18 v2: multi-tranche su remission_practice # DROP UNIQUE su application_id (permette piu tranche per stessa domanda) # aggiunge sequence_number, period_label, suggested_instructor_id # nuova UNIQUE (application_id, sequence_number) # partial index su assigned_instructor_id IS NULL per coda "da assegnare" """ ALTER TABLE gepafin_rendic.remission_practice DROP CONSTRAINT IF EXISTS uq_remission_practice_application; ALTER TABLE gepafin_rendic.remission_practice ADD COLUMN IF NOT EXISTS sequence_number integer NOT NULL DEFAULT 1, ADD COLUMN IF NOT EXISTS period_label varchar(100), ADD COLUMN IF NOT EXISTS suggested_instructor_id integer; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'uq_remission_practice_app_seq' AND conrelid = 'gepafin_rendic.remission_practice'::regclass ) THEN ALTER TABLE gepafin_rendic.remission_practice ADD CONSTRAINT uq_remission_practice_app_seq UNIQUE (application_id, sequence_number); END IF; END$$; CREATE INDEX IF NOT EXISTS idx_remission_practice_unassigned ON gepafin_rendic.remission_practice(assigned_instructor_id) WHERE assigned_instructor_id IS NULL; """, # 2026-04-18 v2: tabella custom checks # allineata allo storage adapter esistente (storage_path + mime + size + sha256) # NON segue le specs RAG p1 che usavano document_filename (v1 obsoleta) """ CREATE TABLE IF NOT EXISTS gepafin_rendic.remission_custom_check_value ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), practice_id uuid NOT NULL REFERENCES gepafin_rendic.remission_practice(id) ON DELETE CASCADE, check_code varchar(64) NOT NULL, beneficiary_declared boolean NOT NULL DEFAULT false, declared_at timestamptz, storage_path varchar(1024), mime varchar(128), size_bytes bigint, sha256 varchar(64), document_uploaded_at timestamptz, uploaded_by integer, verification_status varchar(20) NOT NULL DEFAULT 'PENDING', verification_notes text, verified_by integer, verified_at timestamptz, created_at timestamptz NOT NULL DEFAULT NOW(), updated_at timestamptz NOT NULL DEFAULT NOW(), CONSTRAINT uq_custom_check_practice_code UNIQUE (practice_id, check_code) ); CREATE INDEX IF NOT EXISTS idx_custom_check_practice ON gepafin_rendic.remission_custom_check_value(practice_id); """, ] def run_migrations(engine) -> None: """Esegue tutte le migration in transazione. Log su ciascuna.""" with engine.begin() as conn: for i, sql in enumerate(MIGRATIONS, 1): try: conn.execute(text(sql)) log.info(f"migration {i}/{len(MIGRATIONS)} OK") except Exception as e: log.error(f"migration {i} FAILED: {e}") raise