Skip to content

Commit 7f7ede0

Browse files
committed
implement rendering defer response
1 parent 34df659 commit 7f7ede0

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
@@ -7102,6 +7102,14 @@ func TestExecutionEngine_Execute(t *testing.T) {
71027102
statusCode: 200,
71037103
body: `{"data":{"user":{"info":{"email":"black@sabbat","phone":"123"}}}}`,
71047104
},
7105+
`{"query":"{user {info {phone} title}}"}`: {
7106+
statusCode: 200,
7107+
body: `{"data":{"user":{"info":{"phone":"123"},"title":"Sabbat"}}}`,
7108+
},
7109+
`{"query":"{user {name info {email}}}"}`: {
7110+
statusCode: 200,
7111+
body: `{"data":{"user":{"name":"Black","info":{"email":"black@sabbat"}}}}`,
7112+
},
71057113
`{"query":"{user {name info {__internal__typename_placeholder: __typename}}}"}`: {
71067114
statusCode: 200,
71077115
body: `{"data":{"user":{"name":"Black","info":{"__internal__typename_placeholder":"Info"}}}}`,
@@ -7172,6 +7180,34 @@ func TestExecutionEngine_Execute(t *testing.T) {
71727180
`,
71737181
}, withStreamingResponse()))
71747182

7183+
t.Run("single deffered field between regular fields", runWithoutError(ExecutionEngineTestCase{
7184+
schema: func(t *testing.T) *graphql.Schema {
7185+
t.Helper()
7186+
parseSchema, err := graphql.NewSchemaFromString(definition)
7187+
require.NoError(t, err)
7188+
return parseSchema
7189+
}(t),
7190+
operation: func(t *testing.T) graphql.Request {
7191+
return graphql.Request{
7192+
OperationName: "DeferUserTitle",
7193+
Query: `
7194+
query DeferUserTitle {
7195+
user {
7196+
title
7197+
... @defer {
7198+
name
7199+
}
7200+
id
7201+
}
7202+
}`,
7203+
}
7204+
},
7205+
dataSources: makeDataSource(t, false),
7206+
expectedResponse: `{"data":{"user":{"title":"Sabbat","id":"1"}},"hasNext":true}
7207+
{"incremental":[{"data":{"name":"Black"},"path":["user"]}],"hasNext":false}
7208+
`,
7209+
}, withStreamingResponse()))
7210+
71757211
t.Run("multiple deffered fields", runWithoutError(ExecutionEngineTestCase{
71767212
schema: func(t *testing.T) *graphql.Schema {
71777213
t.Helper()
@@ -7320,6 +7356,39 @@ func TestExecutionEngine_Execute(t *testing.T) {
73207356
`,
73217357
}, withStreamingResponse()))
73227358

7359+
t.Run("defer nested object with duplicated non defered object", runWithoutError(ExecutionEngineTestCase{
7360+
schema: func(t *testing.T) *graphql.Schema {
7361+
t.Helper()
7362+
parseSchema, err := graphql.NewSchemaFromString(definition)
7363+
require.NoError(t, err)
7364+
return parseSchema
7365+
}(t),
7366+
operation: func(t *testing.T) graphql.Request {
7367+
return graphql.Request{
7368+
OperationName: "DeferUserTitle",
7369+
Query: `
7370+
query DeferUserTitle {
7371+
user {
7372+
name
7373+
info {
7374+
email
7375+
}
7376+
... @defer {
7377+
info {
7378+
phone
7379+
}
7380+
title
7381+
}
7382+
}
7383+
}`,
7384+
}
7385+
},
7386+
dataSources: makeDataSource(t, false),
7387+
expectedResponse: `{"data":{"user":{"name":"Black","info":{"email":"black@sabbat"}}},"hasNext":true}
7388+
{"incremental":[{"data":{"title":"Sabbat"},"path":["user"]},{"data":{"phone":"123"},"path":["user","info"]}],"hasNext":false}
7389+
`,
7390+
}, withStreamingResponse()))
7391+
73237392
t.Run("defer nested object fields", runWithoutError(ExecutionEngineTestCase{
73247393
schema: func(t *testing.T) *graphql.Schema {
73257394
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
@@ -182,22 +182,31 @@ func (f *NodeSuggestions) propagateDeferParentsUpToRootNode(i int) {
182182
continue
183183
}
184184

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

193200
if parentIdToUpdate == -1 {
194-
// should not happen
195-
return
201+
// could happen if we haven't set it
202+
// because it already contains this defer id
203+
break
196204
}
197205

198206
parentIndexesToAddDeferID = append(parentIndexesToAddDeferID, parentIdToUpdate)
199207

200208
if f.items[parentIdToUpdate].IsRootNode {
209+
// we have found a root node from which we could branch out
201210
break
202211
}
203212

v2/pkg/engine/resolve/resolvable.go

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

6363
enclosingTypeNames []string
6464

65-
currentFieldInfo *FieldInfo
65+
currentFieldInfo *FieldInfo
66+
incrementalItemWritten bool
6667
}
6768

6869
type ResolvableOptions struct {
@@ -108,6 +109,10 @@ func (r *Resolvable) Reset() {
108109
for k := range r.authorizationDeny {
109110
delete(r.authorizationDeny, k)
110111
}
112+
r.deferMode = false
113+
r.deferID = ""
114+
r.enableDeferRender = false
115+
r.incrementalItemWritten = false
111116
}
112117

113118
func (r *Resolvable) Init(ctx *Context, initialData []byte, operationType ast.OperationType) (err error) {
@@ -189,6 +194,27 @@ func (r *Resolvable) ResolveNode(node Node, data *astjson.Value, out io.Writer)
189194
return nil
190195
}
191196

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

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

0 commit comments

Comments
 (0)