From b47c2aa86f716cf075a021d3303046764fd8ce48 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 7 Jul 2026 19:54:46 -0500 Subject: [PATCH] chore: Deprecate field execution strategy customization The only real logical choices are serial or concurrent (which are deterministic via the spec), and the graphql-js reference implementation doesn't allow customization. --- Sources/GraphQL/Execution/Execute.swift | 161 +++++++++++++++--------- 1 file changed, 105 insertions(+), 56 deletions(-) diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 5db3ff83..c5abd222 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -26,10 +26,6 @@ import OrderedCollections /// Namely, schema of the type system that is currently executing, /// and the fragments defined in the query document public final class ExecutionContext: @unchecked Sendable { - let queryStrategy: QueryFieldExecutionStrategy = ConcurrentFieldExecutionStrategy() - let mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy() - let subscriptionStrategy: SubscriptionFieldExecutionStrategy = - ConcurrentFieldExecutionStrategy() public let schema: GraphQLSchema public let fragments: [String: FragmentDefinition] public let rootValue: any Sendable @@ -84,6 +80,7 @@ public final class ExecutionContext: @unchecked Sendable { } } +@available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public protocol FieldExecutionStrategy: Sendable { func executeFields( exeContext: ExecutionContext, @@ -94,16 +91,24 @@ public protocol FieldExecutionStrategy: Sendable { ) async throws -> OrderedDictionary } +@available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {} + +@available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public protocol QueryFieldExecutionStrategy: FieldExecutionStrategy {} + +@available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public protocol SubscriptionFieldExecutionStrategy: FieldExecutionStrategy {} /// Serial field execution strategy that's suitable for the "Evaluating selection sets" section of the spec for "write" mode. +@available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, MutationFieldExecutionStrategy, SubscriptionFieldExecutionStrategy { + @available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public init() {} + @available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, @@ -111,29 +116,24 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, path: IndexPath, fields: OrderedDictionary ) async throws -> OrderedDictionary { - var results = OrderedDictionary() - for field in fields { - let fieldASTs = field.value - let fieldPath = path.appending(field.key) - results[field.key] = - try await resolveField( - exeContext: exeContext, - parentType: parentType, - source: sourceValue, - fieldASTs: fieldASTs, - path: fieldPath - ) ?? Map.null - } - return results + return try await GraphQL.executeFieldsSerially( + exeContext: exeContext, + parentType: parentType, + sourceValue: sourceValue, + path: path, + fields: fields + ) } } /// Serial field execution strategy that's suitable for the "Evaluating selection sets" section of the spec for "read" mode. /// /// Each field is resolved as an individual task on a concurrent dispatch queue. +@available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public struct ConcurrentFieldExecutionStrategy: QueryFieldExecutionStrategy, SubscriptionFieldExecutionStrategy { + @available(*, deprecated, message: "FieldExecutionStrategy type no longer supported") public func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, @@ -141,31 +141,13 @@ public struct ConcurrentFieldExecutionStrategy: QueryFieldExecutionStrategy, path: IndexPath, fields: OrderedDictionary ) async throws -> OrderedDictionary { - return try await withThrowingTaskGroup(of: (String, (any Sendable)?).self) { group in - // preserve field order by assigning to null and filtering later - var results: OrderedDictionary = - fields - .mapValues { _ -> Any? in nil } - for field in fields { - group.addTask { - let fieldASTs = field.value - let fieldPath = path.appending(field.key) - let result = - try await resolveField( - exeContext: exeContext, - parentType: parentType, - source: sourceValue, - fieldASTs: fieldASTs, - path: fieldPath - ) ?? Map.null - return (field.key, result) - } - } - for try await result in group { - results[result.0] = result.1 - } - return results.compactMapValues { $0 } - } + return try await GraphQL.executeFields( + exeContext: exeContext, + parentType: parentType, + sourceValue: sourceValue, + path: path, + fields: fields + ) } } @@ -311,24 +293,91 @@ func executeOperation( visitedFragmentNames: &visitedFragmentNames ) - let fieldExecutionStrategy: FieldExecutionStrategy - switch operation.operation { case .query: - fieldExecutionStrategy = exeContext.queryStrategy + return try await executeFields( + exeContext: exeContext, + parentType: type, + sourceValue: rootValue, + path: [], + fields: fields + ) case .mutation: - fieldExecutionStrategy = exeContext.mutationStrategy + return try await executeFieldsSerially( + exeContext: exeContext, + parentType: type, + sourceValue: rootValue, + path: [], + fields: fields + ) case .subscription: - fieldExecutionStrategy = exeContext.subscriptionStrategy + return try await executeFields( + exeContext: exeContext, + parentType: type, + sourceValue: rootValue, + path: [], + fields: fields + ) + } +} + +/// Implements the "Executing selection sets" section of the spec for fields that must be executed serially. +func executeFieldsSerially( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: any Sendable, + path: IndexPath, + fields: OrderedDictionary +) async throws -> OrderedDictionary { + var results = OrderedDictionary() + for field in fields { + let fieldASTs = field.value + let fieldPath = path.appending(field.key) + results[field.key] = + try await resolveField( + exeContext: exeContext, + parentType: parentType, + source: sourceValue, + fieldASTs: fieldASTs, + path: fieldPath + ) ?? Map.null } + return results +} - return try await fieldExecutionStrategy.executeFields( - exeContext: exeContext, - parentType: type, - sourceValue: rootValue, - path: [], - fields: fields - ) +/// Implements the "Executing selection sets" section of the spec for fields that may be executed in parallel. +func executeFields( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: any Sendable, + path: IndexPath, + fields: OrderedDictionary +) async throws -> OrderedDictionary { + return try await withThrowingTaskGroup(of: (String, (any Sendable)?).self) { group in + // preserve field order by assigning to null and filtering later + var results: OrderedDictionary = + fields + .mapValues { _ -> Any? in nil } + for field in fields { + group.addTask { + let fieldASTs = field.value + let fieldPath = path.appending(field.key) + let result = + try await resolveField( + exeContext: exeContext, + parentType: parentType, + source: sourceValue, + fieldASTs: fieldASTs, + path: fieldPath + ) ?? Map.null + return (field.key, result) + } + } + for try await result in group { + results[result.0] = result.1 + } + return results.compactMapValues { $0 } + } } /// Extracts the root type of the operation from the schema. @@ -969,7 +1018,7 @@ func completeObjectValue( } } - return try await exeContext.queryStrategy.executeFields( + return try await executeFields( exeContext: exeContext, parentType: returnType, sourceValue: result,