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 ¶
- github.com/go-jet/jet/v2/mysql generated table and column types
- gitea.auvem.com/go-toolkit/dbx.Queryable for running queries (optional when building SQL manually)
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 ¶
- Variables
- func DereferenceSlice[T any](list []*T) []T
- func ExtractNodes[T any](conn *Connection[T]) []*T
- func GetColumn(table, column string) (mysql.Column, error)
- func GetColumnByKey(key ColumnKey) (mysql.Column, error)
- func OrderByClauses(c GenericCursor) []mysql.OrderByClause
- func PaginateConds[IE mysql.Expression, IC mysql.Column](c *Cursor[IE, IC]) mysql.BoolExpression
- func RegisterColumn(columns ...mysql.Column)
- func RegisterColumnList(columns mysql.ColumnList)
- type ColumnKey
- type Connection
- type Cursor
- func NewCursor[IE mysql.Expression, IC mysql.Column](index ExprMarshaler[IE, IC], orderCol CursorOrderCol, orderDir OrderDirection) *Cursor[IE, IC]
- func NewCursorFromAfterPtr[IE mysql.Expression, IC mysql.Column](newZero func() *Cursor[IE, IC], after *string) (*Cursor[IE, IC], error)
- func NewCursorFromJSON[IE mysql.Expression, IC mysql.Column](zeroIndex ExprMarshaler[IE, IC], src []byte) (*Cursor[IE, IC], error)
- func (c *Cursor[IE, IC]) CopyWithVal(val ExprMarshaler[IE, IC]) *Cursor[IE, IC]
- func (c *Cursor[IE, IC]) CopyWithVals(index ExprMarshaler[IE, IC], orderVal GenericExpr) *Cursor[IE, IC]
- func (c *Cursor[IE, IC]) Decode(src string) error
- func (c *Cursor[IE, IC]) DecodeAndOrder(src string, orderDir OrderDirection) error
- func (c *Cursor[IE, IC]) Direction() OrderDirection
- func (c *Cursor[IE, IC]) Encode() (string, error)
- func (c *Cursor[IE, IC]) GenericIndex() GenericExpr
- func (c *Cursor[IE, IC]) GenericOrderValue() GenericExpr
- func (c *Cursor[IE, IC]) IsComposite() bool
- func (c *Cursor[IE, IC]) IsEmpty() bool
- func (c *Cursor[IE, IC]) OrderCol() CursorOrderCol
- func (c *Cursor[IE, IC]) String() string
- func (c *Cursor[IE, IC]) UsesTupleOrdering() bool
- type CursorFunc
- type CursorOrderCol
- type Edge
- type ExprMarshaler
- type GenericCursor
- type GenericExpr
- type Int64Value
- type OrderDirection
- type PageInfo
- type PageQuery
- type QueryCountFn
- type QueryCountResult
- type StringValue
- type TimestampValue
- type Uint64Value
Constants ¶
This section is empty.
Variables ¶
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 GetColumnByKey ¶
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 ¶
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 ¶
ColumnKey identifies a column and table in the database with a given value.
func NewColumnKey ¶
NewColumnKey creates a new ColumnKey with the specified table and column names.
type Connection ¶
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 ¶
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]) 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 ¶
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 ¶
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]) UsesTupleOrdering ¶
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 ¶
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 ¶
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 ¶
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 (i Int64Value) Expr() mysql.IntegerExpression
func (*Int64Value) IsEmpty ¶
func (i *Int64Value) IsEmpty() bool
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 ¶
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 ¶
func QueryCount( sqlo dbx.Queryable, col mysql.Column, tbl mysql.ReadableTable, conds mysql.BoolExpression, start GenericCursor, end GenericCursor, ) (QueryCountResult, error)
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 ¶
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 (s StringValue) Expr() mysql.StringExpression
func (*StringValue) IsEmpty ¶
func (s *StringValue) IsEmpty() bool
type TimestampValue ¶
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 (t TimestampValue) Col() mysql.ColumnTimestamp
func (*TimestampValue) ColumnKey ¶
func (t *TimestampValue) ColumnKey() ColumnKey
func (TimestampValue) Expr ¶
func (t TimestampValue) Expr() mysql.TimestampExpression
func (*TimestampValue) IsEmpty ¶
func (t *TimestampValue) IsEmpty() bool
type Uint64Value ¶
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 (i Uint64Value) Col() mysql.ColumnInteger
func (*Uint64Value) ColumnKey ¶
func (i *Uint64Value) ColumnKey() ColumnKey
func (Uint64Value) Expr ¶
func (i Uint64Value) Expr() mysql.IntegerExpression
func (*Uint64Value) IsEmpty ¶
func (i *Uint64Value) IsEmpty() bool