67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
"""phase43 — scheduled inspection day-of-week / day-of-month recurrence
|
||
|
||
Adds the recurrence-detail columns to `scheduled_inspections` so a weekly
|
||
schedule can name the weekdays it runs on (Mon/Wed/Fri) and a monthly 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 phase36 rows keep NULLs and fall back to
|
||
the legacy "+7 days" / "same day next month" behaviour in
|
||
ScheduledInspection._add_interval(), so no schedule changes cadence on deploy.
|
||
|
||
`month_mode` is VARCHAR rather than ENUM so adding a recurrence style later
|
||
needs no 3-step MySQL ENUM migration (CLAUDE.md rule 3).
|
||
|
||
Uses INFORMATION_SCHEMA column-existence checks — safe to re-run.
|
||
"""
|
||
|
||
revision = 'phase43_sched_recurrence'
|
||
down_revision = 'phase42_internal_contact'
|
||
branch_labels = None
|
||
depends_on = None
|
||
|
||
from alembic import op
|
||
import sqlalchemy as sa
|
||
|
||
|
||
_COLUMNS = (
|
||
('weekdays', 'VARCHAR(20) NULL'),
|
||
('month_mode', 'VARCHAR(20) NULL'),
|
||
('day_of_month', 'SMALLINT NULL'),
|
||
('nth_week', 'SMALLINT NULL'),
|
||
('nth_weekday', 'SMALLINT NULL'),
|
||
)
|
||
|
||
|
||
def _column_exists(conn, table, column):
|
||
result = 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})
|
||
return result.scalar() > 0
|
||
|
||
|
||
def upgrade():
|
||
bind = op.get_bind()
|
||
for name, ddl in _COLUMNS:
|
||
if not _column_exists(bind, 'scheduled_inspections', name):
|
||
op.execute(sa.text(
|
||
f"ALTER TABLE scheduled_inspections ADD COLUMN {name} {ddl}"
|
||
))
|
||
|
||
|
||
def downgrade():
|
||
bind = op.get_bind()
|
||
for name, _ in reversed(_COLUMNS):
|
||
if _column_exists(bind, 'scheduled_inspections', name):
|
||
op.execute(sa.text(
|
||
f"ALTER TABLE scheduled_inspections DROP COLUMN {name}"
|
||
))
|