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.
64 lines
2.7 KiB
Python
64 lines
2.7 KiB
Python
"""FC-3h: backup_run — operator-facing artifact record for a backup run.
|
|
|
|
One row per backup attempt (kind='db' or 'images'). Lifecycle
|
|
tracking (started_at/finished_at/duration_ms/exception text) lives
|
|
in task_run from FC-3i — this row records artifact metadata: file
|
|
paths, sizes, tag (retention protection), and restore lineage via
|
|
restored_from_id.
|
|
|
|
Status values (String, not Postgres ENUM — per
|
|
feedback_check_existing_enums):
|
|
pending — created but task hasn't started yet (rare; usually
|
|
status starts as 'running' from the task body).
|
|
running — backup task is in flight.
|
|
ok — artifact successfully written.
|
|
error — task raised; error column populated.
|
|
restoring — this row represents a restore attempt (kind = restored
|
|
kind); linked to source via restored_from_id.
|
|
restored — restore completed successfully.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import BigInteger, DateTime, ForeignKey, Index, Integer, JSON, String, Text, text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from .base import Base
|
|
|
|
|
|
class BackupRun(Base):
|
|
__tablename__ = "backup_run"
|
|
|
|
|
|
__table_args__ = (
|
|
# alembic 0017: reporting indexes, never declared on the model (#3275).
|
|
Index("ix_backup_run_kind_started", "kind", text("started_at DESC")),
|
|
Index("ix_backup_run_status_finished", "status", text("finished_at DESC")),
|
|
Index("ix_backup_run_tag_partial", "tag", postgresql_where=text("tag IS NOT NULL")),
|
|
)
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
kind: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
|
|
status: Mapped[str] = mapped_column(
|
|
String(16), nullable=False, default="pending", index=True,
|
|
server_default="pending",
|
|
)
|
|
tag: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
triggered_by: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
started_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, index=True,
|
|
)
|
|
finished_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True, index=True,
|
|
)
|
|
sql_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
tar_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
size_bytes: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
manifest: Mapped[dict] = mapped_column(
|
|
JSON, nullable=False, default=dict, server_default="{}",
|
|
)
|
|
restored_from_id: Mapped[int | None] = mapped_column(
|
|
ForeignKey("backup_run.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
)
|