Build images / sign-extension (push) Successful in 4s
CI / lint (push) Failing after 2s
CI / extension-version (push) Successful in 2s
Build images / build-agent (push) Successful in 7s
CI / frontend-build (push) Successful in 27s
Build images / build-ml (push) Successful in 48s
CI / backend-lint-and-test (push) Successful in 1m7s
Build images / build-web (push) Successful in 40s
CI / integration (push) Successful in 4m1s
Milestone 328's acceptance test compared a database built by the real
0001..0087 chain against one built from the models, and found ~130
places where they disagree. This closes them.
Almost all were the MODEL being wrong, so almost all of this is model
edits with no DDL — the database already had these things, nothing in it
changes, and no deploy is needed for this part:
* 92 columns gained server_default. The models carried Python-side
`default=` only, so the ORM filled the value and the column had no
database default. Anything inserting outside the ORM behaved
differently from production.
* Eleven indexes that existed only in migrations are now declared:
the three backup_run reporting indexes, the two date-ordered
image_record browse indexes, import_task and presentation_review,
and the three task_run history indexes. All use text() for their DESC
ordering and postgresql_where for the partial one.
* Two UNIQUE indexes that autogenerate silently proposed DROPPING,
because neither is expressible as a UniqueConstraint:
uq_tag_name_kind_fandom — an EXPRESSION index over
(name, kind, COALESCE(fandom_id, 0))
uq_post_artist_external_id_null_source — PARTIAL, WHERE source_id
IS NULL
post.py already had a comment describing the second one. The comment
was right; nothing declared it.
* The two external_link enum CHECKs (host, status) — rule 36 territory,
and absent from the model entirely.
* Two indexes were named explicitly. A bare index=True generated
ix_tag_alias_canonical_tag_id where the database has
ix_tag_alias_canonical, so autogenerate proposed a drop+create of an
index that was already there under another name. Same for
tag_suggestion_rejection.
Only ONE thing needed DDL, as 0088: tag.fandom_id is declared
index=True but no migration ever created that index.
Deliberately NOT here: image_record.sha256. The model says unique=True;
0001 created a plain index. Duplicates are possible today and the ORM
believes otherwise. The fix depends on whether duplicates already exist
— if they do, that is a dedupe decision, not a constraint — so it waits
on an answer about live data.
The real severity of #3275 is not the squash. It is that --autogenerate
has been unsafe on this project: run against the old models it would
have proposed dropping eleven indexes and two uniqueness guarantees.
49 lines
1.9 KiB
Python
49 lines
1.9 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 one case where the MODEL was right
|
|
and the database was missing something.
|
|
|
|
`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: making `image_record.sha256` unique. The
|
|
model says `unique=True` and `0001` created a plain, non-unique index, so
|
|
duplicates are possible today and the ORM believes they are not. Adding the
|
|
constraint is a real change that FAILS if duplicates already exist, and if
|
|
they do exist the right response is a dedupe decision rather than a constraint
|
|
— so it needs an answer about live data before it is written, not after.
|
|
Tracked in #3275.
|
|
|
|
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
|
|
|
|
|
|
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)")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP INDEX IF EXISTS ix_tag_fandom_id")
|