< Summary

Information
Class: NGql.Core.Abstractions.FieldChildrenEnumerator
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Abstractions/FieldChildren.cs
Line coverage
100%
Covered lines: 14
Uncovered lines: 0
Coverable lines: 14
Total lines: 329
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Current()100%66100%
System.Collections.IEnumerator.get_Current()100%11100%
MoveNext()100%22100%
Reset()100%11100%
Dispose()100%11100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Abstractions/FieldChildren.cs

#LineLine coverage
 1using System.Collections;
 2using System.Diagnostics.CodeAnalysis;
 3using System.Threading;
 4
 5namespace NGql.Core.Abstractions;
 6
 7/// <summary>
 8/// Array-backed ordered collection of child <see cref="FieldDefinition"/> instances.
 9/// Uses linear scan for small counts and a lazy dictionary index for larger counts.
 10/// Null <see cref="FieldDefinition._children"/> on a leaf node costs nothing — no allocation occurs
 11/// until the first child is added.
 12///
 13/// THREAD-SAFETY: Reads on small (no-index) collections are lock-free — readers do volatile loads
 14/// of <see cref="_count"/> and <see cref="_items"/> and walk a contiguous array. Reads against the
 15/// index, and all writes, take <see cref="_lock"/>. Writers publish updates with explicit volatile
 16/// stores: the new slot is written before <see cref="_count"/> bumps, so any reader observing the
 17/// new count is also guaranteed to see the corresponding initialized slot.
 18///
 19/// The index threshold is intentionally moderate: tiny collections do not need an index, and avoiding
 20/// it on the read fast-path eliminates lock acquisition for the common case (most query nodes have
 21/// fewer than a dozen direct children).
 22/// </summary>
 23internal sealed class FieldChildren : IReadOnlyDictionary<string, FieldDefinition>
 24{
 25    private const int InitialCapacity = 4;
 26    private const int IndexThreshold = 16;
 27
 28    /// <summary>Backing storage. Written only under <see cref="_lock"/>; readers do a single volatile load.</summary>
 29    private FieldDefinition[]? _items;
 30    /// <summary>Number of valid entries in <see cref="_items"/>. Volatile-stored last on writes
 31    /// (release semantics) so readers observing the new value also see the corresponding slot.</summary>
 32    private int _count;
 33    /// <summary>Lazy lookup index, built when <see cref="_count"/> reaches <see cref="IndexThreshold"/>.
 34    /// All access (including reads) is guarded by <see cref="_lock"/> because <see cref="Dictionary{TKey,TValue}"/>
 35    /// is not safe for concurrent read+write.</summary>
 36    private Dictionary<string, FieldDefinition>? _index;
 37    private readonly object _lock = new();
 38
 39    // ── Counts ────────────────────────────────────────────────────────────────
 40
 41    int IReadOnlyCollection<KeyValuePair<string, FieldDefinition>>.Count => Volatile.Read(ref _count);
 42    internal int Count => Volatile.Read(ref _count);
 43
 44    // ── Span access ───────────────────────────────────────────────────────────
 45
 46    /// <summary>Returns a span over the items for zero-alloc iteration.
 47    /// Reads count first, then items — combined with the writer's items-then-count publication this
 48    /// guarantees the observed array contains every slot below the observed count.</summary>
 49    internal ReadOnlySpan<FieldDefinition> AsSpan()
 50    {
 51        var count = Volatile.Read(ref _count);
 52        var items = Volatile.Read(ref _items);
 53        return items == null ? ReadOnlySpan<FieldDefinition>.Empty : items.AsSpan(0, count);
 54    }
 55
 56    // ── Lookup ────────────────────────────────────────────────────────────────
 57
 58    /// <summary>Find a child by name (case-insensitive). Returns null if not found.</summary>
 59    internal FieldDefinition? Find(ReadOnlySpan<char> name)
 60    {
 61        // Fast path: no index yet. Lock-free volatile snapshot + linear scan.
 62        if (Volatile.Read(ref _index) == null)
 63        {
 64            var count = Volatile.Read(ref _count);
 65            var items = Volatile.Read(ref _items);
 66            if (items == null) return null;
 67            for (int i = 0; i < count; i++)
 68            {
 69                if (name.Equals(items[i].Name.AsSpan(), StringComparison.OrdinalIgnoreCase))
 70                    return items[i];
 71            }
 72            return null;
 73        }
 74
 75        // Slow path: indexed lookup needs the lock since Dictionary is not concurrent-read-safe.
 76        // _index is monotone: once published non-null by BuildIndexLocked it never reverts, so
 77        // re-checking under the lock would be a dead branch.
 78        lock (_lock)
 79        {
 80            return _index!.TryGetValue(name.ToString(), out var indexed) ? indexed : null;
 81        }
 82    }
 83
 84    /// <summary>Find a child by name (case-insensitive). String overload avoids span/string round-tripping.</summary>
 85    internal FieldDefinition? Find(string name)
 86    {
 87        if (Volatile.Read(ref _index) == null)
 88        {
 89            var count = Volatile.Read(ref _count);
 90            var items = Volatile.Read(ref _items);
 91            if (items == null) return null;
 92            for (int i = 0; i < count; i++)
 93            {
 94                if (string.Equals(items[i].Name, name, StringComparison.OrdinalIgnoreCase))
 95                    return items[i];
 96            }
 97            return null;
 98        }
 99
 100        lock (_lock)
 101        {
 102            return _index!.TryGetValue(name, out var indexed) ? indexed : null;
 103        }
 104    }
 105
 106    internal bool TryGetValue(ReadOnlySpan<char> name, [MaybeNullWhen(false)] out FieldDefinition value)
 107    {
 108        value = Find(name)!;
 109        return value != null;
 110    }
 111
 112    internal bool TryGetValue(string name, [MaybeNullWhen(false)] out FieldDefinition value)
 113    {
 114        value = Find(name)!;
 115        return value != null;
 116    }
 117
 118    bool IReadOnlyDictionary<string, FieldDefinition>.TryGetValue(string key, [MaybeNullWhen(false)] out FieldDefinition
 119        => TryGetValue(key, out value);
 120
 121    bool IReadOnlyDictionary<string, FieldDefinition>.ContainsKey(string key)
 122        => Find(key) != null;
 123
 124    FieldDefinition IReadOnlyDictionary<string, FieldDefinition>.this[string key]
 125    {
 126        get
 127        {
 128            var found = Find(key);
 129            return found ?? throw new KeyNotFoundException($"Key '{key}' not found.");
 130        }
 131    }
 132
 133    // ── Mutation ──────────────────────────────────────────────────────────────
 134
 135    /// <summary>
 136    /// Appends a new child. The caller must ensure the name does not already exist
 137    /// (use <see cref="Set(string,FieldDefinition)"/> when an update may be needed).
 138    /// </summary>
 139    internal void Append(FieldDefinition child)
 140    {
 141        lock (_lock)
 142        {
 143            AppendLocked(child);
 144        }
 145    }
 146
 147    /// <summary>Adds or replaces a child by name (case-insensitive).</summary>
 148    internal void Set(string name, FieldDefinition child)
 149    {
 150        lock (_lock)
 151        {
 152            var items = _items;
 153            if (items != null)
 154            {
 155                for (int i = 0; i < _count; i++)
 156                {
 157                    if (string.Equals(items[i].Name, name, StringComparison.OrdinalIgnoreCase))
 158                    {
 159                        items[i] = child;
 160                        if (_index != null) _index[child.Name] = child;
 161                        return;
 162                    }
 163                }
 164            }
 165            AppendLocked(child);
 166        }
 167    }
 168
 169    /// <summary>
 170    /// Replaces an existing child by span name (case-insensitive). The only call site —
 171    /// <c>FieldFactory.ProcessDottedSegment</c> — invokes this strictly after a successful
 172    /// <see cref="TryGetValue(ReadOnlySpan{char},out FieldDefinition)"/>, so the entry is
 173    /// guaranteed to exist; the loop is simply walking to the index of the known match.
 174    /// </summary>
 175    internal void Set(ReadOnlySpan<char> name, FieldDefinition child)
 176    {
 177        lock (_lock)
 178        {
 179            var items = _items!;
 180            int i = 0;
 181            while (!name.Equals(items[i].Name.AsSpan(), StringComparison.OrdinalIgnoreCase)) i++;
 182            items[i] = child;
 183            if (_index != null) _index[child.Name] = child;
 184        }
 185    }
 186
 187    /// <summary>
 188    /// Supports collection initializer syntax: <c>new FieldChildren { new FieldDefinition("x") }</c>.
 189    /// Delegates to <see cref="Append"/>.
 190    /// </summary>
 191    internal void Add(FieldDefinition child) => Append(child);
 192
 193    // ── Private helpers ───────────────────────────────────────────────────────
 194
 195    /// <summary>
 196    /// Append logic used within locked sections. Publishes the new slot before bumping
 197    /// <see cref="_count"/> so concurrent readers always see fully-initialized data.
 198    /// </summary>
 199    private void AppendLocked(FieldDefinition child)
 200    {
 201        var items = _items;
 202        if (items == null)
 203        {
 204            items = new FieldDefinition[InitialCapacity];
 205            Volatile.Write(ref _items, items);
 206        }
 207        else if (_count == items.Length)
 208        {
 209            // Grow: allocate a new array, copy, then publish. Concurrent readers either see the old
 210            // array (with their bounded count) or the new array — both are coherent snapshots.
 211            var grown = new FieldDefinition[items.Length * 2];
 212            Array.Copy(items, grown, _count);
 213            items = grown;
 214            Volatile.Write(ref _items, items);
 215        }
 216
 217        items[_count] = child;
 218        // Release-store the new count last so any reader that observes it also sees the slot above.
 219        Volatile.Write(ref _count, _count + 1);
 220
 221        if (_index != null)
 222        {
 223            _index[child.Name] = child;
 224        }
 225        else if (_count >= IndexThreshold)
 226        {
 227            BuildIndexLocked();
 228        }
 229    }
 230
 231    private void BuildIndexLocked()
 232    {
 233        var built = new Dictionary<string, FieldDefinition>(_count, StringComparer.OrdinalIgnoreCase);
 234        for (int i = 0; i < _count; i++)
 235            built[_items![i].Name] = _items[i];
 236        // Publish the index pointer last so readers that observe non-null _index see a fully populated dict.
 237        Volatile.Write(ref _index, built);
 238    }
 239
 240    // ── IReadOnlyDictionary (Keys / Values / Enumerator) ─────────────────────
 241
 242    IEnumerable<string> IReadOnlyDictionary<string, FieldDefinition>.Keys
 243    {
 244        get
 245        {
 246            var count = Volatile.Read(ref _count);
 247            var items = Volatile.Read(ref _items);
 248            if (items == null) return [];
 249            var keys = new List<string>(count);
 250            for (int i = 0; i < count; i++)
 251                keys.Add(items[i].Name);
 252            return keys;
 253        }
 254    }
 255
 256    IEnumerable<FieldDefinition> IReadOnlyDictionary<string, FieldDefinition>.Values
 257    {
 258        get
 259        {
 260            var count = Volatile.Read(ref _count);
 261            var items = Volatile.Read(ref _items);
 262            if (items == null) return [];
 263            var values = new List<FieldDefinition>(count);
 264            for (int i = 0; i < count; i++)
 265                values.Add(items[i]);
 266            return values;
 267        }
 268    }
 269
 270    /// <summary>
 271    /// Returns a zero-alloc struct enumerator over a snapshot of this collection.
 272    /// </summary>
 273    public FieldChildrenEnumerator GetEnumerator()
 274    {
 275        var count = Volatile.Read(ref _count);
 276        var items = Volatile.Read(ref _items);
 277        return new FieldChildrenEnumerator(items, count);
 278    }
 279
 280    // Explicit interface implementation — provides fallback for code that uses IEnumerable<T> directly
 281    IEnumerator<KeyValuePair<string, FieldDefinition>> IEnumerable<KeyValuePair<string, FieldDefinition>>.GetEnumerator(
 282        => GetEnumerator();
 283
 284    IEnumerator IEnumerable.GetEnumerator()
 285        => GetEnumerator();
 286}
 287
 288/// <summary>
 289/// Zero-allocation struct enumerator for <see cref="FieldChildren"/>.
 290/// Operates on a snapshot taken at construction time, so concurrent appends do not
 291/// affect an in-flight enumeration.
 292/// </summary>
 293internal struct FieldChildrenEnumerator : IEnumerator<KeyValuePair<string, FieldDefinition>>
 294{
 295    private readonly FieldDefinition[]? _snapshot;
 296    private readonly int _count;
 297    private int _index;
 298
 299    internal FieldChildrenEnumerator(FieldDefinition[]? snapshot, int count)
 300    {
 906301        _snapshot = snapshot;
 906302        _count = count;
 906303        _index = -1;
 906304    }
 305
 306    public KeyValuePair<string, FieldDefinition> Current
 307    {
 308        get
 309        {
 1218310            if (_snapshot == null || _index < 0 || _index >= _count)
 3311                throw new InvalidOperationException("Enumeration has not started or has ended.");
 1215312            var item = _snapshot[_index];
 1215313            return new KeyValuePair<string, FieldDefinition>(item.Name, item);
 314        }
 315    }
 316
 6317    object System.Collections.IEnumerator.Current => Current;
 318
 319    public bool MoveNext()
 320    {
 2025321        if (_snapshot == null) return false;
 1995322        _index++;
 1995323        return _index < _count;
 324    }
 325
 3326    public void Reset() => _index = -1;
 327
 897328    public void Dispose() { }
 329}