feat(page): add typed keyset pagination for Bob (#15)
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
This commit was merged in pull request #15.
This commit is contained in:
1 parent
e5298cff37
commit
c45e8abe08
11 files changed
+1225
-1
No files matched your search
@@ -0,0 +1,69 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user