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 }