Skip to main content

Operation plan

tip

If you're unfamiliar with GraphQL terminology such as "selection set", "field", "type" and so on, please check out our Operation Language and Schema Language cheatsheets.

When Grafast sees an operation for the first time, it builds an operation plan, which is the combination of an execution plan and an output plan. To do so, it walks the selection sets calling the user-provided plan method for each field to determine the steps that need to be executed for that field. That series of steps, which we'll call the field plan, is then woven with all the other field plans in the operation, to form a directed acyclic graph we call the execution plan. Whilst doing this, Grafast keeps track of which fields returned which steps, and uses this information to form the output plan. Finally the execution plan is optimized and finalized and the output plan is finalized, then they are ready for execution.

A simplified version of the process is this:

  1. Start at the root selection set.
  2. For each field in the current selection set:
    1. Call field plan resolver
    2. Call any uncalled argument applyPlan resolvers
    3. Deduplicate new steps
    4. Repeat step 2 using the field's selection set (if any)
  3. Tree shake
  4. Optimise
  5. Tree shake
  6. Finalise

Execution plan

The execution plan is (generally1) asynchronous. It is responsible for fetching all the data required by the operation in as efficient a manner as possible, and can evolve quite significantly as a result of deduplication, tree shaking and optimization.

Output plan

The data fetched by the execution plan will not be in any particular format, and may have been significantly deduplicated and simplified as part of the operations. The output plan is responsible for taking this bundle of data and formatting it back out as a valid GraphQL response, including serializing all the leaves correctly and handling nulls and errors following the GraphQL specification. The output plan always runs synchronously, though in streaming situations such as GraphQL subscriptions or incremental delivery (@stream/@defer) part of the output plan may be executed for each payload in the underlying stream.

Constraints

Whilst Grafast is building the operation plan, it may also determined particular constraints that govern whether the operation plan may be used for a future request or not. For example, if the request contains @skip(if: $variable) then a different operation plan would be needed depending on whether $variable was true or false. Where possible, constraints are kept as narrow as possible - for example "variable $foo is a list" is preferred over "variable $foo is the list [1,2,3]" - to maximize reuse.

When an operation is seen a future time, Grafast first looks for an existing operation plan whose constraints fit the request before falling back to creating a new operation plan.

Lifecycle events

Deduplicate

Once a field is fully planned, Grafast will deduplicate the new steps it has produced, attempting to replace them with existing "peer" steps that already existed. These "peer" steps will have been constructed via the same step class, and will have the same dependencies. By deduplicating at this stage (before planning the child selection set) we help to ensure that the schema remains in adherence to the GraphQL specification. Deduplication can reduce the number of steps in the plan, leading to greater efficiency (and easier to understand plans).

Tree shake

Once every selection set has been fully visited and every field has been planned, the execution plan is complete.

Grafast then walks through the output plans, their required steps and those steps dependencies (and their dependencies and so on), marking them as active. Any step that is not active is "unreachable" and thus is no longer needed, therefore it can be be removed from the execution plan.

Certain steps are immune to tree shaking (they're seen as always active), in particular these include certain system steps, and any step that has side effects.

Optimize

Once the execution plan is complete and the unnecessary steps have been tree shaken away, Grafast optimizes the plan by calling the optimize lifecycle method on each step that supports it. This gives each step a chance to replace themselves with more optimal forms by inspecting and interacting with their ancestors.

info

For example if a "first" step is to optimize itself, and its parent is a "list" step, then it can simply replace itself with the first entry in the list of plans the "list" step contains. More advanced examples may include topics such as joining tables in a database, or adding selection sets to a remote GraphQL operation.

The optimize method is called starting with the dependencies (leaves) and working its way up the dependents (trunk) of the execution plan's directed acyclic graph. Since plans should only talk to their ancestors (and not their descendents) during optimize, this ensures that their dependencies remain what the class expects until after it is optimized.

Once optimization is complete, Grafast tree shakes again to remove any unnecessary steps from the execution plan.

Finalize

Grafast then finalizes the plan by calling the finalize method on steps that support it. This gives each step a chance to do work that need only be done once, for example:

  • a step that talks to a database might compile the SQL it needs and cache it for later
  • a step that performs templating may build an optimized template function
  • etc

Output plan finalize

Finally, the output plans are finalized, building optimized output functions and referencing the latest optimized steps.


  1. If the execution plan contains no asynchronous steps, then it can be executed synchronously. This is rare, though, as typically it will contain steps that fetch data from remote data sources such as databases or web services.