fix(persistence): keep the new item's annotation when reassigning onto an item the user already annotated

ReassignAnnotation was a plain UPDATE; the annotation table is unique on
(user_id, item_id, item_type), so when a user had annotated both items the
statement aborted and none of the rows moved. In the scanner that surfaced as
a warning; in the missing-file remap it rolled back the whole operation.
UPDATE OR IGNORE moves what it can and leaves the conflicting rows for GC.
This commit is contained in:
Deluan 2026-09-12 01:11:01 -04:00
parent ed5fcd83fe
commit b4addc59c7
2 changed files with 40 additions and 4 deletions

View File

@ -185,10 +185,9 @@ func (r sqlRepository) ReassignAnnotation(prevID string, newID string) error {
if prevID == newID || prevID == "" || newID == "" {
return nil
}
upd := Update(annotationTable).Where(And{
Eq{annotationTable + ".item_type": r.tableName},
Eq{annotationTable + ".item_id": prevID},
}).Set("item_id", newID)
// OR IGNORE keeps newID's own row where a user annotated both, instead of aborting the whole statement
upd := Expr("update or ignore "+annotationTable+" set item_id = ? where item_type = ? and item_id = ?",
newID, r.tableName, prevID)
_, err := r.executeSQL(upd)
return err
}

View File

@ -30,6 +30,43 @@ var _ = Describe("Annotation Filters", func() {
_, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": albumWithoutAnnotation.ID}))
})
Describe("ReassignAnnotation", func() {
var prev, next model.Album
BeforeEach(func() {
prev = model.Album{ID: "reassign-prev", Name: "Prev", LibraryID: 1}
next = model.Album{ID: "reassign-next", Name: "Next", LibraryID: 1}
Expect(albumRepo.Put(&prev)).To(Succeed())
Expect(albumRepo.Put(&next)).To(Succeed())
})
AfterEach(func() {
_, _ = albumRepo.executeSQL(squirrel.Delete("annotation").Where(squirrel.Eq{"item_id": []string{prev.ID, next.ID}}))
_, _ = albumRepo.executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": []string{prev.ID, next.ID}}))
})
It("moves the annotation when the new item has none", func() {
Expect(albumRepo.SetRating(4, prev.ID)).To(Succeed())
Expect(albumRepo.ReassignAnnotation(prev.ID, next.ID)).To(Succeed())
got, err := albumRepo.Get(next.ID)
Expect(err).ToNot(HaveOccurred())
Expect(got.Rating).To(Equal(4))
})
It("keeps the new item's annotation when both exist", func() {
Expect(albumRepo.SetRating(4, prev.ID)).To(Succeed())
Expect(albumRepo.SetRating(2, next.ID)).To(Succeed())
Expect(albumRepo.ReassignAnnotation(prev.ID, next.ID)).To(Succeed())
got, err := albumRepo.Get(next.ID)
Expect(err).ToNot(HaveOccurred())
Expect(got.Rating).To(Equal(2))
})
})
Describe("annotationBoolFilter", func() {
DescribeTable("creates correct SQL expressions",
func(field, value string, expectedSQL string, expectedArgs []any) {