Files
JQC_multi_tenant/migrations/versions/phase48_schedule_parent_inspection.py
T

116 lines
4.3 KiB
Python

"""phase48 — scheduled follow-up: link a schedule back to its parent inspection
Ports single-tenant phase45 onto MT's `inspection_schedules` table. Adds:
parent_inspection_id INT NULL -- inspection this schedule is a follow-up of
Lets a follow-up be *planned for a later date* rather than started immediately —
the deferred twin of "Re-inspect Now". A one-time schedule carrying this column
is created via POST /api/v1/scheduled-inspections/follow-up; when the inspector
eventually starts it, the resulting inspection inherits `parent_inspection_id`,
so it lands as a true linked re-inspection: pre-filled from the parent, and
clearing the parent's `follow_up_required` on submit.
Without the column the scheduled run would be an ordinary inspection — no link,
no prefill, and the parent's follow-up flag would stay set forever.
Depends on phase45's `once` frequency: a follow-up is a single planned visit,
not a recurrence.
NULL means "not a follow-up", which is what every existing row is, so there is
no backfill and no schedule changes behaviour on deploy.
ON DELETE SET NULL: deleting the parent inspection must not cascade away a
schedule the inspector still has to perform — it just stops being a follow-up.
This mirrors `inspections.inspection_schedule_id`, which points the other way
with the same rule, so neither side of the pair can delete the other's history.
Uses INFORMATION_SCHEMA existence checks — safe to re-run on every tenant DB.
Additive only: nothing is renamed, retyped or dropped.
"""
revision = 'phase48_schedule_parent_inspection'
down_revision = 'phase47_schedule_end_date'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
_TABLE = 'inspection_schedules'
_COLUMN = 'parent_inspection_id'
_FK = 'fk_inspection_schedules_parent_inspection'
_INDEX = 'ix_inspection_schedules_parent_inspection_id'
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_exists(conn, table, column):
return 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}).scalar() > 0
def _constraint_exists(conn, table, name):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = :t AND CONSTRAINT_NAME = :n"
), {"t": table, "n": name}).scalar() > 0
def _index_exists(conn, table, name):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = :t AND INDEX_NAME = :n"
), {"t": table, "n": name}).scalar() > 0
def upgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
if not _column_exists(bind, _TABLE, _COLUMN):
op.execute(sa.text(
f"ALTER TABLE {_TABLE} ADD COLUMN {_COLUMN} INT NULL AFTER notes"
))
# Named explicitly so the downgrade and the re-run check can find it. MySQL
# auto-creates an index for a foreign key, but only when no usable index
# exists; creating it first means the name is ours and predictable.
if not _index_exists(bind, _TABLE, _INDEX):
op.execute(sa.text(
f"CREATE INDEX {_INDEX} ON {_TABLE} ({_COLUMN})"
))
if not _constraint_exists(bind, _TABLE, _FK):
op.execute(sa.text(
f"ALTER TABLE {_TABLE} ADD CONSTRAINT {_FK} "
f"FOREIGN KEY ({_COLUMN}) REFERENCES inspections(id) "
f"ON DELETE SET NULL"
))
def downgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
# FK first — MySQL refuses to drop a column or index still referenced by one.
if _constraint_exists(bind, _TABLE, _FK):
op.execute(sa.text(f"ALTER TABLE {_TABLE} DROP FOREIGN KEY {_FK}"))
if _index_exists(bind, _TABLE, _INDEX):
op.execute(sa.text(f"DROP INDEX {_INDEX} ON {_TABLE}"))
if _column_exists(bind, _TABLE, _COLUMN):
op.execute(sa.text(f"ALTER TABLE {_TABLE} DROP COLUMN {_COLUMN}"))