"""phase42 — areas.qr_token for per-area public QR landing pages Adds a unique, unguessable token per area. Each area's QR encodes /f/area/, a login-free summary scoped to that area plus a "report a problem" form. Ported from the single-tenant chain (phase39_area_public_token) and adapted: the MT column is named `qr_token` (matching `facilities.qr_token` from phase38_facility_qr), not `public_token`, and is VARCHAR(64) to match the facility column and MT's `secrets.token_urlsafe(32)` generator. Unlike the ST original, existing areas are NOT backfilled: MT mints tokens lazily via `Area.ensure_qr_token()` on first use, exactly as `Facility.ensure_qr_token()` already does. A backfill would mint tokens for areas nobody ever prints a code for. Uses INFORMATION_SCHEMA checks — safe to re-run on every tenant DB. """ revision = 'phase42_area_qr_token' down_revision = 'phase41_auditor_role' branch_labels = None depends_on = None from alembic import op import sqlalchemy as sa 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 _index_exists(conn, table, index): return conn.execute(sa.text( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS " "WHERE TABLE_SCHEMA = DATABASE() " "AND TABLE_NAME = :t AND INDEX_NAME = :i" ), {"t": table, "i": index}).scalar() > 0 def upgrade(): bind = op.get_bind() if not _column_exists(bind, 'areas', 'qr_token'): op.execute(sa.text( "ALTER TABLE areas ADD COLUMN qr_token VARCHAR(64) NULL" )) # Unique index tolerates multiple NULLs in MySQL, so it can be created # immediately — no backfill needed before enforcing uniqueness. if not _index_exists(bind, 'areas', 'uq_area_qr_token'): op.execute(sa.text( "CREATE UNIQUE INDEX uq_area_qr_token ON areas (qr_token)" )) def downgrade(): bind = op.get_bind() if _index_exists(bind, 'areas', 'uq_area_qr_token'): op.execute(sa.text("DROP INDEX uq_area_qr_token ON areas")) if _column_exists(bind, 'areas', 'qr_token'): op.execute(sa.text("ALTER TABLE areas DROP COLUMN qr_token"))