Jul 30 - Internal handler files
This commit is contained in:
@@ -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"))
|
||||
Reference in New Issue
Block a user