111 lines
4.8 KiB
Python
111 lines
4.8 KiB
Python
"""phase44 — issue internal_handler_name / internal_handler_contact,
|
|
and convergence of handler_type to NOT NULL DEFAULT 'internal'
|
|
|
|
Ports single-tenant phase41 + phase42 into the multi-tenant chain, and closes the
|
|
last divergence in the handler model.
|
|
|
|
1. Adds `internal_handler_name` and `internal_handler_contact` to `issues`,
|
|
capturing the janitorial staff member who will handle an issue when
|
|
handler_type == 'internal'. Distinct from `assigned_to` (the JQC User who owns
|
|
follow-up) — the actual crew member may not be a system user. Parallels the
|
|
existing facility_handler_* and vendor_* contact pairs.
|
|
|
|
2. Converges `handler_type` from MT's `NULL`-able column onto the single-tenant
|
|
definition, `NOT NULL DEFAULT 'internal'`. MT's application code already
|
|
treats NULL and 'internal' as the same thing (`handler_type or 'internal'`
|
|
appears in the model property, the API payload and the templates), so this
|
|
only removes a redundant second representation of "internal". Existing NULL
|
|
rows are backfilled to 'internal' BEFORE the MODIFY, otherwise the ALTER
|
|
fails on a table containing NULLs.
|
|
|
|
RE-RUNNABLE. Step 1 uses an INFORMATION_SCHEMA column-existence check. Step 2
|
|
cannot use one — the column already exists; what changes is its nullability — so
|
|
it checks IS_NULLABLE instead and skips when the column is already NOT NULL.
|
|
Checking only for existence there would silently make this migration a no-op on
|
|
re-run against a half-applied schema.
|
|
|
|
No batch_alter_table (MySQL).
|
|
"""
|
|
|
|
revision = 'phase44_internal_handler'
|
|
down_revision = 'phase43_schedule_plan_fields'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
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 _column_is_nullable(conn, table, column):
|
|
"""True when the column exists AND is declared NULL-able.
|
|
|
|
Returns False for a missing column so callers never attempt to MODIFY
|
|
something that isn't there.
|
|
"""
|
|
result = conn.execute(sa.text(
|
|
"SELECT IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS "
|
|
"WHERE TABLE_SCHEMA = DATABASE() "
|
|
"AND TABLE_NAME = :t AND COLUMN_NAME = :c"
|
|
), {"t": table, "c": column})
|
|
row = result.first()
|
|
return bool(row) and row[0] == 'YES'
|
|
|
|
|
|
def upgrade():
|
|
bind = op.get_bind()
|
|
|
|
# ── 1. internal_handler_name ──────────────────────────────────────────
|
|
if not _column_exists(bind, 'issues', 'internal_handler_name'):
|
|
op.execute(sa.text(
|
|
"ALTER TABLE issues ADD COLUMN internal_handler_name VARCHAR(100) NULL"
|
|
))
|
|
|
|
# ── 2. internal_handler_contact ───────────────────────────────────────
|
|
if not _column_exists(bind, 'issues', 'internal_handler_contact'):
|
|
op.execute(sa.text(
|
|
"ALTER TABLE issues ADD COLUMN internal_handler_contact VARCHAR(200) NULL"
|
|
))
|
|
|
|
# ── 3. handler_type -> NOT NULL DEFAULT 'internal' ────────────────────
|
|
# Backfill first: MODIFY ... NOT NULL fails outright if any row holds NULL.
|
|
# This runs unconditionally (it is itself idempotent — a second run matches
|
|
# zero rows) so the data is correct even if a previous attempt aborted
|
|
# between the UPDATE and the ALTER.
|
|
if _column_is_nullable(bind, 'issues', 'handler_type'):
|
|
op.execute(sa.text(
|
|
"UPDATE issues SET handler_type = 'internal' WHERE handler_type IS NULL"
|
|
))
|
|
op.execute(sa.text(
|
|
"ALTER TABLE issues MODIFY COLUMN handler_type "
|
|
"ENUM('internal','facility','vendor') NOT NULL DEFAULT 'internal'"
|
|
))
|
|
|
|
|
|
def downgrade():
|
|
bind = op.get_bind()
|
|
|
|
# Revert handler_type to nullable with no default. Stored values are left
|
|
# as-is: rows that were NULL before the upgrade are now 'internal', which is
|
|
# semantically identical under the application's `handler_type or 'internal'`
|
|
# reads, so there is nothing to undo in the data.
|
|
if not _column_is_nullable(bind, 'issues', 'handler_type'):
|
|
op.execute(sa.text(
|
|
"ALTER TABLE issues MODIFY COLUMN handler_type "
|
|
"ENUM('internal','facility','vendor') NULL"
|
|
))
|
|
|
|
if _column_exists(bind, 'issues', 'internal_handler_contact'):
|
|
op.execute(sa.text("ALTER TABLE issues DROP COLUMN internal_handler_contact"))
|
|
|
|
if _column_exists(bind, 'issues', 'internal_handler_name'):
|
|
op.execute(sa.text("ALTER TABLE issues DROP COLUMN internal_handler_name"))
|