cursor

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 12, 2025 License: MIT Imports: 11 Imported by: 0

README

Cursor

GoDoc Build Status Code Coverage Go Report Card

A lightweight, generic cursor-based pagination package for Go. Designed for MySQL and MariaDB, cursor lets you build encrypted, stateless cursors that encode pagination state and query parameters safely.

Cursor-based pagination is preferred to OFFSET for better performance on large tables. It stores the last seen ID or timestamp in the cursor to build efficient WHERE clauses. It is recommended to check the expiration date with IsExpired(maxAge) to avoid using outdated cursors.

3 main types:

  • Cursor allows computation of data necessary for pagination.
  • Statement builds based on a Cursor SQL query parts, to use to perform a SELECT statement.
  • Pointer defines the data types that can be used as a cursor to filter the query. Such as Int64 to manage the auto-increment field. See also String or List to manage a set of Pointer as Pointer. Finally, RowCount can be used as Pointer to transform the cursor into a standard LIMIT statement, with offset and row count (also see Statement.Offset).
Cursor Encoding Format
  • Internally serialized as JSON, then encoded as Base64 (URL-safe).
  • Optionally encrypted or signed using HMAC for integrity.
  • Fully stateless — no server session needed.

Features

  • 🔒 Encrypted cursors — opaque Base64 tokens with or not HMAC signing.
  • 📜 Cursor-based pagination — no offset drift, efficient for large datasets.
  • 🧠 Stateless by design — all state is encoded in the cursor.
  • 💡 Generic — supports any data type T.
  • 🧩 SQL helpers for LIMIT, ORDER BY, and conditional pagination queries.
  • ⏱️ Expiration support — cursors can self-expire based on max age.

Installation

go get github.com/rvflash/cursor

Example Usage

Codes with multiple shortcuts for demonstration purposes only.

SQL statement
func ListFromDatabase(ctx context.Context, cur *cursor.Cursor[cursor.Int64]) ([]User, error) {
    // Create a Statement based on the Cursor.
    var st = cursor.Statement[cursor.Int64]{
        Cursor:          cur,
        DescendingOrder: false,
    }
    // WHERE uses the cursor semantics (e.g., "id < ?") under descending order
    where, args := st.WhereCondition("id")
    if len(args) > 0 {
        where = " WHERE " + where
    }
    // LIMIT +1 to check if there is a next page.
    args = append(args, st.Limit())
    // ORDER BY applies the desired ordering for the limited page (e.g., "ORDER BY id DESC")
    query := `SELECT id, name FROM users` + where + " ORDER BY" + st.OrderBy("id") + " LIMIT ?"
    // Reset allows to reuse the current cursor to build the next ones.
    cur.Reset()

    rows, err := DB.QueryContext(ctx, query, args...)
    if err != nil {
        return nil, err
    }
    defer func() { _ = rows.Close() }()
    
    var (
		res []User
        u User
	)
    for rows.Next() {
        if err = rows.Scan(&u.ID, &u.Name); err != nil {
            return nil, err
        }
        res = append(res, u)
        cur.Add(cursor.Int64(u.ID)) // we’re pointing by ID in this example
    }
    return res[:min(len(res), st.Cursor.Limit)], rows.Err()
}

type User struct {
    ID        int64  `json:"id"`
    Name      string `json:"name"`
}
Integrating with an HTTP API

Example of returning paginated results in a REST response:

func HTTPHandler(w http.ResponseWriter, r *http.Request) {
    var (
        secret = []byte(os.Getenv("CURSOR_SECRET"))
        cur    *cursor.Cursor[cursor.Int64]
        err    error
    )
    if tok := r.URL.Query().Get("cursor"); tok != "" {
        // Decrypt verifies HMAC and returns the cursor state
        cur, err = cursor.Decrypt[cursor.Int64]([]byte(tok), secret)
        if err != nil || cur.IsExpired(time.Hour) {
         http.Error(w, "invalid or expired cursor", http.StatusBadRequest)
            return
        }
	} else {
        // New(limit, total). If you don’t know total, you can pass 0 (or compute it separately)
        cur = cursor.New[cursor.Int64](20, 0)
    }
    // SQL query
    rows, err := ListFromDatabase(r.Context(), cur)
    if err != nil {
        http.Error(w, "query error", http.StatusInternalServerError)
        return
    }
    // Build pagination tokens: first/prev/next/last.
    pg, err := cursor.Paginate(cur, secret)
    if err != nil {
        http.Error(w, "pagination error", http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    _ = json.NewEncoder(w).Encode(usersResponse{
        Data:       rows,
        Pagination: pg,
    })
}

type usersResponse struct {
    Data        []User  `json:"data"`
    Pagination  *cursor.Pagination `json:"cursor"`
}

Documentation

Overview

Package cursor uses a reference point (cursor) to fetch the next set of results. This reference point is typically a unique identifier that define the sort order.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Encrypt

func Encrypt[T Pointer](c *Cursor[T], secret []byte) ([]byte, error)

Encrypt encrypts the cursor and then ensures it integrity by signing the content. It concatenates the base64-encoded JSON representation of the cursor with a dot and its sha256 hmac signature.

Types

type Cursor

type Cursor[T Pointer] struct {
	Prev     *T         `json:"prev,omitempty"`
	Next     *T         `json:"next,omitempty"`
	IssuedAt int64      `json:"issued_at,omitempty"` // epoch seconds
	Limit    int        `json:"limit"`
	Total    *int       `json:"total,omitempty"`
	Filters  url.Values `json:"filters,omitempty"`
	// contains filtered or unexported fields
}

Cursor contains elements required to paginate based on a cursor, a data pointed the start of the data to list.

func Decrypt

func Decrypt[T Pointer](content, secret []byte) (*Cursor[T], error)

Decrypt decrypts the cursor, ensures its integrity by verifying its HMAC signature.

func First

func First[T Pointer](c *Cursor[T]) *Cursor[T]

First returns the cursor of the first page.

func Last

func Last[T Pointer](c *Cursor[T]) *Cursor[T]

Last returns the cursor of the last page.

func New

func New[T Pointer](limit, total int) *Cursor[T]

New creates a new cursor based on this limit and total.

func Next

func Next[T Pointer](c *Cursor[T]) *Cursor[T]

Next returns the cursor of the next page.

func Prev

func Prev[T Pointer](c *Cursor[T]) *Cursor[T]

Prev returns the cursor of the previous page.

func (*Cursor[T]) Add

func (c *Cursor[T]) Add(d T)

Add notifies a new entry to the managed list of result.

func (*Cursor[T]) Decode

func (c *Cursor[T]) Decode(text []byte) error

Decode decodes a plain cursor.

func (*Cursor[T]) Encode

func (c *Cursor[T]) Encode() ([]byte, error)

Encode encodes the cursor as plain data.

func (*Cursor[T]) IsExpired

func (c *Cursor[T]) IsExpired(maxAge time.Duration) bool

IsExpired returns true if the issued timestamp exceeds the max age allowed.

func (*Cursor[T]) Reset

func (c *Cursor[T]) Reset()

Reset resets the cursor allowing to reuse it in the same context.

func (*Cursor[T]) String

func (c *Cursor[T]) String() string

String implements the fmt.Stringer interface.

func (*Cursor[T]) TotalItems

func (c *Cursor[T]) TotalItems() int

TotalItems returns the total number of items, or -1 if unknown.

func (*Cursor[T]) TotalPages

func (c *Cursor[T]) TotalPages() int

TotalPages returns the total number of pages, or -1 if unknown.

type Int64

type Int64 int64

Int64 mangers int64 pointer.

func (Int64) Args

func (n Int64) Args() []any

Args implements the Pointer interface.

func (Int64) IsZero

func (n Int64) IsZero() bool

IsZero implements the Pointer interface.

type List

type List []Pointer

List allows manipulation of a list of pointer data as pointer.

func (List) Args

func (l List) Args() []any

Args implements the Pointer interface.

func (List) IsZero

func (l List) IsZero() bool

IsZero implements the Pointer interface.

type Pagination

type Pagination struct {
	First string `json:"first,omitempty"`
	Prev  string `json:"prev,omitempty"`
	Last  string `json:"last,omitempty"`
	Next  string `json:"next,omitempty"`
}

Pagination contains all cursors to navigate from a cursor.

func Paginate

func Paginate[T Pointer](c *Cursor[T], secret []byte) (*Pagination, error)

Paginate generations all cursors to navigate from a cursor.

type Pointer

type Pointer interface {
	// Args returns the arguments to use in a statement.
	Args() []any
	// IsZero returns true if the pointer is a zero value.
	IsZero() bool
}

Pointer must be implemented by any cursor point.

type Statement

type Statement[T Pointer] struct {
	// Cursor is the cursor of pagination.
	Cursor *Cursor[T]
	// DescendingOrder defines the result's order by default.
	DescendingOrder bool
}

Statement allows building of SQL query for MySQL or MariaDB. The idea is to build a SQL statement like this one to going forward and returns results based on the cursor with descending order.

WITH d AS (

SELECT * FROM table t WHERE cursor > ? ORDER BY cursor ASC LIMIT ?

) SELECT * FROM p ORDER BY cursor DESC;

Limit statement adds one to the cursor's limit in order to know the start of the next cursor and if there is more data.

func (Statement[T]) Limit

func (s Statement[T]) Limit() int

Limit returns the row count to restrict the number of returned rows. The value is incremented by one to check if there is more to fetch.

func (Statement[T]) OrderBy

func (s Statement[T]) OrderBy(columns ...string) string

OrderBy returns the clause to order the selected and limited resultset. It differs from OrderBy to limit its scope to the WITH statement, also known as data source.

func (Statement[T]) WhereCondition

func (s Statement[T]) WhereCondition(columns ...string) (string, []any)

WhereCondition returns the condition that rows must satisfy to be selected.

type String

type String string

String manages string pointer.

func (String) Args

func (s String) Args() []any

Args implements the Pointer interface.

func (String) IsZero

func (s String) IsZero() bool

IsZero implements the Pointer interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL