A cursor is a query in progress. It holds onto the filter/sort and hands out rows one-at-a-time (nextItem()) or in small batches (nextBatch(n)). Unlike list(), it never builds the full array in memory — handy for large datasets, progress UIs, and pagination that survives across await boundaries.
db.cursor(entity, opts) evaluates the filter/sort and returns a Cursor. opts accepts the same filters, sort, and projection as list().cursor.nextItem() returns the next row or undefined when exhausted.cursor.nextBatch(n) returns up to n rows; returns an empty array when exhausted.cursor.count() is the number of rows remaining (decreases as you advance).cursor.position() is how many rows you've already consumed. cursor.hasMore() is count > 0.cursor.reset() rewinds to the start without re-running the query.
flowchart LR
L["list(opts)
returns all rows at once"] --> Arr[Array of all N rows]
C["cursor(opts)
returns a handle"] --> S["Cursor
count=N position=0"]
S --> N1["nextItem()"] --> R1[1 row, position=1]
R1 --> N2["nextBatch(10)"] --> R2[10 rows, position=11]
R2 --> NE{hasMore?}
NE -- yes --> N1
NE -- no --> E[done]
style L fill:#fff3e0,stroke:#ef6c00
style C fill:#e3f2fd,stroke:#1565c0
style E fill:#c8e6c9,stroke:#2e7d32
Seed 100 records. Creates items with random value (0–99) and category.Create cursor. The default filter (value > 50) runs — the status row shows how many rows remain.Next (1) a few times, then Next batch (10). Watch Remaining drop and Position climb.Reset — same query, position returns to 0, you can iterate again.Run all tests runs 8 steps across a fresh test_cursor namespace and reports pass/fail for each:
value > 25 yields 25 itemsnextItem twice → position=2nextBatch(10) → position=12hasMore() agrees with count() > 0