| | | 1 | | using System.Runtime.CompilerServices; |
| | | 2 | | using NGql.Core.Abstractions; |
| | | 3 | | using NGql.Core.Extensions; |
| | | 4 | | using NGql.Core.Features; |
| | | 5 | | |
| | | 6 | | namespace NGql.Core.Builders; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Represents a query builder. |
| | | 10 | | /// </summary> |
| | | 11 | | public sealed class QueryBuilder |
| | | 12 | | { |
| | | 13 | | /// <summary> |
| | | 14 | | /// The query definition that this builder is working with. |
| | | 15 | | /// </summary> |
| | 50241 | 16 | | public QueryDefinition Definition => _definition; |
| | | 17 | | |
| | | 18 | | /// <inheritdoc cref="QueryBlock.Variables"/> |
| | 45 | 19 | | public IEnumerable<Variable> Variables => Definition.Variables; |
| | | 20 | | |
| | | 21 | | /// <summary> |
| | | 22 | | /// Maps original query names to their merged definition names. |
| | | 23 | | /// </summary> |
| | | 24 | | private QueryMap? _queryMap; |
| | 16770 | 25 | | private QueryMap QueryMapInstance => _queryMap ??= new(); |
| | | 26 | | |
| | | 27 | | private readonly QueryDefinition _definition; |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// Caches paths to fields for O(1) lookup in GetPathTo(). |
| | | 31 | | /// Maps field name/alias → string[] path segments from root. |
| | | 32 | | /// </summary> |
| | | 33 | | /// <summary> |
| | | 34 | | /// Two-level path cache: <c>rootPath → (nodePath → segments)</c>. The two-level structure avoids |
| | | 35 | | /// allocating a concatenated <c>"{root}.{node}"</c> string on every <c>GetPathTo</c> cache hit. |
| | | 36 | | /// </summary> |
| | 9510 | 37 | | private readonly Dictionary<string, Dictionary<string, string[]>> _pathIndex = new(); |
| | | 38 | | |
| | 19020 | 39 | | private QueryBuilder(QueryDefinition queryDefinition) => _definition = queryDefinition; |
| | | 40 | | |
| | | 41 | | /// <summary> |
| | | 42 | | /// Creates a new instance of <see cref="QueryBuilder"/>. |
| | | 43 | | /// </summary> |
| | | 44 | | /// <param name="name">The name of the query.</param> |
| | | 45 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | 9165 | 46 | | public static QueryBuilder CreateDefaultBuilder(string name) => new(new QueryDefinition(name)); |
| | | 47 | | |
| | | 48 | | /// <summary> |
| | | 49 | | /// Creates a new instance of <see cref="QueryBuilder"/> with a specific merging strategy. |
| | | 50 | | /// </summary> |
| | | 51 | | /// <param name="name">The name of the query.</param> |
| | | 52 | | /// <param name="mergingStrategy">The merging strategy to use.</param> |
| | | 53 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 54 | | public static QueryBuilder CreateDefaultBuilder(string name, MergingStrategy mergingStrategy) |
| | | 55 | | { |
| | 312 | 56 | | var definition = new QueryDefinition(name) { MergingStrategy = mergingStrategy }; |
| | 312 | 57 | | return new(definition); |
| | | 58 | | } |
| | | 59 | | |
| | | 60 | | /// <summary> |
| | | 61 | | /// Creates a new <see cref="QueryBuilder"/> that renders as a GraphQL <c>mutation</c>. |
| | | 62 | | /// The fluent surface (<c>AddField</c>, <c>Include</c>, etc.) is identical to the |
| | | 63 | | /// query path; only the operation prefix differs at render time. |
| | | 64 | | /// </summary> |
| | | 65 | | /// <param name="name">The name of the mutation.</param> |
| | | 66 | | /// <returns>Instance of <see cref="QueryBuilder"/> in mutation mode.</returns> |
| | | 67 | | public static QueryBuilder CreateMutationBuilder(string name) |
| | | 68 | | { |
| | 15 | 69 | | var definition = new QueryDefinition(name) { OperationType = OperationType.Mutation }; |
| | 15 | 70 | | return new(definition); |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | /// <summary> |
| | | 74 | | /// Creates a new <see cref="QueryBuilder"/> that renders as a GraphQL <c>mutation</c>, |
| | | 75 | | /// with a specific merging strategy. |
| | | 76 | | /// </summary> |
| | | 77 | | /// <param name="name">The name of the mutation.</param> |
| | | 78 | | /// <param name="mergingStrategy">The merging strategy to use.</param> |
| | | 79 | | /// <returns>Instance of <see cref="QueryBuilder"/> in mutation mode.</returns> |
| | | 80 | | public static QueryBuilder CreateMutationBuilder(string name, MergingStrategy mergingStrategy) |
| | | 81 | | { |
| | 3 | 82 | | var definition = new QueryDefinition(name) |
| | 3 | 83 | | { |
| | 3 | 84 | | OperationType = OperationType.Mutation, |
| | 3 | 85 | | MergingStrategy = mergingStrategy, |
| | 3 | 86 | | }; |
| | 3 | 87 | | return new(definition); |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | /// <summary> |
| | | 91 | | /// Sets the merging strategy for this query builder. |
| | | 92 | | /// </summary> |
| | | 93 | | /// <param name="strategy">The merging strategy to use.</param> |
| | | 94 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 95 | | public QueryBuilder WithMergingStrategy(MergingStrategy strategy) |
| | | 96 | | { |
| | 12 | 97 | | Definition.MergingStrategy = strategy; |
| | 12 | 98 | | return this; |
| | | 99 | | } |
| | | 100 | | |
| | | 101 | | /// <summary> |
| | | 102 | | /// Creates a new instance of <see cref="QueryBuilder"/>. |
| | | 103 | | /// </summary> |
| | | 104 | | /// <param name="queryDefinition">The query definition.</param> |
| | | 105 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | 15 | 106 | | public static QueryBuilder CreateFromDefinition(QueryDefinition queryDefinition) => new(queryDefinition); |
| | | 107 | | |
| | | 108 | | /// <summary> |
| | | 109 | | /// Adds a field to the query. |
| | | 110 | | /// </summary> |
| | | 111 | | /// <param name="field">Field name or path.</param> |
| | | 112 | | /// <param name="arguments">The arguments for the field.</param> |
| | | 113 | | /// <param name="metadata">The field metadata.</param> |
| | | 114 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 115 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 116 | | public QueryBuilder AddField(string field, Dictionary<string, object?>? arguments = null, Dictionary<string, object? |
| | | 117 | | { |
| | | 118 | | // FAST PATH: Most common case - no arguments, no metadata |
| | 25665 | 119 | | if (arguments is null && metadata is null) |
| | | 120 | | { |
| | 18552 | 121 | | return AddFieldFastPath(field); |
| | | 122 | | } |
| | | 123 | | |
| | 7113 | 124 | | if (arguments?.Count > 0) |
| | | 125 | | { |
| | 7038 | 126 | | SortedDictionary<string, object?>? sortedArgs = new SortedDictionary<string, object?>(arguments, StringCompa |
| | 7038 | 127 | | return AddFieldCore(field, sortedArgs, null, metadata); |
| | | 128 | | } |
| | 75 | 129 | | return AddFieldCore(field, null, null, metadata); |
| | | 130 | | } |
| | | 131 | | |
| | | 132 | | /// <summary> |
| | | 133 | | /// Adds a field to the query. |
| | | 134 | | /// </summary> |
| | | 135 | | /// <param name="field">Field name or path.</param> |
| | | 136 | | /// <param name="subFields">The subfields for the field.</param> |
| | | 137 | | /// <param name="metadata">The field metadata.</param> |
| | | 138 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 139 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 140 | | public QueryBuilder AddField(string field, string[]? subFields, Dictionary<string, object?>? metadata = null) |
| | 885 | 141 | | => AddFieldCore(field, null, subFields?.Select(subField => new FieldDefinition(subField)), metadata); |
| | | 142 | | |
| | | 143 | | /// <summary> |
| | | 144 | | /// Adds a field to the query. |
| | | 145 | | /// </summary> |
| | | 146 | | /// <param name="field">Field name or path.</param> |
| | | 147 | | /// <param name="subFields">The subfields for the field.</param> |
| | | 148 | | /// <param name="metadata">The field metadata.</param> |
| | | 149 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 150 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 151 | | public QueryBuilder AddField(string field, FieldDefinition[]? subFields, Dictionary<string, object?>? metadata = nul |
| | 39 | 152 | | => AddFieldCore(field, null, subFields, metadata); |
| | | 153 | | |
| | | 154 | | /// <summary> |
| | | 155 | | /// Adds a field to the query using a field builder. |
| | | 156 | | /// </summary> |
| | | 157 | | /// <param name="field">Field name or path.</param> |
| | | 158 | | /// <param name="fieldBuilder">The field builder action.</param> |
| | | 159 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 160 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 161 | | /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception> |
| | | 162 | | public QueryBuilder AddField(string field, Action<FieldBuilder> fieldBuilder) |
| | | 163 | | { |
| | 237 | 164 | | ArgumentNullException.ThrowIfNull(fieldBuilder); |
| | 231 | 165 | | return AddFieldBuilderCore(field, Constants.DefaultFieldType, null, null, fieldBuilder); |
| | | 166 | | } |
| | | 167 | | |
| | | 168 | | /// <summary> |
| | | 169 | | /// Adds a field to the query. |
| | | 170 | | /// </summary> |
| | | 171 | | /// <param name="field">Field name or path.</param> |
| | | 172 | | /// <param name="arguments">The arguments for the field.</param> |
| | | 173 | | /// <param name="subFields">The subfields for the field.</param> |
| | | 174 | | /// <param name="metadata">The field metadata.</param> |
| | | 175 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 176 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 177 | | public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, string[] subFields, Dictionary<str |
| | | 178 | | { |
| | 135 | 179 | | SortedDictionary<string, object?>? sortedArgs = arguments?.Count > 0 |
| | 135 | 180 | | ? new SortedDictionary<string, object?>(arguments, StringComparer.OrdinalIgnoreCase) |
| | 135 | 181 | | : null; |
| | | 182 | | // Signature declares subFields non-nullable; the public API contract requires non-null. |
| | 291 | 183 | | return AddFieldCore(field, sortedArgs, subFields.Select(subField => new FieldDefinition(subField)), metadata); |
| | | 184 | | } |
| | | 185 | | |
| | | 186 | | /// <summary> |
| | | 187 | | /// Adds a field to the query. |
| | | 188 | | /// </summary> |
| | | 189 | | /// <param name="field">Field name or path.</param> |
| | | 190 | | /// <param name="arguments">The arguments for the field.</param> |
| | | 191 | | /// <param name="subFields">The subfields for the field.</param> |
| | | 192 | | /// <param name="metadata">The field metadata.</param> |
| | | 193 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 194 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 195 | | public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, FieldDefinition[] subFields, Dicti |
| | | 196 | | { |
| | 15 | 197 | | SortedDictionary<string, object?>? sortedArgs = arguments?.Count > 0 |
| | 15 | 198 | | ? new SortedDictionary<string, object?>(arguments, StringComparer.OrdinalIgnoreCase) |
| | 15 | 199 | | : null; |
| | 15 | 200 | | return AddFieldCore(field, sortedArgs, subFields, metadata); |
| | | 201 | | } |
| | | 202 | | |
| | | 203 | | /// <summary> |
| | | 204 | | /// Adds a field with a specific type to the query. |
| | | 205 | | /// </summary> |
| | | 206 | | /// <param name="field">Field name or path</param> |
| | | 207 | | /// <param name="type">The field type</param> |
| | | 208 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 209 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 210 | | public QueryBuilder AddField(string field, string type) |
| | 207 | 211 | | => AddFieldBuilderCore(field, type, null, null, _ => { }); |
| | | 212 | | |
| | | 213 | | /// <summary> |
| | | 214 | | /// Adds a field with a specific type to the query. |
| | | 215 | | /// </summary> |
| | | 216 | | /// <param name="field">Field name or path</param> |
| | | 217 | | /// <param name="type">The field type</param> |
| | | 218 | | /// <param name="metadata">The field metadata</param> |
| | | 219 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 220 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 221 | | public QueryBuilder AddField(string field, string type, Dictionary<string, object?>? metadata) |
| | 78 | 222 | | => AddFieldBuilderCore(field, type, null, metadata, _ => { }); |
| | | 223 | | |
| | | 224 | | /// <summary> |
| | | 225 | | /// Adds a field to the query using a field builder with arguments. |
| | | 226 | | /// </summary> |
| | | 227 | | /// <param name="field">Field name or path</param> |
| | | 228 | | /// <param name="arguments">The arguments for the field</param> |
| | | 229 | | /// <param name="metadata">The field metadata</param> |
| | | 230 | | /// <param name="fieldBuilder">The field builder action</param> |
| | | 231 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 232 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 233 | | /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception> |
| | | 234 | | public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, Dictionary<string, object?>? metad |
| | | 235 | | { |
| | 42 | 236 | | ArgumentNullException.ThrowIfNull(fieldBuilder); |
| | 42 | 237 | | SortedDictionary<string, object?>? sortedArgs = arguments?.Count > 0 |
| | 42 | 238 | | ? new SortedDictionary<string, object?>(arguments, StringComparer.OrdinalIgnoreCase) |
| | 42 | 239 | | : null; |
| | 42 | 240 | | return AddFieldBuilderCore(field, Constants.DefaultFieldType, sortedArgs, metadata, fieldBuilder); |
| | | 241 | | } |
| | | 242 | | |
| | | 243 | | /// <summary> |
| | | 244 | | /// Adds a field to the query using a field builder with a specific type. |
| | | 245 | | /// </summary> |
| | | 246 | | /// <param name="field">Field name or path</param> |
| | | 247 | | /// <param name="type">The field type</param> |
| | | 248 | | /// <param name="metadata">The field metadata</param> |
| | | 249 | | /// <param name="fieldBuilder">The field builder action</param> |
| | | 250 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 251 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 252 | | /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception> |
| | | 253 | | public QueryBuilder AddField(string field, string type, Dictionary<string, object?>? metadata, Action<FieldBuilder> |
| | 9 | 254 | | => AddFieldBuilderCore(field, type, null, metadata, fieldBuilder); |
| | | 255 | | |
| | | 256 | | /// <summary> |
| | | 257 | | /// Adds a field to the query using a field builder with arguments. Convenience overload — |
| | | 258 | | /// equivalent to passing <c>metadata: null</c> to the four-arg form. |
| | | 259 | | /// </summary> |
| | | 260 | | /// <param name="field">Field name or path</param> |
| | | 261 | | /// <param name="arguments">The arguments for the field</param> |
| | | 262 | | /// <param name="fieldBuilder">The field builder action</param> |
| | | 263 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 264 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 265 | | /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception> |
| | | 266 | | public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, Action<FieldBuilder> fieldBuilder) |
| | 9 | 267 | | => AddField(field, arguments, metadata: null, fieldBuilder); |
| | | 268 | | |
| | | 269 | | /// <summary> |
| | | 270 | | /// Adds a field with explicit sub-field names and a field builder action — useful when |
| | | 271 | | /// the caller wants both static sub-fields AND a chance to add nested structure via the builder. |
| | | 272 | | /// </summary> |
| | | 273 | | /// <param name="field">Field name or path</param> |
| | | 274 | | /// <param name="subFields">The static sub-field names to seed into the field</param> |
| | | 275 | | /// <param name="fieldBuilder">The field builder action invoked after the static sub-fields are added</param> |
| | | 276 | | /// <returns>Instance of <see cref="QueryBuilder"/>.</returns> |
| | | 277 | | /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception> |
| | | 278 | | /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception> |
| | | 279 | | public QueryBuilder AddField(string field, string[] subFields, Action<FieldBuilder> fieldBuilder) |
| | | 280 | | { |
| | 3 | 281 | | ArgumentNullException.ThrowIfNull(fieldBuilder); |
| | 3 | 282 | | return AddField(field, b => |
| | 3 | 283 | | { |
| | 18 | 284 | | foreach (var sub in subFields) |
| | 3 | 285 | | { |
| | 6 | 286 | | b.AddField(sub); |
| | 3 | 287 | | } |
| | 3 | 288 | | |
| | 3 | 289 | | fieldBuilder(b); |
| | 6 | 290 | | }); |
| | | 291 | | } |
| | | 292 | | |
| | | 293 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 294 | | private QueryBuilder AddFieldFastPath(string field) |
| | | 295 | | { |
| | 18552 | 296 | | if (string.IsNullOrWhiteSpace(field)) |
| | | 297 | | { |
| | 21 | 298 | | throw new ArgumentException("Field cannot be null or empty", nameof(field)); |
| | | 299 | | } |
| | | 300 | | |
| | | 301 | | // ULTRA FAST PATH: Direct field creation for simple cases |
| | 18531 | 302 | | var fieldSpan = field.AsSpan(); |
| | 18531 | 303 | | if (fieldSpan.IsSimpleField()) |
| | | 304 | | { |
| | | 305 | | // Bypass FieldBuilder.Create for maximum performance |
| | 4086 | 306 | | if (!Definition.Fields.ContainsKey(field)) |
| | | 307 | | { |
| | 4053 | 308 | | Definition.Fields[field] = new FieldDefinition(field, Constants.DefaultFieldType) |
| | 4053 | 309 | | { |
| | 4053 | 310 | | Path = field |
| | 4053 | 311 | | }; |
| | | 312 | | } |
| | | 313 | | } |
| | | 314 | | else |
| | | 315 | | { |
| | | 316 | | // Fallback to standard processing for complex fields |
| | 14445 | 317 | | FieldBuilder.Create(Definition.Fields, field, Constants.DefaultFieldType, null, null); |
| | | 318 | | } |
| | | 319 | | |
| | | 320 | | // Phase 3: Invalidate caches after field addition |
| | 18531 | 321 | | InvalidateLookupCaches(); |
| | | 322 | | |
| | | 323 | | // Defer UpdateRootMapping - will be called when query is built/used |
| | 18531 | 324 | | return this; |
| | | 325 | | } |
| | | 326 | | |
| | | 327 | | /// <summary> |
| | | 328 | | /// Core implementation for adding fields using FieldBuilder pattern. |
| | | 329 | | /// </summary> |
| | | 330 | | /// <param name="field">Field name or path</param> |
| | | 331 | | /// <param name="fieldType">The field type</param> |
| | | 332 | | /// <param name="arguments">Optional arguments dictionary</param> |
| | | 333 | | /// <param name="metadata">Optional metadata dictionary</param> |
| | | 334 | | /// <param name="fieldBuilder">The field builder action</param> |
| | | 335 | | /// <returns>Current QueryBuilder instance for method chaining</returns> |
| | | 336 | | private QueryBuilder AddFieldBuilderCore(string field, string fieldType, SortedDictionary<string, object?>? argument |
| | | 337 | | { |
| | 426 | 338 | | if (string.IsNullOrWhiteSpace(field)) |
| | | 339 | | { |
| | 3 | 340 | | throw new ArgumentException("Field cannot be null or empty", nameof(field)); |
| | | 341 | | } |
| | | 342 | | |
| | 423 | 343 | | ArgumentNullException.ThrowIfNull(fieldBuilder); |
| | | 344 | | |
| | | 345 | | // FAST PATH: Only extract variables if arguments has content |
| | 423 | 346 | | if (arguments?.Count > 0) |
| | | 347 | | { |
| | 36 | 348 | | Helpers.ExtractVariablesFromValue(arguments, Definition.Variables); |
| | | 349 | | } |
| | | 350 | | |
| | | 351 | | // Use the provided field type |
| | 423 | 352 | | var builder = FieldBuilder.Create(Definition.Fields, field, fieldType, arguments, metadata); |
| | 423 | 353 | | fieldBuilder(builder); |
| | | 354 | | |
| | 411 | 355 | | QueryMapInstance.UpdateRootMapping(_definition); |
| | 411 | 356 | | return this; |
| | | 357 | | } |
| | | 358 | | |
| | | 359 | | /// <summary> |
| | | 360 | | /// Merges <paramref name="queryBuilder"/>'s fields and variables into this builder. |
| | | 361 | | /// Merge behavior is governed by this builder's <see cref="MergingStrategy"/>: |
| | | 362 | | /// <see cref="NGql.Core.MergingStrategy.MergeByDefault"/> appends fragments, |
| | | 363 | | /// <see cref="NGql.Core.MergingStrategy.MergeByFieldPath"/> merges compatible same-path |
| | | 364 | | /// fields and auto-aliases on argument conflict, and |
| | | 365 | | /// <see cref="NGql.Core.MergingStrategy.NeverMerge"/> always aliases the included fields |
| | | 366 | | /// as <c>name_1</c>, <c>name_2</c>, … |
| | | 367 | | /// </summary> |
| | | 368 | | /// <param name="queryBuilder">Builder whose fields will be merged into this one.</param> |
| | | 369 | | /// <returns>This builder, for chaining.</returns> |
| | 345 | 370 | | public QueryBuilder Include(QueryBuilder queryBuilder) => IncludeImpl(queryBuilder.Definition); |
| | | 371 | | |
| | | 372 | | /// <summary> |
| | | 373 | | /// Core implementation for adding fields with optional arguments. |
| | | 374 | | /// </summary> |
| | | 375 | | /// <param name="field">Field name or path</param> |
| | | 376 | | /// <param name="arguments">Optional arguments dictionary</param> |
| | | 377 | | /// <param name="subFields">Optional array of sub-field definitions</param> |
| | | 378 | | /// <param name="metadata">Optional metadata dictionary</param> |
| | | 379 | | /// <returns>Current QueryBuilder instance for method chaining</returns> |
| | | 380 | | private QueryBuilder AddFieldCore(string field, SortedDictionary<string, object?>? arguments, IEnumerable<FieldDefin |
| | | 381 | | { |
| | 7662 | 382 | | if (string.IsNullOrWhiteSpace(field)) |
| | 18 | 383 | | throw new ArgumentException("Field cannot be null or empty", nameof(field)); |
| | | 384 | | |
| | 7644 | 385 | | var hasSubFields = subFields?.Any() == true; |
| | | 386 | | |
| | 7644 | 387 | | if (arguments is { Count: > 0 }) |
| | 7164 | 388 | | Helpers.ExtractVariablesFromValue(arguments, Definition.Variables); |
| | | 389 | | |
| | 7644 | 390 | | var type = hasSubFields ? Constants.ObjectFieldType : Constants.DefaultFieldType; |
| | 7644 | 391 | | var builder = FieldBuilder.Create(Definition.Fields, field, type, arguments, metadata); |
| | | 392 | | |
| | 7644 | 393 | | if (!hasSubFields) |
| | | 394 | | { |
| | 7125 | 395 | | QueryMapInstance.UpdateRootMapping(_definition); |
| | | 396 | | // Phase 3: Invalidate caches after field addition |
| | 7125 | 397 | | InvalidateLookupCaches(); |
| | 7125 | 398 | | return this; |
| | | 399 | | } |
| | | 400 | | |
| | 2553 | 401 | | foreach (var subField in subFields!) |
| | 759 | 402 | | builder.AddField(subField); |
| | | 403 | | |
| | 516 | 404 | | QueryMapInstance.UpdateRootMapping(_definition); |
| | | 405 | | // Phase 3: Invalidate caches after field addition |
| | 516 | 406 | | InvalidateLookupCaches(); |
| | 516 | 407 | | return this; |
| | | 408 | | } |
| | | 409 | | |
| | | 410 | | /// <summary> |
| | | 411 | | /// Core implementation for including another query definition with optimized parameter passing. |
| | | 412 | | /// </summary> |
| | | 413 | | /// <param name="queryDefinition">Query definition to include (passed by reference for performance)</param> |
| | | 414 | | /// <returns>Current QueryBuilder instance for method chaining</returns> |
| | | 415 | | private QueryBuilder IncludeImpl(in QueryDefinition queryDefinition) |
| | | 416 | | { |
| | 345 | 417 | | QueryMerger.MergeQuery(_definition, QueryMapInstance, this, in queryDefinition); |
| | | 418 | | |
| | | 419 | | // Phase 3: Invalidate lookup caches after merge since fields changed |
| | 333 | 420 | | InvalidateLookupCaches(); |
| | | 421 | | |
| | 333 | 422 | | return this; |
| | | 423 | | } |
| | | 424 | | |
| | | 425 | | /// <summary> |
| | | 426 | | /// Phase 3 optimization: Invalidates both path and field lookup caches. |
| | | 427 | | /// Called after any modification to Definition.Fields to ensure cache consistency. |
| | | 428 | | /// </summary> |
| | | 429 | | private void InvalidateLookupCaches() |
| | | 430 | | { |
| | 26505 | 431 | | _pathIndex.Clear(); |
| | 26505 | 432 | | } |
| | | 433 | | |
| | | 434 | | /// <summary> |
| | | 435 | | /// Merges <paramref name="metadata"/> into the query definition's metadata bag, deeply |
| | | 436 | | /// combining nested dictionaries. Existing keys are overwritten by <paramref name="metadata"/> |
| | | 437 | | /// only when the new value is not itself a dictionary; nested dictionaries are recursively |
| | | 438 | | /// merged. |
| | | 439 | | /// </summary> |
| | | 440 | | /// <param name="metadata">Metadata to merge in.</param> |
| | | 441 | | /// <returns>This builder, for chaining.</returns> |
| | | 442 | | public QueryBuilder WithMetadata(Dictionary<string, object> metadata) |
| | | 443 | | { |
| | 69 | 444 | | var mergedMetadata = Helpers.MergeMetadata(_definition._metadata, metadata); |
| | 69 | 445 | | _definition.Metadata = mergedMetadata; |
| | | 446 | | |
| | 69 | 447 | | return this; |
| | | 448 | | } |
| | | 449 | | |
| | | 450 | | /// <summary> |
| | | 451 | | /// Gets the path segments to reach a specific node within a query. |
| | | 452 | | /// </summary> |
| | | 453 | | /// <param name="queryName">The name of the query to find the path for.</param> |
| | | 454 | | /// <param name="nodePath">The optional node path within the query (e.g., "edges.node").</param> |
| | | 455 | | /// <returns>An array of path segments to reach the specified node.</returns> |
| | | 456 | | public string[] GetPathTo(string queryName, string? nodePath = null) |
| | 306 | 457 | | => QueryMapInstance.GetPathTo(queryName, nodePath, _definition, _pathIndex); |
| | | 458 | | |
| | | 459 | | /// <summary> |
| | | 460 | | /// Gets the count of fields in the QueryDefinition. |
| | | 461 | | /// This represents the correct value with or without merge. |
| | | 462 | | /// </summary> |
| | 45 | 463 | | internal int DefinitionsCount => Definition.Fields.Count; |
| | | 464 | | |
| | | 465 | | /// <inheritdoc cref="QueryBlock.ToString()"/> |
| | | 466 | | public override string ToString() |
| | | 467 | | { |
| | 8067 | 468 | | QueryMapInstance.UpdateRootMapping(_definition); |
| | 8067 | 469 | | return Definition.ToString(); |
| | | 470 | | } |
| | | 471 | | |
| | 270 | 472 | | public static implicit operator string(QueryBuilder query) => query.ToString(); |
| | | 473 | | } |