< Summary

Information
Class: NGql.Core.Extensions.FieldDefinitionExtensions
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Extensions/FieldDefinitionExtensions.cs
Line coverage
99%
Covered lines: 115
Uncovered lines: 1
Coverable lines: 116
Total lines: 294
Line coverage: 99.1%
Branch coverage
97%
Covered branches: 117
Total branches: 120
Branch coverage: 97.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/NGql/NGql/src/Core/Extensions/FieldDefinitionExtensions.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using NGql.Core.Abstractions;
 3using NGql.Core.Exceptions;
 4using NGql.Core.Features;
 5
 6namespace NGql.Core.Extensions;
 7
 8/// <summary>
 9/// Extension methods and utilities for FieldDefinition operations.
 10/// </summary>
 11internal static class FieldDefinitionExtensions
 12{
 13    /// <summary>
 14    /// Determines if two fields can be merged based on their structure and arguments.
 15    /// The recursion is bounded by the field tree's depth — the public API does not allow
 16    /// constructing cyclic trees (FieldDefinition.Fields is get-only, _children is internal),
 17    /// so the recursion always terminates. A library bug introducing a cycle would surface as
 18    /// a StackOverflowException, which is louder and more actionable than a swallowed throw.
 19    /// </summary>
 20    internal static bool CanMergeFields(FieldDefinition existingField, FieldDefinition incomingField)
 21    {
 19222        if (!Helpers.AreArgumentsEqual(existingField._arguments, incomingField._arguments))
 3923            return false;
 15324        return AreNestedFieldsCompatible(existingField, incomingField);
 25    }
 26
 27    private static bool AreNestedFieldsCompatible(FieldDefinition existingField, FieldDefinition incomingField)
 35428        => IncomingChildrenCompatible(existingField._children, incomingField._children)
 35429        && ExistingExtrasCompatible(existingField, incomingField._children);
 30
 31    private static bool IncomingChildrenCompatible(FieldChildren? existingChildren, FieldChildren? incomingChildren)
 32    {
 40833        if (incomingChildren is not { Count: > 0 }) return true;
 34
 30035        var span = incomingChildren.AsSpan();
 121836        for (int i = 0; i < span.Length; i++)
 37        {
 33938            if (!IsIncomingChildCompatible(existingChildren, span[i]))
 3039                return false;
 40        }
 27041        return true;
 42    }
 43
 44    private static bool IsIncomingChildCompatible(FieldChildren? existingChildren, FieldDefinition incomingChild)
 45    {
 34846        if (existingChildren is null) return !HasAnyArguments(incomingChild);
 44747        if (!existingChildren.TryGetValue(incomingChild.Name, out var existingChild)) return !HasAnyArguments(incomingCh
 21348        return Helpers.AreArgumentsEqual(existingChild!._arguments, incomingChild._arguments)
 21349            && AreNestedFieldsCompatible(existingChild, incomingChild);
 50    }
 51
 52    // Skip the existing-not-in-incoming check entirely when nothing in the existing subtree
 53    // could ever fail it — early-out when no field in the existing subtree carries arguments.
 54    private static bool ExistingExtrasCompatible(FieldDefinition existingField, FieldChildren? incomingChildren)
 55    {
 32456        var existingChildren = existingField._children;
 32457        if (existingChildren is not { Count: > 0 } || !SubtreeHasAnyArguments(existingField))
 30058            return true;
 59
 2460        var span = existingChildren.AsSpan();
 8461        for (int i = 0; i < span.Length; i++)
 62        {
 2463            var existingChild = span[i];
 2464            if (!IsExistingExtraCompatible(existingChild, incomingChildren))
 665                return false;
 66        }
 1867        return true;
 68    }
 69
 70    private static bool IsExistingExtraCompatible(FieldDefinition existingChild, FieldChildren? incomingChildren)
 71    {
 2772        if (incomingChildren is null) return !HasAnyArguments(existingChild);
 2173        return incomingChildren.Find(existingChild.Name) is not null || !HasAnyArguments(existingChild);
 74    }
 75
 76    private static bool SubtreeHasAnyArguments(FieldDefinition field)
 77    {
 87678        if (field._subtreeHasAnyArguments is { } cached) return cached;
 79
 47480        var result = field._arguments is { Count: > 0 } || AnyChildHasArguments(field._children);
 47481        field._subtreeHasAnyArguments = result;
 47482        return result;
 83    }
 84
 85    private static bool AnyChildHasArguments(FieldChildren? children)
 86    {
 62187        if (children is null || children.Count == 0) return false;
 29788        var span = children.AsSpan();
 136889        for (int i = 0; i < span.Length; i++)
 90        {
 42391            if (SubtreeHasAnyArguments(span[i])) return true;
 92        }
 27993        return false;
 94    }
 95
 96    private static bool HasAnyArguments(FieldDefinition field)
 97    {
 20198        if (field._arguments is { Count: > 0 }) return true;
 29199        if (field._children is null) return false;
 171100        foreach (var child in field._children.AsSpan())
 101        {
 51102            if (HasAnyArguments(child)) return true;
 103        }
 36104        return false;
 105    }
 106
 107    /// <summary>
 108    /// Deep-clones <paramref name="source"/> producing a new <see cref="FieldDefinition"/> that
 109    /// shares no mutable state with the original. Used by <see cref="QueryMerger"/> when adding an
 110    /// incoming field reference into the target dictionary, so subsequent in-place merges into the
 111    /// target do not leak back into the source builder.
 112    /// </summary>
 113    internal static FieldDefinition DeepClone(this FieldDefinition source)
 114    {
 774115        var clone = new FieldDefinition(
 774116            source.Name,
 774117            source._type!,
 774118            source._alias,
 774119            source._arguments is null ? null : new SortedDictionary<string, object?>(source._arguments, StringComparer.O
 774120        {
 774121            Path = source.Path,
 774122            IsNeverMerge = source.IsNeverMerge,
 774123        };
 124
 774125        if (source._metadata is { Count: > 0 })
 126        {
 0127            foreach (var kvp in source._metadata) clone.Metadata[kvp.Key] = kvp.Value;
 128        }
 129
 774130        if (source._children is { Count: > 0 })
 131        {
 414132            clone._children = new FieldChildren();
 1836133            foreach (var child in source._children.AsSpan())
 504134                clone._children.Append(child.DeepClone());
 135        }
 136
 774137        return clone;
 138    }
 139
 140    /// <summary>
 141    /// Merges <paramref name="incoming"/> INTO <paramref name="existing"/>, mutating its child collection
 142    /// and argument dictionary in place. The caller must own <paramref name="existing"/> exclusively
 143    /// (no external aliases) — used by <see cref="QueryMerger"/> when the result will overwrite the
 144    /// dictionary entry that holds <paramref name="existing"/>.
 145    /// </summary>
 146    /// <returns><paramref name="existing"/> after mutation.</returns>
 147    internal static FieldDefinition MergeFieldsInPlace(FieldDefinition existing, FieldDefinition incoming)
 148    {
 315149        ThrowIfTypesConflict(existing, incoming);
 306150        MergeIncomingChildrenInPlace(existing, incoming._children);
 294151        MergeIncomingArgumentsInPlace(existing, incoming._arguments);
 294152        return existing;
 153    }
 154
 155    private static void ThrowIfTypesConflict(FieldDefinition existing, FieldDefinition incoming)
 156    {
 157        // _type defaults to Constants.DefaultFieldType in every public constructor and is
 158        // never null on the merge path through Include — the field is always touched by
 159        // QueryBuilder.AddField which goes through FieldFactory.CreateFieldDefinition.
 621160        if (existing._type!.AsSpan().Equals(incoming._type!.AsSpan(), StringComparison.OrdinalIgnoreCase)) return;
 9161        throw new QueryMergeException($"Type conflict: existing field has type '{existing._type}', incoming field has ty
 162    }
 163
 164    private static void MergeIncomingChildrenInPlace(FieldDefinition existing, FieldChildren? incomingChildren)
 165    {
 351166        if (incomingChildren is null) return;
 167
 168        // Existing._children is non-null on every path that reaches here through the public
 169        // Include API: type-compatibility means both sides are object-typed, and object-typed
 170        // FieldDefinitions get a children collection from QueryBuilder.AddField at construction
 171        // time. Trust the invariant.
 261172        var existingChildren = existing._children!;
 261173        var span = incomingChildren.AsSpan();
 1092174        for (int i = 0; i < span.Length; i++)
 175        {
 297176            MergeChildInPlace(existingChildren, span[i]);
 177        }
 249178        existing._subtreeHasAnyArguments = null;
 249179    }
 180
 181    private static void MergeIncomingArgumentsInPlace(FieldDefinition existing, SortedDictionary<string, object?>? incom
 182    {
 573183        if (incomingArguments is not { Count: > 0 }) return;
 15184        existing.MergeFieldArgumentsInPlace(incomingArguments);
 15185        existing._subtreeHasAnyArguments = null;
 15186    }
 187
 188    private static void MergeChildInPlace(FieldChildren existingChildren, FieldDefinition incomingChild)
 189    {
 297190        if (existingChildren.TryGetValue(incomingChild.Name, out var existingNested) && existingNested != null)
 191        {
 192192            MergeFieldsInPlace(existingNested, incomingChild);
 180193            return;
 194        }
 195
 196        // Deep-clone so target's tree never references nodes owned by the source builder. Any
 197        // later in-place merges into existingChildren must not propagate back to the source.
 105198        var clone = incomingChild.DeepClone();
 199
 200        // Effective-name conflict can only occur when the incoming field carries an alias
 201        // distinct from its name; otherwise the Name lookup above would have caught it.
 105202        if (!ReferenceEquals(incomingChild._effectiveName, incomingChild.Name) &&
 105203            HasEffectiveNameConflict(existingChildren, incomingChild._effectiveName))
 204        {
 3205            var uniqueAlias = KeyGenerator.GenerateUniqueKey(incomingChild._effectiveName, existingChildren.AsSpan());
 3206            existingChildren.Append(clone with { Alias = uniqueAlias });
 207        }
 208        else
 209        {
 102210            existingChildren.Append(clone);
 211        }
 102212    }
 213
 214    private static bool HasEffectiveNameConflict(FieldChildren children, string effectiveName)
 215    {
 237216        foreach (var f in children.AsSpan())
 217        {
 81218            if (string.Equals(f._effectiveName, effectiveName, StringComparison.OrdinalIgnoreCase))
 3219                return true;
 220        }
 36221        return false;
 222    }
 223
 224    /// <summary>
 225    /// Merges <paramref name="newArguments"/> into <paramref name="existingField"/>'s argument
 226    /// dictionary in place. Caller (MergeFieldsInPlace via CanMergeFields) guarantees that
 227    /// <c>existingField._arguments</c> already has the same keys as <paramref name="newArguments"/>;
 228    /// the merge here only refines values for keys that hold nested dictionaries.
 229    /// </summary>
 230    // Callers (MergeFieldsInPlace via CanMergeFields) guarantee newArguments has Count > 0
 231    // and existingField._arguments is non-null with the same keys.
 232    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 233    internal static void MergeFieldArgumentsInPlace(this FieldDefinition existingField, IDictionary<string, object?> new
 234    {
 15235        var target = existingField._arguments!;
 60236        foreach (var (key, newValue) in newArguments)
 237        {
 15238            if (target.TryGetValue(key, out var existingValue)
 15239                && existingValue is IDictionary<string, object?> existingDict
 15240                && newValue is IDictionary<string, object?> newDict)
 241            {
 6242                target[key] = Helpers.MergeNullableDictionaries(existingDict, newDict);
 6243                continue;
 244            }
 9245            target[key] = newValue;
 246        }
 15247    }
 248
 249    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 250    internal static FieldDefinition MergeFieldArguments(this FieldDefinition existingField, IDictionary<string, object?>
 251    {
 474252        if (newArguments is not { Count: > 0 }) return existingField;
 253
 372254        if (existingField._arguments is null || existingField._arguments.Count == 0)
 255        {
 327256            return existingField with { _arguments = AsSortedCaseInsensitive(newArguments) };
 257        }
 258
 45259        var merged = CopyToSortedCaseInsensitive(existingField._arguments);
 45260        ApplyArgumentOverrides(merged, newArguments);
 45261        return existingField with { _arguments = merged };
 262    }
 263
 264    private static SortedDictionary<string, object?> AsSortedCaseInsensitive(IDictionary<string, object?> source)
 327265        => source is SortedDictionary<string, object?> sd
 327266            ? sd
 327267            : new SortedDictionary<string, object?>(source, StringComparer.OrdinalIgnoreCase);
 268
 269    private static SortedDictionary<string, object?> CopyToSortedCaseInsensitive(IDictionary<string, object?> source)
 270    {
 45271        var copy = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 243272        foreach (var (key, value) in source) copy[key] = value;
 45273        return copy;
 274    }
 275
 276    private static void ApplyArgumentOverrides(SortedDictionary<string, object?> target, IDictionary<string, object?> ov
 277    {
 192278        foreach (var (key, newValue) in overrides)
 279        {
 51280            target[key] = MergedArgumentValue(target, key, newValue);
 281        }
 45282    }
 283
 284    private static object? MergedArgumentValue(SortedDictionary<string, object?> target, string key, object? newValue)
 285    {
 51286        if (target.TryGetValue(key, out var existingValue)
 51287            && existingValue is IDictionary<string, object?> existingDict
 51288            && newValue is IDictionary<string, object?> newDict)
 289        {
 3290            return Helpers.MergeNullableDictionaries(existingDict, newDict);
 291        }
 48292        return newValue;
 293    }
 294}

Methods/Properties

CanMergeFields(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
AreNestedFieldsCompatible(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
IncomingChildrenCompatible(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldChildren)
IsIncomingChildCompatible(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
ExistingExtrasCompatible(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldChildren)
IsExistingExtraCompatible(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldChildren)
SubtreeHasAnyArguments(NGql.Core.Abstractions.FieldDefinition)
AnyChildHasArguments(NGql.Core.Abstractions.FieldChildren)
HasAnyArguments(NGql.Core.Abstractions.FieldDefinition)
DeepClone(NGql.Core.Abstractions.FieldDefinition)
MergeFieldsInPlace(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
ThrowIfTypesConflict(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
MergeIncomingChildrenInPlace(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldChildren)
MergeIncomingArgumentsInPlace(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>)
MergeChildInPlace(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
HasEffectiveNameConflict(NGql.Core.Abstractions.FieldChildren,System.String)
MergeFieldArgumentsInPlace(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
MergeFieldArguments(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
AsSortedCaseInsensitive(System.Collections.Generic.IDictionary`2<System.String,System.Object>)
CopyToSortedCaseInsensitive(System.Collections.Generic.IDictionary`2<System.String,System.Object>)
ApplyArgumentOverrides(System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
MergedArgumentValue(System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.String,System.Object)