Directive idea to support var blocks in GraphQL

Idea comes from a recent blog post that gave several shoutouts to Dgraph :sunglasses:

which references:

What I found interesting was a directive called @export which is used to export variables to be used in later queries, that is almost exactly what a var block is in Dgraph so… could this be supported!? Let’s try a quick example possibly using @var in a Dgraph way in place of @export:

Schema:

type Foo {
  id: ID
  bars: [Bar] @hasInverse(field: "foos")
}
type Bar {
  id: ID
  isChecked: Boolean @search
  foos: [Foo]
}

GraphQL Query:

query varExample ($findFoo: [ID] = []) {
  queryBar(filter: { isChecked: true }) {
    foos @var(as: "checkedBars") {
      id
    }
  }
  queryFoo(filter: { id: $findFoo }) {
    id
    bars {
      id
      isChecked
    }
  }
}

Rewriter checks each variable if it is in any export block and if so then treat it like a var and not like a GraphQL variable which would equate to DQL:

query {
  queryBar(func: eq(Bar.isChecked,true)) @filter(type(Bar)) {
    findFoo as foos: Bar.foos {
      id: uid
    }
  }
  queryFoo(func: uid(findFoo)) @filter(type(Foo)) {
    id: uid
    bars: Foo.bars {
      id: uid
      isChecked: Bar.isChecked
    }
  }
}

And then maybe another use case for @var without parameters that makes a query block a var block with no response:
GraphQL Query:

query varExample ($findFoo: [ID] = []) {
  queryBar(filter: { isChecked: true }) @var {
    foos @var(as: "checkedBars") {
      id
    }
  }
  queryFoo(filter: { id: $findFoo }) {
    id
    bars {
      id
      isChecked
    }
  }
}

Rewriter checks each variable if it is in any export block and if so then treat it like a var and not like a GraphQL variable which would equate to DQL:

query {
  var(func: eq(Bar.isChecked,true)) @filter(type(Bar)) {
    findFoo as foos: Bar.foos {
      id: uid
    }
  }
  queryFoo(func: uid(findFoo)) @filter(type(Foo)) {
    id: uid
    bars: Foo.bars {
      id: uid
      isChecked: Bar.isChecked
    }
  }
}
1 Like

I am probably more excited about this post than any other post I have written before! What do you think,
@graphql @pawan @mrjn @chewxy @MichelDiz? Could it be possible to support DQL var blocks this way natively in GraphQL?

In GraphQL each block of a query is executed separately, right? I think this would be more complex than that. But if you can have multiple blocks in the same query, looks like feasible to have var blocks.

Yes, GraphQL queries are executed independently in parallel (error of one query does not affect the other) .
Implementing this directive as stated above will involve changing this behaviour.

Well this all depends on how you look at the GraphQL spec and how GraphQL gets translated into DQL. By specification each block of a query should executed separately, yes. But in my opinion this is highly based upon the implementation of the GraphQL server to do this. If someone was building a GraphQL server that did not support async easily such as my experience with PHP then having async functionality might not be possible in reality and the implementation might say oh well with this spec we will just do the best with what we have and that involves running each block synchronously. So knowing this leads to a door of possibility IF the rewriter can be smart enough to know what blocks to run as separate DQL queries and which blocks to run as one.

This would involve looking at what variables are exported by a block and what variables are used in a block and building a dependency tree of sorts. There might be a problem when a block exports two different variables that are used in two separate query blocks because with DQL it will throw errors if you have unused variables :grimacing:

But this idea is sort of using the references above showing how someone else obviously is breaking this spec rule in a purposeful way. Obviously they have to wait for the queries with export to finish before running the queries that use that export variable so that makes it more sync then async, yes.

So this begs the question, when is it okay for an implementation to break specification?

If the answer is never, then the references above pose a GraphQL server that is outside of spec.

If the answer is only on certain aspects of the spec under certain conditions when the results of the clients demand a break in the specification such as is the case with the referenced example server by the LogRocket Blog. And something I consider is what is the aspect of breaking the specification when an implementation chooses to break it on purpose? Breaking the spec of each block runs asynchronously implementing some* query blocks runs synchronously when needed creates no harmful effects on the clients besides possibly being a tad slower than what might be expected, but the tradeoff is having enhanced functionality.

I guess my bottom line is that this spec is already broken by languages implementing GraphQL that have to run everything synchronously already, so why would it matter if other languages that can run things asynchronously decide to on purpose break this rule on certain conditions.

I think the important aspect of the spec not to break in this situation is running mutations. Mutation blocks have to always run synchronously by specification.


GraphQL Specification regarding asynchronous/parallelization

ExecuteQuery(query, schema, variableValues, initialValue)

  1. Let queryType be the root Query type in schema.
  2. Assert: queryType is an Object type.
  3. Let selectionSet be the top level Selection Set in query.
  4. Let data be the result of running ExecuteSelectionSet(selectionSet, queryType, initialValue, variableValues) normally (allowing parallelization).
  5. Let errors be any field errors produced while executing the selection set.
  6. Return an unordered map containing data and errors.

6.2.1 Executing a Query

To execute a selection set, the object value being evaluated and the object type need to be known, as well as whether it must be executed serially, or may be executed in parallel.

6.3 Executing Selection Sets

Normally the executor can execute the entries in a grouped field set in whatever order it chooses (normally in parallel). Because the resolution of fields other than top‐level mutation fields must always be side effect‐free and idempotent, the execution order must not affect the result, and hence the server has the freedom to execute the field entries in whatever order it deems optimal.

6.3.1 Normal and Serial Execution

It is common for resolver to be asynchronous due to relying on reading an underlying database or networked service to produce a value. This necessitates the rest of a GraphQL executor to handle an asynchronous execution flow.

6.4.2 Value Resolution note

  1. Return the result of evaluating ExecuteSelectionSet(subSelectionSet, objectType, result, variableValues) normally (allowing for parallelization).

6.4.3 Value Completion


According to the specification the language seems to be “allowing [for] parallelization”, “or executed in parallel”, “normally in parallel”, and to the point, “the server has the freedom to execute the field entries in whatever order it deems optimal”.

I don’t think implementing such a feature would break any kind of specification, because it does not seem to require for parallelization, but rather allow for it, which the implementation would still allow for it as long as the query blocks did not export vars that other query blocks were using.

1 Like