Query planning
How the plugin turns a GraphQL query into drizzle queries
This page describes how the plugin turns a GraphQL query into drizzle queries, which is worth knowing when a schema issues more queries than you expect.
How fields get their data
A field either reads its data from a row that has already been loaded, or runs a query of its own.
A field's select is planned into the query of the nearest ancestor that runs one: a
t.drizzleField, a t.relation, a connection, or a fallback query. The field
then reads what it needs off the loaded row, without a query of its own. A field runs its own query
when its resolve queries drizzle directly, and when the plugin issues a fallback query for a
relation that is missing from the row.
A field can do both. A t.drizzleField, or any other field with a select, nested under one of
those ancestors has its select planned into the parent's row, and still runs its own query when
it resolves.
A field-level select is merged into the same query as its siblings and the type-level selection,
rather than being kept separate for that field. Two selections of the same relation share a place
in that query only when their arguments (where, orderBy, limit, ...) match. When they differ,
the first one planned wins, and the other is loaded with a query of its own. A type-level select
is planned before any field's selection, no matter where they appear in the document, and fields
are planned in the order they are selected.
Compare two orderings of one relation
A client may need both the newest and oldest published posts on one author page:
query CompareOrderings {
author(id: 1) {
newest: posts {
title
}
oldest: posts(oldestFirst: true) {
title
}
}
}The published-posts field translates those arguments into different database orderings. The two fields cannot reuse the same loaded relation because their orderings differ. The additional ordering is loaded through a fallback query.
In this example, selecting one ordering executes one SQL statement; selecting both executes two. The number of GraphQL field resolvers does not determine the number of database queries.
posts: t.relation('posts', {
args: { oldestFirst: t.arg.boolean() },
query: (args) => ({
where: { published: true },
orderBy: {
createdAt: args.oldestFirst ? 'asc' : 'desc',
id: args.oldestFirst ? 'asc' : 'desc',
},
}),
}),Nested selections
A select function receives nestedSelection, which plans the selection beneath the field for the
field's own type. It returns the query config for that table, or a many config for a list field.
Any keys you pass in are kept as they were given, so columns passed to nestedSelection will
still narrow the parent shape:
builder.drizzleObject('users', {
name: 'User',
fields: (t) => ({
previewPosts: t.field({
type: ['posts'],
select: (args, ctx, nestedSelection) => {
return {
// what the query selects on the posts, limited to one
with: {
posts: nestedSelection({
where: { published: true },
orderBy: { id: 'asc' },
limit: 1,
}),
},
};
},
resolve: (user) => user.posts,
}),
}),
});nestedSelection(query, path, type) takes two more arguments:
path: a list of field names, for selecting a field nested under the field's type. A segment can also be written as{ name, type }to pin the implementation the field must be found under.type: a member of an interface or union, to read the selection as that type.
The nestedSelection function also carries the path and segments of the field being planned,
which are described under Relation queries.
Async selections
When a selection depends on asynchronous request data, enable AsyncSelections: true and await
nested plans. Async selections covers the builder configuration, supported
callbacks, helper options, and errors caused by unsettled promises.
Conflicting selections between variants
When a query selects two variants of one table for the same row, either with a fragment on each
under one field, or through a t.variant field, the plugin will throw a PothosValidationError if
their select options ask for the same relation with different arguments:
PothosValidationError: Type-level selections of Viewer and Admin conflict on relation "posts".
Move the relation arguments to a field-level select on one of the types.The same applies to two variants defining the same extras key with different functions
(... conflict on extra "lowercaseName"). Both variants describe one row, so their select
options are merged into a single query.
To fix this, move the relation with its arguments, or the extra, into the select of the field
that needs it on one of the variants. A field-level selection that conflicts with what the row
already holds falls back to a query for that field, rather than failing the request.