1226cb7583
Migration 0014 adds playlists + playlist_tracks. track_id is nullable with ON DELETE SET NULL — tracks can be removed from the library without silently dropping playlist entries; the denormalized snapshot (title/artist/album/duration) keeps the row legible afterwards. UI renders such rows greyed-out. Indexes: playlists by (user_id, updated_at DESC) and a partial public index for cross-user discovery; playlist_tracks partial index on track_id to support the FK SET NULL lookup. Queries provide CRUD + rollup recompute (track_count, duration_sec) + append/remove primitives. Reorder is service-layer orchestrated via raw tx.Exec; no SQL primitive needed.
111 lines
4.2 KiB
SQL
111 lines
4.2 KiB
SQL
-- name: CreatePlaylist :one
|
|
INSERT INTO playlists (user_id, name, description, is_public)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *;
|
|
|
|
-- name: GetPlaylist :one
|
|
SELECT p.*, u.username AS owner_username
|
|
FROM playlists p
|
|
JOIN users u ON u.id = p.user_id
|
|
WHERE p.id = $1;
|
|
|
|
-- name: ListPlaylistsForUser :many
|
|
-- Owner's playlists (any visibility) + other users' public playlists.
|
|
-- Ordered by updated_at desc so newly-edited ones float to the top.
|
|
SELECT p.*, u.username AS owner_username
|
|
FROM playlists p
|
|
JOIN users u ON u.id = p.user_id
|
|
WHERE p.user_id = $1 OR p.is_public = true
|
|
ORDER BY p.updated_at DESC;
|
|
|
|
-- name: UpdatePlaylist :one
|
|
-- Updates only the fields whose corresponding `updateX` flag is true.
|
|
-- The flags let the service layer keep PATCH semantics (only-touch-what-the-caller-sent)
|
|
-- without writing N variants.
|
|
UPDATE playlists
|
|
SET
|
|
name = CASE WHEN sqlc.arg(update_name)::boolean THEN sqlc.arg(name)::text ELSE name END,
|
|
description = CASE WHEN sqlc.arg(update_description)::boolean THEN sqlc.arg(description)::text ELSE description END,
|
|
is_public = CASE WHEN sqlc.arg(update_is_public)::boolean THEN sqlc.arg(is_public)::boolean ELSE is_public END,
|
|
updated_at = now()
|
|
WHERE id = sqlc.arg(id)
|
|
RETURNING *;
|
|
|
|
-- name: UpdatePlaylistRollups :exec
|
|
-- Set track_count + duration_sec from a fresh aggregate. Called after
|
|
-- every mutation that touches playlist_tracks. Cheap; the table is small.
|
|
UPDATE playlists
|
|
SET
|
|
track_count = (SELECT COUNT(*) FROM playlist_tracks pt WHERE pt.playlist_id = $1),
|
|
duration_sec = (SELECT COALESCE(SUM(pt.duration_sec), 0) FROM playlist_tracks pt WHERE pt.playlist_id = $1),
|
|
updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: SetPlaylistCover :exec
|
|
UPDATE playlists SET cover_path = $2, updated_at = now() WHERE id = $1;
|
|
|
|
-- name: DeletePlaylist :one
|
|
-- Returns cover_path so the caller can clean up the cached collage on disk.
|
|
DELETE FROM playlists WHERE id = $1
|
|
RETURNING id, cover_path;
|
|
|
|
-- name: ListPlaylistTracks :many
|
|
-- Joined to tracks for the live track id (the service layer derives the
|
|
-- stream URL from it); LEFT JOIN preserves the row when track_id is NULL
|
|
-- (track was removed from the library). The denormalized snapshot fields
|
|
-- on playlist_tracks remain authoritative for title/artist/album text.
|
|
SELECT pt.*,
|
|
t.id AS live_track_id,
|
|
albums.id AS album_id,
|
|
artists.id AS artist_id
|
|
FROM playlist_tracks pt
|
|
LEFT JOIN tracks t ON t.id = pt.track_id
|
|
LEFT JOIN albums ON albums.id = t.album_id
|
|
LEFT JOIN artists ON artists.id = t.artist_id
|
|
WHERE pt.playlist_id = $1
|
|
ORDER BY pt.position;
|
|
|
|
-- name: AppendPlaylistTrack :one
|
|
-- Inserts at the next available position. Snapshot fields are copied
|
|
-- from the tracks/albums/artists join at insert time. tracks.duration_ms
|
|
-- is converted to seconds for the snapshot.
|
|
INSERT INTO playlist_tracks (playlist_id, position, track_id, title, artist_name, album_title, duration_sec)
|
|
SELECT
|
|
sqlc.arg(playlist_id)::uuid,
|
|
COALESCE((SELECT MAX(position) + 1 FROM playlist_tracks WHERE playlist_id = sqlc.arg(playlist_id)::uuid), 0),
|
|
t.id,
|
|
t.title,
|
|
artists.name,
|
|
albums.title,
|
|
(t.duration_ms / 1000)::integer
|
|
FROM tracks t
|
|
JOIN albums ON albums.id = t.album_id
|
|
JOIN artists ON artists.id = t.artist_id
|
|
WHERE t.id = sqlc.arg(track_id)::uuid
|
|
RETURNING *;
|
|
|
|
-- name: DeletePlaylistTrack :exec
|
|
-- Two-step: delete the row at `position`, then renumber subsequent rows
|
|
-- to close the gap. The renumber is a single UPDATE; the service layer
|
|
-- runs both in one transaction.
|
|
DELETE FROM playlist_tracks
|
|
WHERE playlist_id = $1 AND position = $2;
|
|
|
|
-- name: RenumberPlaylistTracksAfter :exec
|
|
-- Used after DeletePlaylistTrack to close the gap.
|
|
UPDATE playlist_tracks
|
|
SET position = position - 1
|
|
WHERE playlist_id = $1 AND position > $2;
|
|
|
|
-- name: ListAllPlaylistTracksForCollage :many
|
|
-- First N tracks for the collage. Uses LEFT JOIN on albums for the
|
|
-- cover_path; rows with NULL cover_path get the glyph fallback.
|
|
SELECT pt.position,
|
|
albums.cover_art_path AS album_cover_path
|
|
FROM playlist_tracks pt
|
|
LEFT JOIN tracks t ON t.id = pt.track_id
|
|
LEFT JOIN albums ON albums.id = t.album_id
|
|
WHERE pt.playlist_id = $1
|
|
ORDER BY pt.position
|
|
LIMIT $2;
|