things3_core/database/mutations/
bulk.rs1use crate::{
2 database::{conversions::naive_date_to_things_timestamp, validators, ThingsDatabase},
3 error::{Result as ThingsResult, ThingsError},
4};
5use chrono::Utc;
6use sqlx::Row;
7use tracing::{info, instrument};
8
9impl ThingsDatabase {
10 const MAX_BULK_BATCH_SIZE: usize = 1000;
13
14 #[instrument(skip(self))]
27 pub async fn bulk_move(
28 &self,
29 request: crate::models::BulkMoveRequest,
30 ) -> ThingsResult<crate::models::BulkOperationResult> {
31 if request.task_uuids.is_empty() {
33 return Err(ThingsError::validation("Task UUIDs cannot be empty"));
34 }
35 if request.task_uuids.len() > Self::MAX_BULK_BATCH_SIZE {
36 return Err(ThingsError::validation(format!(
37 "Batch size {} exceeds maximum of {}",
38 request.task_uuids.len(),
39 Self::MAX_BULK_BATCH_SIZE
40 )));
41 }
42 if request.project_uuid.is_none() && request.area_uuid.is_none() {
43 return Err(ThingsError::validation(
44 "Must specify either project_uuid or area_uuid",
45 ));
46 }
47
48 if let Some(project_uuid) = &request.project_uuid {
50 validators::validate_project_exists(&self.pool, project_uuid).await?;
51 }
52 if let Some(area_uuid) = &request.area_uuid {
53 validators::validate_area_exists(&self.pool, area_uuid).await?;
54 }
55
56 let mut tx = self
58 .pool
59 .begin()
60 .await
61 .map_err(|e| ThingsError::unknown(format!("Failed to begin transaction: {e}")))?;
62
63 let placeholders = request
65 .task_uuids
66 .iter()
67 .map(|_| "?")
68 .collect::<Vec<_>>()
69 .join(",");
70 let query_str = format!(
71 "SELECT uuid FROM TMTask WHERE uuid IN ({}) AND trashed = 0",
72 placeholders
73 );
74
75 let mut query = sqlx::query(&query_str);
76 for id in &request.task_uuids {
77 query = query.bind(id.as_str());
78 }
79
80 let found_uuids: Vec<String> = query
81 .fetch_all(&mut *tx)
82 .await
83 .map_err(|e| ThingsError::unknown(format!("Failed to validate tasks: {e}")))?
84 .iter()
85 .map(|row| row.get("uuid"))
86 .collect();
87
88 if found_uuids.len() != request.task_uuids.len() {
90 for id in &request.task_uuids {
92 if !found_uuids.contains(&id.to_string()) {
93 tx.rollback().await.ok();
94 return Err(ThingsError::TaskNotFound {
95 uuid: id.to_string(),
96 });
97 }
98 }
99 }
100
101 let now = Utc::now().timestamp() as f64;
103 let placeholders = request
104 .task_uuids
105 .iter()
106 .map(|_| "?")
107 .collect::<Vec<_>>()
108 .join(",");
109 let query_str = format!(
110 "UPDATE TMTask SET project = ?, area = ?, userModificationDate = ? WHERE uuid IN ({})",
111 placeholders
112 );
113
114 let mut query = sqlx::query(&query_str)
115 .bind(request.project_uuid.map(|u| u.into_string()))
116 .bind(request.area_uuid.map(|u| u.into_string()))
117 .bind(now);
118
119 for id in &request.task_uuids {
120 query = query.bind(id.as_str());
121 }
122
123 query
124 .execute(&mut *tx)
125 .await
126 .map_err(|e| ThingsError::unknown(format!("Failed to bulk move tasks: {e}")))?;
127
128 tx.commit()
130 .await
131 .map_err(|e| ThingsError::unknown(format!("Failed to commit transaction: {e}")))?;
132
133 info!("Bulk moved {} task(s)", request.task_uuids.len());
134 Ok(crate::models::BulkOperationResult {
135 success: true,
136 processed_count: request.task_uuids.len(),
137 message: format!("Successfully moved {} task(s)", request.task_uuids.len()),
138 })
139 }
140
141 #[instrument(skip(self))]
154 pub async fn bulk_update_dates(
155 &self,
156 request: crate::models::BulkUpdateDatesRequest,
157 ) -> ThingsResult<crate::models::BulkOperationResult> {
158 use crate::database::{safe_things_date_to_naive_date, validate_date_range};
159
160 if request.task_uuids.is_empty() {
162 return Err(ThingsError::validation("Task UUIDs cannot be empty"));
163 }
164 if request.task_uuids.len() > Self::MAX_BULK_BATCH_SIZE {
165 return Err(ThingsError::validation(format!(
166 "Batch size {} exceeds maximum of {}",
167 request.task_uuids.len(),
168 Self::MAX_BULK_BATCH_SIZE
169 )));
170 }
171
172 if let (Some(start), Some(deadline)) = (request.start_date, request.deadline) {
174 validate_date_range(Some(start), Some(deadline))?;
175 }
176
177 let mut tx = self
179 .pool
180 .begin()
181 .await
182 .map_err(|e| ThingsError::unknown(format!("Failed to begin transaction: {e}")))?;
183
184 let placeholders = request
186 .task_uuids
187 .iter()
188 .map(|_| "?")
189 .collect::<Vec<_>>()
190 .join(",");
191 let query_str = format!(
192 "SELECT uuid, startDate, deadline FROM TMTask WHERE uuid IN ({}) AND trashed = 0",
193 placeholders
194 );
195
196 let mut query = sqlx::query(&query_str);
197 for id in &request.task_uuids {
198 query = query.bind(id.as_str());
199 }
200
201 let rows = query
202 .fetch_all(&mut *tx)
203 .await
204 .map_err(|e| ThingsError::unknown(format!("Failed to validate tasks: {e}")))?;
205
206 if rows.len() != request.task_uuids.len() {
208 let found_uuids: Vec<String> = rows.iter().map(|row| row.get("uuid")).collect();
210 for id in &request.task_uuids {
211 if !found_uuids.contains(&id.to_string()) {
212 tx.rollback().await.ok();
213 return Err(ThingsError::TaskNotFound {
214 uuid: id.to_string(),
215 });
216 }
217 }
218 }
219
220 for row in &rows {
222 let current_start: Option<i64> = row.get("startDate");
223 let current_deadline: Option<i64> = row.get("deadline");
224
225 let final_start = if request.clear_start_date {
226 None
227 } else if let Some(new_start) = request.start_date {
228 Some(new_start)
229 } else {
230 current_start.and_then(|ts| safe_things_date_to_naive_date(ts).ok())
231 };
232
233 let final_deadline = if request.clear_deadline {
234 None
235 } else if let Some(new_deadline) = request.deadline {
236 Some(new_deadline)
237 } else {
238 current_deadline.and_then(|ts| safe_things_date_to_naive_date(ts).ok())
239 };
240
241 validate_date_range(final_start, final_deadline)?;
242 }
243
244 let now = Utc::now().timestamp() as f64;
246 let placeholders = request
247 .task_uuids
248 .iter()
249 .map(|_| "?")
250 .collect::<Vec<_>>()
251 .join(",");
252
253 let start_date_value = if request.clear_start_date {
254 None
255 } else {
256 request.start_date.map(naive_date_to_things_timestamp)
257 };
258
259 let deadline_value = if request.clear_deadline {
260 None
261 } else {
262 request.deadline.map(naive_date_to_things_timestamp)
263 };
264
265 let query_str = format!(
266 "UPDATE TMTask SET startDate = ?, deadline = ?, userModificationDate = ? WHERE uuid IN ({})",
267 placeholders
268 );
269
270 let mut query = sqlx::query(&query_str)
271 .bind(start_date_value)
272 .bind(deadline_value)
273 .bind(now);
274
275 for id in &request.task_uuids {
276 query = query.bind(id.as_str());
277 }
278
279 query
280 .execute(&mut *tx)
281 .await
282 .map_err(|e| ThingsError::unknown(format!("Failed to bulk update dates: {e}")))?;
283
284 tx.commit()
285 .await
286 .map_err(|e| ThingsError::unknown(format!("Failed to commit transaction: {e}")))?;
287
288 info!(
289 "Bulk updated dates for {} task(s)",
290 request.task_uuids.len()
291 );
292 Ok(crate::models::BulkOperationResult {
293 success: true,
294 processed_count: request.task_uuids.len(),
295 message: format!(
296 "Successfully updated dates for {} task(s)",
297 request.task_uuids.len()
298 ),
299 })
300 }
301
302 #[instrument(skip(self))]
313 pub async fn bulk_complete(
314 &self,
315 request: crate::models::BulkCompleteRequest,
316 ) -> ThingsResult<crate::models::BulkOperationResult> {
317 if request.task_uuids.is_empty() {
319 return Err(ThingsError::validation("Task UUIDs cannot be empty"));
320 }
321 if request.task_uuids.len() > Self::MAX_BULK_BATCH_SIZE {
322 return Err(ThingsError::validation(format!(
323 "Batch size {} exceeds maximum of {}",
324 request.task_uuids.len(),
325 Self::MAX_BULK_BATCH_SIZE
326 )));
327 }
328
329 let mut tx = self
331 .pool
332 .begin()
333 .await
334 .map_err(|e| ThingsError::unknown(format!("Failed to begin transaction: {e}")))?;
335
336 let placeholders = request
338 .task_uuids
339 .iter()
340 .map(|_| "?")
341 .collect::<Vec<_>>()
342 .join(",");
343 let query_str = format!(
344 "SELECT uuid FROM TMTask WHERE uuid IN ({}) AND trashed = 0",
345 placeholders
346 );
347
348 let mut query = sqlx::query(&query_str);
349 for id in &request.task_uuids {
350 query = query.bind(id.as_str());
351 }
352
353 let found_uuids: Vec<String> = query
354 .fetch_all(&mut *tx)
355 .await
356 .map_err(|e| ThingsError::unknown(format!("Failed to validate tasks: {e}")))?
357 .iter()
358 .map(|row| row.get("uuid"))
359 .collect();
360
361 if found_uuids.len() != request.task_uuids.len() {
363 for id in &request.task_uuids {
365 if !found_uuids.contains(&id.to_string()) {
366 tx.rollback().await.ok();
367 return Err(ThingsError::TaskNotFound {
368 uuid: id.to_string(),
369 });
370 }
371 }
372 }
373
374 let now = Utc::now().timestamp() as f64;
376 let placeholders = request
377 .task_uuids
378 .iter()
379 .map(|_| "?")
380 .collect::<Vec<_>>()
381 .join(",");
382 let query_str = format!(
383 "UPDATE TMTask SET status = 3, stopDate = ?, userModificationDate = ? WHERE uuid IN ({})",
384 placeholders
385 );
386
387 let mut query = sqlx::query(&query_str).bind(now).bind(now);
388
389 for id in &request.task_uuids {
390 query = query.bind(id.as_str());
391 }
392
393 query
394 .execute(&mut *tx)
395 .await
396 .map_err(|e| ThingsError::unknown(format!("Failed to bulk complete tasks: {e}")))?;
397
398 tx.commit()
400 .await
401 .map_err(|e| ThingsError::unknown(format!("Failed to commit transaction: {e}")))?;
402
403 info!("Bulk completed {} task(s)", request.task_uuids.len());
404 Ok(crate::models::BulkOperationResult {
405 success: true,
406 processed_count: request.task_uuids.len(),
407 message: format!(
408 "Successfully completed {} task(s)",
409 request.task_uuids.len()
410 ),
411 })
412 }
413
414 #[instrument(skip(self))]
425 pub async fn bulk_delete(
426 &self,
427 request: crate::models::BulkDeleteRequest,
428 ) -> ThingsResult<crate::models::BulkOperationResult> {
429 if request.task_uuids.is_empty() {
431 return Err(ThingsError::validation("Task UUIDs cannot be empty"));
432 }
433 if request.task_uuids.len() > Self::MAX_BULK_BATCH_SIZE {
434 return Err(ThingsError::validation(format!(
435 "Batch size {} exceeds maximum of {}",
436 request.task_uuids.len(),
437 Self::MAX_BULK_BATCH_SIZE
438 )));
439 }
440
441 let mut tx = self
443 .pool
444 .begin()
445 .await
446 .map_err(|e| ThingsError::unknown(format!("Failed to begin transaction: {e}")))?;
447
448 let placeholders = request
450 .task_uuids
451 .iter()
452 .map(|_| "?")
453 .collect::<Vec<_>>()
454 .join(",");
455 let query_str = format!(
456 "SELECT uuid FROM TMTask WHERE uuid IN ({}) AND trashed = 0",
457 placeholders
458 );
459
460 let mut query = sqlx::query(&query_str);
461 for id in &request.task_uuids {
462 query = query.bind(id.as_str());
463 }
464
465 let found_uuids: Vec<String> = query
466 .fetch_all(&mut *tx)
467 .await
468 .map_err(|e| ThingsError::unknown(format!("Failed to validate tasks: {e}")))?
469 .iter()
470 .map(|row| row.get("uuid"))
471 .collect();
472
473 if found_uuids.len() != request.task_uuids.len() {
475 for id in &request.task_uuids {
477 if !found_uuids.contains(&id.to_string()) {
478 tx.rollback().await.ok();
479 return Err(ThingsError::TaskNotFound {
480 uuid: id.to_string(),
481 });
482 }
483 }
484 }
485
486 let now = Utc::now().timestamp() as f64;
488 let placeholders = request
489 .task_uuids
490 .iter()
491 .map(|_| "?")
492 .collect::<Vec<_>>()
493 .join(",");
494 let query_str = format!(
495 "UPDATE TMTask SET trashed = 1, userModificationDate = ? WHERE uuid IN ({})",
496 placeholders
497 );
498
499 let mut query = sqlx::query(&query_str).bind(now);
500
501 for id in &request.task_uuids {
502 query = query.bind(id.as_str());
503 }
504
505 query
506 .execute(&mut *tx)
507 .await
508 .map_err(|e| ThingsError::unknown(format!("Failed to bulk delete tasks: {e}")))?;
509
510 tx.commit()
512 .await
513 .map_err(|e| ThingsError::unknown(format!("Failed to commit transaction: {e}")))?;
514
515 info!("Bulk deleted {} task(s)", request.task_uuids.len());
516 Ok(crate::models::BulkOperationResult {
517 success: true,
518 processed_count: request.task_uuids.len(),
519 message: format!("Successfully deleted {} task(s)", request.task_uuids.len()),
520 })
521 }
522}