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 Relaycompatible 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

Utilities

  • ExtractNodes — nodes from a Connection
  • DereferenceSlice[]*T[]T, skipping nils
S
Description
Relay-style cursor pagination for go-jet MySQL queries.
Readme BSD-3-Clause 64 KiB
Languages
Go 100%