feat(db): cascade-cleanup queries for M7 #372 track removal
DeleteTrack returns album_id + artist_id so the calling service can chain the album-empty / artist-empty cascade. DeleteAlbumIfEmpty and DeleteArtistIfEmpty are no-ops when other rows still reference the parent — the service treats pgx.ErrNoRows as "not orphaned, skip".
This commit is contained in:
@@ -47,6 +47,30 @@ func (q *Queries) CountAlbumsMatching(ctx context.Context, dollar_1 string) (int
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteAlbumIfEmpty = `-- name: DeleteAlbumIfEmpty :one
|
||||||
|
DELETE FROM albums a
|
||||||
|
WHERE a.id = $1
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
||||||
|
RETURNING a.id, a.artist_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteAlbumIfEmptyRow struct {
|
||||||
|
ID pgtype.UUID
|
||||||
|
ArtistID pgtype.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// M7 #372: deletes the album row only when it has no remaining tracks.
|
||||||
|
// Used after a track delete to tidy up orphaned albums. RETURNING gives
|
||||||
|
// us the artist_id so the service can chain into the artist-empty check.
|
||||||
|
// Returns no rows when the album still has tracks; the service treats
|
||||||
|
// pgx.ErrNoRows as "album not orphaned, skip cascade".
|
||||||
|
func (q *Queries) DeleteAlbumIfEmpty(ctx context.Context, id pgtype.UUID) (DeleteAlbumIfEmptyRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, deleteAlbumIfEmpty, id)
|
||||||
|
var i DeleteAlbumIfEmptyRow
|
||||||
|
err := row.Scan(&i.ID, &i.ArtistID)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getAlbumByArtistAndTitle = `-- name: GetAlbumByArtistAndTitle :one
|
const getAlbumByArtistAndTitle = `-- name: GetAlbumByArtistAndTitle :one
|
||||||
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at FROM albums WHERE artist_id = $1 AND title = $2 LIMIT 1
|
SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at FROM albums WHERE artist_id = $1 AND title = $2 LIMIT 1
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -33,6 +33,24 @@ func (q *Queries) CountArtistsMatching(ctx context.Context, dollar_1 string) (in
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteArtistIfEmpty = `-- name: DeleteArtistIfEmpty :one
|
||||||
|
DELETE FROM artists a
|
||||||
|
WHERE a.id = $1
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM albums al WHERE al.artist_id = a.id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM tracks t WHERE t.artist_id = a.id)
|
||||||
|
RETURNING a.id
|
||||||
|
`
|
||||||
|
|
||||||
|
// M7 #372: deletes the artist row only when it has no remaining albums
|
||||||
|
// AND no remaining tracks (defensive — tracks can in principle exist
|
||||||
|
// without an album, though the scanner doesn't write that shape today).
|
||||||
|
func (q *Queries) DeleteArtistIfEmpty(ctx context.Context, id pgtype.UUID) (pgtype.UUID, error) {
|
||||||
|
row := q.db.QueryRow(ctx, deleteArtistIfEmpty, id)
|
||||||
|
var id_2 pgtype.UUID
|
||||||
|
err := row.Scan(&id_2)
|
||||||
|
return id_2, err
|
||||||
|
}
|
||||||
|
|
||||||
const getArtistByID = `-- name: GetArtistByID :one
|
const getArtistByID = `-- name: GetArtistByID :one
|
||||||
SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists WHERE id = $1
|
SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -69,6 +69,37 @@ func (q *Queries) CountTracksMatchingForUser(ctx context.Context, arg CountTrack
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteTrack = `-- name: DeleteTrack :one
|
||||||
|
DELETE FROM tracks WHERE id = $1
|
||||||
|
RETURNING id, album_id, artist_id, file_path, mbid
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteTrackRow struct {
|
||||||
|
ID pgtype.UUID
|
||||||
|
AlbumID pgtype.UUID
|
||||||
|
ArtistID pgtype.UUID
|
||||||
|
FilePath string
|
||||||
|
Mbid *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// M7 #372: hard delete with FK cascade. The CASCADE on track_id from
|
||||||
|
// play_events / general_likes_tracks / lidarr_quarantine /
|
||||||
|
// lidarr_quarantine_actions handles their cleanup. RETURNING gives us
|
||||||
|
// album_id + artist_id for the album-empty / artist-empty cascade
|
||||||
|
// checks the service does next.
|
||||||
|
func (q *Queries) DeleteTrack(ctx context.Context, id pgtype.UUID) (DeleteTrackRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, deleteTrack, id)
|
||||||
|
var i DeleteTrackRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AlbumID,
|
||||||
|
&i.ArtistID,
|
||||||
|
&i.FilePath,
|
||||||
|
&i.Mbid,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getTrackByID = `-- name: GetTrackByID :one
|
const getTrackByID = `-- name: GetTrackByID :one
|
||||||
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks WHERE id = $1
|
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -85,3 +85,14 @@ SELECT COUNT(*) FROM albums;
|
|||||||
-- Used by request-progress reporting to count how many albums have been
|
-- Used by request-progress reporting to count how many albums have been
|
||||||
-- ingested for a matched artist while a request is still in flight.
|
-- ingested for a matched artist while a request is still in flight.
|
||||||
SELECT COUNT(*) FROM albums WHERE artist_id = $1;
|
SELECT COUNT(*) FROM albums WHERE artist_id = $1;
|
||||||
|
|
||||||
|
-- name: DeleteAlbumIfEmpty :one
|
||||||
|
-- M7 #372: deletes the album row only when it has no remaining tracks.
|
||||||
|
-- Used after a track delete to tidy up orphaned albums. RETURNING gives
|
||||||
|
-- us the artist_id so the service can chain into the artist-empty check.
|
||||||
|
-- Returns no rows when the album still has tracks; the service treats
|
||||||
|
-- pgx.ErrNoRows as "album not orphaned, skip cascade".
|
||||||
|
DELETE FROM albums a
|
||||||
|
WHERE a.id = $1
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
||||||
|
RETURNING a.id, a.artist_id;
|
||||||
|
|||||||
@@ -64,3 +64,13 @@ LEFT JOIN LATERAL (
|
|||||||
) cnt ON true
|
) cnt ON true
|
||||||
ORDER BY artists.sort_name, artists.name, artists.id
|
ORDER BY artists.sort_name, artists.name, artists.id
|
||||||
LIMIT $1 OFFSET $2;
|
LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
|
-- name: DeleteArtistIfEmpty :one
|
||||||
|
-- M7 #372: deletes the artist row only when it has no remaining albums
|
||||||
|
-- AND no remaining tracks (defensive — tracks can in principle exist
|
||||||
|
-- without an album, though the scanner doesn't write that shape today).
|
||||||
|
DELETE FROM artists a
|
||||||
|
WHERE a.id = $1
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM albums al WHERE al.artist_id = a.id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM tracks t WHERE t.artist_id = a.id)
|
||||||
|
RETURNING a.id;
|
||||||
|
|||||||
@@ -96,3 +96,12 @@ ORDER BY albums.release_date NULLS LAST, albums.sort_title,
|
|||||||
-- matched artist (sum across all the artist's albums) while a request
|
-- matched artist (sum across all the artist's albums) while a request
|
||||||
-- is still in flight.
|
-- is still in flight.
|
||||||
SELECT COUNT(*) FROM tracks WHERE artist_id = $1;
|
SELECT COUNT(*) FROM tracks WHERE artist_id = $1;
|
||||||
|
|
||||||
|
-- name: DeleteTrack :one
|
||||||
|
-- M7 #372: hard delete with FK cascade. The CASCADE on track_id from
|
||||||
|
-- play_events / general_likes_tracks / lidarr_quarantine /
|
||||||
|
-- lidarr_quarantine_actions handles their cleanup. RETURNING gives us
|
||||||
|
-- album_id + artist_id for the album-empty / artist-empty cascade
|
||||||
|
-- checks the service does next.
|
||||||
|
DELETE FROM tracks WHERE id = $1
|
||||||
|
RETURNING id, album_id, artist_id, file_path, mbid;
|
||||||
|
|||||||
Reference in New Issue
Block a user