gopager

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2025 License: MIT Imports: 13 Imported by: 0

README

GoPager - Cursor-Based Pagination for Go

A cursor-based pagination library for Go applications using GORM. GoPager provides efficient pagination for large datasets without the performance issues of traditional offset-based pagination.

tag Go Version Build Status Go report Coverage PkgGoDev License

Features

  • Efficient pagination for large datasets;
  • DefaultCursor for complex filtering and PseudoCursor for simple offset-based pagination;
  • Seamless integration with GORM ORM;
  • Lookahead pagination detects if there are more pages available further from the current one;
  • Support for multiple column sorting with custom directions;
  • Base64 encoded cursors.

Installation

go get github.com/Alp4ka/gopager@latest

Quick Start

Examples
Basic Usage with DefaultCursor
package main

import (
    "fmt"
    "log"
    
    "github.com/Alp4ka/gopager"
    "gorm.io/gorm"
)

type User struct {
    ID        uint   `gorm:"primaryKey"`
    Name      string
    Email     string
    CreatedAt time.Time
}

func main() {
	...
    // Create a new cursor pager
    pager := gopager.NewCursorPager[*gopager.DefaultCursor]().
        WithLimit(10).
        WithSort(
            gopager.OrderBy{Column: "id", Direction: gopager.DirectionASC}, // IMPORTANT: You must include unique column at least once.
            gopager.OrderBy{Column: "created_at", Direction: gopager.DirectionDESC},
        )
    
    // Apply pagination to GORM query
    var users []User
    db, err := pager.Paginate(db.Model(&User{}))
    if err != nil {
        log.Fatal(err)
    }
    
    // Execute the query
    result := db.Find(&users)
    if result.Error != nil {
        log.Fatal(result.Error)
    }
    
    // Generate next page cursor
    getters := gopager.Getters[User]{
        "id":         func(u User) any { return u.ID },
        "created_at": func(u User) any { return u.CreatedAt },
    }
    
    trimmedUsers, nextCursor, err := gopager.NextPageCursor(pager, users, getters)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Found %d users\n", len(trimmedUsers))
    if nextCursor != nil {
        fmt.Printf("Next page token: %s\n", nextCursor.String())
    }
}

Core Types

CursorPager

The main structure for handling cursor-based pagination. Create a new instance with NewCursorPager or decode an existing one from a string using DecodeCursorPager or DecodePseudoCursorPager.

WithLimit(limit int)

Set the select-request fetch limit.

WithUnlimited()

Enable unlimited select (cannot be used with lookahead option).

WithLookahead()

Enable lookahead to detect if there are more pages. If the page is the last one in the dataset, the next token will be nil.

WithSort(orderBy ...OrderBy)

Set sorting order. MUST include a column with a unique constraint.

WithSubstitutedSort(orderBy ...OrderBy)

This function works the same as CursorPager.WithSort, but it clears all existing sorts and replaces them with the new ones.

Paginate(db *gorm.DB)

Applies pagination to the select statement.

DefaultCursor

A cursor that uses complex filtering conditions for precise pagination. It relies on the values of a specific field from the last element of the previous page. For example, given the unique sorted dataset [1, 2, 3, 4] and a previous page of [1, 2], the next page would start from the first element satisfying the condition x > 2. This approach is significantly faster than using LIMIT/OFFSET on large datasets.

This cursor type requires:

  1. Sorted dataset;
  2. At least one unique element is required for correct filtering.
PseudoCursor

A simple cursor that uses LIMIT/OFFSET for pagination. It's less efficient on big datasets.

ParseSort

Converts a list of strings to the list of sorts. It is considered that each string is given in the next format: <column_alias> <ASC/DESC/asc/desc>. You should pass ColumnMapping as an argument in order to convert column_alias to a real column name inside the dataset.

// Map external column names to internal database columns
columnMapping := gopager.ColumnMapping{
    "user_id":    "users.id",
    "created":    "users.created_at",
    "email_addr": "users.email",
}

// Parse sort parameters with column mapping
sortParams := []string{"user_id asc", "created desc"}
orderings, err := gopager.ParseSort(sortParams, columnMapping)
if err != nil {
    log.Fatal(err)
}

pager := gopager.NewCursorPager[*gopager.DefaultCursor]().
    WithSort(orderings...)

Documentation

Index

Constants

View Source
const (
	NoLimit      = -1
	MaxLimit     = 100
	DefaultLimit = 10
)

Variables

This section is empty.

Functions

func IsLastPage

func IsLastPage[CursorType Cursor, T any](initialPager *CursorPager[CursorType], resultSet []T) bool

IsLastPage returns true if the result set is the last page in the dataset.

The last page is determined by one of two conditions:

  1. The number of returned records is less than Limit.
  2. Lookahead = true and the number of returned records is less than or equal to Limit.

In these cases, return the result set unchanged with an empty token to signal the end of the dataset to the client.

func IsNormalizedLimitMax

func IsNormalizedLimitMax(limit int, maxLimit int) (int, bool)

func NormalizeLimit

func NormalizeLimit(limit int) int

func NormalizeLimitMax

func NormalizeLimitMax(limit int, maxLimit int) int

func TrimResultSet

func TrimResultSet[CursorType Cursor, T any](initialPager *CursorPager[CursorType], resultSet []T) []T

TrimResultSet trims the result set to what should be returned to the client.

If lookahead = true, drop the last element before returning. Suppose resultSet = [a, b, c].

  • With lookahead → resultSet becomes [a, b].
  • Without lookahead → resultSet remains unchanged.

This enables building pagination based on a STRICT comparison with the last element of the result set.

Types

type ColumnAlias

type ColumnAlias = string

type ColumnMapping

type ColumnMapping = map[ColumnAlias]string

ColumnMapping maps external column aliases to fully qualified column names. Use it when bare column names could cause an "ambiguous column name" error. Key is an external alias, value is an internal column name.

type Cursor

type Cursor interface {
	String() string
	IsEmpty() bool
	Apply(*gorm.DB) *gorm.DB
	// contains filtered or unexported methods
}

type CursorElement

type CursorElement struct {
	Column   string   `json:"c"`
	Value    any      `json:"v"`
	Operator Operator `json:"o"`
}

CursorElement represents a triplet (c v o), where:

  • c: an object's field (column)
  • v: the value compared against the field
  • o: the operator applied to the pair (c, v)

type CursorPager

type CursorPager[CursorType Cursor] struct {
	// contains filtered or unexported fields
}

func DecodeCursorPager

func DecodeCursorPager(limit int, rawStartToken string, orderBy ...OrderBy) (*CursorPager[*DefaultCursor], error)

DecodeCursorPager decodes a cursor token into *CursorPager.

Usage guide: https://doc.office.lan/spaces/MBCSHCH/pages/417057947

func DecodePseudoCursorPager

func DecodePseudoCursorPager(limit int, rawStartToken string, orderBy ...OrderBy) (*CursorPager[*PseudoCursor], error)

DecodePseudoCursorPager decodes a pseudo-cursor token into *CursorPager.

Usage guide: https://doc.office.lan/spaces/MBCSHCH/pages/417057947

func NewCursorPager

func NewCursorPager[CursorType Cursor]() *CursorPager[CursorType]

func (*CursorPager[CursorType]) GetCursor

func (c *CursorPager[CursorType]) GetCursor() CursorType

GetCursor returns the cursor stored in CursorPager as-is.

func (*CursorPager[CursorType]) GetDatasetLimit

func (c *CursorPager[CursorType]) GetDatasetLimit() int

GetDatasetLimit returns the limit adjusted for lookahead:

  • if Lookahead = true → GetLimit() + 1
  • if Lookahead = false → GetLimit()

func (*CursorPager[CursorType]) GetLimit

func (c *CursorPager[CursorType]) GetLimit() int

GetLimit returns the limit as it is stored in CursorPager. The return value is >= 0. Returning NoLimit is equivalent to no limit.

func (*CursorPager[CursorType]) GetSort

func (c *CursorPager[CursorType]) GetSort() Orderings

GetSort returns orderings that will be applied to the dataset.

func (*CursorPager[CursorType]) IsLookahead

func (c *CursorPager[CursorType]) IsLookahead() bool

IsLookahead returns true if lookahead pagination is enabled.

func (*CursorPager[CursorType]) IsUnlimited

func (c *CursorPager[CursorType]) IsUnlimited() bool

IsUnlimited returns true if the limit equals NoLimit (unbounded number of records).

func (*CursorPager[CursorType]) Paginate

func (c *CursorPager[CursorType]) Paginate(db *gorm.DB) (*gorm.DB, error)

Paginate applies pagination to the dataset. Returns an error if pagination cannot be applied.

func (*CursorPager[CursorType]) WithCursor

func (c *CursorPager[CursorType]) WithCursor(cursor CursorType) *CursorPager[CursorType]

WithCursor sets the cursor explicitly.

func (*CursorPager[CursorType]) WithLimit

func (c *CursorPager[CursorType]) WithLimit(limit int) *CursorPager[CursorType]

WithLimit sets the maximum number of returned records.

IMPORTANT:

  • NoLimit cannot be used together with WithLookahead.
  • If the limit is not NoLimit, NormalizeLimit will be applied.

func (*CursorPager[CursorType]) WithLookahead

func (c *CursorPager[CursorType]) WithLookahead() *CursorPager[CursorType]

WithLookahead enables lookahead pagination, which checks the next page to determine whether the current page is the last.

IMPORTANT: Cannot be used together with WithUnlimited() or WithLimit(NoLimit).

func (*CursorPager[CursorType]) WithSort

func (c *CursorPager[CursorType]) WithSort(orderBy ...OrderBy) *CursorPager[CursorType]

WithSort appends sort orderings without overwriting existing ones. Order is preserved as if calling:

OrderBy(o1).ThenBy(o2).ThenBy(o3)...

func (*CursorPager[CursorType]) WithSubstitutedSort

func (c *CursorPager[CursorType]) WithSubstitutedSort(orderBy ...OrderBy) *CursorPager[CursorType]

WithSubstitutedSort resets previous orderings and applies the provided ones.

func (*CursorPager[CursorType]) WithUnlimited

func (c *CursorPager[CursorType]) WithUnlimited() *CursorPager[CursorType]

WithUnlimited allows returning all records without a limit.

IMPORTANT: Cannot be used together with WithLookahead.

type DefaultCursor

type DefaultCursor struct {
	// contains filtered or unexported fields
}

DefaultCursor represents a pagination token that defines the starting position for the requested page. An empty token means the beginning of the dataset.

IMPORTANT: The token MUST always include a condition on a unique column!

The token consists of a set of conditions of the form:

[(C1, O1, V1), (C2, O2, V2)... (Cn, On, Vn)]

func DecodeCursor

func DecodeCursor(b64String string) (*DefaultCursor, error)

DecodeCursor attempts to parse a base64-encoded string into *DefaultCursor.

func NewCursor

func NewCursor(elements ...CursorElement) *DefaultCursor

func NewDefaultCursor

func NewDefaultCursor(elements ...CursorElement) *DefaultCursor

func NextPageCursor

func NextPageCursor[T any](
	initialPager *CursorPager[*DefaultCursor],
	resultSet []T,
	getters Getters[T],
) ([]T, *DefaultCursor, error)

NextPageCursor builds a cursor for the next page of the dataset.

func (*DefaultCursor) Apply

func (c *DefaultCursor) Apply(db *gorm.DB) *gorm.DB

Apply - implements Cursor. Applies filter-based offset to the gorm query.

func (*DefaultCursor) GetElements

func (c *DefaultCursor) GetElements() []CursorElement

GetElements returns token elements. Cursor elements are a compressed set of filter conditions.

IMPORTANT: These filter conditions must NOT be applied directly to data, as they are not complete. During pagination they are inflated into a full set of filtering conditions.

func (*DefaultCursor) IsEmpty

func (c *DefaultCursor) IsEmpty() bool

IsEmpty - implements Cursor.

func (*DefaultCursor) String

func (c *DefaultCursor) String() string

String - implements fmt.Stringer.

func (*DefaultCursor) ToSQL

func (c *DefaultCursor) ToSQL() (string, []driver.Value)

ToSQL - implements Cursor. Returns the SQL expression representing the filter.

Usage:

query := fmt.Sprintf("SELECT * FROM table WHERE %s", p.ToSQL())

func (*DefaultCursor) WithElements

func (c *DefaultCursor) WithElements(elements []CursorElement) *DefaultCursor

WithElements explicitly sets the token elements.

type Direction

type Direction string

Direction defines the sort direction for the requested dataset.

const (
	DirectionASC  Direction = "ASC"
	DirectionDESC Direction = "DESC"
)

func (Direction) ForOperator

func (o Direction) ForOperator() Operator

func (Direction) Valid

func (o Direction) Valid() bool

type Getters

type Getters[T any] map[string]func(T) any

Getters is a map of getters for a type. Specify the columns used for pagination. Example:

pager.Getters[models.PlayerPushTarget]{
	"id":          func(last models.PlayerPushTarget) any { return last.ID },
	"deposit_sum": func(last models.PlayerPushTarget) any { return last.DepositSum },
}

type Operator

type Operator string

Operator defines a comparison operator for filtering by column. Used in pagination filtering conditions.

const (
	OperatorGT Operator = ">"
	OperatorLT Operator = "<"
)

func (Operator) ForOrdering

func (o Operator) ForOrdering() Direction

func (Operator) Valid

func (o Operator) Valid() bool

type OrderBy

type OrderBy struct {
	Column    string
	Direction Direction
}

type Orderings

type Orderings []OrderBy

func ParseSort

func ParseSort(stringsOrderings []string, columnMapping ColumnMapping) (Orderings, error)

ParseSort builds Orderings from a list of strings in the format "column asc|desc". Column aliases are resolved via ColumnMapping. Returns an error if an alias is not found in the mapping.

func (Orderings) Apply

func (o Orderings) Apply(db *gorm.DB) *gorm.DB

Apply applies the ordering to a gorm query.

func (Orderings) ToSQL

func (o Orderings) ToSQL() string

ToSQL converts Orderings to a single string "<order_column_1> <order_direction_1>, <order_column_2> <order_direction_2>" suitable for embedding into an SQL query. Example: for [{"a", "ASC"}, {"b", "DESC"}] returns "a ASC, b DESC".

Usage:

query := fmt.Sprintf("SELECT * FROM table ORDER BY %s", orderings.ToSQL())

func (Orderings) ToSQLSlice

func (o Orderings) ToSQLSlice() []string

ToSQLSlice converts Orderings to a slice of strings in the form "<order_column> <order_direction>" suitable for SQL query builders.

Example: for Orderings: [{"a", "ASC"}, {"b", "DESC"}] returns ["a ASC", "b DESC"].

type PaginationResult

type PaginationResult[T any, CursorType Cursor] struct {
	// Items result elements.
	Items []T
	// Total number of elements.
	Total int64
	// AppliedLimit effective limit used for the query.
	AppliedLimit int
	// NextPageToken token for the next page.
	NextPageToken CursorType
}

PaginationResult is a generic paginated result container.

type PseudoCursor

type PseudoCursor struct {
	// contains filtered or unexported fields
}

PseudoCursor is used when an API requires cursor-based pagination but only LIMIT/OFFSET pagination is available.

It implements Cursor and generates a token based on the last offset within the dataset.

func DecodePseudoCursor

func DecodePseudoCursor(b64String string) (*PseudoCursor, error)

DecodePseudoCursor attempts to parse a base64-encoded string into *PseudoCursor.

func NewPseudoCursor

func NewPseudoCursor(offset int) *PseudoCursor

func NextPagePseudoCursor

func NextPagePseudoCursor[T any](
	initialPager *CursorPager[*PseudoCursor],
	resultSet []T,
) ([]T, *PseudoCursor, error)

NextPagePseudoCursor builds a pseudo-cursor for the next page of the dataset.

func (*PseudoCursor) Apply

func (p *PseudoCursor) Apply(db *gorm.DB) *gorm.DB

Apply - implements Cursor. Applies the offset to a gorm query.

func (*PseudoCursor) GetOffset

func (p *PseudoCursor) GetOffset() int

GetOffset returns the numeric offset value.

func (*PseudoCursor) IsEmpty

func (p *PseudoCursor) IsEmpty() bool

IsEmpty - implements Cursor.

func (*PseudoCursor) String

func (p *PseudoCursor) String() string

String - implements fmt.Stringer.

func (*PseudoCursor) ToSQL

func (p *PseudoCursor) ToSQL() string

ToSQL - implements Cursor. Returns the string form of the numeric offset value.

Usage:

query := fmt.Sprintf("SELECT * FROM table OFFSET %s", p.ToSQL())

func (*PseudoCursor) WithOffset

func (p *PseudoCursor) WithOffset(offset int) *PseudoCursor

WithOffset sets the numeric offset value and returns the cursor.

type RawCursorPager

type RawCursorPager struct {
	// Limit - maximum number of records to return in the response.
	Limit int `json:"limit"`
	// StartToken - base64-encoded cursor token obtained via Cursor.String().
	// If empty, the first page with Limit records is returned.
	StartToken string `json:"startToken"`
}

RawCursorPager is intended for API payloads. For proper code generation, inline it:

type MyFilter struct {
    Paging RawCursorPager `json:",inline"`
}

func (RawCursorPager) Decode

func (p RawCursorPager) Decode(orderBy ...OrderBy) (*CursorPager[*DefaultCursor], error)

Decode converts RawCursorPager into *CursorPager[*DefaultCursor], normalizing Limit and validating StartToken. Returns *CursorPager[*DefaultCursor] with WithSort applied.

func (RawCursorPager) DecodePseudo

func (p RawCursorPager) DecodePseudo(orderBy ...OrderBy) (*CursorPager[*PseudoCursor], error)

DecodePseudo converts RawCursorPager into *CursorPager[*PseudoCursor], normalizing Limit and validating StartToken. Returns *CursorPager[*PseudoCursor] with WithSort applied.

Jump to

Keyboard shortcuts

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