| | | 1 | | using NGql.Core.Abstractions; |
| | | 2 | | using NGql.Core.Pooling; |
| | | 3 | | |
| | | 4 | | namespace NGql.Core.Features; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Utility class for generating unique keys. |
| | | 8 | | /// </summary> |
| | | 9 | | internal static class KeyGenerator |
| | | 10 | | { |
| | | 11 | | /// <summary> |
| | | 12 | | /// Generates a unique key by appending a counter suffix if the base key already exists. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <param name="baseKey">The base key to make unique</param> |
| | | 15 | | /// <param name="existingKeys">Collection of existing keys to check against</param> |
| | | 16 | | /// <returns>A unique key that doesn't exist in the collection</returns> |
| | | 17 | | internal static string GenerateUniqueKey(string baseKey, IEnumerable<string> existingKeys) |
| | | 18 | | { |
| | 165 | 19 | | using var pooledSet = LockFreeHashSetPool.GetPooled(existingKeys); |
| | 165 | 20 | | var existingKeySet = pooledSet.Set; |
| | | 21 | | |
| | 165 | 22 | | if (!existingKeySet.Contains(baseKey)) |
| | | 23 | | { |
| | 99 | 24 | | return baseKey; |
| | | 25 | | } |
| | | 26 | | |
| | 66 | 27 | | return GenerateUniqueKeyCore(baseKey, existingKeySet); |
| | 165 | 28 | | } |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Generates a unique key from field definitions' effective names (zero-alloc for span iteration). |
| | | 32 | | /// </summary> |
| | | 33 | | internal static string GenerateUniqueKey(string baseKey, ReadOnlySpan<FieldDefinition> fields) |
| | | 34 | | { |
| | 9 | 35 | | using var pooledSet = LockFreeHashSetPool.GetPooled(); |
| | 9 | 36 | | var existingKeySet = pooledSet.Set; |
| | | 37 | | |
| | | 38 | | // Populate set with effective names from span — zero-alloc iteration |
| | 48 | 39 | | for (int i = 0; i < fields.Length; i++) |
| | 15 | 40 | | existingKeySet.Add(fields[i]._effectiveName); |
| | | 41 | | |
| | 9 | 42 | | if (!existingKeySet.Contains(baseKey)) |
| | | 43 | | { |
| | 3 | 44 | | return baseKey; |
| | | 45 | | } |
| | | 46 | | |
| | 6 | 47 | | return GenerateUniqueKeyCore(baseKey, existingKeySet); |
| | 9 | 48 | | } |
| | | 49 | | |
| | | 50 | | private static string GenerateUniqueKeyCore(string baseKey, HashSet<string> existingKeySet) |
| | | 51 | | { |
| | | 52 | | // 16 chars holds "_" plus a 15-digit counter — counter is int, max ~10 digits. |
| | 72 | 53 | | Span<char> buffer = stackalloc char[baseKey.Length + 16]; |
| | 72 | 54 | | baseKey.AsSpan().CopyTo(buffer); |
| | 72 | 55 | | buffer[baseKey.Length] = '_'; |
| | | 56 | | |
| | | 57 | | // Loop terminates as soon as the formatted candidate isn't in the existingKeySet. |
| | | 58 | | // existingKeySet has finite capacity bounded by the number of fields in the merged tree, |
| | | 59 | | // so a not-present key is always reachable; counter is int, more than enough headroom. |
| | | 60 | | #pragma warning disable S1994 |
| | 84 | 61 | | for (int counter = 1; ; counter++) |
| | | 62 | | #pragma warning restore S1994 |
| | | 63 | | { |
| | 84 | 64 | | counter.TryFormat(buffer[(baseKey.Length + 1)..], out var charsWritten); |
| | 84 | 65 | | var uniqueKey = new string(buffer[..(baseKey.Length + 1 + charsWritten)]); |
| | | 66 | | |
| | 84 | 67 | | if (!existingKeySet.Contains(uniqueKey)) |
| | | 68 | | { |
| | 72 | 69 | | return uniqueKey; |
| | | 70 | | } |
| | | 71 | | } |
| | | 72 | | } |
| | | 73 | | } |