Sorting, Limits and Pagination
A query takes five optional window parameters. They work on query() and — except for count — on querySub and querySubChanged as well.
| Parameter | Type | Meaning |
|---|---|---|
orderBy |
array of field names | Sort by a field on each record: ["views"], ["meta","rating"]. Always an array, never a dotted string — one spelling means one canonical form everywhere (queries, indexes, subscription keys). Omit it to order by record key. Max 8 segments. |
desc |
boolean | Reverse the order. Default false. |
limit |
integer 1–1,000,000 | Maximum records to return. Implies an order — without orderBy, the window is ordered by record key. |
startAt |
integer 0–1,000,000 | Skip this many matching records. Mutually exclusive with cursor. |
cursor |
opaque string | Resume after a previous page. Pass back the nextCursor from the last response. Mutually exclusive with startAt. |
count |
true |
Return only how many records match. data is null; the number is in response.count. Cannot be combined with limit, startAt or cursor, and is not supported on subscriptions. |
// Top 10 posts by view count
const top = await query({
path: ["posts"],
query: "child.published == true",
orderBy: ["views"],
desc: true,
limit: 10
});
// Read the results IN ORDER via response.order — a JSON object's key
// order is not something to depend on.
top.order.forEach(key => console.log(key, top.data[key].views));
// Order by a nested field
await query({
path: ["products"],
query: "true",
orderBy: ["meta", "rating"],
desc: true,
limit: 20
});
// Order by record key (just omit orderBy)
await query({ path: ["messages"], query: "true", limit: 50 });
// Cursor pagination — the correct way to walk a large collection
let cursor;
for (;;) {
const page = await query({
path: ["events"],
query: "true",
orderBy: ["ts"],
limit: 100,
...(cursor && { cursor })
});
page.order.forEach(k => handle(page.data[k]));
if (!page.nextCursor) break; // absent == you reached the end
cursor = page.nextCursor;
}
// Offset pagination — simpler, but O(offset) to skip. Prefer a cursor
// for anything deep.
await query({ path: ["events"], query: "true", orderBy: ["ts"],
startAt: 200, limit: 100 });
// Just the number
const c = await query({ path: ["users"], query: "child.active == true", count: true });
console.log(c.count); // 8412 (c.data is null)
Records missing the orderBy field are excluded from the result, not sorted to one end. A record with no views key cannot participate in an ordering by ["views"], so orderBy: ["views"] silently narrows the result set to records that have one. If you need them included, give every record the field (even as 0 or null).
Ordering across mixed types is total and stable. Values sort by type first — null < boolean < number < string < everything else — and then by value within a type. Ties are broken by record key, where keys that look like integers sort numerically and ahead of non-integer keys. This is what makes a cursor able to resume from an exact position.
Cursors are opaque — but they are not secret. A cursor encodes the last row's sort value and key. Pass it straight back; don't parse or construct one. A malformed or over-long (> 512 character) cursor is rejected with Invalid cursor.
Query response metadata
When a query uses any window parameter (orderBy, limit, startAt, cursor or count), the response carries extra fields alongside data. A plain query({path, query}) keeps its original two-field shape and does not include them.
| Field | Meaning |
|---|---|
order | Array of matching record keys, in window order. This is the authoritative ordering — read the result through it. |
plan | How the query was executed: "scan", "index-filter", "index-order", "index-union" or "index-count". See Indexes. |
examined | How many records the engine had to look at. Compare it to the number returned to see whether an index is doing its job. |
truncated | true if the engine stopped early against a hard scan limit. The accompanying status is "Incomplete" rather than "Success". |
nextCursor | Present only when more records exist beyond this page. Its absence is how you detect the end. |
count | Present only for count: true queries. |
Hard scan limits. A query examines at most 8,000,000 records and returns at most 1,000,000. Hitting either stops the query early and returns status: "Incomplete" with truncated: true — a partial answer, clearly labelled, rather than an unbounded stall on the single-threaded engine. The query string itself is capped at 512 characters.