go.temporal.io/server@v1.23.0/common/persistence/visibility/store/sql/query_converter_test.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  	"fmt"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  	"github.com/stretchr/testify/suite"
    35  	"github.com/temporalio/sqlparser"
    36  
    37  	enumspb "go.temporal.io/api/enums/v1"
    38  	"go.temporal.io/server/common/namespace"
    39  	"go.temporal.io/server/common/persistence/visibility/store/query"
    40  	"go.temporal.io/server/common/searchattribute"
    41  )
    42  
    43  type (
    44  	queryConverterSuite struct {
    45  		suite.Suite
    46  		*require.Assertions
    47  
    48  		pqc            pluginQueryConverter
    49  		queryConverter *QueryConverter
    50  	}
    51  
    52  	testCase struct {
    53  		name     string
    54  		input    string
    55  		args     map[string]any
    56  		output   any
    57  		retValue any
    58  		err      error
    59  	}
    60  )
    61  
    62  const (
    63  	testNamespaceName = namespace.Name("test-namespace")
    64  	testNamespaceID   = namespace.ID("test-namespace-id")
    65  )
    66  
    67  func (s *queryConverterSuite) SetupTest() {
    68  	s.Assertions = require.New(s.T())
    69  	s.queryConverter = newQueryConverterInternal(
    70  		s.pqc,
    71  		testNamespaceName,
    72  		testNamespaceID,
    73  		searchattribute.TestNameTypeMap,
    74  		&searchattribute.TestMapper{},
    75  		"",
    76  	)
    77  }
    78  
    79  // TestConvertWhereString tests convertSelectStmt since convertWhereString is
    80  // just a wrapper for convertSelectStmt to parse users query string.
    81  func (s *queryConverterSuite) TestConvertWhereString() {
    82  	var tests = []testCase{
    83  		{
    84  			name:   "empty string",
    85  			input:  "",
    86  			output: &queryParams{queryString: "TemporalNamespaceDivision is null"},
    87  			err:    nil,
    88  		},
    89  		{
    90  			name:   "single condition int",
    91  			input:  "AliasForInt01 = 1",
    92  			output: &queryParams{queryString: "(Int01 = 1) and TemporalNamespaceDivision is null"},
    93  			err:    nil,
    94  		},
    95  		{
    96  			name:   "single condition keyword",
    97  			input:  "AliasForKeyword01 = 1",
    98  			output: &queryParams{queryString: "(Keyword01 = 1) and TemporalNamespaceDivision is null"},
    99  			err:    nil,
   100  		},
   101  		{
   102  			name:   "or condition keyword",
   103  			input:  "AliasForInt01 = 1 OR AliasForKeyword01 = 1",
   104  			output: &queryParams{queryString: "(Int01 = 1 or Keyword01 = 1) and TemporalNamespaceDivision is null"},
   105  			err:    nil,
   106  		},
   107  		{
   108  			name:   "no double parenthesis",
   109  			input:  "(AliasForInt01 = 1 OR AliasForKeyword01 = 1)",
   110  			output: &queryParams{queryString: "(Int01 = 1 or Keyword01 = 1) and TemporalNamespaceDivision is null"},
   111  			err:    nil,
   112  		},
   113  		{
   114  			name:   "has namespace division",
   115  			input:  "(AliasForInt01 = 1 OR AliasForKeyword01 = 1) AND TemporalNamespaceDivision = 'foo'",
   116  			output: &queryParams{queryString: "((Int01 = 1 or Keyword01 = 1) and TemporalNamespaceDivision = 'foo')"},
   117  			err:    nil,
   118  		},
   119  		{
   120  			name:  "group by one field",
   121  			input: "GROUP BY ExecutionStatus",
   122  			output: &queryParams{
   123  				queryString: "TemporalNamespaceDivision is null",
   124  				groupBy:     []string{searchattribute.ExecutionStatus},
   125  			},
   126  			err: nil,
   127  		},
   128  		{
   129  			name:   "group by two fields not supported",
   130  			input:  "GROUP BY ExecutionStatus, WorkflowType",
   131  			output: nil,
   132  			err: query.NewConverterError(
   133  				"%s: 'group by' clause supports only a single field",
   134  				query.NotSupportedErrMessage,
   135  			),
   136  		},
   137  		{
   138  			name:   "group by non ExecutionStatus",
   139  			input:  "GROUP BY WorkflowType",
   140  			output: nil,
   141  			err: query.NewConverterError(
   142  				"%s: 'group by' clause is only supported for %s search attribute",
   143  				query.NotSupportedErrMessage,
   144  				searchattribute.ExecutionStatus,
   145  			),
   146  		},
   147  		{
   148  			name:   "order by not supported",
   149  			input:  "ORDER BY StartTime",
   150  			output: nil,
   151  			err:    query.NewConverterError("%s: 'order by' clause", query.NotSupportedErrMessage),
   152  		},
   153  		{
   154  			name:   "group by with order by not supported",
   155  			input:  "GROUP BY ExecutionStatus ORDER BY StartTime",
   156  			output: nil,
   157  			err:    query.NewConverterError("%s: 'order by' clause", query.NotSupportedErrMessage),
   158  		},
   159  	}
   160  
   161  	for _, tc := range tests {
   162  		s.Run(tc.name, func() {
   163  			qc := newQueryConverterInternal(
   164  				s.pqc,
   165  				testNamespaceName,
   166  				testNamespaceID,
   167  				searchattribute.TestNameTypeMap,
   168  				&searchattribute.TestMapper{},
   169  				"",
   170  			)
   171  			qp, err := qc.convertWhereString(tc.input)
   172  			if tc.err == nil {
   173  				s.NoError(err)
   174  				s.Equal(tc.output, qp)
   175  			} else {
   176  				s.Error(err)
   177  				s.Equal(err, tc.err)
   178  			}
   179  		})
   180  	}
   181  }
   182  
   183  func (s *queryConverterSuite) TestConvertAndExpr() {
   184  	var tests = []testCase{
   185  		{
   186  			name:   "invalid",
   187  			input:  "AliasForInt01 = 1",
   188  			output: "",
   189  			err:    query.NewConverterError("`AliasForInt01 = 1` is not an 'AND' expression"),
   190  		},
   191  		{
   192  			name:   "two conditions",
   193  			input:  "AliasForInt01 = 1 AND AliasForKeyword01 = 'foo'",
   194  			output: "Int01 = 1 and Keyword01 = 'foo'",
   195  			err:    nil,
   196  		},
   197  		{
   198  			name:   "left side invalid",
   199  			input:  "AliasForInt01 AND AliasForKeyword01 = 'foo'",
   200  			output: "",
   201  			err:    query.NewConverterError("%s: incomplete expression", query.InvalidExpressionErrMessage),
   202  		},
   203  		{
   204  			name:   "right side invalid",
   205  			input:  "AliasForInt01 = 1 AND AliasForKeyword01",
   206  			output: "",
   207  			err:    query.NewConverterError("%s: incomplete expression", query.InvalidExpressionErrMessage),
   208  		},
   209  	}
   210  
   211  	for _, tc := range tests {
   212  		s.Run(tc.name, func() {
   213  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   214  			stmt, err := sqlparser.Parse(sql)
   215  			s.NoError(err)
   216  			expr := stmt.(*sqlparser.Select).Where.Expr
   217  			err = s.queryConverter.convertAndExpr(&expr)
   218  			if tc.err == nil {
   219  				s.NoError(err)
   220  				s.Equal(tc.output, sqlparser.String(expr))
   221  			} else {
   222  				s.Error(err)
   223  				s.Equal(err, tc.err)
   224  			}
   225  		})
   226  	}
   227  }
   228  
   229  func (s *queryConverterSuite) TestConvertOrExpr() {
   230  	var tests = []testCase{
   231  		{
   232  			name:   "invalid",
   233  			input:  "AliasForInt01 = 1",
   234  			output: "",
   235  			err:    query.NewConverterError("`AliasForInt01 = 1` is not an 'OR' expression"),
   236  		},
   237  		{
   238  			name:   "two conditions",
   239  			input:  "AliasForInt01 = 1 OR AliasForKeyword01 = 'foo'",
   240  			output: "Int01 = 1 or Keyword01 = 'foo'",
   241  			err:    nil,
   242  		},
   243  		{
   244  			name:   "left side invalid",
   245  			input:  "AliasForInt01 OR AliasForKeyword01 = 'foo'",
   246  			output: "",
   247  			err:    query.NewConverterError("%s: incomplete expression", query.InvalidExpressionErrMessage),
   248  		},
   249  		{
   250  			name:   "right side invalid",
   251  			input:  "AliasForInt01 = 1 OR AliasForKeyword01",
   252  			output: "",
   253  			err:    query.NewConverterError("%s: incomplete expression", query.InvalidExpressionErrMessage),
   254  		},
   255  	}
   256  
   257  	for _, tc := range tests {
   258  		s.Run(tc.name, func() {
   259  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   260  			stmt, err := sqlparser.Parse(sql)
   261  			s.NoError(err)
   262  			expr := stmt.(*sqlparser.Select).Where.Expr
   263  			err = s.queryConverter.convertOrExpr(&expr)
   264  			if tc.err == nil {
   265  				s.NoError(err)
   266  				s.Equal(tc.output, sqlparser.String(expr))
   267  			} else {
   268  				s.Error(err)
   269  				s.Equal(err, tc.err)
   270  			}
   271  		})
   272  	}
   273  }
   274  
   275  func (s *queryConverterSuite) TestConvertComparisonExpr() {
   276  	var tests = []testCase{
   277  		{
   278  			name:   "invalid",
   279  			input:  "AliasForInt01",
   280  			output: "",
   281  			err:    query.NewConverterError("`AliasForInt01` is not a comparison expression"),
   282  		},
   283  		{
   284  			name:   "equal expression",
   285  			input:  "AliasForKeyword01 = 'foo'",
   286  			output: "Keyword01 = 'foo'",
   287  			err:    nil,
   288  		},
   289  		{
   290  			name:   "not equal expression",
   291  			input:  "AliasForKeyword01 != 'foo'",
   292  			output: "Keyword01 != 'foo'",
   293  			err:    nil,
   294  		},
   295  		{
   296  			name:   "less than expression",
   297  			input:  "AliasForInt01 < 10",
   298  			output: "Int01 < 10",
   299  			err:    nil,
   300  		},
   301  		{
   302  			name:   "greater than expression",
   303  			input:  "AliasForInt01 > 10",
   304  			output: "Int01 > 10",
   305  			err:    nil,
   306  		},
   307  		{
   308  			name:   "less than or equal expression",
   309  			input:  "AliasForInt01 <= 10",
   310  			output: "Int01 <= 10",
   311  			err:    nil,
   312  		},
   313  		{
   314  			name:   "greater than or equal expression",
   315  			input:  "AliasForInt01 >= 10",
   316  			output: "Int01 >= 10",
   317  			err:    nil,
   318  		},
   319  		{
   320  			name:   "in expression",
   321  			input:  "AliasForKeyword01 in ('foo', 'bar')",
   322  			output: "Keyword01 in ('foo', 'bar')",
   323  			err:    nil,
   324  		},
   325  		{
   326  			name:   "not in expression",
   327  			input:  "AliasForKeyword01 not in ('foo', 'bar')",
   328  			output: "Keyword01 not in ('foo', 'bar')",
   329  			err:    nil,
   330  		},
   331  		{
   332  			name:   "starts_with expression",
   333  			input:  "AliasForKeyword01 starts_with 'foo_bar%'",
   334  			output: `Keyword01 like 'foo!_bar!%%' escape '!'`,
   335  			err:    nil,
   336  		},
   337  		{
   338  			name:   "not starts_with expression",
   339  			input:  "AliasForKeyword01 not starts_with 'foo_bar%'",
   340  			output: `Keyword01 not like 'foo!_bar!%%' escape '!'`,
   341  			err:    nil,
   342  		},
   343  		{
   344  			name:   "starts_with expression error",
   345  			input:  "AliasForKeyword01 starts_with 123",
   346  			output: "",
   347  			err: query.NewConverterError(
   348  				"%s: right-hand side of '%s' must be a literal string (got: 123)",
   349  				query.InvalidExpressionErrMessage,
   350  				sqlparser.StartsWithStr,
   351  			),
   352  		},
   353  		{
   354  			name:   "not starts_with expression error",
   355  			input:  "AliasForKeyword01 not starts_with 123",
   356  			output: "",
   357  			err: query.NewConverterError(
   358  				"%s: right-hand side of '%s' must be a literal string (got: 123)",
   359  				query.InvalidExpressionErrMessage,
   360  				sqlparser.NotStartsWithStr,
   361  			),
   362  		},
   363  		{
   364  			name:   "like expression",
   365  			input:  "AliasForKeyword01 like 'foo%'",
   366  			output: "",
   367  			err: query.NewConverterError(
   368  				"%s: invalid operator 'like' in `%s`",
   369  				query.InvalidExpressionErrMessage,
   370  				"AliasForKeyword01 like 'foo%'",
   371  			),
   372  		},
   373  		{
   374  			name:   "not like expression",
   375  			input:  "AliasForKeyword01 NOT LIKE 'foo%'",
   376  			output: "",
   377  			err: query.NewConverterError(
   378  				"%s: invalid operator 'not like' in `%s`",
   379  				query.InvalidExpressionErrMessage,
   380  				"AliasForKeyword01 not like 'foo%'",
   381  			),
   382  		},
   383  	}
   384  
   385  	for _, tc := range tests {
   386  		s.Run(tc.name, func() {
   387  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   388  			stmt, err := sqlparser.Parse(sql)
   389  			s.NoError(err)
   390  			expr := stmt.(*sqlparser.Select).Where.Expr
   391  			err = s.queryConverter.convertComparisonExpr(&expr)
   392  			if tc.err == nil {
   393  				s.NoError(err)
   394  				s.Equal(tc.output, sqlparser.String(expr))
   395  			} else {
   396  				s.Error(err)
   397  				s.Equal(err, tc.err)
   398  			}
   399  		})
   400  	}
   401  }
   402  
   403  func (s *queryConverterSuite) TestConvertRangeCond() {
   404  	fromDatetime, _ := time.Parse(time.RFC3339Nano, "2020-02-15T20:30:40Z")
   405  	toDatetime, _ := time.Parse(time.RFC3339Nano, "2020-02-16T20:30:40Z")
   406  	var tests = []testCase{
   407  		{
   408  			name:   "invalid",
   409  			input:  "AliasForInt01 = 1",
   410  			output: "",
   411  			err:    query.NewConverterError("`AliasForInt01 = 1` is not a range condition expression"),
   412  		},
   413  		{
   414  			name: "between expression",
   415  			input: fmt.Sprintf(
   416  				"AliasForDatetime01 BETWEEN '%s' AND '%s'",
   417  				fromDatetime.Format(time.RFC3339Nano),
   418  				toDatetime.Format(time.RFC3339Nano),
   419  			),
   420  			output: fmt.Sprintf(
   421  				"Datetime01 between '%s' and '%s'",
   422  				fromDatetime.Format(s.queryConverter.getDatetimeFormat()),
   423  				toDatetime.Format(s.queryConverter.getDatetimeFormat()),
   424  			),
   425  			err: nil,
   426  		},
   427  		{
   428  			name: "not between expression",
   429  			input: fmt.Sprintf(
   430  				"AliasForDatetime01 NOT BETWEEN '%s' AND '%s'",
   431  				fromDatetime.Format(time.RFC3339Nano),
   432  				toDatetime.Format(time.RFC3339Nano),
   433  			),
   434  			output: fmt.Sprintf(
   435  				"Datetime01 not between '%s' and '%s'",
   436  				fromDatetime.Format(s.queryConverter.getDatetimeFormat()),
   437  				toDatetime.Format(s.queryConverter.getDatetimeFormat()),
   438  			),
   439  			err: nil,
   440  		},
   441  		{
   442  			name:   "text type not supported",
   443  			input:  "AliasForText01 BETWEEN 'abc' AND 'abd'",
   444  			output: "",
   445  			err: query.NewConverterError(
   446  				"%s: cannot do range condition on search attribute '%s' of type %s",
   447  				query.InvalidExpressionErrMessage,
   448  				"AliasForText01",
   449  				enumspb.INDEXED_VALUE_TYPE_TEXT.String(),
   450  			),
   451  		},
   452  	}
   453  
   454  	for _, tc := range tests {
   455  		s.Run(tc.name, func() {
   456  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   457  			stmt, err := sqlparser.Parse(sql)
   458  			s.NoError(err)
   459  			expr := stmt.(*sqlparser.Select).Where.Expr
   460  			err = s.queryConverter.convertRangeCond(&expr)
   461  			if tc.err == nil {
   462  				s.NoError(err)
   463  				s.Equal(tc.output, sqlparser.String(expr))
   464  			} else {
   465  				s.Error(err)
   466  				s.Equal(err, tc.err)
   467  			}
   468  		})
   469  	}
   470  }
   471  
   472  func (s *queryConverterSuite) TestConvertIsExpr() {
   473  	var tests = []testCase{
   474  		{
   475  			name:   "invalid",
   476  			input:  "AliasForInt01 = 1",
   477  			output: "",
   478  			err:    query.NewConverterError("`AliasForInt01 = 1` is not an 'IS' expression"),
   479  		},
   480  		{
   481  			name:   "is expression",
   482  			input:  "AliasForKeyword01 IS NULL",
   483  			output: "Keyword01 is null",
   484  			err:    nil,
   485  		},
   486  		{
   487  			name:   "is not expression",
   488  			input:  "AliasForKeyword01 IS NOT NULL",
   489  			output: "Keyword01 is not null",
   490  			err:    nil,
   491  		},
   492  	}
   493  
   494  	for _, tc := range tests {
   495  		s.Run(tc.name, func() {
   496  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   497  			stmt, err := sqlparser.Parse(sql)
   498  			s.NoError(err)
   499  			expr := stmt.(*sqlparser.Select).Where.Expr
   500  			err = s.queryConverter.convertIsExpr(&expr)
   501  			if tc.err == nil {
   502  				s.NoError(err)
   503  				s.Equal(tc.output, sqlparser.String(expr))
   504  			} else {
   505  				s.Error(err)
   506  				s.Equal(err, tc.err)
   507  			}
   508  		})
   509  	}
   510  }
   511  
   512  func (s *queryConverterSuite) TestConvertColName() {
   513  	var tests = []testCase{
   514  		{
   515  			name:     "invalid: column name expression",
   516  			input:    "10",
   517  			output:   "",
   518  			retValue: nil,
   519  			err: query.NewConverterError(
   520  				"%s: must be a column name but was *sqlparser.SQLVal",
   521  				query.InvalidExpressionErrMessage,
   522  			),
   523  		},
   524  		{
   525  			name:     "invalid search attribute",
   526  			input:    "InvalidName",
   527  			output:   "",
   528  			retValue: nil,
   529  			err: query.NewConverterError(
   530  				"%s: column name '%s' is not a valid search attribute",
   531  				query.InvalidExpressionErrMessage,
   532  				"InvalidName",
   533  			),
   534  		},
   535  		{
   536  			name:   "valid system search attribute: ExecutionStatus",
   537  			input:  "ExecutionStatus",
   538  			output: "status",
   539  			retValue: newSAColName(
   540  				"status",
   541  				"ExecutionStatus",
   542  				"ExecutionStatus",
   543  				enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   544  			),
   545  			err: nil,
   546  		},
   547  		{
   548  			name:   "valid system search attribute: CloseTime",
   549  			input:  "CloseTime",
   550  			output: sqlparser.String(s.queryConverter.getCoalesceCloseTimeExpr()),
   551  			retValue: newSAColName(
   552  				"close_time",
   553  				"CloseTime",
   554  				"CloseTime",
   555  				enumspb.INDEXED_VALUE_TYPE_DATETIME,
   556  			),
   557  			err: nil,
   558  		},
   559  		{
   560  			name:   "valid predefined search attribute: BinaryChecksums",
   561  			input:  "BinaryChecksums",
   562  			output: "BinaryChecksums",
   563  			retValue: newSAColName(
   564  				"BinaryChecksums",
   565  				"BinaryChecksums",
   566  				"BinaryChecksums",
   567  				enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST,
   568  			),
   569  			err: nil,
   570  		},
   571  		{
   572  			name:   "valid predefined search attribute: TemporalNamespaceDivision",
   573  			input:  "TemporalNamespaceDivision",
   574  			output: "TemporalNamespaceDivision",
   575  			retValue: newSAColName(
   576  				"TemporalNamespaceDivision",
   577  				"TemporalNamespaceDivision",
   578  				"TemporalNamespaceDivision",
   579  				enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   580  			),
   581  			err: nil,
   582  		},
   583  		{
   584  			name:   "valid custom search attribute: int",
   585  			input:  "AliasForInt01",
   586  			output: "Int01",
   587  			retValue: newSAColName(
   588  				"Int01",
   589  				"AliasForInt01",
   590  				"Int01",
   591  				enumspb.INDEXED_VALUE_TYPE_INT,
   592  			),
   593  			err: nil,
   594  		},
   595  		{
   596  			name:   "valid custom search attribute: datetime",
   597  			input:  "AliasForDatetime01",
   598  			output: "Datetime01",
   599  			retValue: newSAColName(
   600  				"Datetime01",
   601  				"AliasForDatetime01",
   602  				"Datetime01",
   603  				enumspb.INDEXED_VALUE_TYPE_DATETIME,
   604  			),
   605  			err: nil,
   606  		},
   607  	}
   608  
   609  	for _, tc := range tests {
   610  		s.Run(tc.name, func() {
   611  			// reset internal state of seenNamespaceDivision
   612  			s.queryConverter.seenNamespaceDivision = false
   613  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   614  			stmt, err := sqlparser.Parse(sql)
   615  			s.NoError(err)
   616  			expr := stmt.(*sqlparser.Select).Where.Expr
   617  			saColNameExpr, err := s.queryConverter.convertColName(&expr)
   618  			if tc.err == nil {
   619  				s.NoError(err)
   620  				s.Equal(tc.output, sqlparser.String(expr))
   621  				s.Equal(tc.retValue, saColNameExpr)
   622  				if tc.input != searchattribute.CloseTime {
   623  					_, ok := expr.(*saColName)
   624  					s.True(ok)
   625  				}
   626  				if tc.input == searchattribute.TemporalNamespaceDivision {
   627  					s.True(s.queryConverter.seenNamespaceDivision)
   628  				} else {
   629  					s.False(s.queryConverter.seenNamespaceDivision)
   630  				}
   631  			} else {
   632  				s.Error(err)
   633  				s.Equal(err, tc.err)
   634  			}
   635  		})
   636  	}
   637  }
   638  
   639  func (s *queryConverterSuite) TestConvertValueExpr() {
   640  	dt, _ := time.Parse(time.RFC3339Nano, "2020-02-15T20:30:40.123456789Z")
   641  	var tests = []testCase{
   642  		{
   643  			name:   "invalid: column name expression",
   644  			input:  "ExecutionStatus",
   645  			args:   map[string]any{"saName": "ExecutionStatus", "saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD},
   646  			output: "",
   647  			err: query.NewConverterError(
   648  				"%s: column name on the right side of comparison expression (did you forget to quote '%s'?)",
   649  				query.NotSupportedErrMessage,
   650  				"ExecutionStatus",
   651  			),
   652  		},
   653  		{
   654  			name:  "valid string",
   655  			input: "'foo'",
   656  			args: map[string]any{
   657  				"saName": "AliasForKeyword01",
   658  				"saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   659  			},
   660  			output: "'foo'",
   661  			err:    nil,
   662  		},
   663  		{
   664  			name:  "valid string escape char",
   665  			input: "'\"foo'",
   666  			args: map[string]any{
   667  				"saName": "AliasForKeyword01",
   668  				"saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   669  			},
   670  			output: "'\\\"foo'",
   671  			err:    nil,
   672  		},
   673  		{
   674  			name:  "valid integer",
   675  			input: "123",
   676  			args: map[string]any{
   677  				"saName": "AliasForInt01",
   678  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   679  			},
   680  			output: "123",
   681  			err:    nil,
   682  		},
   683  		{
   684  			name:  "valid float",
   685  			input: "1.230",
   686  			args: map[string]any{
   687  				"saName": "AliasForDouble01",
   688  				"saType": enumspb.INDEXED_VALUE_TYPE_DOUBLE,
   689  			},
   690  			output: "1.23",
   691  			err:    nil,
   692  		},
   693  		{
   694  			name:  "valid bool",
   695  			input: "true",
   696  			args: map[string]any{
   697  				"saName": "AliasForBool01",
   698  				"saType": enumspb.INDEXED_VALUE_TYPE_BOOL,
   699  			},
   700  			output: "true",
   701  			err:    nil,
   702  		},
   703  		{
   704  			name:  "valid datetime",
   705  			input: fmt.Sprintf("'%s'", dt.Format(time.RFC3339Nano)),
   706  			args: map[string]any{
   707  				"saName": "AliasForDatetime01",
   708  				"saType": enumspb.INDEXED_VALUE_TYPE_DATETIME,
   709  			},
   710  			output: fmt.Sprintf("'%s'", dt.Format(s.queryConverter.getDatetimeFormat())),
   711  			err:    nil,
   712  		},
   713  		{
   714  			name:  "valid tuple",
   715  			input: "('foo', 'bar')",
   716  			args: map[string]any{
   717  				"saName": "AliasForKeywordList01",
   718  				"saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST,
   719  			},
   720  			output: "('foo', 'bar')",
   721  			err:    nil,
   722  		},
   723  	}
   724  
   725  	for _, tc := range tests {
   726  		s.Run(tc.name, func() {
   727  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   728  			stmt, err := sqlparser.Parse(sql)
   729  			s.NoError(err)
   730  			expr := stmt.(*sqlparser.Select).Where.Expr
   731  			err = s.queryConverter.convertValueExpr(
   732  				&expr,
   733  				tc.args["saName"].(string),
   734  				tc.args["saType"].(enumspb.IndexedValueType),
   735  			)
   736  			if tc.err == nil {
   737  				s.NoError(err)
   738  				s.Equal(tc.output, sqlparser.String(expr))
   739  				if len(tc.input) > 0 && tc.input[0] == '\'' {
   740  					_, ok := expr.(*unsafeSQLString)
   741  					s.True(ok)
   742  				}
   743  			} else {
   744  				s.Error(err)
   745  				s.Equal(err, tc.err)
   746  			}
   747  		})
   748  	}
   749  }
   750  
   751  func (s *queryConverterSuite) TestParseSQLVal() {
   752  	dt, _ := time.Parse(time.RFC3339Nano, "2020-02-15T20:30:40.123456789Z")
   753  	var tests = []testCase{
   754  		{
   755  			name:  "valid string",
   756  			input: "'foo'",
   757  			args: map[string]any{
   758  				"saName": "AliasForKeyword01",
   759  				"saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   760  			},
   761  			retValue: "foo",
   762  			err:      nil,
   763  		},
   764  		{
   765  			name:  "valid integer",
   766  			input: "123",
   767  			args: map[string]any{
   768  				"saName": "AliasForInt01",
   769  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   770  			},
   771  			retValue: int64(123),
   772  			err:      nil,
   773  		},
   774  		{
   775  			name:  "valid float",
   776  			input: "1.230",
   777  			args: map[string]any{
   778  				"saName": "AliasForDouble01",
   779  				"saType": enumspb.INDEXED_VALUE_TYPE_DOUBLE,
   780  			},
   781  			retValue: float64(1.23),
   782  			err:      nil,
   783  		},
   784  		{
   785  			name:  "valid datetime",
   786  			input: fmt.Sprintf("'%s'", dt.Format(time.RFC3339Nano)),
   787  			args: map[string]any{
   788  				"saName": "AliasForDatetime01",
   789  				"saType": enumspb.INDEXED_VALUE_TYPE_DATETIME,
   790  			},
   791  			retValue: fmt.Sprintf("%s", dt.Format(s.queryConverter.getDatetimeFormat())),
   792  			err:      nil,
   793  		},
   794  		{
   795  			name:  "invalid datetime",
   796  			input: fmt.Sprintf("'%s'", dt.String()),
   797  			args: map[string]any{
   798  				"saName": "AliasForDatetime01",
   799  				"saType": enumspb.INDEXED_VALUE_TYPE_DATETIME,
   800  			},
   801  			retValue: nil,
   802  			err: query.NewConverterError(
   803  				"%s: unable to parse datetime '%s'",
   804  				query.InvalidExpressionErrMessage,
   805  				dt.String(),
   806  			),
   807  		},
   808  		{
   809  			name:  "valid ExecutionStatus keyword",
   810  			input: "'Running'",
   811  			args: map[string]any{
   812  				"saName": "ExecutionStatus",
   813  				"saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   814  			},
   815  			retValue: int64(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING),
   816  			err:      nil,
   817  		},
   818  		{
   819  			name:  "valid ExecutionStatus code",
   820  			input: "1",
   821  			args: map[string]any{
   822  				"saName": "ExecutionStatus",
   823  				"saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   824  			},
   825  			retValue: int64(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING),
   826  			err:      nil,
   827  		},
   828  		{
   829  			name:  "invalid ExecutionStatus keyword",
   830  			input: "'Foo'",
   831  			args: map[string]any{
   832  				"saName": "ExecutionStatus",
   833  				"saType": enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   834  			},
   835  			retValue: nil,
   836  			err: query.NewConverterError(
   837  				"%s: invalid ExecutionStatus value '%s'",
   838  				query.InvalidExpressionErrMessage,
   839  				"Foo",
   840  			),
   841  		},
   842  		{
   843  			name:  "valid ExecutionDuration day suffix",
   844  			input: "'10d'",
   845  			args: map[string]any{
   846  				"saName": "ExecutionDuration",
   847  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   848  			},
   849  			retValue: int64(10 * 24 * time.Hour),
   850  			err:      nil,
   851  		},
   852  		{
   853  			name:  "valid ExecutionDuration hour suffix",
   854  			input: "'10h'",
   855  			args: map[string]any{
   856  				"saName": "ExecutionDuration",
   857  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   858  			},
   859  			retValue: int64(10 * time.Hour),
   860  			err:      nil,
   861  		},
   862  		{
   863  			name:  "valid ExecutionDuration string nanos",
   864  			input: "'100'",
   865  			args: map[string]any{
   866  				"saName": "ExecutionDuration",
   867  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   868  			},
   869  			retValue: int64(100),
   870  			err:      nil,
   871  		},
   872  		{
   873  			name:  "valid ExecutionDuration int nanos",
   874  			input: "100",
   875  			args: map[string]any{
   876  				"saName": "ExecutionDuration",
   877  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   878  			},
   879  			retValue: int64(100),
   880  			err:      nil,
   881  		},
   882  		{
   883  			name:  "invalid ExecutionDuration",
   884  			input: "'100q'",
   885  			args: map[string]any{
   886  				"saName": "ExecutionDuration",
   887  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   888  			},
   889  			retValue: nil,
   890  			err: query.NewConverterError(
   891  				"invalid value for search attribute ExecutionDuration: 100q (invalid duration)"),
   892  		},
   893  		{
   894  			name:  "invalid ExecutionDuration out of bounds",
   895  			input: "'10000000h'",
   896  			args: map[string]any{
   897  				"saName": "ExecutionDuration",
   898  				"saType": enumspb.INDEXED_VALUE_TYPE_INT,
   899  			},
   900  			retValue: nil,
   901  			err: query.NewConverterError(
   902  				"invalid value for search attribute ExecutionDuration: 10000000h (invalid duration)"),
   903  		},
   904  	}
   905  
   906  	for _, tc := range tests {
   907  		s.Run(tc.name, func() {
   908  			sql := fmt.Sprintf("select * from table1 where %s", tc.input)
   909  			stmt, err := sqlparser.Parse(sql)
   910  			s.NoError(err)
   911  			expr := stmt.(*sqlparser.Select).Where.Expr
   912  			value, err := s.queryConverter.parseSQLVal(
   913  				expr.(*sqlparser.SQLVal),
   914  				tc.args["saName"].(string),
   915  				tc.args["saType"].(enumspb.IndexedValueType),
   916  			)
   917  			if tc.err == nil {
   918  				s.NoError(err)
   919  				s.Equal(tc.input, sqlparser.String(expr)) // parseSQLVal does not change input expr
   920  				s.Equal(tc.retValue, value)
   921  			} else {
   922  				s.Error(err)
   923  				s.Equal(err, tc.err)
   924  			}
   925  		})
   926  	}
   927  }
   928  
   929  func TestSupportedComparisonOperators(t *testing.T) {
   930  	s := assert.New(t)
   931  	msg := "If you're changing the supported operators, remember to check they work with " +
   932  		"MySQL, PostgreSQL and SQLite, and check their respective plugin converters."
   933  	s.True(isSupportedComparisonOperator(sqlparser.EqualStr), msg)
   934  	s.True(isSupportedComparisonOperator(sqlparser.NotEqualStr), msg)
   935  	s.True(isSupportedComparisonOperator(sqlparser.LessThanStr), msg)
   936  	s.True(isSupportedComparisonOperator(sqlparser.GreaterThanStr), msg)
   937  	s.True(isSupportedComparisonOperator(sqlparser.LessEqualStr), msg)
   938  	s.True(isSupportedComparisonOperator(sqlparser.GreaterEqualStr), msg)
   939  	s.True(isSupportedComparisonOperator(sqlparser.InStr), msg)
   940  	s.True(isSupportedComparisonOperator(sqlparser.NotInStr), msg)
   941  	s.False(isSupportedComparisonOperator(sqlparser.LikeStr), msg)
   942  	s.False(isSupportedComparisonOperator(sqlparser.NotLikeStr), msg)
   943  }
   944  
   945  func TestSupportedKeywordListOperators(t *testing.T) {
   946  	s := assert.New(t)
   947  	msg := "If you're changing the supported operators, remember to check they work with " +
   948  		"MySQL, PostgreSQL and SQLite, and check their respective plugin converters."
   949  	s.True(isSupportedKeywordListOperator(sqlparser.EqualStr), msg)
   950  	s.True(isSupportedKeywordListOperator(sqlparser.NotEqualStr), msg)
   951  	s.True(isSupportedKeywordListOperator(sqlparser.InStr), msg)
   952  	s.True(isSupportedKeywordListOperator(sqlparser.NotInStr), msg)
   953  	s.False(isSupportedKeywordListOperator(sqlparser.LessThanStr), msg)
   954  	s.False(isSupportedKeywordListOperator(sqlparser.GreaterThanStr), msg)
   955  	s.False(isSupportedKeywordListOperator(sqlparser.LessEqualStr), msg)
   956  	s.False(isSupportedKeywordListOperator(sqlparser.GreaterEqualStr), msg)
   957  	s.False(isSupportedKeywordListOperator(sqlparser.LikeStr), msg)
   958  	s.False(isSupportedKeywordListOperator(sqlparser.NotLikeStr), msg)
   959  }
   960  
   961  func TestSupportedTextOperators(t *testing.T) {
   962  	s := assert.New(t)
   963  	msg := "If you're changing the supported operators, remember to check they work with " +
   964  		"MySQL, PostgreSQL and SQLite, and check their respective plugin converters."
   965  	s.True(isSupportedTextOperator(sqlparser.EqualStr), msg)
   966  	s.True(isSupportedTextOperator(sqlparser.NotEqualStr), msg)
   967  	s.False(isSupportedTextOperator(sqlparser.LessThanStr), msg)
   968  	s.False(isSupportedTextOperator(sqlparser.GreaterThanStr), msg)
   969  	s.False(isSupportedTextOperator(sqlparser.LessEqualStr), msg)
   970  	s.False(isSupportedTextOperator(sqlparser.GreaterEqualStr), msg)
   971  	s.False(isSupportedTextOperator(sqlparser.InStr), msg)
   972  	s.False(isSupportedTextOperator(sqlparser.NotInStr), msg)
   973  	s.False(isSupportedTextOperator(sqlparser.LikeStr), msg)
   974  	s.False(isSupportedTextOperator(sqlparser.NotLikeStr), msg)
   975  }
   976  
   977  func TestSupportedTypeRangeCond(t *testing.T) {
   978  	s := assert.New(t)
   979  	msg := "If you're changing the supported types for range condition, " +
   980  		"remember to check they work correctly with MySQL, PostgreSQL and SQLite."
   981  	supportedTypesRangeCond = []enumspb.IndexedValueType{
   982  		enumspb.INDEXED_VALUE_TYPE_DATETIME,
   983  		enumspb.INDEXED_VALUE_TYPE_DOUBLE,
   984  		enumspb.INDEXED_VALUE_TYPE_INT,
   985  		enumspb.INDEXED_VALUE_TYPE_KEYWORD,
   986  	}
   987  	for tpCode := range enumspb.IndexedValueType_name {
   988  		tp := enumspb.IndexedValueType(tpCode)
   989  		switch tp {
   990  		case enumspb.INDEXED_VALUE_TYPE_DATETIME,
   991  			enumspb.INDEXED_VALUE_TYPE_DOUBLE,
   992  			enumspb.INDEXED_VALUE_TYPE_INT,
   993  			enumspb.INDEXED_VALUE_TYPE_KEYWORD:
   994  			s.True(isSupportedTypeRangeCond(tp), msg)
   995  		default:
   996  			s.False(isSupportedTypeRangeCond(tp), msg)
   997  		}
   998  	}
   999  }