Aug 4 - Update code to follow up - MT11

This commit is contained in:
2026-08-04 13:00:21 -04:00
parent ceb0b806af
commit 3c2489e289
10 changed files with 1468 additions and 49 deletions
@@ -0,0 +1,115 @@
"""phase45 — widen inspection_schedules.frequency ENUM
Converges MT's cadence set onto the agreed union:
once | daily | weekly | monthly | quarterly | bi-annually | annually
'bi-annually' means every 6 months (not twice a year).
Why now
-------
phase46 (recurrence) and phase47 (end date) both branch on 'once':
* a one-time schedule deactivates on completion instead of rolling forward;
* a one-time schedule may not carry an end date — it ends by being completed.
Adding the value later would mean a second ENUM rewrite of the same table.
'bi-annually' and 'annually' are added in the same pass for the same reason.
The original four values are retained in their original order, so NO existing
row changes and no data migration is needed. This is purely a widening.
MySQL ENUM changes
------------------
`MODIFY COLUMN` on an ENUM cannot be done in place — MySQL rebuilds the table.
`ALGORITHM=COPY, LOCK=SHARED` is stated explicitly rather than left to the
server's default so the behaviour is predictable across versions: the table is
readable throughout and blocks writes for the duration of the copy. It also
takes a metadata lock, which will WAIT behind any long-running transaction
touching inspection_schedules — see the deploy notes. The table is small (one
row per schedule), so the copy itself is fast.
No batch_alter_table (MySQL).
RE-RUNNABLE. An existence check is not enough here — the column already exists;
what changes is its type. This inspects COLUMN_TYPE and skips when the target
values are already present, so a re-run against a fully-migrated schema is a
genuine no-op rather than a needless table rebuild.
"""
revision = 'phase45_schedule_frequency_enum'
down_revision = 'phase44_internal_handler'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
_TABLE = 'inspection_schedules'
_COLUMN = 'frequency'
_NEW_ENUM = ("ENUM('once','daily','weekly','monthly','quarterly',"
"'bi-annually','annually')")
_OLD_ENUM = "ENUM('daily','weekly','monthly','quarterly')"
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_type(conn, table, column):
"""Lowercased COLUMN_TYPE, or None when the column does not exist."""
row = conn.execute(sa.text(
"SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = :t AND COLUMN_NAME = :c"
), {"t": table, "c": column}).first()
if not row:
return None
value = row[0]
if isinstance(value, bytes):
value = value.decode('utf-8', 'replace')
return value.lower()
def upgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
col_type = _column_type(bind, _TABLE, _COLUMN)
if col_type is None:
return
# Already widened? Every new value present means there is nothing to do.
if all(v in col_type for v in ("'once'", "'bi-annually'", "'annually'")):
return
op.execute(sa.text(
f"ALTER TABLE {_TABLE} MODIFY COLUMN {_COLUMN} {_NEW_ENUM} "
f"NOT NULL DEFAULT 'weekly', ALGORITHM=COPY, LOCK=SHARED"
))
def downgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
col_type = _column_type(bind, _TABLE, _COLUMN)
if col_type is None or "'once'" not in col_type:
return
# Rows holding a value the narrow ENUM cannot represent would be silently
# truncated to '' by the MODIFY. Map them to the nearest surviving cadence
# first so the downgrade is lossy in a defined, inspectable way rather than
# producing invalid empty-string rows.
op.execute(sa.text(
f"UPDATE {_TABLE} SET {_COLUMN} = 'monthly' "
f"WHERE {_COLUMN} IN ('once','bi-annually','annually')"
))
op.execute(sa.text(
f"ALTER TABLE {_TABLE} MODIFY COLUMN {_COLUMN} {_OLD_ENUM} "
f"NOT NULL DEFAULT 'weekly', ALGORITHM=COPY, LOCK=SHARED"
))