go.temporal.io/server@v1.23.0/common/persistence/visibility/store/sql/query_converter.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package sql
    26  
    27  import (
    28  	"errors"
    29  	"fmt"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/temporalio/sqlparser"
    35  
    36  	enumspb "go.temporal.io/api/enums/v1"
    37  	"go.temporal.io/server/common/namespace"
    38  	"go.temporal.io/server/common/persistence/sql/sqlplugin"
    39  	"go.temporal.io/server/common/persistence/visibility/store/query"
    40  	"go.temporal.io/server/common/searchattribute"
    41  )
    42  
    43  type (
    44  	pluginQueryConverter interface {
    45  		convertKeywordListComparisonExpr(expr *sqlparser.ComparisonExpr) (sqlparser.Expr, error)
    46  
    47  		convertTextComparisonExpr(expr *sqlparser.ComparisonExpr) (sqlparser.Expr, error)
    48  
    49  		buildSelectStmt(
    50  			namespaceID namespace.ID,
    51  			queryString string,
    52  			pageSize int,
    53  			token *pageToken,
    54  		) (string, []any)
    55  
    56  		buildCountStmt(namespaceID namespace.ID, queryString string, groupBy []string) (string, []any)
    57  
    58  		getDatetimeFormat() string
    59  
    60  		getCoalesceCloseTimeExpr() sqlparser.Expr
    61  	}
    62  
    63  	QueryConverter struct {
    64  		pluginQueryConverter
    65  		namespaceName namespace.Name
    66  		namespaceID   namespace.ID
    67  		saTypeMap     searchattribute.NameTypeMap
    68  		saMapper      searchattribute.Mapper
    69  		queryString   string
    70  
    71  		seenNamespaceDivision bool
    72  	}
    73  
    74  	queryParams struct {
    75  		queryString string
    76  		// List of search attributes to group by (field name, not db name).
    77  		groupBy []string
    78  	}
    79  )
    80  
    81  const (
    82  	// Default escape char is set explicitly to '!' for two reasons:
    83  	// 1. SQLite doesn't have a default escape char;
    84  	// 2. MySQL requires to escape the backslack char unlike SQLite and PostgreSQL.
    85  	// Thus, in order to avoid having specific code for each DB, it's better to
    86  	// set the escape char to a simpler char that doesn't require escaping.
    87  	defaultLikeEscapeChar = '!'
    88  )
    89  
    90  var (
    91  	// strings.Replacer takes a sequence of old to new replacements
    92  	escapeCharMap = []string{
    93  		"'", "''",
    94  		"\"", "\\\"",
    95  		"\b", "\\b",
    96  		"\n", "\\n",
    97  		"\r", "\\r",
    98  		"\t", "\\t",
    99  		"\\", "\\\\",
   100  	}
   101  
   102  	supportedComparisonOperators = []string{
   103  		sqlparser.EqualStr,
   104  		sqlparser.NotEqualStr,
   105  		sqlparser.LessThanStr,
   106  		sqlparser.GreaterThanStr,
   107  		sqlparser.LessEqualStr,
   108  		sqlparser.GreaterEqualStr,
   109  		sqlparser.InStr,
   110  		sqlparser.NotInStr,
   111  		sqlparser.StartsWithStr,
   112  		sqlparser.NotStartsWithStr,
   113  	}
   114  
   115  	supportedKeyworkListOperators = []string{
   116  		sqlparser.EqualStr,
   117  		sqlparser.NotEqualStr,
   118  		sqlparser.InStr,
   119  		sqlparser.NotInStr,
   120  	}
   121  
   122  	supportedTextOperators = []string{
   123  		sqlparser.EqualStr,
   124  		sqlparser.NotEqualStr,
   125  	}
   126  
   127  	supportedTypesRangeCond = []enumspb.IndexedValueType{
   128  		enumspb.INDEXED_VALUE_TYPE_DATETIME,
   129  		enumspb.INDEXED_VALUE_TYPE_DOUBLE,
   130  		enumspb.INDEXED_VALUE_TYPE_INT,
   131  		enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   132  	}
   133  
   134  	defaultLikeEscapeExpr = newUnsafeSQLString(string(defaultLikeEscapeChar))
   135  )
   136  
   137  func newQueryConverterInternal(
   138  	pqc pluginQueryConverter,
   139  	namespaceName namespace.Name,
   140  	namespaceID namespace.ID,
   141  	saTypeMap searchattribute.NameTypeMap,
   142  	saMapper searchattribute.Mapper,
   143  	queryString string,
   144  ) *QueryConverter {
   145  	return &QueryConverter{
   146  		pluginQueryConverter: pqc,
   147  		namespaceName:        namespaceName,
   148  		namespaceID:          namespaceID,
   149  		saTypeMap:            saTypeMap,
   150  		saMapper:             saMapper,
   151  		queryString:          queryString,
   152  
   153  		seenNamespaceDivision: false,
   154  	}
   155  }
   156  
   157  func (c *QueryConverter) BuildSelectStmt(
   158  	pageSize int,
   159  	nextPageToken []byte,
   160  ) (*sqlplugin.VisibilitySelectFilter, error) {
   161  	token, err := deserializePageToken(nextPageToken)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	qp, err := c.convertWhereString(c.queryString)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	if len(qp.groupBy) > 0 {
   170  		return nil, query.NewConverterError("%s: 'group by' clause", query.NotSupportedErrMessage)
   171  	}
   172  	queryString, queryArgs := c.buildSelectStmt(
   173  		c.namespaceID,
   174  		qp.queryString,
   175  		pageSize,
   176  		token,
   177  	)
   178  	return &sqlplugin.VisibilitySelectFilter{Query: queryString, QueryArgs: queryArgs}, nil
   179  }
   180  
   181  func (c *QueryConverter) BuildCountStmt() (*sqlplugin.VisibilitySelectFilter, error) {
   182  	qp, err := c.convertWhereString(c.queryString)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	groupByDbNames := make([]string, len(qp.groupBy))
   187  	for i, fieldName := range qp.groupBy {
   188  		groupByDbNames[i] = searchattribute.GetSqlDbColName(fieldName)
   189  	}
   190  	queryString, queryArgs := c.buildCountStmt(c.namespaceID, qp.queryString, groupByDbNames)
   191  	return &sqlplugin.VisibilitySelectFilter{
   192  		Query:     queryString,
   193  		QueryArgs: queryArgs,
   194  		GroupBy:   qp.groupBy,
   195  	}, nil
   196  }
   197  
   198  func (c *QueryConverter) convertWhereString(queryString string) (*queryParams, error) {
   199  	where := strings.TrimSpace(queryString)
   200  	if where != "" &&
   201  		!strings.HasPrefix(strings.ToLower(where), "order by") &&
   202  		!strings.HasPrefix(strings.ToLower(where), "group by") {
   203  		where = "where " + where
   204  	}
   205  	// sqlparser can't parse just WHERE clause but instead accepts only valid SQL statement.
   206  	sql := "select * from table1 " + where
   207  	stmt, err := sqlparser.Parse(sql)
   208  	if err != nil {
   209  		return nil, query.NewConverterError("%s: %v", query.MalformedSqlQueryErrMessage, err)
   210  	}
   211  
   212  	selectStmt, _ := stmt.(*sqlparser.Select)
   213  	err = c.convertSelectStmt(selectStmt)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	res := &queryParams{}
   219  	if selectStmt.Where != nil {
   220  		res.queryString = sqlparser.String(selectStmt.Where.Expr)
   221  	}
   222  	for _, groupByExpr := range selectStmt.GroupBy {
   223  		// The parser already ensures the type is saColName.
   224  		colName := groupByExpr.(*saColName)
   225  		res.groupBy = append(res.groupBy, colName.fieldName)
   226  	}
   227  	return res, nil
   228  }
   229  
   230  func (c *QueryConverter) convertSelectStmt(sel *sqlparser.Select) error {
   231  	if sel.OrderBy != nil {
   232  		return query.NewConverterError("%s: 'order by' clause", query.NotSupportedErrMessage)
   233  	}
   234  
   235  	if sel.Limit != nil {
   236  		return query.NewConverterError("%s: 'limit' clause", query.NotSupportedErrMessage)
   237  	}
   238  
   239  	if sel.Where == nil {
   240  		sel.Where = &sqlparser.Where{
   241  			Type: sqlparser.WhereStr,
   242  			Expr: nil,
   243  		}
   244  	}
   245  
   246  	if sel.Where.Expr != nil {
   247  		err := c.convertWhereExpr(&sel.Where.Expr)
   248  		if err != nil {
   249  			return err
   250  		}
   251  
   252  		// Wrap user's query in parenthesis. This is to ensure that further changes
   253  		// to the query won't affect the user's query.
   254  		switch sel.Where.Expr.(type) {
   255  		case *sqlparser.ParenExpr:
   256  			// no-op: top-level expression is already a parenthesis
   257  		default:
   258  			sel.Where.Expr = &sqlparser.ParenExpr{
   259  				Expr: sel.Where.Expr,
   260  			}
   261  		}
   262  	}
   263  
   264  	// This logic comes from elasticsearch/visibility_store.go#convertQuery function.
   265  	// If the query did not explicitly filter on TemporalNamespaceDivision,
   266  	// then add "is null" query to it.
   267  	if !c.seenNamespaceDivision {
   268  		namespaceDivisionExpr := &sqlparser.IsExpr{
   269  			Operator: sqlparser.IsNullStr,
   270  			Expr: newColName(
   271  				searchattribute.GetSqlDbColName(searchattribute.TemporalNamespaceDivision),
   272  			),
   273  		}
   274  		if sel.Where.Expr == nil {
   275  			sel.Where.Expr = namespaceDivisionExpr
   276  		} else {
   277  			sel.Where.Expr = &sqlparser.AndExpr{
   278  				Left:  sel.Where.Expr,
   279  				Right: namespaceDivisionExpr,
   280  			}
   281  		}
   282  	}
   283  
   284  	if len(sel.GroupBy) > 1 {
   285  		return query.NewConverterError(
   286  			"%s: 'group by' clause supports only a single field",
   287  			query.NotSupportedErrMessage,
   288  		)
   289  	}
   290  	for k := range sel.GroupBy {
   291  		colName, err := c.convertColName(&sel.GroupBy[k])
   292  		if err != nil {
   293  			return err
   294  		}
   295  		if colName.fieldName != searchattribute.ExecutionStatus {
   296  			return query.NewConverterError(
   297  				"%s: 'group by' clause is only supported for %s search attribute",
   298  				query.NotSupportedErrMessage,
   299  				searchattribute.ExecutionStatus,
   300  			)
   301  		}
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  func (c *QueryConverter) convertWhereExpr(expr *sqlparser.Expr) error {
   308  	if expr == nil || *expr == nil {
   309  		return errors.New("cannot be nil")
   310  	}
   311  
   312  	switch e := (*expr).(type) {
   313  	case *sqlparser.ParenExpr:
   314  		return c.convertWhereExpr(&e.Expr)
   315  	case *sqlparser.NotExpr:
   316  		return c.convertWhereExpr(&e.Expr)
   317  	case *sqlparser.AndExpr:
   318  		return c.convertAndExpr(expr)
   319  	case *sqlparser.OrExpr:
   320  		return c.convertOrExpr(expr)
   321  	case *sqlparser.ComparisonExpr:
   322  		return c.convertComparisonExpr(expr)
   323  	case *sqlparser.RangeCond:
   324  		return c.convertRangeCond(expr)
   325  	case *sqlparser.IsExpr:
   326  		return c.convertIsExpr(expr)
   327  	case *sqlparser.FuncExpr:
   328  		return query.NewConverterError("%s: function expression", query.NotSupportedErrMessage)
   329  	case *sqlparser.ColName:
   330  		return query.NewConverterError("%s: incomplete expression", query.InvalidExpressionErrMessage)
   331  	default:
   332  		return query.NewConverterError("%s: expression of type %T", query.NotSupportedErrMessage, e)
   333  	}
   334  }
   335  
   336  func (c *QueryConverter) convertAndExpr(exprRef *sqlparser.Expr) error {
   337  	expr, ok := (*exprRef).(*sqlparser.AndExpr)
   338  	if !ok {
   339  		return query.NewConverterError("`%s` is not an 'AND' expression", sqlparser.String(*exprRef))
   340  	}
   341  	err := c.convertWhereExpr(&expr.Left)
   342  	if err != nil {
   343  		return err
   344  	}
   345  	return c.convertWhereExpr(&expr.Right)
   346  }
   347  
   348  func (c *QueryConverter) convertOrExpr(exprRef *sqlparser.Expr) error {
   349  	expr, ok := (*exprRef).(*sqlparser.OrExpr)
   350  	if !ok {
   351  		return query.NewConverterError("`%s` is not an 'OR' expression", sqlparser.String(*exprRef))
   352  	}
   353  	err := c.convertWhereExpr(&expr.Left)
   354  	if err != nil {
   355  		return err
   356  	}
   357  	return c.convertWhereExpr(&expr.Right)
   358  }
   359  
   360  func (c *QueryConverter) convertComparisonExpr(exprRef *sqlparser.Expr) error {
   361  	expr, ok := (*exprRef).(*sqlparser.ComparisonExpr)
   362  	if !ok {
   363  		return query.NewConverterError(
   364  			"`%s` is not a comparison expression",
   365  			sqlparser.String(*exprRef),
   366  		)
   367  	}
   368  
   369  	if !isSupportedComparisonOperator(expr.Operator) {
   370  		return query.NewConverterError(
   371  			"%s: invalid operator '%s' in `%s`",
   372  			query.InvalidExpressionErrMessage,
   373  			expr.Operator,
   374  			sqlparser.String(expr),
   375  		)
   376  	}
   377  
   378  	saColNameExpr, err := c.convertColName(&expr.Left)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	err = c.convertValueExpr(&expr.Right, saColNameExpr.alias, saColNameExpr.valueType)
   384  	if err != nil {
   385  		return err
   386  	}
   387  
   388  	switch saColNameExpr.valueType {
   389  	case enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST:
   390  		newExpr, err := c.convertKeywordListComparisonExpr(expr)
   391  		if err != nil {
   392  			return err
   393  		}
   394  		*exprRef = newExpr
   395  	case enumspb.INDEXED_VALUE_TYPE_TEXT:
   396  		newExpr, err := c.convertTextComparisonExpr(expr)
   397  		if err != nil {
   398  			return err
   399  		}
   400  		*exprRef = newExpr
   401  	}
   402  
   403  	switch expr.Operator {
   404  	case sqlparser.StartsWithStr, sqlparser.NotStartsWithStr:
   405  		valueExpr, ok := expr.Right.(*unsafeSQLString)
   406  		if !ok {
   407  			return query.NewConverterError(
   408  				"%s: right-hand side of '%s' must be a literal string (got: %v)",
   409  				query.InvalidExpressionErrMessage,
   410  				expr.Operator,
   411  				sqlparser.String(expr.Right),
   412  			)
   413  		}
   414  		if expr.Operator == sqlparser.StartsWithStr {
   415  			expr.Operator = sqlparser.LikeStr
   416  		} else {
   417  			expr.Operator = sqlparser.NotLikeStr
   418  		}
   419  		expr.Escape = defaultLikeEscapeExpr
   420  		valueExpr.Val = escapeLikeValueForPrefixSearch(valueExpr.Val, defaultLikeEscapeChar)
   421  	}
   422  
   423  	return nil
   424  }
   425  
   426  func (c *QueryConverter) convertRangeCond(exprRef *sqlparser.Expr) error {
   427  	expr, ok := (*exprRef).(*sqlparser.RangeCond)
   428  	if !ok {
   429  		return query.NewConverterError(
   430  			"`%s` is not a range condition expression",
   431  			sqlparser.String(*exprRef),
   432  		)
   433  	}
   434  	saColNameExpr, err := c.convertColName(&expr.Left)
   435  	if err != nil {
   436  		return err
   437  	}
   438  	if !isSupportedTypeRangeCond(saColNameExpr.valueType) {
   439  		return query.NewConverterError(
   440  			"%s: cannot do range condition on search attribute '%s' of type %s",
   441  			query.InvalidExpressionErrMessage,
   442  			saColNameExpr.alias,
   443  			saColNameExpr.valueType.String(),
   444  		)
   445  	}
   446  	err = c.convertValueExpr(&expr.From, saColNameExpr.alias, saColNameExpr.valueType)
   447  	if err != nil {
   448  		return err
   449  	}
   450  	err = c.convertValueExpr(&expr.To, saColNameExpr.alias, saColNameExpr.valueType)
   451  	if err != nil {
   452  		return err
   453  	}
   454  	return nil
   455  }
   456  
   457  func (c *QueryConverter) convertColName(exprRef *sqlparser.Expr) (*saColName, error) {
   458  	expr, ok := (*exprRef).(*sqlparser.ColName)
   459  	if !ok {
   460  		return nil, query.NewConverterError(
   461  			"%s: must be a column name but was %T",
   462  			query.InvalidExpressionErrMessage,
   463  			*exprRef,
   464  		)
   465  	}
   466  	saAlias := strings.ReplaceAll(sqlparser.String(expr), "`", "")
   467  	saFieldName := saAlias
   468  	if searchattribute.IsMappable(saAlias) {
   469  		var err error
   470  		saFieldName, err = c.saMapper.GetFieldName(saAlias, c.namespaceName.String())
   471  		if err != nil {
   472  			return nil, query.NewConverterError(
   473  				"%s: column name '%s' is not a valid search attribute",
   474  				query.InvalidExpressionErrMessage,
   475  				saAlias,
   476  			)
   477  		}
   478  	}
   479  	saType, err := c.saTypeMap.GetType(saFieldName)
   480  	if err != nil {
   481  		// This should never happen since it came from mapping.
   482  		return nil, query.NewConverterError(
   483  			"%s: column name '%s' is not a valid search attribute",
   484  			query.InvalidExpressionErrMessage,
   485  			saAlias,
   486  		)
   487  	}
   488  	if saFieldName == searchattribute.TemporalNamespaceDivision {
   489  		c.seenNamespaceDivision = true
   490  	}
   491  	if saAlias == searchattribute.CloseTime {
   492  		*exprRef = c.getCoalesceCloseTimeExpr()
   493  		return closeTimeSaColName, nil
   494  	}
   495  	newExpr := newSAColName(
   496  		searchattribute.GetSqlDbColName(saFieldName),
   497  		saAlias,
   498  		saFieldName,
   499  		saType,
   500  	)
   501  	*exprRef = newExpr
   502  	return newExpr, nil
   503  }
   504  
   505  func (c *QueryConverter) convertValueExpr(
   506  	exprRef *sqlparser.Expr,
   507  	saName string,
   508  	saType enumspb.IndexedValueType,
   509  ) error {
   510  	expr := *exprRef
   511  	switch e := expr.(type) {
   512  	case *sqlparser.SQLVal:
   513  		value, err := c.parseSQLVal(e, saName, saType)
   514  		if err != nil {
   515  			return err
   516  		}
   517  		switch v := value.(type) {
   518  		case string:
   519  			// escape strings for safety
   520  			replacer := strings.NewReplacer(escapeCharMap...)
   521  			*exprRef = newUnsafeSQLString(replacer.Replace(v))
   522  		case int64:
   523  			*exprRef = sqlparser.NewIntVal([]byte(strconv.FormatInt(v, 10)))
   524  		case float64:
   525  			*exprRef = sqlparser.NewFloatVal([]byte(strconv.FormatFloat(v, 'f', -1, 64)))
   526  		default:
   527  			// this should never happen: query.ParseSqlValue returns one of the types above
   528  			return query.NewConverterError(
   529  				"%s: unexpected value type %T for search attribute %s",
   530  				query.InvalidExpressionErrMessage,
   531  				v,
   532  				saName,
   533  			)
   534  		}
   535  		return nil
   536  	case sqlparser.BoolVal:
   537  		// no-op: no validation needed
   538  		return nil
   539  	case sqlparser.ValTuple:
   540  		// This is "in (1,2,3)" case.
   541  		for i := range e {
   542  			err := c.convertValueExpr(&e[i], saName, saType)
   543  			if err != nil {
   544  				return err
   545  			}
   546  		}
   547  		return nil
   548  	case *sqlparser.GroupConcatExpr:
   549  		return query.NewConverterError("%s: 'group_concat'", query.NotSupportedErrMessage)
   550  	case *sqlparser.FuncExpr:
   551  		return query.NewConverterError("%s: nested func", query.NotSupportedErrMessage)
   552  	case *sqlparser.ColName:
   553  		return query.NewConverterError(
   554  			"%s: column name on the right side of comparison expression (did you forget to quote '%s'?)",
   555  			query.NotSupportedErrMessage,
   556  			sqlparser.String(expr),
   557  		)
   558  	default:
   559  		return query.NewConverterError(
   560  			"%s: unexpected value type %T",
   561  			query.InvalidExpressionErrMessage,
   562  			expr,
   563  		)
   564  	}
   565  }
   566  
   567  // parseSQLVal handles values for specific search attributes.
   568  // Returns a string, an int64 or a float64 if there are no errors.
   569  // For datetime, converts to UTC.
   570  // For execution status, converts string to enum value.
   571  // For execution duration, converts to nanoseconds.
   572  func (c *QueryConverter) parseSQLVal(
   573  	expr *sqlparser.SQLVal,
   574  	saName string,
   575  	saType enumspb.IndexedValueType,
   576  ) (any, error) {
   577  	// Using expr.Val instead of sqlparser.String(expr) because the latter escapes chars using MySQL
   578  	// conventions which is incompatible with SQLite.
   579  	var sqlValue string
   580  	switch expr.Type {
   581  	case sqlparser.StrVal:
   582  		sqlValue = fmt.Sprintf(`'%s'`, expr.Val)
   583  	default:
   584  		sqlValue = string(expr.Val)
   585  	}
   586  	value, err := query.ParseSqlValue(sqlValue)
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  
   591  	if saType == enumspb.INDEXED_VALUE_TYPE_DATETIME {
   592  		var tm time.Time
   593  		switch v := value.(type) {
   594  		case int64:
   595  			tm = time.Unix(0, v)
   596  		case string:
   597  			var err error
   598  			tm, err = time.Parse(time.RFC3339Nano, v)
   599  			if err != nil {
   600  				return nil, query.NewConverterError(
   601  					"%s: unable to parse datetime '%s'",
   602  					query.InvalidExpressionErrMessage,
   603  					v,
   604  				)
   605  			}
   606  		default:
   607  			return nil, query.NewConverterError(
   608  				"%s: unexpected value type %T for search attribute %s",
   609  				query.InvalidExpressionErrMessage,
   610  				v,
   611  				saName,
   612  			)
   613  		}
   614  		return tm.UTC().Format(c.getDatetimeFormat()), nil
   615  	}
   616  
   617  	if saName == searchattribute.ExecutionStatus {
   618  		var status int64
   619  		switch v := value.(type) {
   620  		case int64:
   621  			status = v
   622  		case string:
   623  			code, err := enumspb.WorkflowExecutionStatusFromString(v)
   624  			if err != nil {
   625  				return nil, query.NewConverterError(
   626  					"%s: invalid ExecutionStatus value '%s'",
   627  					query.InvalidExpressionErrMessage,
   628  					v,
   629  				)
   630  			}
   631  			status = int64(code)
   632  		default:
   633  			return nil, query.NewConverterError(
   634  				"%s: unexpected value type %T for search attribute %s",
   635  				query.InvalidExpressionErrMessage,
   636  				v,
   637  				saName,
   638  			)
   639  		}
   640  		return status, nil
   641  	}
   642  
   643  	if saName == searchattribute.ExecutionDuration {
   644  		if durationStr, isString := value.(string); isString {
   645  			duration, err := query.ParseExecutionDurationStr(durationStr)
   646  			if err != nil {
   647  				return nil, query.NewConverterError(
   648  					"invalid value for search attribute %s: %v (%v)", saName, value, err)
   649  			}
   650  			value = duration.Nanoseconds()
   651  		}
   652  	}
   653  
   654  	return value, nil
   655  }
   656  
   657  func (c *QueryConverter) convertIsExpr(exprRef *sqlparser.Expr) error {
   658  	expr, ok := (*exprRef).(*sqlparser.IsExpr)
   659  	if !ok {
   660  		return query.NewConverterError("`%s` is not an 'IS' expression", sqlparser.String(*exprRef))
   661  	}
   662  	_, err := c.convertColName(&expr.Expr)
   663  	if err != nil {
   664  		return err
   665  	}
   666  	switch expr.Operator {
   667  	case sqlparser.IsNullStr, sqlparser.IsNotNullStr:
   668  		// no-op
   669  	default:
   670  		return query.NewConverterError(
   671  			"%s: 'IS' operator can only be used with 'NULL' or 'NOT NULL'",
   672  			query.InvalidExpressionErrMessage,
   673  		)
   674  	}
   675  	return nil
   676  }
   677  
   678  func escapeLikeValueForPrefixSearch(in string, escape byte) string {
   679  	sb := strings.Builder{}
   680  	for _, c := range in {
   681  		if c == '%' || c == '_' || c == rune(escape) {
   682  			sb.WriteByte(escape)
   683  		}
   684  		sb.WriteRune(c)
   685  	}
   686  	sb.WriteByte('%')
   687  	return sb.String()
   688  }
   689  
   690  func isSupportedOperator(supportedOperators []string, operator string) bool {
   691  	for _, op := range supportedOperators {
   692  		if operator == op {
   693  			return true
   694  		}
   695  	}
   696  	return false
   697  }
   698  
   699  func isSupportedComparisonOperator(operator string) bool {
   700  	return isSupportedOperator(supportedComparisonOperators, operator)
   701  }
   702  
   703  func isSupportedKeywordListOperator(operator string) bool {
   704  	return isSupportedOperator(supportedKeyworkListOperators, operator)
   705  }
   706  
   707  func isSupportedTextOperator(operator string) bool {
   708  	return isSupportedOperator(supportedTextOperators, operator)
   709  }
   710  
   711  func isSupportedTypeRangeCond(saType enumspb.IndexedValueType) bool {
   712  	for _, tp := range supportedTypesRangeCond {
   713  		if saType == tp {
   714  			return true
   715  		}
   716  	}
   717  	return false
   718  }