How to implement Cursor Pagination
High-performance keyset pagination for large datasets and real-time feeds without offset drift.
For high-throughput datasets or infinite-scroll feeds, offset pagination causes query slowdowns and duplicate items (offset drift). ZCore provides CursorPagination based on encoded keysets.
1. Enable Cursor Pagination on the Router
Open your router file and set pagination_class = CursorPagination. This automatically changes the GET / endpoint query signature to accept cursor and size:
# routers.py
from zcore import BaseRouter
from zcore.db.pagination import CursorPagination
from .models import Task
from .schemas import TaskCreate, TaskUpdate, TaskResponse
from .services import TaskService
class TaskRouter(BaseRouter[TaskCreate, TaskUpdate]):
model = Task
create_schema = TaskCreate
update_schema = TaskUpdate
schema_out = TaskResponse
service = TaskService
# Switch from default Page-Number to Keyset Cursor pagination
pagination_class = CursorPagination
prefix = "/tasks"
tags = ["Tasks"]
router_instance = TaskRouter()2. Client Requests
The client can fetch the first page by simply requesting the desired size, then passing the returned cursor for subsequent pages:
# Initial request (First page)
curl -X GET "http://localhost:8000/tasks/?size=20"
# Subsequent request using the received next_cursor
curl -X GET "http://localhost:8000/tasks/?size=20&cursor=eyJ2YWx1ZSI6ICI...=="3. Response Structure
The response is wrapped in ZCore's ResponseWrapper. The meta block contains the pagination state:
{
"success": true,
"message": "Success",
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Complete documentation",
"is_completed": false
}
],
"meta": {
"next_cursor": "eyJ2YWx1ZSI6ICIyMDI2LTA4LTE1VDA4OjAwOjAwIiwgInBrIjogImExYjJjM2Q0Li4uIn0",
"has_more": true,
"size": 20
}
}Dynamic Primary Key Inspection:
As of rc.2, CursorPagination() automatically inspects your model's primary key column (supporting both uuid.UUID and int primary keys). The cursor token encapsulates {"value": ..., "pk": ...} into a URL-safe Base64 token to execute fast indexed lookups (WHERE col < val) without scanning rows via OFFSET.
How to enforce multi-tenancy & soft deletion
Automatically isolate tenant data and filter soft-deleted records across all repository queries using SoftDeleteMixin and scope_query.
How to build complex search queries
Construct nested AND/OR/NOT filters, eager-load relations, use inverted/range operators, and execute secure dynamic searches.