Packages

Fast, minimal pagination for Ecto and Phoenix. Supports offset and cursor pagination with LiveView support.

Current section

Files

Jump to
pagex_pagination lib pagex cursor.ex
Raw

lib/pagex/cursor.ex

defmodule Pagex.Cursor do
@moduledoc """
Cursor-based (keyset) pagination for Pagex.
This module implements **cursor pagination**, which is designed for
efficient pagination over large datasets where offset-based pagination
becomes slow or unstable.
Instead of using `OFFSET`, cursor pagination uses a **position marker**
(cursor) derived from a stable, ordered field (typically an `id` or timestamp).
## Why cursor pagination?
Cursor pagination provides:
- Stable performance on large datasets
- No performance degradation with deep pagination
- Consistent results even when rows are inserted/deleted
- Better scalability compared to OFFSET queries
## How it works
1. A query is limited using `LIMIT`
2. Results are fetched from the database
3. The last record is encoded into a cursor
4. The cursor is returned as `next_cursor`
## Important notes
- This implementation assumes ordering by a stable field (e.g. `id`)
- Cursor encoding is opaque (base64 encoded term)
- Sorting direction and filtering must remain consistent between requests
## Example
{posts, meta} =
Pagex.Cursor.paginate(Post, %{}, Repo)
Returning metadata:
%Pagex.Meta{
mode: :cursor,
next_cursor: "...",
prev_cursor: nil,
has_next: true
}
"""
import Ecto.Query
alias Pagex.Meta
@doc """
Executes cursor-based pagination on an Ecto query.
## Parameters
- `query` - an `Ecto.Queryable` (schema or query)
- `_params` - pagination params (reserved for future `after` / `before`)
- `repo` - the Ecto repository module
- `opts` - optional configuration:
- `:limit` - number of records to fetch (default: 20)
## Returns
{items, meta}
where:
- `items` is the list of results (limited by `limit`)
- `meta` contains cursor navigation data:
- `next_cursor` (encoded pointer to next page)
- `prev_cursor` (reserved for future use)
- `has_next`
- `has_prev`
- `limit`
- `count`
## Example
Pagex.Cursor.paginate(Post, %{}, Repo, limit: 50)
## Future extension
Planned support for:
- `"after"` cursor (forward pagination)
- `"before"` cursor (backward pagination)
- deterministic ordering via `order_by`
"""
def paginate(query, _params, repo, opts \\ []) do
limit = Keyword.get(opts, :limit, 20)
query =
query
|> limit(^limit)
items = repo.all(query)
next_cursor =
case List.last(items) do
nil -> nil
last -> encode_cursor(last.id)
end
meta =
Meta.new_cursor(%{
limit: limit,
count: length(items),
next_cursor: next_cursor,
prev_cursor: nil
})
{items, meta}
end
@doc false
defp encode_cursor(id) do
id
|> :erlang.term_to_binary()
|> Base.url_encode64()
end
end