Skip to content

Instantly share code, notes, and snippets.

@lutter
Last active April 16, 2020 18:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lutter/060ca52f0cbe655ec38485a7c1843352 to your computer and use it in GitHub Desktop.
Save lutter/060ca52f0cbe655ec38485a7c1843352 to your computer and use it in GitHub Desktop.
Subgraph composition

GraphQL schema

type _Schema_
  @import(
    types: ["Address", { name: "Account", as: "EthAccount" }],
    from: { id: "Qmsubgraph" })

A string entry in types is shorthand for { name: STRING, as: STRING }

Types that are explicitly imported are available in the schema under the name mentioned in as. All other types are imported by prefixing their name with the subgraph ID. The implementation of the import process must take care to ensure that nested imports do not lead to multiple definitions of the same type in the importing subgraph.

The as names of types are treated as if the importing schema contained type definitions for them, and the schema is validated accordingly.

Only types that are explicitly defined in a schema can be imported. It is not possible to import types from a schema that itself imports that type.

For example, if the subgraph we are importing from above has the GraphQL schema

type Address {
  id: ID!
  owner: User!
}

type User {
  id: ID!
  name: String!
}

type Account {
  id: ID!
  owner: User!
}

then the import statement above has the same effect as putting the following definitions into the GraphQL schema of the importing subgraph; we also add directives to help keep track of where things come from.

type Address @imported(from: "Qmsubgraph", type: "Address", direct: true) {
  id: ID!
  owner: I01_User!
}

type Qmsubgraph_User @imported(from: "Qmsubgraph", type: "User", direct: false) {
  id: ID!
  name: String!
}

type EthAccount @imported(from: "Qmsubgraph", type: "Account", direct: true) {
  id: ID!
  owner: I01_User!
}

The @imported directive always points to the subgraph that actually stores the data for that type. The direct attribute indicates whether this type is directly imported and can therefore be queried through the GraphQL API.

Anywhere where we inject @subgraphId directives today, we will use the id of the source subgraph if the type has an @imported directive.

The API schema is derived from the input schema after expanding imports in the usual way, except that only imported types with a direct: true annotation are turned into attributes of the Query and Subscription root types.

Implementation details

We carry out the expansion of imports very early on in our schema processing (where exactly TBD) The expansion requires acces to a Store since we need to look up the input schema of the imported subgraphs. Because those input schemas will have their imports expanded already, no recursion is needed to deal with nested imports. The expansion simply keeps existing @imported directives, and adds new ones to the types that don’t have one yet.

Database schema

When constructing the Layout, we only generate tables for types that do not have an @imported annotation.

Deployment

When deploying a composed subgraph, all its components must already be deployed. It is a deploy time error if they are not.

A composed subgraph will keep all its components assigned. Today, a deployment will be assigned if it is the current or pending version of a subgraph. This condition will need to be pushed down transitively to all component subgraphs. In other words, a deployment will be assigned if it is the current or pending version of a subgraph, or transitively used as an import by the current or pending version of a subgraph.

To facilitate this logic, we will explicitly track the relationship between composed subgraphs and their components by adding an imports field to SubgraphDeployment:

type SubgraphDeployment {
  id: ID!
  imports: [SubgraphDeployment!]!
  .. other fields we have today ..
}

The list of active subgraphs, i.e., subgraphs that should be assigned can be generated with the following SQL query:

with recursive active(id) as (
  -- base case: current and pending versions
  select d.id
    from subgraph_deployment d
   where exists (select 1
                   from subgraph s, subgraph_version v
                  where d.id = v.deployment
                    and v.id in (s.current_version, s.pending_version))
   union
  -- iteration: add imports of active subgraphs
  select unnest(d.imports)
    from subgraph_deployment d, active a
   where d.id = a.id)
select id from active

This change in assignment rules will require changes to the assignment machinery in core::subgraph::registrar::create_subgraph_version and Store.reconcile_assignments That part of the implementation is nontrivial, and is complicated by the fact that it needs to compute changes to assignments based on metadata changes that are not reflected in the database yet.

Querying

Query execution right now does not access tables from more than one subgraph. That will change ones composed subgraphs can define interfaces and implement them on imported types.

As such, for now query execution simply needs to pick the right subgraph and actual type name when it constructs the EntityQuery in graphql::store::query::build_query: when a type has an @imported annotation, use the subgraph and type name given in the @imported annotation, otherwise use the @subgraphId and the given type name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment