Skip to content
Current documentation — tracks the engine's development branch (v0.4.0 at the last sync).v0.2.1v0.3.0v0.3.1v0.3.2v0.4.0v0.4.1About these docs

Pagination

For walking large result sets, corvid offers keyset (cursor) pagination: each page returns rows plus an opaque cursor that resumes strictly after the last key served. No offset rescan, bounded memory, streamed.

# use corvid::{Db, Value};
# let db = Db::open_in_memory()?; let c = db.collection("docs");
let mut after: Option<Vec<u8>> = None;
loop {
let page = c.page(after.as_deref(), 100)?;
for (key, doc) in &page.rows { /* ... */ let _ = (key, doc); }
match page.next {
Some(cursor) => after = Some(cursor),
None => break, // short page = end of collection
}
}
# Ok::<(), corvid::Error>(())
  • Rows are in key order, from one MVCC snapshot covering the whole chunked walk.
  • after = None (or empty bytes… see below) starts at the beginning.
  • Page.next is Some(cursor) iff the page was full — a full page always yields a cursor; a short page means end.
  • limit 0 returns an empty page and no cursor.

Cursor corner cases, pinned by tests: after = b"" skips exactly the empty key (the empty key is a legal document key and sorts first). Cursors are opaque byte strings — treat them as tokens (the C ABI returns the buffer for corvid_free).

The same walk with a predicate — only matching documents are served, the cursor still advances by key:

# use corvid::{Db, Value, field};
# let db = Db::open_in_memory()?; let c = db.collection("docs");
let page = c.page_where(None, 50, field("category").eq(Value::Text("blog".into())))?;
# let _ = page;
# Ok::<(), corvid::Error>(())

Filtered pagination composes from query().filter() + offset/limit too; page_where gives you the constant-memory cursor form. (The C ABI exposes page only in v1 — see the exclusions.)

Each page call opens its own read snapshot and runs the entire chunked walk inside it: the returned rows always match some committed point in time, even while writers are active. Successive pages see the then-current state — a walk across concurrent writes is per-page consistent, not walk-consistent.

The snapshot-holding cost is space, not latency: freed pages from commits landing during a walk stay in the file until the walk ends (bounded by limit rows and 1024-key chunk reads).

  • offset on the query builder is O(offset + limit) — it walks and discards. Fine for shallow pages, dashboards, small windows.
  • Cursors are O(limit) per page regardless of depth — the shape for exporting a collection, background syncs, and feeding pipelines.
  • Ranked (multi-source fused) result sets do not have a cursor in v0.2 — fusion materializes the candidate set. Filter-only, order-only, and single-source ranked paths are bounded/streamed.

Next: joins.