feat: add CRUD operations for artist_settings table
Implements GetArtistSettings, SaveArtistSettings, GetAllArtistSettings, and UpdateArtistSettings with full test coverage (10 new tests). All 17 database tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -87,14 +87,14 @@ Build the foundation layer of NaviWatcher: a greenfield Go project with zero exi
|
|||||||
- [x] run tests — must pass before task 4
|
- [x] run tests — must pass before task 4
|
||||||
|
|
||||||
### Task 4: Database layer — CRUD operations for artist_settings
|
### Task 4: Database layer — CRUD operations for artist_settings
|
||||||
- [ ] implement `GetArtistSettings(db *DB, id string) (*ArtistSettings, error)`
|
- [x] implement `GetArtistSettings(db *DB, id string) (*ArtistSettings, error)`
|
||||||
- [ ] implement `SaveArtistSettings(db *DB, settings *ArtistSettings) error`
|
- [x] implement `SaveArtistSettings(db *DB, settings *ArtistSettings) error`
|
||||||
- [ ] implement `GetAllArtistSettings(db *DB) ([]ArtistSettings, error)`
|
- [x] implement `GetAllArtistSettings(db *DB) ([]ArtistSettings, error)`
|
||||||
- [ ] implement `UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) error`
|
- [x] implement `UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) error`
|
||||||
- [ ] write tests for GetArtistSettings (found and not found cases)
|
- [x] write tests for GetArtistSettings (found and not found cases)
|
||||||
- [ ] write tests for SaveArtistSettings (insert and update)
|
- [x] write tests for SaveArtistSettings (insert and update)
|
||||||
- [ ] write tests for GetAllArtistSettings (empty and populated)
|
- [x] write tests for GetAllArtistSettings (empty and populated)
|
||||||
- [ ] run tests — must pass before task 5
|
- [x] run tests — must pass before task 5
|
||||||
|
|
||||||
### Task 5: Database layer — CRUD operations for external_releases
|
### Task 5: Database layer — CRUD operations for external_releases
|
||||||
- [ ] implement `GetExternalRelease(db *DB, rgid string) (*ExternalRelease, error)`
|
- [ ] implement `GetExternalRelease(db *DB, rgid string) (*ExternalRelease, error)`
|
||||||
|
|||||||
101
internal/database/artist_settings.go
Normal file
101
internal/database/artist_settings.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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
|
||||||
|
err := db.Conn().QueryRow(
|
||||||
|
"SELECT id, name, ignore_singles, ignore_compilations, monitored FROM artist_settings WHERE id = ?",
|
||||||
|
id,
|
||||||
|
).Scan(&s.ID, &s.Name, &s.IgnoreSingles, &s.IgnoreCompilations, &s.Monitored)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("save artist settings: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query all artist settings: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, fmt.Errorf("scan artist settings: %w", err)
|
||||||
|
}
|
||||||
|
results = append(results, s)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("iterate artist settings: %w", err)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateArtistSettings updates specific fields of an artist_settings row by ID.
|
||||||
|
// The updates map keys must match column names: "name", "ignore_singles", "ignore_compilations", "monitored".
|
||||||
|
func UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) error {
|
||||||
|
// Build the SET clause dynamically from the provided updates.
|
||||||
|
allowed := map[string]bool{
|
||||||
|
"name": true,
|
||||||
|
"ignore_singles": true,
|
||||||
|
"ignore_compilations": true,
|
||||||
|
"monitored": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
setClause := ""
|
||||||
|
for col, val := range updates {
|
||||||
|
if !allowed[col] {
|
||||||
|
return fmt.Errorf("unknown column: %s", col)
|
||||||
|
}
|
||||||
|
if setClause != "" {
|
||||||
|
setClause += ", "
|
||||||
|
}
|
||||||
|
setClause += col + " = ?"
|
||||||
|
args = append(args, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("no updates provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, id)
|
||||||
|
query := fmt.Sprintf("UPDATE artist_settings SET %s WHERE id = ?", setClause)
|
||||||
|
result, err := db.Conn().Exec(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update artist settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rows affected: %w", err)
|
||||||
|
}
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return fmt.Errorf("artist not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
307
internal/database/artist_settings_test.go
Normal file
307
internal/database/artist_settings_test.go
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetArtistSettings_Found verifies retrieving an existing artist.
|
||||||
|
func TestGetArtistSettings_Found(t *testing.T) {
|
||||||
|
db, err := New(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Insert a row directly.
|
||||||
|
_, err = db.Conn().Exec(
|
||||||
|
"INSERT INTO artist_settings (id, name, ignore_singles, ignore_compilations, monitored) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
"artist-1", "Test Artist", true, false, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := GetArtistSettings(db, "artist-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ID != "artist-1" {
|
||||||
|
t.Errorf("expected ID 'artist-1', got %q", s.ID)
|
||||||
|
}
|
||||||
|
if s.Name != "Test Artist" {
|
||||||
|
t.Errorf("expected Name 'Test Artist', got %q", s.Name)
|
||||||
|
}
|
||||||
|
if !s.IgnoreSingles {
|
||||||
|
t.Error("expected IgnoreSingles true")
|
||||||
|
}
|
||||||
|
if s.IgnoreCompilations {
|
||||||
|
t.Error("expected IgnoreCompilations false")
|
||||||
|
}
|
||||||
|
if !s.Monitored {
|
||||||
|
t.Error("expected Monitored true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetArtistSettings_NotFound verifies that a missing artist returns sql.ErrNoRows.
|
||||||
|
func TestGetArtistSettings_NotFound(t *testing.T) {
|
||||||
|
db, err := New(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
_, err = GetArtistSettings(db, "nonexistent")
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
t.Errorf("expected sql.ErrNoRows, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSaveArtistSettings_Insert verifies inserting a new artist.
|
||||||
|
func TestSaveArtistSettings_Insert(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: "New Artist",
|
||||||
|
IgnoreSingles: false,
|
||||||
|
IgnoreCompilations: true,
|
||||||
|
Monitored: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := SaveArtistSettings(db, s); err != nil {
|
||||||
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it was inserted.
|
||||||
|
got, err := GetArtistSettings(db, "artist-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != "New Artist" {
|
||||||
|
t.Errorf("expected Name 'New Artist', got %q", got.Name)
|
||||||
|
}
|
||||||
|
if got.IgnoreCompilations != true {
|
||||||
|
t.Errorf("expected IgnoreCompilations true, got %v", got.IgnoreCompilations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSaveArtistSettings_Update verifies that SaveArtistSettings replaces an existing row.
|
||||||
|
func TestSaveArtistSettings_Update(t *testing.T) {
|
||||||
|
db, err := New(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Insert initial row.
|
||||||
|
s1 := &ArtistSettings{
|
||||||
|
ID: "artist-1",
|
||||||
|
Name: "Original Name",
|
||||||
|
IgnoreSingles: false,
|
||||||
|
IgnoreCompilations: false,
|
||||||
|
Monitored: true,
|
||||||
|
}
|
||||||
|
if err := SaveArtistSettings(db, s1); err != nil {
|
||||||
|
t.Fatalf("first SaveArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the row.
|
||||||
|
s2 := &ArtistSettings{
|
||||||
|
ID: "artist-1",
|
||||||
|
Name: "Updated Name",
|
||||||
|
IgnoreSingles: true,
|
||||||
|
IgnoreCompilations: true,
|
||||||
|
Monitored: false,
|
||||||
|
}
|
||||||
|
if err := SaveArtistSettings(db, s2); err != nil {
|
||||||
|
t.Fatalf("second SaveArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := GetArtistSettings(db, "artist-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != "Updated Name" {
|
||||||
|
t.Errorf("expected Name 'Updated Name', got %q", got.Name)
|
||||||
|
}
|
||||||
|
if !got.IgnoreSingles {
|
||||||
|
t.Error("expected IgnoreSingles true")
|
||||||
|
}
|
||||||
|
if !got.IgnoreCompilations {
|
||||||
|
t.Error("expected IgnoreCompilations true")
|
||||||
|
}
|
||||||
|
if got.Monitored {
|
||||||
|
t.Error("expected Monitored false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetAllArtistSettings_Empty verifies GetAllArtistSettings returns empty slice when no rows.
|
||||||
|
func TestGetAllArtistSettings_Empty(t *testing.T) {
|
||||||
|
db, err := New(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
results, err := GetAllArtistSettings(db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetAllArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) != 0 {
|
||||||
|
t.Errorf("expected 0 results, got %d", len(results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetAllArtistSettings_Populated verifies GetAllArtistSettings returns all rows.
|
||||||
|
func TestGetAllArtistSettings_Populated(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", Monitored: true},
|
||||||
|
{ID: "a2", Name: "Artist Two", IgnoreSingles: true},
|
||||||
|
{ID: "a3", Name: "Artist Three", IgnoreCompilations: 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) != 3 {
|
||||||
|
t.Fatalf("expected 3 results, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all artists are present (order not guaranteed, use a map).
|
||||||
|
byID := make(map[string]ArtistSettings)
|
||||||
|
for _, r := range results {
|
||||||
|
byID[r.ID] = r
|
||||||
|
}
|
||||||
|
for _, expected := range artists {
|
||||||
|
got, ok := byID[expected.ID]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected artist %s in results", expected.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got.Name != expected.Name {
|
||||||
|
t.Errorf("artist %s: expected Name %q, got %q", expected.ID, expected.Name, got.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpdateArtistSettings_Success verifies partial updates work.
|
||||||
|
func TestUpdateArtistSettings_Success(t *testing.T) {
|
||||||
|
db, err := New(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Insert initial row.
|
||||||
|
s := &ArtistSettings{
|
||||||
|
ID: "artist-1",
|
||||||
|
Name: "Original",
|
||||||
|
IgnoreSingles: false,
|
||||||
|
IgnoreCompilations: false,
|
||||||
|
Monitored: true,
|
||||||
|
}
|
||||||
|
if err := SaveArtistSettings(db, s); err != nil {
|
||||||
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only name and monitored.
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"name": "Updated",
|
||||||
|
"monitored": false,
|
||||||
|
}
|
||||||
|
if err := UpdateArtistSettings(db, "artist-1", updates); err != nil {
|
||||||
|
t.Fatalf("UpdateArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := GetArtistSettings(db, "artist-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != "Updated" {
|
||||||
|
t.Errorf("expected Name 'Updated', got %q", got.Name)
|
||||||
|
}
|
||||||
|
if got.Monitored {
|
||||||
|
t.Error("expected Monitored false")
|
||||||
|
}
|
||||||
|
// Unchanged fields should remain.
|
||||||
|
if got.IgnoreSingles != false {
|
||||||
|
t.Error("expected IgnoreSingles unchanged (false)")
|
||||||
|
}
|
||||||
|
if got.IgnoreCompilations != false {
|
||||||
|
t.Error("expected IgnoreCompilations unchanged (false)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpdateArtistSettings_NotFound verifies updating a nonexistent artist returns error.
|
||||||
|
func TestUpdateArtistSettings_NotFound(t *testing.T) {
|
||||||
|
db, err := New(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
updates := map[string]interface{}{"name": "Ghost"}
|
||||||
|
err = UpdateArtistSettings(db, "nonexistent", updates)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for nonexistent artist, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpdateArtistSettings_InvalidColumn verifies unknown columns are rejected.
|
||||||
|
func TestUpdateArtistSettings_InvalidColumn(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"}
|
||||||
|
if err := SaveArtistSettings(db, s); err != nil {
|
||||||
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{"invalid_col": "value"}
|
||||||
|
err = UpdateArtistSettings(db, "artist-1", updates)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid column, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpdateArtistSettings_EmptyUpdates verifies empty updates map returns error.
|
||||||
|
func TestUpdateArtistSettings_EmptyUpdates(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"}
|
||||||
|
if err := SaveArtistSettings(db, s); err != nil {
|
||||||
|
t.Fatalf("SaveArtistSettings() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
err = UpdateArtistSettings(db, "artist-1", updates)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for empty updates, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user