CI / extension-version (push) Successful in 6s
CI / lint (push) Failing after 6s
Build images / sign-extension (push) Successful in 6s
Build images / build-agent (push) Successful in 11s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-ml (push) Successful in 52s
Build images / build-web (push) Successful in 41s
CI / integration (push) Successful in 3m43s
Run 5029 confirmed the four renames landed and surfaced two I had missed: external_link's host and status CHECKs are doubled the same way. They did not show in run 5026's diff because BOTH sides produced the doubled form back then — external_link.py pre-prefixed its names, so the models matched the chain's mistake. Switching all six models to bare names is what exposed the two the migration did not cover. The list in the file now comes from matching ck_(\w+?)_ck_\1_ against the chain's own pg_dump, rather than from reading migrations by eye. Reading by eye is what missed these, in the same way it earlier missed a UNIQUE constraint sitting two lines above the index being looked at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QHszn9H8VBvx5Ke8x1hvw
129 lines
5.4 KiB
Python
129 lines
5.4 KiB
Python
"""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")
|