perf(gallery): materialize indexed effective_date sort key
The gallery cursored on COALESCE(post.post_date, image_record.created_at) across the Post outer join — an expression spanning two tables that no index can serve, so every /scroll sorted a large slice of the library (and the old frontend fired ten serially). Materialize it: - image_record.effective_date column + ix_image_record_effective_date (effective_date DESC, id DESC); alembic 0035 backfills COALESCE(primary post's post_date, created_at) for existing rows. - gallery_service._effective_date_col() now returns the column, so scroll / timeline / jump / neighbors all order off the index instead of re-deriving the COALESCE. _neighbors reads record.effective_date directly (drops an extra Post lookup). - importer._apply_sidecar maintains it: when a primary post with a date is linked, effective_date = post.post_date; plain inserts keep the created_at-equivalent server default. Tests: sidecar import asserts effective_date == post.post_date; gallery ordering/timeline/jump test seeds set effective_date alongside created_at. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,17 @@ class ImageRecord(Base):
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
# Denormalized gallery sort key = COALESCE(primary post's post_date,
|
||||
# created_at) (alembic 0035). The gallery used to compute this as a
|
||||
# COALESCE across the Post outer join on every /scroll, which can't use
|
||||
# an index and re-sorted a large slice of the library per page (×10 with
|
||||
# the old serial batching). Materializing it lets the cursor scroll read
|
||||
# ix_image_record_effective_date directly. Maintained by the importer
|
||||
# (services/importer.py _apply_sidecar) when a primary post with a date
|
||||
# is linked; plain inserts keep the created_at-equivalent server default.
|
||||
effective_date: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
|
||||
@@ -43,16 +43,17 @@ def decode_cursor(cursor: str) -> tuple[datetime, int]:
|
||||
|
||||
|
||||
def _effective_date_col():
|
||||
"""SQL expression: COALESCE(post.post_date, image_record.created_at).
|
||||
"""The materialized gallery sort key: image_record.effective_date
|
||||
(alembic 0035) = COALESCE(primary post's post_date, created_at),
|
||||
maintained at write time by the importer.
|
||||
|
||||
Used as the canonical sort/group/filter key across the gallery so
|
||||
images backfilled with primary_post_id (e.g. via tag_apply phase 4)
|
||||
surface at their original publish date, not their FC import date.
|
||||
Images without a Post (or with Post.post_date NULL) fall back to
|
||||
image_record.created_at and still order coherently against
|
||||
post-attached ones.
|
||||
Canonical sort/group/filter key across the gallery so images attached
|
||||
to a post surface at their original publish date, not their FC import
|
||||
date — and, now that it's a single indexed column rather than a
|
||||
COALESCE across the Post outer join, the cursor scroll is an index
|
||||
range scan instead of a full re-sort per page.
|
||||
"""
|
||||
return func.coalesce(Post.post_date, ImageRecord.created_at)
|
||||
return ImageRecord.effective_date
|
||||
|
||||
|
||||
def _outer_join_primary_post(stmt: Select) -> Select:
|
||||
@@ -357,17 +358,10 @@ class GalleryService:
|
||||
}
|
||||
|
||||
async def _neighbors(self, record: ImageRecord) -> dict:
|
||||
# Compute the boundary image's effective_date in Python (one query
|
||||
# below + the SELECT we already have on `record`) and use it for
|
||||
# the neighbor comparison. Cheaper than re-deriving in SQL via
|
||||
# correlated subquery.
|
||||
boundary_eff = record.created_at
|
||||
if record.primary_post_id is not None:
|
||||
post_date = (await self.session.execute(
|
||||
select(Post.post_date).where(Post.id == record.primary_post_id)
|
||||
)).scalar_one_or_none()
|
||||
if post_date is not None:
|
||||
boundary_eff = post_date
|
||||
# The boundary image's sort key is materialized on the row now
|
||||
# (alembic 0035) — read it directly instead of re-deriving COALESCE
|
||||
# via an extra Post lookup.
|
||||
boundary_eff = record.effective_date
|
||||
|
||||
eff = _effective_date_col()
|
||||
prev_stmt = _outer_join_primary_post(
|
||||
|
||||
@@ -960,6 +960,14 @@ class Importer:
|
||||
sp.rollback()
|
||||
if record.primary_post_id is None:
|
||||
record.primary_post_id = post.id
|
||||
# Keep the denormalized gallery sort key (alembic 0035) aligned with
|
||||
# the primary post's publish date so /scroll orders off
|
||||
# ix_image_record_effective_date instead of COALESCE-ing across the
|
||||
# post join. Only override when THIS post is the primary AND carries
|
||||
# a date; otherwise the column keeps its created_at-equivalent server
|
||||
# default (matches the old COALESCE(post_date, created_at) fallback).
|
||||
if record.primary_post_id == post.id and post.post_date is not None:
|
||||
record.effective_date = post.post_date
|
||||
self.session.flush()
|
||||
|
||||
def _copy_to_library(
|
||||
|
||||
Reference in New Issue
Block a user