Reader: order articles within a group newest-first #26

Manually merged
marvin8 merged 2 commits from feat/issue-19-aggregate-article-ordering into main 2026-09-11 23:58:18 +00:00
Collaborator

get_articles_in_aggregate previously returned articles in insertion order (ORDER BY article_id), so the newest article in a group could sit anywhere in the reader view.

The query now joins to articles and orders by COALESCE(published_at, created_at) DESC, created_at DESC — newest publication first, articles without a publish date fall back to fetch time, ties break on fetch time. This matches the ordering precedent already used by the other article-list queries.

Side effect (desirable): the reader index featured image now comes from the newest article in the group instead of the first-inserted one.

Design note recorded on the issue (corrects the findings comment: articles has no updated_at column). Covered by a new ordering test with deliberately out-of-order insertion and a missing publish date.

Closes #19

`get_articles_in_aggregate` previously returned articles in insertion order (`ORDER BY article_id`), so the newest article in a group could sit anywhere in the reader view. The query now joins to `articles` and orders by `COALESCE(published_at, created_at) DESC, created_at DESC` — newest publication first, articles without a publish date fall back to fetch time, ties break on fetch time. This matches the ordering precedent already used by the other article-list queries. Side effect (desirable): the reader index featured image now comes from the newest article in the group instead of the first-inserted one. Design note recorded on the issue (corrects the findings comment: `articles` has no `updated_at` column). Covered by a new ordering test with deliberately out-of-order insertion and a missing publish date. Closes #19
Order aggregate articles newest-first
All checks were successful
/ gitleaks (pull_request) Successful in 20s
/ checks (pull_request) Successful in 1m16s
/ pr-review (pull_request) Successful in 1m38s
e8df81821d
forgejo-actions left a comment

WuMing

Found 3 issue(s). See inline comments below.

## WuMing Found **3** issue(s). See inline comments below.
@ -212,0 +217,4 @@
FROM article_aggregates AS aa
JOIN articles AS a ON a.id = aa.article_id
WHERE aa.aggregate_id = ?
ORDER BY COALESCE(a.published_at, a.created_at) DESC, a.created_at DESC

code [LOW]

The ORDER BY clause has no final deterministic tie-breaker. If multiple articles have the same COALESCE(published_at, created_at) and created_at values (e.g., identical timestamps), the result order is undefined and may vary between executions. Add a stable tie-breaker such as aa.article_id DESC (or ASC) to ensure deterministic output.

**code** [LOW] The ORDER BY clause has no final deterministic tie-breaker. If multiple articles have the same COALESCE(published_at, created_at) and created_at values (e.g., identical timestamps), the result order is undefined and may vary between executions. Add a stable tie-breaker such as aa.article_id DESC (or ASC) to ensure deterministic output. <!-- wuming:sha256:4ab0d70c23392638a896b8e4491dff10adccf91bc9178ce39e6ceee2db9bd9c2 -->
@ -336,0 +366,4 @@
article_ids.append(article.id)
return aggregate, article_ids
def test_get_articles_in_aggregate_orders_newest_first(self, db_conn):

code [LOW]

The new ordering test only covers one article with a missing publish date and distinct published_at values. It does not cover tie-breaking behavior when two or more articles share the same published_at (and rely on created_at), nor multiple NULL published_at articles. This leaves the ORDER BY's second sort key and any future tie-breaker unverified.

**code** [LOW] The new ordering test only covers one article with a missing publish date and distinct published_at values. It does not cover tie-breaking behavior when two or more articles share the same published_at (and rely on created_at), nor multiple NULL published_at articles. This leaves the ORDER BY's second sort key and any future tie-breaker unverified. <!-- wuming:sha256:ce2320871fcc1bf5d57e662d03944c5c022a3e7118daf090b16cff8fa23850bc -->
tests/test_db.py Outdated
@ -336,0 +380,4 @@
result = aggregates.get_articles_in_aggregate(db_conn, aggregate.id)
assert result == [insertion_ids[2], insertion_ids[1], insertion_ids[0]]

tests [MEDIUM]

The test data makes the expected order [insertion_ids[2], insertion_ids[1], insertion_ids[0]] identical to what a query ordering by created_at alone (or even by article_id DESC) would return: article 2 is fetched last (newest created_at, published_at NULL), article 1 is published 1 day ago, article 0 3 days ago, so published_at order, created_at order and reverse insertion order all coincide. As a result the published_at-primary part of ORDER BY COALESCE(a.published_at, a.created_at) DESC, a.created_at DESC is never actually discriminated — the assertion would still pass if the query dropped published_at from the sort entirely. Add an article whose publish date is newer than the previously inserted ones but which is inserted first (or an article with an old published_at fetched most recently), so the published_at ordering conflicts with created_at/insertion ordering and only the correct query produces the expected list.

**tests** [MEDIUM] The test data makes the expected order [insertion_ids[2], insertion_ids[1], insertion_ids[0]] identical to what a query ordering by created_at alone (or even by article_id DESC) would return: article 2 is fetched last (newest created_at, published_at NULL), article 1 is published 1 day ago, article 0 3 days ago, so published_at order, created_at order and reverse insertion order all coincide. As a result the published_at-primary part of `ORDER BY COALESCE(a.published_at, a.created_at) DESC, a.created_at DESC` is never actually discriminated — the assertion would still pass if the query dropped published_at from the sort entirely. Add an article whose publish date is newer than the previously inserted ones but which is inserted first (or an article with an old published_at fetched most recently), so the published_at ordering conflicts with created_at/insertion ordering and only the correct query produces the expected list. <!-- wuming:sha256:b87632c9e733c122405fc311934776700f425c34a21defd93d3ce163399a7508 -->
Author
Collaborator

src/yunjin/db/aggregates.py

The ORDER BY clause has no final deterministic tie-breaker…

Fixed — aa.article_id DESC appended as the final sort key, with a dedicated test that forces identical published_at/created_at on two articles and asserts the stable descending-id order.

[`src/yunjin/db/aggregates.py`](https://forge.marvin8.zone/marvin8/yunjin/pulls/26#issuecomment-2667) > The ORDER BY clause has no final deterministic tie-breaker… ✅ Fixed — `aa.article_id DESC` appended as the final sort key, with a dedicated test that forces identical `published_at`/`created_at` on two articles and asserts the stable descending-id order.
Add deterministic tie-breaker and discriminating ordering tests
All checks were successful
/ gitleaks (pull_request) Successful in 13s
/ checks (pull_request) Successful in 1m21s
/ pr-review (pull_request) Successful in 3m45s
c4ae5a1f09
Author
Collaborator

tests/test_db.py

The new ordering test only covers one article with a missing publish date…

Fixed — the dataset now includes two dateless articles (verifying the created_at fallback order between them), a same-published_at pair (verifying the created_at tie-break), and an old-published/newly-fetched article; the tie-breaker test covers the exact-tie case.

[`tests/test_db.py`](https://forge.marvin8.zone/marvin8/yunjin/pulls/26#issuecomment-2668) > The new ordering test only covers one article with a missing publish date… ✅ Fixed — the dataset now includes two dateless articles (verifying the created_at fallback order between them), a same-`published_at` pair (verifying the created_at tie-break), and an old-published/newly-fetched article; the tie-breaker test covers the exact-tie case.
Author
Collaborator

tests/test_db.py

The test data makes the expected order identical to what created_at alone (or article_id DESC) would return…

Fixed — the rewritten dataset is constructed so the expected order differs from both alternatives: the last-inserted article has the oldest publish date, so only publication-based ordering places it last (plain created_at or reverse-insertion would place it first).

[`tests/test_db.py`](https://forge.marvin8.zone/marvin8/yunjin/pulls/26#issuecomment-2669) > The test data makes the expected order identical to what created_at alone (or article_id DESC) would return… ✅ Fixed — the rewritten dataset is constructed so the expected order differs from both alternatives: the last-inserted article has the oldest publish date, so only publication-based ordering places it last (plain created_at or reverse-insertion would place it first).
marvin8 approved these changes 2026-09-11 23:57:30 +00:00
marvin8 manually merged commit 0b8ef1006c into main 2026-09-11 23:58:18 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/yunjin!26
No description provided.