Developers

Query reference

Plato queries read records: pick a model, filter and order it, and select the fields and relations you want back. A query never changes anything; changes go through the actions your schema declares.

Start from your schema

A query names the models, fields and relations of your database's schema, exactly as the schema spells them: names are case-sensitive.

The examples on this page query this schema:

model Customer {
	fields {
		name String
	}
}

model Invoice {
	fields {
		number String
		status String
		customer Customer? @relation("invoices")
	}
}

model Item {
	fields {
		description String
		amount Int
		invoice Invoice @relation("items")
	}
}

model CreditNote extends Invoice {
	fields {
		reason String
	}
}

Select records and fields

Name a model, then the fields to return in braces. A model can be queried directly; no view needs declaring first.

Invoice { number status }

The result is a list of rows. Every row, and every record nested in one, carries its id whether or not you select it, so you can name the record in a later action.

Filter and order

@where(…) keeps the records its condition holds for, and @orderBy(field) sorts them, ascending unless you add : desc.

Invoice @where(status == "Open") @orderBy(number: desc) {
  number
  status
}

Conditions compare with ==, !=, <, <=, > and >=, and combine with && and ||. Strings are quoted; numbers, true, false and null are not. A condition can follow a reference to a field of the record it points at:

Invoice @where(customer.name == "Acme" && status != "Paid") {
  number
}

Follow relations

Braces after a reference or a relation select from the records it leads to: one record for a reference, a list for a relation.

Invoice @orderBy(number) {
  number
  customer? { name }
  items { description amount }
}

A relation with no records reads as an empty list, and never drops its row. A referenced record you cannot read comes back as its id and title alone.

Empty references: ? and !

customer is optional in the schema, so an invoice may have none. A query must say what it wants then, or it is refused with a message naming both choices. The ? above includes the invoice, with customer as null. A ! leaves it out, returning only the invoices that have a customer:

Invoice @orderBy(number) {
  number
  customer! { name }
}
  • A selected reference needs ? or ! when the schema lets it be empty, or when it carries its own @where, which empties it when the record it points at does not match; a @where filters the record, so it goes on a braced reference. A reference the schema requires needs neither marker, though it may take either.
  • This holds for a bare reference (customer!, its id), a braced one, and a document field. A bare reference is empty only when it holds no id; a braced one also when the record it names no longer exists.
  • A reference you cannot read is not empty: under ! its row is kept, with the reference's id and title.
  • Emptiness passes outward only through !: a reference marked ! whose own reference marked ! is empty is empty in turn.
  • A relation to many records, a value, and an aggregate take no marker and are never left out for being empty.
  • @first(n) counts only the rows returned, so a page is not cut short by the rows left out.
  • A view declared in a schema is checked the same way when the schema is installed, so a change that makes a field it selects optional is refused until the view says which it means. A refusal names every selection to fix at once.

Subtypes

Braces after a subtype's name select its own fields, on a record of a model it extends. A record may or may not be of that type, so a subtype selection is optional like a reference and takes ? or !. With !, only credit notes are returned; with ?, every invoice, with CreditNote as null for the others:

Invoice @orderBy(number) {
  number
  CreditNote! { reason }
}
  • A @where on a subtype selection filters the subtype record: CreditNote! @where(…) keeps only the credit notes it admits, and with ? the selection is null for the others. With a @where, even a selection of the model being read or one it extends needs a marker.
  • Braces after a model's name select only a subtype of the model being read, or that model or one it extends, which need no marker. Any other model, and any model selected on a derived model, is refused: query it directly.
  • Whoever can read a record can read every type it is, so a subtype selection decides only on records you can read. Under a reference you cannot read it never leaves a row out: the reference comes back as its id and title.

Aliases and aggregates

name: field returns a field under another name. An aggregate folds a relation into one value per row: @count, @sum(field), @avg(field), @min(field) and @max(field). A @where before the aggregate folds only the records it keeps.

Invoice @orderBy(number) {
  invoiceNumber: number
  itemCount: items @count
  total: items @sum(amount)
  large: items @where(amount >= 1000) @count
}

Page through long results

@first(n) returns at most n rows, with a pageInfo beside them:

Invoice @orderBy(number) @first(2) { number }

When pageInfo.hasNextPage is true, repeat the same query with @after and the pageInfo.endCursor it returned to read the next page:

Invoice @orderBy(number) @first(2) @after("<endCursor>") { number }

A cursor is refused by a query of another model or order than the one that returned it. Only the top level of a query pages; nested relations do not.

Limits

A query Plato runs on its server reads within a fixed budget. A query that needs more is refused whole, with a message naming the limit it passed; it never returns part of a result.

  • At most 100,000 stored entries and 16,000,000 characters of stored data per query, counted beneath sorting, joins and nested selections.
  • A query that selects only aggregates, such as n: items @count, may read up to 1,000,000 entries within the same characters.

Coming under the limit

Add a @where filter, page with @first(n), or select fewer fields and nested relations. @orderBy sorts every matching row before the first page is cut, so paging a sorted query does not bring it under the limit: narrow it with a filter instead.

Errors

A query that cannot run is refused with a message saying what went wrong. It never returns an empty result in its place.

  • The text does not parse. Plato queries are not SQL; check the syntax above.
  • A name is unknown, or types do not match. The query names a model or field the schema lacks, or compares values of different types. Check the names against the schema.
  • The query read too much. See Limits; the same query will be refused again.