Named queries
Every read of stored records is a named query. An entity is a kind of
stored record, like a table with types (Entities).
A query is one read of an entity: which records, in what order, and how
many. Each query lives in a file of its own in its domain's queries/
folder (Files and names).
You call it by name, as you call an action, the function that changes
records (Functions and actions).
This query gives an owner's todos a page at a time, newest first. Its
filter parameter is a ListTodos, an Input type a route fills from the
request: an optional status, an optional due_before, a page_size
that defaults to 25 and an optional after, the cursor the previous page
returned:
// domains/planning/queries/todos_for_owner.jadpo/// The owner's visible todos, newest first, one page at a time.query todos_for_owner(owner_id: User.id, filter: ListTodos) -> Page<TodoView> { from: Todo where: Todo.owner_id == owner_id where_present: Todo.status == filter.status order: [Todo.created_at desc] page: { size: filter.page_size after: filter.after }}
from: names the entity, where: the records it keeps, order: their
order and page: how many come back at once and where the page starts.
where_present: applies its condition only when the value on the right
is there: when the request leaves out status, every status comes back.
The Filters and Order and pages topics
cover each section in full.
How many records come back
The result type says how many records come back, so there is no separate
word for it. A query for exactly one record also needs missing:, which
names the failure it gives when no record matches
(missing:):
| Result type | Returns | Needs |
|---|---|---|
TodoView |
exactly one record | a where: on a unique field, and missing: |
TodoView? |
zero or one | a where: on a unique field |
List<TodoView> |
up to the number limit: sets |
order: and limit: |
Page<TodoView> |
one page and a cursor for the next | order: and page: |
FeatureCollection<Point, TodoPin> |
a map layer of up to the number limit: sets, each record a point on the map (A query giving a map layer) |
geometry:, order: and limit: |
Some apps are shared by several tenants, such as companies, whose records
are kept apart (Tenancy). There a unique: rule
holds within each tenant: two companies can each have a project called
Launch (A unique rule holds within its tenant).
So a unique field alone doesn't pick one record. A query for one record
compares the record's tenancy reference in its where: as well as the
unique field, unless it compares a reference to another record of the
same tenant, which already picks the tenant.
What a result holds
A result's element type is the entity, or a type built from its fields.
type TodoView = Output { pick: [Todo.id, Todo.title] } is a type for
output that picks two of Todo's fields
(Building a type from other types).
The compiler reads only the fields that type needs.
That type can't have a rule across its fields, such as "ends_at is
after starts_at" (Rules across fields).
This holds even when each field's own limits would make the rule always
true. The same goes for every other type that holds a query's records,
including a Nearest<T>'s item (Nearest first), a map layer's properties,
an aggregate's rows and the related records a query brings with it. A
result type with such a rule is the error
query.result_rule.
The reason is that Jadpo checks a rule whenever a value is made, and a
broken rule is a failure your code handles with attempt
(Passing a failure on). A
read has no attempt around it, and the database can't check the rule as
it reads. So a stored record that broke the rule could only be a
defect, a bug rather than a failure the caller
handles. Instead, read into a type with the same fields and no rule, then
build the type with the rule from it in code, inside an attempt.
Records the entity's lifecycle hides, such as soft-deleted todos, are left out of every query.
A query writes nothing and calls no actions
(Functions and actions). Call it by
its bare name in its own domain. From another domain, put the domain's
name first, planning.todos_for_owner(...), which works when the query
is public.
The statement it sends
jadpo sql todos_for_owner prints the SQL statement the query sends. It
is one SELECT with the query's conditions, an ORDER BY, and a LIMIT
one more than the page's size, so the runtime knows whether a next page
exists. It selects only the columns the result type needs, plus the
order: fields a page cursor needs and any field a
policy condition checks outside the database.
The statement's text is fixed when the project is compiled. A
where_present: condition and the cursor are written
($n IS NULL OR ...), so the same query always sends the same statement.
A query called inside an action reads in the action's transaction. Called from a test, it runs in a short transaction of its own.
Markdown for agents: /docs/named-queries.md.Built from Jadpo main at a707efb.