diff --git a/db/migration/20211105162746_remove_invalid_artist_ids.go b/db/migration/20211105162746_remove_invalid_artist_ids.go new file mode 100644 index 00000000..4fd7c7bd --- /dev/null +++ b/db/migration/20211105162746_remove_invalid_artist_ids.go @@ -0,0 +1,22 @@ +package migrations + +import ( + "database/sql" + + "github.com/pressly/goose" +) + +func init() { + goose.AddMigration(upRemoveInvalidArtistIds, downRemoveInvalidArtistIds) +} + +func upRemoveInvalidArtistIds(tx *sql.Tx) error { + _, err := tx.Exec(` +update media_file set artist_id = '' where not exists(select 1 from artist where id = artist_id) +`) + return err +} + +func downRemoveInvalidArtistIds(tx *sql.Tx) error { + return nil +} diff --git a/persistence/helpers.go b/persistence/helpers.go index af812675..583366ef 100644 --- a/persistence/helpers.go +++ b/persistence/helpers.go @@ -37,17 +37,25 @@ func toSnakeCase(str string) string { } func exists(subTable string, cond squirrel.Sqlizer) existsCond { - return existsCond{subTable: subTable, cond: cond} + return existsCond{subTable: subTable, cond: cond, not: false} +} + +func notExists(subTable string, cond squirrel.Sqlizer) existsCond { + return existsCond{subTable: subTable, cond: cond, not: true} } type existsCond struct { subTable string cond squirrel.Sqlizer + not bool } func (e existsCond) ToSql() (string, []interface{}, error) { sql, args, err := e.cond.ToSql() sql = fmt.Sprintf("exists (select 1 from %s where %s)", e.subTable, sql) + if e.not { + sql = "not " + sql + } return sql, args, err } diff --git a/persistence/helpers_test.go b/persistence/helpers_test.go index 56dc1719..3b4469d8 100644 --- a/persistence/helpers_test.go +++ b/persistence/helpers_test.go @@ -59,6 +59,16 @@ var _ = Describe("Helpers", func() { }) }) + Describe("notExists", func() { + It("constructs the correct NOT EXISTS query", func() { + e := notExists("artist", squirrel.ConcatExpr("id = artist_id")) + sql, args, err := e.ToSql() + Expect(sql).To(Equal("not exists (select 1 from artist where id = artist_id)")) + Expect(args).To(BeEmpty()) + Expect(err).To(BeNil()) + }) + }) + Describe("getMostFrequentMbzID", func() { It(`returns "" when no ids are passed`, func() { Expect(getMostFrequentMbzID(context.TODO(), " ", "", "")).To(Equal("")) diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index 902cfb72..d073b2c2 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -177,6 +177,13 @@ func (r *mediaFileRepository) DeleteByPath(basePath string) (int64, error) { return r.executeSQL(del) } +func (r *mediaFileRepository) removeNonAlbumArtistIds() error { + upd := Update(r.tableName).Set("artist_id", "").Where(notExists("artist", ConcatExpr("id = artist_id"))) + log.Debug(r.ctx, "Removing non-album artist_id") + _, err := r.executeSQL(upd) + return err +} + func (r *mediaFileRepository) Search(q string, offset int, size int) (model.MediaFiles, error) { results := model.MediaFiles{} err := r.doSearch(q, offset, size, &results, "title") diff --git a/persistence/persistence.go b/persistence/persistence.go index 54e7f9f7..e4b7af20 100644 --- a/persistence/persistence.go +++ b/persistence/persistence.go @@ -135,6 +135,11 @@ func (s *SQLStore) GC(ctx context.Context, rootFolder string) error { log.Error(ctx, "Error removing dangling tracks", err) return err } + err = s.MediaFile(ctx).(*mediaFileRepository).removeNonAlbumArtistIds() + if err != nil { + log.Error(ctx, "Error removing non-album artist_ids", err) + return err + } err = s.Album(ctx).(*albumRepository).purgeEmpty() if err != nil { log.Error(ctx, "Error removing empty albums", err)