< Summary

Information
Class: NGql.Core.Features.QueryMerger
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Features/QueryMerger.cs
Line coverage
100%
Covered lines: 85
Uncovered lines: 0
Coverable lines: 85
Total lines: 256
Line coverage: 100%
Branch coverage
100%
Covered branches: 54
Total branches: 54
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
MergeQuery(...)100%66100%
MergeNamedFragments(...)100%66100%
MergeNamedFragmentBody(...)100%66100%
MergeVariables(...)100%88100%
ApplyFieldMerge(...)100%66100%
ApplyMergeByFieldPath(...)100%22100%
GetEffectiveMergingStrategy(...)100%66100%
AddFieldWithUniqueKey(...)100%22100%
FindMergeTarget(...)100%1010100%
MarkAsNeverMerge(...)100%22100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Features/QueryMerger.cs

#LineLine coverage
 1using NGql.Core.Abstractions;
 2using NGql.Core.Builders;
 3using NGql.Core.Exceptions;
 4using NGql.Core.Extensions;
 5
 6namespace NGql.Core.Features;
 7
 8/// <summary>
 9/// Handles merging of query definitions using different strategies
 10/// </summary>
 11internal static class QueryMerger
 12{
 13    /// <summary>
 14    /// Merges an incoming query definition into the target query definition, updating all related state.
 15    /// </summary>
 16    public static void MergeQuery(
 17        QueryDefinition targetDefinition,
 18        QueryMap queryMap,
 19        QueryBuilder? queryBuilder,
 20        in QueryDefinition incomingQuery)
 21    {
 22        // Named fragments are operation-scoped, not field-scoped: merge them even when the incoming
 23        // query declares nothing but fragments (an unusual but valid shape — the renderer emits
 24        // declared-but-unused fragment definitions).
 218425        MergeNamedFragments(targetDefinition, incomingQuery);
 218126        MergeVariables(targetDefinition, incomingQuery);
 27
 219028        if (incomingQuery._fields == null || incomingQuery._fields.Count == 0) return;
 29
 217230        var beforeCount = targetDefinition.Fields.Count;
 217231        ApplyFieldMerge(targetDefinition, incomingQuery, targetDefinition.MergingStrategy, queryMap);
 32
 215733        if (targetDefinition.Fields.Count != beforeCount)
 34        {
 101135            queryMap.UpdateRootMapping(targetDefinition);
 36        }
 215737    }
 38
 39    /// <summary>
 40    /// Merges every named fragment the incoming query declares into <paramref name="targetDefinition"/>
 41    /// using the target's <see cref="QueryDefinition.GetOrAddNamedFragment"/> collision semantics:
 42    /// same name + same <c>onType</c> reuses the existing fragment (and merges its body); same name +
 43    /// a DIFFERENT <c>onType</c> throws <see cref="InvalidOperationException"/>. Each fragment's body
 44    /// (fields, nested inline fragments, and spreads) is deep-cloned before entering the target so the
 45    /// merged operation never aliases the source's fragment definitions.
 46    /// </summary>
 47    private static void MergeNamedFragments(QueryDefinition targetDefinition, in QueryDefinition incomingQuery)
 48    {
 218449        var incomingFragments = incomingQuery._namedFragments;
 432950        if (incomingFragments is not { Count: > 0 }) return;
 51
 15952        foreach (var incoming in incomingFragments.Values)
 53        {
 54            // Throws InvalidOperationException on a name collision with a conflicting onType —
 55            // reuses the existing declaration semantics rather than inventing new merge rules.
 4256            var target = targetDefinition.GetOrAddNamedFragment(incoming.Name, incoming.OnType);
 3957            MergeNamedFragmentBody(target, incoming);
 58        }
 3659    }
 60
 61    private static void MergeNamedFragmentBody(NamedFragmentDefinition target, NamedFragmentDefinition incoming)
 62    {
 3963        if (incoming._fields is { Count: > 0 })
 64        {
 3365            var targetFields = target.GetOrCreateFieldsStore();
 16866            foreach (var child in incoming._fields.AsSpan())
 67            {
 5168                FieldBuilder.Include(targetFields, child);
 69            }
 70        }
 71
 3972        FieldDefinitionExtensions.MergeInlineFragmentsInto(ref target._fragments, incoming._fragments);
 3973        FieldDefinitionExtensions.MergeSpreadNamesInto(ref target._spreadFragments, incoming._spreadFragments);
 3974    }
 75
 76    private static void MergeVariables(QueryDefinition targetDefinition, in QueryDefinition incomingQuery)
 77    {
 218178        var incomingVars = incomingQuery._variables;
 432979        if (incomingVars is null || incomingVars.Count == 0) return;
 80
 3381        var targetVars = targetDefinition._variables;
 3382        if (targetVars is null)
 83        {
 1884            targetDefinition.Variables = new SortedSet<Variable>(incomingVars);
 1885            return;
 86        }
 87
 6088        foreach (var v in incomingVars)
 1589            targetVars.Add(v);
 1590    }
 91
 92    /// <summary>
 93    /// Mutates <paramref name="targetDefinition"/>'s root fields in place by applying every field
 94    /// from the incoming query according to the resolved merging strategy. Avoids the O(N)
 95    /// copy-out / copy-back of the existing approach so a chain of <c>Include()</c>s is O(K) per
 96    /// call instead of O(N+K).
 97    /// </summary>
 98    private static void ApplyFieldMerge(
 99        QueryDefinition targetDefinition,
 100        QueryDefinition incomingQuery,
 101        MergingStrategy rootStrategy,
 102        QueryMap queryMap)
 103    {
 2172104        var fields = targetDefinition.FieldsInternal;
 2172105        var strategy = GetEffectiveMergingStrategy(rootStrategy, incomingQuery.MergingStrategy);
 2172106        var queryName = incomingQuery.Name;
 107
 8679108        foreach (var (originalFieldKey, incomingField) in incomingQuery.FieldsInternal)
 109        {
 110            switch (strategy)
 111            {
 112                case MergingStrategy.MergeByDefault:
 113                    // FieldBuilder.Include may insert a brand-new root key (when no existing
 114                    // field shares its Name) — resync the merge index by count on next use rather
 115                    // than tracking this path's insertions individually, since MergeByDefault
 116                    // never consults FindMergeTarget/the fingerprint index itself.
 276117                    FieldBuilder.Include(fields, incomingField);
 276118                    queryMap.SetMapping(queryName, originalFieldKey);
 276119                    break;
 120
 121                case MergingStrategy.NeverMerge:
 132122                    AddFieldWithUniqueKey(targetDefinition, originalFieldKey, MarkAsNeverMerge(incomingField), queryMap,
 132123                    break;
 124
 125                case MergingStrategy.MergeByFieldPath:
 1764126                    ApplyMergeByFieldPath(targetDefinition, originalFieldKey, incomingField, queryMap, queryName);
 1752127                    break;
 128
 129                default:
 3130                    throw new ArgumentOutOfRangeException(nameof(rootStrategy), $"Merging strategy {strategy} is not imp
 131            }
 132        }
 2157133    }
 134
 135    private static void ApplyMergeByFieldPath(
 136        QueryDefinition targetDefinition,
 137        string originalFieldKey,
 138        FieldDefinition incomingField,
 139        QueryMap queryMap,
 140        string queryName)
 141    {
 1764142        var fields = targetDefinition.FieldsInternal;
 1764143        var mergeTarget = FindMergeTarget(targetDefinition, fields, incomingField);
 144
 1764145        if (mergeTarget == null)
 146        {
 786147            AddFieldWithUniqueKey(targetDefinition, originalFieldKey, incomingField, queryMap, queryName);
 786148            return;
 149        }
 150
 151        try
 152        {
 153            // Mutate the existing field in place — we own the reference (we're about to overwrite the
 154            // dictionary entry with the same instance). Avoids cloning the entire subtree on every Include.
 978155            FieldDefinitionExtensions.MergeFieldsInPlace(mergeTarget.Value.Field, incomingField);
 156
 157            // The merge can change the target's deep fingerprint (new argument-bearing content
 158            // merged deeper into an already-significant subtree — see MergeFieldsInPlace's memo
 159            // invalidation). The fingerprint-bucketed index must be told explicitly: the field kept
 160            // its identity (same reference, same key) but may now belong under a different
 161            // fingerprint bucket, and nothing else would ever notice the move.
 966162            targetDefinition.MergeIndex.ReindexFingerprint(fields, incomingField.Name, mergeTarget.Value.Key);
 966163            queryMap.SetMapping(queryName, mergeTarget.Value.Key);
 966164        }
 12165        catch (QueryMergeException ex)
 166        {
 12167            throw new QueryMergeException($"Cannot merge query '{queryName}' due to type conflicts in field '{incomingFi
 168        }
 966169    }
 170
 171    private static MergingStrategy GetEffectiveMergingStrategy(MergingStrategy rootStrategy, MergingStrategy childStrate
 172    {
 2172173        if (childStrategy == MergingStrategy.NeverMerge)
 174        {
 48175            return MergingStrategy.NeverMerge;
 176        }
 177
 2124178        return rootStrategy switch
 2124179        {
 84180            MergingStrategy.NeverMerge => MergingStrategy.NeverMerge,
 282181            MergingStrategy.MergeByDefault => childStrategy,
 1758182            _ => rootStrategy,
 2124183        };
 184    }
 185
 186    private static void AddFieldWithUniqueKey(
 187        QueryDefinition targetDefinition,
 188        string originalFieldKey,
 189        FieldDefinition incomingField,
 190        QueryMap queryMap,
 191        string queryName)
 192    {
 918193        var fields = targetDefinition.FieldsInternal;
 918194        var mergeIndex = targetDefinition.MergeIndex;
 918195        var uniqueKey = KeyGenerator.GenerateUniqueKey(mergeIndex, fields, incomingField._effectiveName);
 196
 197        // Deep-clone so the target dictionary owns its subtree exclusively. Subsequent in-place
 198        // merges (MergeFieldsInPlace below) must not leak field additions back into the source
 199        // QueryBuilder that supplied incomingField.
 918200        var fieldToAdd = incomingField.DeepClone();
 918201        if (!string.Equals(uniqueKey, originalFieldKey, StringComparison.OrdinalIgnoreCase))
 202        {
 711203            fieldToAdd = fieldToAdd with { Alias = uniqueKey };
 204        }
 205
 918206        fields[uniqueKey] = fieldToAdd;
 918207        mergeIndex.IndexAdd(uniqueKey, fieldToAdd);
 918208        queryMap.SetMapping(queryName, uniqueKey);
 918209    }
 210
 211    private static (string Key, FieldDefinition Field)? FindMergeTarget(
 212        QueryDefinition targetDefinition,
 213        Dictionary<string, FieldDefinition> existingFields,
 214        FieldDefinition incomingField)
 215    {
 216        // Deep (subtree) fingerprint — strictly refines the old own-arguments-only fingerprint by
 217        // also folding in every descendant whose subtree carries an argument anywhere (see
 218        // FieldDefinitionExtensions.ComputeDeepFingerprint for the full conservatism proof). This is
 219        // what lets the production shape (a root field with zero arguments, whose only discriminator
 220        // is a filter several levels down) get bucketed correctly instead of every candidate
 221        // colliding into one fingerprint and falling through to a full CanMergeFields scan.
 1764222        var incomingFingerprint = FieldDefinitionExtensions.ComputeDeepFingerprint(incomingField);
 223
 224        // Sub-bucketed by (Name, fingerprint): narrows the candidate set to just the handful of
 225        // existing fields that could actually match, instead of every field sharing incomingField's
 226        // Name (which is exactly what makes the production shape — N fragments, one shared root
 227        // Name, N distinct deep filters — quadratic under Name-only bucketing). The index
 228        // self-heals any entry whose live fingerprint has drifted since it was bucketed (see
 229        // FieldMergeIndex remarks), so this can never produce a false split: every candidate
 230        // returned here is confirmed, from its LIVE field, to share incomingFingerprint.
 1764231        var candidateKeys = targetDefinition.MergeIndex.GetMergeCandidatesByFingerprint(
 1764232            existingFields, incomingField.Name, incomingFingerprint);
 2511233        if (candidateKeys is null) return null;
 234
 3108235        foreach (var key in candidateKeys)
 236        {
 1026237            if (!existingFields.TryGetValue(key, out var existingField))
 238                continue;
 239
 1026240            if (existingField.IsNeverMerge)
 241                continue;
 242
 243            // CanMergeFields remains the sole source of truth — the fingerprint match above is a
 244            // conservative pre-filter only. A match does NOT prove equality (a hash collision costs
 245            // one extra, unmodified CanMergeFields call here; it is never treated as sufficient on
 246            // its own).
 1014247            if (FieldDefinitionExtensions.CanMergeFields(existingField, incomingField))
 978248                return (key, existingField);
 249        }
 250
 39251        return null;
 978252    }
 253
 254    private static FieldDefinition MarkAsNeverMerge(FieldDefinition field)
 132255        => field.IsNeverMerge ? field : field with { IsNeverMerge = true };
 256}