Schema Types¶
A GraphQL schema might look like the one below. Do not be intimidated by the number of components since we will immediately proceed to dissect the schema.
schema {
query: RootSchemaQuery
}
type RootSchemaQuery {
Animal: [Animal]
Entity: [Entity]
Food: [Food]
Species: [Species]
Toy: [Toy]
}
directive @filter(op_name: String!, value: [String!]) on FIELD | INLINE_FRAGMENT
directive @tag(tag_name: String!) on FIELD
directive @output(out_name: String!) on FIELD
directive @output_source on FIELD
directive @optional on FIELD
directive @recurse(depth: Int!) on FIELD
directive @fold on FIELD
scalar Date
scalar DateTime
scalar Decimal
type Animal implements Entity {
_x_count: Int
uuid: ID
name: String
alias: [String]
color: String
birthday: Date
net_worth: Decimal
in_Animal_ParentOf: [Animal]
out_Animal_ParentOf: [Animal]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
out_Animal_OfSpecies: [Species]
out_Animal_PlaysWith: [Toy]
}
type Food implements Entity {
_x_count: Int
uuid: ID
name: String
alias: [String]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
in_Species_Eats: [Species]
}
type Species implements Entity {
_x_count: Int
uuid: ID
name: String
alias: [String]
in_Animal_OfSpecies: [Animal]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
in_Species_Eats: [Species]
out_Species_Eats: [Union__Food__Species]
}
type Toy {
_x_count: Int
uuid: ID
name: String
in_Animal_PlaysWith: [Animal]
}
interface Entity {
_x_count: Int
uuid: ID
name: String
alias: [String]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
}
union Union__Food__Species = Food | Species
Note
A GraphQL schema can be serialized with the print_schema
function from the
graphql.utils.schema_printer
module.
Objects types and fields¶
The core components of a GraphQL schema are GraphQL object types. They conceptually represent the concrete vertex types in the underlying database. For relational databases, we think of the tables as the concrete vertex types.
Lets go over a toy example of a GraphQL object type:
type Toy {
_x_count: Int
name: String
in_Animal_PlaysWith: [Animal]
}
Here are some of the details:
_x_count
: is a meta field. Meta fields are an advanced compiler feature.name
is a property field that represents concrete data.in_Animal_PlaysWith
is a vertex field representing an inbound edge.String
is a built-in GraphQL scalar type.[Animal]
is a GraphQL list representing a list ofAnimal
objects.
Directives¶
Directives are keywords that modify query execution. The compiler includes a list of directives, which we’ll talk about more in the query directives section. For now lets see how they are defined by looking at an example:
directive @output(out_name: String!) on FIELD
@output
defines the directive name.out_name: String!
is a GraphQL argument. The!
indicates that it must not be null.on FIELD
defines where the directive can be located. According to the definition, this directive can only be located next to fields. The compiler might have additional restrictions for where a query can be located.
Scalar types¶
The compiler uses the built-in GraphQL scalar types as well as three custom scalar types:
DateTime
represents timezone-naive second-accuracy timestamps.Date
represents day-accuracy date objects.Decimal
is an arbitrary-precision decimal number object useful for representing values that should never be rounded, such as currency amounts.
Operation types¶
GraphQL allows for three operation types query, mutation and subscription. The compiler only allows for read-only query operation types as shown in the code snippet below:
schema {
query: RootSchemaQuery
}
A query may begin in any of the root vertex types specified by the special
RootSchemaQuery
object type:
type RootSchemaQuery {
Animal: [Animal]
Entity: [Entity]
Food: [Food]
Species: [Species]
Toy: [Toy]
}
Inheritance¶
The compiler uses interface and union types in representing the inheritance structure of the underlying schema. Some database backends do not support inheritance, (e.g. SQL), so this feature is only supported for certain backends.
Interface types¶
Object types may declare that they implement an interface type, meaning that they contain all property and vertex fields that the interface declares. In many programming languages, this concept is called interface inheritance or abstract inheritance. The compiler uses interface implementation in the GraphQL schema to model the abstract inheritance in the underlying database.
interface Entity {
_x_count: Int
name: String
alias: [String]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
}
type Food implements Entity {
_x_count: Int
name: String
alias: [String]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
in_Species_Eats: [Species]
}
Querying an interface type without any type coercion returns all of the
the objects implemented by the interface. For instance, the following query returns the name of all
Food
, Species
and Animal
objects.
{
Entity {
name @output(out_name: "entity_name")
}
}
Union types and type_equivalence_hints
¶
GraphQL’s type system does not allow object types to inherit other object types (i.e. it has no notion of concrete inheritance). However, to model the database schema of certain backends and to emit the right query in certain cases, the compiler needs to have a notion of the underlying concrete inheritance.
In order to work around this limitation, the GraphQL compiler uses GraphQL union types as means of
listing the subclasses of an object with multiple implicit subclasses. It also takes in a
type_equivalence_hints
parameter to match an object type with the union
type listing its subclasses.
For example, suppose Food
and Species
are concrete types and Food
is a
superclass of Species
in an OrientDB schema. Then the GraphQL schema info generation
function would generate a union type in the schema
union Union__Food__Species = Food | Species
as well an entry in type_equivalence_hints
mapping Food
to
Union_Food_Species
.
To query an union type, one must always type coerce to one of the encompassed object types as illustrated in the section below.
Type coercions¶
Type coercions are operations than can be run against interfaces and unions to create a new scope whose type is different than the type of the enclosing scope of the coercion. Type coercions are represented with GraphQL inline fragments.
Example Use¶
{
Species {
name @output(out_name: "species_name")
out_Species_Eats {
... on Food {
name @output(out_name: "food_name")
}
}
}
}
Here, the out_Species_Eats
vertex field is of the
Union__Food__FoodOrSpecies__Species
union type. To proceed with the
query, the user must choose which of the types in the
Union__Food__FoodOrSpecies__Species
union to use. In this example,
... on Food
indicates that the Food
type was chosen, and any
vertices at that scope that are not of type Food
are filtered out
and discarded.
{
Species {
name @output(out_name: "species_name")
out_Entity_Related {
... on Species {
name @output(out_name: "entity_name")
}
}
}
}
In this query, the out_Entity_Related
is of Entity
type.
However, the query only wants to return results where the related entity
is a Species
, which ... on Species
ensures is the case.
Constraints and Rules¶
- Must be the only selection in scope. No field may exist in the same scope as a type coercion. No scope may contain more than one type coercion.
Meta fields¶
Meta fields are fields that do not represent a property/column in the underlying vertex type. They are also an advanced compiler feature. Before continuing, readers should familiarize themselves with the various query directives supported by the compiler.
__typename¶
The compiler supports the standard GraphQL meta field __typename
,
which returns the runtime type of the scope where the field is found.
Assuming the GraphQL schema matches the database’s schema, the runtime
type will always be a subtype of (or exactly equal to) the static type
of the scope determined by the GraphQL type system. Below, we provide an
example query in which the runtime type is a subtype of the static type,
but is not equal to it.
The __typename
field is treated as a property field of type
String
, and supports all directives that can be applied to any other
property field.
Example Use¶
{
Entity {
__typename @output(out_name: "entity_type")
name @output(out_name: "entity_name")
}
}
This query returns one row for each Entity
vertex. The scope in
which __typename
appears is of static type Entity
. However,
Animal
is a type of Entity
, as are Species
, Food
, and
others. Vertices of all subtypes of Entity
will therefore be
returned, and the entity_type
column that outputs the __typename
field will show their runtime type: Animal
, Species
, Food
,
etc.
_x_count¶
The _x_count
meta field is a non-standard meta field defined by the
GraphQL compiler that makes it possible to interact with the number of
elements in a scope marked @fold
. By applying directives like
@output
and @filter
to this meta field, queries can output the
number of elements captured in the @fold
and filter down results to
select only those with the desired fold sizes.
We use the _x_
prefix to signify that this is an extension meta
field introduced by the compiler, and not part of the canonical set of
GraphQL meta fields defined by the GraphQL specification. We do not use
the GraphQL standard double-underscore (__
) prefix for meta fields,
since all names with that prefix are explicitly reserved and prohibited
from being
used
in directives, fields, or any other artifacts.
Adding the _x_count
meta field to your schema¶
Since the _x_count
meta field is not currently part of the GraphQL
standard, it has to be explicitly added to all interfaces and types in
your schema. There are two ways to do this.
The preferred way to do this is to use the
EXTENDED_META_FIELD_DEFINITIONS
constant as a starting point for
building your interfaces’ and types’ field descriptions:
from graphql import GraphQLInt, GraphQLField, GraphQLObjectType, GraphQLString
from graphql_compiler import EXTENDED_META_FIELD_DEFINITIONS
fields = EXTENDED_META_FIELD_DEFINITIONS.copy()
fields.update({
'foo': GraphQLField(GraphQLString),
'bar': GraphQLField(GraphQLInt),
# etc.
})
graphql_type = GraphQLObjectType('MyType', fields)
# etc.
If you are not able to programmatically define the schema, and instead
simply have a pre-made GraphQL schema object that you are able to
mutate, the alternative approach is via the
insert_meta_fields_into_existing_schema()
helper function defined by
the compiler:
# assuming that existing_schema is your GraphQL schema object
insert_meta_fields_into_existing_schema(existing_schema)
# existing_schema was mutated in-place and all custom meta-fields were added
Example Use¶
{
Animal {
name @output(out_name: "name")
out_Animal_ParentOf @fold {
_x_count @output(out_name: "number_of_children")
name @output(out_name: "child_names")
}
}
}
This query returns one row for each Animal
vertex. Each row contains
its name, and the number and names of its children. While the output
type of the child_names
selection is a list of strings, the output
type of the number_of_children
selection is an integer.
{
Animal {
name @output(out_name: "name")
out_Animal_ParentOf @fold {
_x_count @filter(op_name: ">=", value: ["$min_children"])
@output(out_name: "number_of_children")
name @filter(op_name: "has_substring", value: ["$substr"])
@output(out_name: "child_names")
}
}
}
Here, we’ve modified the above query to add two more filtering constraints to the returned rows:
- child
Animal
vertices must contain the value of$substr
as a substring in their name, and Animal
vertices must have at least$min_children
children that satisfy the above filter.
Importantly, any filtering on _x_count
is applied after any other
filters and type coercions that are present in the @fold
in
question. This order of operations matters a lot: selecting Animal
vertices with 3+ children, then filtering the children based on their
names is not the same as filtering the children first, and then
selecting Animal
vertices that have 3+ children that matched the
earlier filter.
Constraints and Rules¶
- The
_x_count
field is only allowed to appear within a vertex field marked@fold
. - Filtering on
_x_count
is always applied after any other filters and type coercions present in that@fold
. - Filtering or outputting the value of the
_x_count
field must always be done at the innermost scope of the@fold
. It is invalid to expand vertex fields within a@fold
after filtering or outputting the value of the_x_count
meta field.
How is filtering on _x_count
different from @filter
with has_edge_degree
?¶
The has_edge_degree
filter allows filtering based on the number of
edges of a particular type. There are situations in which filtering with
has_edge_degree
and filtering using =
on _x_count
produce
equivalent queries. Here is one such pair of queries:
{
Species {
name @output(out_name: "name")
in_Animal_OfSpecies @filter(op_name: "has_edge_degree", value: ["$num_animals"]) {
uuid
}
}
}
and
{
Species {
name @output(out_name: "name")
in_Animal_OfSpecies @fold {
_x_count @filter(op_name: "=", value: ["$num_animals"])
}
}
}
In both of these queries, we ask for the names of the Species
vertices that have precisely $num_animals
members. However, we have
expressed this question in two different ways: once as a property of the
Species
vertex (“the degree of the in_Animal_OfSpecies
is
$num_animals
”), and once as a property of the list of Animal
vertices produced by the @fold
(“the number of elements in the
@fold
is $num_animals
”).
When we add additional filtering within the Animal
vertices of the
in_Animal_OfSpecies
vertex field, this distinction becomes very
important. Compare the following two queries:
{
Species {
name @output(out_name: "name")
in_Animal_OfSpecies @filter(op_name: "has_edge_degree", value: ["$num_animals"]) {
out_Animal_LivesIn {
name @filter(op_name: "=", value: ["$location"])
}
}
}
}
versus
{
Species {
name @output(out_name: "name")
in_Animal_OfSpecies @fold {
out_Animal_LivesIn {
_x_count @filter(op_name: "=", value: ["$num_animals"])
name @filter(op_name: "=", value: ["$location"])
}
}
}
}
In the first, for the purposes of the has_edge_degree
filtering, the
location where the animals live is irrelevant: the has_edge_degree
only makes sure that the Species
vertex has the correct number of
edges of type in_Animal_OfSpecies
, and that’s it. In contrast, the
second query ensures that only Species
vertices that have
$num_animals
animals that live in the selected location are returned
– the location matters since the @filter
on the _x_count
field
applies to the number of elements in the @fold
scope.