"""Reconcile the database with what the models have always claimed (#3275). Milestone 328 discovered ~130 places where the ORM models and the deployed schema disagreed. Almost all of them were the MODEL being wrong — missing `server_default`s, indexes and CHECK constraints that only ever existed in a migration — and those are fixed in the model files with no DDL at all, because the database already had them. This migration carries the remainder — the two places where DDL is actually needed, because the database is what is wrong. `tag.fandom_id` is declared `index=True` on the model, but no migration ever created that index. Every autogenerate run since would have proposed adding it; nobody ran one, so the model and the database simply drifted apart and stayed that way. Deliberately NOT in this migration: anything about `image_record.sha256`. An earlier draft of this file claimed sha256 was not unique in the database and that duplicate rows were therefore possible. That was WRONG, and it was wrong because it was read off `op.create_index("ix_image_record_sha256", ...)` at 0001 line 151 without reading line 149 two lines above it: sa.UniqueConstraint("sha256", name="uq_image_record_sha256"), Uniqueness has been enforced since the initial schema. The database simply expresses it as a CONSTRAINT plus a separate non-unique lookup index, where the model expressed it as one `unique=True, index=True` column — the same guarantee built from different objects, which is why the two schemas did not line up. The model now declares the constraint and the plain index separately, so it describes what is actually there. No DDL is needed for it. Also here: six CHECK constraints whose names carry their table prefix TWICE. `base.py`'s naming convention is `ck_%(table_name)s_%(constraint_name)s`, and unlike the uq/fk/ix entries it applies even to a constraint that already has a name. Six migrations passed an already-prefixed name, so the convention prefixed it again: ck_external_link_ck_external_link_host ck_external_link_ck_external_link_status ck_import_settings_ck_import_settings_singleton ck_ml_settings_ck_ml_settings_singleton ck_post_ck_post_translation_override ck_tag_ck_tag_fandom_requires_character Nothing reads a CHECK constraint by name, so this has never done any harm — but it is exactly the development-era residue the collapsed baseline exists to leave behind, and a public schema should not ship it. The models now declare bare names, which the convention renders into the single-prefix form; this renames the deployed constraints to match. RENAME CONSTRAINT is a catalog-only operation: no table scan, no rewrite, no validation of existing rows. It takes a brief ACCESS EXCLUSIVE lock and returns. That is why this is safe to do on `post` and `tag`, which are the two large tables in the schema. Revision ID: 0088 Revises: 0087 Create Date: 2026-08-30 """ from typing import Sequence, Union from alembic import op revision: str = "0088" down_revision: Union[str, None] = "0087" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None # (table, doubled name, single-prefix name) # # Six, not the four a first read of the migrations turned up. The list that # settles it is the one extracted from the chain's pg_dump by matching # `ck_(\w+?)_ck_\1_` — reading the migrations by eye missed external_link # twice over, in the same way an earlier pass missed a UNIQUE constraint two # lines above the index it was looking at (see the sha256 note above). DOUBLED_CHECKS = ( ("external_link", "ck_external_link_ck_external_link_host", "ck_external_link_host"), ("external_link", "ck_external_link_ck_external_link_status", "ck_external_link_status"), ("import_settings", "ck_import_settings_ck_import_settings_singleton", "ck_import_settings_singleton"), ("ml_settings", "ck_ml_settings_ck_ml_settings_singleton", "ck_ml_settings_singleton"), ("post", "ck_post_ck_post_translation_override", "ck_post_translation_override"), ("tag", "ck_tag_ck_tag_fandom_requires_character", "ck_tag_fandom_requires_character"), ) def _rename_check(table: str, old: str, new: str) -> None: # Guarded on pg_constraint rather than run bare: a database built from the # models (a fresh install, or the CI integration schema) already has the # single-prefix name, and this migration must be a no-op there rather than # an error. Same reasoning as the CREATE INDEX IF NOT EXISTS below. op.execute( f""" DO $$ BEGIN IF EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = '{old}' AND conrelid = '{table}'::regclass ) THEN ALTER TABLE {table} RENAME CONSTRAINT {old} TO {new}; END IF; END $$; """ ) def upgrade() -> None: # IF NOT EXISTS because the index is what the model already asks for: any # database built from metadata rather than from this chain will have it, # and this migration must be a no-op there rather than an error. op.execute("CREATE INDEX IF NOT EXISTS ix_tag_fandom_id ON tag (fandom_id)") for table, old, new in DOUBLED_CHECKS: _rename_check(table, old, new) def downgrade() -> None: for table, old, new in DOUBLED_CHECKS: _rename_check(table, new, old) op.execute("DROP INDEX IF EXISTS ix_tag_fandom_id")