go.temporal.io/server@v1.23.0/common/persistence/sql/sqlplugin/visibility.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 sqlplugin
    26  
    27  import (
    28  	"context"
    29  	"database/sql"
    30  	"database/sql/driver"
    31  	"encoding/json"
    32  	"fmt"
    33  	"reflect"
    34  	"strings"
    35  	"time"
    36  
    37  	"github.com/iancoleman/strcase"
    38  
    39  	enumspb "go.temporal.io/api/enums/v1"
    40  	"go.temporal.io/api/serviceerror"
    41  	"go.temporal.io/server/common/searchattribute"
    42  )
    43  
    44  type (
    45  	// VisibilitySearchAttributes represents the search attributes json
    46  	// in executions_visibility table
    47  	VisibilitySearchAttributes map[string]interface{}
    48  
    49  	// VisibilityRow represents a row in executions_visibility table
    50  	VisibilityRow struct {
    51  		NamespaceID          string
    52  		RunID                string
    53  		WorkflowTypeName     string
    54  		WorkflowID           string
    55  		StartTime            time.Time
    56  		ExecutionTime        time.Time
    57  		Status               int32
    58  		CloseTime            *time.Time
    59  		HistoryLength        *int64
    60  		HistorySizeBytes     *int64
    61  		ExecutionDuration    *time.Duration
    62  		StateTransitionCount *int64
    63  		Memo                 []byte
    64  		Encoding             string
    65  		TaskQueue            string
    66  		SearchAttributes     *VisibilitySearchAttributes
    67  		ParentWorkflowID     *string
    68  		ParentRunID          *string
    69  	}
    70  
    71  	// VisibilitySelectFilter contains the column names within executions_visibility table that
    72  	// can be used to filter results through a WHERE clause
    73  	VisibilitySelectFilter struct {
    74  		NamespaceID      string
    75  		RunID            *string
    76  		WorkflowID       *string
    77  		WorkflowTypeName *string
    78  		Status           int32
    79  		MinTime          *time.Time
    80  		MaxTime          *time.Time
    81  		PageSize         *int
    82  
    83  		Query     string
    84  		QueryArgs []interface{}
    85  		GroupBy   []string
    86  	}
    87  
    88  	VisibilityGetFilter struct {
    89  		NamespaceID string
    90  		RunID       string
    91  	}
    92  
    93  	VisibilityDeleteFilter struct {
    94  		NamespaceID string
    95  		RunID       string
    96  	}
    97  
    98  	VisibilityCountRow struct {
    99  		GroupValues []any
   100  		Count       int64
   101  	}
   102  
   103  	Visibility interface {
   104  		// InsertIntoVisibility inserts a row into visibility table. If a row already exist,
   105  		// no changes will be made by this API
   106  		InsertIntoVisibility(ctx context.Context, row *VisibilityRow) (sql.Result, error)
   107  		// ReplaceIntoVisibility deletes old row (if it exist) and inserts new row into visibility table
   108  		ReplaceIntoVisibility(ctx context.Context, row *VisibilityRow) (sql.Result, error)
   109  		// SelectFromVisibility returns one or more rows from visibility table
   110  		// Required filter params:
   111  		// - getClosedWorkflowExecution - retrieves single row - {namespaceID, runID, closed=true}
   112  		// - All other queries retrieve multiple rows (range):
   113  		//   - MUST specify following required params:
   114  		//     - namespaceID, minStartTime, maxStartTime, runID and pageSize where some or all of these may come from previous page token
   115  		//   - OPTIONALLY specify one of following params
   116  		//     - workflowID, workflowTypeName, status (along with closed=true)
   117  		SelectFromVisibility(ctx context.Context, filter VisibilitySelectFilter) ([]VisibilityRow, error)
   118  		GetFromVisibility(ctx context.Context, filter VisibilityGetFilter) (*VisibilityRow, error)
   119  		DeleteFromVisibility(ctx context.Context, filter VisibilityDeleteFilter) (sql.Result, error)
   120  		CountFromVisibility(ctx context.Context, filter VisibilitySelectFilter) (int64, error)
   121  		CountGroupByFromVisibility(ctx context.Context, filter VisibilitySelectFilter) ([]VisibilityCountRow, error)
   122  	}
   123  )
   124  
   125  var _ sql.Scanner = (*VisibilitySearchAttributes)(nil)
   126  var _ driver.Valuer = (*VisibilitySearchAttributes)(nil)
   127  
   128  var DbFields = getDbFields()
   129  
   130  func (vsa *VisibilitySearchAttributes) Scan(src interface{}) error {
   131  	if src == nil {
   132  		return nil
   133  	}
   134  	switch v := src.(type) {
   135  	case []byte:
   136  		return json.Unmarshal(v, &vsa)
   137  	case string:
   138  		return json.Unmarshal([]byte(v), &vsa)
   139  	default:
   140  		return fmt.Errorf("unsupported type for VisibilitySearchAttributes: %T", v)
   141  	}
   142  }
   143  
   144  func (vsa VisibilitySearchAttributes) Value() (driver.Value, error) {
   145  	if vsa == nil {
   146  		return nil, nil
   147  	}
   148  	return json.Marshal(vsa)
   149  }
   150  
   151  func ParseCountGroupByRows(rows *sql.Rows, groupBy []string) ([]VisibilityCountRow, error) {
   152  	// Number of columns is number of group by fields plus the count column.
   153  	rowValues := make([]any, len(groupBy)+1)
   154  	for i := range rowValues {
   155  		rowValues[i] = new(any)
   156  	}
   157  
   158  	var res []VisibilityCountRow
   159  	for rows.Next() {
   160  		err := rows.Scan(rowValues...)
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		groupValues := make([]any, len(groupBy))
   165  		for i := range groupBy {
   166  			groupValues[i], err = parseCountGroupByGroupValue(groupBy[i], *(rowValues[i].(*any)))
   167  			if err != nil {
   168  				return nil, err
   169  			}
   170  		}
   171  		count := *(rowValues[len(rowValues)-1].(*any))
   172  		res = append(res, VisibilityCountRow{
   173  			GroupValues: groupValues,
   174  			Count:       count.(int64),
   175  		})
   176  	}
   177  	return res, nil
   178  }
   179  
   180  func parseCountGroupByGroupValue(fieldName string, value any) (any, error) {
   181  	switch fieldName {
   182  	case searchattribute.ExecutionStatus:
   183  		switch typedValue := value.(type) {
   184  		case int:
   185  			return enumspb.WorkflowExecutionStatus(typedValue).String(), nil
   186  		case int32:
   187  			return enumspb.WorkflowExecutionStatus(typedValue).String(), nil
   188  		case int64:
   189  			return enumspb.WorkflowExecutionStatus(typedValue).String(), nil
   190  		default:
   191  			// This should never happen.
   192  			return nil, serviceerror.NewInternal(
   193  				fmt.Sprintf(
   194  					"Unable to parse %s value from DB (got: %v of type: %T, expected type: integer)",
   195  					searchattribute.ExecutionStatus,
   196  					value,
   197  					value,
   198  				),
   199  			)
   200  		}
   201  	default:
   202  		return value, nil
   203  	}
   204  }
   205  
   206  func getDbFields() []string {
   207  	t := reflect.TypeOf(VisibilityRow{})
   208  	dbFields := make([]string, t.NumField())
   209  	for i := 0; i < t.NumField(); i++ {
   210  		f := t.Field(i)
   211  		dbFields[i] = f.Tag.Get("db")
   212  		if dbFields[i] == "" {
   213  			dbFields[i] = strcase.ToSnake(f.Name)
   214  		}
   215  	}
   216  	return dbFields
   217  }
   218  
   219  // TODO (rodrigozhou): deprecate with standard visibility code.
   220  // GenerateSelectQuery generates the SELECT query based on the fields of VisibilitySelectFilter
   221  // for backward compatibility of any use case using old format (eg: unit test).
   222  // It will be removed after all use cases change to use query converter.
   223  func GenerateSelectQuery(
   224  	filter *VisibilitySelectFilter,
   225  	convertToDbDateTime func(time.Time) time.Time,
   226  ) error {
   227  	whereClauses := make([]string, 0, 10)
   228  	queryArgs := make([]interface{}, 0, 10)
   229  
   230  	whereClauses = append(
   231  		whereClauses,
   232  		fmt.Sprintf("%s = ?", searchattribute.GetSqlDbColName(searchattribute.NamespaceID)),
   233  	)
   234  	queryArgs = append(queryArgs, filter.NamespaceID)
   235  
   236  	if filter.WorkflowID != nil {
   237  		whereClauses = append(
   238  			whereClauses,
   239  			fmt.Sprintf("%s = ?", searchattribute.GetSqlDbColName(searchattribute.WorkflowID)),
   240  		)
   241  		queryArgs = append(queryArgs, *filter.WorkflowID)
   242  	}
   243  
   244  	if filter.WorkflowTypeName != nil {
   245  		whereClauses = append(
   246  			whereClauses,
   247  			fmt.Sprintf("%s = ?", searchattribute.GetSqlDbColName(searchattribute.WorkflowType)),
   248  		)
   249  		queryArgs = append(queryArgs, *filter.WorkflowTypeName)
   250  	}
   251  
   252  	timeAttr := searchattribute.StartTime
   253  	if filter.Status != int32(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING) {
   254  		timeAttr = searchattribute.CloseTime
   255  	}
   256  	if filter.Status == int32(enumspb.WORKFLOW_EXECUTION_STATUS_UNSPECIFIED) {
   257  		whereClauses = append(
   258  			whereClauses,
   259  			fmt.Sprintf("%s != ?", searchattribute.GetSqlDbColName(searchattribute.ExecutionStatus)),
   260  		)
   261  		queryArgs = append(queryArgs, int32(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING))
   262  	} else {
   263  		whereClauses = append(
   264  			whereClauses,
   265  			fmt.Sprintf("%s = ?", searchattribute.GetSqlDbColName(searchattribute.ExecutionStatus)),
   266  		)
   267  		queryArgs = append(queryArgs, filter.Status)
   268  	}
   269  
   270  	switch {
   271  	case filter.RunID != nil && filter.MinTime == nil && filter.Status != 1:
   272  		whereClauses = append(
   273  			whereClauses,
   274  			fmt.Sprintf("%s = ?", searchattribute.GetSqlDbColName(searchattribute.RunID)),
   275  		)
   276  		queryArgs = append(
   277  			queryArgs,
   278  			*filter.RunID,
   279  			1, // page size arg
   280  		)
   281  	case filter.RunID != nil && filter.MinTime != nil && filter.MaxTime != nil && filter.PageSize != nil:
   282  		// pagination filters
   283  		*filter.MinTime = convertToDbDateTime(*filter.MinTime)
   284  		*filter.MaxTime = convertToDbDateTime(*filter.MaxTime)
   285  		whereClauses = append(
   286  			whereClauses,
   287  			fmt.Sprintf("%s >= ?", searchattribute.GetSqlDbColName(timeAttr)),
   288  			fmt.Sprintf("%s <= ?", searchattribute.GetSqlDbColName(timeAttr)),
   289  			fmt.Sprintf(
   290  				"((%s = ? AND %s > ?) OR %s < ?)",
   291  				searchattribute.GetSqlDbColName(timeAttr),
   292  				searchattribute.GetSqlDbColName(searchattribute.RunID),
   293  				searchattribute.GetSqlDbColName(timeAttr),
   294  			),
   295  		)
   296  		queryArgs = append(
   297  			queryArgs,
   298  			*filter.MinTime,
   299  			*filter.MaxTime,
   300  			*filter.MaxTime,
   301  			*filter.RunID,
   302  			*filter.MaxTime,
   303  			*filter.PageSize,
   304  		)
   305  	default:
   306  		return fmt.Errorf("invalid query filter")
   307  	}
   308  
   309  	filter.Query = fmt.Sprintf(
   310  		`SELECT %s FROM executions_visibility
   311  		WHERE %s
   312  		ORDER BY %s DESC, %s
   313  		LIMIT ?`,
   314  		strings.Join(DbFields, ", "),
   315  		strings.Join(whereClauses, " AND "),
   316  		searchattribute.GetSqlDbColName(timeAttr),
   317  		searchattribute.GetSqlDbColName(searchattribute.RunID),
   318  	)
   319  	filter.QueryArgs = queryArgs
   320  	return nil
   321  }