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