Per-feed aggregation opt-out for status and announcement feeds #90
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
3 participants
Notifications
Due date
No due date set.
Reference
marvin8/yunjin!90
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/issue-87-feed-aggregation-opt-out"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Status/announcement feeds (Ultra.cc, Shock Hosting, Opentrackers, Contabo) collapsed into one per-feed mega-aggregate on every full refresh because their items are not distinct news stories: the prod DB held a 46-article Ultra.cc aggregate spanning 2023–2026 alongside 24- and 20-article dumps, recreated each time refresh ran and polluting the UI until the 7-day TTL cleared them.
A
feeds.aggregation_enabledflag (default on, alembic migration included) lets a feed opt out:true/falsevalidation, subscriber-only).TDD throughout: 15 new tests across db (flag persistence +
get_aggregating_feed_ids), service (three exclusion paths + opted-in control), and web (toggle both ways, non-subscriber rejected, invalid value 400, page state). Full nox green.Closes #87
WuMing
Found 1 issue(s). See inline comments below.
@ -229,0 +257,4 @@placeholders = ", ".join("?" for _ in feed_ids)cursor = conn.cursor()cursor.execute(f"SELECT id FROM feeds WHERE aggregation_enabled = 1 AND id IN ({placeholders})", # noqa: S608code [HIGH]
SQLite has a maximum of 999 bound variables per statement. If a user subscribes to more than 999 feeds, this query will fail with an OperationalError. The parameterised IN clause should be split into batches (e.g., by iterating sub-lists) to stay under the limit.
src/yunjin/db/feeds.py✅ Fixed in commit
86442fd— the query no longer binds the feed-id list at all: it selects the enabled feed ids (the feeds table is tiny) and intersects them with the subscription list in Python, so no bound-variable ceiling applies regardless of subscription count. Behaviour is pinned by the existing filter tests.WuMing
Found 3 issue(s). See inline comments below.
@ -98,6 +103,7 @@ def get_feed(conn: sqlite3.Connection, feed_id: int) -> Feed | None:updated_at=datetime.fromisoformat(row[7]),http_etag=row[8],http_last_modified=row[9],aggregation_enabled=bool(row[10]),code [HIGH]
The conversion
bool(row[10])treats SQL NULL as False, which would silently disable aggregation for feeds that were created before the migration (if the migration did not set a default or enforce NOT NULL). The intended default isTrue(opt-in). Use an explicit NULL-safe conversion likeaggregation_enabled = row[10] if row[10] is not None else Trueorbool(row[10]) if row[10] is not None else True. This pattern repeats at lines 106, 142, 174.@ -133,6 +139,7 @@ def get_feed_by_url(conn: sqlite3.Connection, url: str) -> Feed | None:updated_at=datetime.fromisoformat(row[7]),http_etag=row[8],http_last_modified=row[9],aggregation_enabled=bool(row[10]),code [HIGH]
Same NULL‑handling issue as line 106.
@ -229,0 +255,4 @@if not feed_ids:return set()cursor = conn.cursor()cursor.execute("SELECT id FROM feeds WHERE aggregation_enabled = 1")code [HIGH]
The query
WHERE aggregation_enabled = 1excludes rows where the column is NULL. If the migration allowed NULLs and the intention is that NULL means enabled (default-on), this would incorrectly skip those feeds. Should beWHERE aggregation_enabled IS NOT 0orWHERE aggregation_enabled != 0to treat NULL as enabled, or the schema must guarantee NOT NULL.🔴 Not actioned — the NULL-handling premise does not hold
src/yunjin/db/feeds.pyline 106 ·src/yunjin/db/feeds.pyline 142 ·src/yunjin/db/feeds.pyline 174All three findings are conditional on "if the migration did not set a default or enforce NOT NULL" — it enforces both. The migration adds the column as
BOOLEAN NOT NULL DEFAULT 1(verified viaPRAGMA table_info(feeds):notnull=1,dflt_value='1'), so:IntegrityErrorALTER TABLE ADD COLUMNbehaviour), sobool(row[10])maps them toTrue, andWHERE aggregation_enabled = 1cannot skip themAdding NULL fallbacks would imply a state the schema makes unreachable. The invariant is now pinned by a schema test (
test_schema_enforces_not_null_default_on, commitb8e4d64): if the constraint ever drifts, the suite fails before the mapping can be wrong. No further action.