import files, add README & LICENSE
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"gitea.auvem.com/go-toolkit/dbx"
|
||||
"github.com/go-jet/jet/v2/mysql"
|
||||
)
|
||||
|
||||
// QueryCountResult is a struct that contains the total number of rows that will
|
||||
// be returned by a query, the number of rows after an end row, and the number of
|
||||
// rows before a start row.
|
||||
type QueryCountResult struct {
|
||||
Total int
|
||||
Before int
|
||||
After int
|
||||
}
|
||||
|
||||
// QueryCountFn is a function type that abstracts the counting of rows down to
|
||||
// a single function that takes a Queryable interface and returns a QueryCountResult.
|
||||
type QueryCountFn = func(
|
||||
sqlo dbx.Queryable,
|
||||
start GenericCursor,
|
||||
end GenericCursor,
|
||||
) (QueryCountResult, error)
|
||||
|
||||
// QueryCount counts the number of rows returned from a table with the given
|
||||
// conditions. Returns a QueryCountResult or an error if anything goes wrong.
|
||||
func QueryCount(
|
||||
sqlo dbx.Queryable,
|
||||
col mysql.Column,
|
||||
tbl mysql.ReadableTable,
|
||||
conds mysql.BoolExpression,
|
||||
start GenericCursor,
|
||||
end GenericCursor,
|
||||
) (QueryCountResult, error) {
|
||||
var comparator struct {
|
||||
start string
|
||||
end string
|
||||
}
|
||||
|
||||
if start.Direction() == OrderAscending {
|
||||
comparator.start = "<"
|
||||
comparator.end = ">"
|
||||
} else {
|
||||
comparator.start = ">"
|
||||
comparator.end = "<"
|
||||
}
|
||||
|
||||
stmt := tbl.SELECT(
|
||||
mysql.COUNT(col).AS("QueryCountResult.Total"),
|
||||
mysql.COUNT(countBoundExpr(start, comparator.start)).AS("QueryCountResult.Before"),
|
||||
mysql.COUNT(countBoundExpr(end, comparator.end)).AS("QueryCountResult.After"),
|
||||
).WHERE(conds)
|
||||
var res QueryCountResult
|
||||
err := stmt.Query(sqlo, &res)
|
||||
if err != nil {
|
||||
return QueryCountResult{}, fmt.Errorf("failed to query count: %w", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func countBoundExpr(c GenericCursor, comparator string) mysql.Expression {
|
||||
if c.IsComposite() {
|
||||
return mysql.Raw(
|
||||
fmt.Sprintf("IF(%s, 1, NULL)", compositeTupleSQL(comparator, c)),
|
||||
compositeTupleArgs(c),
|
||||
)
|
||||
}
|
||||
|
||||
ck := c.GenericIndex().ColumnKey()
|
||||
return mysql.Raw(
|
||||
fmt.Sprintf("IF(%s.%s %s ?, 1, NULL)", ck.Table, ck.Column, comparator),
|
||||
mysql.RawArgs{"?": c.GenericIndex()},
|
||||
)
|
||||
}
|
||||
|
||||
func compositeTupleSQL(comparator string, c GenericCursor) string {
|
||||
orderCK := NewColumnKey(c.OrderCol())
|
||||
indexCK := c.GenericIndex().ColumnKey()
|
||||
return fmt.Sprintf(
|
||||
"(%s.%s, %s.%s) %s (#order, #index)",
|
||||
orderCK.Table, orderCK.Column,
|
||||
indexCK.Table, indexCK.Column,
|
||||
comparator,
|
||||
)
|
||||
}
|
||||
|
||||
func compositeTupleArgs(c GenericCursor) mysql.RawArgs {
|
||||
return mysql.RawArgs{
|
||||
"#order": c.GenericOrderValue(),
|
||||
"#index": c.GenericIndex(),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildQueryCountFn builds a QueryCountFn that can be used to count rows in a
|
||||
// table with the given conditions.
|
||||
func BuildQueryCountFn(
|
||||
col mysql.Column,
|
||||
tbl mysql.ReadableTable,
|
||||
conds mysql.BoolExpression,
|
||||
) QueryCountFn {
|
||||
return func(sqlo dbx.Queryable, start GenericCursor, end GenericCursor) (QueryCountResult, error) {
|
||||
return QueryCount(sqlo, col, tbl, conds, start, end)
|
||||
}
|
||||
}
|
||||
|
||||
// PaginateConds returns a mysql.BoolExpression that paginates results using the
|
||||
// provided cursor as a base position.
|
||||
func PaginateConds[IE mysql.Expression, IC mysql.Column](c *Cursor[IE, IC]) mysql.BoolExpression {
|
||||
return paginateFromGeneric(c)
|
||||
}
|
||||
|
||||
// PaginateIntConds returns a mysql.BoolExpression that paginates results using
|
||||
// the provided integer cursor as a base position.
|
||||
func PaginateIntConds[IE mysql.IntegerExpression, IC mysql.ColumnInteger](c *Cursor[IE, IC]) mysql.BoolExpression {
|
||||
return paginateFromGeneric(c)
|
||||
}
|
||||
|
||||
// PaginateStringConds returns a mysql.BoolExpression that paginates results using
|
||||
// the provided string cursor as a base position.
|
||||
func PaginateStringConds[IE mysql.StringExpression, IC mysql.ColumnString](c *Cursor[IE, IC]) mysql.BoolExpression {
|
||||
return paginateFromGeneric(c)
|
||||
}
|
||||
|
||||
// PaginateUUIDConds returns a mysql.BoolExpression that paginates results using
|
||||
// the provided UUID cursor as a base position.
|
||||
func PaginateUUIDConds[IE mysql.StringExpression, IC mysql.ColumnString](c *Cursor[IE, IC]) mysql.BoolExpression {
|
||||
return paginateFromGeneric(c)
|
||||
}
|
||||
|
||||
// PaginateTimestampConds returns a mysql.BoolExpression that paginates results
|
||||
// using the provided timestamp cursor as a base position.
|
||||
func PaginateTimestampConds[IE mysql.TimestampExpression, IC mysql.ColumnTimestamp](c *Cursor[IE, IC]) mysql.BoolExpression {
|
||||
return paginateFromGeneric(c)
|
||||
}
|
||||
|
||||
func paginateFromGeneric(c GenericCursor) mysql.BoolExpression {
|
||||
if c == nil || c.IsEmpty() {
|
||||
return mysql.Bool(true)
|
||||
}
|
||||
|
||||
if c.IsComposite() {
|
||||
return paginateComposite(c)
|
||||
}
|
||||
|
||||
switch idx := c.GenericIndex().(type) {
|
||||
case *Int64Value:
|
||||
if c.Direction() == OrderAscending {
|
||||
return idx.Col().GT(idx.Expr())
|
||||
}
|
||||
return idx.Col().LT(idx.Expr())
|
||||
case *Uint64Value:
|
||||
if c.Direction() == OrderAscending {
|
||||
return idx.Col().GT(idx.Expr())
|
||||
}
|
||||
return idx.Col().LT(idx.Expr())
|
||||
case *StringValue:
|
||||
if c.Direction() == OrderAscending {
|
||||
return idx.Col().GT(idx.Expr())
|
||||
}
|
||||
return idx.Col().LT(idx.Expr())
|
||||
case *TimestampValue:
|
||||
if c.Direction() == OrderAscending {
|
||||
return idx.Col().GT(idx.Expr())
|
||||
}
|
||||
return idx.Col().LT(idx.Expr())
|
||||
default:
|
||||
// fallback: emit a raw comparison using the column key and the value
|
||||
ck := c.GenericIndex().ColumnKey()
|
||||
if ck.IsEmpty() {
|
||||
return mysql.Bool(true)
|
||||
}
|
||||
slog.Warn(
|
||||
"Cursor pagination conditions generated via raw fallback",
|
||||
"table", ck.Table, "col", ck.Column,
|
||||
)
|
||||
if c.Direction() == OrderAscending {
|
||||
return mysql.RawBool(
|
||||
fmt.Sprintf("%s.%s > ?", ck.Table, ck.Column),
|
||||
mysql.RawArgs{"?": c.GenericIndex()},
|
||||
)
|
||||
}
|
||||
return mysql.RawBool(
|
||||
fmt.Sprintf("%s.%s < ?", ck.Table, ck.Column),
|
||||
mysql.RawArgs{"?": c.GenericIndex()},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func paginateComposite(c GenericCursor) mysql.BoolExpression {
|
||||
comparator := "<"
|
||||
if c.Direction() == OrderAscending {
|
||||
comparator = ">"
|
||||
}
|
||||
|
||||
return mysql.RawBool(
|
||||
compositeTupleSQL(comparator, c),
|
||||
compositeTupleArgs(c),
|
||||
)
|
||||
}
|
||||
|
||||
// OrderByClauses returns ORDER BY clauses matching the cursor pagination semantics.
|
||||
func OrderByClauses(c GenericCursor) []mysql.OrderByClause {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
orderCol := c.OrderCol()
|
||||
if c.UsesTupleOrdering() {
|
||||
indexCol, err := GetColumnByKey(c.GenericIndex().ColumnKey())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
indexOrderCol, ok := indexCol.(CursorOrderCol)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("index column %s is not orderable", c.GenericIndex().ColumnKey()))
|
||||
}
|
||||
if c.Direction() == OrderAscending {
|
||||
return []mysql.OrderByClause{orderCol.ASC(), indexOrderCol.ASC()}
|
||||
}
|
||||
return []mysql.OrderByClause{orderCol.DESC(), indexOrderCol.DESC()}
|
||||
}
|
||||
|
||||
if c.Direction() == OrderAscending {
|
||||
return []mysql.OrderByClause{orderCol.ASC()}
|
||||
}
|
||||
return []mysql.OrderByClause{orderCol.DESC()}
|
||||
}
|
||||
Reference in New Issue
Block a user