CI / lint (push) Failing after 3s
Build images / sign-extension (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / build-agent (push) Successful in 9s
CI / frontend-build (push) Successful in 34s
Build images / build-ml (push) Successful in 53s
Build images / build-web (push) Successful in 44s
CI / backend-lint-and-test (push) Successful in 1m6s
CI / integration (push) Successful in 4m5s
Closes the residue the first reconciliation pass left, and corrects a
factual error I put into the record.
sha256 was NOT missing a uniqueness guarantee. I read
`op.create_index("ix_image_record_sha256", ...)` at 0001 line 151 and
concluded duplicates were possible, without reading line 149 two lines
above it:
sa.UniqueConstraint("sha256", name="uq_image_record_sha256"),
Uniqueness has held since the initial schema. The database expresses it
as a CONSTRAINT plus a separate non-unique lookup index; the model said
`unique=True, index=True`, which is one UNIQUE index under a different
name. Same guarantee, different objects — which is exactly why the two
schemas did not line up. The model now declares both objects. No DDL.
0088's docstring, which repeated the claim, is corrected in place.
Two real divergences, both the MODEL over-claiming:
* source: uq_source_artist_platform_url (alembic 0010) was declared
nowhere in the models — source.py had no __table_args__ at all — so
autogenerate would have proposed DROPPING it.
* head_metrics_snapshot.tag_id: model said NOT NULL, 0060 created it
nullable. Left nullable; the FK already cascades.
Seven constraints renamed to what the chain actually created, rather than
what base.py's naming convention renders: uq_series_page_image,
uq_series_chapter_anchor_page, fk_series_chapter_anchor_page,
fk_image_record_artist_id, fk_image_provenance_from_attachment, and the
two hand-shortened fk_tsr_* names from 0003.
Float server_defaults now mirror their own migration, per column. The
chain is MIXED: a plain string renders DEFAULT '0.90'::double precision,
sa.text() renders DEFAULT 0.90, and the migrations used both. Seven
columns take text(); the rest stay strings. Two literals also disagreed
outright — process_{auto_apply,conflict}_threshold said 0.9/0.5 against
the migration's 0.90/0.50.
baseline.yml gains two things. A repair for a SECOND generator defect in
the same class as the missing pgvector import: base.py's ck convention
contains %(constraint_name)s, so it applies even to a NAMED
CheckConstraint — autogenerate writes the already-rendered name into the
migration and running it applies the convention again, yielding
ck_ml_settings_ck_ml_settings_singleton. That is round-tripping damage,
not a claim the models make, so it is undone rather than counted.
And the diff now runs twice. Column ORDER differs permanently between a
schema built by 87 ADD COLUMNs and one built in a single shot — the
operator's database keeps chain order forever, a fresh install gets model
order — so a check that failed on it could never pass. The second pass
SORTS column lines within each CREATE TABLE instead of deleting them,
which cannot hide a column present on one side only, or one whose type,
nullability or default differs. Ordered diff is reported as information;
the order-insensitive one is the verdict.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QHszn9H8VBvx5Ke8x1hvw
67 lines
2.4 KiB
Python
67 lines
2.4 KiB
Python
"""SeriesChapter — a cosmetic chapter DIVIDER within a series (FC-6.x reframe).
|
|
|
|
A series is ONE flat, series-global ordered run of SeriesPages. A chapter is NOT
|
|
a container — it owns no pages. It is a labeled divider anchored to the page that
|
|
BEGINS the chapter (anchor_page_id → series_page): "a new chapter starts here."
|
|
A page's chapter is derived at read time as the nearest preceding divider.
|
|
|
|
Dividers never affect page ordering or the series-global page numbers; they stay
|
|
pinned to their anchor page across reorders. anchor_page_id is UNIQUE — at most
|
|
one chapter begins at a given page — and FK-cascades, so removing the anchor page
|
|
from the series drops the divider (the chapter merges into the preceding run).
|
|
|
|
title is the optional chapter name; stated_part is the optional operator-facing
|
|
"Part N" label (shown instead of a derived ordinal when set).
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
DateTime,
|
|
ForeignKey,
|
|
Integer,
|
|
Text,
|
|
UniqueConstraint,
|
|
func,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from .base import Base
|
|
|
|
|
|
class SeriesChapter(Base):
|
|
__tablename__ = "series_chapter"
|
|
|
|
__table_args__ = (
|
|
# alembic 0047 named the UNIQUE `uq_series_chapter_anchor_page`, not
|
|
# the `uq_series_chapter_anchor_page_id` a bare `unique=True` would
|
|
# render (#3275).
|
|
UniqueConstraint("anchor_page_id", name="uq_series_chapter_anchor_page"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
series_tag_id: Mapped[int] = mapped_column(
|
|
ForeignKey("tag.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
# Both the UNIQUE (above) and the FK carry the names 0047 gave them; the
|
|
# convention would render the FK `fk_series_chapter_anchor_page_id_series_page`.
|
|
anchor_page_id: Mapped[int] = mapped_column(
|
|
ForeignKey(
|
|
"series_page.id",
|
|
ondelete="CASCADE",
|
|
name="fk_series_chapter_anchor_page",
|
|
),
|
|
nullable=False,
|
|
)
|
|
title: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
stated_part: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
)
|