This reverts 2529b51. Not a retreat — a reordering, on the operator's
call, and the better sequence.
The squash's acceptance test (run 4971) found ~130 places where the ORM
models do not describe the deployed schema (#3275), including a
unique=True the database never had and two UNIQUE indexes that exist
only in migrations. Collapsing now would have baked all of that into the
one file a public installer starts from.
So: fix the drift first as ordinary migrations on the intact chain, let
the operator deploy so their database moves to the corrected head, and
only then collapse. The baseline is then generated from reconciled
models and reproduces a schema worth reproducing.
Nothing is lost by reverting. The baseline was never deployed, and
regenerating it after the fixes is strictly better than patching this
copy — it will come out of autogenerate correct rather than needing the
same hand-finishing twice.
70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
"""image_prediction table (DDL only — backfill runs as a background task)
|
|
|
|
Normalizes the per-image tagger predictions out of the JSON blob into a
|
|
queryable table (#768). This migration creates ONLY the table + indexes — it
|
|
is pure DDL and commits instantly, so web boots immediately.
|
|
|
|
The data backfill from the existing image_record.tagger_predictions JSON is
|
|
deliberately NOT done here. Doing it inline made the whole migration one
|
|
transaction over the ~100 GB TOAST: nothing committed until the very end, it
|
|
was invisible/unmonitorable mid-run, and an early MATERIALIZED-CTE form spilled
|
|
the full 100 GB to temp. Instead the backfill is the
|
|
backend.app.tasks.admin.backfill_image_predictions_task — batched by id window,
|
|
committed per chunk (visible progress + resumable), idempotent
|
|
(ON CONFLICT DO NOTHING). Trigger it from Settings → Maintenance once web is up.
|
|
|
|
The old image_record.tagger_predictions column is left in place (vestigial) and
|
|
dropped in a follow-up once the backfill + code cutover are verified — dropping
|
|
it needs an ACCESS EXCLUSIVE lock on the hot image_record table (the 0044 lock
|
|
class), so it's deferred to a quiesced-worker window.
|
|
|
|
Revision ID: 0045
|
|
Revises: 0044
|
|
Create Date: 2026-06-10
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "0045"
|
|
down_revision: Union[str, None] = "0044"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"image_prediction",
|
|
sa.Column("id", sa.Integer(), primary_key=True),
|
|
sa.Column(
|
|
"image_record_id", sa.Integer(),
|
|
sa.ForeignKey("image_record.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("raw_name", sa.String(length=255), nullable=False),
|
|
sa.Column("category", sa.String(length=64), nullable=False),
|
|
sa.Column("score", sa.Float(), nullable=False),
|
|
sa.UniqueConstraint(
|
|
"image_record_id", "raw_name", name="image_raw_name",
|
|
),
|
|
)
|
|
op.create_index(
|
|
"ix_image_prediction_image", "image_prediction", ["image_record_id"],
|
|
)
|
|
op.create_index(
|
|
"ix_image_prediction_name_score", "image_prediction",
|
|
["raw_name", "score"],
|
|
)
|
|
# No data backfill here — see the module docstring. The one-time copy from
|
|
# image_record.tagger_predictions runs as backfill_image_predictions_task
|
|
# (batched, resumable, idempotent), kept out of this transaction so web boots
|
|
# without waiting on a ~100 GB pass.
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("ix_image_prediction_name_score", "image_prediction")
|
|
op.drop_index("ix_image_prediction_image", "image_prediction")
|
|
op.drop_table("image_prediction")
|