Skip to main content

max / goingson

17.5 KB · 557 lines History Blame Raw
1 //! SQLite implementation of the TaskRepository.
2 //!
3 //! Manages tasks with full support for:
4 //! - Status tracking (pending, in_progress, completed, deleted)
5 //! - Priority and urgency calculations
6 //! - Due dates and recurrence patterns
7 //! - Annotations and subtasks (delegated to annotation_repo and subtask_repo)
8 //! - Snoozing and waiting-for-response states
9 //! - Day planning with scheduled time blocks
10 //!
11 //! The trait impls here are the facade: each method checks a connection out of
12 //! the pool and hands it to a free function in one of the sibling modules.
13
14 mod complete;
15 mod crud;
16 mod fetch;
17 mod query;
18 mod row;
19 mod state;
20
21 use chrono::{DateTime, NaiveDate, Utc};
22
23 use goingson_core::{
24 Annotation, AnnotationId, ContactId, CoreError, MilestoneId, NewTask, PositiveMinutes,
25 Priority, ProjectId, Result, StatusToken, StatusTokenId, Subtask, SubtaskId, Task,
26 TaskAnnotations, TaskCrud, TaskFilterQuery, TaskId, TaskScheduling, TaskStatus,
27 TaskTimeTracking, TimeSession, TimeSessionMode, TimeTrackingSummary, TokenState, UpdateTask,
28 UserId,
29 };
30
31 use super::annotation_repo;
32 use super::status_token_repo;
33 use super::subtask_repo;
34 use super::time_session_repo;
35
36 use crate::Db;
37
38 pub(crate) use fetch::rows_to_tasks;
39 pub(crate) use row::{TASK_SELECT_COLUMNS, TaskRowWithProject};
40
41 /// SQLite-backed implementation of [`TaskRepository`].
42 ///
43 /// The most complex repository in the system, handling tasks with all their
44 /// related data (annotations, subtasks) and supporting advanced filtering,
45 /// sorting, and recurrence logic.
46 pub struct SqliteTaskRepository {
47 /// Visible to `dependency_repo`, which implements the fifth task sub-trait
48 /// on this type and needs the same pool.
49 pub(in crate::repository) db: Db,
50 }
51
52 impl SqliteTaskRepository {
53 #[tracing::instrument(skip_all)]
54 pub fn new(db: Db) -> Self {
55 Self { db }
56 }
57 }
58
59 impl TaskCrud for SqliteTaskRepository {
60 #[tracing::instrument(skip_all)]
61 fn list_all(&self, user_id: UserId) -> Result<Vec<Task>> {
62 let conn = self.db.conn()?;
63 query::list_all(&conn, user_id)
64 }
65
66 #[tracing::instrument(skip_all)]
67 fn list_all_for_backup(&self, user_id: UserId) -> Result<Vec<Task>> {
68 let conn = self.db.conn()?;
69 query::list_all_for_backup(&conn, user_id)
70 }
71
72 #[tracing::instrument(skip_all)]
73 fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Task>> {
74 let conn = self.db.conn()?;
75 query::list_by_project(&conn, user_id, project_id)
76 }
77
78 #[tracing::instrument(skip_all)]
79 fn list_by_contact(&self, user_id: UserId, contact_id: ContactId) -> Result<Vec<Task>> {
80 let conn = self.db.conn()?;
81 query::list_by_contact(&conn, user_id, contact_id)
82 }
83
84 #[tracing::instrument(skip_all)]
85 fn list_filtered(&self, user_id: UserId, query: TaskFilterQuery) -> Result<(Vec<Task>, i64)> {
86 let conn = self.db.conn()?;
87 self::query::list_filtered(&conn, user_id, &query)
88 }
89
90 #[tracing::instrument(skip_all)]
91 fn get_by_id(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>> {
92 let conn = self.db.conn()?;
93 fetch::get_task_by_id(&conn, id, user_id)
94 }
95
96 #[tracing::instrument(skip_all)]
97 fn get_update_context(
98 &self,
99 id: TaskId,
100 user_id: UserId,
101 ) -> Result<Option<goingson_core::models::TaskUpdateContext>> {
102 let conn = self.db.conn()?;
103 fetch::get_task_update_context(&conn, id, user_id)
104 }
105
106 #[tracing::instrument(skip_all)]
107 fn create(&self, user_id: UserId, task: NewTask) -> Result<Task> {
108 let conn = self.db.conn()?;
109 crud::create(&conn, user_id, &task)
110 }
111
112 #[tracing::instrument(skip_all)]
113 fn restore(&self, user_id: UserId, task: &Task) -> Result<()> {
114 let conn = self.db.conn()?;
115 crud::restore(&conn, user_id, task)
116 }
117
118 #[tracing::instrument(skip_all)]
119 fn update(&self, id: TaskId, user_id: UserId, task: UpdateTask) -> Result<Option<Task>> {
120 let mut conn = self.db.conn()?;
121 crud::update(&mut conn, id, user_id, &task)
122 }
123
124 #[tracing::instrument(skip_all)]
125 fn bulk_set_project(
126 &self,
127 user_id: UserId,
128 ids: &[TaskId],
129 project_id: Option<ProjectId>,
130 ) -> Result<usize> {
131 let mut conn = self.db.conn()?;
132 crud::bulk_set_project(&mut conn, user_id, ids, project_id)
133 }
134
135 #[tracing::instrument(skip_all)]
136 fn bulk_set_priority(
137 &self,
138 user_id: UserId,
139 ids: &[TaskId],
140 priority: Priority,
141 ) -> Result<usize> {
142 let mut conn = self.db.conn()?;
143 crud::bulk_set_priority(&mut conn, user_id, ids, &priority)
144 }
145
146 #[tracing::instrument(skip_all)]
147 fn delete(&self, id: TaskId, user_id: UserId) -> Result<bool> {
148 let conn = self.db.conn()?;
149 crud::delete(&conn, id, user_id)
150 }
151
152 #[tracing::instrument(skip_all)]
153 fn start(&self, id: TaskId, user_id: UserId) -> Result<bool> {
154 let conn = self.db.conn()?;
155 complete::start(&conn, id, user_id)
156 }
157
158 #[tracing::instrument(skip_all)]
159 fn complete(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>> {
160 let conn = self.db.conn()?;
161 complete::complete(&conn, id, user_id)
162 }
163
164 #[tracing::instrument(skip_all)]
165 fn complete_recurring(
166 &self,
167 id: TaskId,
168 user_id: UserId,
169 next: Option<NewTask>,
170 ) -> Result<(Option<Task>, Option<Task>)> {
171 // Scoped so the read's connection goes back to the pool before the
172 // write checks one out: shadowing it instead held two at once.
173 let task = {
174 let conn = self.db.conn()?;
175 let Some(task) = fetch::get_task_by_id(&conn, id, user_id)? else {
176 return Ok((None, None));
177 };
178 task
179 };
180
181 if task.status == TaskStatus::Completed {
182 return Ok((None, None));
183 }
184
185 let mut conn = self.db.conn()?;
186 complete::complete_recurring(&mut conn, &task, user_id, next.as_ref())
187 }
188
189 #[tracing::instrument(skip_all)]
190 fn count_incomplete_by_milestone(
191 &self,
192 milestone_id: MilestoneId,
193 user_id: UserId,
194 ) -> Result<i64> {
195 let conn = self.db.conn()?;
196 complete::count_incomplete_by_milestone(&conn, milestone_id, user_id)
197 }
198
199 // Reporting (delegated to state)
200
201 #[tracing::instrument(skip_all)]
202 fn list_completed_between(
203 &self,
204 user_id: UserId,
205 start: DateTime<Utc>,
206 end: DateTime<Utc>,
207 ) -> Result<Vec<Task>> {
208 let conn = self.db.conn()?;
209 state::list_completed_between(&conn, user_id, start, end)
210 }
211
212 #[tracing::instrument(skip_all)]
213 fn list_became_overdue_between(
214 &self,
215 user_id: UserId,
216 start: DateTime<Utc>,
217 end: DateTime<Utc>,
218 ) -> Result<Vec<Task>> {
219 let conn = self.db.conn()?;
220 state::list_became_overdue_between(&conn, user_id, start, end)
221 }
222
223 #[tracing::instrument(skip_all)]
224 fn list_due_between(
225 &self,
226 user_id: UserId,
227 start: DateTime<Utc>,
228 end: DateTime<Utc>,
229 ) -> Result<Vec<Task>> {
230 let conn = self.db.conn()?;
231 state::list_due_between(&conn, user_id, start, end)
232 }
233
234 #[tracing::instrument(skip_all)]
235 fn list_created_between(
236 &self,
237 user_id: UserId,
238 start: DateTime<Utc>,
239 end: DateTime<Utc>,
240 ) -> Result<Vec<Task>> {
241 let conn = self.db.conn()?;
242 state::list_created_between(&conn, user_id, start, end)
243 }
244
245 #[tracing::instrument(skip_all)]
246 fn list_recurrence_chain(&self, root_id: TaskId, user_id: UserId) -> Result<Vec<Task>> {
247 let conn = self.db.conn()?;
248 query::list_recurrence_chain(&conn, root_id, user_id)
249 }
250 }
251
252 impl TaskAnnotations for SqliteTaskRepository {
253 // Annotations (delegated to annotation_repo)
254
255 #[tracing::instrument(skip_all)]
256 fn get_annotations_for_task(&self, task_id: TaskId) -> Result<Vec<Annotation>> {
257 let conn = self.db.conn()?;
258 annotation_repo::get_annotations_for_task(&conn, task_id)
259 }
260
261 #[tracing::instrument(skip_all)]
262 fn add_annotation(
263 &self,
264 task_id: TaskId,
265 user_id: UserId,
266 note: &str,
267 ) -> Result<Option<Annotation>> {
268 let conn = self.db.conn()?;
269 annotation_repo::add_annotation(&conn, task_id, user_id, note)
270 }
271
272 #[tracing::instrument(skip_all)]
273 fn delete_annotation(&self, annotation_id: AnnotationId, user_id: UserId) -> Result<bool> {
274 let conn = self.db.conn()?;
275 annotation_repo::delete_annotation(&conn, annotation_id, user_id)
276 }
277
278 // Subtasks (delegated to subtask_repo)
279
280 #[tracing::instrument(skip_all)]
281 fn get_subtasks_for_task(&self, task_id: TaskId) -> Result<Vec<Subtask>> {
282 let conn = self.db.conn()?;
283 subtask_repo::get_subtasks_for_task(&conn, task_id)
284 }
285
286 #[tracing::instrument(skip_all)]
287 fn add_subtask(&self, task_id: TaskId, user_id: UserId, text: &str) -> Result<Option<Subtask>> {
288 let conn = self.db.conn()?;
289 subtask_repo::add_subtask(&conn, task_id, user_id, text)
290 }
291
292 #[tracing::instrument(skip_all)]
293 fn toggle_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result<Option<Subtask>> {
294 let conn = self.db.conn()?;
295 subtask_repo::toggle_subtask(&conn, subtask_id, user_id)
296 }
297
298 #[tracing::instrument(skip_all)]
299 fn update_subtask(
300 &self,
301 subtask_id: SubtaskId,
302 user_id: UserId,
303 text: &str,
304 ) -> Result<Option<Subtask>> {
305 let conn = self.db.conn()?;
306 subtask_repo::update_subtask(&conn, subtask_id, user_id, text)
307 }
308
309 #[tracing::instrument(skip_all)]
310 fn delete_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result<bool> {
311 let conn = self.db.conn()?;
312 subtask_repo::delete_subtask(&conn, subtask_id, user_id)
313 }
314
315 #[tracing::instrument(skip_all)]
316 fn add_subtask_link(
317 &self,
318 task_id: TaskId,
319 user_id: UserId,
320 linked_task_id: TaskId,
321 ) -> Result<Option<Subtask>> {
322 let conn = self.db.conn()?;
323 // Verify linked task exists and belongs to user
324 let linked_task = fetch::get_task_by_id(&conn, linked_task_id, user_id)?
325 .ok_or_else(|| CoreError::not_found("linked task", linked_task_id.to_string()))?;
326
327 subtask_repo::add_subtask_link(
328 &conn,
329 task_id,
330 user_id,
331 linked_task_id,
332 &linked_task.title,
333 &linked_task.status,
334 )
335 }
336
337 // Status tokens (delegated to status_token_repo)
338
339 #[tracing::instrument(skip_all)]
340 fn get_status_tokens_for_task(&self, task_id: TaskId) -> Result<Vec<StatusToken>> {
341 let conn = self.db.conn()?;
342 status_token_repo::get_tokens_for_task(&conn, task_id)
343 }
344
345 #[tracing::instrument(skip_all)]
346 fn record_status_token(
347 &self,
348 task_id: TaskId,
349 user_id: UserId,
350 kind: &str,
351 reference: &str,
352 state: TokenState,
353 is_primary: bool,
354 ) -> Result<Option<StatusToken>> {
355 let mut conn = self.db.conn()?;
356 status_token_repo::record_token(
357 &mut conn, task_id, user_id, kind, reference, state, is_primary,
358 )
359 }
360
361 #[tracing::instrument(skip_all)]
362 fn delete_status_token(&self, token_id: StatusTokenId, user_id: UserId) -> Result<bool> {
363 let conn = self.db.conn()?;
364 status_token_repo::delete_token(&conn, token_id, user_id)
365 }
366 }
367
368 impl TaskScheduling for SqliteTaskRepository {
369 // Snooze (delegated to task_repo_state)
370
371 #[tracing::instrument(skip_all)]
372 fn snooze(&self, id: TaskId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Task>> {
373 let conn = self.db.conn()?;
374 state::snooze(&conn, id, user_id, until)
375 }
376
377 #[tracing::instrument(skip_all)]
378 fn unsnooze(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>> {
379 let conn = self.db.conn()?;
380 state::unsnooze(&conn, id, user_id)
381 }
382
383 #[tracing::instrument(skip_all)]
384 fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Task>> {
385 let conn = self.db.conn()?;
386 state::list_snoozed(&conn, user_id)
387 }
388
389 // Waiting (delegated to task_repo_state)
390
391 #[tracing::instrument(skip_all)]
392 fn mark_waiting(
393 &self,
394 id: TaskId,
395 user_id: UserId,
396 expected_response: Option<DateTime<Utc>>,
397 ) -> Result<Option<Task>> {
398 let conn = self.db.conn()?;
399 state::mark_waiting(&conn, id, user_id, expected_response)
400 }
401
402 #[tracing::instrument(skip_all)]
403 fn clear_waiting(&self, id: TaskId, user_id: UserId) -> Result<Option<Task>> {
404 let conn = self.db.conn()?;
405 state::clear_waiting(&conn, id, user_id)
406 }
407
408 #[tracing::instrument(skip_all)]
409 fn list_waiting(&self, user_id: UserId) -> Result<Vec<Task>> {
410 let conn = self.db.conn()?;
411 state::list_waiting(&conn, user_id)
412 }
413
414 // Scheduling (delegated to task_repo_state)
415
416 #[tracing::instrument(skip_all)]
417 fn list_scheduled_for_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Task>> {
418 let conn = self.db.conn()?;
419 state::list_scheduled_for_date(&conn, user_id, date)
420 }
421
422 #[tracing::instrument(skip_all)]
423 fn list_unscheduled_due_on_date(&self, user_id: UserId, date: NaiveDate) -> Result<Vec<Task>> {
424 let conn = self.db.conn()?;
425 state::list_unscheduled_due_on_date(&conn, user_id, date)
426 }
427
428 #[tracing::instrument(skip_all)]
429 fn list_unscheduled_due_between(
430 &self,
431 user_id: UserId,
432 start: DateTime<Utc>,
433 end: DateTime<Utc>,
434 ) -> Result<Vec<Task>> {
435 let conn = self.db.conn()?;
436 state::list_unscheduled_due_between(&conn, user_id, start, end)
437 }
438
439 #[tracing::instrument(skip_all)]
440 fn update_schedule(
441 &self,
442 id: TaskId,
443 user_id: UserId,
444 start: Option<DateTime<Utc>>,
445 duration: Option<i32>,
446 ) -> Result<Option<Task>> {
447 let conn = self.db.conn()?;
448 state::update_schedule(&conn, id, user_id, start, duration)
449 }
450
451 // Focus (delegated to task_repo_state)
452
453 #[tracing::instrument(skip_all)]
454 fn set_focus(&self, id: TaskId, user_id: UserId, is_focus: bool) -> Result<Option<Task>> {
455 let conn = self.db.conn()?;
456 state::set_focus(&conn, id, user_id, is_focus)
457 }
458
459 #[tracing::instrument(skip_all)]
460 fn list_focused(&self, user_id: UserId) -> Result<Vec<Task>> {
461 let conn = self.db.conn()?;
462 state::list_focused(&conn, user_id)
463 }
464
465 #[tracing::instrument(skip_all)]
466 fn clear_all_focus(&self, user_id: UserId) -> Result<u64> {
467 let conn = self.db.conn()?;
468 state::clear_all_focus(&conn, user_id)
469 }
470
471 #[tracing::instrument(skip_all)]
472 fn list_available_for_focus(&self, user_id: UserId, limit: i64) -> Result<Vec<Task>> {
473 let conn = self.db.conn()?;
474 state::list_available_for_focus(&conn, user_id, limit)
475 }
476 }
477
478 impl TaskTimeTracking for SqliteTaskRepository {
479 // Time Tracking (delegated to time_session_repo)
480
481 #[tracing::instrument(skip_all)]
482 fn start_timer(&self, task_id: TaskId, user_id: UserId) -> Result<TimeSession> {
483 let mut conn = self.db.conn()?;
484 time_session_repo::start_timer(&mut conn, task_id, user_id, TimeSessionMode::Track, None)
485 }
486
487 #[tracing::instrument(skip_all)]
488 fn start_focus_session(
489 &self,
490 task_id: TaskId,
491 user_id: UserId,
492 ends_at: DateTime<Utc>,
493 ) -> Result<TimeSession> {
494 let mut conn = self.db.conn()?;
495 time_session_repo::start_timer(
496 &mut conn,
497 task_id,
498 user_id,
499 TimeSessionMode::Focus,
500 Some(ends_at),
501 )
502 }
503
504 #[tracing::instrument(skip_all)]
505 fn stop_timer(&self, task_id: TaskId, user_id: UserId) -> Result<Option<TimeSession>> {
506 let mut conn = self.db.conn()?;
507 time_session_repo::stop_timer(&mut conn, task_id, user_id)
508 }
509
510 #[tracing::instrument(skip_all)]
511 fn discard_timer(&self, task_id: TaskId, user_id: UserId) -> Result<bool> {
512 let conn = self.db.conn()?;
513 time_session_repo::discard_timer(&conn, task_id, user_id)
514 }
515
516 #[tracing::instrument(skip_all)]
517 fn get_active_timer(&self, user_id: UserId) -> Result<Option<(TimeSession, String)>> {
518 let conn = self.db.conn()?;
519 time_session_repo::get_active_timer(&conn, user_id)
520 }
521
522 #[tracing::instrument(skip_all)]
523 fn list_time_sessions(&self, task_id: TaskId, user_id: UserId) -> Result<Vec<TimeSession>> {
524 let conn = self.db.conn()?;
525 time_session_repo::list_time_sessions(&conn, task_id, user_id)
526 }
527
528 #[tracing::instrument(skip_all)]
529 fn list_all_time_sessions(&self, user_id: UserId) -> Result<Vec<TimeSession>> {
530 let conn = self.db.conn()?;
531 time_session_repo::list_all_time_sessions(&conn, user_id)
532 }
533
534 #[tracing::instrument(skip_all)]
535 fn log_manual_time(
536 &self,
537 task_id: TaskId,
538 user_id: UserId,
539 minutes: PositiveMinutes,
540 date: DateTime<Utc>,
541 ) -> Result<TimeSession> {
542 let mut conn = self.db.conn()?;
543 time_session_repo::log_manual_time(&mut conn, task_id, user_id, minutes, date)
544 }
545
546 #[tracing::instrument(skip_all)]
547 fn get_time_summary(
548 &self,
549 user_id: UserId,
550 start: DateTime<Utc>,
551 end: DateTime<Utc>,
552 ) -> Result<Vec<TimeTrackingSummary>> {
553 let conn = self.db.conn()?;
554 time_session_repo::get_time_summary(&conn, user_id, start, end)
555 }
556 }
557