From 737918c1dccd87ddfc2e808ef96a77564a8b6dfd Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:27:32 -0700 Subject: [PATCH] fix(ddl): backfill album saves and reposts to playlist An album is a playlist with is_album = true. The indexer briefly derived a separate 'album' save_type/repost_type by reading playlists.is_album at index time; it no longer does (OpenAudio/go-openaudio#428). This backfills the rows written while that was live: 670 saves and 528 reposts, all first appearing on 2026-05-28. Two hazards this has to avoid: * on_save/on_repost fire on AFTER INSERT OR UPDATE, and the notification group_id embeds the type ('save::type:'). A plain UPDATE would mint a second favourite/repost notification per row under a new group_id, so those two triggers are disabled for the backfill. trg_saves/trg_reposts stay enabled so the search indexer still sees the rows change. * save_type/repost_type are part of the primary key. Verified against prod data that no (user_id, item_id, txhash) has both a 'playlist' and an 'album' row, so this is a straight UPDATE with no conflict handling. Aggregate counts are unaffected either way: handle_save's delta is transition-aware and evaluates to 0 when is_delete does not change. Each table gets its own transaction to keep the ACCESS EXCLUSIVE lock taken by ALTER TABLE ... DISABLE TRIGGER as short as possible. Re-running is a no-op once no 'album' rows remain. The 'album' label is left in the savetype/reposttype enums since Postgres cannot drop an enum value without rebuilding the type. Co-Authored-By: Claude Opus 5 --- .../0236_saves_reposts_album_to_playlist.sql | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ddl/migrations/0236_saves_reposts_album_to_playlist.sql diff --git a/ddl/migrations/0236_saves_reposts_album_to_playlist.sql b/ddl/migrations/0236_saves_reposts_album_to_playlist.sql new file mode 100644 index 00000000..c0c967d4 --- /dev/null +++ b/ddl/migrations/0236_saves_reposts_album_to_playlist.sql @@ -0,0 +1,57 @@ +-- Collapse save_type / repost_type 'album' back into 'playlist'. +-- +-- An album is a playlist with is_album = true. The indexer briefly derived a +-- separate 'album' type by reading playlists.is_album at index time; it was +-- introduced as a side effect of a fix for entity-id collisions (the real bug +-- there was falling through to track inference when the chain said "Playlist") +-- and went live on 2026-05-28. The indexer no longer does this. Two problems +-- with the derived value: +-- +-- * is_album is mutable, but save_type is written once and is part of the +-- saves / reposts primary key. Replaying the same chain history at a +-- different time therefore produced different rows. +-- * handle_save / handle_repost build the notification group_id from the +-- type ('save::type:'), so the same favourite could notify +-- twice under two different group_ids. +-- +-- Nothing reads the distinction: every consumer is track / not-track, or ORs +-- the two together (get_account_playlists, reconcile_aggregates). Callers that +-- need to know read playlists.is_album at query time, which is what the +-- notification triggers already do. +-- +-- Affected rows at time of writing: 670 saves, 528 reposts. There are no +-- primary-key collisions — no (user_id, item_id, txhash) has both a 'playlist' +-- and an 'album' row — so this is a straight UPDATE. +-- +-- on_save / on_repost are disabled for the backfill so it does not emit a +-- second round of favourite/repost notifications or re-run milestone checks. +-- Aggregate counts are unaffected either way: handle_save's delta is +-- transition-aware and evaluates to 0 when is_delete does not change. +-- trg_saves / trg_reposts stay enabled so the search indexer still sees the +-- rows change. +-- +-- Each table gets its own transaction to keep the ACCESS EXCLUSIVE lock taken +-- by ALTER TABLE ... DISABLE TRIGGER as short as possible. Re-running is a +-- no-op once no 'album' rows remain. +-- +-- The 'album' label is deliberately left in the savetype / reposttype enums: +-- Postgres cannot drop an enum value without rebuilding the type, and the +-- indexer no longer writes it. + +BEGIN; +SET LOCAL lock_timeout = '5s'; + +ALTER TABLE saves DISABLE TRIGGER on_save; +UPDATE saves SET save_type = 'playlist' WHERE save_type = 'album'; +ALTER TABLE saves ENABLE TRIGGER on_save; + +COMMIT; + +BEGIN; +SET LOCAL lock_timeout = '5s'; + +ALTER TABLE reposts DISABLE TRIGGER on_repost; +UPDATE reposts SET repost_type = 'playlist' WHERE repost_type = 'album'; +ALTER TABLE reposts ENABLE TRIGGER on_repost; + +COMMIT;