From 36d73eec0db2783cd804a5e501b8f9e42a0d4cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Mon, 21 Jul 2025 22:55:28 -0400 Subject: [PATCH] fix(scanner): prevent foreign key constraint error in tag UpdateCounts (#4370) * fix: prevent foreign key constraint error in tag UpdateCounts Added JOIN clause with tag table in UpdateCounts SQL query to filter out tag IDs from JSON that don't exist in the tag table. This prevents 'FOREIGN KEY constraint failed' errors when the library_tag table tries to reference non-existent tag IDs during scanner operations. The fix ensures only valid tag references are counted while maintaining data integrity and preventing scanner failures during library updates. * test(tag): add regression tests for foreign key constraint fix Add comprehensive regression tests to prevent the foreign key constraint error when tag IDs in JSON data don't exist in the tag table. Tests cover both album and media file scenarios with non-existent tag IDs. - Test UpdateCounts() with albums containing non-existent tag IDs - Test UpdateCounts() with media files containing non-existent tag IDs - Verify operations complete without foreign key errors Signed-off-by: Deluan --------- Signed-off-by: Deluan --- persistence/tag_repository.go | 1 + persistence/tag_repository_test.go | 62 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/persistence/tag_repository.go b/persistence/tag_repository.go index 729208999..b224450ab 100644 --- a/persistence/tag_repository.go +++ b/persistence/tag_repository.go @@ -56,6 +56,7 @@ INSERT INTO library_tag (tag_id, library_id, %[1]s_count) SELECT jt.value as tag_id, %[1]s.library_id, count(distinct %[1]s.id) as %[1]s_count FROM %[1]s JOIN json_tree(%[1]s.tags, '$.genre') as jt ON jt.atom IS NOT NULL AND jt.key = 'id' +JOIN tag ON tag.id = jt.value GROUP BY jt.value, %[1]s.library_id ON CONFLICT (tag_id, library_id) DO UPDATE SET %[1]s_count = excluded.%[1]s_count; diff --git a/persistence/tag_repository_test.go b/persistence/tag_repository_test.go index 9b8f93cd9..c3947a9f7 100644 --- a/persistence/tag_repository_test.go +++ b/persistence/tag_repository_test.go @@ -13,6 +13,7 @@ import ( "github.com/navidrome/navidrome/model/request" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/pocketbase/dbx" ) var _ = Describe("TagRepository", func() { @@ -135,6 +136,67 @@ var _ = Describe("TagRepository", func() { err = repo.UpdateCounts() Expect(err).ToNot(HaveOccurred()) }) + + It("should handle albums with non-existent tag IDs in JSON gracefully", func() { + // Regression test for foreign key constraint error + // Create an album with tag IDs in JSON that don't exist in tag table + db := GetDBXBuilder() + + // First, create a non-existent tag ID (this simulates tags in JSON that aren't in tag table) + nonExistentTagID := id.NewTagID("genre", "nonexistent-genre") + + // Create album with JSON containing the non-existent tag ID + albumWithBadTags := `{"genre":[{"id":"` + nonExistentTagID + `","value":"nonexistent-genre"}]}` + + // Insert album directly into database with the problematic JSON + _, err := db.NewQuery("INSERT INTO album (id, name, library_id, tags) VALUES ({:id}, {:name}, {:lib}, {:tags})"). + Bind(dbx.Params{ + "id": "test-album-bad-tags", + "name": "Album With Bad Tags", + "lib": 1, + "tags": albumWithBadTags, + }).Execute() + Expect(err).ToNot(HaveOccurred()) + + // This should not fail with foreign key constraint error + err = repo.UpdateCounts() + Expect(err).ToNot(HaveOccurred()) + + // Cleanup + _, err = db.NewQuery("DELETE FROM album WHERE id = {:id}"). + Bind(dbx.Params{"id": "test-album-bad-tags"}).Execute() + Expect(err).ToNot(HaveOccurred()) + }) + + It("should handle media files with non-existent tag IDs in JSON gracefully", func() { + // Regression test for foreign key constraint error with media files + db := GetDBXBuilder() + + // Create a non-existent tag ID + nonExistentTagID := id.NewTagID("genre", "another-nonexistent-genre") + + // Create media file with JSON containing the non-existent tag ID + mediaFileWithBadTags := `{"genre":[{"id":"` + nonExistentTagID + `","value":"another-nonexistent-genre"}]}` + + // Insert media file directly into database with the problematic JSON + _, err := db.NewQuery("INSERT INTO media_file (id, title, library_id, tags) VALUES ({:id}, {:title}, {:lib}, {:tags})"). + Bind(dbx.Params{ + "id": "test-media-bad-tags", + "title": "Media File With Bad Tags", + "lib": 1, + "tags": mediaFileWithBadTags, + }).Execute() + Expect(err).ToNot(HaveOccurred()) + + // This should not fail with foreign key constraint error + err = repo.UpdateCounts() + Expect(err).ToNot(HaveOccurred()) + + // Cleanup + _, err = db.NewQuery("DELETE FROM media_file WHERE id = {:id}"). + Bind(dbx.Params{"id": "test-media-bad-tags"}).Execute() + Expect(err).ToNot(HaveOccurred()) + }) }) Describe("Count", func() {