Rewrites the internal/db/queries comments to rule 32 and regenerates dbq, whose doc comments and embedded SQL carry them. The bugs behind the tiered mixes, the md5 order seed, the score bands and the round-robin arms are restated as the constraint each one enforces; counts from one library and stale cross-references are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.9 KiB
SQL
125 lines
4.9 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.
|
|
--
|
|
-- Deliberately NOT filtered on missing_since, unlike every browse /
|
|
-- discover / mix query. A playlist entry is something the user put here on
|
|
-- purpose, so a missing file stays in the list and renders as a dead row
|
|
-- rather than silently vanishing; missing_since rides along so the service
|
|
-- layer can mark it unplayable.
|
|
SELECT pt.*,
|
|
t.id AS live_track_id,
|
|
t.missing_since AS missing_since,
|
|
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. pick_kind is NULL for manual
|
|
-- playlists; the For You builder stamps 'taste'/'fresh'.
|
|
INSERT INTO playlist_tracks (playlist_id, position, track_id, title, artist_name, album_title, duration_sec, pick_kind)
|
|
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,
|
|
sqlc.narg(pick_kind)::text
|
|
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;
|
|
|
|
-- name: GetPlaylistsByIDs :many
|
|
-- Batched lookup used by /api/library/sync to hydrate upsert payloads
|
|
--. Mirror of GetArtistsByIDs.
|
|
SELECT * FROM playlists WHERE id = ANY($1::uuid[]);
|