feat: add mbid column to artist_settings

Additive migration 008 adds an mbid column to artist_settings, extend the
ArtistSettings struct and persistence functions (SaveArtistSettings,
GetArtistSettings, GetAllArtistSettings, UpdateArtistSettings) to carry the
MusicBrainz ID, and add round-trip tests covering empty and set MBID values.
This commit is contained in:
2026-07-19 22:13:57 +03:00
parent 35d6a5234f
commit 85c42ec858
5 changed files with 346 additions and 11 deletions

View File

@@ -1,28 +1,33 @@
package database
import (
"database/sql"
"fmt"
)
// GetArtistSettings retrieves an artist_settings row by ID.
// Returns sql.ErrNoRows if the artist is not found.
func GetArtistSettings(db *DB, id string) (*ArtistSettings, error) {
var s ArtistSettings
var (
s ArtistSettings
mbid sql.NullString
)
err := db.Conn().QueryRow(
"SELECT id, name, ignore_singles, ignore_compilations, monitored FROM artist_settings WHERE id = ?",
"SELECT id, name, mbid, ignore_singles, ignore_compilations, monitored FROM artist_settings WHERE id = ?",
id,
).Scan(&s.ID, &s.Name, &s.IgnoreSingles, &s.IgnoreCompilations, &s.Monitored)
).Scan(&s.ID, &s.Name, &mbid, &s.IgnoreSingles, &s.IgnoreCompilations, &s.Monitored)
if err != nil {
return nil, err
}
s.MBID = mbid.String
return &s, nil
}
// SaveArtistSettings inserts or replaces an artist_settings row.
func SaveArtistSettings(db *DB, settings *ArtistSettings) error {
_, err := db.Conn().Exec(
"INSERT OR REPLACE INTO artist_settings (id, name, ignore_singles, ignore_compilations, monitored) VALUES (?, ?, ?, ?, ?)",
settings.ID, settings.Name, settings.IgnoreSingles, settings.IgnoreCompilations, settings.Monitored,
"INSERT OR REPLACE INTO artist_settings (id, name, mbid, ignore_singles, ignore_compilations, monitored) VALUES (?, ?, ?, ?, ?, ?)",
settings.ID, settings.Name, settings.MBID, settings.IgnoreSingles, settings.IgnoreCompilations, settings.Monitored,
)
if err != nil {
return fmt.Errorf("save artist settings: %w", err)
@@ -33,7 +38,7 @@ func SaveArtistSettings(db *DB, settings *ArtistSettings) error {
// GetAllArtistSettings returns all rows from artist_settings.
func GetAllArtistSettings(db *DB) ([]ArtistSettings, error) {
rows, err := db.Conn().Query(
"SELECT id, name, ignore_singles, ignore_compilations, monitored FROM artist_settings",
"SELECT id, name, mbid, ignore_singles, ignore_compilations, monitored FROM artist_settings",
)
if err != nil {
return nil, fmt.Errorf("query all artist settings: %w", err)
@@ -43,7 +48,7 @@ func GetAllArtistSettings(db *DB) ([]ArtistSettings, error) {
var results []ArtistSettings
for rows.Next() {
var s ArtistSettings
if err := rows.Scan(&s.ID, &s.Name, &s.IgnoreSingles, &s.IgnoreCompilations, &s.Monitored); err != nil {
if err := rows.Scan(&s.ID, &s.Name, &s.MBID, &s.IgnoreSingles, &s.IgnoreCompilations, &s.Monitored); err != nil {
return nil, fmt.Errorf("scan artist settings: %w", err)
}
results = append(results, s)
@@ -74,6 +79,12 @@ func UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) err
}
setClause += "name = ?"
args = append(args, val)
case "mbid":
if setClause != "" {
setClause += ", "
}
setClause += "mbid = ?"
args = append(args, val)
case "ignore_singles":
if setClause != "" {
setClause += ", "

View File

@@ -305,3 +305,158 @@ func TestUpdateArtistSettings_EmptyUpdates(t *testing.T) {
t.Error("expected error for empty updates, got nil")
}
}
// TestMigration008_MbidColumnExists verifies the 008 migration adds the mbid
// column and that rows created before resolution have a NULL/empty MBID.
func TestMigration008_MbidColumnExists(t *testing.T) {
db, err := New(":memory:")
if err != nil {
t.Fatalf("New() error: %v", err)
}
defer db.Close()
// Insert a row without supplying mbid (simulates a pre-resolution row).
if _, err := db.Conn().Exec(
"INSERT INTO artist_settings (id, name) VALUES (?, ?)",
"artist-1", "No MBID Yet",
); err != nil {
t.Fatalf("insert without mbid: %v", err)
}
var mbid sql.NullString
if err := db.Conn().QueryRow(
"SELECT mbid FROM artist_settings WHERE id = ?", "artist-1",
).Scan(&mbid); err != nil {
t.Fatalf("query mbid: %v", err)
}
if mbid.Valid && mbid.String != "" {
t.Errorf("expected empty mbid for pre-resolution row, got %q", mbid.String)
}
}
// TestArtistSettings_MbidRoundTrip verifies Save/Get round-trips an MBID,
// and that an empty MBID is preserved (not overwritten with garbage).
func TestArtistSettings_MbidRoundTrip(t *testing.T) {
db, err := New(":memory:")
if err != nil {
t.Fatalf("New() error: %v", err)
}
defer db.Close()
s := &ArtistSettings{
ID: "artist-1",
Name: "Test Artist",
MBID: "f27e6623-8771-4a2e-8dcb-6c8b1a4f8b9a",
Monitored: true,
}
if err := SaveArtistSettings(db, s); err != nil {
t.Fatalf("SaveArtistSettings() error: %v", err)
}
got, err := GetArtistSettings(db, "artist-1")
if err != nil {
t.Fatalf("GetArtistSettings() error: %v", err)
}
if got.MBID != s.MBID {
t.Errorf("expected MBID %q, got %q", s.MBID, got.MBID)
}
}
// TestArtistSettings_MbidEmptyAllowed verifies an artist can be saved and
// retrieved with no MBID set (lazy resolution not yet performed).
func TestArtistSettings_MbidEmptyAllowed(t *testing.T) {
db, err := New(":memory:")
if err != nil {
t.Fatalf("New() error: %v", err)
}
defer db.Close()
s := &ArtistSettings{ID: "artist-1", Name: "No MBID", Monitored: true}
if err := SaveArtistSettings(db, s); err != nil {
t.Fatalf("SaveArtistSettings() error: %v", err)
}
got, err := GetArtistSettings(db, "artist-1")
if err != nil {
t.Fatalf("GetArtistSettings() error: %v", err)
}
if got.MBID != "" {
t.Errorf("expected empty MBID, got %q", got.MBID)
}
}
// TestArtistSettings_MbidUpdatePersists verifies UpdateArtistSettings can set
// and clear the MBID column.
func TestArtistSettings_MbidUpdatePersists(t *testing.T) {
db, err := New(":memory:")
if err != nil {
t.Fatalf("New() error: %v", err)
}
defer db.Close()
if err := SaveArtistSettings(db, &ArtistSettings{ID: "artist-1", Name: "Test", Monitored: true}); err != nil {
t.Fatalf("SaveArtistSettings() error: %v", err)
}
mbid := "f27e6623-8771-4a2e-8dcb-6c8b1a4f8b9a"
if err := UpdateArtistSettings(db, "artist-1", map[string]interface{}{"mbid": mbid}); err != nil {
t.Fatalf("UpdateArtistSettings(set mbid) error: %v", err)
}
got, err := GetArtistSettings(db, "artist-1")
if err != nil {
t.Fatalf("GetArtistSettings() error: %v", err)
}
if got.MBID != mbid {
t.Errorf("expected MBID %q after set, got %q", mbid, got.MBID)
}
// Clear it again.
if err := UpdateArtistSettings(db, "artist-1", map[string]interface{}{"mbid": ""}); err != nil {
t.Fatalf("UpdateArtistSettings(clear mbid) error: %v", err)
}
got, err = GetArtistSettings(db, "artist-1")
if err != nil {
t.Fatalf("GetArtistSettings() error: %v", err)
}
if got.MBID != "" {
t.Errorf("expected empty MBID after clear, got %q", got.MBID)
}
}
// TestArtistSettings_MbidInGetAll verifies GetAllArtistSettings returns the
// MBID field for all rows.
func TestArtistSettings_MbidInGetAll(t *testing.T) {
db, err := New(":memory:")
if err != nil {
t.Fatalf("New() error: %v", err)
}
defer db.Close()
artists := []ArtistSettings{
{ID: "a1", Name: "Artist One", MBID: "mbid-1", Monitored: true},
{ID: "a2", Name: "Artist Two", Monitored: true},
}
for _, a := range artists {
if err := SaveArtistSettings(db, &a); err != nil {
t.Fatalf("SaveArtistSettings(%s) error: %v", a.ID, err)
}
}
results, err := GetAllArtistSettings(db)
if err != nil {
t.Fatalf("GetAllArtistSettings() error: %v", err)
}
if len(results) != 2 {
t.Fatalf("expected 2 results, got %d", len(results))
}
byID := make(map[string]ArtistSettings)
for _, r := range results {
byID[r.ID] = r
}
if byID["a1"].MBID != "mbid-1" {
t.Errorf("artist a1: expected MBID 'mbid-1', got %q", byID["a1"].MBID)
}
if byID["a2"].MBID != "" {
t.Errorf("artist a2: expected empty MBID, got %q", byID["a2"].MBID)
}
}

View File

@@ -125,6 +125,10 @@ func (db *DB) migrate() error {
name: "007_index_external_releases_artist_id",
sql: `CREATE INDEX IF NOT EXISTS idx_external_releases_artist_id ON external_releases(artist_id);`,
},
{
name: "008_add_mbid_to_artist_settings",
sql: `ALTER TABLE artist_settings ADD COLUMN mbid TEXT;`,
},
}
for _, m := range migrations {
@@ -173,6 +177,7 @@ func (db *DB) isMigrationApplied(name string) (bool, error) {
type ArtistSettings struct {
ID string `json:"id"`
Name string `json:"name"`
MBID string `json:"mbid"`
IgnoreSingles bool `json:"ignore_singles"`
IgnoreCompilations bool `json:"ignore_compilations"`
Monitored bool `json:"monitored"`

View File

@@ -205,10 +205,11 @@ func TestMigrationTracking(t *testing.T) {
t.Fatalf("query migrations count: %v", err)
}
// We have 7 recorded migrations: artist_settings, external_releases,
// We have 8 recorded migrations: artist_settings, external_releases,
// local_albums, notifications_sent, cached_at column, secondary_types
// column, and the external_releases.artist_id index.
if count != 7 {
t.Errorf("expected 7 applied migrations, got %d", count)
// column, the external_releases.artist_id index, and the artist_settings
// mbid column.
if count != 8 {
t.Errorf("expected 8 applied migrations, got %d", count)
}
}