Cursor Pagination
Page through large result sets without gaps or duplicates using after_hlc — a cursor derived from the timestamp_hlc of the last record on each page.
SKIP N) re-scans records on every page and drifts when new records are written between requests — you can skip records or see them twice. Cursor pagination anchors to a specific point in the strand using an HLC timestamp, so new writes never disturb in-progress pages and every record appears exactly once.How It Works
SapixDB assigns every record a timestamp_hlc — a 64-bit Hybrid Logical Clock value that is strictly ordered within a strand. Passing this value as after_hlc in a subsequent request tells the engine to return only records after that position. The comparison is exclusive (>), so the cursor record itself is never repeated.
- Send a scan with a
limit. Receive up tolimitrecords. - Read
timestamp_hlcfrom the last record in the response. - Send the next request with
after_hlcset to that value. - Repeat until the response contains fewer records than
limit— that signals the end of results.
Page 1 — First Request
Fetch the first page without after_hlc and extract the cursor from the last record.
LAST_HLC=$(curl -s -X POST http://localhost:7475/v1/agents/events/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer spx_root_YOUR_ROOT_KEY" \
-d '{"type": "scan", "limit": 10}' \
| python3 -c "
import sys, json
r = json.load(sys.stdin)['records']
print(r[-1]['timestamp_hlc'])
")
echo "Cursor: $LAST_HLC"Page 2 — Using the Cursor
Pass the captured timestamp_hlc as after_hlc in the next request body.
curl -s -X POST http://localhost:7475/v1/agents/events/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer spx_root_YOUR_ROOT_KEY" \
-d "{\"type\": \"scan\", \"limit\": 10, \"after_hlc\": $LAST_HLC}" \
| python3 -m json.toolafter_hlc. The value you read from each record is timestamp_hlc. These are different names — do not use ts_hlc, which does not exist.Detecting End of Results
SapixDB does not return a separate "has more" flag. The signal is simple: when the number of records returned is less than your limit, you have reached the end. An empty records array also indicates the end.
{
"type": "scan",
"limit": 10,
"after_hlc": 1751900065536000
}
// If len(response.records) < 10 → last page reachedPagination with Filters
after_hlc and filter compose naturally. The filter is applied first (inside the strand), and after_hlc acts as an additional lower bound on the HLC timestamp. Both constraints are respected simultaneously.
{
"type": "scan",
"limit": 10,
"after_hlc": 1751900065536000,
"filter": {
"field": "type",
"op": "eq",
"value": "page_view"
}
}Python: Paginate All Records
The following pattern pages through an entire agent strand, collecting every record regardless of total size.
import requests
BASE = "http://localhost:7475"
AGENT = "events"
HEADERS = {
"Content-Type": "application/json",
"Authorization": "Bearer spx_root_YOUR_ROOT_KEY",
}
PAGE_SIZE = 100
all_records = []
cursor = None
while True:
body: dict = {"type": "scan", "limit": PAGE_SIZE}
if cursor is not None:
body["after_hlc"] = cursor
resp = requests.post(
f"{BASE}/v1/agents/{AGENT}/query",
json=body,
headers=HEADERS,
)
resp.raise_for_status()
page = resp.json()["records"]
all_records.extend(page)
if len(page) < PAGE_SIZE:
break # last page
cursor = page[-1]["timestamp_hlc"]
print(f"Total records fetched: {len(all_records)}")Field Reference
| Field | In | Type | Description |
|---|---|---|---|
after_hlc | request body | integer | Exclusive lower bound. Returns only records whose timestamp_hlc is strictly greater than this value. |
timestamp_hlc | response record | integer | The HLC timestamp of each record. Use the value from the last record as the next after_hlc. |
limit | request body | integer | Page size. When the response contains fewer records than this value, the final page has been reached. |
"order": "desc". The cursor still comes from timestamp_hlc of the last record in each page response. See Sort Order.