This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / db / pipeline.go
8.2 kB 416 lines
1package db 2 3import ( 4 "context" 5 "fmt" 6 "slices" 7 "strings" 8 "time" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 "tangled.org/core/appview/models" 12 "tangled.org/core/orm" 13) 14 15func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) { 16 var pipelines []models.Pipeline 17 18 var conditions []string 19 var args []any 20 for _, filter := range filters { 21 conditions = append(conditions, filter.Condition()) 22 args = append(args, filter.Arg()...) 23 } 24 25 whereClause := "" 26 if conditions != nil { 27 whereClause = " where " + strings.Join(conditions, " and ") 28 } 29 30 query := fmt.Sprintf(`select id, rkey, knot, repo_owner, repo_name, sha, created from pipelines %s`, whereClause) 31 32 rows, err := e.Query(query, args...) 33 34 if err != nil { 35 return nil, err 36 } 37 defer rows.Close() 38 39 for rows.Next() { 40 var pipeline models.Pipeline 41 var createdAt string 42 err = rows.Scan( 43 &pipeline.Id, 44 &pipeline.Rkey, 45 &pipeline.Knot, 46 &pipeline.RepoOwner, 47 &pipeline.RepoName, 48 &pipeline.Sha, 49 &createdAt, 50 ) 51 if err != nil { 52 return nil, err 53 } 54 55 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 56 pipeline.Created = t 57 } 58 59 pipelines = append(pipelines, pipeline) 60 } 61 62 if err = rows.Err(); err != nil { 63 return nil, err 64 } 65 66 return pipelines, nil 67} 68 69func AddPipeline(e Execer, pipeline models.Pipeline) error { 70 args := []any{ 71 pipeline.Rkey, 72 pipeline.Knot, 73 pipeline.RepoOwner, 74 pipeline.RepoName, 75 pipeline.TriggerId, 76 pipeline.Sha, 77 } 78 79 placeholders := make([]string, len(args)) 80 for i := range placeholders { 81 placeholders[i] = "?" 82 } 83 84 query := fmt.Sprintf(` 85 insert or ignore into pipelines ( 86 rkey, 87 knot, 88 repo_owner, 89 repo_name, 90 trigger_id, 91 sha 92 ) values (%s) 93 `, strings.Join(placeholders, ",")) 94 95 _, err := e.Exec(query, args...) 96 97 return err 98} 99 100func AddTrigger(e Execer, trigger models.Trigger) (int64, error) { 101 args := []any{ 102 trigger.Kind, 103 trigger.PushRef, 104 trigger.PushNewSha, 105 trigger.PushOldSha, 106 trigger.PRSourceBranch, 107 trigger.PRTargetBranch, 108 trigger.PRSourceSha, 109 trigger.PRAction, 110 } 111 112 placeholders := make([]string, len(args)) 113 for i := range placeholders { 114 placeholders[i] = "?" 115 } 116 117 query := fmt.Sprintf(`insert or ignore into triggers ( 118 kind, 119 push_ref, 120 push_new_sha, 121 push_old_sha, 122 pr_source_branch, 123 pr_target_branch, 124 pr_source_sha, 125 pr_action 126 ) values (%s)`, strings.Join(placeholders, ",")) 127 128 res, err := e.Exec(query, args...) 129 if err != nil { 130 return 0, err 131 } 132 133 return res.LastInsertId() 134} 135 136func AddPipelineStatus(ctx context.Context, e Execer, status models.PipelineStatus) error { 137 args := []any{ 138 status.Spindle, 139 status.Rkey, 140 status.PipelineKnot, 141 status.PipelineRkey, 142 status.Workflow, 143 status.Status, 144 status.Error, 145 status.ExitCode, 146 status.Created.Format(time.RFC3339), 147 } 148 149 placeholders := make([]string, len(args)) 150 for i := range placeholders { 151 placeholders[i] = "?" 152 } 153 154 query := fmt.Sprintf(` 155 insert or ignore into pipeline_statuses ( 156 spindle, 157 rkey, 158 pipeline_knot, 159 pipeline_rkey, 160 workflow, 161 status, 162 error, 163 exit_code, 164 created 165 ) values (%s) 166 `, strings.Join(placeholders, ",")) 167 168 _, err := e.ExecContext(ctx, query, args...) 169 return err 170} 171 172// this is a mega query, but the most useful one: 173// get N pipelines, for each one get the latest status of its N workflows 174// 175// the pipelines table is aliased to `p` 176// the triggers table is aliased to `t` 177func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) { 178 var conditions []string 179 var args []any 180 for _, filter := range filters { 181 conditions = append(conditions, filter.Condition()) 182 args = append(args, filter.Arg()...) 183 } 184 185 whereClause := "" 186 if conditions != nil { 187 whereClause = " where " + strings.Join(conditions, " and ") 188 } 189 190 query := fmt.Sprintf(` 191 select 192 p.id, 193 p.knot, 194 p.rkey, 195 p.repo_owner, 196 p.repo_name, 197 p.sha, 198 p.created, 199 t.id, 200 t.kind, 201 t.push_ref, 202 t.push_new_sha, 203 t.push_old_sha, 204 t.pr_source_branch, 205 t.pr_target_branch, 206 t.pr_source_sha, 207 t.pr_action 208 from 209 pipelines p 210 join 211 triggers t ON p.trigger_id = t.id 212 %s 213 order by p.created desc 214 limit %d 215 `, whereClause, limit) 216 217 rows, err := e.Query(query, args...) 218 if err != nil { 219 return nil, err 220 } 221 defer rows.Close() 222 223 pipelines := make(map[syntax.ATURI]models.Pipeline) 224 for rows.Next() { 225 var p models.Pipeline 226 var t models.Trigger 227 var created string 228 229 err := rows.Scan( 230 &p.Id, 231 &p.Knot, 232 &p.Rkey, 233 &p.RepoOwner, 234 &p.RepoName, 235 &p.Sha, 236 &created, 237 &p.TriggerId, 238 &t.Kind, 239 &t.PushRef, 240 &t.PushNewSha, 241 &t.PushOldSha, 242 &t.PRSourceBranch, 243 &t.PRTargetBranch, 244 &t.PRSourceSha, 245 &t.PRAction, 246 ) 247 if err != nil { 248 return nil, err 249 } 250 251 p.Created, err = time.Parse(time.RFC3339, created) 252 if err != nil { 253 return nil, fmt.Errorf("invalid pipeline created timestamp %q: %w", created, err) 254 } 255 256 t.Id = p.TriggerId 257 p.Trigger = &t 258 p.Statuses = make(map[string]models.WorkflowStatus) 259 260 pipelines[p.AtUri()] = p 261 } 262 263 // get all statuses 264 // the where clause here is of the form: 265 // 266 // where (pipeline_knot = k1 and pipeline_rkey = r1) 267 // or (pipeline_knot = k2 and pipeline_rkey = r2) 268 conditions = nil 269 args = nil 270 for _, p := range pipelines { 271 knotFilter := orm.FilterEq("pipeline_knot", p.Knot) 272 rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey) 273 conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition())) 274 args = append(args, p.Knot) 275 args = append(args, p.Rkey) 276 } 277 whereClause = "" 278 if conditions != nil { 279 whereClause = "where " + strings.Join(conditions, " or ") 280 } 281 query = fmt.Sprintf(` 282 select 283 id, spindle, rkey, pipeline_knot, pipeline_rkey, created, workflow, status, error, exit_code 284 from 285 pipeline_statuses 286 %s 287 `, whereClause) 288 289 rows, err = e.Query(query, args...) 290 if err != nil { 291 return nil, err 292 } 293 defer rows.Close() 294 295 for rows.Next() { 296 var ps models.PipelineStatus 297 var created string 298 299 err := rows.Scan( 300 &ps.ID, 301 &ps.Spindle, 302 &ps.Rkey, 303 &ps.PipelineKnot, 304 &ps.PipelineRkey, 305 &created, 306 &ps.Workflow, 307 &ps.Status, 308 &ps.Error, 309 &ps.ExitCode, 310 ) 311 if err != nil { 312 return nil, err 313 } 314 315 ps.Created, err = time.Parse(time.RFC3339, created) 316 if err != nil { 317 return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err) 318 } 319 320 pipelineAt := ps.PipelineAt() 321 322 // extract 323 pipeline, ok := pipelines[pipelineAt] 324 if !ok { 325 continue 326 } 327 statuses, _ := pipeline.Statuses[ps.Workflow] 328 if !ok { 329 pipeline.Statuses[ps.Workflow] = models.WorkflowStatus{} 330 } 331 332 // append 333 statuses.Data = append(statuses.Data, ps) 334 335 // reassign 336 pipeline.Statuses[ps.Workflow] = statuses 337 pipelines[pipelineAt] = pipeline 338 } 339 340 var all []models.Pipeline 341 for _, p := range pipelines { 342 for _, s := range p.Statuses { 343 slices.SortFunc(s.Data, func(a, b models.PipelineStatus) int { 344 if a.Created.After(b.Created) { 345 return 1 346 } 347 if a.Created.Before(b.Created) { 348 return -1 349 } 350 if a.ID > b.ID { 351 return 1 352 } 353 if a.ID < b.ID { 354 return -1 355 } 356 return 0 357 }) 358 } 359 all = append(all, p) 360 } 361 362 // sort pipelines by date 363 slices.SortFunc(all, func(a, b models.Pipeline) int { 364 if a.Created.After(b.Created) { 365 return -1 366 } 367 return 1 368 }) 369 370 return all, nil 371} 372 373// the pipelines table is aliased to `p` 374// the triggers table is aliased to `t` 375func GetTotalPipelineStatuses(e Execer, filters ...orm.Filter) (int64, error) { 376 var conditions []string 377 var args []any 378 for _, filter := range filters { 379 conditions = append(conditions, filter.Condition()) 380 args = append(args, filter.Arg()...) 381 } 382 383 whereClause := "" 384 if conditions != nil { 385 whereClause = " where " + strings.Join(conditions, " and ") 386 } 387 388 query := fmt.Sprintf(` 389 select 390 count(1) 391 from 392 pipelines p 393 join 394 triggers t ON p.trigger_id = t.id 395 %s 396 `, whereClause) 397 398 rows, err := e.Query(query, args...) 399 if err != nil { 400 return 0, err 401 } 402 defer rows.Close() 403 404 for rows.Next() { 405 var count int64 406 err := rows.Scan(&count) 407 if err != nil { 408 return 0, err 409 } 410 411 return count, nil 412 } 413 414 // unreachable 415 return 0, nil 416}