feat(gallery): sort by earliest post date across all posts (new default)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 16s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m24s

The gallery's newest/oldest sort keys off image_record.effective_date =
COALESCE(primary post's post_date, created_at). The primary post is often the
repost/download the file came from, so the grid led with download dates rather
than when content was first posted (operator-flagged).

Add a second materialized sort key, earliest_post_date = MIN(post_date) across
ALL of an image's provenance posts (every post it appears in), else created_at —
the original publish date. Mirrors the effective_date pattern so the sort stays a
forward index scan.

- alembic 0071: add earliest_post_date + index (DESC, id DESC); backfill
  created_at baseline then MIN over image_provenance ⋈ post.
- importer: recompute earliest_post_date whenever a dated post is linked (MIN over
  the image's provenance, which now includes the just-added row).
- gallery_service: new sorts posted_new / posted_old key off earliest_post_date;
  cursor + year/month grouping follow the active column transparently.
- api: accept posted_new|posted_old; DEFAULT is now posted_new so the grid leads
  with original publish date. newest/oldest (effective_date) still available.
- frontend: sort dropdown gains "Newest/Oldest post date" (default Newest post
  date); existing effective-date sorts relabelled "Newest/Oldest added".
- tests: service test asserts posted_new/posted_old key off earliest_post_date;
  frontend default-sort omission test updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
2026-07-01 10:46:09 -04:00
parent ccbb5cbc9e
commit c22f37d64d
9 changed files with 176 additions and 15 deletions
+24 -2
View File
@@ -55,6 +55,25 @@ def _effective_date_col():
return ImageRecord.effective_date
# Sort key -> the materialized column the gallery orders + cursors on. Both are
# indexed (DESC, id DESC), so every sort is a forward index range scan.
# newest/oldest → effective_date (primary post's date, else download)
# posted_new/_old → earliest_post_date (earliest publish across ALL posts)
_SORT_COLUMNS = {
"newest": ImageRecord.effective_date,
"oldest": ImageRecord.effective_date,
"posted_new": ImageRecord.earliest_post_date,
"posted_old": ImageRecord.earliest_post_date,
}
_ASCENDING_SORTS = {"oldest", "posted_old"}
def _sort_column(sort: str):
"""The materialized date column a gallery sort orders/cursors on (falls back
to effective_date for any unknown sort)."""
return _SORT_COLUMNS.get(sort, ImageRecord.effective_date)
def _outer_join_primary_post(stmt: Select) -> Select:
"""LEFT JOIN Post on ImageRecord.primary_post_id so the COALESCE
above sees Post.post_date when available. Images without a post
@@ -406,7 +425,10 @@ class GalleryService:
tag_ids, post_id, artist_id, tag_or_groups, tag_exclude,
)
eff = _effective_date_col()
# eff is the ACTIVE sort column (effective_date or earliest_post_date);
# the cursor, ordering and year/month grouping all key off it, so the
# 'post date' sort paginates + buckets by original publish transparently.
eff = _sort_column(sort)
stmt = select(ImageRecord, Post.post_date, eff.label("eff"))
stmt = _outer_join_primary_post(stmt)
stmt = _apply_scope(
@@ -417,7 +439,7 @@ class GalleryService:
date_from=date_from, date_to=date_to,
)
descending = sort != "oldest"
descending = sort not in _ASCENDING_SORTS
if cursor:
cur_ts, cur_id = decode_cursor(cursor)
# The cursor is just (last eff, last id); the request's sort