-- Duplicate merge (Scribe #3911). Every statement here runs inside the one -- transaction library.MergeDuplicateGroup opens, after the removed copy's file -- is already gone. The loser's own track row is deleted last with DeleteTrack; -- what these do is move everything it carries onto the survivor first, so that -- delete's CASCADE finds nothing left to destroy. -- name: LockDuplicateGroupForMerge :one -- Locks the group for the rest of the transaction, so two merges of one group -- cannot run at once. SELECT id, tier, status FROM duplicate_groups WHERE id = sqlc.arg(id) FOR UPDATE; -- name: ListDuplicateGroupMergeMembers :many SELECT t.id, t.file_path, t.file_format, t.file_size, t.added_at, t.album_id, t.mbid, albums.mbid AS album_mbid FROM duplicate_group_members m JOIN tracks t ON t.id = m.track_id JOIN albums ON albums.id = t.album_id WHERE m.group_id = sqlc.arg(group_id) ORDER BY t.id; -- Plain repoints: no unique key involves track_id, so moving rows cannot collide. -- name: MergeRepointPlayEvents :execrows UPDATE play_events SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid; -- name: MergeRepointSkipEvents :execrows UPDATE skip_events SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid; -- name: MergeRepointContextualLikes :execrows UPDATE contextual_likes SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid; -- name: MergeRepointPlaybackErrors :execrows UPDATE playback_errors SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid; -- name: MergeRepointLidarrRequests :execrows UPDATE lidarr_requests SET matched_track_id = sqlc.arg(survivor_id)::uuid WHERE matched_track_id = sqlc.arg(loser_id)::uuid; -- name: MergeRepointPlaylistTracks :many -- playlist_tracks is keyed by (playlist_id, position), so repointing keeps every -- entry exactly where it was. A playlist that held both copies simply holds the -- survivor twice — the user put two entries there, and both stay. UPDATE playlist_tracks SET track_id = sqlc.arg(survivor_id)::uuid WHERE track_id = sqlc.arg(loser_id)::uuid RETURNING playlist_id; -- Collision-safe merges: a unique key includes track_id, so the survivor may -- already hold a matching row. Copy what it lacks; DeleteTrack's CASCADE then -- removes the loser's originals. -- name: MergeCopyGeneralLikes :many -- One like per user. A user who liked both copies keeps a single like, dated to -- the earlier of the two. INSERT INTO general_likes (user_id, track_id, liked_at) SELECT user_id, sqlc.arg(survivor_id)::uuid, liked_at FROM general_likes WHERE track_id = sqlc.arg(loser_id)::uuid ON CONFLICT (user_id, track_id) DO UPDATE SET liked_at = LEAST(general_likes.liked_at, EXCLUDED.liked_at) RETURNING user_id; -- name: MergeCopyTrackTags :execrows INSERT INTO track_tags (track_id, tag, weight) SELECT sqlc.arg(survivor_id)::uuid, tag, weight FROM track_tags WHERE track_id = sqlc.arg(loser_id)::uuid ON CONFLICT (track_id, tag) DO NOTHING; -- name: MergeCopyTrackSimilarity :execrows -- Rewrites the loser to the survivor on either side of an edge. An edge between -- the two copies would become a track similar to itself — the table forbids -- that, and it means nothing — so it is dropped. An edge the survivor already -- has from the same source is kept as it is. INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at) SELECT CASE WHEN track_a_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_a_id END, CASE WHEN track_b_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_b_id END, score, source, fetched_at FROM track_similarity WHERE (track_a_id = sqlc.arg(loser_id)::uuid OR track_b_id = sqlc.arg(loser_id)::uuid) AND (CASE WHEN track_a_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_a_id END) <> (CASE WHEN track_b_id = sqlc.arg(loser_id)::uuid THEN sqlc.arg(survivor_id)::uuid ELSE track_b_id END) ON CONFLICT (track_a_id, track_b_id, source) DO NOTHING; -- name: MergeInheritTrackMbid :exec -- A recording MBID is what the similarity pipeline keys on. If only the removed -- copy carried one, the survivor takes it rather than going dark to similarity. UPDATE tracks AS survivor SET mbid = loser.mbid FROM tracks AS loser WHERE survivor.id = sqlc.arg(survivor_id)::uuid AND loser.id = sqlc.arg(loser_id)::uuid AND survivor.mbid IS NULL AND loser.mbid IS NOT NULL; -- name: MarkDuplicateGroupMerged :execrows UPDATE duplicate_groups SET status = 'merged', resolved_at = now() WHERE id = sqlc.arg(id) AND status = 'pending';