snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
197
oryx/pagination/paginationplanner/planner.go
Normal file
197
oryx/pagination/paginationplanner/planner.go
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package paginationplanner
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
keysetpagination "github.com/ory/x/pagination/keysetpagination_v2"
|
||||
)
|
||||
|
||||
// PaginationPlanner chooses the best [PaginationPlan] for a queriedColumns.
|
||||
// Plan is eligible when its required constraint set is satisfied (and an optional Condition matches).
|
||||
// If no plan matches, the FallbackPlan is used.
|
||||
type PaginationPlanner struct {
|
||||
Plans []PaginationPlan
|
||||
FallbackPlan PaginationPlan
|
||||
}
|
||||
|
||||
func NewPaginationPlanner(fallbackPlan PaginationPlan, plans []PaginationPlan) (*PaginationPlanner, error) {
|
||||
if len(fallbackPlan.DefaultPageToken.Columns()) == 0 {
|
||||
return nil, errors.New("plan must define at least one PageTokenColumn")
|
||||
}
|
||||
|
||||
for i := range plans {
|
||||
plan := &plans[i]
|
||||
if len(plan.ApplicableQueries) == 0 {
|
||||
return nil, errors.New("plan must define at least one ApplicableQueries")
|
||||
}
|
||||
if len(plan.DefaultPageToken.Columns()) == 0 {
|
||||
return nil, errors.New("plan must define a DefaultPageToken")
|
||||
}
|
||||
plan.populateInternals()
|
||||
}
|
||||
|
||||
return &PaginationPlanner{
|
||||
Plans: plans,
|
||||
FallbackPlan: fallbackPlan,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPaginator selects the first eligible plan for the given queriedColumns constraints.
|
||||
// Eligibility requires an exact ColumnSet match and (if set) Condition match.
|
||||
// If none match, the FallbackPlan is used for building the Paginator.
|
||||
func (p *PaginationPlanner) GetPaginator(q Query, pageOpts ...keysetpagination.Option) (*keysetpagination.Paginator, error) {
|
||||
if len(q) == 0 {
|
||||
return keysetpagination.NewPaginator(append(pageOpts, keysetpagination.WithDefaultToken(p.FallbackPlan.DefaultPageToken))...)
|
||||
}
|
||||
|
||||
plan := p.pickPlan(q)
|
||||
|
||||
origCols := plan.DefaultPageToken.Columns()
|
||||
|
||||
// Make a fresh copy so we don't mutate the original backing array.
|
||||
cols := make([]keysetpagination.Column, len(origCols))
|
||||
copy(cols, origCols)
|
||||
|
||||
for i, col := range cols {
|
||||
if val, ok := q.colByName(col.Name); ok && val.IsConstrained() {
|
||||
cols[i].HasConstraint = true
|
||||
}
|
||||
}
|
||||
|
||||
defaultToken := keysetpagination.WithDefaultToken(keysetpagination.NewPageToken(cols...))
|
||||
return keysetpagination.NewPaginator(append(pageOpts, defaultToken)...)
|
||||
}
|
||||
|
||||
func (p *PaginationPlanner) pickPlan(q Query) PaginationPlan {
|
||||
constrainedCols := q.constrainedCols()
|
||||
|
||||
for _, plan := range p.Plans {
|
||||
if _, ok := plan.applicableQueries[constrainedCols]; !ok {
|
||||
continue
|
||||
}
|
||||
if plan.Condition != nil && !plan.Condition(q) {
|
||||
continue
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
return p.FallbackPlan
|
||||
}
|
||||
|
||||
type PaginationPlan struct {
|
||||
Name string
|
||||
|
||||
// DefaultPageToken defines the columns that the pagination will be made of
|
||||
DefaultPageToken keysetpagination.PageToken
|
||||
|
||||
// ApplicableQueries defines the exact sets of columns that this pagination plan is applicable for.
|
||||
ApplicableQueries [][]Column
|
||||
applicableQueries map[uint]struct{}
|
||||
|
||||
// Condition further restricts the Plan eligibility for given queriedColumns.
|
||||
// This can be used for plan-specific logic, e.g. verifying partial-index predicates match.
|
||||
Condition func(q Query) bool
|
||||
}
|
||||
|
||||
func (pp *PaginationPlan) populateInternals() {
|
||||
pp.applicableQueries = make(map[uint]struct{}, len(pp.ApplicableQueries))
|
||||
for _, cols := range pp.ApplicableQueries {
|
||||
var colSet uint
|
||||
for _, col := range cols {
|
||||
colSet |= col.bit
|
||||
}
|
||||
pp.applicableQueries[colSet] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
nextBit uint
|
||||
usedNames map[string]struct{}
|
||||
}
|
||||
|
||||
func NewTable() *Table {
|
||||
return &Table{
|
||||
nextBit: 1,
|
||||
usedNames: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) NewColumn(name string) Column {
|
||||
defer func() { t.nextBit = t.nextBit << 1 }()
|
||||
if _, exists := t.usedNames[name]; exists {
|
||||
panic("column name already used: " + name)
|
||||
}
|
||||
t.usedNames[name] = struct{}{}
|
||||
return Column{
|
||||
name: name,
|
||||
bit: t.nextBit,
|
||||
}
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
bit uint
|
||||
name string
|
||||
}
|
||||
|
||||
func (c Column) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
type ColumnConstraint uint8
|
||||
|
||||
const (
|
||||
colUnconstrained ColumnConstraint = iota
|
||||
colConstraintEq // col = ?
|
||||
colConstraintIsNull // col IS NULL
|
||||
)
|
||||
|
||||
func (cs ColumnConstraint) IsConstrained() bool {
|
||||
return cs == colConstraintEq || cs == colConstraintIsNull
|
||||
}
|
||||
|
||||
func (cs ColumnConstraint) IsNull() bool {
|
||||
return cs == colConstraintIsNull
|
||||
}
|
||||
|
||||
// Query is used to track which column constraints are set.
|
||||
type Query map[Column]ColumnConstraint
|
||||
|
||||
func NewQuery() Query { return make(Query) }
|
||||
|
||||
func (qc Query) SetEq(cols ...Column) Query {
|
||||
return qc.set(colConstraintEq, cols...)
|
||||
}
|
||||
|
||||
func (qc Query) SetIsNull(cols ...Column) Query {
|
||||
return qc.set(colConstraintIsNull, cols...)
|
||||
}
|
||||
|
||||
func (qc Query) set(c ColumnConstraint, cols ...Column) Query {
|
||||
for _, col := range cols {
|
||||
qc[col] = c
|
||||
}
|
||||
return qc
|
||||
}
|
||||
|
||||
func (qc Query) colByName(name string) (ColumnConstraint, bool) {
|
||||
for col, constraint := range qc {
|
||||
if col.name == name {
|
||||
return constraint, true
|
||||
}
|
||||
}
|
||||
return colUnconstrained, false
|
||||
}
|
||||
|
||||
func (qc Query) constrainedCols() uint {
|
||||
var cols uint
|
||||
for col, state := range qc {
|
||||
if !state.IsConstrained() {
|
||||
continue
|
||||
}
|
||||
cols |= col.bit
|
||||
}
|
||||
return cols
|
||||
}
|
||||
Reference in New Issue
Block a user