Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 11s
Build images / build-ml (push) Successful in 32s
Build images / build-web (push) Successful in 26s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m44s
extension / lint (pull_request) Successful in 24s
A structural sweep of the deployed schema, run AFTER 0088 got the models and the chain to exact agreement. That agreement is what 0088 achieved, and it is worth naming what it does not prove: a models-vs-chain diff shows the two describe the same schema, not that the schema is right. Everything here was wrong in BOTH. The one that matters: image_tag has PRIMARY KEY (image_record_id, tag_id) and no other index, so tag_id is unindexed. That is the gallery's tag filter (tag_query.py builds `image_tag.c.tag_id == tid`) and the ON DELETE CASCADE from tag, both scanning the largest table in the schema. Six more FKs were unindexed on smaller tables; presentation_review.tag_id also CASCADEs. Dropped, on the other side: ix_image_record_sha256 was an exact duplicate of the index uq_image_record_sha256 already builds — two btrees on the same column of the highest-insert-rate table. The other six are single-column indexes a later composite superseded without the narrow one being retired; a btree on (a,b) already serves lookups on a. 0088 deliberately taught the models to declare BOTH sha256 indexes so they would describe reality. This changes the reality instead, and the models change with it — otherwise the next baseline.yml run reintroduces exactly the drift 0088 removed. CONCURRENTLY throughout, so building the image_tag index does not hold an ACCESS EXCLUSIVE lock over every write for the duration. The cost is that the migration cannot run in a transaction and so is not atomic: every statement is IF NOT EXISTS / IF EXISTS, making a re-run after a partial failure safe. The docstring carries the query for finding an INVALID index left by an interrupted CONCURRENTLY build. What the sweep found clean, for the record: all 43 tables have a primary key; all 51 FKs declare an explicit ON DELETE, so none silently blocks a delete; the three enum CHECKs match the code that writes them (rule 36). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QHszn9H8VBvx5Ke8x1hvw
69 lines
3.0 KiB
Python
69 lines
3.0 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 JSON, BigInteger, DateTime, ForeignKey, Index, Integer, 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)
|
|
# No index=True: ix_backup_run_kind_started (above) already leads with
|
|
# `kind`, so a single-column index on it was pure write cost (#3301).
|
|
kind: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
status: Mapped[str] = mapped_column(
|
|
# No index=True — ix_backup_run_status_finished leads with `status`.
|
|
String(16), nullable=False, default="pending",
|
|
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="{}",
|
|
)
|
|
# Self-referential FK, unindexed until 0089 (#3300): SET NULL has to find
|
|
# the rows pointing at a deleted run before it can null them.
|
|
restored_from_id: Mapped[int | None] = mapped_column(
|
|
ForeignKey("backup_run.id", ondelete="SET NULL"),
|
|
nullable=True, index=True,
|
|
)
|