Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion v2/pkg/engine/postprocess/deduplicate_single_fetches.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
)

// deduplicateSingleFetches is a post-processing step that removes duplicate single fetches
// from the initial fetch tree. It merges their fetch paths and updates dependencies accordingly.
// NOTE: initial tree structure should be flat and contain a single root item with all fetches as children.
type deduplicateSingleFetches struct {
disable bool
}
Expand All @@ -16,16 +19,52 @@ func (d *deduplicateSingleFetches) ProcessFetchTree(root *resolve.FetchTreeNode)
}
for i := range root.ChildNodes {
for j := i + 1; j < len(root.ChildNodes); j++ {
if root.ChildNodes[i].Item.Equals(root.ChildNodes[j].Item) {
if root.ChildNodes[i].Item.EqualSingleFetch(root.ChildNodes[j].Item) {
root.ChildNodes[i].Item.FetchPath = d.mergeFetchPath(root.ChildNodes[i].Item.FetchPath, root.ChildNodes[j].Item.FetchPath)

newId := root.ChildNodes[i].Item.Fetch.Dependencies().FetchID
oldId := root.ChildNodes[j].Item.Fetch.Dependencies().FetchID

root.ChildNodes = append(root.ChildNodes[:j], root.ChildNodes[j+1:]...)
j--

// when we merge duplicated fetches, we need to update the dependencies of the other fetches
// because they might depend on the fetch that we are removing
d.replaceDependsOnFetchId(root, oldId, newId)
Comment thread
devsergiy marked this conversation as resolved.
}
}
}
}

// replaceDependsOnFetchId replaces all occurrences of oldId with newId in the dependencies of the fetch tree.
func (d *deduplicateSingleFetches) replaceDependsOnFetchId(root *resolve.FetchTreeNode, oldId, newId int) {
for i := range root.ChildNodes {
replaced := false
for j := range root.ChildNodes[i].Item.Fetch.Dependencies().DependsOnFetchIDs {
Comment thread
jensneuse marked this conversation as resolved.
if root.ChildNodes[i].Item.Fetch.Dependencies().DependsOnFetchIDs[j] == oldId {
root.ChildNodes[i].Item.Fetch.Dependencies().DependsOnFetchIDs[j] = newId
replaced = true
}
}

if !replaced {
continue
}

for j := range root.ChildNodes[i].Item.Fetch.DependenciesCoordinates() {
for k := range root.ChildNodes[i].Item.Fetch.DependenciesCoordinates()[j].DependsOn {
if root.ChildNodes[i].Item.Fetch.DependenciesCoordinates()[j].DependsOn[k].FetchID == oldId {
root.ChildNodes[i].Item.Fetch.DependenciesCoordinates()[j].DependsOn[k].FetchID = newId
}
}
}
}
}
Comment thread
devsergiy marked this conversation as resolved.

// mergeFetchPath merges the fetch paths of two single fetches.
// The goal of this method is to merge typename conditions.
// When fetches originate from different parent fragments -
// they will have different typenames, but the same path in response.
func (d *deduplicateSingleFetches) mergeFetchPath(left, right []resolve.FetchItemPathElement) []resolve.FetchItemPathElement {
for i := range left {
left[i].TypeNames = d.mergeTypeNames(left[i].TypeNames, right[i].TypeNames)
Expand Down
196 changes: 196 additions & 0 deletions v2/pkg/engine/postprocess/deduplicate_single_fetches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,202 @@ func TestDeduplicateSingleFetches_ProcessFetchTree(t *testing.T) {
assert.Equal(t, output, input)
})

t.Run("same path, same input, different fetch ids - should update dependencies with merged fetch ids", func(t *testing.T) {
input := &resolve.FetchTreeNode{
ChildNodes: []*resolve.FetchTreeNode{
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 0,
DependsOnFetchIDs: []int{},
},
FetchConfiguration: resolve.FetchConfiguration{Input: "rootQuery"},
},
},
},
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root.a"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 1,
DependsOnFetchIDs: []int{0},
},
FetchConfiguration: resolve.FetchConfiguration{
Input: "a",
CoordinateDependencies: []resolve.FetchDependency{
{
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
},
},
},
},
},
},
},
},
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root.a"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 2,
DependsOnFetchIDs: []int{0},
},
FetchConfiguration: resolve.FetchConfiguration{
Input: "a",
CoordinateDependencies: []resolve.FetchDependency{
{
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
},
},
},
},
},
},
},
},
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root.a.b"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 4,
DependsOnFetchIDs: []int{0, 2},
},
FetchConfiguration: resolve.FetchConfiguration{
Input: "b",
CoordinateDependencies: []resolve.FetchDependency{
{
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
},
{
FetchID: 2,
},
},
},
},
},
},
},
},
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root.a.b"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 3,
DependsOnFetchIDs: []int{0, 1},
},
FetchConfiguration: resolve.FetchConfiguration{
Input: "b",
CoordinateDependencies: []resolve.FetchDependency{
{
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
},
{
FetchID: 1,
},
},
},
},
},
},
},
},
},
}

output := &resolve.FetchTreeNode{
ChildNodes: []*resolve.FetchTreeNode{
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 0,
DependsOnFetchIDs: []int{},
},
FetchConfiguration: resolve.FetchConfiguration{Input: "rootQuery"},
},
},
},
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root.a"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 1,
DependsOnFetchIDs: []int{0},
},
FetchConfiguration: resolve.FetchConfiguration{
Input: "a",
CoordinateDependencies: []resolve.FetchDependency{
{
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
},
},
},
},
},
},
},
},
{
Kind: resolve.FetchTreeNodeKindSingle,
Item: &resolve.FetchItem{
FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"root.a.b"}}},
Fetch: &resolve.SingleFetch{
FetchDependencies: resolve.FetchDependencies{
FetchID: 4,
Comment thread
devsergiy marked this conversation as resolved.
DependsOnFetchIDs: []int{0, 1},
},
FetchConfiguration: resolve.FetchConfiguration{
Input: "b",
CoordinateDependencies: []resolve.FetchDependency{
{
DependsOn: []resolve.FetchDependencyOrigin{
{
FetchID: 0,
},
{
FetchID: 1,
},
},
},
},
},
},
},
},
},
}

dedup := &deduplicateSingleFetches{}
dedup.ProcessFetchTree(input)

assert.Equal(t, output, input)
})

t.Run("different path, same input", func(t *testing.T) {
input := &resolve.FetchTreeNode{
ChildNodes: []*resolve.FetchTreeNode{
Expand Down
5 changes: 5 additions & 0 deletions v2/pkg/engine/postprocess/postprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ func (p *Processor) Process(pre plan.Plan) plan.Plan {
for i := range p.processResponseTree {
p.processResponseTree[i].Process(t.Response.Data)
}
// initialize the fetch tree
p.createFetchTree(t.Response)
// NOTE: deduplication relies on the fact that the fetch tree
// have flat structure of child fetches
p.dedupe.ProcessFetchTree(t.Response.Fetches)
p.resolveInputTemplates.ProcessFetchTree(t.Response.Fetches)
for i := range p.processFetchTree {
Expand All @@ -156,6 +159,8 @@ func (p *Processor) Process(pre plan.Plan) plan.Plan {
return pre
}

// createFetchTree creates an inital fetch tree from the raw fetches in the GraphQL response.
// The initial fetch tree is a node of sequence fetch kind, with a flat list of fetches as children.
func (p *Processor) createFetchTree(res *resolve.GraphQLResponse) {
if p.disableExtractFetches {
return
Expand Down
Loading
Loading