lyrics

lyrics
git clone git://brookjeynes.dev/bjeynes/lyrics.git
Log | Files | Refs

commit ef9ed069961be450b71e869edb46eb00f683448e
parent 432b5c4901ad301858d951338ed1880bb0fd46fe
Author: brookjeynes <me@brookjeynes.dev>
Date:   Sun,  7 Jun 2026 16:50:08 +1000

refactor: normalize database row handling

Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
Minternal/db/album.go | 128++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Minternal/db/artist.go | 148+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Minternal/db/song.go | 184++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
3 files changed, 254 insertions(+), 206 deletions(-)

diff --git a/internal/db/album.go b/internal/db/album.go @@ -33,27 +33,31 @@ func GetAlbum(e Execer, id domain.AlbumID) (domain.Album, error) { } var artistRows []albumArtistRow - rows, err := e.Query(` - select artist_id, role, sort_order - from album_artists - where album_id = ? - order by sort_order - `, id) - if err != nil { - return domain.Album{}, fmt.Errorf("failed to query album artists: %w", err) - } - defer rows.Close() - for rows.Next() { - var r albumArtistRow - if err := rows.Scan(&r.artistID, &r.role, &r.sortOrder); err != nil { - return domain.Album{}, fmt.Errorf("failed to scan album artist: %w", err) + { + rows, err := e.Query(` + select artist_id, role, sort_order + from album_artists + where album_id = ? + order by sort_order + `, id) + if err != nil { + return domain.Album{}, fmt.Errorf("failed to query album artists: %w", err) } - artistRows = append(artistRows, r) - } - if err := rows.Err(); err != nil { - return domain.Album{}, fmt.Errorf("failed to iterate album artists: %w", err) + + for rows.Next() { + var r albumArtistRow + if err := rows.Scan(&r.artistID, &r.role, &r.sortOrder); err != nil { + _ = rows.Close() + return domain.Album{}, fmt.Errorf("failed to scan album artist: %w", err) + } + artistRows = append(artistRows, r) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Album{}, fmt.Errorf("failed to iterate album artists: %w", err) + } + _ = rows.Close() } - rows.Close() // fetch full artist records in one query if len(artistRows) > 0 { @@ -65,26 +69,30 @@ func GetAlbum(e Execer, id domain.AlbumID) (domain.Album, error) { placeholders = placeholders[:len(placeholders)-1] artistMap := make(map[domain.ArtistID]domain.Artist, len(artistIDs)) - aRows, err := e.Query(` - select id, name, latin_name, disambiguation, bio - from artists - where id in (`+placeholders+`) - `, artistIDs...) - if err != nil { - return domain.Album{}, fmt.Errorf("failed to query artists: %w", err) - } - defer aRows.Close() - for aRows.Next() { - var a domain.Artist - if err := aRows.Scan(&a.ID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil { - return domain.Album{}, fmt.Errorf("failed to scan artist: %w", err) + { + rows, err := e.Query(` + select id, name, latin_name, disambiguation, bio + from artists + where id in (`+placeholders+`) + `, artistIDs...) + if err != nil { + return domain.Album{}, fmt.Errorf("failed to query artists: %w", err) } - artistMap[a.ID] = a - } - if err := aRows.Err(); err != nil { - return domain.Album{}, fmt.Errorf("failed to iterate artists: %w", err) + + for rows.Next() { + var a domain.Artist + if err := rows.Scan(&a.ID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil { + _ = rows.Close() + return domain.Album{}, fmt.Errorf("failed to scan artist: %w", err) + } + artistMap[a.ID] = a + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Album{}, fmt.Errorf("failed to iterate artists: %w", err) + } + _ = rows.Close() } - aRows.Close() album.Artists = make([]domain.AlbumArtist, len(artistRows)) for i, r := range artistRows { @@ -103,27 +111,31 @@ func GetAlbum(e Execer, id domain.AlbumID) (domain.Album, error) { } var songRows []albumSongRow - sRows, err := e.Query(` - select song_id, track_number - from song_albums - where album_id = ? - order by track_number - `, id) - if err != nil { - return domain.Album{}, fmt.Errorf("failed to query album songs: %w", err) - } - defer sRows.Close() - for sRows.Next() { - var r albumSongRow - if err := sRows.Scan(&r.songID, &r.trackNumber); err != nil { - return domain.Album{}, fmt.Errorf("failed to scan album song: %w", err) + { + rows, err := e.Query(` + select song_id, track_number + from song_albums + where album_id = ? + order by track_number + `, id) + if err != nil { + return domain.Album{}, fmt.Errorf("failed to query album songs: %w", err) } - songRows = append(songRows, r) - } - if err := sRows.Err(); err != nil { - return domain.Album{}, fmt.Errorf("failed to iterate album songs: %w", err) + + for rows.Next() { + var r albumSongRow + if err := rows.Scan(&r.songID, &r.trackNumber); err != nil { + _ = rows.Close() + return domain.Album{}, fmt.Errorf("failed to scan album song: %w", err) + } + songRows = append(songRows, r) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Album{}, fmt.Errorf("failed to iterate album songs: %w", err) + } + _ = rows.Close() } - sRows.Close() album.Songs = make([]domain.Song, 0, len(songRows)) for _, r := range songRows { @@ -194,7 +206,9 @@ func SearchAlbum(e Execer, albumName string) ([]domain.Album, error) { if err != nil { return nil, fmt.Errorf("failed to query albums: %w", err) } - defer rows.Close() + defer func() { + _ = rows.Close() + }() var results []domain.Album for rows.Next() { diff --git a/internal/db/artist.go b/internal/db/artist.go @@ -19,30 +19,34 @@ func GetArtist(e Execer, id domain.ArtistID) (domain.Artist, []domain.Album, []d return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to get artist: %w", err) } - // fetch albums where artist is primary, ordered by release_date - albumRows, err := e.Query(` - select album_id - from album_artists - where artist_id = ? - and role = ? - order by (select release_date from albums where id = album_id) - `, id, domain.RolePrimary) - if err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to query artist albums: %w", err) - } - defer albumRows.Close() var albumIDs []domain.AlbumID - for albumRows.Next() { - var albumID domain.AlbumID - if err := albumRows.Scan(&albumID); err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to scan album id: %w", err) + { + // fetch albums where artist is primary, ordered by release_date + rows, err := e.Query(` + select album_id + from album_artists + where artist_id = ? + and role = ? + order by (select release_date from albums where id = album_id) + `, id, domain.RolePrimary) + if err != nil { + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to query artist albums: %w", err) } - albumIDs = append(albumIDs, albumID) - } - if err := albumRows.Err(); err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to iterate artist albums: %w", err) + + for rows.Next() { + var albumID domain.AlbumID + if err := rows.Scan(&albumID); err != nil { + _ = rows.Close() + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to scan album id: %w", err) + } + albumIDs = append(albumIDs, albumID) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to iterate artist albums: %w", err) + } + _ = rows.Close() } - albumRows.Close() albums := make([]domain.Album, 0, len(albumIDs)) for _, albumID := range albumIDs { @@ -53,32 +57,36 @@ func GetArtist(e Execer, id domain.ArtistID) (domain.Artist, []domain.Album, []d albums = append(albums, album) } - // fetch primary singles (primary role, no album) ordered by release_date - singlesRows, err := e.Query(` - select sa.song_id - from song_artists sa - join songs s on s.id = sa.song_id - where sa.artist_id = ? - and sa.role = ? - and not exists (select 1 from song_albums where song_id = sa.song_id) - order by s.release_date - `, id, domain.RolePrimary) - if err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to query artist singles: %w", err) - } - defer singlesRows.Close() var singleIDs []domain.SongID - for singlesRows.Next() { - var songID domain.SongID - if err := singlesRows.Scan(&songID); err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to scan single id: %w", err) + { + // fetch primary singles (primary role, no album) ordered by release_date + rows, err := e.Query(` + select sa.song_id + from song_artists sa + join songs s on s.id = sa.song_id + where sa.artist_id = ? + and sa.role = ? + and not exists (select 1 from song_albums where song_id = sa.song_id) + order by s.release_date + `, id, domain.RolePrimary) + if err != nil { + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to query artist singles: %w", err) } - singleIDs = append(singleIDs, songID) - } - if err := singlesRows.Err(); err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to iterate artist singles: %w", err) + + for rows.Next() { + var songID domain.SongID + if err := rows.Scan(&songID); err != nil { + _ = rows.Close() + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to scan single id: %w", err) + } + singleIDs = append(singleIDs, songID) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to iterate artist singles: %w", err) + } + _ = rows.Close() } - singlesRows.Close() singles := make([]domain.Song, 0, len(singleIDs)) for _, songID := range singleIDs { @@ -89,31 +97,35 @@ func GetArtist(e Execer, id domain.ArtistID) (domain.Artist, []domain.Album, []d singles = append(singles, song) } - // fetch featured songs (featured role, regardless of album) ordered by release_date - featuredRows, err := e.Query(` - select sa.song_id - from song_artists sa - join songs s on s.id = sa.song_id - where sa.artist_id = ? - and sa.role = ? - order by s.release_date - `, id, domain.RoleFeatured) - if err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to query featured songs: %w", err) - } - defer featuredRows.Close() var featuredIDs []domain.SongID - for featuredRows.Next() { - var songID domain.SongID - if err := featuredRows.Scan(&songID); err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to scan featured song id: %w", err) + { + // fetch featured songs (featured role, regardless of album) ordered by release_date + rows, err := e.Query(` + select sa.song_id + from song_artists sa + join songs s on s.id = sa.song_id + where sa.artist_id = ? + and sa.role = ? + order by s.release_date + `, id, domain.RoleFeatured) + if err != nil { + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to query featured songs: %w", err) } - featuredIDs = append(featuredIDs, songID) - } - if err := featuredRows.Err(); err != nil { - return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to iterate featured songs: %w", err) + + for rows.Next() { + var songID domain.SongID + if err := rows.Scan(&songID); err != nil { + _ = rows.Close() + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to scan featured song id: %w", err) + } + featuredIDs = append(featuredIDs, songID) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Artist{}, nil, nil, nil, fmt.Errorf("failed to iterate featured songs: %w", err) + } + _ = rows.Close() } - featuredRows.Close() featured := make([]domain.Song, 0, len(featuredIDs)) for _, songID := range featuredIDs { @@ -180,7 +192,9 @@ func SearchArtist(e Execer, artistName string) ([]domain.Artist, error) { if err != nil { return nil, fmt.Errorf("failed to query artists: %w", err) } - defer rows.Close() + defer func() { + _ = rows.Close() + }() var results []domain.Artist for rows.Next() { diff --git a/internal/db/song.go b/internal/db/song.go @@ -33,27 +33,31 @@ func GetSong(e Execer, id domain.SongID) (domain.Song, error) { } var artistRows []songArtistRow - rows, err := e.Query(` - select artist_id, role, sort_order - from song_artists - where song_id = ? - order by sort_order - `, id) - if err != nil { - return domain.Song{}, fmt.Errorf("failed to query song artists: %w", err) - } - defer rows.Close() - for rows.Next() { - var r songArtistRow - if err := rows.Scan(&r.artistID, &r.role, &r.sortOrder); err != nil { - return domain.Song{}, fmt.Errorf("failed to scan song artist: %w", err) + { + rows, err := e.Query(` + select artist_id, role, sort_order + from song_artists + where song_id = ? + order by sort_order + `, id) + if err != nil { + return domain.Song{}, fmt.Errorf("failed to query song artists: %w", err) } - artistRows = append(artistRows, r) - } - if err := rows.Err(); err != nil { - return domain.Song{}, fmt.Errorf("failed to iterate song artists: %w", err) + + for rows.Next() { + var r songArtistRow + if err := rows.Scan(&r.artistID, &r.role, &r.sortOrder); err != nil { + _ = rows.Close() + return domain.Song{}, fmt.Errorf("failed to scan song artist: %w", err) + } + artistRows = append(artistRows, r) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Song{}, fmt.Errorf("failed to iterate song artists: %w", err) + } + _ = rows.Close() } - rows.Close() // fetch full artist records in one query if len(artistRows) > 0 { @@ -65,7 +69,7 @@ func GetSong(e Execer, id domain.SongID) (domain.Song, error) { placeholders = placeholders[:len(placeholders)-1] artistMap := make(map[domain.ArtistID]domain.Artist, len(artistIDs)) - aRows, err := e.Query(` + rows, err := e.Query(` select id, name, latin_name, disambiguation, bio from artists where id in (`+placeholders+`) @@ -73,18 +77,20 @@ func GetSong(e Execer, id domain.SongID) (domain.Song, error) { if err != nil { return domain.Song{}, fmt.Errorf("failed to query artists: %w", err) } - defer aRows.Close() - for aRows.Next() { + + for rows.Next() { var a domain.Artist - if err := aRows.Scan(&a.ID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil { + if err := rows.Scan(&a.ID, &a.Name, &a.LatinName, &a.Disambiguation, &a.Bio); err != nil { + _ = rows.Close() return domain.Song{}, fmt.Errorf("failed to scan artist: %w", err) } artistMap[a.ID] = a } - if err := aRows.Err(); err != nil { + if err := rows.Err(); err != nil { + _ = rows.Close() return domain.Song{}, fmt.Errorf("failed to iterate artists: %w", err) } - aRows.Close() + _ = rows.Close() song.Artists = make([]domain.SongArtist, len(artistRows)) for i, r := range artistRows { @@ -103,26 +109,30 @@ func GetSong(e Execer, id domain.SongID) (domain.Song, error) { } var albumRows []songAlbumRow - alRows, err := e.Query(` - select album_id, track_number - from song_albums - where song_id = ? - `, id) - if err != nil { - return domain.Song{}, fmt.Errorf("failed to query song albums: %w", err) - } - defer alRows.Close() - for alRows.Next() { - var r songAlbumRow - if err := alRows.Scan(&r.albumID, &r.trackNumber); err != nil { - return domain.Song{}, fmt.Errorf("failed to scan song album: %w", err) + { + rows, err := e.Query(` + select album_id, track_number + from song_albums + where song_id = ? + `, id) + if err != nil { + return domain.Song{}, fmt.Errorf("failed to query song albums: %w", err) } - albumRows = append(albumRows, r) - } - if err := alRows.Err(); err != nil { - return domain.Song{}, fmt.Errorf("failed to iterate song albums: %w", err) + + for rows.Next() { + var r songAlbumRow + if err := rows.Scan(&r.albumID, &r.trackNumber); err != nil { + _ = rows.Close() + return domain.Song{}, fmt.Errorf("failed to scan song album: %w", err) + } + albumRows = append(albumRows, r) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Song{}, fmt.Errorf("failed to iterate song albums: %w", err) + } + _ = rows.Close() } - alRows.Close() // fetch full album records in one query if len(albumRows) > 0 { @@ -134,31 +144,36 @@ func GetSong(e Execer, id domain.SongID) (domain.Song, error) { placeholders = placeholders[:len(placeholders)-1] albumMap := make(map[domain.AlbumID]domain.Album, len(albumIDs)) - abRows, err := e.Query(` - select id, title, latin_name, release_date - from albums - where id in (`+placeholders+`) - `, albumIDs...) - if err != nil { - return domain.Song{}, fmt.Errorf("failed to query albums: %w", err) - } - defer abRows.Close() - for abRows.Next() { - var a domain.Album - var relDate string - if err := abRows.Scan(&a.ID, &a.Title, &a.LatinName, &relDate); err != nil { - return domain.Song{}, fmt.Errorf("failed to scan album: %w", err) - } - a.ReleaseDate, err = time.Parse(time.RFC3339, relDate) + { + rows, err := e.Query(` + select id, title, latin_name, release_date + from albums + where id in (`+placeholders+`) + `, albumIDs...) if err != nil { - return domain.Song{}, fmt.Errorf("failed to parse album release_date: %w", err) + return domain.Song{}, fmt.Errorf("failed to query albums: %w", err) } - albumMap[a.ID] = a - } - if err := abRows.Err(); err != nil { - return domain.Song{}, fmt.Errorf("failed to iterate albums: %w", err) + + for rows.Next() { + var a domain.Album + var relDate string + if err := rows.Scan(&a.ID, &a.Title, &a.LatinName, &relDate); err != nil { + _ = rows.Close() + return domain.Song{}, fmt.Errorf("failed to scan album: %w", err) + } + a.ReleaseDate, err = time.Parse(time.RFC3339, relDate) + if err != nil { + _ = rows.Close() + return domain.Song{}, fmt.Errorf("failed to parse album release_date: %w", err) + } + albumMap[a.ID] = a + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.Song{}, fmt.Errorf("failed to iterate albums: %w", err) + } + _ = rows.Close() } - abRows.Close() song.Albums = make([]domain.SongAlbum, len(albumRows)) for i, r := range albumRows { @@ -170,24 +185,29 @@ func GetSong(e Execer, id domain.SongID) (domain.Song, error) { } // fetch genres - gRows, err := e.Query(` - select genre - from song_genres - where song_id = ? - `, id) - if err != nil { - return domain.Song{}, fmt.Errorf("failed to query song genres: %w", err) - } - defer gRows.Close() - for gRows.Next() { - var g domain.Genre - if err := gRows.Scan(&g); err != nil { - return domain.Song{}, fmt.Errorf("failed to scan song genre: %w", err) + { + rows, err := e.Query(` + select genre + from song_genres + where song_id = ? + `, id) + if err != nil { + return domain.Song{}, fmt.Errorf("failed to query song genres: %w", err) + } + defer func() { + _ = rows.Close() + }() + + for rows.Next() { + var g domain.Genre + if err := rows.Scan(&g); err != nil { + return domain.Song{}, fmt.Errorf("failed to scan song genre: %w", err) + } + song.Genres = append(song.Genres, g) + } + if err := rows.Err(); err != nil { + return domain.Song{}, fmt.Errorf("failed to iterate song genres: %w", err) } - song.Genres = append(song.Genres, g) - } - if err := gRows.Err(); err != nil { - return domain.Song{}, fmt.Errorf("failed to iterate song genres: %w", err) } return song, nil