AnnouncementsIntroducing Arkoren

    Menu

    Pagination

    Return result windows with offset or cursor pagination, request parameters, and cursor safety.

    What's on this page
    1. Choose numbered pages with totals or cursor windows without a count.
    2. Validate request limits and provide a stable non-null ordering.
    3. Sign cursor positions while keeping authorization and filter checks separate.
    On this page

    Use numbered pages when users need totals or direct page selection. Use cursor windows when they move forward or backward through a large ordered result and do not need a total. For background processing of every row, use Batching and Streaming instead.

    Assume a configured db and request or job context ctx. Import arkoren.dev/foundation/query and arkoren.dev/core/pagination. The examples read users with a non-null unique id and a non-null email:

    type User struct {
    	ID    int64  `db:"id"`
    	Email string `db:"email"`
    }

    Stable Ordering

    Both helpers require a SELECT with an explicit order and no existing Limit or Offset. Define the common query once:

    users := query.Table("users", func(statement *query.Builder) {
    	statement.Select("id", "email")
    	statement.OrderBy("id")
    })

    End any compound order with a unique tiebreaker, such as created_at followed by id. The helper requires an order but does not verify uniqueness. Reapply authorization and tenant filters on every request, including requests with cursors.

    Numbered Pages

    To fetch the third page of twenty users:

    page, err := db.Paginate[User](ctx, users, pagination.PageParams{
    	Page:    3,
    	PerPage: 20,
    })

    Paginate runs a count and a separate SELECT with offset 40 and limit 20. The result exposes Items, Total, PerPage, CurrentPage, and LastPage. No matches produces an empty collection and a last page of at least one. A request past the end keeps its requested page number instead of inventing rows.

    The count uses Query.Count, including grouped or raw-derived result sets. Count and items are not automatically one snapshot: concurrent changes can make them disagree unless the database isolation policy preserves a snapshot across both statements. Deep offsets also require the server to skip preceding rows; use cursor pagination when that cost is inappropriate.

    Cursor Windows

    To read the first window without counting the full result:

    page, err := db.CursorPaginate[User](ctx, users, pagination.CursorParams{
    	PerPage: 20,
    })

    The helper fetches up to one extra row to detect another window. Items contains at most twenty users; NextCursor and PrevCursor are opaque continuation tokens. Pass either token unchanged as Cursor on the next request. An empty token means no token was emitted; an empty result has neither token. There is no total count. Backward queries are reversed back into the declared display order.

    Every ordered column must be present in the result type and selected into it. All ordering keys in the selected dataset must be non-NULL. Fetched NULL values, including lookahead rows, fail with database.ErrCursorValue; SQL seeks can exclude NULL rows before this check can see them. Choose non-null keys or explicitly exclude NULLs rather than relying on the check to validate the dataset.

    Tokens encode boundary values and ordering, not a snapshot. Inserts, deletes, and ordering-key updates between requests can change traversal. Keep tokens with the same filters and authorization context. The built-in ordering fingerprint checks columns and directions, not filters or tenant identity.

    Request Parameters

    For an existing HTTP request r, import arkoren.dev/core/http/request and parse the query parameters before execution:

    params, err := request.Page(r, pagination.WithMaxPerPage(50))

    This reads page and per_page; request.Cursor(r) reads cursor and per_page. Handle parsing errors before passing parameters to the facade. The facade normalizes again with its built-in limits, so request options can impose stricter limits but do not raise the facade’s ceilings.

    For directly constructed parameters, zero page selects page 1 and zero size selects 25. HTTP parsing distinguishes absence from explicit zero: missing values use defaults, while supplied zero or negative values become one. Size is capped at 100, and numbered pages above 10,000 or with overflowing offsets fail with pagination.ErrPageOverflow. Cursor normalization preserves the token unchanged; validation happens when the database helper decodes it.

    Missing order and preset limits return database.ErrNoOrder and database.ErrPaged. Cursor fields missing from the result type return database.ErrCursorColumn. Malformed, incompatible-order, or invalidly signed tokens match pagination.ErrCursorDecode. Do not replace these failures with an apparently successful first page. Encoding can fail too; never return a partial cursor result as success.

    Signed Cursors

    By default, positions are unsigned base64 JSON and clients can forge them. To reject tampering, load private application material with arkoren.dev/foundation/key and construct a codec:

    material, err := key.LoadShared("cursor.key")
    codec, err := pagination.NewCursorCodec(material)

    The key must contain exactly 32 bytes; follow key provisioning instead of committing it. After handling those construction errors, configure the facade using arkoren.dev/core/service/database:

    db := database.New(driver, func(connection *database.Builder) {
    	connection.CursorCodec(codec)
    })

    The codec rejects unsigned, altered, and wrongly signed tokens. Enabling it invalidates previously issued unsigned tokens. Rotate by constructing the new codec with the current key first and previous keys afterward, then retire old keys according to your cursor-retention policy.

    Signing does not encrypt boundary values, enforce expiration, bind filters, or authorize access. The codec provides no expiry check. Reapply access filters and implement any additional token policy in the application. Both signed and unsigned token formats have a 4096-byte limit.

    Items is an eager collection for this window only. Local filtering or mapping does not fetch rows, recompute totals, or change cursor boundaries.

    See the database API and pagination API for the full contracts.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive