| | | 1 | | using System.Buffers; |
| | | 2 | | using NGql.Core.Abstractions; |
| | | 3 | | using NGql.Core.Pooling; |
| | | 4 | | |
| | | 5 | | namespace NGql.Core.Features; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Utility class for generating unique keys. |
| | | 9 | | /// </summary> |
| | | 10 | | internal static class KeyGenerator |
| | | 11 | | { |
| | | 12 | | /// <summary> |
| | | 13 | | /// Generates a unique key using a <see cref="FieldMergeIndex"/>'s live key set and |
| | | 14 | | /// per-base-name suffix counter instead of rebuilding a <see cref="HashSet{T}"/> from every |
| | | 15 | | /// existing key: O(1) amortized per inserted field across a chain of <c>Include()</c> calls. |
| | | 16 | | /// </summary> |
| | | 17 | | internal static string GenerateUniqueKey(FieldMergeIndex mergeIndex, Dictionary<string, FieldDefinition> fields, str |
| | 918 | 18 | | => mergeIndex.NextUniqueKey(fields, baseKey); |
| | | 19 | | |
| | | 20 | | /// <summary> |
| | | 21 | | /// Generates a unique key from field definitions' effective names (zero-alloc for span iteration). |
| | | 22 | | /// </summary> |
| | | 23 | | internal static string GenerateUniqueKey(string baseKey, ReadOnlySpan<FieldDefinition> fields) |
| | | 24 | | { |
| | 33 | 25 | | using var pooledSet = LockFreeHashSetPool.GetPooled(); |
| | 33 | 26 | | var existingKeySet = pooledSet.Set; |
| | | 27 | | |
| | | 28 | | // Populate set with effective names from span — zero-alloc iteration |
| | 192 | 29 | | for (int i = 0; i < fields.Length; i++) |
| | 63 | 30 | | existingKeySet.Add(fields[i]._effectiveName); |
| | | 31 | | |
| | 33 | 32 | | if (!existingKeySet.Contains(baseKey)) |
| | | 33 | | { |
| | 6 | 34 | | return baseKey; |
| | | 35 | | } |
| | | 36 | | |
| | 27 | 37 | | return GenerateUniqueKeyCore(baseKey, existingKeySet); |
| | 33 | 38 | | } |
| | | 39 | | |
| | | 40 | | private static string GenerateUniqueKeyCore(string baseKey, HashSet<string> existingKeySet) |
| | | 41 | | { |
| | | 42 | | // 16 chars holds "_" plus a 15-digit counter — counter is int, max ~10 digits. |
| | 27 | 43 | | var length = checked(baseKey.Length + 16); |
| | 27 | 44 | | char[]? rented = length > 512 ? ArrayPool<char>.Shared.Rent(length) : null; |
| | 27 | 45 | | Span<char> buffer = rented is null ? stackalloc char[length] : rented; |
| | | 46 | | try |
| | | 47 | | { |
| | 27 | 48 | | baseKey.AsSpan().CopyTo(buffer); |
| | 27 | 49 | | buffer[baseKey.Length] = '_'; |
| | | 50 | | #if NET9_0_OR_GREATER |
| | 18 | 51 | | var lookup = existingKeySet.GetAlternateLookup<ReadOnlySpan<char>>(); |
| | | 52 | | #endif |
| | | 53 | | #pragma warning disable S1994 |
| | 51 | 54 | | for (int counter = 1; ; counter++) |
| | | 55 | | #pragma warning restore S1994 |
| | | 56 | | { |
| | 51 | 57 | | counter.TryFormat(buffer[(baseKey.Length + 1)..], out var charsWritten); |
| | 51 | 58 | | var candidate = buffer[..(baseKey.Length + 1 + charsWritten)]; |
| | | 59 | | #if NET9_0_OR_GREATER |
| | 52 | 60 | | if (!lookup.Contains(candidate)) return new string(candidate); |
| | | 61 | | #else |
| | 17 | 62 | | var uniqueKey = new string(candidate); |
| | 26 | 63 | | if (!existingKeySet.Contains(uniqueKey)) return uniqueKey; |
| | | 64 | | #endif |
| | | 65 | | } |
| | | 66 | | } |
| | | 67 | | finally |
| | | 68 | | { |
| | 33 | 69 | | if (rented is not null) ArrayPool<char>.Shared.Return(rented, clearArray: true); |
| | 27 | 70 | | } |
| | 27 | 71 | | } |
| | | 72 | | } |