Jul 30 - Internal handler files

This commit is contained in:
2026-07-30 12:14:04 -04:00
parent d9bf4be709
commit ceb0b806af
7 changed files with 323 additions and 20 deletions
@@ -0,0 +1,110 @@
"""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"))