Skip to content

Commit

Permalink
reduced the parenthesis in the generated filter sql query
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Dec 14, 2022
1 parent 5183280 commit 8815f60
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 61 deletions.
68 changes: 63 additions & 5 deletions tools/search/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (f FilterData) build(data []fexpr.ExprGroup, fieldResolver FieldResolver) (
return nil, errors.New("Empty filter expression.")
}

var result dbx.Expression
result := &concatExpr{separator: " "}

for _, group := range data {
var expr dbx.Expression
Expand All @@ -68,11 +68,17 @@ func (f FilterData) build(data []fexpr.ExprGroup, fieldResolver FieldResolver) (
return nil, exprErr
}

if group.Join == fexpr.JoinAnd {
result = dbx.And(result, expr)
} else {
result = dbx.Or(result, expr)
if len(result.parts) > 0 {
var op string
if group.Join == fexpr.JoinOr {
op = "OR"
} else {
op = "AND"
}
result.parts = append(result.parts, &opExpr{op})
}

result.parts = append(result.parts, expr)
}

return result, nil
Expand Down Expand Up @@ -209,3 +215,55 @@ func wrapLikeParams(params dbx.Params) dbx.Params {

return result
}

// -------------------------------------------------------------------

// opExpr defines an expression that contains a raw sql operator string.
type opExpr struct {
op string
}

// Build converts an expression into a SQL fragment.
//
// Implements [dbx.Expression] interface.
func (e *opExpr) Build(db *dbx.DB, params dbx.Params) string {
return e.op
}

// concatExpr defines an expression that concatenates multiple
// other expressions with a specified separator.
type concatExpr struct {
parts []dbx.Expression
separator string
}

// Build converts an expression into a SQL fragment.
//
// Implements [dbx.Expression] interface.
func (e *concatExpr) Build(db *dbx.DB, params dbx.Params) string {
if len(e.parts) == 0 {
return ""
}

stringParts := make([]string, 0, len(e.parts))

for _, a := range e.parts {
if a == nil {
continue
}

if sql := a.Build(db, params); sql != "" {
stringParts = append(stringParts, sql)
}
}

// skip extra parenthesis for single concat expression
if len(stringParts) == 1 &&
// check for already concatenated raw/plain expressions
!strings.Contains(strings.ToUpper(stringParts[0]), " AND ") &&
!strings.Contains(strings.ToUpper(stringParts[0]), " OR ") {
return stringParts[0]
}

return "(" + strings.Join(stringParts, e.separator) + ")"
}
108 changes: 65 additions & 43 deletions tools/search/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,118 +12,140 @@ func TestFilterDataBuildExpr(t *testing.T) {
resolver := search.NewSimpleFieldResolver("test1", "test2", "test3", "test4.sub")

scenarios := []struct {
name string
filterData search.FilterData
expectError bool
expectPattern string
}{
// empty
{"", true, ""},
// invalid format
{"(test1 > 1", true, ""},
// invalid operator
{"test1 + 123", true, ""},
// unknown field
{"test1 = 'example' && unknown > 1", true, ""},
// simple expression
{"test1 > 1", false,
{
"empty",
"",
true,
"",
},
{
"invalid format",
"(test1 > 1", true, ""},
{
"invalid operator",
"test1 + 123",
true,
"",
},
{
"unknown field",
"test1 = 'example' && unknown > 1",
true,
"",
},
{
"simple expression",
"test1 > 1", false,
"^" +
regexp.QuoteMeta("[[test1]] > {:") +
".+" +
regexp.QuoteMeta("}") +
"$",
},
// like with 2 columns
{"test1 ~ test2", false,
{
"like with 2 columns",
"test1 ~ test2", false,
"^" +
regexp.QuoteMeta("[[test1]] LIKE ('%' || [[test2]] || '%') ESCAPE '\\'") +
"$",
},
// like with right column operand
{"'lorem' ~ test1", false,
{
"like with right column operand",
"'lorem' ~ test1", false,
"^" +
regexp.QuoteMeta("{:") +
".+" +
regexp.QuoteMeta("} LIKE ('%' || [[test1]] || '%') ESCAPE '\\'") +
"$",
},
// like with left column operand and text as right operand
{"test1 ~ 'lorem'", false,
{
"like with left column operand and text as right operand",
"test1 ~ 'lorem'", false,
"^" +
regexp.QuoteMeta("[[test1]] LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\'") +
"$",
},
// not like with 2 columns
{"test1 !~ test2", false,
{
"not like with 2 columns",
"test1 !~ test2", false,
"^" +
regexp.QuoteMeta("[[test1]] NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\'") +
"$",
},
// not like with right column operand
{"'lorem' !~ test1", false,
{
"not like with right column operand",
"'lorem' !~ test1", false,
"^" +
regexp.QuoteMeta("{:") +
".+" +
regexp.QuoteMeta("} NOT LIKE ('%' || [[test1]] || '%') ESCAPE '\\'") +
"$",
},
// like with left column operand and text as right operand
{"test1 !~ 'lorem'", false,
{
"like with left column operand and text as right operand",
"test1 !~ 'lorem'", false,
"^" +
regexp.QuoteMeta("[[test1]] NOT LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\'") +
"$",
},
// current datetime constant
{"test1 > @now", false,
{
"current datetime constant",
"test1 > @now", false,
"^" +
regexp.QuoteMeta("[[test1]] > {:") +
".+" +
regexp.QuoteMeta("}") +
"$",
},
// complex expression
{
"complex expression",
"((test1 > 1) || (test2 != 2)) && test3 ~ '%%example' && test4.sub = null",
false,
"^" +
regexp.QuoteMeta("((([[test1]] > {:") +
regexp.QuoteMeta("(([[test1]] > {:") +
".+" +
regexp.QuoteMeta("}) OR (COALESCE([[test2]], '') != COALESCE({:") +
regexp.QuoteMeta("} OR COALESCE([[test2]], '') != COALESCE({:") +
".+" +
regexp.QuoteMeta("}, ''))) AND ([[test3]] LIKE {:") +
regexp.QuoteMeta("}, '')) AND [[test3]] LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\')) AND (COALESCE([[test4.sub]], '') = COALESCE(NULL, ''))") +
regexp.QuoteMeta("} ESCAPE '\\' AND COALESCE([[test4.sub]], '') = COALESCE(NULL, ''))") +
"$",
},
// combination of special literals (null, true, false)
{
"combination of special literals (null, true, false)",
"test1=true && test2 != false && test3 = null || test4.sub != null",
false,
"^" + regexp.QuoteMeta("(((COALESCE([[test1]], '') = COALESCE(1, '')) AND (COALESCE([[test2]], '') != COALESCE(0, ''))) AND (COALESCE([[test3]], '') = COALESCE(NULL, ''))) OR (COALESCE([[test4.sub]], '') != COALESCE(NULL, ''))") + "$",
"^" + regexp.QuoteMeta("(COALESCE([[test1]], '') = COALESCE(1, '') AND COALESCE([[test2]], '') != COALESCE(0, '') AND COALESCE([[test3]], '') = COALESCE(NULL, '') OR COALESCE([[test4.sub]], '') != COALESCE(NULL, ''))") + "$",
},
// all operators
{
"all operators",
"(test1 = test2 || test2 != test3) && (test2 ~ 'example' || test2 !~ '%%abc') && 'switch1%%' ~ test1 && 'switch2' !~ test2 && test3 > 1 && test3 >= 0 && test3 <= 4 && 2 < 5",
false,
"^" +
regexp.QuoteMeta("((((((((COALESCE([[test1]], '') = COALESCE([[test2]], '')) OR (COALESCE([[test2]], '') != COALESCE([[test3]], ''))) AND (([[test2]] LIKE {:") +
regexp.QuoteMeta("((COALESCE([[test1]], '') = COALESCE([[test2]], '') OR COALESCE([[test2]], '') != COALESCE([[test3]], '')) AND ([[test2]] LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\') OR ([[test2]] NOT LIKE {:") +
regexp.QuoteMeta("} ESCAPE '\\' OR [[test2]] NOT LIKE {:") +
".+" +
regexp.QuoteMeta("} ESCAPE '\\'))) AND ({:") +
regexp.QuoteMeta("} ESCAPE '\\') AND {:") +
".+" +
regexp.QuoteMeta("} LIKE ('%' || [[test1]] || '%') ESCAPE '\\')) AND ({:") +
regexp.QuoteMeta("} LIKE ('%' || [[test1]] || '%') ESCAPE '\\' AND {:") +
".+" +
regexp.QuoteMeta("} NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\')) AND ([[test3]] > {:") +
regexp.QuoteMeta("} NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\' AND [[test3]] > {:") +
".+" +
regexp.QuoteMeta("})) AND ([[test3]] >= {:") +
regexp.QuoteMeta("} AND [[test3]] >= {:") +
".+" +
regexp.QuoteMeta("})) AND ([[test3]] <= {:") +
regexp.QuoteMeta("} AND [[test3]] <= {:") +
".+" +
regexp.QuoteMeta("})) AND ({:") +
regexp.QuoteMeta("} AND {:") +
".+" +
regexp.QuoteMeta("} < {:") +
".+" +
Expand All @@ -132,12 +154,12 @@ func TestFilterDataBuildExpr(t *testing.T) {
},
}

for i, s := range scenarios {
for _, s := range scenarios {
expr, err := s.filterData.BuildExpr(resolver)

hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err)
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
continue
}

Expand All @@ -150,7 +172,7 @@ func TestFilterDataBuildExpr(t *testing.T) {

pattern := regexp.MustCompile(s.expectPattern)
if !pattern.MatchString(rawSql) {
t.Errorf("(%d) Pattern %v don't match with expression: \n%v", i, s.expectPattern, rawSql)
t.Errorf("[%s] Pattern %v don't match with expression: \n%v", s.name, s.expectPattern, rawSql)
}
}
}
Loading

0 comments on commit 8815f60

Please sign in to comment.