4.2 KiB
4.2 KiB
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 1.25+
- go-jet/v2/mysql — generated table/column types
- go-toolkit/dbx —
Queryablefor running queries
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 aConnectionDereferenceSlice—[]*T→[]T, skipping nils