Skip to content

Commit

Permalink
docs: a page about eval() (#2213)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie authored Oct 16, 2024
2 parents eeffdb0 + 958dbdf commit 1942056
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 1 deletion.
4 changes: 4 additions & 0 deletions grafast/website/grafast/access-control.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
sidebar_position: 12
---

# Access control

Access control is typically the responsibility of your business logic layer, as
Expand Down
129 changes: 129 additions & 0 deletions grafast/website/grafast/eval.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
title: Plan branching via `.eval*()`
sidebar_position: 13
---

import Mermaid from "@theme/Mermaid";

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:

```graphql
query GetUserDetails($includeFriends: Boolean! = false) {
currentUser {
name
avatarUrl
friends(first: 100) @include(if: $includeFriends) {
name
avatarUrl
}
}
}
```

<Mermaid
chart={String.raw`%%{init: {'themeVariables': { 'fontSize': '12px'}}}%%
flowchart TD
classDef path fill:#eee,stroke:#000,color:#000
classDef plan fill:#fff,stroke-width:1px,color:#000
classDef itemplan fill:#fff,stroke-width:2px,color:#000
classDef unbatchedplan fill:#dff,stroke-width:1px,color:#000
classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000
classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left
subgraph "includeFriends=false"
ZAccess6{{"Access[6∈0] ➊<br />ᐸ2.currentUserIdᐳ"}}:::plan
Z__Value2["__Value[2∈0] ➊<br />ᐸcontextᐳ"]:::plan
Z__Value2 --> ZAccess6
ZLoad7[["Load[7∈0] ➊<br />ᐸuserByIdᐳ"]]:::plan
ZAccess6 --> ZLoad7
end
subgraph "includeFriends=true"
Access6{{"Access[6∈0] ➊<br />ᐸ2.currentUserIdᐳ"}}:::plan
__Value2["__Value[2∈0] ➊<br />ᐸcontextᐳ"]:::plan
__Value2 --> Access6
Load7[["Load[7∈0] ➊<br />ᐸuserByIdᐳ"]]:::plan
Access6 --> Load7
Load10[["Load[10∈0] ➊<br />ᐸfriendshipsByUserIdᐳ"]]:::plan
Access6 --> Load10
__Item14[/"__Item[14∈3]<br />ᐸ10ᐳ"\]:::itemplan
Load10 ==> __Item14
Access16{{"Access[16∈3]<br />ᐸ14.friend_idᐳ"}}:::plan
__Item14 --> Access16
Load17[["Load[17∈3]<br />ᐸuserByIdᐳ"]]:::plan
Access16 --> Load17
end
classDef bucket0 stroke:#696969
class Bucket0,__Value2,__Value4,Access6,Load7,Load10 bucket0
class ZAccess6,Z__Value2,ZLoad7 bucket0
classDef bucket1 stroke:#00bfff
class Bucket1 bucket1
classDef bucket3 stroke:#ffa500
class Bucket3,__Item14,Access16,Load17 bucket3
classDef bucket4 stroke:#0000ff
class Bucket4 bucket4
`}
/>

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`.

:::note Plans are established on demand

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 2<sup>x</sup> 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
specified `key` 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!)

:::info `.eval*()` might be going away!

We are currently _evaulating_ whether to remove `.eval*()` completely from
Grafast in a future version, see [issue
#2060](https://github.com/graphile/crystal/issues/2060).

**If possible, you should avoid branching the plan and instead incorporate the
required logic into your step classes directly.**

:::
1 change: 1 addition & 0 deletions grafast/website/grafast/plan-diagrams.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
title: "Plan diagrams"
sidebar_position: 10
---

import Mermaid from "@theme/Mermaid";
Expand Down
4 changes: 4 additions & 0 deletions grafast/website/grafast/polymorphism.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
sidebar_position: 11
---

# Polymorphism

GraphQL has two types of output polymorphism currently: interfaces and unions.
Expand Down
4 changes: 4 additions & 0 deletions grafast/website/grafast/production-considerations.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
sidebar_position: 14
---

# Production Considerations

## Overview
Expand Down
4 changes: 3 additions & 1 deletion postgraphile/website/postgraphile/evaluating.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
---
layout: page
path: /postgraphile/evaluating/
title: Evaluating
title: Evaluating PostGraphile
fullTitle: Evaluating PostGraphile For Your Project
---

# Does PostGraphile Fit Your Project?

Hopefully you’ve been convinced that PostGraphile serves an awesome GraphQL API,
but now let’s take a more critical look at whether or not you should adopt
PostGraphile for your project.
Expand Down
5 changes: 5 additions & 0 deletions postgraphile/website/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ figure figcaption {
background-color: var(--docusaurus-highlighted-code-line-bg);
}
*/
.theme-doc-sidebar-item-link h4 {
margin-bottom: 5px;
margin-top: 5px;
padding-top: 5px;
}
.ul-check li {
background-image: url("../../static/img/check.svg");
background-repeat: no-repeat;
Expand Down

0 comments on commit 1942056

Please sign in to comment.