cursor

package module
v0.0.0-...-683b0dd Latest Latest
Warning

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

Go to latest
Published: Jun 30, 2026 License: BSD-3-Clause Imports: 10 Imported by: 0

README

cursor

Relay-style cursor pagination for go-jet MySQL queries.

Encodes opaque cursor strings from column values, applies consistent WHERE / ORDER BY clauses, and builds GraphQL Relay–compatible Connection results with PageInfo and total counts.

Requirements

go get gitea.auvem.com/go-toolkit/cursor

Setup

Register every column referenced by a cursor at application startup. The registry resolves columns when encoding, decoding, and building SQL.

import (
    "gitea.auvem.com/go-toolkit/cursor"
    "yourapp/.gen/yourdb/table"
)

func init() {
    cursor.RegisterColumn(table.User.ID, table.Meeting.StartTime)
}

Concepts

Term Meaning
Index Stable position column (usually the primary key).
Order column Column users sort by. May differ from the index.
Simple cursor Index and order column are the same; paginate on one value.
Tuple ordering Order column ≠ index; results sort by (order_col, index_col).
Composite cursor Tuple ordering plus an encoded OrderValue on each edge — required for correct after pagination when sort values can repeat.

Quick start

1. Define a cursor factory
func newUserCursor() *cursor.Cursor[mysql.IntegerExpression, mysql.ColumnInteger] {
    return cursor.NewCursor(
        cursor.NewInt64Value(0, table.User.ID),
        table.User.ID,
        cursor.OrderDescending,
    )
}
2. Paginate a query
active := newUserCursor()
if after != nil {
    _ = active.Decode(*after)
}

stmt := table.User.
    SELECT(table.User.AllColumns).
    WHERE(filters).
    WHERE(cursor.PaginateConds(active)).
    ORDER_BY(cursor.OrderByClauses(active)...).
    LIMIT(int64(limit))

var rows []*User
dbx.MustQuery(db, stmt, &rows, nil)

Or use PageQuery to run the query and build edges in one step:

conn, err := cursor.PageQuery[User, mysql.IntegerExpression, mysql.ColumnInteger]{
    Sqlo:    db,
    Stmt:    table.User.SELECT(table.User.AllColumns),
    Conds:   filters,
    Cursor:  decodedCursor,       // nil for first page
    Default: newUserCursor,
    Limit:   limit,
    CountFn: cursor.BuildQueryCountFn(table.User.ID, table.User, filters),
    ToEdge: func(_ *cursor.Cursor[...], item *User) (cursor.GenericCursor, error) {
        return newUserCursor().CopyWithVal(
            cursor.NewInt64Value(item.ID, table.User.ID),
        ), nil
    },
}.Run()
3. Relay after / first args
conn, err := cursor.ConnectionFromRelayArgs(
    after, first, newUserCursor,
    func(c *cursor.Cursor[...], limit int) (*cursor.Connection[User], error) {
        return listUsers(c, limit)
    },
)
4. Composite (multi-column) sort

When sorting by a non-unique column, include both index and order values on each edge:

base := cursor.NewCursor(
    cursor.NewStringValue("", table.Meeting.ID),
    table.Meeting.StartTime,
    cursor.OrderDescending,
)

edgeCursor := base.CopyWithVals(
    cursor.NewStringValue(row.ID, table.Meeting.ID),
    cursor.NewTimestampValue(row.StartTime, table.Meeting.StartTime),
)

API overview

Layer Types / functions
Column registry RegisterColumn, RegisterColumnList, GetColumn, GetColumnByKey
Cursor values NewInt64Value, NewUint64Value, NewStringValue, NewTimestampValue
Cursor NewCursor, CopyWithVal, CopyWithVals, Encode, Decode, NewCursorFromAfterPtr
SQL helpers PaginateConds, OrderByClauses, QueryCount, BuildQueryCountFn
Relay output BuildEdges, PageQuery, ConnectionFromRelayArgs, Connection, PageInfo

Value types

Index / order type Constructor
int64 NewInt64Value
uint64 NewUint64Value
string / UUID NewStringValue
time.Time NewTimestampValue

Extension points

Custom cursor value types implement [GenericExpr] (order values) and [ExprMarshaler] (index values). See package godoc for requirements.

Errors

Decode failures return ErrBadCursorString; use errors.Is to detect invalid client cursors.

Utilities

  • ExtractNodes — nodes from a Connection
  • DereferenceSlice[]*T[]T, skipping nils

Documentation

Overview

Package cursor implements Relay-style cursor pagination for go-jet MySQL queries.

It encodes opaque cursor strings from column values, generates matching WHERE and ORDER BY clauses, and builds GraphQL Relay–compatible Connection results with PageInfo and total counts.

Requirements

Setup

Call RegisterColumn (or RegisterColumnList) at application startup for every column referenced by a cursor—index column, order column, and any column held in OrderValue. The registry resolves columns when encoding, decoding, and building SQL. Cursor.OrderCol, value Col methods, and GetColumnByKey panic when a column is not registered.

Pagination modes

Simple pagination sorts and filters on a single column (typically the primary key). When the order column differs from the index column, tuple ordering applies: results sort by (order_col, index_col). See Cursor.UsesTupleOrdering.

Composite cursors extend tuple ordering with an encoded OrderValue on each edge. Required for correct after pagination when sort values can repeat (for example, many rows sharing the same timestamp). See Cursor.IsComposite and [CopyWithVals].

API overview

Setup — RegisterColumn, RegisterColumnList, GetColumn, GetColumnByKey

Cursor construction — NewCursor, NewInt64Value, NewUint64Value, NewStringValue, NewTimestampValue, [CopyWithVal], [CopyWithVals]

Serialization — Cursor.Encode, Cursor.Decode, Cursor.DecodeAndOrder, NewCursorFromAfterPtr, NewCursorFromJSON

Query integration — PaginateConds, OrderByClauses, QueryCount, BuildQueryCountFn

Relay layer — PageQuery, BuildEdges, ConnectionFromRelayArgs, Connection, PageInfo, Edge

Utilities — ExtractNodes, DereferenceSlice

Extension points

Consumers may define custom cursor value types by implementing the interfaces below. Built-in types (Int64Value, StringValue, and others) demonstrate the pattern.

GenericExpr is the untyped value contract shared by index and order columns. Implement ColumnKey, IsEmpty, and driver.Valuer. Use JSON struct tags matching the built-in value types when values are encoded inside a cursor.

ExprMarshaler extends GenericExpr for index columns. Implement Expr and Col to produce type-safe go-jet expressions. Col requires the column to be registered. Custom index types that are not handled by the built-in type switch in pagination fall back to raw SQL comparisons; registering the column is still required.

GenericCursor is implemented by Cursor and is the interface used when building edges, counting pages, and applying pagination conditions without concrete type parameters.

OrderValue decoding selects a built-in wrapper type from the registered go-jet column type (string, integer, timestamp). Custom order values should use one of the built-in value types or match their JSON shape for the corresponding column kind.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrBadCursorString is returned when a cursor string cannot be decoded.
	ErrBadCursorString = errors.New("bad cursor string")
)

Functions

func DereferenceSlice

func DereferenceSlice[T any](list []*T) []T

DereferenceSlice takes a slice of pointers to T and returns a slice of T by dereferencing each pointer and discarding any nil pointers.

func ExtractNodes

func ExtractNodes[T any](conn *Connection[T]) []*T

ExtractNodes extracts the nodes from a Connection object and returns them as a slice of pointers to type T.

func GetColumn

func GetColumn(table, column string) (mysql.Column, error)

GetColumn retrieves a column from the cursor registry.

func GetColumnByKey

func GetColumnByKey(key ColumnKey) (mysql.Column, error)

GetColumnByKey retrieves a column from the cursor registry by its ColumnKey.

func OrderByClauses

func OrderByClauses(c GenericCursor) []mysql.OrderByClause

OrderByClauses returns ORDER BY clauses matching the cursor pagination semantics, including tuple ordering when GenericCursor.UsesTupleOrdering is true.

func PaginateConds

func PaginateConds[IE mysql.Expression, IC mysql.Column](c *Cursor[IE, IC]) mysql.BoolExpression

PaginateConds returns a mysql.BoolExpression that paginates results using the provided cursor as a base position. Nil or empty cursors match all rows.

func RegisterColumn

func RegisterColumn(columns ...mysql.Column)

RegisterColumn registers one or more columns with the cursor registry.

Required at application startup before encoding, decoding, or SQL generation. Every index column, order column, and OrderValue column must be registered.

func RegisterColumnList

func RegisterColumnList(columns mysql.ColumnList)

RegisterColumnList registers a list of columns with the cursor registry. See RegisterColumn for setup requirements.

Types

type ColumnKey

type ColumnKey struct {
	Table  string `json:"table"`
	Column string `json:"column"`
}

ColumnKey identifies a column and table in the database with a given value.

func NewColumnKey

func NewColumnKey(col mysql.Column) ColumnKey

NewColumnKey creates a new ColumnKey with the specified table and column names.

func (*ColumnKey) IsEmpty

func (k *ColumnKey) IsEmpty() bool

IsEmpty returns true if the ColumnKey is empty.

func (ColumnKey) String

func (k ColumnKey) String() string

String returns a string representation of the ColumnKey.

type Connection

type Connection[T any] struct {
	Edges      []*Edge[T]
	PageInfo   *PageInfo
	TotalCount int
}

Connection represents a paginated list of edges, along with page information.

func BuildEdges

func BuildEdges[T any](
	sqlo dbx.Queryable,
	countFn QueryCountFn,
	list []*T,
	cursorFunc CursorFunc[T],
) (
	conn *Connection[T],
	err error,
)

BuildEdges constructs a Connection object from a list of items of type T, using the provided Queryable interface for database operations and a count function to determine the total number of items. It also uses a cursor function to generate cursors for each item in the list.

func ConnectionFromRelayArgs

func ConnectionFromRelayArgs[T any, IE mysql.Expression, IC mysql.Column](
	after *string,
	first *int,
	newZero func() *Cursor[IE, IC],
	list func(cursor *Cursor[IE, IC], limit int) (*Connection[T], error),
) (*Connection[T], error)

ConnectionFromRelayArgs parses Relay after/first args and calls list with the decoded cursor and limit. newZero returns a template cursor used to decode after (same shape as the cursor factory for the resource, e.g. newUserCursor).

type Cursor

type Cursor[IE mysql.Expression, IC mysql.Column] struct {
	// Index identifies the column used for positioning the cursor and its current value.
	Index ExprMarshaler[IE, IC] `json:"index"`

	// OrderValue holds the order column value at this cursor position when using
	// composite tuple pagination.
	OrderValue GenericExpr `json:"order_val,omitempty"`

	// OrderColumnKey identifies the column used for ordering the results.
	OrderColumnKey ColumnKey `json:"order_col"`
	// OrderDir is the direction of the order (ASC or DESC).
	OrderDir OrderDirection `json:"order_dir"`
}

Cursor identifies a location and order in the database.

Index is the stable position column (typically the primary key). OrderColumnKey is the primary sort column shown to users. When they differ and OrderValue is set, pagination uses lexicographic tuple comparison on (order_col, index_col).

func NewCursor

func NewCursor[IE mysql.Expression, IC mysql.Column](index ExprMarshaler[IE, IC], orderCol CursorOrderCol, orderDir OrderDirection) *Cursor[IE, IC]

NewCursor creates a new Cursor with the specified parameters.

func NewCursorFromAfterPtr

func NewCursorFromAfterPtr[IE mysql.Expression, IC mysql.Column](
	newZero func() *Cursor[IE, IC],
	after *string,
) (*Cursor[IE, IC], error)

NewCursorFromAfterPtr decodes a Relay-style after cursor. Nil or empty after returns (nil, nil). Returns ErrBadCursorString when after is not valid cursor JSON.

func NewCursorFromJSON

func NewCursorFromJSON[IE mysql.Expression, IC mysql.Column](zeroIndex ExprMarshaler[IE, IC], src []byte) (*Cursor[IE, IC], error)

NewCursorFromJSON returns a Cursor from a JSON representation. Returns ErrBadCursorString when src is not valid cursor JSON.

func (*Cursor[IE, IC]) CopyWithVal

func (c *Cursor[IE, IC]) CopyWithVal(val ExprMarshaler[IE, IC]) *Cursor[IE, IC]

CopyWithVal returns a new cursor with the specified index value and the current ordering. Use for simple pagination where the index and order column are the same.

func (*Cursor[IE, IC]) CopyWithVals

func (c *Cursor[IE, IC]) CopyWithVals(index ExprMarshaler[IE, IC], orderVal GenericExpr) *Cursor[IE, IC]

CopyWithVals returns a new cursor with the specified index and order values. Use when building edge cursors under tuple ordering: set both values so [IsComposite] is true and after pagination compares (order_col, index_col).

func (*Cursor[IE, IC]) Decode

func (c *Cursor[IE, IC]) Decode(src string) error

Decode decodes a stringified JSON representation of the cursor into this object. Returns ErrBadCursorString when the input is not valid cursor JSON.

func (*Cursor[IE, IC]) DecodeAndOrder

func (c *Cursor[IE, IC]) DecodeAndOrder(src string, orderDir OrderDirection) error

DecodeAndOrder decodes a cursor string like Cursor.Decode but replaces the encoded order direction with orderDir.

func (*Cursor[IE, IC]) Direction

func (c *Cursor[IE, IC]) Direction() OrderDirection

Direction returns the order direction expected by the cursor.

func (*Cursor[IE, IC]) Encode

func (c *Cursor[IE, IC]) Encode() (string, error)

Encode returns a stringified JSON representation of the cursor.

func (*Cursor[IE, IC]) GenericIndex

func (c *Cursor[IE, IC]) GenericIndex() GenericExpr

GenericIndex returns the Index ExprMarshaler as a GenericExpr.

func (*Cursor[IE, IC]) GenericOrderValue

func (c *Cursor[IE, IC]) GenericOrderValue() GenericExpr

GenericOrderValue returns the order column value when using composite pagination.

func (*Cursor[IE, IC]) IsComposite

func (c *Cursor[IE, IC]) IsComposite() bool

IsComposite reports whether after-pagination filters on (order_col, index_col).

True when tuple ordering is active and OrderValue is set—typical for encoded edge cursors when the sort column is non-unique. Drives PaginateConds and page counting via lexicographic tuple comparison.

func (*Cursor[IE, IC]) IsEmpty

func (c *Cursor[IE, IC]) IsEmpty() bool

IsEmpty returns true if the cursor or any of its keys are empty or unmapped.

func (*Cursor[IE, IC]) OrderCol

func (c *Cursor[IE, IC]) OrderCol() CursorOrderCol

OrderCol returns the column used for ordering the results. Panics if the column is not registered.

func (*Cursor[IE, IC]) String

func (c *Cursor[IE, IC]) String() string

String returns a string representation of the Cursor.

func (*Cursor[IE, IC]) UsesTupleOrdering

func (c *Cursor[IE, IC]) UsesTupleOrdering() bool

UsesTupleOrdering reports whether results are sorted by (order_col, index_col).

True when the order column differs from the index column. Applies to default cursors on the first page and drives OrderByClauses. Unlike [IsComposite], OrderValue is not required.

type CursorFunc

type CursorFunc[T any] = func(*T) (GenericCursor, error)

CursorFunc builds a GenericCursor for a connection node.

type CursorOrderCol

type CursorOrderCol interface {
	mysql.Column
	ASC() mysql.OrderByClause
	DESC() mysql.OrderByClause
}

CursorOrderCol is an interface for SQL expression that support the ORDER BY clause.

type Edge

type Edge[T any] struct {
	Node   *T
	Cursor string
}

Edge represents a single edge in a connection, containing a node of type T and a cursor string for pagination.

type ExprMarshaler

type ExprMarshaler[E mysql.Expression, C mysql.Column] interface {
	GenericExpr
	Expr() E
	Col() C
}

ExprMarshaler is the typed index-value extension point. Implement GenericExpr plus Expr and Col to produce go-jet expressions. Col panics when the column is not registered via RegisterColumn.

type GenericCursor

type GenericCursor interface {
	IsEmpty() bool
	IsComposite() bool
	UsesTupleOrdering() bool
	OrderCol() CursorOrderCol
	Encode() (string, error)
	Decode(src string) error
	String() string
	GenericIndex() GenericExpr
	GenericOrderValue() GenericExpr
	Direction() OrderDirection
}

GenericCursor is the cursor extension point used by edge building, page counting, and pagination helpers without concrete type parameters. Cursor implements this interface.

type GenericExpr

type GenericExpr interface {
	ColumnKey() ColumnKey
	IsEmpty() bool
	driver.Valuer
}

GenericExpr is the cursor value extension point shared by index and order columns. Implement ColumnKey, IsEmpty, and driver.Valuer. Order values decoded from cursor JSON must match a registered go-jet column kind; see package documentation.

type Int64Value

type Int64Value struct {
	Key ColumnKey `json:"key"`
	Val int64     `json:"val"`
}

func NewInt64Value

func NewInt64Value(val int64, col mysql.ColumnInteger) *Int64Value

NewInt64Value creates a cursor value for a signed integer column. The column must be registered via RegisterColumn before Col or decode use it.

func (Int64Value) Col

func (i Int64Value) Col() mysql.ColumnInteger

func (*Int64Value) ColumnKey

func (i *Int64Value) ColumnKey() ColumnKey

func (Int64Value) Expr

func (*Int64Value) IsEmpty

func (i *Int64Value) IsEmpty() bool

func (Int64Value) Value

func (i Int64Value) Value() (driver.Value, error)

type OrderDirection

type OrderDirection int

OrderDirection represents the order direction of a cursor.

const (
	// OrderAscending sorts results in ascending order.
	OrderAscending OrderDirection = iota
	// OrderDescending sorts results in descending order.
	OrderDescending
)

func (OrderDirection) MarshalJSON

func (od OrderDirection) MarshalJSON() ([]byte, error)

MarshalJSON marshals the OrderDirection to JSON.

func (*OrderDirection) UnmarshalJSON

func (od *OrderDirection) UnmarshalJSON(data []byte) error

UnmarshalJSON unmarshals the OrderDirection from JSON.

type PageInfo

type PageInfo struct {
	// The cursor string for the last item in the current page.
	EndCursor *string `json:"endCursor,omitempty"`
	// Whether there are more items after the current page.
	HasNextPage bool `json:"hasNextPage"`
	// Whether there are more items before the current page.
	HasPreviousPage bool `json:"hasPreviousPage"`
	// The cursor string for the first item in the current page.
	StartCursor *string `json:"startCursor,omitempty"`
}

PageInfo holds standard pagination information, including cursors for the start and end of the page, and flags indicating whether there are more pages before or after the current page.

Modeled after the PageInfo type in the GraphQL Relay specification.

type PageQuery

type PageQuery[T any, IE mysql.Expression, IC mysql.Column] struct {
	Sqlo    dbx.Queryable
	Stmt    mysql.SelectStatement
	Conds   mysql.BoolExpression
	Cursor  *Cursor[IE, IC]
	Default func() *Cursor[IE, IC]
	Limit   int
	CountFn QueryCountFn
	ToEdge  func(active *Cursor[IE, IC], item *T) (GenericCursor, error)
	// Scan runs the paginated query into dest. When nil, Run uses Stmt.Query.
	Scan func(stmt mysql.SelectStatement, dest *[]*T) error
	// AfterScan optionally transforms rows after the query and before building edges.
	AfterScan func(items []*T) ([]*T, error)
}

PageQuery runs a paginated SELECT and builds a Connection from the results.

func (PageQuery[T, IE, IC]) Run

func (q PageQuery[T, IE, IC]) Run() (*Connection[T], error)

Run executes the paginated query and returns a Connection.

type QueryCountFn

type QueryCountFn = func(
	sqlo dbx.Queryable,
	start GenericCursor,
	end GenericCursor,
) (QueryCountResult, error)

QueryCountFn is a function type that abstracts the counting of rows down to a single function that takes a Queryable interface and returns a QueryCountResult.

func BuildQueryCountFn

func BuildQueryCountFn(
	col mysql.Column,
	tbl mysql.ReadableTable,
	conds mysql.BoolExpression,
) QueryCountFn

BuildQueryCountFn builds a QueryCountFn that can be used to count rows in a table with the given conditions.

type QueryCountResult

type QueryCountResult struct {
	Total  int
	Before int
	After  int
}

QueryCountResult is a struct that contains the total number of rows that will be returned by a query, the number of rows after an end row, and the number of rows before a start row.

func QueryCount

QueryCount counts the number of rows returned from a table with the given conditions. Returns a QueryCountResult or an error if anything goes wrong.

type StringValue

type StringValue struct {
	Key ColumnKey `json:"key"`
	Val string    `json:"val"`
}

func NewStringValue

func NewStringValue(val string, col mysql.ColumnString) *StringValue

NewStringValue creates a cursor value for a string column (including UUIDs). The column must be registered via RegisterColumn before Col or decode use it.

func (StringValue) Col

func (s StringValue) Col() mysql.ColumnString

func (*StringValue) ColumnKey

func (s *StringValue) ColumnKey() ColumnKey

func (StringValue) Expr

func (*StringValue) IsEmpty

func (s *StringValue) IsEmpty() bool

func (StringValue) Value

func (s StringValue) Value() (driver.Value, error)

type TimestampValue

type TimestampValue struct {
	Key ColumnKey `json:"key"`
	Val time.Time `json:"val"`
}

TimestampValue stores a timestamp cursor position.

func NewTimestampValue

func NewTimestampValue(val time.Time, col mysql.ColumnTimestamp) *TimestampValue

NewTimestampValue creates a timestamp cursor value for the given column. The column must be registered via RegisterColumn before Col or decode use it.

func (TimestampValue) Col

func (*TimestampValue) ColumnKey

func (t *TimestampValue) ColumnKey() ColumnKey

func (TimestampValue) Expr

func (*TimestampValue) IsEmpty

func (t *TimestampValue) IsEmpty() bool

func (TimestampValue) Value

func (t TimestampValue) Value() (driver.Value, error)

type Uint64Value

type Uint64Value struct {
	Key ColumnKey `json:"key"`
	Val uint64    `json:"val"`
}

func NewUint64Value

func NewUint64Value(val uint64, col mysql.ColumnInteger) *Uint64Value

NewUint64Value creates a cursor value for an unsigned integer column. The column must be registered via RegisterColumn before Col or decode use it.

func (Uint64Value) Col

func (*Uint64Value) ColumnKey

func (i *Uint64Value) ColumnKey() ColumnKey

func (Uint64Value) Expr

func (*Uint64Value) IsEmpty

func (i *Uint64Value) IsEmpty() bool

func (Uint64Value) Value

func (i Uint64Value) Value() (driver.Value, error)

Jump to

Keyboard shortcuts

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