|   |  | 1 |  | using System.Buffers; | 
|   |  | 2 |  |  | 
|   |  | 3 |  | namespace Pozitron.QuerySpecification; | 
|   |  | 4 |  |  | 
|   |  | 5 |  | /* | 
|   |  | 6 |  |     public IQueryable<T> Evaluate<T>(IQueryable<T> source, Specification<T> specification) where T : class | 
|   |  | 7 |  |     { | 
|   |  | 8 |  |         foreach (var likeGroup in specification.LikeExpressions.GroupBy(x => x.Group)) | 
|   |  | 9 |  |         { | 
|   |  | 10 |  |             source = source.Like(likeGroup); | 
|   |  | 11 |  |         } | 
|   |  | 12 |  |         return source; | 
|   |  | 13 |  |     } | 
|   |  | 14 |  |     This was the previous implementation. We're trying to avoid allocations of LikeExpressions and GroupBy. | 
|   |  | 15 |  |     The new implementation preserves the behavior and has zero allocations. | 
|   |  | 16 |  |     Instead of GroupBy, we have a single array, sorted by group, and we slice it to get the groups. | 
|   |  | 17 |  | */ | 
|   |  | 18 |  |  | 
|   |  | 19 |  | /// <summary> | 
|   |  | 20 |  | /// Evaluates a specification to apply "like" expressions for filtering. | 
|   |  | 21 |  | /// </summary> | 
|   |  | 22 |  | [EvaluatorDiscovery(Order = 20)] | 
|   |  | 23 |  | public sealed class LikeEvaluator : IEvaluator | 
|   |  | 24 |  | { | 
|   | 4 | 25 |  |     private LikeEvaluator() { } | 
|   |  | 26 |  |  | 
|   |  | 27 |  |     /// <summary> | 
|   |  | 28 |  |     /// Gets the singleton instance of the <see cref="LikeEvaluator"/> class. | 
|   |  | 29 |  |     /// </summary> | 
|   | 2 | 30 |  |     public static LikeEvaluator Instance = new(); | 
|   |  | 31 |  |  | 
|   |  | 32 |  |     /// <inheritdoc/> | 
|   |  | 33 |  |     public IQueryable<T> Evaluate<T>(IQueryable<T> source, Specification<T> specification) where T : class | 
|   |  | 34 |  |     { | 
|   | 53 | 35 |  |         var likeCount = GetLikeCount(specification); | 
|   | 95 | 36 |  |         if (likeCount == 0) return source; | 
|   |  | 37 |  |  | 
|   | 11 | 38 |  |         if (likeCount == 1) | 
|   |  | 39 |  |         { | 
|   |  | 40 |  |             // Specs with a single Like are the most common. We can optimize for this case to avoid all the additional o | 
|   | 2 | 41 |  |             var items = specification.Items; | 
|   | 6 | 42 |  |             for (var i = 0; i < items.Length; i++) | 
|   |  | 43 |  |             { | 
|   | 3 | 44 |  |                 if (items[i].Type == ItemType.Like) | 
|   |  | 45 |  |                 { | 
|   | 2 | 46 |  |                     return source.ApplyLikesAsOrGroup(items.Slice(i, 1)); | 
|   |  | 47 |  |                 } | 
|   |  | 48 |  |             } | 
|   |  | 49 |  |         } | 
|   |  | 50 |  |  | 
|   | 9 | 51 |  |         SpecItem[] array = ArrayPool<SpecItem>.Shared.Rent(likeCount); | 
|   |  | 52 |  |  | 
|   |  | 53 |  |         try | 
|   |  | 54 |  |         { | 
|   | 9 | 55 |  |             var span = array.AsSpan()[..likeCount]; | 
|   | 9 | 56 |  |             FillSorted(specification, span); | 
|   | 9 | 57 |  |             source = ApplyLike(source, span); | 
|   | 9 | 58 |  |         } | 
|   |  | 59 |  |         finally | 
|   |  | 60 |  |         { | 
|   | 9 | 61 |  |             ArrayPool<SpecItem>.Shared.Return(array); | 
|   | 9 | 62 |  |         } | 
|   |  | 63 |  |  | 
|   | 9 | 64 |  |         return source; | 
|   |  | 65 |  |     } | 
|   |  | 66 |  |  | 
|   |  | 67 |  |     private static IQueryable<T> ApplyLike<T>(IQueryable<T> source, ReadOnlySpan<SpecItem> span) where T : class | 
|   |  | 68 |  |     { | 
|   | 9 | 69 |  |         var groupStart = 0; | 
|   | 74 | 70 |  |         for (var i = 1; i <= span.Length; i++) | 
|   |  | 71 |  |         { | 
|   |  | 72 |  |             // If we reached the end of the span or the group has changed, we slice and process the group. | 
|   | 28 | 73 |  |             if (i == span.Length || span[i].Bag != span[groupStart].Bag) | 
|   |  | 74 |  |             { | 
|   | 19 | 75 |  |                 source = source.ApplyLikesAsOrGroup(span[groupStart..i]); | 
|   | 19 | 76 |  |                 groupStart = i; | 
|   |  | 77 |  |             } | 
|   |  | 78 |  |         } | 
|   | 9 | 79 |  |         return source; | 
|   |  | 80 |  |     } | 
|   |  | 81 |  |  | 
|   |  | 82 |  |     private static int GetLikeCount<T>(Specification<T> specification) | 
|   |  | 83 |  |     { | 
|   | 53 | 84 |  |         var count = 0; | 
|   | 590 | 85 |  |         foreach (var item in specification.Items) | 
|   |  | 86 |  |         { | 
|   | 242 | 87 |  |             if (item.Type == ItemType.Like) | 
|   | 30 | 88 |  |                 count++; | 
|   |  | 89 |  |         } | 
|   | 53 | 90 |  |         return count; | 
|   |  | 91 |  |     } | 
|   |  | 92 |  |  | 
|   |  | 93 |  |     private static void FillSorted<T>(Specification<T> specification, Span<SpecItem> span) | 
|   |  | 94 |  |     { | 
|   | 9 | 95 |  |         var i = 0; | 
|   | 278 | 96 |  |         foreach (var item in specification.Items) | 
|   |  | 97 |  |         { | 
|   | 130 | 98 |  |             if (item.Type == ItemType.Like) | 
|   |  | 99 |  |             { | 
|   |  | 100 |  |                 // Find the correct insertion point | 
|   | 28 | 101 |  |                 var j = i; | 
|   | 29 | 102 |  |                 while (j > 0 && span[j - 1].Bag > item.Bag) | 
|   |  | 103 |  |                 { | 
|   | 1 | 104 |  |                     span[j] = span[j - 1]; | 
|   | 1 | 105 |  |                     j--; | 
|   |  | 106 |  |                 } | 
|   |  | 107 |  |  | 
|   |  | 108 |  |                 // Insert the current item in the sorted position | 
|   | 28 | 109 |  |                 span[j] = item; | 
|   | 28 | 110 |  |                 i++; | 
|   |  | 111 |  |             } | 
|   |  | 112 |  |         } | 
|   | 9 | 113 |  |     } | 
|   |  | 114 |  | } |