116 lines
4.0 KiB
Python
116 lines
4.0 KiB
Python
"""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"
|
|
))
|