---
url: /guide/query/data-query.md
description: Use shared query shapes to read snapshot or event-stream data.
---

# 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](./filter-expression.md) 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`) |

```mermaid
flowchart LR
    Source{"Choose the data source"} --> Snapshot["Snapshot: current state"]
    Source --> Event["EventStream: historical body"]
    Shape{"Choose the result shape"} --> Single["SingleQuery: at most one"]
    Shape --> List["ListQuery: list"]
    Shape --> Paged["PagedQuery: page + total"]
    Shape --> Count["FilterExpression: count"]
    Snapshot --> Execute["Query entry point"]
    Event --> Execute
    Single --> Execute
    List --> Execute
    Paged --> Execute
    Count --> Execute
```

These shapes can operate on different data models. See [Snapshot Queries](./snapshot-query.md) and [Event Stream Queries](./event-stream-query.md) 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.

```kotlin
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:

```json
{
  "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:

```json
{ "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:

```json
{ "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](./snapshot-query.md) |
| Read the complete event history and query event-stream fields | [Event Stream Queries](./event-stream-query.md) |

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.
