< 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: 61
Uncovered lines: 0
Coverable lines: 61
Total lines: 172
Line coverage: 100%
Branch coverage
100%
Covered branches: 40
Total branches: 40
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%
MergeVariables(...)100%88100%
ApplyFieldMerge(...)100%66100%
ApplyMergeByFieldPath(...)100%22100%
GetEffectiveMergingStrategy(...)100%66100%
AddFieldWithUniqueKey(...)100%22100%
FindMergeTarget(...)100%88100%
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    {
 34822        if (incomingQuery._fields == null || incomingQuery._fields.Count == 0) return;
 23
 34224        MergeVariables(targetDefinition, incomingQuery);
 25
 34226        var beforeCount = targetDefinition.Fields.Count;
 34227        ApplyFieldMerge(targetDefinition.Fields, incomingQuery, targetDefinition.MergingStrategy, queryMap);
 28
 33029        if (targetDefinition.Fields.Count != beforeCount)
 30        {
 18931            queryMap.UpdateRootMapping(targetDefinition);
 32        }
 33033    }
 34
 35    private static void MergeVariables(QueryDefinition targetDefinition, in QueryDefinition incomingQuery)
 36    {
 34237        var incomingVars = incomingQuery._variables;
 67838        if (incomingVars is null || incomingVars.Count == 0) return;
 39
 640        var targetVars = targetDefinition._variables;
 641        if (targetVars is null)
 42        {
 343            targetDefinition.Variables = new SortedSet<Variable>(incomingVars);
 344            return;
 45        }
 46
 1247        foreach (var v in incomingVars)
 348            targetVars.Add(v);
 349    }
 50
 51    /// <summary>
 52    /// Mutates <paramref name="fields"/> in place by applying every field from the incoming query
 53    /// according to the resolved merging strategy. Avoids the O(N) copy-out / copy-back of the
 54    /// existing approach so a chain of <c>Include()</c>s is O(K) per call instead of O(N+K).
 55    /// </summary>
 56    private static void ApplyFieldMerge(
 57        Dictionary<string, FieldDefinition> fields,
 58        QueryDefinition incomingQuery,
 59        MergingStrategy rootStrategy,
 60        QueryMap queryMap)
 61    {
 34262        var strategy = GetEffectiveMergingStrategy(rootStrategy, incomingQuery.MergingStrategy);
 34263        var queryName = incomingQuery.Name;
 64
 136265        foreach (var (originalFieldKey, incomingField) in incomingQuery.Fields)
 66        {
 67            switch (strategy)
 68            {
 69                case MergingStrategy.MergeByDefault:
 5470                    FieldBuilder.Include(fields, incomingField);
 5471                    queryMap.SetMapping(queryName, originalFieldKey);
 5472                    break;
 73
 74                case MergingStrategy.NeverMerge:
 5475                    AddFieldWithUniqueKey(fields, originalFieldKey, MarkAsNeverMerge(incomingField), queryMap, queryName
 5476                    break;
 77
 78                case MergingStrategy.MergeByFieldPath:
 23479                    ApplyMergeByFieldPath(fields, originalFieldKey, incomingField, queryMap, queryName);
 22580                    break;
 81
 82                default:
 383                    throw new ArgumentOutOfRangeException(nameof(rootStrategy), $"Merging strategy {strategy} is not imp
 84            }
 85        }
 33086    }
 87
 88    private static void ApplyMergeByFieldPath(
 89        Dictionary<string, FieldDefinition> fields,
 90        string originalFieldKey,
 91        FieldDefinition incomingField,
 92        QueryMap queryMap,
 93        string queryName)
 94    {
 23495        var mergeTarget = FindMergeTarget(fields, incomingField);
 96
 23497        if (mergeTarget == null)
 98        {
 11199            AddFieldWithUniqueKey(fields, originalFieldKey, incomingField, queryMap, queryName);
 111100            return;
 101        }
 102
 103        try
 104        {
 105            // Mutate the existing field in place — we own the reference (we're about to overwrite the
 106            // dictionary entry with the same instance). Avoids cloning the entire subtree on every Include.
 123107            FieldDefinitionExtensions.MergeFieldsInPlace(mergeTarget.Value.Field, incomingField);
 114108            queryMap.SetMapping(queryName, mergeTarget.Value.Key);
 114109        }
 9110        catch (QueryMergeException ex)
 111        {
 9112            throw new QueryMergeException($"Cannot merge query '{queryName}' due to type conflicts in field '{incomingFi
 113        }
 114114    }
 115
 116    private static MergingStrategy GetEffectiveMergingStrategy(MergingStrategy rootStrategy, MergingStrategy childStrate
 117    {
 342118        if (childStrategy == MergingStrategy.NeverMerge)
 119        {
 36120            return MergingStrategy.NeverMerge;
 121        }
 122
 306123        return rootStrategy switch
 306124        {
 18125            MergingStrategy.NeverMerge => MergingStrategy.NeverMerge,
 54126            MergingStrategy.MergeByDefault => childStrategy,
 234127            _ => rootStrategy,
 306128        };
 129    }
 130
 131    private static void AddFieldWithUniqueKey(
 132        Dictionary<string, FieldDefinition> fields,
 133        string originalFieldKey,
 134        FieldDefinition incomingField,
 135        QueryMap queryMap,
 136        string queryName)
 137    {
 165138        var uniqueKey = KeyGenerator.GenerateUniqueKey(incomingField._effectiveName, fields.Keys);
 139
 140        // Deep-clone so the target dictionary owns its subtree exclusively. Subsequent in-place
 141        // merges (MergeFieldsInPlace below) must not leak field additions back into the source
 142        // QueryBuilder that supplied incomingField.
 165143        var fieldToAdd = incomingField.DeepClone();
 165144        if (!string.Equals(uniqueKey, originalFieldKey, StringComparison.OrdinalIgnoreCase))
 145        {
 123146            fieldToAdd = fieldToAdd with { Alias = uniqueKey, _effectiveName = uniqueKey };
 147        }
 148
 165149        fields[uniqueKey] = fieldToAdd;
 165150        queryMap.SetMapping(queryName, uniqueKey);
 165151    }
 152
 153    private static (string Key, FieldDefinition Field)? FindMergeTarget(Dictionary<string, FieldDefinition> existingFiel
 154    {
 765155        foreach (var (key, existingField) in existingFields)
 156        {
 210157            if (!string.Equals(existingField.Name, incomingField.Name, StringComparison.OrdinalIgnoreCase))
 158                continue;
 159
 204160            if (existingField.IsNeverMerge)
 161                continue;
 162
 192163            if (FieldDefinitionExtensions.CanMergeFields(existingField, incomingField))
 123164                return (key, existingField);
 165        }
 166
 111167        return null;
 123168    }
 169
 170    private static FieldDefinition MarkAsNeverMerge(FieldDefinition field)
 54171        => field.IsNeverMerge ? field : field with { IsNeverMerge = true };
 172}