Skip to content

Commit

Permalink
fix(query): added LimitedResult/LimitedQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
cnlangzi committed Apr 19, 2024
1 parent 7457db4 commit 91ef419
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 70 deletions.
25 changes: 25 additions & 0 deletions limited_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package sqle

// LimitedQueryOption is a function type that modifies a LimitedQuery.
type LimitedQueryOption func(q *LimitedQuery)

// LimitedQuery represents a query with pagination and ordering options.
type LimitedQuery struct {
Offset int64 // PageIndex represents the index of the page to retrieve.
Limit int64 // PageSize represents the number of items per page.
OrderBy *OrderByBuilder // OrderBy represents the ordering of the query.
}

// WithPageSize is a LimitedQueryOption that sets the page size of the LimitedQuery.
func WithPageSize(size int64) LimitedQueryOption {
return func(q *LimitedQuery) {
q.Limit = size

Check warning on line 16 in limited_query.go

View check run for this annotation

Codecov / codecov/patch

limited_query.go#L14-L16

Added lines #L14 - L16 were not covered by tests
}
}

// WithOrderBy is a LimitedQueryOption that sets the ordering of the LimitedQuery.
func WithOrderBy(ob *OrderByBuilder) LimitedQueryOption {
return func(q *LimitedQuery) {
q.OrderBy = ob

Check warning on line 23 in limited_query.go

View check run for this annotation

Codecov / codecov/patch

limited_query.go#L21-L23

Added lines #L21 - L23 were not covered by tests
}
}
7 changes: 7 additions & 0 deletions limited_result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sqle

// LimitedResult represents a limited result set with items and total count.
type LimitedResult[T any] struct {
Items []T `json:"items,omitempty"` // Items contains the limited result items.
Total int64 `json:"total,omitempty"` // Total represents the total count.
}
37 changes: 27 additions & 10 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ func (e *Errors) Error() string {
}

type Query[T any] struct {
db *DB
queryer Queryer[T]
tables []string
db *DB
queryer Queryer[T]
withRotatedTables []string
}

// NewQuery create a Query
// NewQuery creates a new Query instance.
// It takes a *DB as the first argument and optional QueryOption functions as the rest.
// It returns a pointer to the created Query instance.
func NewQuery[T any](db *DB, options ...QueryOption[T]) *Query[T] {
q := &Query[T]{
db: db,
Expand All @@ -31,8 +33,8 @@ func NewQuery[T any](db *DB, options ...QueryOption[T]) *Query[T] {
}
}

if q.tables == nil {
q.tables = []string{""}
if q.withRotatedTables == nil {
q.withRotatedTables = []string{""}
}

if q.queryer == nil {
Expand All @@ -44,18 +46,33 @@ func NewQuery[T any](db *DB, options ...QueryOption[T]) *Query[T] {
return q
}

// First executes the query and returns the first result.
// It takes a context.Context and a *Builder as arguments.
// It returns the result of type T and an error, if any.
func (q *Query[T]) First(ctx context.Context, b *Builder) (T, error) {
return q.queryer.First(ctx, q.tables, b)
return q.queryer.First(ctx, q.withRotatedTables, b)
}

// Count executes the query and returns the number of results.
// It takes a context.Context and a *Builder as arguments.
// It returns the count as an integer and an error, if any.
func (q *Query[T]) Count(ctx context.Context, b *Builder) (int, error) {
return q.queryer.Count(ctx, q.tables, b)
return q.queryer.Count(ctx, q.withRotatedTables, b)
}

// Query executes the query and returns all the results.
// It takes a context.Context, a *Builder, and a comparison function as arguments.
// The comparison function is used to sort the results.
// It returns a slice of results of type T and an error, if any.
func (q *Query[T]) Query(ctx context.Context, b *Builder, less func(i, j T) bool) ([]T, error) {
return q.queryer.Query(ctx, q.tables, b, less)
return q.queryer.Query(ctx, q.withRotatedTables, b, less)
}

// QueryLimit executes the query and returns a limited number of results.
// It takes a context.Context, a *Builder, a comparison function, and a limit as arguments.
// The comparison function is used to sort the results.
// The limit specifies the maximum number of results to return.
// It returns a slice of results of type T and an error, if any.
func (q *Query[T]) QueryLimit(ctx context.Context, b *Builder, less func(i, j T) bool, limit int) ([]T, error) {
return q.queryer.QueryLimit(ctx, q.tables, b, less, limit)
return q.queryer.QueryLimit(ctx, q.withRotatedTables, b, less, limit)
}
6 changes: 3 additions & 3 deletions query_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ type QueryOption[T any] func(q *Query[T])
func WithMonths[T any](start, end time.Time) QueryOption[T] {
return func(q *Query[T]) {
for t := start; !t.After(end); t = t.AddDate(0, 1, 0) {
q.tables = append(q.tables, shardid.FormatMonth(t))
q.withRotatedTables = append(q.withRotatedTables, shardid.FormatMonth(t))
}
}
}

func WithWeeks[T any](start, end time.Time) QueryOption[T] {
return func(q *Query[T]) {
for t := start; !t.After(end); t = t.AddDate(0, 0, 7) {
q.tables = append(q.tables, shardid.FormatWeek(t))
q.withRotatedTables = append(q.withRotatedTables, shardid.FormatWeek(t))
}
}
}

func WithDays[T any](start, end time.Time) QueryOption[T] {
return func(q *Query[T]) {
for t := start; !t.After(end); t = t.AddDate(0, 0, 1) {
q.tables = append(q.tables, shardid.FormatDay(t))
q.withRotatedTables = append(q.withRotatedTables, shardid.FormatDay(t))
}
}
}
Expand Down
16 changes: 12 additions & 4 deletions queryer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ package sqle

import "context"

// Queryer is a query provider interface that defines methods for querying data.
type Queryer[T any] interface {
First(ctx context.Context, tables []string, b *Builder) (T, error)
Count(ctx context.Context, tables []string, b *Builder) (int, error)
Query(ctx context.Context, tables []string, b *Builder, less func(i, j T) bool) ([]T, error)
QueryLimit(ctx context.Context, tables []string, b *Builder, less func(i, j T) bool, limit int) ([]T, error)
// First retrieves the first result that matches the query criteria.
First(ctx context.Context, rotatedTables []string, b *Builder) (T, error)

// Count returns the number of results that match the query criteria.
Count(ctx context.Context, rotatedTables []string, b *Builder) (int, error)

// Query retrieves all results that match the query criteria and sorts them using less function if it is provided.
Query(ctx context.Context, rotatedTables []string, b *Builder, less func(i, j T) bool) ([]T, error)

// QueryLimit retrieves a limited number of results that match the query criteria, sorts them using the provided less function, and limits the number of results to the specified limit.
QueryLimit(ctx context.Context, rotatedTables []string, b *Builder, less func(i, j T) bool, limit int) ([]T, error)
}
25 changes: 16 additions & 9 deletions queryer_mapr.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import (
"github.com/yaitoo/async"
)

// MapR Map/Reduce Query
// MapR is a Map/Reduce Query Provider based on databases.
type MapR[T any] struct {
dbs []*Context
}

func (q *MapR[T]) First(ctx context.Context, tables []string, b *Builder) (T, error) {
// First executes the query and returns the first result.
func (q *MapR[T]) First(ctx context.Context, rotatedTables []string, b *Builder) (T, error) {
var it T
b.Input("rotate", "<rotate>") // lazy replace on async.Wait
query, args, err := b.Build()
Expand All @@ -24,7 +25,7 @@ func (q *MapR[T]) First(ctx context.Context, tables []string, b *Builder) (T, er

w := async.New[T]()

for _, r := range tables {
for _, r := range rotatedTables {
qr := strings.ReplaceAll(query, "<rotate>", r)
for _, db := range q.dbs {
w.Add(func(db *Context, qr string) func(context.Context) (T, error) {
Expand All @@ -44,7 +45,9 @@ func (q *MapR[T]) First(ctx context.Context, tables []string, b *Builder) (T, er
d, _, err := w.WaitAny(ctx)
return d, err
}
func (q *MapR[T]) Count(ctx context.Context, tables []string, b *Builder) (int, error) {

// Count executes the query and returns the count of results.
func (q *MapR[T]) Count(ctx context.Context, rotatedTables []string, b *Builder) (int, error) {
b.Input("rotate", "<rotate>") // lazy replace on async.Wait
query, args, err := b.Build()
if err != nil {
Expand All @@ -53,7 +56,7 @@ func (q *MapR[T]) Count(ctx context.Context, tables []string, b *Builder) (int,

w := async.New[int]()

for _, r := range tables {
for _, r := range rotatedTables {
qr := strings.ReplaceAll(query, "<rotate>", r)
for _, db := range q.dbs {
w.Add(func(db *Context, qr string) func(context.Context) (int, error) {
Expand Down Expand Up @@ -84,7 +87,9 @@ func (q *MapR[T]) Count(ctx context.Context, tables []string, b *Builder) (int,

return total, nil
}
func (q *MapR[T]) Query(ctx context.Context, tables []string, b *Builder, less func(i, j T) bool) ([]T, error) {

// Query executes the query and returns a list of results.
func (q *MapR[T]) Query(ctx context.Context, rotatedTables []string, b *Builder, less func(i, j T) bool) ([]T, error) {

b.Input("rotate", "<rotate>") // lazy replace on async.Wait
query, args, err := b.Build()
Expand All @@ -94,7 +99,7 @@ func (q *MapR[T]) Query(ctx context.Context, tables []string, b *Builder, less f

w := async.New[[]T]()

for _, r := range tables {
for _, r := range rotatedTables {
qr := strings.ReplaceAll(query, "<rotate>", r)
for _, db := range q.dbs {
w.Add(func(db *Context, qr string) func(context.Context) ([]T, error) {
Expand Down Expand Up @@ -138,13 +143,15 @@ func (q *MapR[T]) Query(ctx context.Context, tables []string, b *Builder, less f

return list, nil
}
func (q *MapR[T]) QueryLimit(ctx context.Context, tables []string, b *Builder, less func(i, j T) bool, limit int) ([]T, error) {

// QueryLimit executes the query and returns a limited list of results.
func (q *MapR[T]) QueryLimit(ctx context.Context, rotatedTables []string, b *Builder, less func(i, j T) bool, limit int) ([]T, error) {

if limit > 0 {
b.SQL(" LIMIT " + strconv.Itoa(limit*len(q.dbs)))
}

list, err := q.Query(ctx, tables, b, less)
list, err := q.Query(ctx, rotatedTables, b, less)
if err != nil {
return nil, err
}
Expand Down
35 changes: 2 additions & 33 deletions sqlbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,38 +146,6 @@ func (b *Builder) Build() (string, []any, error) {

}

// WithWhere adds the input and parameter values from the given WhereBuilder to the current Builder
// and sets the WHERE clause of the SQL statement to the string representation of the WhereBuilder's statement.
// It returns the modified WhereBuilder.
func (b *Builder) WithWhere(wb *WhereBuilder) *WhereBuilder {
for k, v := range wb.inputs {
b.Input(k, v)
}

for k, v := range wb.params {
b.Param(k, v)
}

return b.Where(strings.TrimSpace(wb.stmt.String()))
}

// Where starts a new WhereBuilder and adds the given conditions to the current query builder.
// Returns the new WhereBuilder.
func (b *Builder) Where(cmd ...string) *WhereBuilder {
wb := &WhereBuilder{Builder: b}

b.stmt.WriteString(" WHERE")
for _, it := range cmd {
if it != "" {
wb.written = true
b.stmt.WriteString(" ")
b.stmt.WriteString(it)
}
}

return wb
}

// quoteColumn escapes the given column name using the Builder's Quote character.
func (b *Builder) quoteColumn(c string) string {
if strings.ContainsAny(c, "(") || strings.ContainsAny(c, " ") || strings.ContainsAny(c, "as") {
Expand Down Expand Up @@ -243,7 +211,8 @@ func (b *Builder) On(id shardid.ID) *Builder {
return b.Input("rotate", id.RotateName())
}

// sortColumns sorts the columns in the given map and returns them as a slice.
// sortColumns sorts the columns in the given map and returns them as a pre-sorted columns slice.
// It helps PrepareStmt works with sql statement as less as possible.
// It also allows customization of column names using BuilderOptions.
func sortColumns(m map[string]any, opts ...BuilderOption) []string {
bo := &BuilderOptions{}
Expand Down
21 changes: 21 additions & 0 deletions sqlbuilder_orderby.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ type OrderByBuilder struct {
allowedColumns []string
}

// NewOrderBy creates a new instance of the OrderByBuilder.
// It takes a variadic parameter `allowedColumns` which specifies the columns that are allowed to be used in the ORDER BY clause.
func NewOrderBy(allowedColumns ...string) *OrderByBuilder {
return &OrderByBuilder{
Builder: New(),
allowedColumns: allowedColumns,
}
}

// WithOrderBy sets the order by clause for the SQL query.
// It takes an instance of the OrderByBuilder and adds the allowed columns to the Builder's order list.
// It also appends the SQL string representation of the OrderByBuilder to the Builder's SQL string.
// It returns a new instance of the OrderByBuilder.
func (b *Builder) WithOrderBy(ob *OrderByBuilder) *OrderByBuilder {
n := b.Order(ob.allowedColumns...)

b.SQL(ob.String())

return n
}

// Order create an OrderByBuilder with allowed columns to prevent sql injection. NB: any input is allowed if it is not provided
func (b *Builder) Order(allowedColumns ...string) *OrderByBuilder {
ob := &OrderByBuilder{
Expand Down
14 changes: 14 additions & 0 deletions sqlbuilder_orderby_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ func TestOrderByBuilder(t *testing.T) {
},
wanted: "SELECT * FROM users ORDER BY created_at DESC, id ASC, updated_at ASC",
},
{
name: "with_order_should_work",
build: func() *Builder {
b := New("SELECT * FROM users")

ob := NewOrderBy("id", "created_at", "updated_at", "age")
ob.By("created_at desc, id, name asc, updated_at asc, age invalid_by, unsafe_asc, unsafe_desc desc")

b.WithOrderBy(ob)

return b
},
wanted: "SELECT * FROM users ORDER BY created_at DESC, id ASC, updated_at ASC",
},
}

for _, test := range tests {
Expand Down
Loading

0 comments on commit 91ef419

Please sign in to comment.