db: rename the four double-prefixed CHECK constraints (#3275)
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Failing after 5s
CI / extension-version (push) Successful in 5s
Build images / build-agent (push) Successful in 9s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-ml (push) Successful in 44s
Build images / build-web (push) Successful in 41s
CI / integration (push) Successful in 3m52s

Run 5026 got the models-vs-chain diff to 7 lines. Three findings, and one
of them reverses an assumption I made in the previous commit.

The doubled CHECK names are what the DATABASE has, not what the generator
invented. base.py's convention is ck_%(table_name)s_%(constraint_name)s,
which — unlike uq/fk/ix — applies even to a constraint that already has a
name, so four migrations that passed an already-prefixed name got it
prefixed twice:

    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

The workflow repair added last commit is still correct and still needed —
autogenerate really does re-double a name on the round trip — but it was
making the MODELS side clean against a chain that is dirty. The
comment in ml_settings.py claiming its bare name "matches migration 0003"
was simply false; 0003 produces the doubled form.

Nothing reads a CHECK constraint by name, so this has never done harm.
But it is precisely the development-era residue the collapsed baseline
exists to leave behind, and a public schema should not ship it — so 0088
renames the deployed constraints and all six models now declare bare
names. RENAME CONSTRAINT is catalog-only: no scan, no rewrite, no
revalidation, which is why this is safe on post and tag. Guarded on
pg_constraint scoped by conrelid, so it is a no-op on a database built
from the models.

ix_tag_fandom_id showed as a difference only because chain_ref was pinned
to 0a5bbe8, which predates 0088 — the comparison was measuring the models
against a chain missing the migration that closes the gap. chain_ref now
defaults to blank, meaning "the chain in this ref". Pin it to a commit
only after the collapse, when the tree no longer carries the revisions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QHszn9H8VBvx5Ke8x1hvw
This commit is contained in:
2026-08-31 00:29:57 -04:00
co-authored by Claude Opus 5
parent 573228b9da
commit b979062dd7
7 changed files with 96 additions and 10 deletions
@@ -6,8 +6,8 @@ schema disagreed. Almost all of them were the MODEL being wrong — missing
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 one case where the MODEL was right
and the database was missing something.
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
@@ -29,6 +29,29 @@ 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: four 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. Four migrations passed an already-prefixed name, so the convention
prefixed it again:
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
@@ -43,6 +66,38 @@ 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)
DOUBLED_CHECKS = (
("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
@@ -50,6 +105,12 @@ def upgrade() -> None:
# 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")