Skip to main content

ralph/queue/operations/batch/
delete.rs

1//! Batch delete and archive operations for tasks.
2//!
3//! Responsibilities:
4//! - Batch delete multiple tasks from the queue.
5//! - Batch archive terminal tasks (Done/Rejected) from active queue to done.
6//!
7//! Does not handle:
8//! - Task filtering/selection (see filters.rs).
9//! - Task updates or field modifications (see update.rs).
10//!
11//! Assumptions/invariants:
12//! - Archive only works on terminal tasks (Done or Rejected status).
13//! - Delete permanently removes tasks without archival.
14
15use crate::contracts::{QueueFile, TaskStatus};
16use anyhow::Result;
17
18use super::{
19    BatchOperationResult, BatchResultCollector, preprocess_batch_ids, validate_task_ids_exist,
20};
21
22/// Batch delete multiple tasks from the queue.
23///
24/// # Arguments
25/// * `queue` - The queue file to modify
26/// * `task_ids` - List of task IDs to delete
27/// * `continue_on_error` - If true, continue processing on individual failures
28///
29/// # Returns
30/// A `BatchOperationResult` with details of successes and failures.
31pub fn batch_delete_tasks(
32    queue: &mut QueueFile,
33    task_ids: &[String],
34    continue_on_error: bool,
35) -> Result<BatchOperationResult> {
36    let unique_ids = preprocess_batch_ids(task_ids, "delete")?;
37
38    // In atomic mode, validate all IDs exist first
39    if !continue_on_error {
40        validate_task_ids_exist(queue, &unique_ids)?;
41    }
42
43    let mut collector = BatchResultCollector::new(unique_ids.len(), continue_on_error, "delete");
44
45    for task_id in &unique_ids {
46        match queue.tasks.iter().position(|t| t.id == *task_id) {
47            Some(idx) => {
48                queue.tasks.remove(idx);
49                collector.record_success(task_id.clone(), Vec::new());
50            }
51            None => {
52                collector.record_failure(
53                    task_id.clone(),
54                    crate::error_messages::task_not_found_batch_failure(task_id),
55                )?;
56            }
57        }
58    }
59
60    Ok(collector.finish())
61}
62
63/// Batch archive terminal tasks (Done/Rejected) from active queue to done.
64///
65/// # Arguments
66/// * `active` - The active queue file to modify
67/// * `done` - The done archive file to append to
68/// * `task_ids` - List of task IDs to archive
69/// * `now_rfc3339` - Current timestamp for updated_at/completed_at
70/// * `continue_on_error` - If true, continue processing on individual failures
71///
72/// # Returns
73/// A `BatchOperationResult` with details of successes and failures.
74pub fn batch_archive_tasks(
75    active: &mut QueueFile,
76    done: &mut QueueFile,
77    task_ids: &[String],
78    now_rfc3339: &str,
79    continue_on_error: bool,
80) -> Result<BatchOperationResult> {
81    let unique_ids = preprocess_batch_ids(task_ids, "archive")?;
82
83    // In atomic mode, validate all IDs exist first
84    if !continue_on_error {
85        validate_task_ids_exist(active, &unique_ids)?;
86    }
87
88    let mut collector = BatchResultCollector::new(unique_ids.len(), continue_on_error, "archive");
89
90    for task_id in &unique_ids {
91        // Find the task in active queue
92        let task_idx = active.tasks.iter().position(|t| t.id == *task_id);
93
94        match task_idx {
95            Some(idx) => {
96                let task = &active.tasks[idx];
97
98                // Check if task is terminal (Done or Rejected)
99                if !matches!(task.status, TaskStatus::Done | TaskStatus::Rejected) {
100                    let err_msg = format!(
101                        "Task {} has status '{}' which is not terminal (Done/Rejected)",
102                        task_id, task.status
103                    );
104                    collector.record_failure(task_id.clone(), err_msg)?;
105                    continue;
106                }
107
108                // Remove task from active and add to done
109                let mut task = active.tasks.remove(idx);
110
111                // Stamp completed_at if missing
112                if task.completed_at.is_none() || task.completed_at.as_ref().unwrap().is_empty() {
113                    task.completed_at = Some(now_rfc3339.to_string());
114                }
115                task.updated_at = Some(now_rfc3339.to_string());
116
117                done.tasks.push(task);
118
119                collector.record_success(task_id.clone(), Vec::new());
120            }
121            None => {
122                collector.record_failure(
123                    task_id.clone(),
124                    crate::error_messages::task_not_found_in_queue(task_id),
125                )?;
126            }
127        }
128    }
129
130    Ok(collector.finish())
131}