Skip to content

Commit ee840d7

Browse files
committed
implement rendering defer response
1 parent 830dc45 commit ee840d7

4 files changed

Lines changed: 278 additions & 73 deletions

File tree

execution/engine/execution_engine_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5682,6 +5682,14 @@ func TestExecutionEngine_Execute(t *testing.T) {
56825682
statusCode: 200,
56835683
body: `{"data":{"user":{"info":{"email":"black@sabbat","phone":"123"}}}}`,
56845684
},
5685+
`{"query":"{user {info {phone} title}}"}`: {
5686+
statusCode: 200,
5687+
body: `{"data":{"user":{"info":{"phone":"123"},"title":"Sabbat"}}}`,
5688+
},
5689+
`{"query":"{user {name info {email}}}"}`: {
5690+
statusCode: 200,
5691+
body: `{"data":{"user":{"name":"Black","info":{"email":"black@sabbat"}}}}`,
5692+
},
56855693
`{"query":"{user {name info {__internal__typename_placeholder: __typename}}}"}`: {
56865694
statusCode: 200,
56875695
body: `{"data":{"user":{"name":"Black","info":{"__internal__typename_placeholder":"Info"}}}}`,
@@ -5752,6 +5760,34 @@ func TestExecutionEngine_Execute(t *testing.T) {
57525760
`,
57535761
}, withStreamingResponse()))
57545762

5763+
t.Run("single deffered field between regular fields", runWithoutError(ExecutionEngineTestCase{
5764+
schema: func(t *testing.T) *graphql.Schema {
5765+
t.Helper()
5766+
parseSchema, err := graphql.NewSchemaFromString(definition)
5767+
require.NoError(t, err)
5768+
return parseSchema
5769+
}(t),
5770+
operation: func(t *testing.T) graphql.Request {
5771+
return graphql.Request{
5772+
OperationName: "DeferUserTitle",
5773+
Query: `
5774+
query DeferUserTitle {
5775+
user {
5776+
title
5777+
... @defer {
5778+
name
5779+
}
5780+
id
5781+
}
5782+
}`,
5783+
}
5784+
},
5785+
dataSources: makeDataSource(t, false),
5786+
expectedResponse: `{"data":{"user":{"title":"Sabbat","id":"1"}},"hasNext":true}
5787+
{"incremental":[{"data":{"name":"Black"},"path":["user"]}],"hasNext":false}
5788+
`,
5789+
}, withStreamingResponse()))
5790+
57555791
t.Run("multiple deffered fields", runWithoutError(ExecutionEngineTestCase{
57565792
schema: func(t *testing.T) *graphql.Schema {
57575793
t.Helper()
@@ -5900,6 +5936,39 @@ func TestExecutionEngine_Execute(t *testing.T) {
59005936
`,
59015937
}, withStreamingResponse()))
59025938

5939+
t.Run("defer nested object with duplicated non defered object", runWithoutError(ExecutionEngineTestCase{
5940+
schema: func(t *testing.T) *graphql.Schema {
5941+
t.Helper()
5942+
parseSchema, err := graphql.NewSchemaFromString(definition)
5943+
require.NoError(t, err)
5944+
return parseSchema
5945+
}(t),
5946+
operation: func(t *testing.T) graphql.Request {
5947+
return graphql.Request{
5948+
OperationName: "DeferUserTitle",
5949+
Query: `
5950+
query DeferUserTitle {
5951+
user {
5952+
name
5953+
info {
5954+
email
5955+
}
5956+
... @defer {
5957+
info {
5958+
phone
5959+
}
5960+
title
5961+
}
5962+
}
5963+
}`,
5964+
}
5965+
},
5966+
dataSources: makeDataSource(t, false),
5967+
expectedResponse: `{"data":{"user":{"name":"Black","info":{"email":"black@sabbat"}}},"hasNext":true}
5968+
{"incremental":[{"data":{"title":"Sabbat"},"path":["user"]},{"data":{"phone":"123"},"path":["user","info"]}],"hasNext":false}
5969+
`,
5970+
}, withStreamingResponse()))
5971+
59035972
t.Run("defer nested object fields", runWithoutError(ExecutionEngineTestCase{
59045973
schema: func(t *testing.T) *graphql.Schema {
59055974
t.Helper()

v2/pkg/engine/plan/datasource_filter_node_suggestions.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,22 +180,31 @@ func (f *NodeSuggestions) propagateDeferParentsUpToRootNode(i int) {
180180
continue
181181
}
182182

183+
if f.items[parentIdx].deferInfo != nil && f.items[parentIdx].deferInfo.ID == f.items[i].deferInfo.ID {
184+
// if parent item is in the same defer -
185+
// we should not mark it as a defer parent,
186+
// because defer parents are planned twice - in a deffered planner and regular
187+
break
188+
}
189+
183190
if slices.Contains(f.items[parentIdx].deferIDs, f.items[i].deferInfo.ID) {
184-
// no need to update
185-
return
191+
// no need to update already contains this defer id
192+
break
186193
} else {
187194
parentIdToUpdate = parentIdx
188195
}
189196
}
190197

191198
if parentIdToUpdate == -1 {
192-
// should not happen
193-
return
199+
// could happen if we haven't set it
200+
// because it already contains this defer id
201+
break
194202
}
195203

196204
parentIndexesToAddDeferID = append(parentIndexesToAddDeferID, parentIdToUpdate)
197205

198206
if f.items[parentIdToUpdate].IsRootNode {
207+
// we have found a root node from which we could branch out
199208
break
200209
}
201210

v2/pkg/engine/resolve/resolvable.go

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ type Resolvable struct {
6060

6161
enclosingTypeNames []string
6262

63-
currentFieldInfo *FieldInfo
63+
currentFieldInfo *FieldInfo
64+
incrementalItemWritten bool
6465
}
6566

6667
type ResolvableOptions struct {
@@ -106,6 +107,10 @@ func (r *Resolvable) Reset() {
106107
for k := range r.authorizationDeny {
107108
delete(r.authorizationDeny, k)
108109
}
110+
r.deferMode = false
111+
r.deferID = ""
112+
r.enableDeferRender = false
113+
r.incrementalItemWritten = false
109114
}
110115

111116
func (r *Resolvable) Init(ctx *Context, initialData []byte, operationType ast.OperationType) (err error) {
@@ -186,6 +191,27 @@ func (r *Resolvable) ResolveNode(node Node, data *astjson.Value, out io.Writer)
186191
return nil
187192
}
188193

194+
func (r *Resolvable) renderPath() {
195+
r.printBytes(lBrack)
196+
for i, p := range r.path {
197+
if i > 0 {
198+
r.printBytes(comma)
199+
}
200+
if p.Name != "" {
201+
r.printBytes(quote)
202+
r.printBytes(unsafebytes.StringToBytes(p.Name))
203+
r.printBytes(quote)
204+
} else {
205+
r.printBytes(unsafebytes.StringToBytes(strconv.Itoa(p.Idx)))
206+
}
207+
}
208+
r.printBytes(rBrack)
209+
}
210+
211+
func (r *Resolvable) printDeferDelimeter() {
212+
r.printBytes(literalNewLine)
213+
}
214+
189215
func (r *Resolvable) Resolve(ctx context.Context, rootData *Object, fetchTree *FetchTreeNode, out io.Writer) error {
190216
r.out = out
191217
r.enableRender = false
@@ -233,7 +259,70 @@ func (r *Resolvable) Resolve(ctx context.Context, rootData *Object, fetchTree *F
233259
r.printBytes(comma)
234260
r.printErr = r.printExtensions(ctx, fetchTree)
235261
}
262+
263+
if r.deferMode {
264+
r.printHasNext(true)
265+
}
266+
236267
r.printBytes(rBrace)
268+
269+
if r.deferMode {
270+
r.printDeferDelimeter()
271+
}
272+
273+
return r.printErr
274+
}
275+
276+
func (r *Resolvable) ResolveDefer(rootData *Object, out io.Writer, hasNext bool) error {
277+
r.out = out
278+
r.printErr = nil
279+
r.authorizationError = nil
280+
281+
// This method acts as a generator for the incremental response
282+
// It will print the incremental response envelope and then use walkObject to find and render the deferred fields
283+
284+
// First pass: validate and check for authorization errors
285+
r.enableRender = false
286+
r.deferMode = true
287+
r.enableDeferRender = false
288+
289+
_ = r.walkObject(rootData, r.data)
290+
if r.authorizationError != nil {
291+
return r.authorizationError
292+
}
293+
294+
// Second pass: render the incremental response
295+
r.enableRender = true
296+
r.incrementalItemWritten = false
297+
// deferMode stays true
298+
// enableDeferRender starts false, will be toggled in walkObject when match found
299+
300+
r.printBytes(lBrace)
301+
r.printBytes(quote)
302+
r.printBytes(literalIncremental)
303+
r.printBytes(quote)
304+
r.printBytes(colon)
305+
r.printBytes(lBrack)
306+
307+
_ = r.walkObject(rootData, r.data)
308+
309+
r.printBytes(rBrack)
310+
311+
r.printHasNext(hasNext)
312+
313+
if r.hasErrors() {
314+
r.printBytes(comma)
315+
r.printBytes(quote)
316+
r.printBytes(literalErrors)
317+
r.printBytes(quote)
318+
r.printBytes(colon)
319+
r.printNode(r.errors)
320+
}
321+
322+
r.printBytes(rBrace)
323+
324+
r.printDeferDelimeter()
325+
237326
return r.printErr
238327
}
239328

@@ -635,6 +724,79 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
635724
defer func() {
636725
r.typeNames = r.typeNames[:len(r.typeNames)-1]
637726
}()
727+
728+
// In Defer Seeking Mode, we first identify and render all matching fields for the current DeferID as a single incremental item.
729+
if r.deferMode && !r.enableDeferRender {
730+
var (
731+
deferFieldIndices []int
732+
)
733+
734+
for k := range obj.Fields {
735+
if obj.Fields[k].Defer == nil || obj.Fields[k].Defer.DeferID != r.deferID {
736+
continue
737+
}
738+
739+
// Duplicate skip checks to ensure we only include valid fields
740+
if obj.Fields[k].ParentOnTypeNames != nil {
741+
if r.skipFieldOnParentTypeNames(obj.Fields[k]) {
742+
continue
743+
}
744+
}
745+
if obj.Fields[k].OnTypeNames != nil {
746+
if r.skipFieldOnTypeNames(obj.Fields[k]) {
747+
continue
748+
}
749+
}
750+
751+
deferFieldIndices = append(deferFieldIndices, k)
752+
}
753+
754+
if len(deferFieldIndices) > 0 && r.enableRender {
755+
if r.incrementalItemWritten {
756+
r.printBytes(comma)
757+
}
758+
759+
// Render Incremental Item Envelope: {"data":{...},"path":[...]}
760+
r.printBytes(lBrace)
761+
762+
r.printBytes(quote)
763+
r.printBytes(literalData)
764+
r.printBytes(quote)
765+
r.printBytes(colon)
766+
r.printBytes(lBrace)
767+
768+
for k, fieldIdx := range deferFieldIndices {
769+
if k > 0 {
770+
r.printBytes(comma)
771+
}
772+
773+
r.enableDeferRender = true
774+
r.printBytes(quote)
775+
r.printBytes(obj.Fields[fieldIdx].Name)
776+
r.printBytes(quote)
777+
r.printBytes(colon)
778+
779+
r.currentFieldInfo = obj.Fields[fieldIdx].Info
780+
_ = r.walkNode(obj.Fields[fieldIdx].Value, value)
781+
r.enableDeferRender = false
782+
}
783+
784+
r.printBytes(rBrace)
785+
786+
r.printBytes(comma)
787+
r.printBytes(quote)
788+
r.printBytes(literalPath)
789+
r.printBytes(quote)
790+
r.printBytes(colon)
791+
r.renderPath()
792+
793+
r.printBytes(rBrace)
794+
795+
r.wroteData = true
796+
r.incrementalItemWritten = true
797+
}
798+
}
799+
638800
for i := range obj.Fields {
639801
if obj.Fields[i].ParentOnTypeNames != nil {
640802
if r.skipFieldOnParentTypeNames(obj.Fields[i]) {
@@ -646,6 +808,38 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
646808
continue
647809
}
648810
}
811+
812+
// When NOT in defer mode (initial response), skip fields that are deferred.
813+
// They will be handled by the deferred response.
814+
// Also if in deferMode but deferID is empty, it means we are in the initial response of a deferred request.
815+
if obj.Fields[i].Defer != nil {
816+
if !r.deferMode || (r.deferMode && r.deferID == "") {
817+
continue
818+
}
819+
}
820+
821+
if r.deferMode && !r.enableDeferRender {
822+
// DEFER SEEKING MODE
823+
824+
// Check if this field matches the current defer ID
825+
isMatch := obj.Fields[i].Defer != nil && obj.Fields[i].Defer.DeferID == r.deferID
826+
827+
if isMatch {
828+
// Match found - already rendered in pre-scan
829+
continue
830+
}
831+
832+
// No match - recurse to find nested defers
833+
// We only need to recurse if the node is an Object or Array, as Scalars cannot have nested defers.
834+
// Recursing into Scalars would trigger "non-nullable field returned null" error in handleNodeNotRendered because we are not rendering them.
835+
kind := obj.Fields[i].Value.NodeKind()
836+
if kind == NodeKindObject || kind == NodeKindArray {
837+
r.currentFieldInfo = obj.Fields[i].Info
838+
_ = r.walkNode(obj.Fields[i].Value, value)
839+
}
840+
continue
841+
}
842+
649843
if !r.render() {
650844
skip := r.authorizeField(value, obj.Fields[i])
651845
if skip {

0 commit comments

Comments
 (0)