Table of Contents
Major Grafast beta: three down, one to go
Announced 2025-03-24 by Benjie
In the first Grafast Working Group, we outlined 4 major issues in Grafast that needed to be addressed before we could think about general release. With this release, 3 of these are now complete!
- ✅⤵️ Global dependencies - solved via "unary" steps
- ✅⤵️ Early exit - solved via "flags"
- ✅🎉 Eradicating eval - this release!
- 🤔🔜 Polymorphism
After 3 months of gruelling work, we're proud to announce that the third of
these, eradicating eval, is now addressed with the launch of
grafast@0.1.1-beta.21
(used as the core execution engine in
postgraphile@5.0.0-beta.39
). Let's look into what that has meant.
Input evaluation moved to runtime
Since the beginning, Grafast has had the ability to add plan resolvers not just to fields, not just to arguments, but also to input object fields (including those within lists). This made Grafast's planning really ergonomic for things like nested filters, which was great for PostGraphile! But it turns out it's really problematic for certain shapes of input — planning would put constraints on the variables compatible with the plan, requiring potentially unlimited numbers of operation plans needing to be built for the same GraphQL document. Worse: for large input trees involving lists, the number of steps generated could be overwhelming, resulting in the deduplication phase taking excessive time.
One particular user example that could cause 4 minutes of planning time from
just a 100kB input made it clear that we had overreached with using plan
resolvers too deep into inputs; so we've scaled it back so that you can only add
plan resolvers to fields and arguments, you can no longer attach applyPlan
or
inputPlan
to input object fields. This was something that we used a lot
internally (hence the huge time investment migrating away!), but very few people
(no-one?) used externally so it was ripe for removal.
That problematic query that took 4 minutes to plan before? It now takes 1.1ms to plan, yielding a 200,000x speedup!
What does this mean for my codebase?
Hopefully good things! Please update to the latest @beta
of all the
PostGraphile and/or Grafast related modules you're using (including plugins)
and for most users everything should work as before, only better.
I've outlined some of the most common changes you may need to make below, but if you are impacted by any other changes, please ask for help in the chat — AFAIK most of the other things that have had significant changes are used by almost no-one except me, so it doesn't make sense for me to invest time documenting it here. If you're curious, many items are documented in both the changelogs and the pull requests where the changes occurred.
Change fieldArgs.get
to fieldArgs.getRaw
Because we've removed inputPlan
, the fieldArgs.get(key)
method is no more;
instead use fieldArgs.getRaw(key)
which is equivalent unless the inputs had
plans (which they cannot any more).
Converting applyPlan
and inputPlan
If your input object fields did have plan resolvers then instead of having
Grafast automatically call them on each and every input field recursively at
plan-time, we now have the applyInput
and bakedInput
steps that represent
runtime application or transform of these inputs recursively via a single step
in our plan diagram.
We've managed to make this new runtime system very similar in shape to the old
plan-time system, so PostGraphile plugins don't need to change much — this was
largely enabled by how closely we managed to get the Grafast plan syntax to the
syntax of code you would normally write at runtime. The first change is to
rename applyPlan
to apply
, and inputPlan
to baked
. From there, your code
might just work straight away, or it might need some more small tweaks (e.g.
fieldArgs
is no longer present, it's been replaced with simply the runtime
value of the current field).
No more $step.eval*()
The eval methods are now marked as internal so you will get TypeScript errors if you try and use them. They will likely be removed at some point after release, so you should be sure to migrate away from using them at your earliest opportunity. But you weren't using them anyway… right?
ExecutableStep renamed to Step
This one is more cosmetic…
Since we no longer have plan resolvers deep in inputs, we no longer have the
ModifierStep
system that was used for managing them (it's been replaced with
Modifier
which happens at runtime). Since we no longer have ModifierStep, we
no longer need BaseStep
to be separate from and inherited by ExecutableStep
,
so we've merged them. Since this is the base class for all steps now, we've
renamed it to simply Step
.
We have kept an ExecutableStep
export for backwards
compatibility.
PostGraphile changes
In addition to the changes above that impact everything that uses Grafast, here are some of the changes that specifically impact PostGraphile users.
SQL generation moved to runtime
PostGraphile's various SQL-running steps like PgSelectStep now build their
queries at runtime rather than plantime. They use the "builder" pattern, where
much of the SQL query can be established at plan-time, but final tweaks can be
applied at run-time (register tweaks via the $pgSelect.apply($callback)
method) before the query is built.
SQL efficiency increased
Since we have more information at run-time, our SQL queries were able to become even simpler, 10% smaller on average across our test suite! This nets us a modest performance improvement inside PostgreSQL, but the shift to runtime does cost us a little performance in the JS layer since queries now need to be built for every request, rather than once per plan. We're happy with this tradeoff; one of the core goals of PostGraphile V5 (and the motivation for Grafast in the first place) was to shift load from the PostgreSQL layer (which is non-trivial to scale) to the Node.js layer (which is easy to scale horizontally).
Postgres Arrays now parse 5x faster
I've also backported
these fixes into the
postgres-array
npm module for everyone that uses pg
to benefit from.
Easier to write SQL fragments
Added a new feature to pg-sql2
that allows us to handle non-SQL parameter
embeds with custom code, making it easier to write custom SQL, e.g. if a value
is already coming from SQL you can embed it directly without having to invoke
placeholder:
const $fooId = $foo.get('id');
-$pgSelect.where(sql`foo_id = ${$pgSelect.placeholder($fooId)}`);
+$pgSelect.where(sql`foo_id = ${$fooId}`);
We've also added the ability to embed dynamic SQL fragments that can be dependent on runtime values (these values must be unary, i.e. they must come from GraphQL field arguments or derivatives thereof):
const $includeArchived = fieldArgs.getRaw("includeArchived");
const $condition = lambda($includeArchived, includeArchived =>
includeArchived ? sql.true : sql`is_archived is false`
);
$pgSelect.where($condition);
Additional changes
makeGrafastSchema
- 🚨The structure of
makeGrafastSchema
as it relates to arguments and input object fields has changed a little; use TypeScript to guide you. I'm hoping this is the last change of its kind before release. - New shortcuts added for argument
applyPlan()
and input fieldapply()
methods. - Trimmed a load of unnecessary exported code, such as empty objects and field resolvers that do the same as the default field resolver.
- Fix bug in
makeGrafastSchema
that fails to build schema sometimes if a field uses a function shortcut rather than object definition. - Fix bug in
makeGrafastSchema
that sometimes doesn't allow defining input objects
🚨 If you use graphile-export
to export your schema as executable code, be
sure to regenerate your schemas as the old generated code could be
misinterpreted by the new makeGrafastSchema
.
graphile-export
- Massively improved the executable code output from
graphile-export
in combination with the changes tomakeGrafastSchema
above. - PostGraphile's "kitchen sink" schema export code now outputs 37KLOC rather than 47KLOC - a significant reduction in complexity!
Improved plan diagrams
- Plan diagrams now reveal (via
@s
text) if a step is meant to be streamed. - Constant steps improved.
Object: null prototype
simplified to§
in output.- Hoist steps during
optimize
phase. - We no longer render dependencies on the
undefined
constant, because it's messy and doesn't add value - We group when there are multiple dependencies to the same step from the same step, and label the line with the count instead.
Step classes
When writing your own step classes:
ExecutionValue
has gained a new.unaryValue()
method that returns the unary value for unary execution values, and throws an error for non-unary execution values. This is much safer than the previous.at(0)
trick which did not assert that you were actually dealing with a unary execution value.- If you were using
@stream
(incremental delivery) and had written your ownStep
class with stream support, first of all: amazing! Please let me know you did that (via email or Discord)! Secondly, you'll need to either rename yourstream
function toexecute
or merge its code into your existingexecute
method if you have one. It turns out there wasn't much point in separating them, and you can confer a lot of benefit from merging them.
Other Grafast improvements
- Compatible mutation operations can now complete synchronously via
grafastSync()
- Fixes bug in input objects where keys that weren't set would still be present
with value
undefined
- Fix bug in step caching relating to polymorphism
- New
items()
conventional method for extracting the items from a collection (makes for easier compatibility with connections) - Error handling improved
- Lists improved - especially error handling and deduplication logic; as well as allowing returning connection-capable steps in list positions
- Optimization to Grafast's internal execution values, which are used heavily in hot paths.
Thank you Sponsors
Grafast and PostGraphile are crowd-funded open-source software, they rely on crowd-sourced funding from individuals and companies to keep advancing.
If your company benefits from Grafast, PostGraphile or the wider Graphile suite, you should consider asking them to fund our work. By significantly reducing the amount of work needed to achieve business goals and reducing running costs, Graphile's software results in huge time and money savings for users. We encourage companies to contribute a portion of these savings back, enabling the projects to advance more rapidly, and result in even greater savings for your company. Find out more about sponsorship here on our website.