synced with master

This commit is contained in:
Gani Georgiev
2024-01-03 12:46:49 +02:00
105 changed files with 5204 additions and 4548 deletions
+16
View File
@@ -7,6 +7,8 @@ import (
)
func TestCronNew(t *testing.T) {
t.Parallel()
c := New()
expectedInterval := 1 * time.Minute
@@ -29,6 +31,8 @@ func TestCronNew(t *testing.T) {
}
func TestCronSetInterval(t *testing.T) {
t.Parallel()
c := New()
interval := 2 * time.Minute
@@ -41,6 +45,8 @@ func TestCronSetInterval(t *testing.T) {
}
func TestCronSetTimezone(t *testing.T) {
t.Parallel()
c := New()
timezone, _ := time.LoadLocation("Asia/Tokyo")
@@ -53,6 +59,8 @@ func TestCronSetTimezone(t *testing.T) {
}
func TestCronAddAndRemove(t *testing.T) {
t.Parallel()
c := New()
if err := c.Add("test0", "* * * * *", nil); err == nil {
@@ -126,6 +134,8 @@ func TestCronAddAndRemove(t *testing.T) {
}
func TestCronMustAdd(t *testing.T) {
t.Parallel()
c := New()
defer func() {
@@ -144,6 +154,8 @@ func TestCronMustAdd(t *testing.T) {
}
func TestCronRemoveAll(t *testing.T) {
t.Parallel()
c := New()
if err := c.Add("test1", "* * * * *", func() {}); err != nil {
@@ -170,6 +182,8 @@ func TestCronRemoveAll(t *testing.T) {
}
func TestCronTotal(t *testing.T) {
t.Parallel()
c := New()
if v := c.Total(); v != 0 {
@@ -195,6 +209,8 @@ func TestCronTotal(t *testing.T) {
}
func TestCronStartStop(t *testing.T) {
t.Parallel()
c := New()
c.SetInterval(1 * time.Second)
+6
View File
@@ -9,6 +9,8 @@ import (
)
func TestNewMoment(t *testing.T) {
t.Parallel()
date, err := time.Parse("2006-01-02 15:04", "2023-05-09 15:20")
if err != nil {
t.Fatal(err)
@@ -38,6 +40,8 @@ func TestNewMoment(t *testing.T) {
}
func TestNewSchedule(t *testing.T) {
t.Parallel()
scenarios := []struct {
cronExpr string
expectError bool
@@ -272,6 +276,8 @@ func TestNewSchedule(t *testing.T) {
}
func TestScheduleIsDue(t *testing.T) {
t.Parallel()
scenarios := []struct {
cronExpr string
moment *cron.Moment
+30 -12
View File
@@ -184,14 +184,15 @@ func buildResolversExpr(
if !isAnyMatchOp(op) {
if left.MultiMatchSubQuery != nil && right.MultiMatchSubQuery != nil {
mm := &manyVsManyExpr{
leftSubQuery: left.MultiMatchSubQuery,
rightSubQuery: right.MultiMatchSubQuery,
op: op,
left: left,
right: right,
op: op,
}
expr = dbx.Enclose(dbx.And(expr, mm))
} else if left.MultiMatchSubQuery != nil {
mm := &manyVsOneExpr{
noCoalesce: left.NoCoalesce,
subQuery: left.MultiMatchSubQuery,
op: op,
otherOperand: right,
@@ -200,6 +201,7 @@ func buildResolversExpr(
expr = dbx.Enclose(dbx.And(expr, mm))
} else if right.MultiMatchSubQuery != nil {
mm := &manyVsOneExpr{
noCoalesce: right.NoCoalesce,
subQuery: right.MultiMatchSubQuery,
op: op,
otherOperand: left,
@@ -290,17 +292,29 @@ func resolveEqualExpr(equal bool, left, right *ResolverResult) dbx.Expression {
isRightEmpty := isEmptyIdentifier(right) || (len(right.Params) == 1 && hasEmptyParamValue(right))
equalOp := "="
nullEqualOp := "IS"
concatOp := "OR"
nullExpr := "IS NULL"
if !equal {
// use `IS NOT` instead of `!=` because direct non-equal comparisons
// always use `IS NOT` instead of `!=` because direct non-equal comparisons
// to nullable column values that are actually NULL yields to NULL instead of TRUE, eg.:
// `'example' != nullableColumn` -> NULL even if nullableColumn row value is NULL
equalOp = "IS NOT"
nullEqualOp = equalOp
concatOp = "AND"
nullExpr = "IS NOT NULL"
}
// no coalesce (eg. compare to a json field)
// a IS b
// a IS NOT b
if left.NoCoalesce || right.NoCoalesce {
return dbx.NewExp(
fmt.Sprintf("%s %s %s", left.Identifier, nullEqualOp, right.Identifier),
mergeParams(left.Params, right.Params),
)
}
// both operands are empty
if isLeftEmpty && isRightEmpty {
return dbx.NewExp(fmt.Sprintf("'' %s ''", equalOp), mergeParams(left.Params, right.Params))
@@ -459,8 +473,8 @@ var _ dbx.Expression = (*concatExpr)(nil)
// concatExpr defines an expression that concatenates multiple
// other expressions with a specified separator.
type concatExpr struct {
parts []dbx.Expression
separator string
parts []dbx.Expression
}
// Build converts the expression into a SQL fragment.
@@ -503,16 +517,16 @@ var _ dbx.Expression = (*manyVsManyExpr)(nil)
// Expects leftSubQuery and rightSubQuery to return a subquery with a
// single "multiMatchValue" column.
type manyVsManyExpr struct {
leftSubQuery dbx.Expression
rightSubQuery dbx.Expression
op fexpr.SignOp
left *ResolverResult
right *ResolverResult
op fexpr.SignOp
}
// Build converts the expression into a SQL fragment.
//
// Implements [dbx.Expression] interface.
func (e *manyVsManyExpr) Build(db *dbx.DB, params dbx.Params) string {
if e.leftSubQuery == nil || e.rightSubQuery == nil {
if e.left.MultiMatchSubQuery == nil || e.right.MultiMatchSubQuery == nil {
return "0=1"
}
@@ -521,10 +535,12 @@ func (e *manyVsManyExpr) Build(db *dbx.DB, params dbx.Params) string {
whereExpr, buildErr := buildResolversExpr(
&ResolverResult{
NoCoalesce: e.left.NoCoalesce,
Identifier: "[[" + lAlias + ".multiMatchValue]]",
},
e.op,
&ResolverResult{
NoCoalesce: e.right.NoCoalesce,
Identifier: "[[" + rAlias + ".multiMatchValue]]",
// note: the AfterBuild needs to be handled only once and it
// doesn't matter whether it is applied on the left or right subquery operand
@@ -538,9 +554,9 @@ func (e *manyVsManyExpr) Build(db *dbx.DB, params dbx.Params) string {
return fmt.Sprintf(
"NOT EXISTS (SELECT 1 FROM (%s) {{%s}} LEFT JOIN (%s) {{%s}} WHERE %s)",
e.leftSubQuery.Build(db, params),
e.left.MultiMatchSubQuery.Build(db, params),
lAlias,
e.rightSubQuery.Build(db, params),
e.right.MultiMatchSubQuery.Build(db, params),
rAlias,
whereExpr.Build(db, params),
)
@@ -556,10 +572,11 @@ var _ dbx.Expression = (*manyVsOneExpr)(nil)
//
// You can set inverse=false to reverse the condition sides (aka. one<->many).
type manyVsOneExpr struct {
otherOperand *ResolverResult
subQuery dbx.Expression
op fexpr.SignOp
otherOperand *ResolverResult
inverse bool
noCoalesce bool
}
// Build converts the expression into a SQL fragment.
@@ -573,6 +590,7 @@ func (e *manyVsOneExpr) Build(db *dbx.DB, params dbx.Params) string {
alias := "__sm" + security.PseudorandomString(5)
r1 := &ResolverResult{
NoCoalesce: e.noCoalesce,
Identifier: "[[" + alias + ".multiMatchValue]]",
AfterBuild: multiMatchAfterBuildFunc(e.op, alias),
}
+7 -1
View File
@@ -13,7 +13,7 @@ import (
)
func TestFilterDataBuildExpr(t *testing.T) {
resolver := search.NewSimpleFieldResolver("test1", "test2", "test3", `^test4_\w+$`)
resolver := search.NewSimpleFieldResolver("test1", "test2", "test3", `^test4_\w+$`, `^test5\.[\w\.\:]*\w+$`)
scenarios := []struct {
name string
@@ -93,6 +93,12 @@ func TestFilterDataBuildExpr(t *testing.T) {
false,
"[[test1]] NOT LIKE {:TEST} ESCAPE '\\'",
},
{
"nested json no coalesce",
"test5.a = test5.b || test5.c != test5.d",
false,
"(JSON_EXTRACT([[test5]], '$.a') IS JSON_EXTRACT([[test5]], '$.b') OR JSON_EXTRACT([[test5]], '$.c') IS NOT JSON_EXTRACT([[test5]], '$.d'))",
},
{
"macros",
`
+5
View File
@@ -16,6 +16,10 @@ type ResolverResult struct {
// in the final db expression as left or right operand.
Identifier string
// NoCoalesce instructs to not use COALESCE or NULL fallbacks
// when building the identifier expression.
NoCoalesce bool
// Params is a map with db placeholder->value pairs that will be added
// to the query when building both resolved operands/sides in a single expression.
Params dbx.Params
@@ -99,6 +103,7 @@ func (r *SimpleFieldResolver) Resolve(field string) (*ResolverResult, error) {
}
return &ResolverResult{
NoCoalesce: true,
Identifier: fmt.Sprintf(
"JSON_EXTRACT([[%s]], '%s')",
inflector.Columnify(parts[0]),