//! SQLite implementation of the TaskRepository. //! //! Manages tasks with full support for: //! - Status tracking (pending, in_progress, completed, deleted) //! - Priority and urgency calculations //! - Due dates and recurrence patterns //! - Annotations and subtasks (delegated to annotation_repo and subtask_repo) //! - Snoozing and waiting-for-response states //! - Day planning with scheduled time blocks //! //! The trait impls here are the facade: each method checks a connection out of //! the pool and hands it to a free function in one of the sibling modules. mod complete; mod crud; mod fetch; mod query; mod row; mod state; use chrono::{DateTime, NaiveDate, Utc}; use goingson_core::{ Annotation, AnnotationId, ContactId, CoreError, MilestoneId, NewTask, PositiveMinutes, Priority, ProjectId, Result, StatusToken, StatusTokenId, Subtask, SubtaskId, Task, TaskAnnotations, TaskCrud, TaskFilterQuery, TaskId, TaskScheduling, TaskStatus, TaskTimeTracking, TimeSession, TimeSessionMode, TimeTrackingSummary, TokenState, UpdateTask, UserId, }; use super::annotation_repo; use super::status_token_repo; use super::subtask_repo; use super::time_session_repo; use crate::Db; pub(crate) use fetch::rows_to_tasks; pub(crate) use row::{TASK_SELECT_COLUMNS, TaskRowWithProject}; /// SQLite-backed implementation of [`TaskRepository`]. /// /// The most complex repository in the system, handling tasks with all their /// related data (annotations, subtasks) and supporting advanced filtering, /// sorting, and recurrence logic. pub struct SqliteTaskRepository { /// Visible to `dependency_repo`, which implements the fifth task sub-trait /// on this type and needs the same pool. pub(in crate::repository) db: Db, } impl SqliteTaskRepository { #[tracing::instrument(skip_all)] pub fn new(db: Db) -> Self { Self { db } } } impl TaskCrud for SqliteTaskRepository { #[tracing::instrument(skip_all)] fn list_all(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; query::list_all(&conn, user_id) } #[tracing::instrument(skip_all)] fn list_all_for_backup(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; query::list_all_for_backup(&conn, user_id) } #[tracing::instrument(skip_all)] fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result> { let conn = self.db.conn()?; query::list_by_project(&conn, user_id, project_id) } #[tracing::instrument(skip_all)] fn list_by_contact(&self, user_id: UserId, contact_id: ContactId) -> Result> { let conn = self.db.conn()?; query::list_by_contact(&conn, user_id, contact_id) } #[tracing::instrument(skip_all)] fn list_filtered(&self, user_id: UserId, query: TaskFilterQuery) -> Result<(Vec, i64)> { let conn = self.db.conn()?; self::query::list_filtered(&conn, user_id, &query) } #[tracing::instrument(skip_all)] fn get_by_id(&self, id: TaskId, user_id: UserId) -> Result> { let conn = self.db.conn()?; fetch::get_task_by_id(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn get_update_context( &self, id: TaskId, user_id: UserId, ) -> Result> { let conn = self.db.conn()?; fetch::get_task_update_context(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn create(&self, user_id: UserId, task: NewTask) -> Result { let conn = self.db.conn()?; crud::create(&conn, user_id, &task) } #[tracing::instrument(skip_all)] fn restore(&self, user_id: UserId, task: &Task) -> Result<()> { let conn = self.db.conn()?; crud::restore(&conn, user_id, task) } #[tracing::instrument(skip_all)] fn update(&self, id: TaskId, user_id: UserId, task: UpdateTask) -> Result> { let mut conn = self.db.conn()?; crud::update(&mut conn, id, user_id, &task) } #[tracing::instrument(skip_all)] fn bulk_set_project( &self, user_id: UserId, ids: &[TaskId], project_id: Option, ) -> Result { let mut conn = self.db.conn()?; crud::bulk_set_project(&mut conn, user_id, ids, project_id) } #[tracing::instrument(skip_all)] fn bulk_set_priority( &self, user_id: UserId, ids: &[TaskId], priority: Priority, ) -> Result { let mut conn = self.db.conn()?; crud::bulk_set_priority(&mut conn, user_id, ids, &priority) } #[tracing::instrument(skip_all)] fn delete(&self, id: TaskId, user_id: UserId) -> Result { let conn = self.db.conn()?; crud::delete(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn start(&self, id: TaskId, user_id: UserId) -> Result { let conn = self.db.conn()?; complete::start(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn complete(&self, id: TaskId, user_id: UserId) -> Result> { let conn = self.db.conn()?; complete::complete(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn complete_recurring( &self, id: TaskId, user_id: UserId, next: Option, ) -> Result<(Option, Option)> { // Scoped so the read's connection goes back to the pool before the // write checks one out: shadowing it instead held two at once. let task = { let conn = self.db.conn()?; let Some(task) = fetch::get_task_by_id(&conn, id, user_id)? else { return Ok((None, None)); }; task }; if task.status == TaskStatus::Completed { return Ok((None, None)); } let mut conn = self.db.conn()?; complete::complete_recurring(&mut conn, &task, user_id, next.as_ref()) } #[tracing::instrument(skip_all)] fn count_incomplete_by_milestone( &self, milestone_id: MilestoneId, user_id: UserId, ) -> Result { let conn = self.db.conn()?; complete::count_incomplete_by_milestone(&conn, milestone_id, user_id) } // Reporting (delegated to state) #[tracing::instrument(skip_all)] fn list_completed_between( &self, user_id: UserId, start: DateTime, end: DateTime, ) -> Result> { let conn = self.db.conn()?; state::list_completed_between(&conn, user_id, start, end) } #[tracing::instrument(skip_all)] fn list_became_overdue_between( &self, user_id: UserId, start: DateTime, end: DateTime, ) -> Result> { let conn = self.db.conn()?; state::list_became_overdue_between(&conn, user_id, start, end) } #[tracing::instrument(skip_all)] fn list_due_between( &self, user_id: UserId, start: DateTime, end: DateTime, ) -> Result> { let conn = self.db.conn()?; state::list_due_between(&conn, user_id, start, end) } #[tracing::instrument(skip_all)] fn list_created_between( &self, user_id: UserId, start: DateTime, end: DateTime, ) -> Result> { let conn = self.db.conn()?; state::list_created_between(&conn, user_id, start, end) } #[tracing::instrument(skip_all)] fn list_recurrence_chain(&self, root_id: TaskId, user_id: UserId) -> Result> { let conn = self.db.conn()?; query::list_recurrence_chain(&conn, root_id, user_id) } } impl TaskAnnotations for SqliteTaskRepository { // Annotations (delegated to annotation_repo) #[tracing::instrument(skip_all)] fn get_annotations_for_task(&self, task_id: TaskId) -> Result> { let conn = self.db.conn()?; annotation_repo::get_annotations_for_task(&conn, task_id) } #[tracing::instrument(skip_all)] fn add_annotation( &self, task_id: TaskId, user_id: UserId, note: &str, ) -> Result> { let conn = self.db.conn()?; annotation_repo::add_annotation(&conn, task_id, user_id, note) } #[tracing::instrument(skip_all)] fn delete_annotation(&self, annotation_id: AnnotationId, user_id: UserId) -> Result { let conn = self.db.conn()?; annotation_repo::delete_annotation(&conn, annotation_id, user_id) } // Subtasks (delegated to subtask_repo) #[tracing::instrument(skip_all)] fn get_subtasks_for_task(&self, task_id: TaskId) -> Result> { let conn = self.db.conn()?; subtask_repo::get_subtasks_for_task(&conn, task_id) } #[tracing::instrument(skip_all)] fn add_subtask(&self, task_id: TaskId, user_id: UserId, text: &str) -> Result> { let conn = self.db.conn()?; subtask_repo::add_subtask(&conn, task_id, user_id, text) } #[tracing::instrument(skip_all)] fn toggle_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result> { let conn = self.db.conn()?; subtask_repo::toggle_subtask(&conn, subtask_id, user_id) } #[tracing::instrument(skip_all)] fn update_subtask( &self, subtask_id: SubtaskId, user_id: UserId, text: &str, ) -> Result> { let conn = self.db.conn()?; subtask_repo::update_subtask(&conn, subtask_id, user_id, text) } #[tracing::instrument(skip_all)] fn delete_subtask(&self, subtask_id: SubtaskId, user_id: UserId) -> Result { let conn = self.db.conn()?; subtask_repo::delete_subtask(&conn, subtask_id, user_id) } #[tracing::instrument(skip_all)] fn add_subtask_link( &self, task_id: TaskId, user_id: UserId, linked_task_id: TaskId, ) -> Result> { let conn = self.db.conn()?; // Verify linked task exists and belongs to user let linked_task = fetch::get_task_by_id(&conn, linked_task_id, user_id)? .ok_or_else(|| CoreError::not_found("linked task", linked_task_id.to_string()))?; subtask_repo::add_subtask_link( &conn, task_id, user_id, linked_task_id, &linked_task.title, &linked_task.status, ) } // Status tokens (delegated to status_token_repo) #[tracing::instrument(skip_all)] fn get_status_tokens_for_task(&self, task_id: TaskId) -> Result> { let conn = self.db.conn()?; status_token_repo::get_tokens_for_task(&conn, task_id) } #[tracing::instrument(skip_all)] fn record_status_token( &self, task_id: TaskId, user_id: UserId, kind: &str, reference: &str, state: TokenState, is_primary: bool, ) -> Result> { let mut conn = self.db.conn()?; status_token_repo::record_token( &mut conn, task_id, user_id, kind, reference, state, is_primary, ) } #[tracing::instrument(skip_all)] fn delete_status_token(&self, token_id: StatusTokenId, user_id: UserId) -> Result { let conn = self.db.conn()?; status_token_repo::delete_token(&conn, token_id, user_id) } } impl TaskScheduling for SqliteTaskRepository { // Snooze (delegated to task_repo_state) #[tracing::instrument(skip_all)] fn snooze(&self, id: TaskId, user_id: UserId, until: DateTime) -> Result> { let conn = self.db.conn()?; state::snooze(&conn, id, user_id, until) } #[tracing::instrument(skip_all)] fn unsnooze(&self, id: TaskId, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::unsnooze(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn list_snoozed(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::list_snoozed(&conn, user_id) } // Waiting (delegated to task_repo_state) #[tracing::instrument(skip_all)] fn mark_waiting( &self, id: TaskId, user_id: UserId, expected_response: Option>, ) -> Result> { let conn = self.db.conn()?; state::mark_waiting(&conn, id, user_id, expected_response) } #[tracing::instrument(skip_all)] fn clear_waiting(&self, id: TaskId, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::clear_waiting(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn list_waiting(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::list_waiting(&conn, user_id) } // Scheduling (delegated to task_repo_state) #[tracing::instrument(skip_all)] fn list_scheduled_for_date(&self, user_id: UserId, date: NaiveDate) -> Result> { let conn = self.db.conn()?; state::list_scheduled_for_date(&conn, user_id, date) } #[tracing::instrument(skip_all)] fn list_unscheduled_due_on_date(&self, user_id: UserId, date: NaiveDate) -> Result> { let conn = self.db.conn()?; state::list_unscheduled_due_on_date(&conn, user_id, date) } #[tracing::instrument(skip_all)] fn list_unscheduled_due_between( &self, user_id: UserId, start: DateTime, end: DateTime, ) -> Result> { let conn = self.db.conn()?; state::list_unscheduled_due_between(&conn, user_id, start, end) } #[tracing::instrument(skip_all)] fn update_schedule( &self, id: TaskId, user_id: UserId, start: Option>, duration: Option, ) -> Result> { let conn = self.db.conn()?; state::update_schedule(&conn, id, user_id, start, duration) } // Focus (delegated to task_repo_state) #[tracing::instrument(skip_all)] fn set_focus(&self, id: TaskId, user_id: UserId, is_focus: bool) -> Result> { let conn = self.db.conn()?; state::set_focus(&conn, id, user_id, is_focus) } #[tracing::instrument(skip_all)] fn list_focused(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::list_focused(&conn, user_id) } #[tracing::instrument(skip_all)] fn clear_all_focus(&self, user_id: UserId) -> Result { let conn = self.db.conn()?; state::clear_all_focus(&conn, user_id) } #[tracing::instrument(skip_all)] fn list_available_for_focus(&self, user_id: UserId, limit: i64) -> Result> { let conn = self.db.conn()?; state::list_available_for_focus(&conn, user_id, limit) } } impl TaskTimeTracking for SqliteTaskRepository { // Time Tracking (delegated to time_session_repo) #[tracing::instrument(skip_all)] fn start_timer(&self, task_id: TaskId, user_id: UserId) -> Result { let mut conn = self.db.conn()?; time_session_repo::start_timer(&mut conn, task_id, user_id, TimeSessionMode::Track, None) } #[tracing::instrument(skip_all)] fn start_focus_session( &self, task_id: TaskId, user_id: UserId, ends_at: DateTime, ) -> Result { let mut conn = self.db.conn()?; time_session_repo::start_timer( &mut conn, task_id, user_id, TimeSessionMode::Focus, Some(ends_at), ) } #[tracing::instrument(skip_all)] fn stop_timer(&self, task_id: TaskId, user_id: UserId) -> Result> { let mut conn = self.db.conn()?; time_session_repo::stop_timer(&mut conn, task_id, user_id) } #[tracing::instrument(skip_all)] fn discard_timer(&self, task_id: TaskId, user_id: UserId) -> Result { let conn = self.db.conn()?; time_session_repo::discard_timer(&conn, task_id, user_id) } #[tracing::instrument(skip_all)] fn get_active_timer(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; time_session_repo::get_active_timer(&conn, user_id) } #[tracing::instrument(skip_all)] fn list_time_sessions(&self, task_id: TaskId, user_id: UserId) -> Result> { let conn = self.db.conn()?; time_session_repo::list_time_sessions(&conn, task_id, user_id) } #[tracing::instrument(skip_all)] fn list_all_time_sessions(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; time_session_repo::list_all_time_sessions(&conn, user_id) } #[tracing::instrument(skip_all)] fn log_manual_time( &self, task_id: TaskId, user_id: UserId, minutes: PositiveMinutes, date: DateTime, ) -> Result { let mut conn = self.db.conn()?; time_session_repo::log_manual_time(&mut conn, task_id, user_id, minutes, date) } #[tracing::instrument(skip_all)] fn get_time_summary( &self, user_id: UserId, start: DateTime, end: DateTime, ) -> Result> { let conn = self.db.conn()?; time_session_repo::get_time_summary(&conn, user_id, start, end) } }