From 8bc56f6fe75cbd1a81aa45e89a0443a6dee7ff6f Mon Sep 17 00:00:00 2001 From: Silvan Date: Tue, 2 Jan 2024 16:27:36 +0100 Subject: [PATCH] fix(query): escape wildcards in text search (#7131) (#7135) * fix(query): escape like wildcards * test: search query wildcards * add do nothing --- .gitignore | 1 + internal/database/database.go | 8 + internal/query/search_query.go | 56 ++-- internal/query/search_query_test.go | 414 +++++++++++++++++++++++-- internal/view/repository/query.go | 6 + internal/view/repository/query_test.go | 302 ++++++++++++++++++ 6 files changed, 736 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 8b8a107f0..fe02b30b6 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,7 @@ migrations/cockroach/migrate_cloud.go !/.artifacts/zitadel /zitadel node_modules/ +.kreya go.work go.work.sum diff --git a/internal/database/database.go b/internal/database/database.go index 30e03150c..cd72d6d24 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "reflect" + "strings" "github.com/mitchellh/mapstructure" "github.com/zitadel/logging" @@ -84,6 +85,7 @@ func (db *DB) QueryRowContext(ctx context.Context, scan func(row *sql.Row) error }() row := tx.QueryRowContext(ctx, query, args...) + logging.OnError(row.Err()).Error("unexpected query error") err = scan(row) if err != nil { @@ -170,3 +172,9 @@ func (c Config) Password() string { func (c Config) Type() string { return c.connector.Type() } + +func EscapeLikeWildcards(value string) string { + value = strings.ReplaceAll(value, "%", "\\%") + value = strings.ReplaceAll(value, "_", "\\_") + return value +} diff --git a/internal/query/search_query.go b/internal/query/search_query.go index c92f155d2..b4944a8f2 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -8,6 +8,7 @@ import ( sq "github.com/Masterminds/squirrel" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" ) @@ -255,7 +256,7 @@ func NewInTextQuery(col Column, values []string) (*InTextQuery, error) { }, nil } -type TextQuery struct { +type textQuery struct { Column Column Text string Compare TextComparison @@ -269,21 +270,38 @@ var ( ErrEmptyValues = errors.New("values array must not be empty") ) -func NewTextQuery(col Column, value string, compare TextComparison) (*TextQuery, error) { +func NewTextQuery(col Column, value string, compare TextComparison) (*textQuery, error) { if compare < 0 || compare >= textCompareMax { return nil, ErrInvalidCompare } if col.isZero() { return nil, ErrMissingColumn } - return &TextQuery{ + // handle the comparisons which use (i)like and therefore need to escape potential wildcards in the value + switch compare { + case TextEqualsIgnoreCase, + TextStartsWith, + TextStartsWithIgnoreCase, + TextEndsWith, + TextEndsWithIgnoreCase, + TextContains, + TextContainsIgnoreCase: + value = database.EscapeLikeWildcards(value) + case TextEquals, + TextListContains, + TextNotEquals, + textCompareMax: + // do nothing + } + + return &textQuery{ Column: col, Text: value, Compare: compare, }, nil } -func (q *TextQuery) Col() Column { +func (q *textQuery) Col() Column { return q.Column } @@ -296,11 +314,11 @@ func (q *InTextQuery) comp() sq.Sqlizer { return sq.Eq{q.Column.identifier(): q.Values} } -func (q *TextQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { +func (q *textQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query.Where(q.comp()) } -func (q *TextQuery) comp() sq.Sqlizer { +func (q *textQuery) comp() sq.Sqlizer { switch q.Compare { case TextEquals: return sq.Eq{q.Column.identifier(): q.Text} @@ -346,32 +364,6 @@ const ( textCompareMax ) -// Deprecated: Use TextComparison, will be removed as soon as all calls are changed to query -func TextComparisonFromMethod(m domain.SearchMethod) TextComparison { - switch m { - case domain.SearchMethodEquals: - return TextEquals - case domain.SearchMethodEqualsIgnoreCase: - return TextEqualsIgnoreCase - case domain.SearchMethodStartsWith: - return TextStartsWith - case domain.SearchMethodStartsWithIgnoreCase: - return TextStartsWithIgnoreCase - case domain.SearchMethodContains: - return TextContains - case domain.SearchMethodContainsIgnoreCase: - return TextContainsIgnoreCase - case domain.SearchMethodEndsWith: - return TextEndsWith - case domain.SearchMethodEndsWithIgnoreCase: - return TextEndsWithIgnoreCase - case domain.SearchMethodListContains: - return TextListContains - default: - return textCompareMax - } -} - type NumberQuery struct { Column Column Number interface{} diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index a2c1fb123..c64b2c131 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -191,7 +191,7 @@ func TestNewSubSelect(t *testing.T) { name: "no column 1", args: args{ column: Column{}, - queries: []SearchQuery{&TextQuery{testCol, "horst", TextEquals}}, + queries: []SearchQuery{&textQuery{testCol, "horst", TextEquals}}, }, wantErr: func(err error) bool { return errors.Is(err, ErrMissingColumn) @@ -201,7 +201,7 @@ func TestNewSubSelect(t *testing.T) { name: "no column name 1", args: args{ column: testNoCol, - queries: []SearchQuery{&TextQuery{testCol, "horst", TextEquals}}, + queries: []SearchQuery{&textQuery{testCol, "horst", TextEquals}}, }, wantErr: func(err error) bool { return errors.Is(err, ErrMissingColumn) @@ -211,22 +211,22 @@ func TestNewSubSelect(t *testing.T) { name: "correct 1", args: args{ column: testCol, - queries: []SearchQuery{&TextQuery{testCol, "horst", TextEquals}}, + queries: []SearchQuery{&textQuery{testCol, "horst", TextEquals}}, }, want: &SubSelect{ Column: testCol, - Queries: []SearchQuery{&TextQuery{testCol, "horst", TextEquals}}, + Queries: []SearchQuery{&textQuery{testCol, "horst", TextEquals}}, }, }, { name: "correct 3", args: args{ column: testCol, - queries: []SearchQuery{&TextQuery{testCol, "horst1", TextEquals}, &TextQuery{testCol, "horst2", TextEquals}, &TextQuery{testCol, "horst3", TextEquals}}, + queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}, &textQuery{testCol, "horst2", TextEquals}, &textQuery{testCol, "horst3", TextEquals}}, }, want: &SubSelect{ Column: testCol, - Queries: []SearchQuery{&TextQuery{testCol, "horst1", TextEquals}, &TextQuery{testCol, "horst2", TextEquals}, &TextQuery{testCol, "horst3", TextEquals}}, + Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}, &textQuery{testCol, "horst2", TextEquals}, &textQuery{testCol, "horst3", TextEquals}}, }, }, } @@ -275,7 +275,7 @@ func TestSubSelect_comp(t *testing.T) { name: "queries 1", fields: fields{ Column: testCol, - Queries: []SearchQuery{&TextQuery{testCol, "horst", TextEquals}}, + Queries: []SearchQuery{&textQuery{testCol, "horst", TextEquals}}, }, want: want{ query: sq.Select("test_table.test_col").From("test_table").Where(sq.Eq{"test_table.test_col": interface{}("horst")}), @@ -285,7 +285,7 @@ func TestSubSelect_comp(t *testing.T) { name: "queries 1 with alias", fields: fields{ Column: testColAlias, - Queries: []SearchQuery{&TextQuery{testColAlias, "horst", TextEquals}}, + Queries: []SearchQuery{&textQuery{testColAlias, "horst", TextEquals}}, }, want: want{ query: sq.Select("test_alias.test_col").From("test_table AS test_alias").Where(sq.Eq{"test_alias.test_col": interface{}("horst")}), @@ -295,7 +295,7 @@ func TestSubSelect_comp(t *testing.T) { name: "queries 3", fields: fields{ Column: testCol, - Queries: []SearchQuery{&TextQuery{testCol, "horst1", TextEquals}, &TextQuery{testCol, "horst2", TextEquals}, &TextQuery{testCol, "horst3", TextEquals}}, + Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}, &textQuery{testCol, "horst2", TextEquals}, &textQuery{testCol, "horst3", TextEquals}}, }, want: want{ query: sq.Select("test_table.test_col").From("test_table").From("test_table").Where(sq.Eq{"test_table.test_col": "horst1"}).From("test_table").Where(sq.Eq{"test_table.test_col": "horst2"}).From("test_table").Where(sq.Eq{"test_table.test_col": "horst3"}), @@ -585,12 +585,12 @@ func TestNewListQuery(t *testing.T) { name: "correct", args: args{ column: testCol, - data: &SubSelect{Column: testCol, Queries: []SearchQuery{&TextQuery{testCol, "horst1", TextEquals}}}, + data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}}, compare: ListIn, }, want: &ListQuery{ Column: testCol, - Data: &SubSelect{Column: testCol, Queries: []SearchQuery{&TextQuery{testCol, "horst1", TextEquals}}}, + Data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}}, Compare: ListIn, }, }, @@ -697,7 +697,7 @@ func TestListQuery_comp(t *testing.T) { name: "in subquery text", fields: fields{ Column: testCol, - Data: &SubSelect{Column: testCol, Queries: []SearchQuery{&TextQuery{testCol, "horst", TextEquals}}}, + Data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst", TextEquals}}}, Compare: ListIn, }, want: want{ @@ -779,7 +779,7 @@ func TestNewTextQuery(t *testing.T) { tests := []struct { name string args args - want *TextQuery + want *textQuery wantErr func(error) bool }{ { @@ -827,18 +827,317 @@ func TestNewTextQuery(t *testing.T) { }, }, { - name: "correct", + name: "equals", args: args{ column: testCol, value: "hurst", compare: TextEquals, }, - want: &TextQuery{ + want: &textQuery{ Column: testCol, Text: "hurst", Compare: TextEquals, }, }, + { + name: "equals ignore case", + args: args{ + column: testCol, + value: "hurst", + compare: TextEqualsIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextEqualsIgnoreCase, + }, + }, + { + name: "equals ignore case % wildcard", + args: args{ + column: testCol, + value: "hu%rst", + compare: TextEqualsIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hu\\%rst", + Compare: TextEqualsIgnoreCase, + }, + }, + { + name: "equals ignore case _ wildcard", + args: args{ + column: testCol, + value: "hu_rst", + compare: TextEqualsIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hu\\_rst", + Compare: TextEqualsIgnoreCase, + }, + }, + { + name: "equals ignore case _, % wildcards", + args: args{ + column: testCol, + value: "h_urst%", + compare: TextEqualsIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "h\\_urst\\%", + Compare: TextEqualsIgnoreCase, + }, + }, + { + name: "not equal", + args: args{ + column: testCol, + value: "hurst", + compare: TextNotEquals, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextNotEquals, + }, + }, + { + name: "starts with", + args: args{ + column: testCol, + value: "hurst", + compare: TextStartsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextStartsWith, + }, + }, + { + name: "starts with _ wildcard", + args: args{ + column: testCol, + value: "_hurst", + compare: TextStartsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "\\_hurst", + Compare: TextStartsWith, + }, + }, + { + name: "starts with % wildcard", + args: args{ + column: testCol, + value: "hurst%", + compare: TextStartsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst\\%", + Compare: TextStartsWith, + }, + }, + { + name: "starts with %, % wildcard", + args: args{ + column: testCol, + value: "hu%%rst", + compare: TextStartsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "hu\\%\\%rst", + Compare: TextStartsWith, + }, + }, + { + name: "starts with ignore case", + args: args{ + column: testCol, + value: "hurst", + compare: TextStartsWithIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextStartsWithIgnoreCase, + }, + }, + { + name: "starts with ignore case _ wildcard", + args: args{ + column: testCol, + value: "hur_st", + compare: TextStartsWithIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hur\\_st", + Compare: TextStartsWithIgnoreCase, + }, + }, + { + name: "starts with ignore case % wildcard", + args: args{ + column: testCol, + value: "hurst%", + compare: TextStartsWithIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst\\%", + Compare: TextStartsWithIgnoreCase, + }, + }, + { + name: "starts with ignore case _, _ wildcard", + args: args{ + column: testCol, + value: "h_r_t", + compare: TextStartsWithIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "h\\_r\\_t", + Compare: TextStartsWithIgnoreCase, + }, + }, + { + name: "ends with", + args: args{ + column: testCol, + value: "hurst", + compare: TextEndsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextEndsWith, + }, + }, + { + name: "ends with % wildcard", + args: args{ + column: testCol, + value: "%hurst", + compare: TextEndsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "\\%hurst", + Compare: TextEndsWith, + }, + }, + { + name: "ends with _ wildcard", + args: args{ + column: testCol, + value: "hurst_", + compare: TextEndsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst\\_", + Compare: TextEndsWith, + }, + }, + { + name: "ends with _, % wildcard", + args: args{ + column: testCol, + value: "hurst_%", + compare: TextEndsWith, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst\\_\\%", + Compare: TextEndsWith, + }, + }, + { + name: "ends with ignore case", + args: args{ + column: testCol, + value: "hurst", + compare: TextEndsWithIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextEndsWithIgnoreCase, + }, + }, + { + name: "ends with ignore case _, %, _ wildcards", + args: args{ + column: testCol, + value: "h_r_t%", + compare: TextEndsWithIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "h\\_r\\_t\\%", + Compare: TextEndsWithIgnoreCase, + }, + }, + { + name: "contains", + args: args{ + column: testCol, + value: "hurst", + compare: TextContains, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextContains, + }, + }, + { + name: "contains % wildcard", + args: args{ + column: testCol, + value: "%", + compare: TextContains, + }, + want: &textQuery{ + Column: testCol, + Text: "\\%", + Compare: TextContains, + }, + }, + { + name: "contains ignore csae", + args: args{ + column: testCol, + value: "hurst", + compare: TextContainsIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hurst", + Compare: TextContainsIgnoreCase, + }, + }, + { + name: "contains ignore csae _ wildcard", + args: args{ + column: testCol, + value: "hurs_", + compare: TextContainsIgnoreCase, + }, + want: &textQuery{ + Column: testCol, + Text: "hurs\\_", + Compare: TextContainsIgnoreCase, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -894,6 +1193,17 @@ func TestTextQuery_comp(t *testing.T) { query: sq.ILike{"test_table.test_col": "Hurst"}, }, }, + { + name: "equals ignore case wildcard", + fields: fields{ + Column: testCol, + Text: "Hu%%rst", + Compare: TextEqualsIgnoreCase, + }, + want: want{ + query: sq.ILike{"test_table.test_col": "Hu\\%\\%rst"}, + }, + }, { name: "starts with", fields: fields{ @@ -905,6 +1215,17 @@ func TestTextQuery_comp(t *testing.T) { query: sq.Like{"test_table.test_col": "Hurst%"}, }, }, + { + name: "starts with wildcards", + fields: fields{ + Column: testCol, + Text: "_Hurst%", + Compare: TextStartsWith, + }, + want: want{ + query: sq.Like{"test_table.test_col": "\\_Hurst\\%%"}, + }, + }, { name: "starts with ignore case", fields: fields{ @@ -916,6 +1237,17 @@ func TestTextQuery_comp(t *testing.T) { query: sq.ILike{"test_table.test_col": "Hurst%"}, }, }, + { + name: "starts with ignore case wildcards", + fields: fields{ + Column: testCol, + Text: "Hurst%", + Compare: TextStartsWithIgnoreCase, + }, + want: want{ + query: sq.ILike{"test_table.test_col": "Hurst\\%%"}, + }, + }, { name: "ends with", fields: fields{ @@ -927,6 +1259,17 @@ func TestTextQuery_comp(t *testing.T) { query: sq.Like{"test_table.test_col": "%Hurst"}, }, }, + { + name: "ends with wildcards", + fields: fields{ + Column: testCol, + Text: "Hurst%", + Compare: TextEndsWith, + }, + want: want{ + query: sq.Like{"test_table.test_col": "%Hurst\\%"}, + }, + }, { name: "ends with ignore case", fields: fields{ @@ -938,6 +1281,17 @@ func TestTextQuery_comp(t *testing.T) { query: sq.ILike{"test_table.test_col": "%Hurst"}, }, }, + { + name: "ends with ignore case wildcards", + fields: fields{ + Column: testCol, + Text: "%Hurst", + Compare: TextEndsWithIgnoreCase, + }, + want: want{ + query: sq.ILike{"test_table.test_col": "%\\%Hurst"}, + }, + }, { name: "contains", fields: fields{ @@ -949,6 +1303,17 @@ func TestTextQuery_comp(t *testing.T) { query: sq.Like{"test_table.test_col": "%Hurst%"}, }, }, + { + name: "contains wildcards", + fields: fields{ + Column: testCol, + Text: "Hu%rst%", + Compare: TextContains, + }, + want: want{ + query: sq.Like{"test_table.test_col": "%Hu\\%rst\\%%"}, + }, + }, { name: "containts ignore case", fields: fields{ @@ -960,6 +1325,17 @@ func TestTextQuery_comp(t *testing.T) { query: sq.ILike{"test_table.test_col": "%Hurst%"}, }, }, + { + name: "contains ignore case wildcards", + fields: fields{ + Column: testCol, + Text: "%Hurst%", + Compare: TextContainsIgnoreCase, + }, + want: want{ + query: sq.ILike{"test_table.test_col": "%\\%Hurst\\%%"}, + }, + }, { name: "list containts", fields: fields{ @@ -999,10 +1375,10 @@ func TestTextQuery_comp(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := &TextQuery{ - Column: tt.fields.Column, - Text: tt.fields.Text, - Compare: tt.fields.Compare, + s, _ := NewTextQuery(tt.fields.Column, tt.fields.Text, tt.fields.Compare) + if s == nil { + // used to check correct behavior of comp + s = &textQuery{Column: tt.fields.Column, Text: tt.fields.Text, Compare: tt.fields.Compare} } query := s.comp() if query == nil && tt.want.isNil { diff --git a/internal/view/repository/query.go b/internal/view/repository/query.go index b7800fed2..4b931b7d5 100644 --- a/internal/view/repository/query.go +++ b/internal/view/repository/query.go @@ -95,36 +95,42 @@ func SetQuery(query *gorm.DB, key ColumnKey, value interface{}, method domain.Se if !ok { return nil, zerrors.ThrowInvalidArgument(nil, "VIEW-SLj7s", "Starts with only possible for strings") } + valueText = database.EscapeLikeWildcards(valueText) query = query.Where(column+" LIKE ?", valueText+"%") case domain.SearchMethodStartsWithIgnoreCase: valueText, ok := value.(string) if !ok { return nil, zerrors.ThrowInvalidArgument(nil, "VIEW-eidus", "Starts with ignore case only possible for strings") } + valueText = database.EscapeLikeWildcards(valueText) query = query.Where("LOWER("+column+") LIKE LOWER(?)", valueText+"%") case domain.SearchMethodEndsWith: valueText, ok := value.(string) if !ok { return nil, zerrors.ThrowInvalidArgument(nil, "VIEW-Hswd3", "Ends with only possible for strings") } + valueText = database.EscapeLikeWildcards(valueText) query = query.Where(column+" LIKE ?", "%"+valueText) case domain.SearchMethodEndsWithIgnoreCase: valueText, ok := value.(string) if !ok { return nil, zerrors.ThrowInvalidArgument(nil, "VIEW-dAG31", "Ends with ignore case only possible for strings") } + valueText = database.EscapeLikeWildcards(valueText) query = query.Where("LOWER("+column+") LIKE LOWER(?)", "%"+valueText) case domain.SearchMethodContains: valueText, ok := value.(string) if !ok { return nil, zerrors.ThrowInvalidArgument(nil, "VIEW-3ids", "Contains with only possible for strings") } + valueText = database.EscapeLikeWildcards(valueText) query = query.Where(column+" LIKE ?", "%"+valueText+"%") case domain.SearchMethodContainsIgnoreCase: valueText, ok := value.(string) if !ok { return nil, zerrors.ThrowInvalidArgument(nil, "VIEW-eid73", "Contains with ignore case only possible for strings") } + valueText = database.EscapeLikeWildcards(valueText) query = query.Where("LOWER("+column+") LIKE LOWER(?)", "%"+valueText+"%") case domain.SearchMethodNotEquals: query = query.Where(""+column+" <> ?", value) diff --git a/internal/view/repository/query_test.go b/internal/view/repository/query_test.go index dd068d2b8..49cd961b1 100644 --- a/internal/view/repository/query_test.go +++ b/internal/view/repository/query_test.go @@ -1,6 +1,7 @@ package repository import ( + "reflect" "testing" "github.com/jinzhu/gorm" @@ -155,3 +156,304 @@ func TestPrepareSearchQuery(t *testing.T) { }) } } + +func TestSetQuery(t *testing.T) { + query := mockDB(t).db.Select("test_field").Table("test_table") + exprPrefix := `(SELECT test_field FROM "test_table" WHERE ` + type args struct { + key ColumnKey + value interface{} + method domain.SearchMethod + } + type want struct { + isErr func(t *testing.T, got error) + query *gorm.SqlExpr + } + tests := []struct { + name string + args args + want want + }{ + { + name: "contains", + args: args{ + key: TestSearchKey_TEST, + value: "asdf", + method: domain.SearchMethodContains, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%asdf%"), + }, + }, + { + name: "contains _ wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as_df", + method: domain.SearchMethodContains, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%as\\_df%"), + }, + }, + { + name: "contains % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as%df", + method: domain.SearchMethodContains, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%as\\%df%"), + }, + }, + { + name: "contains % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "a_s%d_f", + method: domain.SearchMethodContains, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%a\\_s\\%d\\_f%"), + }, + }, + { + name: "starts with", + args: args{ + key: TestSearchKey_TEST, + value: "asdf", + method: domain.SearchMethodStartsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "asdf%"), + }, + }, + { + name: "starts with _ wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as_df", + method: domain.SearchMethodStartsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "as\\_df%"), + }, + }, + { + name: "starts with % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as%df", + method: domain.SearchMethodStartsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "as\\%df%"), + }, + }, + { + name: "starts with % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "a_s%d_f", + method: domain.SearchMethodStartsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "a\\_s\\%d\\_f%"), + }, + }, + { + name: "ends with", + args: args{ + key: TestSearchKey_TEST, + value: "asdf", + method: domain.SearchMethodEndsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%asdf"), + }, + }, + { + name: "ends with _ wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as_df", + method: domain.SearchMethodEndsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%as\\_df"), + }, + }, + { + name: "ends with % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as%df", + method: domain.SearchMethodEndsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%as\\%df"), + }, + }, + { + name: "ends with % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "a_s%d_f", + method: domain.SearchMethodEndsWith, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(test LIKE ?))", "%a\\_s\\%d\\_f"), + }, + }, + { + name: "starts with ignore case", + args: args{ + key: TestSearchKey_TEST, + value: "asdf", + method: domain.SearchMethodStartsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "asdf%"), + }, + }, + { + name: "starts with ignore case _ wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as_df", + method: domain.SearchMethodStartsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "as\\_df%"), + }, + }, + { + name: "starts with ignore case % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as%df", + method: domain.SearchMethodStartsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "as\\%df%"), + }, + }, + { + name: "starts with ignore case % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "a_s%d_f", + method: domain.SearchMethodStartsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "a\\_s\\%d\\_f%"), + }, + }, + { + name: "ends with ignore case", + args: args{ + key: TestSearchKey_TEST, + value: "asdf", + method: domain.SearchMethodEndsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%asdf"), + }, + }, + { + name: "ends with ignore case _ wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as_df", + method: domain.SearchMethodEndsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%as\\_df"), + }, + }, + { + name: "ends with ignore case % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as%df", + method: domain.SearchMethodEndsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%as\\%df"), + }, + }, + { + name: "ends with ignore case % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "a_s%d_f", + method: domain.SearchMethodEndsWithIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%a\\_s\\%d\\_f"), + }, + }, + { + name: "contains ignore case", + args: args{ + key: TestSearchKey_TEST, + value: "asdf", + method: domain.SearchMethodContainsIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%asdf%"), + }, + }, + { + name: "contains ignore case _ wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as_df", + method: domain.SearchMethodContainsIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%as\\_df%"), + }, + }, + { + name: "contains ignore case % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "as%df", + method: domain.SearchMethodContainsIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%as\\%df%"), + }, + }, + { + name: "contains ignore case % wildcard", + args: args{ + key: TestSearchKey_TEST, + value: "a_s%d_f", + method: domain.SearchMethodContainsIgnoreCase, + }, + want: want{ + query: gorm.Expr(exprPrefix+"(LOWER(test) LIKE LOWER(?)))", "%a\\_s\\%d\\_f%"), + }, + }, + } + for _, tt := range tests { + if tt.want.isErr == nil { + tt.want.isErr = func(t *testing.T, got error) { + if got == nil { + return + } + t.Errorf("no error expected got: %v", got) + } + } + t.Run(tt.name, func(t *testing.T) { + got, err := SetQuery(query, tt.args.key, tt.args.value, tt.args.method) + tt.want.isErr(t, err) + if !reflect.DeepEqual(got.SubQuery(), tt.want.query) { + t.Errorf("unexpected query: \nwant: %v\n got: %v", *tt.want.query, *got.SubQuery()) + } + }) + } +}