Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/v2'
Browse files Browse the repository at this point in the history
  • Loading branch information
soranoba committed Oct 31, 2020
2 parents f7c0817 + 1007845 commit 1b48faa
Show file tree
Hide file tree
Showing 20 changed files with 1,932 additions and 1,237 deletions.
32 changes: 31 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ default: &default
MYSQL_USER: pageboy
MYSQL_PASSWORD: pageboy
MYSQL_ROOT_PASSWORD: pageboy
- image: postgres:13.0
<<: *dockerhub_auth
environment:
POSTGRES_DB: pageboy
POSTGRES_USER: pageboy
POSTGRES_PASSWORD: pageboy
- image: mcmoe/mssqldocker:v2019.CU4.0
<<: *dockerhub_auth
environment:
ACCEPT_EULA: Y
SA_PASSWORD: hXUeLZvM4p3r2XeBG

jobs:
build:
Expand All @@ -42,6 +53,26 @@ jobs:
sleep 1
done
echo Failed waiting for MySQL && exit 1
- run:
name: Waiting for PostgresSQL to be ready
command: |
for i in `seq 1 10`;
do
nc -z localhost 5432 && echo Success && exit 0
echo -n .
sleep 1
done
echo Failed waiting for PostgresSQL && exit 1
- run:
name: Waiting for SQLServer to be ready
command: |
for i in `seq 1 10`;
do
nc -z localhost 1433 && echo Success && exit 0
echo -n .
sleep 1
done
echo Failed waiting for SQLServer && exit 1
- run: make test

workflows:
Expand All @@ -54,4 +85,3 @@ workflows:
context: org-global
- test:
context: org-global

8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ build:
go build

test:
DB=sqlite go test ./... -count=1
DB=mysql go test ./... -count=1
go test ./core/... -count=1
cd tests; \
DB=sqlite go test ./... -count=1 && \
DB=mysql go test ./... -count=1 && \
DB=postgres go test ./... -count=1 && \
DB=sqlserver go test ./... -count=1

format:
gofmt -w ./
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ pageboy
- Like them: `?page=1&per_page=2` and `?before=1585706584&limit=10`
- We can also customize it if needed.
- 💖 We can write smart code using GORM scopes.
- 👌 Supports all DB engine officialy supported by GORM.
- MySQL, PostgreSQL, SQLite, SQL Server

## Installation

To install it, run:

```bash
go get -u github.com/soranoba/pageboy
go get -u github.com/soranoba/pageboy/v2
```

## Usage
Expand Down Expand Up @@ -47,7 +49,7 @@ You should create an index when using a Cursor.<br>
Example using CreatedAt and ID for sorting:

```sql
CREATE INDEX created_at_id ON `users` (`created_at` DESC, `id` DESC);
CREATE INDEX created_at_id ON users (created_at DESC, id DESC);
```

#### Usage in Codes
Expand Down Expand Up @@ -76,6 +78,17 @@ func getUsers(ctx echo.Context) error {
}
```

#### NULLS FIRST / NULLS LAST

PostgresSQL can accept NULLS FIRST or NULLS LAST for index.<br>
In that case, it can use the index by adding NULLS FIRST or NULLS LAST in Order.

It is not supported other engines because they cannot accept these for index.

```go
cursor.Paginate("CreatedAt", "UpdatedAt").Order("DESC NULLS LAST", "ASC NULLS FIRST").Scope()
```

### Pager

Pager can be used to indicate a range that is specified a page size and a page number.
Expand Down
118 changes: 63 additions & 55 deletions core/comparison.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,71 +18,79 @@ const (
LessThan Comparison = "<"
)

// MakeComparisonScopeBuildFunc returns a GORM scope builder.
// MakeComparisonScope returns a GORM scope builder.
// This scope add a where clauses filtered by comparisons ranges.
func MakeComparisonScopeBuildFunc(columns ...string) func(comparisons ...Comparison) func(values ...interface{}) func(*gorm.DB) *gorm.DB {
return func(comparisons ...Comparison) func(values ...interface{}) func(*gorm.DB) *gorm.DB {
if len(columns) != len(comparisons) {
panic("columns and comparisons must have the same length")
}
func MakeComparisonScope(columns []string, comparisons []Comparison, nullsOrders []NullsOrder, values []interface{}) func(*gorm.DB) *gorm.DB {
if len(columns) != len(comparisons) {
panic("columns and comparisons must have the same length")
}
if len(columns) != len(nullsOrders) {
panic("columns and nullsOrders must have the same length")
}

return func(values ...interface{}) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
queryValues := make([]interface{}, 0)
nonNilValues := make([]interface{}, 0)
queries := make([]string, 0)
var eqQuery string
return func(db *gorm.DB) *gorm.DB {
queryValues := make([]interface{}, 0)
nonNilValues := make([]interface{}, 0)
queries := make([]string, 0)
var eqQuery string

var length = (func() int {
if len(values) > len(columns) {
return len(columns)
}
return len(values)
})()
length := (func() int {
if len(values) > len(columns) {
return len(columns)
}
return len(values)
})()

comparisons = comparisons[0:length]
for i := len(comparisons); i < length; i++ {
if i == 0 {
comparisons = append(comparisons, GreaterThan)
} else {
comparisons = append(comparisons, comparisons[i-1])
}
}
comparisons = comparisons[0:length]
for i := len(comparisons); i < length; i++ {
if i == 0 {
comparisons = append(comparisons, GreaterThan)
} else {
comparisons = append(comparisons, comparisons[i-1])
}
}

isPostgres := (db.Dialector.Name() == "postgres")

Loop:
for i, column := range columns[:length] {
column = toSnake(column)
Loop:
for i, column := range columns[:length] {
column = toSnake(column)

val := reflect.ValueOf(values[i])
isNil := val.Kind() == reflect.Ptr && val.IsNil()
nullsOrder := nullsOrders[i]
if nullsOrder == TreatsAsEngineDefault {
if isPostgres {
nullsOrder = TreatsAsHighest
} else {
nullsOrder = TreatsAsLowest
}
}

switch comparisons[i] {
case LessThan:
if isNil {
eqQuery += fmt.Sprintf("`%s` IS NULL AND ", column)
continue Loop
} else {
query := fmt.Sprintf("(%s(`%s` IS NULL OR `%s` %s ?))", eqQuery, column, column, comparisons[i])
queries = append(queries, query)
}
case GreaterThan:
if isNil {
eqQuery += fmt.Sprintf("`%s` IS NOT NULL OR ", column)
continue Loop
} else {
query := fmt.Sprintf("(%s`%s` %s ?)", eqQuery, column, comparisons[i])
queries = append(queries, query)
}
default:
panic("Unsupported compareStr")
}
val := reflect.ValueOf(values[i])
isNil := val.Kind() == reflect.Ptr && val.IsNil()

eqQuery += fmt.Sprintf("`%s` = ? AND ", column)
nonNilValues = append(nonNilValues, values[i])
queryValues = append(queryValues, nonNilValues...)
if (comparisons[i] == LessThan && nullsOrder == TreatsAsLowest) ||
(comparisons[i] == GreaterThan && nullsOrder == TreatsAsHighest) {
if isNil {
eqQuery += fmt.Sprintf("%s IS NULL AND ", column)
continue Loop
} else {
query := fmt.Sprintf("(%s(%s IS NULL OR %s %s ?))", eqQuery, column, column, comparisons[i])
queries = append(queries, query)
}
} else {
if isNil {
eqQuery += fmt.Sprintf("%s IS NOT NULL OR ", column)
continue Loop
} else {
query := fmt.Sprintf("(%s%s %s ?)", eqQuery, column, comparisons[i])
queries = append(queries, query)
}
return db.Where("("+strings.Join(queries, " OR ")+")", queryValues...)
}

eqQuery += fmt.Sprintf("%s = ? AND ", column)
nonNilValues = append(nonNilValues, values[i])
queryValues = append(queryValues, nonNilValues...)
}
return db.Where("("+strings.Join(queries, " OR ")+")", queryValues...)
}
}
13 changes: 13 additions & 0 deletions core/nulls_order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package core

// NullsOrder is how to handle null values when sorting.
type NullsOrder int

const (
// TreatsAsEngineDefault is the default behavior of the engine.
TreatsAsEngineDefault NullsOrder = iota
// TreatsAsLowest is that Null values are treated as the lowest.
TreatsAsLowest
// TreatsAsHighest is that Null values are treated as the highest.
TreatsAsHighest
)
20 changes: 9 additions & 11 deletions core/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,27 @@ const (
)

// OrderClauseBuilder returns a function that create ORDER BY clause to specifies the order of DB records.
func OrderClauseBuilder(columns ...string) func(orders ...Order) string {
return func(orders ...Order) string {
func OrderClauseBuilder(columns ...string) func(orders ...string) string {
return func(orders ...string) string {
if len(columns) != len(orders) {
panic("columns and orders must have the same length")
}

parts := make([]string, len(columns))
for i, column := range columns {
parts[i] = fmt.Sprintf("`%s` %s", toSnake(column), strings.ToUpper(string(orders[i])))
parts[i] = fmt.Sprintf("%s %s", toSnake(column), strings.ToUpper(string(orders[i])))
}
return strings.Join(parts, ", ")
}
}

// ReverseOrders returns a slice of Order converted from ASC to DESC, DESC to ASC.
func ReverseOrders(orders []Order) []Order {
newOrders := make([]Order, len(orders))
// ReverseOrders returns a slice of Order converted from ASC to DESC, DESC to ASC, FIRST to LAST, LAST to FIRST.
func ReverseOrders(orders []string) []string {
replacer := strings.NewReplacer("ASC", "DESC", "DESC", "ASC", "FIRST", "LAST", "LAST", "FIRST")
newOrders := make([]string, len(orders))
for i := 0; i < len(orders); i++ {
if orders[i] == ASC {
newOrders[i] = DESC
} else {
newOrders[i] = ASC
}
order := strings.ToUpper(orders[i])
newOrders[i] = replacer.Replace(order)
}
return newOrders
}
20 changes: 10 additions & 10 deletions core/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import (
)

func ExampleOrderClauseBuilder() {
fmt.Printf("%s\n", OrderClauseBuilder("ID", "CreatedAt")(ASC, ASC))
fmt.Printf("%s\n", OrderClauseBuilder("ID", "CreatedAt")(DESC, DESC))
fmt.Printf("%s\n", OrderClauseBuilder("ID", "CreatedAt")(ASC, DESC))
fmt.Printf("%s\n", OrderClauseBuilder("ID", "CreatedAt")("asc", "ASC"))
fmt.Printf("%s\n", OrderClauseBuilder("ID", "CreatedAt")("desc", "DESC"))
fmt.Printf("%s\n", OrderClauseBuilder("ID", "CreatedAt")("ASC", "desc"))

// Output:
// `id` ASC, `created_at` ASC
// `id` DESC, `created_at` DESC
// `id` ASC, `created_at` DESC
// id ASC, created_at ASC
// id DESC, created_at DESC
// id ASC, created_at DESC
}

func ExampleReverseOrders() {
fmt.Printf("%v\n", ReverseOrders([]Order{ASC, DESC, DESC}))
fmt.Printf("%v\n", ReverseOrders([]Order{DESC, ASC, ASC}))
fmt.Printf("%#v\n", ReverseOrders([]string{"asc", "DESC", "desc nulls first"}))
fmt.Printf("%#v\n", ReverseOrders([]string{"DESC", "asc", "ASC NULLS LAST"}))

// Output:
// [desc asc asc]
// [asc desc desc]
// []string{"DESC", "ASC", "ASC NULLS LAST"}
// []string{"ASC", "DESC", "DESC NULLS FIRST"}
}
Loading

0 comments on commit 1b48faa

Please sign in to comment.