124 lines
4.7 KiB
Python
124 lines
4.7 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 id note
|
|
----------------
|
|
`alembic_version.version_num` is VARCHAR(32). The descriptive form
|
|
'phase48_schedule_parent_inspection' is 34 characters and fails the stamp UPDATE
|
|
after the DDL has already run. The id is abbreviated to fit, matching the
|
|
single-tenant chain's own 'phase45_sched_parent_insp'. The FILENAME stays
|
|
descriptive — Alembic keys on the `revision` string, not the filename.
|
|
"""
|
|
|
|
revision = 'phase48_sched_parent_insp'
|
|
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}"))
|