From c3a7de54f37a88a331ff93a19b878adca714212d Mon Sep 17 00:00:00 2001 From: Felix Förtsch Date: Wed, 2 Sep 2026 11:33:54 +0200 Subject: avoid correlated item value name queries --- .../Item/BaseItemRepository.ByName.cs | 14 +- .../Item/BaseItemRepositoryItemValueTests.cs | 174 +++++++++++++++++++++ .../Item/BaseItemRepositoryLegacyFilterTests.cs | 130 --------------- 3 files changed, 182 insertions(+), 136 deletions(-) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryItemValueTests.cs delete mode 100644 tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs index 2ff8131ade..70e4ca3b1d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs @@ -121,20 +121,22 @@ public sealed partial class BaseItemRepository { using var context = _dbProvider.CreateDbContext(); - var query = context.ItemValuesMap - .AsNoTracking() - .Where(e => itemValueTypes.Any(w => w == e.ItemValue.Type)); + var maps = context.ItemValuesMap.AsNoTracking(); if (withItemTypes.Count > 0) { - query = query.Where(e => withItemTypes.Contains(e.Item.Type)); + maps = maps.Where(e => withItemTypes.Contains(e.Item.Type)); } if (excludeItemTypes.Count > 0) { - query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type)); + maps = maps.Where(e => !excludeItemTypes.Contains(e.Item.Type)); } - return query.Select(e => e.ItemValue) + return context.ItemValues + .AsNoTracking() + .WhereOneOrMany(itemValueTypes, e => e.Type) + .Where(e => maps.Any(m => m.ItemValueId == e.ItemValueId)) + .Select(e => new { e.CleanValue, e.Value }) .GroupBy(e => e.CleanValue) .Select(g => g.Min(v => v.Value)!) .ToArray(); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryItemValueTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryItemValueTests.cs new file mode 100644 index 0000000000..91148501ce --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryItemValueTests.cs @@ -0,0 +1,174 @@ +using System; +using Emby.Server.Implementations.Data; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Server.Implementations.Item; +using MediaBrowser.Controller.Entities; +using Xunit; +using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind; + +namespace Jellyfin.Server.Implementations.Tests.Item; + +public sealed class BaseItemRepositoryItemValueTests : SqliteDbTestFixture +{ + private readonly BaseItemRepository _repository; + private readonly string _audioTypeName; + private readonly string _movieTypeName; + + public BaseItemRepositoryItemValueTests() + { + var itemTypeLookup = new ItemTypeLookup(); + _audioTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; + _movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; + _repository = CreateBaseItemRepository(itemTypeLookup); + } + + [Fact] + public void GetQueryFiltersLegacy_GroupsAndFiltersItemValues() + { + var firstItem = CreateMovieEntity(Guid.NewGuid(), "First"); + var secondItem = CreateMovieEntity(Guid.NewGuid(), "Second"); + var excludedItem = new BaseItemEntity + { + Id = Guid.NewGuid(), + Type = _audioTypeName, + Name = "Excluded Audio", + MediaType = "Audio", + IsMovie = false, + IsFolder = false, + IsVirtualItem = false + }; + var firstTag = new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = ItemValueType.Tags, + Value = "Alpha", + CleanValue = "alpha" + }; + var duplicateTag = new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = ItemValueType.Tags, + Value = "alpha", + CleanValue = "alpha" + }; + var secondTag = new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = ItemValueType.Tags, + Value = "Beta", + CleanValue = "beta" + }; + var genre = new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = ItemValueType.Genre, + Value = "Genre Leak", + CleanValue = "genre leak" + }; + var excludedTag = new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = ItemValueType.Tags, + Value = "Excluded Tag", + CleanValue = "excluded tag" + }; + var excludedGenre = new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = ItemValueType.Genre, + Value = "Excluded Genre", + CleanValue = "excluded genre" + }; + + using (var context = CreateDbContext()) + { + context.BaseItems.AddRange(firstItem, secondItem, excludedItem); + context.ItemValues.AddRange(firstTag, duplicateTag, secondTag, genre, excludedTag, excludedGenre); + context.ItemValuesMap.AddRange( + CreateMap(firstItem, firstTag), + CreateMap(firstItem, duplicateTag), + CreateMap(secondItem, secondTag), + CreateMap(firstItem, genre), + CreateMap(excludedItem, excludedTag), + CreateMap(excludedItem, excludedGenre)); + context.SaveChanges(); + } + + var result = _repository.GetQueryFiltersLegacy(new InternalItemsQuery(new Database.Implementations.Entities.User("test", "auth", "reset")) + { + IncludeItemTypes = [BaseItemKind.Movie] + }); + + Assert.Equal(["Alpha", "Beta"], result.Tags); + Assert.Equal(["Genre Leak"], result.Genres); + } + + [Fact] + public void GetGenreNames_GroupsAndFiltersMappedItemValues() + { + var movie = CreateMovieEntity(Guid.NewGuid(), "Movie"); + var audio = new BaseItemEntity + { + Id = Guid.NewGuid(), + Type = _audioTypeName, + Name = "Audio", + MediaType = "Audio", + IsFolder = false, + IsVirtualItem = false + }; + var movieGenre = CreateItemValue(ItemValueType.Genre, "Movie Genre", "movie genre"); + var duplicateMovieGenre = CreateItemValue(ItemValueType.Genre, "movie genre", "movie genre"); + var musicGenre = CreateItemValue(ItemValueType.Genre, "Music Genre", "music genre"); + var orphanedGenre = CreateItemValue(ItemValueType.Genre, "Orphaned Genre", "orphaned genre"); + + using (var context = CreateDbContext()) + { + context.BaseItems.AddRange(movie, audio); + context.ItemValues.AddRange(movieGenre, duplicateMovieGenre, musicGenre, orphanedGenre); + context.ItemValuesMap.AddRange( + CreateMap(movie, movieGenre), + CreateMap(movie, duplicateMovieGenre), + CreateMap(audio, musicGenre)); + context.SaveChanges(); + } + + Assert.Equal(["Movie Genre"], _repository.GetGenreNames()); + Assert.Equal(["Music Genre"], _repository.GetMusicGenreNames()); + } + + private BaseItemEntity CreateMovieEntity(Guid id, string name) + { + return new BaseItemEntity + { + Id = id, + Type = _movieTypeName, + Name = name, + MediaType = "Video", + IsMovie = true, + IsFolder = false, + IsVirtualItem = false + }; + } + + private static ItemValueMap CreateMap(BaseItemEntity item, ItemValue itemValue) + { + return new ItemValueMap + { + ItemId = item.Id, + ItemValueId = itemValue.ItemValueId, + Item = item, + ItemValue = itemValue + }; + } + + private static ItemValue CreateItemValue(ItemValueType type, string value, string cleanValue) + { + return new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = type, + Value = value, + CleanValue = cleanValue + }; + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs deleted file mode 100644 index 12b85cb021..0000000000 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using Emby.Server.Implementations.Data; -using Jellyfin.Database.Implementations.Entities; -using Jellyfin.Server.Implementations.Item; -using MediaBrowser.Controller.Entities; -using Xunit; -using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind; - -namespace Jellyfin.Server.Implementations.Tests.Item; - -public sealed class BaseItemRepositoryLegacyFilterTests : SqliteDbTestFixture -{ - private readonly BaseItemRepository _repository; - private readonly string _audioTypeName; - private readonly string _movieTypeName; - - public BaseItemRepositoryLegacyFilterTests() - { - var itemTypeLookup = new ItemTypeLookup(); - _audioTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; - _movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; - _repository = CreateBaseItemRepository(itemTypeLookup); - } - - [Fact] - public void GetQueryFiltersLegacy_GroupsAndFiltersItemValues() - { - var firstItem = CreateMovieEntity(Guid.NewGuid(), "First"); - var secondItem = CreateMovieEntity(Guid.NewGuid(), "Second"); - var excludedItem = new BaseItemEntity - { - Id = Guid.NewGuid(), - Type = _audioTypeName, - Name = "Excluded Audio", - MediaType = "Audio", - IsMovie = false, - IsFolder = false, - IsVirtualItem = false - }; - var firstTag = new ItemValue - { - ItemValueId = Guid.NewGuid(), - Type = ItemValueType.Tags, - Value = "Alpha", - CleanValue = "alpha" - }; - var duplicateTag = new ItemValue - { - ItemValueId = Guid.NewGuid(), - Type = ItemValueType.Tags, - Value = "alpha", - CleanValue = "alpha" - }; - var secondTag = new ItemValue - { - ItemValueId = Guid.NewGuid(), - Type = ItemValueType.Tags, - Value = "Beta", - CleanValue = "beta" - }; - var genre = new ItemValue - { - ItemValueId = Guid.NewGuid(), - Type = ItemValueType.Genre, - Value = "Genre Leak", - CleanValue = "genre leak" - }; - var excludedTag = new ItemValue - { - ItemValueId = Guid.NewGuid(), - Type = ItemValueType.Tags, - Value = "Excluded Tag", - CleanValue = "excluded tag" - }; - var excludedGenre = new ItemValue - { - ItemValueId = Guid.NewGuid(), - Type = ItemValueType.Genre, - Value = "Excluded Genre", - CleanValue = "excluded genre" - }; - - using (var context = CreateDbContext()) - { - context.BaseItems.AddRange(firstItem, secondItem, excludedItem); - context.ItemValues.AddRange(firstTag, duplicateTag, secondTag, genre, excludedTag, excludedGenre); - context.ItemValuesMap.AddRange( - CreateMap(firstItem, firstTag), - CreateMap(firstItem, duplicateTag), - CreateMap(secondItem, secondTag), - CreateMap(firstItem, genre), - CreateMap(excludedItem, excludedTag), - CreateMap(excludedItem, excludedGenre)); - context.SaveChanges(); - } - - var result = _repository.GetQueryFiltersLegacy(new InternalItemsQuery(new Database.Implementations.Entities.User("test", "auth", "reset")) - { - IncludeItemTypes = [BaseItemKind.Movie] - }); - - Assert.Equal(["Alpha", "Beta"], result.Tags); - Assert.Equal(["Genre Leak"], result.Genres); - } - - private BaseItemEntity CreateMovieEntity(Guid id, string name) - { - return new BaseItemEntity - { - Id = id, - Type = _movieTypeName, - Name = name, - MediaType = "Video", - IsMovie = true, - IsFolder = false, - IsVirtualItem = false - }; - } - - private static ItemValueMap CreateMap(BaseItemEntity item, ItemValue itemValue) - { - return new ItemValueMap - { - ItemId = item.Id, - ItemValueId = itemValue.ItemValueId, - Item = item, - ItemValue = itemValue - }; - } -} -- cgit v1.2.3