Plan branching via `.eval*()`
Input steps (that is: steps that represent inputs to your GraphQL operations
such as context()
and steps representing field arguments accessed through
FieldArgs
) have a suite of eval*
methods used to constrain query plans
based on the concrete values seen at runtime. For example, if the user has used
the @skip
or @include
directives to turn on/off sections of their query,
it's more efficient to have separate plans for this so that we don't overfetch
data:
query GetUserDetails($includeFriends: Boolean! = false) {
currentUser {
name
avatarUrl
friends(first: 100) @include(if: $includeFriends) {
name
avatarUrl
}
}
}
Grafast does this branching automatically by evaluating whether the value of
$includeFriends
is true
or not ($includeFriends.evalIs(true)
) before
deciding which plan resolvers to call. This requirement is then stored as a
constraint on the plan, such that the next request may reuse the plan only if
the result of $includeFriends.evalIs(true)
retains the same value. Thus this
allows the same operation to actually generate two plans, one for
includeFriends=false
and one for includeFriends=true
.
Grafast doesn't plan all possible values of includeFriends
up front; instead
it just evaluates the version it's currently handling (e.g.
includeFriends=false
) and will only plan the alternative when a request comes
through for it (e.g. with includeFriends=true
).
Note that this can result in up to 2x potential plans being
generated for the same operation, where x
is the number of unique
@skip
/@include
variables. This reduces the "reusability" of the operation
plans - each time a new request comes through that doesn't match the
constraints of any pre-existing plans, we have to plan the entire operation
again from scratch.
The .eval*()
family
It is possible, but recommended against, for user plan resolvers to use this functionality baked into Grafast. All the eval methods have a cost; being specific about the exact circumstances under which the plan should fork reduces the number of potential branches and reduces the resulting planning time and code complexity.
Note that only input-related steps implement any of these methods, and even then these steps only implement the methods appropriate to them. Tread carefully.
.evalIs(val)
- Branches the plan into two forks: one where$__inputStep
's value=== val
, and one where it isn't..evalHas(key)
- Branches the plan into two forks based on whether the specifiedkey
is set or not..evalLength()
- Branches the plan into a fork for each length of the list seen at runtime..evalIsEmpty()
- Branches the plan into a fork based on whether the step represents an "empty" object (an object with no attributes) or not..eval()
- Branches the plan into a fork for every possible value - you should almost never use.eval()
in your plan resolvers, instead choose one of the more specific options above (or, better, avoid.eval*()
altogether!)
.eval*()
might be going away!We are currently evaulating whether to remove .eval*()
completely from
Grafast in a future version, see issue
#2060.
If possible, you should avoid branching the plan and instead incorporate the required logic into your step classes directly.