BatchUpdateIssueStatuses updates the status of multiple issues. Returns a map of issueUID -> old status for the updated issues.
(ctx context.Context, projectID string, issueUIDs []int64, newStatus storepb.Issue_Status)
| 445 | // BatchUpdateIssueStatuses updates the status of multiple issues. |
| 446 | // Returns a map of issueUID -> old status for the updated issues. |
| 447 | func (s *Store) BatchUpdateIssueStatuses(ctx context.Context, projectID string, issueUIDs []int64, newStatus storepb.Issue_Status) (map[int64]storepb.Issue_Status, error) { |
| 448 | tx, err := s.GetDB().BeginTx(ctx, nil) |
| 449 | if err != nil { |
| 450 | return nil, errors.Wrapf(err, "failed to begin transaction") |
| 451 | } |
| 452 | defer tx.Rollback() |
| 453 | |
| 454 | // Fetch current issues to validate project membership and get old statuses. |
| 455 | fetchQuery := qb.Q().Space("SELECT id, status FROM issue WHERE id = ANY(?) AND project = ?", issueUIDs, projectID) |
| 456 | fetchSQL, fetchArgs, err := fetchQuery.ToSQL() |
| 457 | if err != nil { |
| 458 | return nil, errors.Wrapf(err, "failed to build fetch sql") |
| 459 | } |
| 460 | |
| 461 | rows, err := tx.QueryContext(ctx, fetchSQL, fetchArgs...) |
| 462 | if err != nil { |
| 463 | return nil, errors.Wrapf(err, "failed to fetch issues") |
| 464 | } |
| 465 | defer rows.Close() |
| 466 | |
| 467 | oldStatuses := make(map[int64]storepb.Issue_Status) |
| 468 | for rows.Next() { |
| 469 | var issueID int64 |
| 470 | var statusString string |
| 471 | if err := rows.Scan(&issueID, &statusString); err != nil { |
| 472 | return nil, errors.Wrapf(err, "failed to scan issue") |
| 473 | } |
| 474 | statusValue, ok := storepb.Issue_Status_value[statusString] |
| 475 | if !ok { |
| 476 | return nil, errors.Errorf("invalid status string: %s", statusString) |
| 477 | } |
| 478 | issueStatus := storepb.Issue_Status(statusValue) |
| 479 | |
| 480 | // Prevent changing status from DONE to other statuses. |
| 481 | if issueStatus == storepb.Issue_DONE && newStatus != storepb.Issue_DONE { |
| 482 | return nil, &common.Error{Code: common.Invalid, Err: errors.Errorf("cannot change status from DONE to %s for issue %d", newStatus.String(), issueID)} |
| 483 | } |
| 484 | |
| 485 | oldStatuses[issueID] = issueStatus |
| 486 | } |
| 487 | if err := rows.Err(); err != nil { |
| 488 | return nil, errors.Wrapf(err, "failed to iterate rows") |
| 489 | } |
| 490 | |
| 491 | // Validate that all requested issues were found in the project. |
| 492 | if len(oldStatuses) != len(issueUIDs) { |
| 493 | return nil, &common.Error{Code: common.Invalid, Err: errors.Errorf("expected %d issues in project %s, found %d", len(issueUIDs), projectID, len(oldStatuses))} |
| 494 | } |
| 495 | |
| 496 | // Update the statuses. |
| 497 | updateQuery := qb.Q().Space("UPDATE issue SET status = ? WHERE id = ANY(?) AND project = ?", newStatus.String(), issueUIDs, projectID) |
| 498 | updateSQL, updateArgs, err := updateQuery.ToSQL() |
| 499 | if err != nil { |
| 500 | return nil, errors.Wrapf(err, "failed to build update sql") |
| 501 | } |
| 502 | |
| 503 | if _, err := tx.ExecContext(ctx, updateSQL, updateArgs...); err != nil { |
| 504 | return nil, errors.Wrapf(err, "failed to update") |