package page import ( "context" "fmt" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/clause" "github.com/stephenafamo/bob/expr" "github.com/stephenafamo/scan" ) // Mod applies validated keyset WHERE, ORDER BY and integer LIMIT size+1 to native // PostgreSQL, SQLite or MySQL selects. Apply once to a query with no existing // ORDER BY, LIMIT, OFFSET or FETCH; do not override pagination afterward. // Callers may compose additional WHERE filters before or after this mod. func Mod[Q interface { AppendWhere(...any) AppendOrder(bob.Expression) SetLimit(any) }, T any](p *Prepared[T]) bob.Mod[Q] { return bob.ModFunc[Q](func(q Q) { var alternatives []bob.Expression for i, c := range p.sort.Columns { desc := c.Direction == Desc if p.direction == Backward { desc = !desc } direction, operator := "ASC", ">" if desc { direction, operator = "DESC", "<" } q.AppendOrder(clause.OrderDef{Expression: c.Expression, Direction: direction}) if len(p.anchor) == 0 { continue } terms := make([]bob.Expression, 0, i+1) for j := 0; j < i; j++ { terms = append(terms, expr.OP("=", p.sort.Columns[j].Expression, expr.Arg(p.anchor[j]))) } terms = append(terms, expr.OP(operator, c.Expression, expr.Arg(p.anchor[i]))) alternatives = append(alternatives, grouped(terms, " AND ")) } // authz: group the seek disjunction so caller filters constrain every branch. if len(alternatives) > 0 { q.AppendWhere(grouped(alternatives, " OR ")) } q.SetLimit(p.size + 1) }) } func grouped(expressions []bob.Expression, separator string) bob.Expression { return expr.Join{Exprs: []bob.Expression{ expr.Raw("("), expr.Join{Exprs: expressions, Sep: separator}, expr.Raw(")"), }, Sep: expr.NoSep} } // Count executes a separate caller-built, authorized count query returning one // int64 column. It never rewrites a paginated query or infers grouped/distinct // semantics. Nothing runs unless called. func Count(ctx context.Context, executor bob.Executor, query bob.Query) (int64, error) { // authz: callers supply the same authorized relation as their item query. count, err := bob.One(ctx, executor, query, scan.SingleColumnMapper[int64]) if err != nil { return 0, fmt.Errorf("page: count: %w", err) } return count, nil }