Files
tasks/common/persistence/sql/sqlplugin/mysql/query_converter.go
Alan WuandGitHub a3a96c8e5e Support CHASM Count Group By Filtering (#8754)
Support CHASM Count "Group By" Filtering.

## What changed?
* Adds implementation of CHASM Count Executions `Group By` Filter
support.
* Unifies `visibility_store.go` implementation to get count execution
info.
* Adds functional tests for validation.

## Why?
Needed to unblock standalone activities, and fully support CHASM Count
Execution requirements.

## How did you test it?
- [X] built
- [X] run locally and tested manually
- [X] covered by existing tests
- [X] added new unit test(s)
- [X] added new functional test(s)
2025-12-05 20:10:29 -05:00

265 lines
6.7 KiB
Go

package mysql
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/temporalio/sqlparser"
"go.temporal.io/server/common/persistence/sql/sqlplugin"
"go.temporal.io/server/common/persistence/visibility/store/query"
"go.temporal.io/server/common/searchattribute/sadefs"
)
var maxDatetime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
type (
castExpr struct {
sqlparser.Expr
Value sqlparser.Expr
Type *sqlparser.ConvertType
}
memberOfExpr struct {
sqlparser.Expr
Value sqlparser.Expr
JSONArr sqlparser.Expr
}
jsonOverlapsExpr struct {
sqlparser.Expr
JSONDoc1 sqlparser.Expr
JSONDoc2 sqlparser.Expr
}
)
var (
convertTypeDatetime = &sqlparser.ConvertType{Type: "datetime"}
convertTypeJSON = &sqlparser.ConvertType{Type: "json"}
)
var _ sqlparser.Expr = (*castExpr)(nil)
var _ sqlparser.Expr = (*memberOfExpr)(nil)
var _ sqlparser.Expr = (*jsonOverlapsExpr)(nil)
func (node *castExpr) Format(buf *sqlparser.TrackedBuffer) {
buf.Myprintf("cast(%v as %v)", node.Value, node.Type)
}
func (node *memberOfExpr) Format(buf *sqlparser.TrackedBuffer) {
buf.Myprintf("%v member of (%v)", node.Value, node.JSONArr)
}
func (node *jsonOverlapsExpr) Format(buf *sqlparser.TrackedBuffer) {
buf.Myprintf("json_overlaps(%v, %v)", node.JSONDoc1, node.JSONDoc2)
}
type queryConverter struct{}
var _ sqlplugin.VisibilityQueryConverter = (*queryConverter)(nil)
func (c *queryConverter) GetDatetimeFormat() string {
return "2006-01-02 15:04:05.999999"
}
func (c *queryConverter) GetCoalesceCloseTimeExpr() sqlparser.Expr {
return query.NewFuncExpr(
"coalesce",
query.CloseTimeSAColumn,
&castExpr{
Value: query.NewUnsafeSQLString(maxDatetime.Format(c.GetDatetimeFormat())),
Type: convertTypeDatetime,
},
)
}
func (c *queryConverter) ConvertKeywordListComparisonExpr(
operator string,
col *query.SAColumn,
value sqlparser.Expr,
) (sqlparser.Expr, error) {
var negate bool
var newExpr sqlparser.Expr
switch operator {
case sqlparser.EqualStr, sqlparser.NotEqualStr:
newExpr = &memberOfExpr{
Value: value,
JSONArr: col,
}
negate = operator == sqlparser.NotEqualStr
case sqlparser.InStr, sqlparser.NotInStr:
var err error
newExpr, err = c.buildJSONOverlapsExpr(col, value)
if err != nil {
return nil, err
}
negate = operator == sqlparser.NotInStr
default:
// this should never happen since isSupportedKeywordListOperator should already fail
return nil, query.NewConverterError(
"%s: operator '%s' not supported for KeywordList type",
query.InvalidExpressionErrMessage,
operator,
)
}
if negate {
newExpr = &sqlparser.NotExpr{Expr: newExpr}
}
return newExpr, nil
}
func (c *queryConverter) ConvertTextComparisonExpr(
operator string,
col *query.SAColumn,
value sqlparser.Expr,
) (sqlparser.Expr, error) {
// build the following expression:
// `match ({col}) against ({value} in natural language mode)`
var newExpr sqlparser.Expr = &sqlparser.MatchExpr{
Columns: []sqlparser.SelectExpr{&sqlparser.AliasedExpr{Expr: col}},
Expr: value,
Option: sqlparser.NaturalLanguageModeStr,
}
if operator == sqlparser.NotEqualStr {
newExpr = &sqlparser.NotExpr{Expr: newExpr}
}
return newExpr, nil
}
func (c *queryConverter) BuildSelectStmt(
queryParams *query.QueryParams[sqlparser.Expr],
pageSize int,
token *sqlplugin.VisibilityPageToken,
) (string, []any) {
var whereClauses []string
var queryArgs []any
if queryParams.QueryExpr != nil {
if queryString := sqlparser.String(queryParams.QueryExpr); queryString != "" {
whereClauses = append(whereClauses, queryString)
}
}
if token != nil {
whereClauses = append(
whereClauses,
fmt.Sprintf(
"((%s = ? AND %s = ? AND %s > ?) OR (%s = ? AND %s < ?) OR %s < ?)",
sqlparser.String(c.GetCoalesceCloseTimeExpr()),
sadefs.GetSqlDbColName(sadefs.StartTime),
sadefs.GetSqlDbColName(sadefs.RunID),
sqlparser.String(c.GetCoalesceCloseTimeExpr()),
sadefs.GetSqlDbColName(sadefs.StartTime),
sqlparser.String(c.GetCoalesceCloseTimeExpr()),
),
)
queryArgs = append(
queryArgs,
token.CloseTime,
token.StartTime,
token.RunID,
token.CloseTime,
token.StartTime,
token.CloseTime,
)
}
dbFields := make([]string, len(sqlplugin.DbFields))
for i, field := range sqlplugin.DbFields {
dbFields[i] = "ev." + field
}
whereString := ""
if len(whereClauses) > 0 {
whereString = " WHERE " + strings.Join(whereClauses, " AND ")
}
stmt := fmt.Sprintf(
"SELECT %s FROM executions_visibility ev "+
"LEFT JOIN custom_search_attributes USING (%s, %s) "+
"LEFT JOIN chasm_search_attributes USING (%s, %s)"+
"%s "+
"ORDER BY %s DESC, %s DESC, %s LIMIT ?",
strings.Join(dbFields, ", "),
sadefs.GetSqlDbColName(sadefs.NamespaceID),
sadefs.GetSqlDbColName(sadefs.RunID),
sadefs.GetSqlDbColName(sadefs.NamespaceID),
sadefs.GetSqlDbColName(sadefs.RunID),
whereString,
sqlparser.String(c.GetCoalesceCloseTimeExpr()),
sadefs.GetSqlDbColName(sadefs.StartTime),
sadefs.GetSqlDbColName(sadefs.RunID),
)
queryArgs = append(queryArgs, pageSize)
return stmt, queryArgs
}
func (c *queryConverter) BuildCountStmt(
queryParams *query.QueryParams[sqlparser.Expr],
) (string, []any) {
whereString := ""
if queryParams.QueryExpr != nil {
whereString = sqlparser.String(queryParams.QueryExpr)
if whereString != "" {
whereString = " WHERE " + whereString
}
}
groupBy := make([]string, 0, len(queryParams.GroupBy)+1)
for _, field := range queryParams.GroupBy {
groupBy = append(groupBy, sadefs.GetSqlDbColName(field.FieldName))
}
groupByClause := ""
if len(queryParams.GroupBy) > 0 {
groupByClause = fmt.Sprintf(" GROUP BY %s", strings.Join(groupBy, ", "))
}
return fmt.Sprintf(
"SELECT %s FROM executions_visibility ev "+
"LEFT JOIN custom_search_attributes USING (%s, %s) "+
"LEFT JOIN chasm_search_attributes USING (%s, %s)"+
"%s"+
"%s",
strings.Join(append(groupBy, "COUNT(*)"), ", "),
sadefs.GetSqlDbColName(sadefs.NamespaceID),
sadefs.GetSqlDbColName(sadefs.RunID),
sadefs.GetSqlDbColName(sadefs.NamespaceID),
sadefs.GetSqlDbColName(sadefs.RunID),
whereString,
groupByClause,
), nil
}
func (c *queryConverter) buildJSONOverlapsExpr(
col *query.SAColumn,
value sqlparser.Expr,
) (*jsonOverlapsExpr, error) {
valTuple, isValTuple := value.(sqlparser.ValTuple)
if !isValTuple {
return nil, query.NewConverterError(
"%s: unexpected value type (expected tuple of strings, got %s)",
query.InvalidExpressionErrMessage,
sqlparser.String(value),
)
}
values, err := query.GetUnsafeStringTupleValues(valTuple)
if err != nil {
return nil, err
}
jsonValue, err := json.Marshal(values)
if err != nil {
return nil, err
}
return &jsonOverlapsExpr{
JSONDoc1: col,
JSONDoc2: &castExpr{
Value: query.NewUnsafeSQLString(string(jsonValue)),
Type: convertTypeJSON,
},
}, nil
}