From 145258865adb5904c9598d1e3e84412204f95b02 Mon Sep 17 00:00:00 2001 From: Felix Förtsch Date: Mon, 3 Aug 2026 10:58:30 +0200 Subject: avoid correlated legacy item value filter queries The legacy filter queries projected ItemValue before grouping tags and genres. EF Core translated that shape into duplicated correlated aggregates, causing multi-minute requests for libraries with many distinct values and blocking unrelated SQLite-backed API calls. Join ItemValuesMap directly to ItemValues before grouping. This preserves type and item filtering, clean-value grouping, minimum-value selection and ordering while producing one aggregate query. Add an in-memory SQLite regression test for result semantics and both SQL shapes. --- .../Item/BaseItemRepository.Querying.cs | 20 +- .../Item/BaseItemRepositoryLegacyFilterTests.cs | 227 +++++++++++++++++++++ 2 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs index c9e08b1b5d..1ed10cce2b 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs @@ -626,18 +626,26 @@ public sealed partial class BaseItemRepository .ToArray(); var tags = context.ItemValuesMap - .Where(ivm => ivm.ItemValue.Type == ItemValueType.Tags) - .Where(ivm => matchingItemIds.Contains(ivm.ItemId)) - .Select(ivm => ivm.ItemValue) + .Join( + context.ItemValues, + ivm => ivm.ItemValueId, + iv => iv.ItemValueId, + (ivm, iv) => new { ivm.ItemId, iv.Type, iv.CleanValue, iv.Value }) + .Where(iv => iv.Type == ItemValueType.Tags) + .Where(iv => matchingItemIds.Contains(iv.ItemId)) .GroupBy(iv => iv.CleanValue) .Select(g => g.Min(iv => iv.Value)) .OrderBy(t => t) .ToArray(); var genres = context.ItemValuesMap - .Where(ivm => ivm.ItemValue.Type == ItemValueType.Genre) - .Where(ivm => matchingItemIds.Contains(ivm.ItemId)) - .Select(ivm => ivm.ItemValue) + .Join( + context.ItemValues, + ivm => ivm.ItemValueId, + iv => iv.ItemValueId, + (ivm, iv) => new { ivm.ItemId, iv.Type, iv.CleanValue, iv.Value }) + .Where(iv => iv.Type == ItemValueType.Genre) + .Where(iv => matchingItemIds.Contains(iv.ItemId)) .GroupBy(iv => iv.CleanValue) .Select(g => g.Min(iv => iv.Value)) .OrderBy(g => g) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs new file mode 100644 index 0000000000..9ecd6d5458 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using Emby.Server.Implementations.Data; +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.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; +using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind; + +namespace Jellyfin.Server.Implementations.Tests.Item; + +public sealed class BaseItemRepositoryLegacyFilterTests : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly DbContextOptions _dbOptions; + private readonly CommandRecordingInterceptor _interceptor = new(); + private readonly BaseItemRepository _repository; + private readonly string _audioTypeName; + private readonly string _movieTypeName; + + public BaseItemRepositoryLegacyFilterTests() + { + _connection = new SqliteConnection("Data Source=:memory:"); + _connection.Open(); + + _dbOptions = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .AddInterceptors(_interceptor) + .Options; + + using (var context = CreateDbContext()) + { + context.Database.EnsureCreated(); + } + + var factory = new Mock>(); + factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext); + + var itemTypeLookup = new ItemTypeLookup(); + _audioTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; + _movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; + + var serverConfigurationManager = new Mock(); + serverConfigurationManager.Setup(c => c.Configuration).Returns(new ServerConfiguration()); + + _repository = new BaseItemRepository( + factory.Object, + new Mock().Object, + itemTypeLookup, + serverConfigurationManager.Object, + NullLogger.Instance); + } + + public void Dispose() + { + _connection.Dispose(); + } + + // Verifies eligible tags and genres retain normalized result semantics with one direct ItemValues join. + [Fact] + public void GetQueryFiltersLegacy_ItemValues_GroupByCleanValueWithOneItemValuesJoin() + { + 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); + var tagsCommand = Assert.Single( + _interceptor.Commands, + command => command.Contains("GROUP BY", StringComparison.Ordinal) + && command.Contains("\"Type\" = 4", StringComparison.Ordinal)); + var genresCommand = Assert.Single( + _interceptor.Commands, + command => command.Contains("GROUP BY", StringComparison.Ordinal) + && command.Contains("\"Type\" = 2", StringComparison.Ordinal)); + Assert.Equal(1, CountOccurrences(tagsCommand, "INNER JOIN \"ItemValues\"")); + Assert.DoesNotContain("SELECT (", tagsCommand, StringComparison.Ordinal); + Assert.Equal(1, CountOccurrences(genresCommand, "INNER JOIN \"ItemValues\"")); + Assert.DoesNotContain("SELECT (", genresCommand, StringComparison.Ordinal); + } + + private static int CountOccurrences(string value, string pattern) + { + var count = 0; + var index = 0; + while ((index = value.IndexOf(pattern, index, StringComparison.Ordinal)) >= 0) + { + count++; + index += pattern.Length; + } + + return count; + } + + 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 JellyfinDbContext CreateDbContext() + { + return new JellyfinDbContext( + _dbOptions, + NullLogger.Instance, + new SqliteDatabaseProvider(null!, NullLogger.Instance), + new NoLockBehavior(NullLogger.Instance)); + } + + private sealed class CommandRecordingInterceptor : DbCommandInterceptor + { + public List Commands { get; } = []; + + public override InterceptionResult ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult result) + { + Commands.Add(command.CommandText); + return result; + } + } +} -- cgit v1.2.3 From 3df58ab775eeff53c8476d5ae833f701e2ae1bb9 Mon Sep 17 00:00:00 2001 From: Felix Förtsch Date: Wed, 2 Sep 2026 11:05:36 +0200 Subject: Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Item/BaseItemRepositoryLegacyFilterTests.cs | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs index 9ecd6d5458..c578714749 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs @@ -150,18 +150,18 @@ public sealed class BaseItemRepositoryLegacyFilterTests : IDisposable Assert.Equal(["Alpha", "Beta"], result.Tags); Assert.Equal(["Genre Leak"], result.Genres); - var tagsCommand = Assert.Single( - _interceptor.Commands, - command => command.Contains("GROUP BY", StringComparison.Ordinal) - && command.Contains("\"Type\" = 4", StringComparison.Ordinal)); - var genresCommand = Assert.Single( - _interceptor.Commands, - command => command.Contains("GROUP BY", StringComparison.Ordinal) - && command.Contains("\"Type\" = 2", StringComparison.Ordinal)); - Assert.Equal(1, CountOccurrences(tagsCommand, "INNER JOIN \"ItemValues\"")); - Assert.DoesNotContain("SELECT (", tagsCommand, StringComparison.Ordinal); - Assert.Equal(1, CountOccurrences(genresCommand, "INNER JOIN \"ItemValues\"")); - Assert.DoesNotContain("SELECT (", genresCommand, StringComparison.Ordinal); + var itemValueCommands = _interceptor.Commands + .Where(command => command.Contains("GROUP BY", StringComparison.Ordinal) + && command.Contains("INNER JOIN \"ItemValues\"", StringComparison.Ordinal)) + .ToArray(); + + Assert.Equal(2, itemValueCommands.Length); + + foreach (var command in itemValueCommands) + { + Assert.Equal(1, CountOccurrences(command, "INNER JOIN \"ItemValues\"")); + Assert.DoesNotContain("SELECT (SELECT", command, StringComparison.Ordinal); + } } private static int CountOccurrences(string value, string pattern) -- cgit v1.2.3 From 5adafb446fc019682091216948e84139075f9614 Mon Sep 17 00:00:00 2001 From: Felix Förtsch Date: Wed, 2 Sep 2026 11:29:24 +0200 Subject: test legacy item value filter semantics --- .../Item/BaseItemRepositoryLegacyFilterTests.cs | 103 +-------------------- 1 file changed, 3 insertions(+), 100 deletions(-) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs index c578714749..12b85cb021 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryLegacyFilterTests.cs @@ -1,78 +1,29 @@ using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; using Emby.Server.Implementations.Data; -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.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; using Xunit; using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind; namespace Jellyfin.Server.Implementations.Tests.Item; -public sealed class BaseItemRepositoryLegacyFilterTests : IDisposable +public sealed class BaseItemRepositoryLegacyFilterTests : SqliteDbTestFixture { - private readonly SqliteConnection _connection; - private readonly DbContextOptions _dbOptions; - private readonly CommandRecordingInterceptor _interceptor = new(); private readonly BaseItemRepository _repository; private readonly string _audioTypeName; private readonly string _movieTypeName; public BaseItemRepositoryLegacyFilterTests() { - _connection = new SqliteConnection("Data Source=:memory:"); - _connection.Open(); - - _dbOptions = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .AddInterceptors(_interceptor) - .Options; - - using (var context = CreateDbContext()) - { - context.Database.EnsureCreated(); - } - - var factory = new Mock>(); - factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext); - var itemTypeLookup = new ItemTypeLookup(); _audioTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; _movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; - - var serverConfigurationManager = new Mock(); - serverConfigurationManager.Setup(c => c.Configuration).Returns(new ServerConfiguration()); - - _repository = new BaseItemRepository( - factory.Object, - new Mock().Object, - itemTypeLookup, - serverConfigurationManager.Object, - NullLogger.Instance); - } - - public void Dispose() - { - _connection.Dispose(); + _repository = CreateBaseItemRepository(itemTypeLookup); } - // Verifies eligible tags and genres retain normalized result semantics with one direct ItemValues join. [Fact] - public void GetQueryFiltersLegacy_ItemValues_GroupByCleanValueWithOneItemValuesJoin() + public void GetQueryFiltersLegacy_GroupsAndFiltersItemValues() { var firstItem = CreateMovieEntity(Guid.NewGuid(), "First"); var secondItem = CreateMovieEntity(Guid.NewGuid(), "Second"); @@ -150,31 +101,6 @@ public sealed class BaseItemRepositoryLegacyFilterTests : IDisposable Assert.Equal(["Alpha", "Beta"], result.Tags); Assert.Equal(["Genre Leak"], result.Genres); - var itemValueCommands = _interceptor.Commands - .Where(command => command.Contains("GROUP BY", StringComparison.Ordinal) - && command.Contains("INNER JOIN \"ItemValues\"", StringComparison.Ordinal)) - .ToArray(); - - Assert.Equal(2, itemValueCommands.Length); - - foreach (var command in itemValueCommands) - { - Assert.Equal(1, CountOccurrences(command, "INNER JOIN \"ItemValues\"")); - Assert.DoesNotContain("SELECT (SELECT", command, StringComparison.Ordinal); - } - } - - private static int CountOccurrences(string value, string pattern) - { - var count = 0; - var index = 0; - while ((index = value.IndexOf(pattern, index, StringComparison.Ordinal)) >= 0) - { - count++; - index += pattern.Length; - } - - return count; } private BaseItemEntity CreateMovieEntity(Guid id, string name) @@ -201,27 +127,4 @@ public sealed class BaseItemRepositoryLegacyFilterTests : IDisposable ItemValue = itemValue }; } - - private JellyfinDbContext CreateDbContext() - { - return new JellyfinDbContext( - _dbOptions, - NullLogger.Instance, - new SqliteDatabaseProvider(null!, NullLogger.Instance), - new NoLockBehavior(NullLogger.Instance)); - } - - private sealed class CommandRecordingInterceptor : DbCommandInterceptor - { - public List Commands { get; } = []; - - public override InterceptionResult ReaderExecuting( - DbCommand command, - CommandEventData eventData, - InterceptionResult result) - { - Commands.Add(command.CommandText); - return result; - } - } } -- cgit v1.2.3 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