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
117 lines
4.7 KiB
Python
117 lines
4.7 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: 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
|
|
|
|
"""
|
|
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)
|
|
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
|
|
# 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")
|