Files
EventSnap/backend/migrations/028_feed_counts_exclude_banned.up.sql
fabi 963f6449a1 feat(db): idempotency keys, ban-aware counts, and a host audit trail
Four migrations, all additive against a database that already has 001-025 applied.

026 narrows the client-upload idempotency index with `AND deleted_at IS NULL`. The old
index made a soft-deleted row keep its key forever, so a guest who deleted a photo and
re-sent the same one had the retry silently swallowed. The new indexed set is a strict
subset of the old, so it cannot fail on existing rows.

027 adds `client_join_id`, which lets a join retry after a lost response resume the same
account instead of 409ing on a name the caller itself owns. Every existing row gets NULL
and the partial index excludes NULLs, so it indexes nothing at creation.

028 brings the feed view's like/comment counts in line with what the feed actually renders:
a banned guest's rows were still counted, so a card showed "3 comments" above two.
`comment.rs` gets the matching `NOT u.is_banned` on the live read path — the export and
hashtag queries already filtered it, so the two views of one moderation action disagreed.

029 records host moderation actions, which were previously invisible after the fact.

Verified by applying 001-029 to a real Postgres against seeded data, including a
soft-deleted row holding a key and a banned user's like and comment. 026's down-migration
legitimately fails where a deleted and a live row share a key — that is inherent to the
direction, documented in the file, and sqlx never runs downs at boot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:43:11 +02:00

45 lines
1.9 KiB
SQL

-- Exclude banned users' likes and comments from the feed's scalar counts (H11).
--
-- `v_feed` already excludes banned UPLOADERS (`usr.is_banned = FALSE` on the join), but the two
-- correlated subqueries added by migration 024 counted every like and every non-deleted comment
-- regardless of who wrote it. So after a ban:
--
-- * the banned guest's own photos disappear from the feed (correct), but
-- * their likes still inflate the counter on everyone else's photos, and
-- * their comments still contribute to `comment_count` — and, until the change to
-- `Comment::list_for_upload` that ships with this migration, were still RENDERED in the
-- lightbox on the most-viewed photo of the evening.
--
-- The host's mental model of "ban" is "this person's contributions are gone". Photos honoured it;
-- likes and comments did not. Migration 021 already applied exactly this reasoning to hashtag
-- counts, and the export query filters `is_banned` too — this brings the last read path in line.
--
-- Derived at read time, so `unban_user` restores the counts with no extra work, exactly as it
-- already restores the photos.
CREATE OR REPLACE VIEW v_feed AS
SELECT
u.id,
u.event_id,
u.user_id,
usr.display_name AS uploader_name,
usr.is_banned,
usr.uploads_hidden,
u.preview_path,
u.thumbnail_path,
u.display_path,
u.mime_type,
u.caption,
u.created_at,
(SELECT count(*) FROM "like" l
JOIN "user" lu ON lu.id = l.user_id
WHERE l.upload_id = u.id AND NOT lu.is_banned) AS like_count,
(SELECT count(*) FROM comment c
JOIN "user" cu ON cu.id = c.user_id
WHERE c.upload_id = u.id AND c.deleted_at IS NULL AND NOT cu.is_banned) AS comment_count
FROM upload u
JOIN "user" usr ON u.user_id = usr.id
WHERE u.deleted_at IS NULL
AND usr.uploads_hidden = FALSE
AND usr.is_banned = FALSE;