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 }