DocsWorkflowsComponentsAppapp:record:query

app:record:query

Queries records from a schema table using filters, sorting and pagination

Options

NameTypeDescription
schemastringThe name of the schema (database) to query records from
tablestringThe name of the table to query records from
pageintThe page number to retrieve (1-based)
queryQueryThe filter, sort, limit and offset parameters for the query
  ↳ filters[]FilterAre AND-combined predicates that records must satisfy
    ↳ fieldstringIs the name of the field to compare. Must exist on the target table.
    ↳ opFilterOpIs the comparison operator.
    ↳ valueanyIs the operand. For OpIn and OpNotIn it must be a slice. Ignored for OpIsNull and OpIsNotNull, and ignored entirely when ValueField names a column to compare against instead.
    ↳ value_fieldstringNames a second column of the same table to compare Field against, in place of the literal in Value. It answers a same-row question such as "the source text differs from the translation" without a caller reading both columns of every row to find out. The shape is fixed and deliberately not a grammar. Both sides are bare columns of the one table being queried, so there is no expression, no arithmetic, no function and no correlated subquery, and the comparison is always between two values of the same row. Reaching another row or another table is the join a declared lookup exists to keep out of a request. Only the scalar comparisons take it. Membership and the null tests have no second column to read and the substring operators would need a concatenation, so a filter pairing ValueField with any of them is refused rather than quietly compared against the literal. The column is held to the caller's read allowlist as well as to the table's columns. Whether a predicate holds is a fact about the value it compared, so comparing against a column the caller may not read hands back that column's content one comparison at a time.
  ↳ any[]FilterIs a set of alternative filter groups. A record matches when it satisfies every entry in Filters and at least one group in Any, where a group is itself AND-combined. Nil means no alternative applies, while a non-nil Any carrying no group matches nothing. It exists because an authorization envelope is the union of what several grants allow, and a union is an OR that Filters cannot express. Nothing else fills it: the surfaces that build a Query from a request read only Filters, so a caller's own criteria can narrow this and never widen it
    ↳ fieldstringIs the name of the field to compare. Must exist on the target table.
    ↳ opFilterOpIs the comparison operator.
    ↳ valueanyIs the operand. For OpIn and OpNotIn it must be a slice. Ignored for OpIsNull and OpIsNotNull, and ignored entirely when ValueField names a column to compare against instead.
    ↳ value_fieldstringNames a second column of the same table to compare Field against, in place of the literal in Value. It answers a same-row question such as "the source text differs from the translation" without a caller reading both columns of every row to find out. The shape is fixed and deliberately not a grammar. Both sides are bare columns of the one table being queried, so there is no expression, no arithmetic, no function and no correlated subquery, and the comparison is always between two values of the same row. Reaching another row or another table is the join a declared lookup exists to keep out of a request. Only the scalar comparisons take it. Membership and the null tests have no second column to read and the substring operators would need a concatenation, so a filter pairing ValueField with any of them is refused rather than quietly compared against the literal. The column is held to the caller's read allowlist as well as to the table's columns. Whether a predicate holds is a fact about the value it compared, so comparing against a column the caller may not read hands back that column's content one comparison at a time.
  ↳ alternatives[]AlternationAre the OR-combined filter groups a product concern contributes. Each entry is an alternation of its own, AND-combined with Filters, with Any and with every other entry, so a record has to satisfy at least one group of each entry. It exists because Any is the authorization channel and nothing but an authorization decision may write there. A keyset cursor and a pushed-down search are both an OR, and either one borrowing Any would replace a caller's envelope with a page position or a search term. Here they narrow the envelope instead, which is the only direction a product concern is allowed to move a read in. A caller cannot fill it either. The surfaces that build a Query from a request read Filters and the parameters they know, and this is written by the engine out of the cursor and the search it was handed.
  ↳ sort[]SortFieldIs the ordered list of sort criteria. Earlier entries take precedence
    ↳ fieldstringIs the name of the field to sort by.
    ↳ directionSortDirectionIs the sort order (ascending or descending).
  ↳ limitintCaps the number of returned records. 0 means no limit
  ↳ offsetintSkips the first N matching records. Intended for pagination
  ↳ include_deletedboolIncludeDeleted, when true, includes soft-deleted records in the result Defaults to false so deleted records are filtered out
  ↳ searchstringIs a full-text search query
  ↳ cursorstringResumes a listing from the position a previous page ended at, walking the sort key instead of counting rows off the front of the set. It is what a caller reaches for once a set is large enough that the offset is the cost of the page. A cursor page names exactly one sort column, which the row id breaks the ties of, and it carries no total, since a count of the whole set is the work the walk exists to avoid and a grouped count answers a question about how many rows there are far more cheaply. Setting it ignores Offset. The two are different ways to say where a page starts and honoring both would answer neither.
  ↳ ReadFields[]stringIs the caller's readable field allowlist, empty meaning every field, carried here because a search is a read of every column it scans. The exact pass compiles to an OR of substring tests, so a term matching in a column the caller may not read still returns the row and still counts it, which answers whether that column contains the term one substring at a time. The engine holds no envelope of its own, so the allowlist travels with the query and the search considers only the columns it names. It is written by AccessScope.Narrow and by nothing else. Like Any it is an enforcement channel rather than a request field, so it is not read off the wire at all and a caller has no way to fill it.
relatedRecordQueryRelatedThe second tables this query reaches through the queried table's own relation declarations
  ↳ lookups[]LookupProject named columns of the row a belongs_to field points at, so a workflow reads a related row's columns without a record read per row
    ↳ fieldstringIs the belongs_to field of the table being queried. The table read through it is the one that field declares.
    ↳ columns[]stringAre the columns of the related row that travel back. They are matched against the related table's own columns, never the source table's.
    ↳ any[]FilterIs the related table's authorization envelope, carrying the meaning it carries on Query. It reaches the join condition rather than the outer predicate, so a related row the caller may not see leaves its source row unmatched instead of filtering the source row away. A nil Any is every related row and a non-nil Any carrying no group is none, which is what DenyAllScope resolves to.
      ↳ fieldstringIs the name of the field to compare. Must exist on the target table.
      ↳ opFilterOpIs the comparison operator.
      ↳ valueanyIs the operand. For OpIn and OpNotIn it must be a slice. Ignored for OpIsNull and OpIsNotNull, and ignored entirely when ValueField names a column to compare against instead.
      ↳ value_fieldstringNames a second column of the same table to compare Field against, in place of the literal in Value. It answers a same-row question such as "the source text differs from the translation" without a caller reading both columns of every row to find out. The shape is fixed and deliberately not a grammar. Both sides are bare columns of the one table being queried, so there is no expression, no arithmetic, no function and no correlated subquery, and the comparison is always between two values of the same row. Reaching another row or another table is the join a declared lookup exists to keep out of a request. Only the scalar comparisons take it. Membership and the null tests have no second column to read and the substring operators would need a concatenation, so a filter pairing ValueField with any of them is refused rather than quietly compared against the literal. The column is held to the caller's read allowlist as well as to the table's columns. Whether a predicate holds is a fact about the value it compared, so comparing against a column the caller may not read hands back that column's content one comparison at a time.
    ↳ searchboolAsks the listing's search term to match this lookup's projected columns as well as the queried table's own text. It is what makes a review queue searchable by the name of the key a translation belongs to, which is the term a person types and which lives on the other table. It covers the projected columns and only those. A searched column is read to decide whether the row comes back, so it answers to the target's second grant and to the related table's read allowlist exactly as a projected one does, and reusing the projection is what makes that true by construction rather than by a second check somebody has to remember.
  ↳ absent[]AbsenceNarrow the page to the rows no row of a has_many's table points at, which is the one question a listing of the related table cannot answer
    ↳ relationstringIs the has_many field of the table being queried. The table whose rows are looked for is the one that field declares, and the column joined on is the one it names as pointing back.
    ↳ filters[]FilterNarrow which related rows count as present, AND-combined, matched against the related table's own columns and never the source table's. An empty set is a bare existence check. A filter here is a predicate over a table the caller reached through a relation rather than through a listing, so every column named is held to the related table's own read allowlist. A caller who may not read a column must not narrow by it, for the reason a lookup projects and nothing more.
      ↳ fieldstringIs the name of the field to compare. Must exist on the target table.
      ↳ opFilterOpIs the comparison operator.
      ↳ valueanyIs the operand. For OpIn and OpNotIn it must be a slice. Ignored for OpIsNull and OpIsNotNull, and ignored entirely when ValueField names a column to compare against instead.
      ↳ value_fieldstringNames a second column of the same table to compare Field against, in place of the literal in Value. It answers a same-row question such as "the source text differs from the translation" without a caller reading both columns of every row to find out. The shape is fixed and deliberately not a grammar. Both sides are bare columns of the one table being queried, so there is no expression, no arithmetic, no function and no correlated subquery, and the comparison is always between two values of the same row. Reaching another row or another table is the join a declared lookup exists to keep out of a request. Only the scalar comparisons take it. Membership and the null tests have no second column to read and the substring operators would need a concatenation, so a filter pairing ValueField with any of them is refused rather than quietly compared against the literal. The column is held to the caller's read allowlist as well as to the table's columns. Whether a predicate holds is a fact about the value it compared, so comparing against a column the caller may not read hands back that column's content one comparison at a time.

Outputs

NameTypeDescription
limitintThe maximum number of records per page applied to the query
pageintThe current page number returned
totalintThe total number of matching records across all pages
items[]RecordThe records on the current page
lookupsLookupRowsThe related rows the query's lookups reached, keyed by the source field that reached them and then by the related row's id. They sit beside the items rather than inside each record, so a page pointing at one related row many times carries it once and a projected column can never collide with a column of the queried table. A workflow reads one as lookups.<field> keyed by the source row's own value, and a source row whose relation is null or whose related row the run may not see has no entry at all.

Example

name: Query published articles
actions:
  - name: articles
    component: app:record:query
    vars:
      schema: content
      table: articles
      query:
        status: published

outputs:
  articles: "{{ articles.items }}"
  total: "{{ articles.total }}"