Files
JQC_multi_tenant/migrations/versions/phase47_schedule_end_date.py
T

74 lines
2.6 KiB
Python

"""phase47 — inspection schedule end date
Ports single-tenant phase44 onto MT's `inspection_schedules` table. Adds:
end_date DATE NULL -- last date this schedule may produce an occurrence
Separates the two ideas that `next_run_at` was carrying at once. `next_run_at`
is *mutable state* — InspectionSchedule.advance_due_date() rewrites it after
every occurrence, whether the inspector completed a plan-mode schedule or the
cron materialised an auto-mode one — whereas `end_date` is a *fixed boundary*
set by the manager and never touched by the app. NULL means "repeat
indefinitely", which is the behaviour every existing row has today, so there is
no backfill and no schedule changes cadence on deploy.
Applies to BOTH modes. Scoping it to plan mode would leave an auto schedule
materialising inspections past its boundary forever, which is the exact failure
the column exists to prevent.
Only meaningful for recurring schedules; the create/edit routes force it to NULL
when frequency == 'once' (a one-time schedule already ends by deactivating on
completion) and reject an end date submitted against one.
Uses an INFORMATION_SCHEMA column-existence check — safe to re-run on every
tenant DB. Additive only: no existing column is renamed, retyped or dropped. In
particular `next_run_at` keeps its name and its index — it is the API payload
key (`next_due_date`) the iPad decodes and drives the cron.
"""
revision = 'phase47_schedule_end_date'
down_revision = 'phase46_schedule_recurrence'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
_TABLE = 'inspection_schedules'
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 upgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
if not _column_exists(bind, _TABLE, 'end_date'):
op.execute(sa.text(
f"ALTER TABLE {_TABLE} ADD COLUMN end_date DATE NULL AFTER next_run_at"
))
def downgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
if _column_exists(bind, _TABLE, 'end_date'):
op.execute(sa.text(
f"ALTER TABLE {_TABLE} DROP COLUMN end_date"
))