go.temporal.io/server@v1.23.0/common/persistence/visibility/store/sql/query_converter_postgresql.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  	"strings"
    30  
    31  	"github.com/temporalio/sqlparser"
    32  	"go.temporal.io/server/common/namespace"
    33  	"go.temporal.io/server/common/persistence/sql/sqlplugin"
    34  	"go.temporal.io/server/common/persistence/visibility/store/query"
    35  	"go.temporal.io/server/common/searchattribute"
    36  )
    37  
    38  type (
    39  	pgCastExpr struct {
    40  		sqlparser.Expr
    41  		Value sqlparser.Expr
    42  		Type  *sqlparser.ConvertType
    43  	}
    44  
    45  	pgQueryConverter struct{}
    46  )
    47  
    48  const (
    49  	jsonBuildArrayFuncName = "jsonb_build_array"
    50  	jsonContainsOp         = "@>"
    51  	ftsMatchOp             = "@@"
    52  )
    53  
    54  var (
    55  	convertTypeTSQuery = &sqlparser.ConvertType{Type: "tsquery"}
    56  )
    57  
    58  var _ sqlparser.Expr = (*pgCastExpr)(nil)
    59  var _ pluginQueryConverter = (*pgQueryConverter)(nil)
    60  
    61  func (node *pgCastExpr) Format(buf *sqlparser.TrackedBuffer) {
    62  	buf.Myprintf("%v::%v", node.Value, node.Type)
    63  }
    64  
    65  func newPostgreSQLQueryConverter(
    66  	namespaceName namespace.Name,
    67  	namespaceID namespace.ID,
    68  	saTypeMap searchattribute.NameTypeMap,
    69  	saMapper searchattribute.Mapper,
    70  	queryString string,
    71  ) *QueryConverter {
    72  	return newQueryConverterInternal(
    73  		&pgQueryConverter{},
    74  		namespaceName,
    75  		namespaceID,
    76  		saTypeMap,
    77  		saMapper,
    78  		queryString,
    79  	)
    80  }
    81  
    82  func (c *pgQueryConverter) getDatetimeFormat() string {
    83  	return "2006-01-02 15:04:05.999999"
    84  }
    85  
    86  func (c *pgQueryConverter) getCoalesceCloseTimeExpr() sqlparser.Expr {
    87  	return newFuncExpr(
    88  		coalesceFuncName,
    89  		closeTimeSaColName,
    90  		newUnsafeSQLString(maxDatetimeValue.Format(c.getDatetimeFormat())),
    91  	)
    92  }
    93  
    94  func (c *pgQueryConverter) convertKeywordListComparisonExpr(
    95  	expr *sqlparser.ComparisonExpr,
    96  ) (sqlparser.Expr, error) {
    97  	if !isSupportedKeywordListOperator(expr.Operator) {
    98  		return nil, query.NewConverterError(
    99  			"%s: operator '%s' not supported for KeywordList type search attribute in `%s`",
   100  			query.InvalidExpressionErrMessage,
   101  			expr.Operator,
   102  			formatComparisonExprStringForError(*expr),
   103  		)
   104  	}
   105  
   106  	switch expr.Operator {
   107  	case sqlparser.EqualStr, sqlparser.NotEqualStr:
   108  		newExpr := c.newJsonContainsExpr(expr.Left, expr.Right)
   109  		if expr.Operator == sqlparser.NotEqualStr {
   110  			newExpr = &sqlparser.NotExpr{Expr: newExpr}
   111  		}
   112  		return newExpr, nil
   113  	case sqlparser.InStr, sqlparser.NotInStr:
   114  		valTupleExpr, isValTuple := expr.Right.(sqlparser.ValTuple)
   115  		if !isValTuple {
   116  			return nil, query.NewConverterError(
   117  				"%s: unexpected value type (expected tuple of strings, got %s)",
   118  				query.InvalidExpressionErrMessage,
   119  				sqlparser.String(expr.Right),
   120  			)
   121  		}
   122  		var newExpr sqlparser.Expr = &sqlparser.ParenExpr{
   123  			Expr: c.convertInExpr(expr.Left, valTupleExpr),
   124  		}
   125  		if expr.Operator == sqlparser.NotInStr {
   126  			newExpr = &sqlparser.NotExpr{Expr: newExpr}
   127  		}
   128  		return newExpr, nil
   129  	default:
   130  		// this should never happen since isSupportedKeywordListOperator should already fail
   131  		return nil, query.NewConverterError(
   132  			"%s: operator '%s' not supported for KeywordList type search attribute in `%s`",
   133  			query.InvalidExpressionErrMessage,
   134  			expr.Operator,
   135  			formatComparisonExprStringForError(*expr),
   136  		)
   137  	}
   138  }
   139  
   140  func (c *pgQueryConverter) convertInExpr(
   141  	leftExpr sqlparser.Expr,
   142  	values sqlparser.ValTuple,
   143  ) sqlparser.Expr {
   144  	exprs := make([]sqlparser.Expr, len(values))
   145  	for i, value := range values {
   146  		exprs[i] = c.newJsonContainsExpr(leftExpr, value)
   147  	}
   148  	for len(exprs) > 1 {
   149  		k := 0
   150  		for i := 0; i < len(exprs); i += 2 {
   151  			if i+1 < len(exprs) {
   152  				exprs[k] = &sqlparser.OrExpr{
   153  					Left:  exprs[i],
   154  					Right: exprs[i+1],
   155  				}
   156  			} else {
   157  				exprs[k] = exprs[i]
   158  			}
   159  			k++
   160  		}
   161  		exprs = exprs[:k]
   162  	}
   163  	return exprs[0]
   164  }
   165  
   166  func (c *pgQueryConverter) convertTextComparisonExpr(
   167  	expr *sqlparser.ComparisonExpr,
   168  ) (sqlparser.Expr, error) {
   169  	if !isSupportedTextOperator(expr.Operator) {
   170  		return nil, query.NewConverterError(
   171  			"%s: operator '%s' not supported for Text type search attribute in `%s`",
   172  			query.InvalidExpressionErrMessage,
   173  			expr.Operator,
   174  			formatComparisonExprStringForError(*expr),
   175  		)
   176  	}
   177  	valueExpr, ok := expr.Right.(*unsafeSQLString)
   178  	if !ok {
   179  		return nil, query.NewConverterError(
   180  			"%s: unexpected value type (expected string, got %s)",
   181  			query.InvalidExpressionErrMessage,
   182  			sqlparser.String(expr.Right),
   183  		)
   184  	}
   185  	tokens := tokenizeTextQueryString(valueExpr.Val)
   186  	if len(tokens) == 0 {
   187  		return nil, query.NewConverterError(
   188  			"%s: unexpected value for Text type search attribute (no tokens found in %s)",
   189  			query.InvalidExpressionErrMessage,
   190  			sqlparser.String(expr.Right),
   191  		)
   192  	}
   193  	valueExpr.Val = strings.Join(tokens, " | ")
   194  	var newExpr sqlparser.Expr = &sqlparser.ComparisonExpr{
   195  		Operator: ftsMatchOp,
   196  		Left:     expr.Left,
   197  		Right: &pgCastExpr{
   198  			Value: expr.Right,
   199  			Type:  convertTypeTSQuery,
   200  		},
   201  	}
   202  	if expr.Operator == sqlparser.NotEqualStr {
   203  		newExpr = &sqlparser.NotExpr{Expr: newExpr}
   204  	}
   205  	return newExpr, nil
   206  }
   207  
   208  func (c *pgQueryConverter) newJsonContainsExpr(
   209  	jsonExpr sqlparser.Expr,
   210  	valueExpr sqlparser.Expr,
   211  ) sqlparser.Expr {
   212  	return &sqlparser.ComparisonExpr{
   213  		Operator: jsonContainsOp,
   214  		Left:     jsonExpr,
   215  		Right:    newFuncExpr(jsonBuildArrayFuncName, valueExpr),
   216  	}
   217  }
   218  
   219  func (c *pgQueryConverter) buildSelectStmt(
   220  	namespaceID namespace.ID,
   221  	queryString string,
   222  	pageSize int,
   223  	token *pageToken,
   224  ) (string, []any) {
   225  	var whereClauses []string
   226  	var queryArgs []any
   227  
   228  	whereClauses = append(
   229  		whereClauses,
   230  		fmt.Sprintf("%s = ?", searchattribute.GetSqlDbColName(searchattribute.NamespaceID)),
   231  	)
   232  	queryArgs = append(queryArgs, namespaceID.String())
   233  
   234  	if len(queryString) > 0 {
   235  		whereClauses = append(whereClauses, queryString)
   236  	}
   237  
   238  	if token != nil {
   239  		whereClauses = append(
   240  			whereClauses,
   241  			fmt.Sprintf(
   242  				"((%s = ? AND %s = ? AND %s > ?) OR (%s = ? AND %s < ?) OR %s < ?)",
   243  				sqlparser.String(c.getCoalesceCloseTimeExpr()),
   244  				searchattribute.GetSqlDbColName(searchattribute.StartTime),
   245  				searchattribute.GetSqlDbColName(searchattribute.RunID),
   246  				sqlparser.String(c.getCoalesceCloseTimeExpr()),
   247  				searchattribute.GetSqlDbColName(searchattribute.StartTime),
   248  				sqlparser.String(c.getCoalesceCloseTimeExpr()),
   249  			),
   250  		)
   251  		queryArgs = append(
   252  			queryArgs,
   253  			token.CloseTime,
   254  			token.StartTime,
   255  			token.RunID,
   256  			token.CloseTime,
   257  			token.StartTime,
   258  			token.CloseTime,
   259  		)
   260  	}
   261  
   262  	queryArgs = append(queryArgs, pageSize)
   263  
   264  	return fmt.Sprintf(
   265  		`SELECT %s
   266  		FROM executions_visibility
   267  		WHERE %s
   268  		ORDER BY %s DESC, %s DESC, %s
   269  		LIMIT ?`,
   270  		strings.Join(sqlplugin.DbFields, ", "),
   271  		strings.Join(whereClauses, " AND "),
   272  		sqlparser.String(c.getCoalesceCloseTimeExpr()),
   273  		searchattribute.GetSqlDbColName(searchattribute.StartTime),
   274  		searchattribute.GetSqlDbColName(searchattribute.RunID),
   275  	), queryArgs
   276  }
   277  
   278  func (c *pgQueryConverter) buildCountStmt(
   279  	namespaceID namespace.ID,
   280  	queryString string,
   281  	groupBy []string,
   282  ) (string, []any) {
   283  	var whereClauses []string
   284  	var queryArgs []any
   285  
   286  	whereClauses = append(
   287  		whereClauses,
   288  		fmt.Sprintf("(%s = ?)", searchattribute.GetSqlDbColName(searchattribute.NamespaceID)),
   289  	)
   290  	queryArgs = append(queryArgs, namespaceID.String())
   291  
   292  	if len(queryString) > 0 {
   293  		whereClauses = append(whereClauses, queryString)
   294  	}
   295  
   296  	groupByClause := ""
   297  	if len(groupBy) > 0 {
   298  		groupByClause = fmt.Sprintf("GROUP BY %s", strings.Join(groupBy, ", "))
   299  	}
   300  
   301  	return fmt.Sprintf(
   302  		"SELECT %s FROM executions_visibility WHERE %s %s",
   303  		strings.Join(append(groupBy, "COUNT(*)"), ", "),
   304  		strings.Join(whereClauses, " AND "),
   305  		groupByClause,
   306  	), queryArgs
   307  }