Skip to main content

things3_core/mutations/
sqlx.rs

1//! `SqlxBackend` — direct-SQLite implementation of [`MutationBackend`].
2//!
3//! Forwards every call to the corresponding method on [`ThingsDatabase`]. Behavior
4//! is byte-for-byte identical to calling the database directly. Kept around after
5//! AppleScript becomes the default (#125) for offline tests, CI, and the
6//! `--unsafe-direct-db` opt-in.
7
8use std::sync::Arc;
9
10use async_trait::async_trait;
11
12use super::MutationBackend;
13use crate::database::ThingsDatabase;
14use crate::error::Result as ThingsResult;
15use crate::models::{
16    BulkCompleteRequest, BulkCreateTasksRequest, BulkDeleteRequest, BulkMoveRequest,
17    BulkOperationResult, BulkUpdateDatesRequest, CreateAreaRequest, CreateProjectRequest,
18    CreateTagRequest, CreateTaskRequest, DeleteChildHandling, ProjectChildHandling,
19    TagAssignmentResult, TagCreationResult, TagMatch, ThingsId, UpdateAreaRequest,
20    UpdateProjectRequest, UpdateTagRequest, UpdateTaskRequest,
21};
22
23pub struct SqlxBackend {
24    db: Arc<ThingsDatabase>,
25}
26
27impl SqlxBackend {
28    #[must_use]
29    pub fn new(db: Arc<ThingsDatabase>) -> Self {
30        Self { db }
31    }
32}
33
34#[async_trait]
35impl MutationBackend for SqlxBackend {
36    fn kind(&self) -> &'static str {
37        "sqlx"
38    }
39
40    // ---- Tasks ----
41
42    async fn create_task(&self, request: CreateTaskRequest) -> ThingsResult<ThingsId> {
43        self.db.create_task(request).await
44    }
45
46    async fn bulk_create_tasks(
47        &self,
48        request: BulkCreateTasksRequest,
49    ) -> ThingsResult<BulkOperationResult> {
50        const MAX_BULK_BATCH_SIZE: usize = 1000;
51        if request.tasks.is_empty() {
52            return Err(crate::error::ThingsError::validation(
53                "Tasks array cannot be empty",
54            ));
55        }
56        if request.tasks.len() > MAX_BULK_BATCH_SIZE {
57            return Err(crate::error::ThingsError::validation(format!(
58                "Batch size {} exceeds maximum of {}",
59                request.tasks.len(),
60                MAX_BULK_BATCH_SIZE
61            )));
62        }
63        let total = request.tasks.len();
64        let mut processed = 0usize;
65        let mut errors: Vec<String> = Vec::new();
66        for (idx, task) in request.tasks.into_iter().enumerate() {
67            match self.db.create_task(task).await {
68                Ok(_) => processed += 1,
69                Err(e) => errors.push(format!("task {idx}: {e}")),
70            }
71        }
72        let success = errors.is_empty();
73        let message = if success {
74            format!("Successfully created {processed} task(s)")
75        } else {
76            format!("Created {processed}/{total}; errors: {}", errors.join("; "))
77        };
78        Ok(BulkOperationResult {
79            success,
80            processed_count: processed,
81            message,
82        })
83    }
84
85    async fn update_task(&self, request: UpdateTaskRequest) -> ThingsResult<()> {
86        self.db.update_task(request).await
87    }
88
89    async fn complete_task(&self, id: &ThingsId) -> ThingsResult<()> {
90        self.db.complete_task(id).await
91    }
92
93    async fn uncomplete_task(&self, id: &ThingsId) -> ThingsResult<()> {
94        self.db.uncomplete_task(id).await
95    }
96
97    async fn delete_task(
98        &self,
99        id: &ThingsId,
100        child_handling: DeleteChildHandling,
101    ) -> ThingsResult<()> {
102        self.db.delete_task(id, child_handling).await
103    }
104
105    async fn bulk_delete(&self, request: BulkDeleteRequest) -> ThingsResult<BulkOperationResult> {
106        self.db.bulk_delete(request).await
107    }
108
109    async fn bulk_move(&self, request: BulkMoveRequest) -> ThingsResult<BulkOperationResult> {
110        self.db.bulk_move(request).await
111    }
112
113    async fn bulk_update_dates(
114        &self,
115        request: BulkUpdateDatesRequest,
116    ) -> ThingsResult<BulkOperationResult> {
117        self.db.bulk_update_dates(request).await
118    }
119
120    async fn bulk_complete(
121        &self,
122        request: BulkCompleteRequest,
123    ) -> ThingsResult<BulkOperationResult> {
124        self.db.bulk_complete(request).await
125    }
126
127    // ---- Projects ----
128
129    async fn create_project(&self, request: CreateProjectRequest) -> ThingsResult<ThingsId> {
130        self.db.create_project(request).await
131    }
132
133    async fn update_project(&self, request: UpdateProjectRequest) -> ThingsResult<()> {
134        self.db.update_project(request).await
135    }
136
137    async fn complete_project(
138        &self,
139        id: &ThingsId,
140        child_handling: ProjectChildHandling,
141    ) -> ThingsResult<()> {
142        self.db.complete_project(id, child_handling).await
143    }
144
145    async fn delete_project(
146        &self,
147        id: &ThingsId,
148        child_handling: ProjectChildHandling,
149    ) -> ThingsResult<()> {
150        self.db.delete_project(id, child_handling).await
151    }
152
153    // ---- Areas ----
154
155    async fn create_area(&self, request: CreateAreaRequest) -> ThingsResult<ThingsId> {
156        self.db.create_area(request).await
157    }
158
159    async fn update_area(&self, request: UpdateAreaRequest) -> ThingsResult<()> {
160        self.db.update_area(request).await
161    }
162
163    async fn delete_area(&self, id: &ThingsId) -> ThingsResult<()> {
164        self.db.delete_area(id).await
165    }
166
167    // ---- Tags ----
168
169    async fn create_tag(
170        &self,
171        request: CreateTagRequest,
172        force: bool,
173    ) -> ThingsResult<TagCreationResult> {
174        if force {
175            let id = self.db.create_tag_force(request).await?;
176            Ok(TagCreationResult::Created {
177                uuid: id,
178                is_new: true,
179            })
180        } else {
181            self.db.create_tag_smart(request).await
182        }
183    }
184
185    async fn update_tag(&self, request: UpdateTagRequest) -> ThingsResult<()> {
186        self.db.update_tag(request).await
187    }
188
189    async fn delete_tag(&self, id: &ThingsId, remove_from_tasks: bool) -> ThingsResult<()> {
190        self.db.delete_tag(id, remove_from_tasks).await
191    }
192
193    async fn merge_tags(&self, source_id: &ThingsId, target_id: &ThingsId) -> ThingsResult<()> {
194        self.db.merge_tags(source_id, target_id).await
195    }
196
197    async fn add_tag_to_task(
198        &self,
199        task_id: &ThingsId,
200        tag_title: &str,
201    ) -> ThingsResult<TagAssignmentResult> {
202        self.db.add_tag_to_task(task_id, tag_title).await
203    }
204
205    async fn remove_tag_from_task(&self, task_id: &ThingsId, tag_title: &str) -> ThingsResult<()> {
206        self.db.remove_tag_from_task(task_id, tag_title).await
207    }
208
209    async fn set_task_tags(
210        &self,
211        task_id: &ThingsId,
212        tag_titles: Vec<String>,
213    ) -> ThingsResult<Vec<TagMatch>> {
214        self.db.set_task_tags(task_id, tag_titles).await
215    }
216}