mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
fix(playlists): chunk track deletes to stay under the SQLite variable limit (#5977)
PlaylistTrackRepository.Delete built a single IN clause with one bind variable per track, so removing more tracks than SQLITE_MAX_VARIABLE_NUMBER (32766) failed with "too many SQL variables". Clients that sync a large playlist by adding the desired tracks and then removing the stale ones would get the add committed and the removal rejected, leaving the playlist with both sets of tracks and growing it on every sync. Delete now works in chunks of 200, the same size addTracks already uses, and renumbers once after the last chunk. Both callers already run inside a transaction, so the delete stays atomic.
This commit is contained in:
parent
ea1e2b95a7
commit
65751d7665
@ -2,6 +2,7 @@ package persistence
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"slices"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
@ -224,10 +225,14 @@ func (r *playlistTrackRepository) AddDiscs(discs []model.DiscID) (int, error) {
|
||||
return r.addMediaFileIds(clauses)
|
||||
}
|
||||
|
||||
// deleteChunkSize keeps each DELETE under SQLITE_MAX_VARIABLE_NUMBER, matching addTracks.
|
||||
const deleteChunkSize = 200
|
||||
|
||||
func (r *playlistTrackRepository) Delete(ids ...string) error {
|
||||
err := r.delete(And{Eq{"playlist_id": r.playlistId}, Eq{"id": ids}})
|
||||
if err != nil {
|
||||
return err
|
||||
for chunk := range slices.Chunk(ids, deleteChunkSize) {
|
||||
if err := r.delete(And{Eq{"playlist_id": r.playlistId}, Eq{"id": chunk}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return r.playlistRepo.renumber(r.playlistId)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
@ -8,6 +10,9 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// sqliteMaxVariables is SQLITE_MAX_VARIABLE_NUMBER as compiled into the driver
|
||||
const sqliteMaxVariables = 32766
|
||||
|
||||
var _ = Describe("PlaylistTrackRepository", func() {
|
||||
var repo model.PlaylistTrackRepository
|
||||
|
||||
@ -72,4 +77,49 @@ var _ = Describe("PlaylistTrackRepository", func() {
|
||||
To(Equal([]string{songRadioactivity.ID}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Delete", func() {
|
||||
var tracks model.PlaylistTrackRepository
|
||||
const numTracks = deleteChunkSize*2 + 1
|
||||
|
||||
positionsUpTo := func(n int) []string {
|
||||
positions := make([]string, 0, n)
|
||||
for i := 1; i <= n; i++ {
|
||||
positions = append(positions, strconv.Itoa(i))
|
||||
}
|
||||
return positions
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx := log.NewContext(GinkgoT().Context())
|
||||
ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "userid", IsAdmin: true})
|
||||
plsRepo := NewPlaylistRepository(ctx, GetDBXBuilder())
|
||||
|
||||
pls := model.Playlist{Name: "Chunked Delete", OwnerID: "userid", OwnerName: "userid"}
|
||||
Expect(plsRepo.Put(&pls)).To(Succeed())
|
||||
DeferCleanup(func() { Expect(plsRepo.Delete(pls.ID)).To(Succeed()) })
|
||||
|
||||
tracks = plsRepo.Tracks(pls.ID, false)
|
||||
songIds := make([]string, numTracks)
|
||||
for i := range songIds {
|
||||
songIds[i] = songDayInALife.ID
|
||||
}
|
||||
Expect(tracks.Add(songIds)).To(Equal(numTracks))
|
||||
})
|
||||
|
||||
It("removes positions spanning several chunks, and renumbers what is left", func() {
|
||||
Expect(tracks.Delete(positionsUpTo(numTracks - 1)...)).To(Succeed())
|
||||
|
||||
Expect(tracks.CountAll()).To(Equal(int64(1)))
|
||||
remaining, err := tracks.GetAll(model.QueryOptions{Sort: "id"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(remaining[0].ID).To(Equal("1"), "the surviving track must be renumbered to position 1")
|
||||
})
|
||||
|
||||
It("accepts more ids than SQLite allows as bind variables", func() {
|
||||
Expect(tracks.Delete(positionsUpTo(sqliteMaxVariables + 100)...)).To(Succeed())
|
||||
|
||||
Expect(tracks.CountAll()).To(BeZero())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user