3
0

Supports coalesce sorting

Updated parse sorting method to support coalesce sorting with multiple columns, by extending the current sortExpr with modifier and multiple columns, IE. `coalesce` as modifier and given arguments will be multiple columns for the sortExpr.

Also, updated cursor to support modifier and multiple column support along with rdbms.go.tpl for the same.
This commit is contained in:
Vivek Patel
2022-11-22 15:06:24 +05:30
committed by Jože Fortun
parent 1bba89a910
commit 79dc5b9a70
9 changed files with 1451 additions and 432 deletions
+45 -43
View File
@@ -39,7 +39,7 @@
###############################################################################
# Allow insecure (invalid, expired TLS/SSL certificates) connections.
#
#
# [IMPORTANT]
# ====
# We strongly recommend keeping this value set to false except for local development or demos.
@@ -209,7 +209,7 @@
###############################################################################
# Password for the web console endpoint. When running in dev environment, password is not required.
#
#
# Corteza intentionally sets default password to random chars to prevent security incidents.
# Type: string
# Default: <no value>
@@ -296,7 +296,7 @@
# Email sending
#
# Configure your local SMTP server or use one of the available providers.
#
#
# These values are copied to settings when the server starts and can be managed from the administration console.
# We recommend you remove these values after they are copied to settings.
# If server detects difference between these options and settings, it shows a warning in the log on server start.
@@ -436,7 +436,7 @@
###############################################################################
# Password security allows you to disable constraints to which passwords must conform to.
#
#
# [CAUTION]
# ====
# Disabling password security can be useful for development environments as it removes the need for complex passwords.
@@ -448,12 +448,12 @@
###############################################################################
# Algoritm to be use for JWT signature.
#
#
# Supported valus:
# - HS256, HS384, HS512
# - PS256, PS384, PS512,
# - RS256, RS384, RS512
#
#
# Provide shared secret string for HS256, HS384, HS512 and full private key or path to the file PS* and RS* algorithms.
# Type: string
# Default: HS512
@@ -462,7 +462,7 @@
###############################################################################
# Secret used for signing JWT tokens.
# Value is used only when HS256, HS384 or HS512 algorithm is used.
#
#
# [IMPORTANT]
# ====
# If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables.
@@ -486,7 +486,7 @@
###############################################################################
# Lifetime of the refresh token. Should be much longer than lifetime of the access token.
#
#
# Refresh tokens are used to exchange expired access tokens with new ones.
# Type: time.Duration
# Default: 72h
@@ -494,7 +494,7 @@
###############################################################################
# Redirect URL to be sent with OAuth2 authentication request to provider
#
#
# `provider` placeholder is replaced with the actual value when used.
# Type: string
# Default: <no value>
@@ -502,7 +502,7 @@
###############################################################################
# Secret used for securing cookies
#
#
# [IMPORTANT]
# ====
# If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables.
@@ -545,21 +545,21 @@
###############################################################################
# Maximum time user is allowed to stay idle when logged in without "remember-me" option and before session is expired.
#
#
# Recomended value is between an hour and a day.
#
#
# [IMPORTANT]
# ====
# This affects only profile (/auth) pages. Using applications (admin, compose, ...) does not prolong the session.
# ====
#
#
# Type: time.Duration
# Default: 24h
# AUTH_SESSION_LIFETIME=24h
###############################################################################
# Duration of the session in /auth lasts when user logs-in with "remember-me" option.
#
#
# If set to 0, "remember-me" option is removed.
# Type: time.Duration
# Default: 8640h
@@ -586,7 +586,7 @@
###############################################################################
# Secret used for securing CSRF protection
#
#
# [IMPORTANT]
# ====
# If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables.
@@ -616,19 +616,19 @@
###############################################################################
# Handle for OAuth2 client used for automatic redirect from /auth/oauth2/go endpoint.
#
#
# This simplifies configuration for OAuth2 flow for Corteza Web applications as it removes
# the need to suply redirection URL and client ID (oauth2/go endpoint does that internally)
#
#
# Type: string
# Default: corteza-webapp
# AUTH_DEFAULT_CLIENT=corteza-webapp
###############################################################################
# Path to js, css, images and template source files
#
#
# When corteza starts, if path exists it tries to load template files from it.
#
#
# When empty path is set (default value), embedded files are used.
# Type: string
# Default: <no value>
@@ -637,7 +637,7 @@
###############################################################################
# When enabled, corteza reloads template before every execution.
# Enable this for debugging or when developing auth templates.
#
#
# Should be disabled in production where templates do not change between server restarts.
# Type: bool
# Default: <no value>
@@ -815,15 +815,17 @@
###############################################################################
# List of compa delimited languages (language tags) to enable.
# In case when an enabled language can not be loaded, error is logged.
#
#
# When loading language configurations (config.xml) from the configured path(s).
#
#
# Type: string
# Default: en
# LOCALE_LANGUAGES=en
###############################################################################
# One or more paths to locale config and translation files, separated by colon
# One or more paths to locale config and translation files, separated by colon
#
# When with LOCALE_DEVELOPMENT_MODE=true, default value for path is ../../locale
# Type: string
# Default: <no value>
# LOCALE_PATH=<no value>
@@ -832,7 +834,7 @@
# Name of the query string parameter used to pass the language tag (it overrides Accept-Language header).
# Set it to empty string to disable detection from the query string.
# This parameter is ignored if only one language is enabled
#
#
# Type: string
# Default: lng
# LOCALE_QUERY_STRING_PARAM=lng
@@ -863,9 +865,9 @@
###############################################################################
# Disables json format for logging and enables more human-readable output with colors.
#
#
# Disable for production.
#
#
# Type: bool
# Default: <no value>
# LOG_DEBUG=<no value>
@@ -873,11 +875,11 @@
###############################################################################
# Minimum logging level. If set to "warn",
# Levels warn, error, dpanic panic and fatal will be logged.
#
#
# Recommended value for production: warn
#
#
# Possible values: debug, info, warn, error, dpanic, panic, fatal
#
#
# Type: string
# Default: warn
# LOG_LEVEL=warn
@@ -885,25 +887,25 @@
###############################################################################
# Log filtering rules by level and name (log-level:log-namespace).
# Please note that level (LOG_LEVEL) is applied before filter and it affects the final output!
#
#
# Leave unset for production.
#
#
# Example:
# `warn+:* *:auth,workflow.*`
# Log warnings, errors, panic, fatals. Everything from auth and workflow is logged.
#
#
#
#
# See more examples and documentation here: https://github.com/moul/zapfilter
#
#
# Type: string
# Default: <no value>
# LOG_FILTER=<no value>
###############################################################################
# Set to true to see where the logging was called from.
#
#
# Disable for production.
#
#
# Type: bool
# Default: <no value>
# LOG_INCLUDE_CALLER=<no value>
@@ -911,9 +913,9 @@
###############################################################################
# Include stack-trace when logging at a specified level or below.
# Disable for production.
#
#
# Possible values: debug, info, warn, error, dpanic, panic, fatal
#
#
# Type: string
# Default: dpanic
# LOG_STACKTRACE_LEVEL=dpanic
@@ -1008,11 +1010,11 @@
#
# Provisioning allows you to configure a {PRODUCT_NAME} instance when deployed.
# It occurs automatically after the {PRODUCT_NAME} server starts.
#
#
# [IMPORTANT]
# ====
# We recommend you to keep provisioning enabled as it simplifies version updates by updating the database and updating settings.
#
#
# If you're doing local development or some debugging, you can disable this.
# ====
#
@@ -1038,7 +1040,7 @@
# ====
# These parameters help in the development and testing process.
# When you are deploying to production, these should be disabled to improve performance and reduce storage usage.
#
#
# You should configure external services such as Sentry or ELK to keep track of logs and error reports.
# ====
#
@@ -1144,7 +1146,7 @@
# Delay system startup
#
# You can configure these options to defer API execution until another external (HTTP) service is up and running.
#
#
# [ TIP ]
# ====
# Delaying API execution can come in handy in complex setups where execution order is important.
@@ -1168,7 +1170,7 @@
###############################################################################
# Space delimited list of hosts and/or URLs to probe.
# Host format: `host` or `host:443` (port will default to 80).
#
#
# [NOTE]
# ====
# Services are probed in parallel.
@@ -10,6 +10,7 @@ import (
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
"github.com/modern-go/reflect2"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
@@ -551,19 +552,35 @@ func (s *Store) {{ .fnIdent }}(res *{{ .goType }}, cc ...*filter.SortExpr) *filt
{{- end }}
collect = func(cc ...*filter.SortExpr) {
for _, c := range cc {
switch c.Column {
getVal := func(col string) interface{} {
switch col {
{{- range .fields }}
case {{ printf "%q" .ident }}:
cur.Set(c.Column, res.{{ .expIdent }}, c.Descending)
{{- if .primaryKey }}
pk{{ .expIdent }} = true
{{- else if .unique }}
hasUnique = true
{{- end }}
pk{{ .expIdent }} = true
{{- else if .unique }}
hasUnique = true
{{- end }}
return res.{{ .expIdent }}
{{- end }}
}
return nil
}
for _, c := range cc {
switch c.Modifier() {
case filter.COALESCE:
var val interface{}
for _, col := range c.Columns() {
if reflect2.IsNil(val) {
val = getVal(col)
}
}
cur.SetModifier(c.Column, val, c.Descending, c.Modifier(), c.Columns()...)
default:
cur.Set(c.Column, getVal(c.Column), c.Descending)
}
}
}
)
+30 -4
View File
@@ -46,9 +46,11 @@ type (
}
PagingCursor struct {
keys []string
values []interface{}
desc []bool
keys []string
kk [][]string
values []interface{}
modifier []string
desc []bool
// sort in desc order
ROrder bool
@@ -109,6 +111,10 @@ func (p *PagingCursor) Walk(fn func(string, interface{}, bool)) {
}
func (p *PagingCursor) Set(k string, v interface{}, d bool) {
p.SetModifier(k, v, d, "", k)
}
func (p *PagingCursor) SetModifier(k string, v interface{}, d bool, m string, kk ...string) {
for i, key := range p.keys {
if key == k {
p.values[i] = v
@@ -118,6 +124,8 @@ func (p *PagingCursor) Set(k string, v interface{}, d bool) {
p.keys = append(p.keys, k)
p.values = append(p.values, v)
p.kk = append(p.kk, kk)
p.modifier = append(p.modifier, m)
p.desc = append(p.desc, d)
}
@@ -129,6 +137,14 @@ func (p *PagingCursor) Values() []interface{} {
return p.values
}
func (p *PagingCursor) KK() [][]string {
return p.kk
}
func (p *PagingCursor) Modifiers() []string {
return p.modifier
}
func (p *PagingCursor) IsLThen() bool {
return p.LThen
}
@@ -177,13 +193,17 @@ func (p *PagingCursor) String() string {
func (p *PagingCursor) MarshalJSON() ([]byte, error) {
buf, err := json.Marshal(struct {
K []string
KK [][]string
V []interface{}
M []string
D []bool
R bool
LT bool
}{
p.keys,
p.kk,
p.values,
p.modifier,
p.desc,
p.ROrder,
p.LThen,
@@ -209,7 +229,9 @@ func (p *PagingCursor) UnmarshalJSON(in []byte) error {
var (
aux struct {
K []string
KK [][]string
V []pagingCursorValue
M []string
D []bool
R bool
LT bool
@@ -235,6 +257,8 @@ func (p *PagingCursor) UnmarshalJSON(in []byte) error {
}
p.keys = aux.K
p.kk = aux.KK
p.modifier = aux.M
p.desc = aux.D
p.ROrder = aux.R
p.LThen = aux.LT
@@ -277,6 +301,8 @@ func (p *PagingCursor) Sort(sort SortExprSet) (SortExprSet, error) {
for k := range p.keys {
sort = append(sort, &SortExpr{
Column: p.keys[k],
columns: p.kk[k],
modifier: p.modifier[k],
Descending: p.desc[k],
})
}
@@ -363,7 +389,7 @@ func (cur *PagingCursor) ToAST(identLookup func(i string) (string, error), castF
op := ltOp[lt]
//// Typecast the value so comparison can work properly
// // Typecast the value so comparison can work properly
// Either BOTH (field and value) are NULL or field is grater-then value
base := &ql.ASTNode{
+130 -24
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/cortezaproject/corteza/server/pkg/slice"
@@ -19,6 +18,8 @@ type (
SortExpr struct {
Column string
columns []string
modifier string
Descending bool
// NullsFirst bool
}
@@ -26,6 +27,10 @@ type (
SortExprSet []*SortExpr
)
const (
COALESCE = "coalesce"
)
func NewSorting(sort string) (s Sorting, err error) {
s = Sorting{}
@@ -40,16 +45,7 @@ func (s *Sorting) OrderBy() SortExprSet {
return s.Sort
}
// parses sort string
//
// We allow a simplified version of what SQL supports, so:
// "<name>( <direction>), ..."
//
// Unlike before, we do not use pkg/ql for parsing this as we do not allow
// any complex sorting expressions
func parseSort(in string) (set SortExprSet, err error) {
exprMatcher := regexp.MustCompile(`([0-9a-zA-Z_\.]+)(\s+(asc|ASC|desc|DESC))?`)
set = SortExprSet{}
if in == "" {
return
@@ -60,24 +56,89 @@ func parseSort(in string) (set SortExprSet, err error) {
return
}
for _, expr := range strings.Split(in, ",") {
mm := exprMatcher.FindStringSubmatch(strings.TrimSpace(expr))
aux := splitCommaParenthesis(in)
o := &SortExpr{}
switch {
case len(mm) == 0:
return nil, fmt.Errorf("invalid sort expression")
case len(mm) >= 2:
o.Column = mm[1]
fallthrough
case len(mm) >= 4:
o.Descending = strings.ToUpper(mm[3]) == "DESC"
// iterate over aux, split each part and generate SortExpr
// if string contains ( and ) then extract modifier before (
// and optional direction after the closing )
// if there are no parenthesis, then the whole string is the column name with the desc/asc at the end
for _, s := range aux {
var (
modifier string
columns []string
sortExpr = &SortExpr{}
toLower = strings.ToLower
hasSuffix = strings.HasSuffix
indexByte = strings.IndexByte
)
// extract modifier
if idx := indexByte(s, '('); idx > 0 {
modifier = s[:idx]
s = s[idx+1:]
}
set = append(set, o)
// extract columns
idx := indexByte(s, ')')
// extract desc
if hasSuffix(toLower(s), " desc") {
sortExpr.Descending = true
s = s[:len(s)-5]
} else if hasSuffix(toLower(s), " asc") {
sortExpr.Descending = false
s = s[:len(s)-4]
}
if idx > 0 {
columns = splitCommaParenthesis(s[:idx])
s = s[idx+1:]
} else {
columns = []string{s}
}
sortExpr.SetColumns(columns...)
err = sortExpr.SetModifier(modifier)
if err != nil {
return
}
set = append(set, sortExpr)
}
return set, nil
return
}
// splitCommaParenthesis can split a string into a slice of strings
// string is separated by commas but can have parenthesis with commas inside
// and the commas inside the parenthesis should not be considered separators
// and the parenthesis should be removed from the output
func splitCommaParenthesis(in string) (out []string) {
var (
depth int
start int
)
for i, r := range in {
switch r {
case '(':
depth++
case ')':
depth--
case ',':
if depth == 0 {
out = append(out, strings.TrimSpace(in[start:i]))
start = i + 1
}
}
}
if start < len(in) {
out = append(out, strings.TrimSpace(in[start:]))
}
return
}
// UnmarshalJSON parses sort expression when passed inside JSON
@@ -133,6 +194,8 @@ func (set SortExprSet) Clone() (out SortExprSet) {
for i := range set {
out[i] = &SortExpr{
Column: set[i].Column,
columns: set[i].columns,
modifier: set[i].modifier,
Descending: set[i].Descending,
}
}
@@ -178,7 +241,11 @@ func (set SortExprSet) Columns() []string {
func (set SortExprSet) String() string {
out := make([]string, len(set))
for i := range set {
out[i] = set[i].Column
if set[i].modifier != "" {
out[i] = fmt.Sprintf("%s(%s)", set[i].modifier, strings.Join(set[i].columns, ","))
} else {
out[i] = set[i].Column
}
if set[i].Descending {
out[i] += " DESC"
@@ -188,3 +255,42 @@ func (set SortExprSet) String() string {
return strings.Join(out, ", ")
}
func (s *SortExpr) Columns() []string {
if len(s.columns) == 0 && s.Column != "" {
s.columns = []string{s.Column}
}
return s.columns
}
func (s *SortExpr) SetColumns(columns ...string) {
// @todo this can be improved by supporting multiple columns even without modifier
// fallback to single column
if s.modifier == "" && len(columns) == 1 {
s.Column = columns[0]
}
s.columns = append(s.columns, columns...)
}
func (s *SortExpr) Modifier() string {
if s == nil {
return ""
}
return s.modifier
}
func (s *SortExpr) SetModifier(modifier string) error {
if s == nil {
return nil
}
switch strings.ToLower(modifier) {
case COALESCE:
s.modifier = modifier
default:
if len(s.modifier) > 0 {
return fmt.Errorf("invalid modifier %q", modifier)
}
}
return nil
}
+51 -5
View File
@@ -17,22 +17,44 @@ func Test_parseSort(t *testing.T) {
{
"one simple column",
"name",
SortExprSet{&SortExpr{Column: "name"}},
SortExprSet{
&SortExpr{
columns: []string{"name"},
Column: "name",
},
},
false,
},
{
"one simple column, descending",
"name desc",
SortExprSet{&SortExpr{Column: "name", Descending: true}},
SortExprSet{
&SortExpr{
columns: []string{"name"},
Column: "name",
Descending: true},
},
false,
},
{
"combo",
"name desc, email asc, age desc",
SortExprSet{
&SortExpr{Column: "name", Descending: true},
&SortExpr{Column: "email", Descending: false},
&SortExpr{Column: "age", Descending: true},
&SortExpr{
columns: []string{"name"},
Column: "name",
Descending: true,
},
&SortExpr{
columns: []string{"email"},
Column: "email",
Descending: false,
},
&SortExpr{
columns: []string{"age"},
Column: "age",
Descending: true,
},
},
false,
},
@@ -42,6 +64,30 @@ func Test_parseSort(t *testing.T) {
SortExprSet{},
false,
},
{
"COALESCE with multiple column",
"COALESCE(updated_at,created_at)",
SortExprSet{
&SortExpr{
modifier: "COALESCE",
columns: []string{"updated_at", "created_at"},
Descending: false,
},
},
false,
},
{
"COALESCE with multiple column, descending",
"COALESCE(updated_at,created_at) desc",
SortExprSet{
&SortExpr{
modifier: "COALESCE",
columns: []string{"updated_at", "created_at"},
Descending: true,
},
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+16 -1
View File
@@ -19,6 +19,8 @@ type (
pagingCursor interface {
Keys() []string
KK() [][]string
Modifiers() []string
Values() []interface{}
IsLThen() bool
Desc() []bool
@@ -121,6 +123,8 @@ func (c *cursorCondition) sql() (cnd string, err error) {
var (
cc = c.cur.Keys()
kk = c.cur.KK()
mm = c.cur.Modifiers()
vv = c.cur.Values()
ltOp = map[bool]string{
@@ -168,6 +172,17 @@ func (c *cursorCondition) sql() (cnd string, err error) {
if v, ok := c.sortableCols[strings.ToLower(cc[i])]; ok {
colName = v
}
// update column name as per the modifier if needed
if len(kk[i]) > 0 && strings.ToLower(mm[i]) == filter.COALESCE {
var tmp []string
for _, k := range kk[i] {
if v, ok := c.sortableCols[strings.ToLower(k)]; ok {
tmp = append(tmp, v)
}
}
colName = fmt.Sprintf("COALESCE(%s)", strings.Join(tmp, ", "))
}
}
km, err := c.keyMapper(colName)
@@ -291,7 +306,7 @@ func CursorExpression(
op := ltOp[lt]
//// Typecast the value so comparison can work properly
// // Typecast the value so comparison can work properly
// Either BOTH (field and value) are NULL or field is grater-then value
base := exp.NewExpressionList(
+62 -17
View File
@@ -90,22 +90,22 @@ func DefaultFilters() (f *extendedFilters) {
case composeType.RecordAttachment:
panic("@todo pending implementation")
//query = query.
// query = query.
// Join("compose_record_value AS v ON (v.ref = a.id)")
//
//if f.ModuleID > 0 {
// if f.ModuleID > 0 {
// query = query.
// Join("compose_record AS r ON (r.id = v.record_id)").
// Where(squirrel.Eq{"r.module_id": f.ModuleID})
//}
// }
//
//if f.RecordID > 0 {
// if f.RecordID > 0 {
// query = query.Where(squirrel.Eq{"v.record_id": f.RecordID})
//}
// }
//
//if f.FieldName != "" {
// if f.FieldName != "" {
// query = query.Where(squirrel.Eq{"v.name": f.FieldName})
//}
// }
default:
err = fmt.Errorf("unsupported kind value")
@@ -269,7 +269,7 @@ func DefaultFilters() (f *extendedFilters) {
}
// Always sort by ID descending
//query = query.OrderBy("id DESC")
// query = query.OrderBy("id DESC")
if f.FromTimestamp != nil {
ee = append(ee, goqu.C("ts").Gte(f.ToTimestamp))
@@ -351,22 +351,19 @@ func Order(sort filter.SortExprSet, sortables map[string]string) (oo []exp.Order
func order(sort filter.SortExprSet, sortables map[string]string) (oo []exp.OrderedExpression, err error) {
var (
has bool
col string
sortExpr goqu.Expression
)
for _, s := range sort {
if len(sortables) > 0 {
if col, has = sortables[strings.ToLower(s.Column)]; !has {
return nil, fmt.Errorf("column %q is not sortable", s.Column)
}
s.Column = col
sortExpr, err = generateSorting(sortables, s)
if err != nil {
return
}
if s.Descending {
oo = append(oo, exp.NewOrderedExpression(goqu.I(s.Column), exp.DescSortDir, exp.NoNullsSortType))
oo = append(oo, exp.NewOrderedExpression(sortExpr, exp.DescSortDir, exp.NoNullsSortType))
} else {
oo = append(oo, exp.NewOrderedExpression(goqu.I(s.Column), exp.AscDir, exp.NoNullsSortType))
oo = append(oo, exp.NewOrderedExpression(sortExpr, exp.AscDir, exp.NoNullsSortType))
}
}
@@ -403,3 +400,51 @@ func stateFalseComparison(lit string, fs filter.State) goqu.Expression {
return nil
}
}
// @todo: Currently we have for support for MsSQL, MySql, PSQL, SQLite drivers,
// this changes is supported by all DB but we need to move to store.driver
// generateSorting verify and converts given sorting to literal if required
func generateSorting(sortables map[string]string, s *filter.SortExpr) (out goqu.Expression, err error) {
const COALESCE string = "coalesce"
var (
val string
has bool
literalCols []interface{}
toLower = strings.ToLower
hasSorting = func(column string) (string, bool) {
v, ok := sortables[toLower(column)]
return v, ok
}
)
if len(sortables) > 0 && s != nil {
for _, col := range s.Columns() {
val, has = hasSorting(col)
if has {
if out == nil {
out = goqu.I(val)
}
literalCols = append(literalCols, goqu.I(val))
} else {
if len(s.Columns()) == 1 {
err = fmt.Errorf("invalid column name: %s", col)
} else {
err = fmt.Errorf("column %q is not sortable in %s sorting", col, s.Columns())
}
return nil, err
}
}
if toLower(s.Modifier()) == COALESCE {
if len(literalCols) > 0 {
out = goqu.COALESCE(literalCols...)
} else {
return nil, fmt.Errorf("invalid sorting with %s modifier and columns: %s", s.Modifier(), s.Columns())
}
}
}
return
}
@@ -0,0 +1,89 @@
package rdbms
import (
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/doug-martin/goqu/v9"
"reflect"
"testing"
)
func Test_generateSorting(t *testing.T) {
tests := []struct {
name string
sort filter.SortExpr
columns []string
modifier string
sortables map[string]string
wantOut goqu.Expression
wantErr bool
}{
{
name: "single column sorting",
sort: filter.SortExpr{Column: "createdat"},
sortables: map[string]string{
"createdat": "created_at",
},
wantOut: goqu.I("created_at"),
wantErr: false,
},
{
name: "path sorting",
sort: filter.SortExpr{Column: "user.createdat"},
sortables: map[string]string{
"user.createdat": "user.created_at",
},
wantOut: goqu.I("user.created_at"),
wantErr: false,
},
{
name: "coalesce single column sorting",
sort: filter.SortExpr{},
columns: []string{"createdat"},
modifier: "coalesce",
sortables: map[string]string{
"createdat": "created_at",
"updatedat": "updated_at",
},
wantOut: goqu.COALESCE(goqu.I("created_at")),
wantErr: false,
},
{
name: "coalesce multiple column sorting",
sort: filter.SortExpr{},
columns: []string{"createdat", "updatedat"},
modifier: "coalesce",
sortables: map[string]string{
"createdat": "created_at",
"updatedat": "updated_at",
},
wantOut: goqu.COALESCE(goqu.I("created_at"), goqu.I("updated_at")),
wantErr: false,
},
{
name: "invalid coalesce multiple column sorting",
sort: filter.SortExpr{},
columns: []string{"createdat", "deletedat"},
modifier: "coalesce",
sortables: map[string]string{
"createdat": "created_at",
"updatedat": "updated_at",
},
wantOut: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.sort.SetColumns(tt.columns...)
_ = tt.sort.SetModifier(tt.modifier)
gotOut, err := generateSorting(tt.sortables, &tt.sort)
if (err != nil) != tt.wantErr {
t.Errorf("generateSorting() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotOut, tt.wantOut) {
t.Errorf("generateSorting() gotOut = %v, want %v", gotOut, tt.wantOut)
}
})
}
}
+1003 -330
View File
File diff suppressed because it is too large Load Diff