"""phase46 — inspection schedule day-of-week / day-of-month recurrence Ports single-tenant phase43 onto MT's `inspection_schedules` table (MT's name for the same thing — no rename, rule 7). Adds the recurrence-detail columns so a weekly schedule can name the weekdays it runs on (Mon/Wed/Fri) and a monthly (or quarterly / bi-annual / annual) schedule can name either a day of the month ("the 15th") or an nth weekday ("the 2nd Tuesday"): weekdays VARCHAR(20) -- CSV of Python weekday ints, Mon=0, e.g. '0,2,4' month_mode VARCHAR(20) -- 'day_of_month' | 'nth_weekday' day_of_month SMALLINT -- 1–31, clamped to the month's last day nth_week SMALLINT -- 1–5, or -1 for "last" nth_weekday SMALLINT -- 0–6, Mon=0 All nullable with no backfill: existing phase34/phase43 rows keep NULLs and fall back to the plain-interval behaviour in InspectionSchedule._add_interval(), so no schedule changes cadence on deploy. `next_run_at` is NOT touched — it keeps its name, its DATETIME type and its index. It remains the due datetime (ST's `next_due_date` by another name); the recurrence maths runs on its DATE part and preserves its TIME part. `month_mode` is VARCHAR rather than ENUM so adding a recurrence style later needs no 3-step MySQL ENUM migration. Uses INFORMATION_SCHEMA column-existence checks — safe to re-run on every tenant DB. Additive only: nothing is renamed, retyped or dropped. """ revision = 'phase46_schedule_recurrence' down_revision = 'phase45_schedule_frequency_enum' branch_labels = None depends_on = None from alembic import op import sqlalchemy as sa _TABLE = 'inspection_schedules' _COLUMNS = ( ('weekdays', 'VARCHAR(20) NULL'), ('month_mode', 'VARCHAR(20) NULL'), ('day_of_month', 'SMALLINT NULL'), ('nth_week', 'SMALLINT NULL'), ('nth_weekday', 'SMALLINT NULL'), ) def _table_exists(conn, table): return conn.execute(sa.text( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t" ), {"t": table}).scalar() > 0 def _column_exists(conn, table, column): return conn.execute(sa.text( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() " "AND TABLE_NAME = :t AND COLUMN_NAME = :c" ), {"t": table, "c": column}).scalar() > 0 def upgrade(): bind = op.get_bind() if not _table_exists(bind, _TABLE): return for name, ddl in _COLUMNS: if not _column_exists(bind, _TABLE, name): op.execute(sa.text( f"ALTER TABLE {_TABLE} ADD COLUMN {name} {ddl}" )) def downgrade(): bind = op.get_bind() if not _table_exists(bind, _TABLE): return for name, _ddl in reversed(_COLUMNS): if _column_exists(bind, _TABLE, name): op.execute(sa.text( f"ALTER TABLE {_TABLE} DROP COLUMN {name}" ))