Merge pull request #17658 from martin-77/fix/child-count-sql-variable-limit

Fix SQLite variable limit in child count batches
This commit is contained in:
Cody Robibero
2026-08-17 18:18:29 -04:00
committed by GitHub
2 changed files with 133 additions and 2 deletions
@@ -318,13 +318,14 @@ public class ItemCountService : IItemCountService
var parentIdsArray = parentIds.ToArray();
var hierarchicalCounts = dbContext.BaseItems
.Where(b => b.ParentId.HasValue && parentIdsArray.Contains(b.ParentId.Value))
.Where(b => b.ParentId.HasValue)
.WhereOneOrMany(parentIdsArray, b => b.ParentId!.Value)
.GroupBy(b => b.ParentId!.Value)
.Select(g => new { ParentId = g.Key, Count = g.Count() })
.ToDictionary(x => x.ParentId, x => x.Count);
var linkedCounts = dbContext.LinkedChildren
.Where(lc => parentIdsArray.Contains(lc.ParentId))
.WhereOneOrMany(parentIdsArray, lc => lc.ParentId)
.GroupBy(lc => lc.ParentId)
.Select(g => new { ParentId = g.Key, Count = g.Count() })
.ToDictionary(x => x.ParentId, x => x.Count);
@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Locking;
using Jellyfin.Database.Providers.Sqlite;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Item;
public sealed class ItemCountServiceTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly DbContextOptions<JellyfinDbContext> _dbOptions;
private readonly IApplicationPaths _applicationPaths;
private readonly ItemCountService _service;
public ItemCountServiceTests()
{
_applicationPaths = new Mock<IApplicationPaths>().Object;
_connection = new SqliteConnection("Data Source=:memory:");
_connection.Open();
_dbOptions = new DbContextOptionsBuilder<JellyfinDbContext>()
.UseSqlite(_connection)
.Options;
using (var context = CreateDbContext())
{
context.Database.EnsureCreated();
}
var factory = new Mock<IDbContextFactory<JellyfinDbContext>>();
factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
_service = new ItemCountService(
factory.Object,
new Mock<IItemTypeLookup>().Object,
new Mock<IItemQueryHelpers>().Object);
}
public void Dispose()
{
_connection.Dispose();
}
[Fact]
public void GetChildCountBatch_LargeParentIdSet_DoesNotExceedSqliteVariableLimit()
{
var hierarchicalParentId = Guid.NewGuid();
var linkedParentId = Guid.NewGuid();
var hierarchicalChildId = Guid.NewGuid();
var linkedChildId1 = Guid.NewGuid();
var linkedChildId2 = Guid.NewGuid();
using (var context = CreateDbContext())
{
context.BaseItems.AddRange(
CreateItem(hierarchicalParentId),
CreateItem(linkedParentId),
CreateItem(hierarchicalChildId, hierarchicalParentId),
CreateItem(linkedChildId1),
CreateItem(linkedChildId2));
context.LinkedChildren.AddRange(
new LinkedChildEntity
{
ParentId = linkedParentId,
ChildId = linkedChildId1,
ChildType = LinkedChildType.Manual,
SortOrder = 0
},
new LinkedChildEntity
{
ParentId = linkedParentId,
ChildId = linkedChildId2,
ChildType = LinkedChildType.Manual,
SortOrder = 1
});
context.SaveChanges();
}
var parentIds = Enumerable.Range(0, 40_000)
.Select(_ => Guid.NewGuid())
.ToList();
parentIds.Add(hierarchicalParentId);
parentIds.Add(linkedParentId);
var result = _service.GetChildCountBatch(parentIds, null);
Assert.Equal(1, result[hierarchicalParentId]);
Assert.Equal(2, result[linkedParentId]);
Assert.Equal(parentIds.Count, result.Count);
}
private static BaseItemEntity CreateItem(Guid id, Guid? parentId = null)
{
return new BaseItemEntity
{
Id = id,
Type = "Folder",
ParentId = parentId,
IsFolder = true
};
}
private JellyfinDbContext CreateDbContext()
{
return new JellyfinDbContext(
_dbOptions,
NullLogger<JellyfinDbContext>.Instance,
new SqliteDatabaseProvider(
_applicationPaths,
NullLogger<SqliteDatabaseProvider>.Instance),
new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
}
}