| | | 1 | | using NGql.Core.Abstractions; |
| | | 2 | | using NGql.Core.Builders; |
| | | 3 | | using NGql.Core.Exceptions; |
| | | 4 | | using NGql.Core.Extensions; |
| | | 5 | | |
| | | 6 | | namespace NGql.Core.Features; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Handles merging of query definitions using different strategies |
| | | 10 | | /// </summary> |
| | | 11 | | internal 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 | | { |
| | 348 | 22 | | if (incomingQuery._fields == null || incomingQuery._fields.Count == 0) return; |
| | | 23 | | |
| | 342 | 24 | | MergeVariables(targetDefinition, incomingQuery); |
| | | 25 | | |
| | 342 | 26 | | var beforeCount = targetDefinition.Fields.Count; |
| | 342 | 27 | | ApplyFieldMerge(targetDefinition.Fields, incomingQuery, targetDefinition.MergingStrategy, queryMap); |
| | | 28 | | |
| | 330 | 29 | | if (targetDefinition.Fields.Count != beforeCount) |
| | | 30 | | { |
| | 189 | 31 | | queryMap.UpdateRootMapping(targetDefinition); |
| | | 32 | | } |
| | 330 | 33 | | } |
| | | 34 | | |
| | | 35 | | private static void MergeVariables(QueryDefinition targetDefinition, in QueryDefinition incomingQuery) |
| | | 36 | | { |
| | 342 | 37 | | var incomingVars = incomingQuery._variables; |
| | 678 | 38 | | if (incomingVars is null || incomingVars.Count == 0) return; |
| | | 39 | | |
| | 6 | 40 | | var targetVars = targetDefinition._variables; |
| | 6 | 41 | | if (targetVars is null) |
| | | 42 | | { |
| | 3 | 43 | | targetDefinition.Variables = new SortedSet<Variable>(incomingVars); |
| | 3 | 44 | | return; |
| | | 45 | | } |
| | | 46 | | |
| | 12 | 47 | | foreach (var v in incomingVars) |
| | 3 | 48 | | targetVars.Add(v); |
| | 3 | 49 | | } |
| | | 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 | | { |
| | 342 | 62 | | var strategy = GetEffectiveMergingStrategy(rootStrategy, incomingQuery.MergingStrategy); |
| | 342 | 63 | | var queryName = incomingQuery.Name; |
| | | 64 | | |
| | 1362 | 65 | | foreach (var (originalFieldKey, incomingField) in incomingQuery.Fields) |
| | | 66 | | { |
| | | 67 | | switch (strategy) |
| | | 68 | | { |
| | | 69 | | case MergingStrategy.MergeByDefault: |
| | 54 | 70 | | FieldBuilder.Include(fields, incomingField); |
| | 54 | 71 | | queryMap.SetMapping(queryName, originalFieldKey); |
| | 54 | 72 | | break; |
| | | 73 | | |
| | | 74 | | case MergingStrategy.NeverMerge: |
| | 54 | 75 | | AddFieldWithUniqueKey(fields, originalFieldKey, MarkAsNeverMerge(incomingField), queryMap, queryName |
| | 54 | 76 | | break; |
| | | 77 | | |
| | | 78 | | case MergingStrategy.MergeByFieldPath: |
| | 234 | 79 | | ApplyMergeByFieldPath(fields, originalFieldKey, incomingField, queryMap, queryName); |
| | 225 | 80 | | break; |
| | | 81 | | |
| | | 82 | | default: |
| | 3 | 83 | | throw new ArgumentOutOfRangeException(nameof(rootStrategy), $"Merging strategy {strategy} is not imp |
| | | 84 | | } |
| | | 85 | | } |
| | 330 | 86 | | } |
| | | 87 | | |
| | | 88 | | private static void ApplyMergeByFieldPath( |
| | | 89 | | Dictionary<string, FieldDefinition> fields, |
| | | 90 | | string originalFieldKey, |
| | | 91 | | FieldDefinition incomingField, |
| | | 92 | | QueryMap queryMap, |
| | | 93 | | string queryName) |
| | | 94 | | { |
| | 234 | 95 | | var mergeTarget = FindMergeTarget(fields, incomingField); |
| | | 96 | | |
| | 234 | 97 | | if (mergeTarget == null) |
| | | 98 | | { |
| | 111 | 99 | | AddFieldWithUniqueKey(fields, originalFieldKey, incomingField, queryMap, queryName); |
| | 111 | 100 | | 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. |
| | 123 | 107 | | FieldDefinitionExtensions.MergeFieldsInPlace(mergeTarget.Value.Field, incomingField); |
| | 114 | 108 | | queryMap.SetMapping(queryName, mergeTarget.Value.Key); |
| | 114 | 109 | | } |
| | 9 | 110 | | catch (QueryMergeException ex) |
| | | 111 | | { |
| | 9 | 112 | | throw new QueryMergeException($"Cannot merge query '{queryName}' due to type conflicts in field '{incomingFi |
| | | 113 | | } |
| | 114 | 114 | | } |
| | | 115 | | |
| | | 116 | | private static MergingStrategy GetEffectiveMergingStrategy(MergingStrategy rootStrategy, MergingStrategy childStrate |
| | | 117 | | { |
| | 342 | 118 | | if (childStrategy == MergingStrategy.NeverMerge) |
| | | 119 | | { |
| | 36 | 120 | | return MergingStrategy.NeverMerge; |
| | | 121 | | } |
| | | 122 | | |
| | 306 | 123 | | return rootStrategy switch |
| | 306 | 124 | | { |
| | 18 | 125 | | MergingStrategy.NeverMerge => MergingStrategy.NeverMerge, |
| | 54 | 126 | | MergingStrategy.MergeByDefault => childStrategy, |
| | 234 | 127 | | _ => rootStrategy, |
| | 306 | 128 | | }; |
| | | 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 | | { |
| | 165 | 138 | | 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. |
| | 165 | 143 | | var fieldToAdd = incomingField.DeepClone(); |
| | 165 | 144 | | if (!string.Equals(uniqueKey, originalFieldKey, StringComparison.OrdinalIgnoreCase)) |
| | | 145 | | { |
| | 123 | 146 | | fieldToAdd = fieldToAdd with { Alias = uniqueKey, _effectiveName = uniqueKey }; |
| | | 147 | | } |
| | | 148 | | |
| | 165 | 149 | | fields[uniqueKey] = fieldToAdd; |
| | 165 | 150 | | queryMap.SetMapping(queryName, uniqueKey); |
| | 165 | 151 | | } |
| | | 152 | | |
| | | 153 | | private static (string Key, FieldDefinition Field)? FindMergeTarget(Dictionary<string, FieldDefinition> existingFiel |
| | | 154 | | { |
| | 765 | 155 | | foreach (var (key, existingField) in existingFields) |
| | | 156 | | { |
| | 210 | 157 | | if (!string.Equals(existingField.Name, incomingField.Name, StringComparison.OrdinalIgnoreCase)) |
| | | 158 | | continue; |
| | | 159 | | |
| | 204 | 160 | | if (existingField.IsNeverMerge) |
| | | 161 | | continue; |
| | | 162 | | |
| | 192 | 163 | | if (FieldDefinitionExtensions.CanMergeFields(existingField, incomingField)) |
| | 123 | 164 | | return (key, existingField); |
| | | 165 | | } |
| | | 166 | | |
| | 111 | 167 | | return null; |
| | 123 | 168 | | } |
| | | 169 | | |
| | | 170 | | private static FieldDefinition MarkAsNeverMerge(FieldDefinition field) |
| | 54 | 171 | | => field.IsNeverMerge ? field : field with { IsNeverMerge = true }; |
| | | 172 | | } |