go.temporal.io/server@v1.23.0/common/archiver/filestore/query_parser.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  //go:generate mockgen -copyright_file ../../../LICENSE -package $GOPACKAGE -source query_parser.go -destination query_parser_mock.go -mock_names Interface=MockQueryParser
    26  
    27  package filestore
    28  
    29  import (
    30  	"errors"
    31  	"fmt"
    32  	"strconv"
    33  	"strings"
    34  	"time"
    35  
    36  	"github.com/temporalio/sqlparser"
    37  	enumspb "go.temporal.io/api/enums/v1"
    38  
    39  	"go.temporal.io/server/common/convert"
    40  	"go.temporal.io/server/common/primitives/timestamp"
    41  	"go.temporal.io/server/common/util"
    42  )
    43  
    44  type (
    45  	// QueryParser parses a limited SQL where clause into a struct
    46  	QueryParser interface {
    47  		Parse(query string) (*parsedQuery, error)
    48  	}
    49  
    50  	queryParser struct{}
    51  
    52  	parsedQuery struct {
    53  		earliestCloseTime time.Time
    54  		latestCloseTime   time.Time
    55  		workflowID        *string
    56  		runID             *string
    57  		workflowTypeName  *string
    58  		status            *enumspb.WorkflowExecutionStatus
    59  		emptyResult       bool
    60  	}
    61  )
    62  
    63  // All allowed fields for filtering
    64  const (
    65  	WorkflowID   = "WorkflowId"
    66  	RunID        = "RunId"
    67  	WorkflowType = "WorkflowType"
    68  	CloseTime    = "CloseTime"
    69  	// Field name can't be just "Status" because it is reserved keyword in MySQL parser.
    70  	ExecutionStatus = "ExecutionStatus"
    71  )
    72  
    73  const (
    74  	queryTemplate = "select * from dummy where %s"
    75  
    76  	defaultDateTimeFormat = time.RFC3339
    77  )
    78  
    79  // NewQueryParser creates a new query parser for filestore
    80  func NewQueryParser() QueryParser {
    81  	return &queryParser{}
    82  }
    83  
    84  func (p *queryParser) Parse(query string) (*parsedQuery, error) {
    85  	parsedQuery := &parsedQuery{
    86  		earliestCloseTime: time.Time{},
    87  		latestCloseTime:   time.Now().UTC(),
    88  	}
    89  	if strings.TrimSpace(query) == "" {
    90  		return parsedQuery, nil
    91  	}
    92  	stmt, err := sqlparser.Parse(fmt.Sprintf(queryTemplate, query))
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	whereExpr := stmt.(*sqlparser.Select).Where.Expr
    97  	if err := p.convertWhereExpr(whereExpr, parsedQuery); err != nil {
    98  		return nil, err
    99  	}
   100  	return parsedQuery, nil
   101  }
   102  
   103  func (p *queryParser) convertWhereExpr(expr sqlparser.Expr, parsedQuery *parsedQuery) error {
   104  	if expr == nil {
   105  		return errors.New("where expression is nil")
   106  	}
   107  
   108  	switch expr := expr.(type) {
   109  	case *sqlparser.ComparisonExpr:
   110  		return p.convertComparisonExpr(expr, parsedQuery)
   111  	case *sqlparser.AndExpr:
   112  		return p.convertAndExpr(expr, parsedQuery)
   113  	case *sqlparser.ParenExpr:
   114  		return p.convertParenExpr(expr, parsedQuery)
   115  	default:
   116  		return errors.New("only comparison and \"and\" expression is supported")
   117  	}
   118  }
   119  
   120  func (p *queryParser) convertParenExpr(parenExpr *sqlparser.ParenExpr, parsedQuery *parsedQuery) error {
   121  	return p.convertWhereExpr(parenExpr.Expr, parsedQuery)
   122  }
   123  
   124  func (p *queryParser) convertAndExpr(andExpr *sqlparser.AndExpr, parsedQuery *parsedQuery) error {
   125  	if err := p.convertWhereExpr(andExpr.Left, parsedQuery); err != nil {
   126  		return err
   127  	}
   128  	return p.convertWhereExpr(andExpr.Right, parsedQuery)
   129  }
   130  
   131  func (p *queryParser) convertComparisonExpr(compExpr *sqlparser.ComparisonExpr, parsedQuery *parsedQuery) error {
   132  	colName, ok := compExpr.Left.(*sqlparser.ColName)
   133  	if !ok {
   134  		return fmt.Errorf("invalid filter name: %s", sqlparser.String(compExpr.Left))
   135  	}
   136  	colNameStr := sqlparser.String(colName)
   137  	op := compExpr.Operator
   138  	valExpr, ok := compExpr.Right.(*sqlparser.SQLVal)
   139  	if !ok {
   140  		return fmt.Errorf("invalid value: %s", sqlparser.String(compExpr.Right))
   141  	}
   142  	valStr := sqlparser.String(valExpr)
   143  
   144  	switch colNameStr {
   145  	case WorkflowID:
   146  		val, err := extractStringValue(valStr)
   147  		if err != nil {
   148  			return err
   149  		}
   150  		if op != "=" {
   151  			return fmt.Errorf("only operation = is support for %s", WorkflowID)
   152  		}
   153  		if parsedQuery.workflowID != nil && *parsedQuery.workflowID != val {
   154  			parsedQuery.emptyResult = true
   155  			return nil
   156  		}
   157  		parsedQuery.workflowID = convert.StringPtr(val)
   158  	case RunID:
   159  		val, err := extractStringValue(valStr)
   160  		if err != nil {
   161  			return err
   162  		}
   163  		if op != "=" {
   164  			return fmt.Errorf("only operation = is support for %s", RunID)
   165  		}
   166  		if parsedQuery.runID != nil && *parsedQuery.runID != val {
   167  			parsedQuery.emptyResult = true
   168  			return nil
   169  		}
   170  		parsedQuery.runID = convert.StringPtr(val)
   171  	case WorkflowType:
   172  		val, err := extractStringValue(valStr)
   173  		if err != nil {
   174  			return err
   175  		}
   176  		if op != "=" {
   177  			return fmt.Errorf("only operation = is support for %s", WorkflowType)
   178  		}
   179  		if parsedQuery.workflowTypeName != nil && *parsedQuery.workflowTypeName != val {
   180  			parsedQuery.emptyResult = true
   181  			return nil
   182  		}
   183  		parsedQuery.workflowTypeName = convert.StringPtr(val)
   184  	case ExecutionStatus:
   185  		val, err := extractStringValue(valStr)
   186  		if err != nil {
   187  			// if failed to extract string value, it means user input close status as a number
   188  			val = valStr
   189  		}
   190  		if op != "=" {
   191  			return fmt.Errorf("only operation = is support for %s", ExecutionStatus)
   192  		}
   193  		status, err := convertStatusStr(val)
   194  		if err != nil {
   195  			return err
   196  		}
   197  		if parsedQuery.status != nil && *parsedQuery.status != status {
   198  			parsedQuery.emptyResult = true
   199  			return nil
   200  		}
   201  		parsedQuery.status = &status
   202  	case CloseTime:
   203  		timestamp, err := convertToTime(valStr)
   204  		if err != nil {
   205  			return err
   206  		}
   207  		return p.convertCloseTime(timestamp, op, parsedQuery)
   208  	default:
   209  		return fmt.Errorf("unknown filter name: %s", colNameStr)
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  func (p *queryParser) convertCloseTime(timestamp time.Time, op string, parsedQuery *parsedQuery) error {
   216  	switch op {
   217  	case "=":
   218  		if err := p.convertCloseTime(timestamp, ">=", parsedQuery); err != nil {
   219  			return err
   220  		}
   221  		if err := p.convertCloseTime(timestamp, "<=", parsedQuery); err != nil {
   222  			return err
   223  		}
   224  	case "<":
   225  		parsedQuery.latestCloseTime = util.MinTime(parsedQuery.latestCloseTime, timestamp.Add(-1*time.Nanosecond))
   226  	case "<=":
   227  		parsedQuery.latestCloseTime = util.MinTime(parsedQuery.latestCloseTime, timestamp)
   228  	case ">":
   229  		parsedQuery.earliestCloseTime = util.MaxTime(parsedQuery.earliestCloseTime, timestamp.Add(1*time.Nanosecond))
   230  	case ">=":
   231  		parsedQuery.earliestCloseTime = util.MaxTime(parsedQuery.earliestCloseTime, timestamp)
   232  	default:
   233  		return fmt.Errorf("operator %s is not supported for close time", op)
   234  	}
   235  	return nil
   236  }
   237  
   238  func convertToTime(timeStr string) (time.Time, error) {
   239  	ts, err := strconv.ParseInt(timeStr, 10, 64)
   240  	if err == nil {
   241  		return timestamp.UnixOrZeroTime(ts), nil
   242  	}
   243  	timestampStr, err := extractStringValue(timeStr)
   244  	if err != nil {
   245  		return time.Time{}, err
   246  	}
   247  	parsedTime, err := time.Parse(defaultDateTimeFormat, timestampStr)
   248  	if err != nil {
   249  		return time.Time{}, err
   250  	}
   251  	return parsedTime, nil
   252  }
   253  
   254  func convertStatusStr(statusStr string) (enumspb.WorkflowExecutionStatus, error) {
   255  	statusStr = strings.ToLower(strings.TrimSpace(statusStr))
   256  	switch statusStr {
   257  	case "completed", convert.Int32ToString(int32(enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED)):
   258  		return enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, nil
   259  	case "failed", convert.Int32ToString(int32(enumspb.WORKFLOW_EXECUTION_STATUS_FAILED)):
   260  		return enumspb.WORKFLOW_EXECUTION_STATUS_FAILED, nil
   261  	case "canceled", convert.Int32ToString(int32(enumspb.WORKFLOW_EXECUTION_STATUS_CANCELED)):
   262  		return enumspb.WORKFLOW_EXECUTION_STATUS_CANCELED, nil
   263  	case "terminated", convert.Int32ToString(int32(enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED)):
   264  		return enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED, nil
   265  	case "continuedasnew", "continued_as_new", convert.Int32ToString(int32(enumspb.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW)):
   266  		return enumspb.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW, nil
   267  	case "timedout", "timed_out", convert.Int32ToString(int32(enumspb.WORKFLOW_EXECUTION_STATUS_TIMED_OUT)):
   268  		return enumspb.WORKFLOW_EXECUTION_STATUS_TIMED_OUT, nil
   269  	default:
   270  		return 0, fmt.Errorf("unknown workflow close status: %s", statusStr)
   271  	}
   272  }
   273  
   274  func extractStringValue(s string) (string, error) {
   275  	if len(s) >= 2 && s[0] == '\'' && s[len(s)-1] == '\'' {
   276  		return s[1 : len(s)-1], nil
   277  	}
   278  	return "", fmt.Errorf("value %s is not a string value", s)
   279  }