Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Executing multiple operations in a query #375

Open
bbakerman opened this issue Nov 6, 2017 · 7 comments
Open

[RFC] Executing multiple operations in a query #375

bbakerman opened this issue Nov 6, 2017 · 7 comments
Labels
👻 Needs Champion RFC Needs a champion to progress (See CONTRIBUTING.md) 💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)

Comments

@bbakerman
Copy link

RFC - Executing multiple operations in a query

Today you can define multiple operations in the one query document but the specification states you can only execute one of them at a time. So given :

        query A {
            hero 
            {
                id
                name 
                friends
                {
                    name
                }
            }
        }
        
        query
            droid (id : $droidId ) {
                name
            }
        }

You must indicate which operation to run. See http://facebook.github.io/graphql/October2016/#sec-Executing-Requests

This RFC proposes that we add the ability to run multiple operations at the one time.

Naming the operation

We propose that a list of operations be able to be given to the execution

ExecuteRequest(schema, document, operationNames, variableValues, initialValue)

  • Let operations be the result of GetOperations(document, operationNames).
  • Let coercedVariableValues be the result of CoerceVariableValues(schema, operation, variableValues).
    • For each operation in operations
    • If operation is a query operation:
      • Return ExecuteQuery(operations, schema, coercedVariableValues, initialValue).
    • Otherwise if operation is a mutation operation:
      • Return ExecuteMutation(operations, schema, coercedVariableValues, initialValue).

GetOperations(document, operationNames)

  • If operationName is null or empty:
    • If document contains exactly one operation.
      • Return the Operation contained in the document.
    • Otherwise produce a query error requiring operationName.
  • Otherwise:
    • Let operations be the Operations named operationNames in document.
    • If any of the operations are not found, produce a query error.
    • Return operations.

Executing each operation

The list of operations will be executed in the order specified. A list of ExecutionResults will be produced that represent each operation.

If an operation is a mutation, then the serial execution will be applied to it as per a single operation.

The finaly result will be a single ExecutionResult that contains the list of ExecutionResult for each operation executed. This preserves the current expected signature from an graphql execution.

However like subsciptions, the top level data iterm is a proscibed shape, that is list of ExecutionResult

var executionResult = ExecuteRequest(schema,document,["A,"B"], variables)

var listOfResults = executionResult.data
for (result in listOfResults) {
    // process each result
}

Operation dependencies

Directive capabilities can create dependencies between the execution of operations.

For example B might depend on A executing first. If the graphql implementation detects such a dependency then it will wait for dependee operation (say A) to complete before executing the dependent operation (say B)

If not dependencies are detected, or the graphql implementation does not allow dependencies, then the operations can be executed in parralel, assuming they are query operations.

Variables given to each operation

The variables that are specified on the original execution will be given to each operation.

The graphql implementation is free to augment those variables with new values as it executes.

New variables will be subject to the same validation rules as the initial variables.

Capabilities unlocked because of multiple operations

Multiple operations allows for more efficient graphql queries. Multiple queries can be sent over slow mobile networks (which preparsed queries make even more efficient) and results can be sent back as soon as thye are obtained

One you have the ability to have multiple operations, you can introduce new capabilities like variable export.

This RFC is not concerned with defining exactly how this happens but having multiple operations is a pre-cursor to such capabilities.

For examaple imagine a query like

        query A {
            hero 
            {
                id @export(as:"droidId")
                name 
                friends  @export(as:"r2d2Friends") 
                {
                    name @export(as:"friendNames")
                }
            }
        }
        
        query B($droidId : String!) {
            droid (id : $droidId ) {
                name
            }
        }

Output values from operation A would be exported as the variables named "droidId", "r2d2Friends", and "friendNames"

The operation B would be detected to be dependent on operation A and would be executed after operation A completes. It would receive the value of "droidId" as a variable from operation A

Open Questions

The return shape of the results is an open question. Should it be a list (as proposed) or be async iterator in line with subscription operations.

When the returned results are Promise based then the distinction between list of Promise and async iterator of Promise is very small and we prefer the simple list.

There is a symmetry between a list of operations as input and a list of results as output.

Current implementations

Sangria has this capability today sangria-graphql.org/learn/#batch-executor and graphql-java has a PR exploring the idea graphql-java/graphql-java#808

@jlouis
Copy link

jlouis commented Nov 6, 2017

Immediate hunch: how is this different from:

query A {
    ...HeroFragment
}

fragment HeroFragment on Query {
            hero 
            {
                id
                name 
                friends
                {
                    name
                }
            }
        }
}        
 
query QD($droidId : ID) {
    ...DroidFragment
}

fragment DroidFragment on Query {
            droid (id : $droidId ) {
                name
            }
        }
}

I.e., if you fragment the queries, then building up one which does both seems to be as easy as doing

query QB($droidId : ID) {
    ...HeroFragment
    ...DroidFragment
}

and this only requires a single query. I think this should be part of a discussion, simply because the added functionality needs to have something which cannot be obtained by simply fragmenting the query. YMMV of course.

@bbakerman
Copy link
Author

@jlouis - its different because it allows 2 major things

  • query variable dependency - that is operation A can feed into operation B
  • it allows results to be streamed out - A can sent back out as a result while B is still operating

In your example above the $droidId can't be an output of A that is fed into B

Also in your example above the data for ... HeroFragment cannot be sent out before ...DroidFragment is executed (assuming they are not dependent.

@leebyron
Copy link
Collaborator

leebyron commented Oct 2, 2018

This is aging, but it would be interesting to see a more holistic approach to batch requests. I think it probably requires a new algorithm other than Execute

@sungam3r
Copy link
Contributor

It seems to me that this task should be solved at the transport level. Query batching is a well-known concept within which it is possible to solve the described problem. Several queries are transmitted in one (HTTP) request, then the server parses them and passes to the graphql engine for execution, making all the necessary transformations, variables matching etc. That is, I want to say that this is not part of the graphql specification, this can be a part of the transport protocol. Related to graphql/graphql-over-http#5

@acao
Copy link
Member

acao commented Sep 12, 2020

though it does seem to make sense as a transport spec, there is this section in the main spec that does prescribe a single operationName per operation, regardless of transport. So I feel we would need to modify both the main and the transport specifications to support this.

for example, a primitive graphql.executeSync(document, operationName) (current spec) should be able to accept graph.executeSync(document, operationName[]) or executeSync(document) for all operations

@Konard
Copy link

Konard commented Apr 29, 2021

It should be implemented for mutations as well:
This will also make possible to eliminate the need of GUID/UUID generation on the client for making multiple mutations.
And finally it will be possible to use just integer identifiers.
The number of round trips with the server will be reduced.
It will also simplify the way the permissions are verified.

And to make it even better for permissions checks there should be a way to specify how non-depend mutations are executed: in parallel or in sequence order. Or may be a way to specify dependent mutations without data exchange between mutations (so all non-dependent can be executed in parallel).

@michaelstaib
Copy link
Member

The GraphQL HTTP spec started an early RFC.

https://github.com/graphql/graphql-over-http/blob/main/rfcs/Batching.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
👻 Needs Champion RFC Needs a champion to progress (See CONTRIBUTING.md) 💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)
Projects
None yet
Development

No branches or pull requests

7 participants