//! Reads shared across the task modules: batch hydration of task rows and the //! single-task fetches every write path re-reads through. use goingson_core::{ParseableEnum, Result, Task, TaskId, TaskStatus, UserId}; use rusqlite::{Connection, params, params_from_iter}; use crate::utils::{parse_datetime, parse_uuid, query_all, query_opt}; use super::row::{TASK_SELECT_COLUMNS, TaskRowWithProject}; use crate::repository::{annotation_repo, status_token_repo, subtask_repo, time_session_repo}; /// Converts task rows to Task objects with annotations, subtasks, and active sessions. /// /// This helper encapsulates the common pattern of: /// 1. Extracting task IDs from rows /// 2. Batch-fetching annotations, subtasks, and active sessions for all tasks /// 3. Converting each row to a Task with its related data /// /// Returns an empty vec if rows is empty (no database calls made). pub(crate) fn rows_to_tasks(conn: &Connection, rows: Vec) -> Result> { if rows.is_empty() { return Ok(vec![]); } let task_ids: Vec = rows.iter().map(|r| r.id.clone()).collect(); let annotations_map = annotation_repo::get_annotations_for_tasks(conn, &task_ids)?; let subtasks_map = subtask_repo::get_subtasks_for_tasks(conn, &task_ids)?; let tokens_map = status_token_repo::get_tokens_for_tasks(conn, &task_ids)?; let active_sessions = time_session_repo::get_active_sessions_for_tasks(conn, &task_ids)?; let mut tasks = Vec::with_capacity(rows.len()); for row in rows { let id: TaskId = parse_uuid(&row.id)?.into(); let annotations = annotations_map.get(&id).cloned().unwrap_or_default(); let subtasks = subtasks_map.get(&id).cloned().unwrap_or_default(); let status_tokens = tokens_map.get(&id).cloned().unwrap_or_default(); let mut task = row.into_task(annotations, subtasks, status_tokens)?; task.active_session = active_sessions.get(&id).cloned(); tasks.push(task); } Ok(tasks) } /// Fetch only the fields needed for update logic; avoids annotation/subtask/session sub-queries. pub(crate) fn get_task_update_context( conn: &Connection, id: TaskId, user_id: UserId, ) -> Result> { struct Row { created_at: String, status: String, completed_at: Option, scheduled_start: Option, scheduled_duration: Option, } impl Row { fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(Self { created_at: row.get("created_at")?, status: row.get("status")?, completed_at: row.get("completed_at")?, scheduled_start: row.get("scheduled_start")?, scheduled_duration: row.get("scheduled_duration")?, }) } } let row = query_opt( conn, "SELECT created_at, status, completed_at, scheduled_start, scheduled_duration FROM tasks WHERE id = ? AND user_id = ?", params![id.to_string(), user_id.to_string()], Row::from_row, )?; match row { Some(r) => Ok(Some(goingson_core::models::TaskUpdateContext { created_at: parse_datetime(&r.created_at)?, status: TaskStatus::from_str_or_default(&r.status), completed_at: r .completed_at .as_ref() .map(|s| parse_datetime(s)) .transpose()?, scheduled_start: r .scheduled_start .as_ref() .map(|s| parse_datetime(s)) .transpose()?, scheduled_duration: r.scheduled_duration, })), None => Ok(None), } } /// Fetch a single task by ID and user, with annotations and subtasks. pub(crate) fn get_task_by_id( conn: &Connection, id: TaskId, user_id: UserId, ) -> Result> { let sql = format!( r" SELECT {TASK_SELECT_COLUMNS} FROM tasks t LEFT JOIN projects p ON t.project_id = p.id AND p.user_id = ? LEFT JOIN contacts ct ON ct.id = t.contact_id WHERE t.id = ? AND t.user_id = ? " ); let row = query_opt( conn, &sql, params![user_id.to_string(), id.to_string(), user_id.to_string()], TaskRowWithProject::from_row, )?; match row { Some(row) => { let annotations = annotation_repo::get_annotations_for_task(conn, id)?; let subtasks = subtask_repo::get_subtasks_for_task(conn, id)?; let status_tokens = status_token_repo::get_tokens_for_task(conn, id)?; let active_sessions = time_session_repo::get_active_sessions_for_tasks( conn, std::slice::from_ref(&row.id), )?; let mut task = row.into_task(annotations, subtasks, status_tokens)?; task.active_session = active_sessions.get(&task.id).cloned(); Ok(Some(task)) } None => Ok(None), } } /// Run a task query with string bind parameters and convert rows to tasks. /// /// Handles the common pattern of: format SQL with TASK_SELECT_COLUMNS, /// bind string params in order, fetch rows, convert via rows_to_tasks. pub(crate) fn query_tasks(conn: &Connection, sql: &str, binds: &[String]) -> Result> { let rows = query_all( conn, sql, params_from_iter(binds.iter()), TaskRowWithProject::from_row, )?; rows_to_tasks(conn, rows) }