Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor GraphQL execution #3005

Merged
merged 30 commits into from
Jan 24, 2022
Merged

Refactor GraphQL execution #3005

merged 30 commits into from
Jan 24, 2022

Conversation

lutter
Copy link
Collaborator

@lutter lutter commented Nov 24, 2021

Refactor how we execute GraphQL queries by transforming the AST we get from graphql_parser into a form that is easier to execute. The preprocessing step that translates from graphql_parser's AST to our AST does the following:

  • interpolate variables
  • coerce argument values to their correct type
  • expand fragments so that our selection sets only contain fields
  • resolve interfaces (and unions) into their constituent object types

This removes a lot of hard-to-follow code in query execution, especially in prefetch.

Besides these simplifications, this PR also fixes a few bugs, and changes the order in which fields appear in the response to what the GraphQL spec mandates. This is a breaking change for the network, as it changes the hash for semantically equivalent query responses (cc @That3Percent on rolling this out on the network)

The refactored code also makes a few weaknesses of the current query execution visible, in particular, field merges (from users specifying the same field multiple times, or from fragment expansion) continue to use a yolo strategy that is not entirely standard conformant. This needs to be addressed in a separate PR.

Fixes #2961.

@kamilkisiela
Copy link
Contributor

@lutter Hi, I started a Pull Request for graphql_parser that introduces Document Transformer graphql-rust/graphql-parser#56

Not sure it's useful but with such trait, we could have one API for transformations and split the transformation logic into multiple pieces. What do you think?

Copy link
Collaborator

@leoyvens leoyvens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comments from reading ast.rs.

pub struct SelectionSet {
// Map object types to the list of fields that should be selected for
// them
items: Vec<(String, Vec<Field>)>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is items.len() > 1 only when the selection is over a union or interface? If so it would be clarifying to note that in the comment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to use a type for strings that represent object names (or type names in general, whatever makes more sense).

Copy link
Collaborator Author

@lutter lutter Nov 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've wanted to use something like &s::ObjectType here, but that's not possible for various reasons. I've now resolved to do the next best thing, which is keeping a copy of all object types in ApiSchema, and wrapping them in an Arc. With that, a SelectionSet now has basically items: Vec<(Arc<s::ObjectType>, Vec<Field>)> (in reality a little more complicated to work around other shortcomings of s::ObjectType)

I am curious what you think of that approach.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I see that saved some calls to get_object_type_definition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment to clarify when items.len() > 1

@That3Percent
Copy link
Contributor

Our arbitration charter allows for flexibility around bugfixes, so we are good to go to push this to The Network @lutter.

@lutter
Copy link
Collaborator Author

lutter commented Nov 24, 2021

@lutter Hi, I started a Pull Request for graphql_parser that introduces Document Transformer graphql-rust/graphql-parser#56

Not sure it's useful but with such trait, we could have one API for transformations and split the transformation logic into multiple pieces. What do you think?

I saw that PR and was sad that it was just a PR (I even went as far as trying to implement a visitor, but then decided it's too much work for what we need) Now that I've been through this refactor, I actually think it would be better if graphql_parser was more opinionated than just offering transformers, and instead had something along the lines of what this PR does, i.e., a preprocessing step for GraphQL queries. A lot of this PR is very generic, and useful for anyone who wants to execute GraphQL queries, but by necessity closely tied to our codebase. The things that I think should live upstream are

  • the graph::data::graphql::ext module which is just an attempt to make the graphql_parser API more ergonomic (in graphql_parser, these could just be methods on its types, rather than the awkward dance with extension traits)
  • the graphql::execution::ast module and the transformation to turn a graphql_parser query into that form in graphql::execution::query
  • the IntrospectionResolver and the code that merges the introspection schema into the user's schema (APISchema.schema in our case) Keeping the introspection schema and the user schema separate lead to a lot of awkward code which is now gone, but even that you have to switch resolvers is a bit awkward.

The biggest question around doing that to me is how values should be handled, since they are user-specific, and the graphql_parser API for the above would need to be templated in some form around the concrete notion of Value, and, for example, how to coerce a q::Value to the user's notion of Value.

Besides these, one big feature request I'd have for graphql_parser is to use more Arcs in the schema AST, especially for types, since the current data structures make it hard to keep pointers to types around, and you end up referencing them with strings that need to be looked up in the schema all the time.

I have no idea what the upstream devs would think about these changes, but it would make our lives a lot easier.

@lutter
Copy link
Collaborator Author

lutter commented Nov 29, 2021

Addressed all the comments so far

@lutter
Copy link
Collaborator Author

lutter commented Jan 19, 2022

Rebased to latest master and a dded a few commits to replicate the behavior of the current GraphQL execution for some queries that are really invalid

@lutter lutter force-pushed the lutter/gql branch 2 times, most recently from 3727335 to 4c4a261 Compare January 20, 2022 20:15
@lutter
Copy link
Collaborator Author

lutter commented Jan 21, 2022

This PR is now ready for review (and hopefully merging/rollout soon)

I've tested this branch against all the queries we get in the hosted service, and the only difference in responses comes from treating the (invalid) construct of not having a selection on a field with an object type slightly differently: the current implementation includes an empty object (or list of empty objects, depending on the field type) in the response, with this PR such fields do not appear in the response.

In other words, for a type like

type Person @entity {
  id ID!
  spouse Person!
  friends [Person!]!
}

and the query

{ persons { id spouse friends } }

with current master, the response has { "id": "..", "spouse": {}, friends: [ {}, {}, {}]} for each person in it (with one empty object in the list for each friend). With this PR, the response would contain neither a spouse nor a friends field. The impact on apps should be negligible since they've been getting no real data all along.

lutter added 16 commits January 24, 2022 15:38
No cows were harmed in making this change
That is demanded by the GraphQL spec. We need to have a predictable order
to ensure attestations remain stable, though this change breaks
attestations since it changes the order in which fields appear in the
output from alphabetical (however a `BTreeMap` orders string keys) to the
order in which fields appear in a query.

It also allows us to replace `BTreeMaps`, which are fairly memory
intensive, with cheaper `Vec`.

The test changes all reflect the changed output behavior; they only reorder
fields in the expected output but do not otherwise alter the tests.

Fixes #2943
Rather than use a string name, use the actual object type to identify
types. It's not possible to do this with plain references, for example,
because we pass a reference to a SelectionSet to graph::spawn_blocking, so
we do the next best thing and use an Arc. Unfortunately, because
`graphql_parser` doesn't wrap its object types in an Arc, that means we
need to keep a copy of all of them in ApiSchema.
This just shuffles some code around, but doesn't change anything else, in
preparation for representing the schema in a way that's more useful to us.
Set ENABLE_GRAPHQL_VALIDATIONS to any value in the environment to enable
validations, rather than enabling them by default and disabling them on
demand
Make sure that we handle queries that have a selection from a scalar
field by ignoring the selection or that have no selection for a non-scalar
field by ignoring that field.

The latter differs from the previous behavior where the result would
contain an entry for such a field, but the data for the field would be an
empty object or a list of empty objects.
Instead of immediately reporting an error, treat missing variables as nulls
and let the rest of the execution logic deal with nulls
@lutter lutter merged commit 9e6435b into master Jan 24, 2022
@lutter lutter deleted the lutter/gql branch January 24, 2022 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GraphQL simplification & refactor
5 participants