diff --git a/common/persistence/visibility/store/elasticsearch/converter.go b/common/persistence/visibility/store/elasticsearch/converter.go index 98f7c87bc2e..d539483d0c4 100644 --- a/common/persistence/visibility/store/elasticsearch/converter.go +++ b/common/persistence/visibility/store/elasticsearch/converter.go @@ -17,6 +17,8 @@ var allowedComparisonOperators = map[string]struct{}{ sqlparser.NotInStr: {}, sqlparser.StartsWithStr: {}, sqlparser.NotStartsWithStr: {}, + sqlparser.ContainsStr: {}, + sqlparser.NotContainsStr: {}, } func NewQueryConverter( diff --git a/common/persistence/visibility/store/query/converter.go b/common/persistence/visibility/store/query/converter.go index 4fa0013f747..ed9c844d5af 100644 --- a/common/persistence/visibility/store/query/converter.go +++ b/common/persistence/visibility/store/query/converter.go @@ -494,6 +494,18 @@ func (c *comparisonExprConverter) Convert(expr sqlparser.Expr) (elastic.Query, e return nil, NewConverterError("right-hand side of '%v' must be a string", comparisonExpr.Operator) } query = elastic.NewBoolQuery().MustNot(elastic.NewPrefixQuery(colName, v)) + case sqlparser.ContainsStr: + v, ok := colValues[0].(string) + if !ok { + return nil, NewConverterError("right-hand side of '%v' must be a string", comparisonExpr.Operator) + } + query = elastic.NewWildcardQuery(colName, "*"+v+"*") + case sqlparser.NotContainsStr: + v, ok := colValues[0].(string) + if !ok { + return nil, NewConverterError("right-hand side of '%v' must be a string", comparisonExpr.Operator) + } + query = elastic.NewBoolQuery().MustNot(elastic.NewWildcardQuery(colName, "*"+v+"*")) } return query, nil diff --git a/common/persistence/visibility/store/sql/query_converter.go b/common/persistence/visibility/store/sql/query_converter.go index 3ce15df2784..4151dbb6363 100644 --- a/common/persistence/visibility/store/sql/query_converter.go +++ b/common/persistence/visibility/store/sql/query_converter.go @@ -87,6 +87,8 @@ var ( sqlparser.NotInStr, sqlparser.StartsWithStr, sqlparser.NotStartsWithStr, + sqlparser.ContainsStr, + sqlparser.NotContainsStr, } supportedKeyworkListOperators = []string{ @@ -395,6 +397,23 @@ func (c *QueryConverter) convertComparisonExpr(exprRef *sqlparser.Expr) error { } expr.Escape = defaultLikeEscapeExpr valueExpr.Val = escapeLikeValueForPrefixSearch(valueExpr.Val, defaultLikeEscapeChar) + case sqlparser.ContainsStr, sqlparser.NotContainsStr: + valueExpr, ok := expr.Right.(*unsafeSQLString) + if !ok { + return query.NewConverterError( + "%s: right-hand side of '%s' must be a literal string (got: %v)", + query.InvalidExpressionErrMessage, + expr.Operator, + sqlparser.String(expr.Right), + ) + } + if expr.Operator == sqlparser.ContainsStr { + expr.Operator = sqlparser.LikeStr + } else { + expr.Operator = sqlparser.NotLikeStr + } + expr.Escape = defaultLikeEscapeExpr + valueExpr.Val = escapeLikeValueForSubstringSearch(valueExpr.Val, defaultLikeEscapeChar) } return nil @@ -662,6 +681,19 @@ func escapeLikeValueForPrefixSearch(in string, escape byte) string { return sb.String() } +func escapeLikeValueForSubstringSearch(in string, escape byte) string { + sb := strings.Builder{} + sb.WriteByte('%') + for _, c := range in { + if c == '%' || c == '_' || c == rune(escape) { + sb.WriteByte(escape) + } + sb.WriteRune(c) + } + sb.WriteByte('%') + return sb.String() +} + func isSupportedOperator(supportedOperators []string, operator string) bool { for _, op := range supportedOperators { if operator == op { diff --git a/common/persistence/visibility/store/sql/query_converter_test.go b/common/persistence/visibility/store/sql/query_converter_test.go index bff4f259618..ed326a2d733 100644 --- a/common/persistence/visibility/store/sql/query_converter_test.go +++ b/common/persistence/visibility/store/sql/query_converter_test.go @@ -338,6 +338,44 @@ func (s *queryConverterSuite) TestConvertComparisonExpr() { sqlparser.NotStartsWithStr, ), }, + { + name: "contains expression", + input: "AliasForKeyword01 contains 'substring'", + output: `Keyword01 like '%substring%' escape '!'`, + err: nil, + }, + { + name: "not contains expression", + input: "AliasForKeyword01 not contains 'substring'", + output: `Keyword01 not like '%substring%' escape '!'`, + err: nil, + }, + { + name: "contains expression with special chars", + input: "AliasForKeyword01 contains 'foo_bar%'", + output: `Keyword01 like '%foo!_bar!%%' escape '!'`, + err: nil, + }, + { + name: "contains expression error", + input: "AliasForKeyword01 contains 123", + output: "", + err: query.NewConverterError( + "%s: right-hand side of '%s' must be a literal string (got: 123)", + query.InvalidExpressionErrMessage, + sqlparser.ContainsStr, + ), + }, + { + name: "not contains expression error", + input: "AliasForKeyword01 not contains 123", + output: "", + err: query.NewConverterError( + "%s: right-hand side of '%s' must be a literal string (got: 123)", + query.InvalidExpressionErrMessage, + sqlparser.NotContainsStr, + ), + }, { name: "like expression", input: "AliasForKeyword01 like 'foo%'",