JellyfinQueryHelperExtensions.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. #pragma warning disable RS0030 // Do not use banned APIs
  2. using System;
  3. using System.Collections.Concurrent;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Linq.Expressions;
  7. using System.Reflection;
  8. using Jellyfin.Database.Implementations.Entities;
  9. using Microsoft.EntityFrameworkCore;
  10. namespace Jellyfin.Database.Implementations;
  11. /// <summary>
  12. /// Contains a number of query related extensions.
  13. /// </summary>
  14. public static class JellyfinQueryHelperExtensions
  15. {
  16. private static readonly MethodInfo _containsMethodGenericCache = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static).First(m => m.Name == nameof(Enumerable.Contains) && m.GetParameters().Length == 2);
  17. private static readonly MethodInfo _efParameterInstruction = typeof(EF).GetMethod(nameof(EF.Parameter), BindingFlags.Public | BindingFlags.Static)!;
  18. private static readonly ConcurrentDictionary<Type, MethodInfo> _containsQueryCache = new();
  19. /// <summary>
  20. /// Builds an optimised query checking one property against a list of values while maintaining an optimal query.
  21. /// </summary>
  22. /// <typeparam name="TEntity">The entity.</typeparam>
  23. /// <typeparam name="TProperty">The property type to compare.</typeparam>
  24. /// <param name="query">The source query.</param>
  25. /// <param name="oneOf">The list of items to check.</param>
  26. /// <param name="property">Property expression.</param>
  27. /// <returns>A Query.</returns>
  28. public static IQueryable<TEntity> WhereOneOrMany<TEntity, TProperty>(this IQueryable<TEntity> query, IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
  29. {
  30. return query.Where(OneOrManyExpressionBuilder(oneOf, property));
  31. }
  32. /// <summary>
  33. /// Builds a query that checks referenced ItemValues for a cross BaseItem lookup.
  34. /// </summary>
  35. /// <param name="baseQuery">The source query.</param>
  36. /// <param name="context">The database context.</param>
  37. /// <param name="itemValueType">The type of item value to reference.</param>
  38. /// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
  39. /// <param name="invert">If set an exclusion check is performed instead.</param>
  40. /// <returns>A Query.</returns>
  41. public static IQueryable<BaseItemEntity> WhereReferencedItem(
  42. this IQueryable<BaseItemEntity> baseQuery,
  43. JellyfinDbContext context,
  44. ItemValueType itemValueType,
  45. IList<Guid> referenceIds,
  46. bool invert = false)
  47. {
  48. return baseQuery.Where(ReferencedItemFilterExpressionBuilder(context, itemValueType, referenceIds, invert));
  49. }
  50. /// <summary>
  51. /// Builds a query expression that checks referenced ItemValues for a cross BaseItem lookup.
  52. /// </summary>
  53. /// <param name="context">The database context.</param>
  54. /// <param name="itemValueType">The type of item value to reference.</param>
  55. /// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
  56. /// <param name="invert">If set an exclusion check is performed instead.</param>
  57. /// <returns>A Query.</returns>
  58. public static Expression<Func<BaseItemEntity, bool>> ReferencedItemFilterExpressionBuilder(
  59. this JellyfinDbContext context,
  60. ItemValueType itemValueType,
  61. IList<Guid> referenceIds,
  62. bool invert = false)
  63. {
  64. // Well genre/artist/album etc items do not actually set the ItemValue of thier specitic types so we cannot match it that way.
  65. /*
  66. "(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreIds and Type=2)))"
  67. */
  68. var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
  69. return item =>
  70. context.ItemValues
  71. .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (item, map) => new { item, map })
  72. .Any(val =>
  73. val.item.Type == itemValueType
  74. && context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.item.CleanValue)
  75. && val.map.ItemId == item.Id) == EF.Constant(!invert);
  76. }
  77. /// <summary>
  78. /// Builds an optimised query expression checking one property against a list of values while maintaining an optimal query.
  79. /// </summary>
  80. /// <typeparam name="TEntity">The entity.</typeparam>
  81. /// <typeparam name="TProperty">The property type to compare.</typeparam>
  82. /// <param name="oneOf">The list of items to check.</param>
  83. /// <param name="property">Property expression.</param>
  84. /// <returns>A Query.</returns>
  85. public static Expression<Func<TEntity, bool>> OneOrManyExpressionBuilder<TEntity, TProperty>(this IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
  86. {
  87. var parameter = Expression.Parameter(typeof(TEntity), "item");
  88. property = ParameterReplacer.Replace<Func<TEntity, TProperty>, Func<TEntity, TProperty>>(property, property.Parameters[0], parameter);
  89. if (oneOf.Count == 1)
  90. {
  91. var value = oneOf[0];
  92. if (typeof(TProperty).IsValueType)
  93. {
  94. return Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(property.Body, Expression.Constant(value)), parameter);
  95. }
  96. else
  97. {
  98. return Expression.Lambda<Func<TEntity, bool>>(Expression.ReferenceEqual(property.Body, Expression.Constant(value)), parameter);
  99. }
  100. }
  101. var containsMethodInfo = _containsQueryCache.GetOrAdd(typeof(TProperty), static (key) => _containsMethodGenericCache.MakeGenericMethod(key));
  102. if (oneOf.Count < 4) // arbitrary value choosen.
  103. {
  104. // if we have 3 or fewer values to check against its faster to do a IN(const,const,const) lookup
  105. return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Constant(oneOf), property.Body), parameter);
  106. }
  107. return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Call(null, _efParameterInstruction.MakeGenericMethod(oneOf.GetType()), Expression.Constant(oneOf)), property.Body), parameter);
  108. }
  109. internal static class ParameterReplacer
  110. {
  111. // Produces an expression identical to 'expression'
  112. // except with 'source' parameter replaced with 'target' expression.
  113. internal static Expression<TOutput> Replace<TInput, TOutput>(
  114. Expression<TInput> expression,
  115. ParameterExpression source,
  116. ParameterExpression target)
  117. {
  118. return new ParameterReplacerVisitor<TOutput>(source, target)
  119. .VisitAndConvert(expression);
  120. }
  121. private sealed class ParameterReplacerVisitor<TOutput> : ExpressionVisitor
  122. {
  123. private readonly ParameterExpression _source;
  124. private readonly ParameterExpression _target;
  125. public ParameterReplacerVisitor(ParameterExpression source, ParameterExpression target)
  126. {
  127. _source = source;
  128. _target = target;
  129. }
  130. internal Expression<TOutput> VisitAndConvert<T>(Expression<T> root)
  131. {
  132. return (Expression<TOutput>)VisitLambda(root);
  133. }
  134. protected override Expression VisitLambda<T>(Expression<T> node)
  135. {
  136. // Leave all parameters alone except the one we want to replace.
  137. var parameters = node.Parameters.Select(p => p == _source ? _target : p);
  138. return Expression.Lambda<TOutput>(Visit(node.Body), parameters);
  139. }
  140. protected override Expression VisitParameter(ParameterExpression node)
  141. {
  142. // Replace the source with the target, visit other params as usual.
  143. return node == _source ? _target : base.VisitParameter(node);
  144. }
  145. }
  146. }
  147. }