Add transport-neutral keyset pagination through native Bob query mods. Preserve supported cursor value types in a compact versioned format, with bidirectional pages, explicit row keys, and a separate opt-in count. Include SQLite tests, PostgreSQL service CI, and the cursor-format ADR. Closes #6
70 lines
2.3 KiB
Go
70 lines
2.3 KiB
Go
package page_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/stephenafamo/bob"
|
|
"github.com/stephenafamo/bob/dialect/sqlite"
|
|
sd "github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
|
"github.com/stephenafamo/bob/dialect/sqlite/sm"
|
|
"github.com/stephenafamo/bob/expr"
|
|
"github.com/stephenafamo/scan"
|
|
"go.auvem.com/corestack/page"
|
|
)
|
|
|
|
func Example() {
|
|
ctx := context.Background()
|
|
db, err := bob.Open("sqlite3", ":memory:")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer db.Close()
|
|
db.SetMaxOpenConns(1)
|
|
if _, err := db.ExecContext(ctx, "CREATE TABLE orders (id BIGINT PRIMARY KEY, priority BIGINT NOT NULL, tenant BIGINT NOT NULL)"); err != nil {
|
|
panic(err)
|
|
}
|
|
if _, err := db.ExecContext(ctx, "INSERT INTO orders VALUES (1,10,7),(2,10,7),(3,5,7),(4,20,8)"); err != nil {
|
|
panic(err)
|
|
}
|
|
type Order struct {
|
|
ID int64
|
|
Priority int64
|
|
}
|
|
sort := page.Sort{ID: "orders/priority/v1", Columns: []page.Column{
|
|
{Expression: expr.Quote("priority"), Direction: page.Desc},
|
|
{Expression: expr.Quote("id"), Direction: page.Asc, Unique: true},
|
|
}}
|
|
// With Bob-generated models, use their column references for these expressions.
|
|
// The key must independently match their order and actual stored value types.
|
|
key := func(o Order) []any { return []any{o.Priority, o.ID} }
|
|
p, err := page.Prepare(sort, key, page.Request{Size: 2}, page.Limits{Default: 2, Maximum: 100})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
q := sqlite.Select(sm.Columns("id", "priority"), sm.From("orders"), page.Mod[*sd.SelectQuery](p))
|
|
q.Apply(sm.Where(sqlite.Quote("tenant").EQ(sqlite.Arg(int64(7)))))
|
|
rows, err := bob.All(ctx, db, q, scan.StructMapper[Order]())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
result, err := p.Page(rows)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
fmt.Println(result.Items, result.HasNext, result.HasPrevious)
|
|
// Continue with Forward + result.EndCursor, or Backward + result.StartCursor.
|
|
// A mismatched key returns no partial Page, including for an overflow row.
|
|
bad, err := page.Prepare(sort, func(o Order) []any { return []any{o.ID} }, page.Request{}, page.Limits{Default: 2, Maximum: 100})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
_, err = bad.Page(rows)
|
|
var keyError *page.KeyError
|
|
fmt.Println(errors.As(err, &keyError), keyError.Expected, keyError.Actual)
|
|
// Output:
|
|
// [{1 10} {2 10}] true false
|
|
// true 2 1
|
|
}
|