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 <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2025-07-21 22:55:28 -04:00
committed by GitHub
parent e9a8d7ed66
commit 36d73eec0d
2 changed files with 63 additions and 0 deletions

View File

@@ -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;

View File

@@ -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() {