Data Queries
“Data Queries” is a documentation category, not a source type named DataQuery. The shared query contract consists of SingleQuery, ListQuery, PagedQuery, and count requests that use a FilterExpression directly. See Filter Expressions for filter JSON and the Kotlin DSL.
Four Query Shapes
| Shape | Request focus | Result |
|---|---|---|
SingleQuery | filter, projection, sort | At most one item |
ListQuery | filter, projection, sort, limit | A list of items |
PagedQuery | filter, projection, sort, pagination | Current-page data and total |
| Count | A FilterExpression directly | An exact count (Long) |
These shapes can operate on different data models. See Snapshot Queries and Event Stream Queries for their field paths and model-specific defaults. This page does not assume state.* or body.* paths.
SingleQuery
SingleQuery returns at most one matching item. filter selects matches, projection controls returned fields, and sort determines which item comes first when several items match. The empty or error semantics for no match are defined by the concrete query entry point.
ListQuery
ListQuery returns a list and can cap the number of items with limit. For JVM queries, limit = 0 means unlimited; an HTTP entry point may still apply a request-protection limit. It has no page index; use PagedQuery when pagination is required.
PagedQuery
PagedQuery returns a PagedList: total is the total number of matching items and list contains the current page. Page indexes start at 1 and size is the page size. Provide a stable sort to avoid results moving between pages.
val query = PagedQuery(
filter = filterExpression { "status" eq "READY" },
projection = Projection(include = listOf(QueryField("id"), QueryField("status"))),
sort = listOf(Sort(QueryField("updatedAt"), Sort.Direction.DESC)),
pagination = Pagination(index = 1, size = 20)
)The equivalent JSON request shape is below. status and updatedAt are neutral examples; the data-model page defines the actual logical fields:
{
"filter": { "op": "EQ", "field": "status", "value": "READY" },
"projection": { "include": ["id", "status"] },
"sort": [{ "field": "updatedAt", "direction": "DESC" }],
"pagination": { "index": 1, "size": 20 }
}Here, index is the 1-based page number and size is the page size; sort names logical fields and directions; and filter is a FilterExpression. On the JVM, Projection and Sort use QueryField; valid fields remain strings in JSON.
projection can use include or exclude; an empty projection returns all fields. Each QueryField selects one node and all of its descendants: selecting an object returns its whole subtree, while selecting a scalar is an exact field selection. Public Projection and Sort do not accept backend wildcard patterns. Select state itself for the whole state subtree instead of using a storage-side expression.
MongoDB ID projection exception
MongoDB single/list/paged queries retain the native default of returning _id: even when include omits the ID, Snapshot results still contain aggregateId and EventStream results still contain id. This is an accepted backend difference; Elasticsearch does not add an unrequested ID.
To return only version, explicitly exclude the logical ID. For Snapshot queries:
{ "projection": { "include": ["version"], "exclude": ["aggregateId"] } }For EventStream queries, change exclude to ["id"]. MongoDB allows this ID exception when combining include and exclude; it does not permit arbitrary field combinations. Public requests use logical field names, not the physical _id.
Cursor queries remove internal fields fetched only for sorting, including an ID not selected by the projection, so the default ID retention exception does not apply. With include: ["version"], cursor results contain only version on both backends. State-only endpoints also unwrap state and do not return the snapshot's outer aggregateId.
Count
The count body is a FilterExpression directly, without an outer filter property:
{ "op": "EQ", "field": "status", "value": "READY" }On the JVM, use filter.count(queryGateway). Execution and exactness follow the selected backend contract; HTTP cost protection may reject expensive or unfiltered requests. Count does not return a data list.
Sorting and Pagination
Sort fields must be logical fields supported by the current query model, with direction ASC or DESC. Pagination changes the returned window but not total; when data can change between requests, use a stable ordering that sufficiently distinguishes records. ListQuery.limit and PagedQuery.pagination are alternative retrieval modes and should not be mixed in one shape.
For the same backend, dataset, filter, and complete effective sort, paged and cursor queries preserve the same ordering semantics. Paged queries must explicitly include the unique sort field used by cursor queries. For single-valued sort fields supported by both query forms, when no null substitution such as null_value is configured, MongoDB and Elasticsearch place null or missing values before non-null values in ascending order and after them in descending order; subsequent sort fields break ties. This does not provide snapshot consistency across requests.
Results and Empty Results
Queries may return typed, state-only, or ObjectNode results; the concrete entry point determines which forms and unwrapping rules are available. After the Backend returns nodes, the Gateway masks them with the same Query Model Schema, then optionally uses Jackson for typed materialization. It skips masking immediately when the root Schema has no masked fields. Empty-result behavior is also entry-point-specific: JVM, WebFlux, and API Client 404, empty-value, or empty-list semantics are explained in their child pages and client page. This page does not generalize one transport semantic to every entry point.
Choosing Snapshot or Event Stream
| Need | Entry |
|---|---|
| Read current aggregate state and query business-state fields | Snapshot Queries |
| Read the complete event history and query event-stream fields | Event Stream Queries |
Both models support the shared data-query shapes, but their field roots, deletion semantics, available transport entries, and result models may differ. Choose the model from the source of truth first, then confirm field paths, entry points, and empty-result behavior on its page.