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:
| M | internal/db/album.go | | | 128 | ++++++++++++++++++++++++++++++++++++++++++++----------------------------------- |
| M | internal/db/artist.go | | | 148 | +++++++++++++++++++++++++++++++++++++++++++------------------------------------ |
| M | internal/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