Skip to content

Commit c6ed075

Browse files
committed
refactor: decoupled merge logic from executor package
1 parent 4b6f994 commit c6ed075

10 files changed

Lines changed: 362 additions & 350 deletions

File tree

federation/executor/executor.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"sync"
1313

14+
"github.com/n9te9/go-graphql-federation-gateway/federation/executor/query_builder"
1415
"github.com/n9te9/go-graphql-federation-gateway/federation/graph"
1516
"github.com/n9te9/go-graphql-federation-gateway/federation/planner"
1617
"github.com/n9te9/graphql-parser/ast"
@@ -32,7 +33,7 @@ type Executor interface {
3233
}
3334

3435
type executor struct {
35-
QueryBuilder
36+
QueryBuilder query_builder.QueryBuilder
3637

3738
superGraph *graph.SuperGraph
3839
typeDefinitions map[string]*ast.ObjectTypeDefinition
@@ -48,7 +49,7 @@ type ExecutorOption struct {
4849
}
4950

5051
func NewExecutor(httpClient *http.Client, superGraph *graph.SuperGraph, option ExecutorOption) *executor {
51-
qb := NewQueryBuilder()
52+
qb := query_builder.NewQueryBuilder()
5253
e := &executor{
5354
QueryBuilder: qb,
5455
superGraph: superGraph,
@@ -153,7 +154,7 @@ func (e *executor) Execute(ctx context.Context, plan *planner.Plan, variables ma
153154
defer close(ectx.concurrencyMap[step.ID])
154155
e.waitDependStepEnded(ectx, plan, step)
155156
var currentRefs []entityRef
156-
var stepEntities Entities = make(Entities, 0)
157+
var stepEntities query_builder.Entities = make(query_builder.Entities, 0)
157158

158159
if len(step.DependsOn) != 0 {
159160
targetTypes := make(map[string]struct{})

federation/executor/executor_v2.go

Lines changed: 29 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"sync"
1212
"time"
1313

14+
"github.com/n9te9/go-graphql-federation-gateway/federation/executor/merger"
15+
"github.com/n9te9/go-graphql-federation-gateway/federation/executor/query_builder"
1416
"github.com/n9te9/go-graphql-federation-gateway/federation/graph"
1517
"github.com/n9te9/go-graphql-federation-gateway/federation/planner"
1618
"github.com/n9te9/graphql-parser/ast"
@@ -29,7 +31,8 @@ type ExecutorV2 struct {
2931
httpClient *http.Client
3032
pool sync.Pool // pool of *ExecutionContext
3133
bufPool sync.Pool // pool of *bytes.Buffer for request body serialization
32-
queryBuilder *QueryBuilderV2
34+
queryBuilder query_builder.QueryBuilderV2
35+
merger merger.Merger
3336
superGraph *graph.SuperGraphV2
3437
subgraphTimeout time.Duration // per-subgraph request timeout; 0 means no extra timeout
3538
}
@@ -66,8 +69,9 @@ func newExecutorV2(httpClient *http.Client, superGraph *graph.SuperGraphV2, time
6669
return bytes.NewBuffer(make([]byte, 0, 4096))
6770
},
6871
},
69-
queryBuilder: NewQueryBuilderV2(superGraph),
72+
queryBuilder: query_builder.NewQueryBuilderV2(superGraph),
7073
superGraph: superGraph,
74+
merger: merger.NewMerger(),
7175
}
7276
}
7377

@@ -82,11 +86,7 @@ type ExecutionContext struct {
8286

8387
// Execute executes a query plan and returns the merged result.
8488
// It validates the plan is a DAG, then executes steps in dependency order.
85-
func (e *ExecutorV2) Execute(
86-
ctx context.Context,
87-
plan *planner.PlanV2,
88-
variables map[string]interface{},
89-
) (map[string]interface{}, error) {
89+
func (e *ExecutorV2) Execute(ctx context.Context, plan *planner.PlanV2, variables map[string]interface{}) (map[string]interface{}, error) {
9090
// Validate DAG
9191
if err := e.validateDAG(plan); err != nil {
9292
return nil, fmt.Errorf("invalid plan: %w", err)
@@ -206,11 +206,7 @@ func (e *ExecutorV2) validateDAG(plan *planner.PlanV2) error {
206206
}
207207

208208
// executeSteps executes a group of steps in parallel and then recursively executes dependent steps.
209-
func (e *ExecutorV2) executeSteps(
210-
execCtx *ExecutionContext,
211-
stepIDs []int,
212-
variables map[string]interface{},
213-
) error {
209+
func (e *ExecutorV2) executeSteps(execCtx *ExecutionContext, stepIDs []int, variables map[string]interface{}) error {
214210
if len(stepIDs) == 0 {
215211
return nil
216212
}
@@ -335,12 +331,7 @@ func (e *ExecutorV2) findReadySteps(execCtx *ExecutionContext) []int {
335331
}
336332

337333
// processStep processes a single step.
338-
func (e *ExecutorV2) processStep(
339-
ctx context.Context,
340-
execCtx *ExecutionContext,
341-
step *planner.StepV2,
342-
variables map[string]interface{},
343-
) error {
334+
func (e *ExecutorV2) processStep(ctx context.Context, execCtx *ExecutionContext, step *planner.StepV2, variables map[string]interface{}) error {
344335
// Guard against nil subgraph
345336
if step.SubGraph == nil {
346337
err := fmt.Errorf("step %d has nil subgraph", step.ID)
@@ -398,14 +389,22 @@ func (e *ExecutorV2) processStep(
398389
execCtx.results[step.ID] = result
399390
execCtx.mu.Unlock()
400391
} else {
401-
// Merge entity results into parent
402-
if err := e.mergeEntityResults(execCtx, step, result); err != nil {
403-
e.recordError(execCtx, step, fmt.Errorf("failed to merge entity results: %w", err))
392+
rootResult, rootResultIndex, err := e.extractRootResult(execCtx)
393+
if err != nil {
394+
e.recordError(execCtx, step, fmt.Errorf("failed to extract root result for merging: %w", err))
404395
e.setNullForFailedStep(execCtx, step)
405-
return nil // Don't propagate error
396+
return nil
406397
}
398+
407399
execCtx.mu.Lock()
408-
execCtx.results[step.ID] = result
400+
mergedRootResult, err := e.merger.MergeEntities(rootResult, result, step)
401+
if err != nil {
402+
e.recordError(execCtx, step, fmt.Errorf("failed to merge entities: %w", err))
403+
e.setNullForFailedStep(execCtx, step)
404+
return nil
405+
}
406+
execCtx.results[rootResultIndex] = mergedRootResult
407+
execCtx.results[step.ID] = mergedRootResult
409408
execCtx.mu.Unlock()
410409
}
411410

@@ -982,21 +981,12 @@ func (e *ExecutorV2) buildRepresentation(entity map[string]interface{}, typeName
982981
return representation
983982
}
984983

985-
// mergeEntityResults merges entity query results back into parent results.
986-
func (e *ExecutorV2) mergeEntityResults(execCtx *ExecutionContext, step *planner.StepV2, result map[string]interface{}) error {
987-
execCtx.mu.Lock()
988-
defer execCtx.mu.Unlock()
989-
990-
// Get parent step result
991-
if len(step.DependsOn) == 0 {
992-
return nil
993-
}
984+
func (e *ExecutorV2) extractRootResult(execCtx *ExecutionContext) (map[string]interface{}, int, error) {
985+
execCtx.mu.RLock()
986+
defer execCtx.mu.RUnlock()
994987

995-
// Always merge into the root step (Step 0), not the immediate parent
996-
// This is because nested entity steps (e.g., Step 2 depends on Step 1)
997-
// cannot merge into Step 1's _entities result format
998988
var rootStepID int
999-
var rootResult interface{}
989+
var rootResult any
1000990
for _, s := range execCtx.plan.Steps {
1001991
if len(s.DependsOn) == 0 {
1002992
rootStepID = s.ID
@@ -1006,196 +996,15 @@ func (e *ExecutorV2) mergeEntityResults(execCtx *ExecutionContext, step *planner
1006996
}
1007997

1008998
if rootResult == nil {
1009-
return fmt.Errorf("root step result not found")
999+
return nil, -1, fmt.Errorf("root step result not found")
10101000
}
10111001

1012-
// Extract data from root result
10131002
rootResultMap, ok := rootResult.(map[string]interface{})
10141003
if !ok {
1015-
return fmt.Errorf("root result is not a map")
1016-
}
1017-
1018-
rootData, ok := rootResultMap["data"].(map[string]interface{})
1019-
if !ok {
1020-
return fmt.Errorf("root result does not have data field")
1021-
}
1022-
1023-
// Extract _entities from entity query result
1024-
resultData, ok := result["data"].(map[string]interface{})
1025-
if !ok {
1026-
return nil // No data to merge
1027-
}
1028-
1029-
entitiesData, ok := resultData["_entities"]
1030-
if !ok {
1031-
return nil // No entities to merge
1032-
}
1033-
1034-
// Build merge path (skip root type name)
1035-
mergePath := make([]string, 0)
1036-
for i, segment := range step.InsertionPath {
1037-
// Skip root type names (Query, Mutation, Subscription)
1038-
if i == 0 && (segment == "Query" || segment == "Mutation" || segment == "Subscription") {
1039-
continue
1040-
}
1041-
mergePath = append(mergePath, segment)
1042-
}
1043-
1044-
// Navigate to the target field to check if it's an array or object
1045-
// Also collect all array positions in the path for nested array handling
1046-
var current interface{} = rootData
1047-
var firstArrayIndex = -1 // Index of the first array in the path
1048-
1049-
for i, segment := range mergePath {
1050-
if currentMap, ok := current.(map[string]interface{}); ok {
1051-
if next, exists := currentMap[segment]; exists {
1052-
current = next
1053-
1054-
// Check if the value we just navigated to is an array
1055-
if _, isArray := current.([]interface{}); isArray {
1056-
// We hit an array - mark it
1057-
if firstArrayIndex < 0 {
1058-
firstArrayIndex = i
1059-
}
1060-
break
1061-
}
1062-
} else {
1063-
// Path doesn't exist yet
1064-
current = nil
1065-
break
1066-
}
1067-
} else {
1068-
// Not a map or array, can't navigate further
1069-
current = nil
1070-
break
1071-
}
1072-
}
1073-
1074-
// Handle different merge scenarios
1075-
if firstArrayIndex >= 0 {
1076-
// We encountered an array - need to handle nested array merging
1077-
entities, ok := entitiesData.([]interface{})
1078-
if !ok {
1079-
return fmt.Errorf("entities data is not an array")
1080-
}
1081-
1082-
// Navigate to the first array
1083-
var arrayContainer interface{} = rootData
1084-
arrayPath := mergePath[:firstArrayIndex+1] // Include the array field itself
1085-
for _, segment := range arrayPath {
1086-
if containerMap, ok := arrayContainer.(map[string]interface{}); ok {
1087-
arrayContainer = containerMap[segment]
1088-
}
1089-
}
1090-
1091-
arrayData, ok := arrayContainer.([]interface{})
1092-
if !ok {
1093-
return fmt.Errorf("expected array at merge path %v", arrayPath)
1094-
}
1095-
1096-
// The remaining path after the array
1097-
remainingPath := mergePath[firstArrayIndex+1:]
1098-
1099-
// Merge entities into the nested structure
1100-
entityIndex := 0
1101-
for _, elem := range arrayData {
1102-
elemMap, ok := elem.(map[string]interface{})
1103-
if !ok {
1104-
continue
1105-
}
1106-
1107-
// Recursively merge entities into potentially nested arrays
1108-
entityIndex = e.mergeIntoNestedArrays(elemMap, entities, remainingPath, entityIndex, step)
1109-
}
1110-
1111-
} else if current == nil {
1112-
// Path doesn't exist yet, treat as single object and let Merge handle it
1113-
entities, ok := entitiesData.([]interface{})
1114-
if !ok || len(entities) == 0 {
1115-
return nil
1116-
}
1117-
1118-
firstEntity, ok := entities[0].(map[string]interface{})
1119-
if !ok {
1120-
return fmt.Errorf("first entity is not a map")
1121-
}
1122-
1123-
if err := Merge(rootData, firstEntity, mergePath); err != nil {
1124-
return fmt.Errorf("failed to merge entity object: %w", err)
1125-
}
1126-
} else if _, isArray := current.([]interface{}); isArray {
1127-
// Target is an array, merge entities directly
1128-
if err := Merge(rootData, entitiesData, mergePath); err != nil {
1129-
return fmt.Errorf("failed to merge entities array: %w", err)
1130-
}
1131-
} else {
1132-
// Target is a single object, merge first entity
1133-
entities, ok := entitiesData.([]interface{})
1134-
if !ok || len(entities) == 0 {
1135-
return nil
1136-
}
1137-
1138-
// For single object, merge the first entity's fields
1139-
firstEntity, ok := entities[0].(map[string]interface{})
1140-
if !ok {
1141-
return fmt.Errorf("first entity is not a map")
1142-
}
1143-
1144-
if err := Merge(rootData, firstEntity, mergePath); err != nil {
1145-
return fmt.Errorf("failed to merge entity object: %w", err)
1146-
}
1147-
}
1148-
1149-
// Update the root step's result to reflect the merge
1150-
execCtx.results[rootStepID] = rootResultMap
1151-
1152-
return nil
1153-
}
1154-
1155-
// mergeIntoNestedArrays recursively merges entities into potentially nested array structures
1156-
// Returns the next entity index to use
1157-
func (e *ExecutorV2) mergeIntoNestedArrays(
1158-
current map[string]interface{},
1159-
entities []interface{},
1160-
path []string,
1161-
entityIndex int,
1162-
step *planner.StepV2,
1163-
) int {
1164-
if len(path) == 0 {
1165-
// Reached the target - merge the entity here
1166-
if entityIndex < len(entities) {
1167-
if entityMap, ok := entities[entityIndex].(map[string]interface{}); ok {
1168-
// Deep merge entity fields into current
1169-
// Use the Merge function to properly handle nested structures
1170-
Merge(current, entityMap, []string{})
1171-
}
1172-
return entityIndex + 1
1173-
}
1174-
return entityIndex
1175-
}
1176-
1177-
segment := path[0]
1178-
remainingPath := path[1:]
1179-
1180-
next, exists := current[segment]
1181-
if !exists {
1182-
return entityIndex
1183-
}
1184-
1185-
// Check if next is an array
1186-
if arr, isArray := next.([]interface{}); isArray {
1187-
// Process each array element
1188-
for _, elem := range arr {
1189-
if elemMap, ok := elem.(map[string]interface{}); ok {
1190-
entityIndex = e.mergeIntoNestedArrays(elemMap, entities, remainingPath, entityIndex, step)
1191-
}
1192-
}
1193-
} else if nextMap, ok := next.(map[string]interface{}); ok {
1194-
// Continue navigating
1195-
entityIndex = e.mergeIntoNestedArrays(nextMap, entities, remainingPath, entityIndex, step)
1004+
return nil, -1, fmt.Errorf("root result is not a map")
11961005
}
11971006

1198-
return entityIndex
1007+
return rootResultMap, rootStepID, nil
11991008
}
12001009

12011010
// sendRequest sends a GraphQL request to a subgraph.

0 commit comments

Comments
 (0)