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 }