Skip to main content

shipd_core/db/
tasks.rs

1use crate::models::{Priority, Status, Task};
2use rusqlite::Connection;
3
4pub fn get_task_by_id(conn: &Connection, id: i64) -> Result<Option<Task>, rusqlite::Error> {
5    let mut stmt = conn.prepare(
6        "SELECT id, title, description, status, priority, project_id, branch, due_date, created_at, updated_at FROM tasks WHERE id = ?1",
7    )?;
8
9    let task = stmt.query_row((id,), |row| {
10        Ok(Task {
11            id: row.get(0)?,
12            title: row.get(1)?,
13            description: row.get(2)?,
14            status: row.get::<_, String>(3)?.parse().unwrap_or(Status::Todo),
15            priority: row.get::<_, String>(4)?.parse().unwrap_or(Priority::Medium),
16            project_id: row.get(5)?,
17            branch: row.get(6)?,
18            due_date: row.get(7)?,
19            created_at: row.get(8)?,
20            updated_at: row.get(9)?,
21        })
22    });
23
24    match task {
25        Ok(t) => Ok(Some(t)),
26        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
27        Err(e) => Err(e),
28    }
29}
30
31pub fn create_task(
32    conn: &Connection,
33    title: &str,
34    priority: Option<Priority>,
35) -> Result<i64, rusqlite::Error> {
36    let priority = priority.unwrap_or(Priority::Medium).to_string();
37
38    conn.execute(
39        "INSERT INTO tasks (title, priority) VALUES (?1, ?2)",
40        (title, priority),
41    )?;
42
43    Ok(conn.last_insert_rowid())
44}
45
46pub fn update_task_status(
47    conn: &Connection,
48    id: i64,
49    status: &Status,
50) -> Result<usize, rusqlite::Error> {
51    conn.execute(
52        "UPDATE tasks SET status = ?1, updated_at = datetime('now') WHERE id = ?2",
53        (status.to_string(), id),
54    )
55}
56
57pub fn delete_task(conn: &Connection, id: i64) -> Result<usize, rusqlite::Error> {
58    conn.execute("DELETE FROM tasks WHERE id = ?1", (id,))
59}
60
61pub fn edit_task(
62    conn: &Connection,
63    id: i64,
64    title: Option<&str>,
65    priority: Option<Priority>,
66    description: Option<&str>,
67) -> Result<usize, rusqlite::Error> {
68    let mut updates = Vec::new();
69    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
70
71    if let Some(t) = title {
72        updates.push("title = ?");
73        params.push(Box::new(t.to_string()));
74    }
75
76    if let Some(p) = priority {
77        updates.push("priority = ?");
78        params.push(Box::new(p.to_string()));
79    }
80
81    if let Some(d) = description {
82        updates.push("description = ?");
83        params.push(Box::new(d.to_string()));
84    }
85
86    if updates.is_empty() {
87        return Ok(0);
88    }
89
90    updates.push("updated_at = datetime('now')");
91    params.push(Box::new(id));
92
93    let sql = format!("UPDATE tasks SET {} WHERE id = ?", updates.join(", "));
94
95    conn.execute(&sql, rusqlite::params_from_iter(params))
96}
97
98pub fn link_branch(conn: &Connection, id: i64, branch: &str) -> Result<usize, rusqlite::Error> {
99    conn.execute(
100        "UPDATE tasks SET branch = ?1, updated_at = datetime('now') WHERE id = ?2",
101        (branch, id),
102    )
103}
104
105pub fn get_task_by_branch(
106    conn: &Connection,
107    branch: &str,
108) -> Result<Option<Task>, rusqlite::Error> {
109    let mut stmt = conn.prepare(
110        "SELECT id, title, description, status, priority, project_id, branch, due_date, created_at, updated_at FROM tasks WHERE branch = ?1",
111    )?;
112
113    let task = stmt.query_row((branch,), |row| {
114        Ok(Task {
115            id: row.get(0)?,
116            title: row.get(1)?,
117            description: row.get(2)?,
118            status: row.get::<_, String>(3)?.parse().unwrap_or(Status::Todo),
119            priority: row.get::<_, String>(4)?.parse().unwrap_or(Priority::Medium),
120            project_id: row.get(5)?,
121            branch: row.get(6)?,
122            due_date: row.get(7)?,
123            created_at: row.get(8)?,
124            updated_at: row.get(9)?,
125        })
126    });
127
128    match task {
129        Ok(t) => Ok(Some(t)),
130        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
131        Err(e) => Err(e),
132    }
133}
134
135pub fn list_tasks_filtered(
136    conn: &Connection,
137    status: Option<&str>,
138    priority: Option<&str>,
139) -> Result<Vec<Task>, rusqlite::Error> {
140    let mut conditions = Vec::new();
141    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
142
143    if let Some(s) = status {
144        conditions.push("status = ?");
145        params.push(Box::new(s.to_string()));
146    }
147
148    if let Some(p) = priority {
149        conditions.push("priority = ?");
150        params.push(Box::new(p.to_string()));
151    }
152
153    let sql = if conditions.is_empty() {
154        "SELECT id, title, description, status, priority, project_id, branch, due_date, created_at, updated_at FROM tasks".to_string()
155    } else {
156        format!(
157            "SELECT id, title, description, status, priority, project_id, branch, due_date, created_at, updated_at FROM tasks WHERE {}",
158            conditions.join(" AND ")
159        )
160    };
161
162    let mut stmt = conn.prepare(&sql)?;
163
164    let tasks = stmt.query_map(rusqlite::params_from_iter(params), |row| {
165        Ok(Task {
166            id: row.get(0)?,
167            title: row.get(1)?,
168            description: row.get(2)?,
169            status: row.get::<_, String>(3)?.parse().unwrap_or(Status::Todo),
170            priority: row.get::<_, String>(4)?.parse().unwrap_or(Priority::Medium),
171            project_id: row.get(5)?,
172            branch: row.get(6)?,
173            due_date: row.get(7)?,
174            created_at: row.get(8)?,
175            updated_at: row.get(9)?,
176        })
177    })?;
178
179    tasks.collect()
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::db::initialize;
186
187    fn setup_db() -> Connection {
188        initialize(":memory:").expect("Failed to initialize test database")
189    }
190
191    #[test]
192    fn test_create_task() {
193        let conn = setup_db();
194        let id = create_task(&conn, "Test task", Some(Priority::High)).unwrap();
195        assert_eq!(id, 1);
196    }
197
198    #[test]
199    fn test_create_task_default_priority() {
200        let conn = setup_db();
201        let id = create_task(&conn, "Test task", None).unwrap();
202        let tasks = list_tasks_filtered(&conn, None, None).unwrap();
203        assert_eq!(tasks.len(), 1);
204        assert_eq!(tasks[0].id, id);
205        assert_eq!(tasks[0].priority, Priority::Medium);
206    }
207
208    #[test]
209    fn test_list_tasks_empty() {
210        let conn = setup_db();
211        let tasks = list_tasks_filtered(&conn, None, None).unwrap();
212        assert!(tasks.is_empty());
213    }
214
215    #[test]
216    fn test_list_tasks_filtered_by_status() {
217        let conn = setup_db();
218        create_task(&conn, "Task 1", None).unwrap();
219        create_task(&conn, "Task 2", None).unwrap();
220        update_task_status(&conn, 1, &Status::Done).unwrap();
221
222        let done = list_tasks_filtered(&conn, Some("done"), None).unwrap();
223        assert_eq!(done.len(), 1);
224        assert_eq!(done[0].title, "Task 1");
225
226        let todo = list_tasks_filtered(&conn, Some("todo"), None).unwrap();
227        assert_eq!(todo.len(), 1);
228        assert_eq!(todo[0].title, "Task 2");
229    }
230
231    #[test]
232    fn test_list_tasks_filtered_by_priority() {
233        let conn = setup_db();
234        create_task(&conn, "Low task", Some(Priority::Low)).unwrap();
235        create_task(&conn, "High task", Some(Priority::High)).unwrap();
236
237        let high = list_tasks_filtered(&conn, None, Some("high")).unwrap();
238        assert_eq!(high.len(), 1);
239        assert_eq!(high[0].title, "High task");
240    }
241
242    #[test]
243    fn test_update_task_status() {
244        let conn = setup_db();
245        create_task(&conn, "Test task", None).unwrap();
246
247        let rows = update_task_status(&conn, 1, &Status::Done).unwrap();
248        assert_eq!(rows, 1);
249
250        let tasks = list_tasks_filtered(&conn, Some("done"), None).unwrap();
251        assert_eq!(tasks.len(), 1);
252    }
253
254    #[test]
255    fn test_update_nonexistent_task() {
256        let conn = setup_db();
257        let rows = update_task_status(&conn, 999, &Status::Done).unwrap();
258        assert_eq!(rows, 0);
259    }
260
261    #[test]
262    fn test_delete_task() {
263        let conn = setup_db();
264        create_task(&conn, "Test task", None).unwrap();
265
266        let rows = delete_task(&conn, 1).unwrap();
267        assert_eq!(rows, 1);
268
269        let tasks = list_tasks_filtered(&conn, None, None).unwrap();
270        assert!(tasks.is_empty());
271    }
272
273    #[test]
274    fn test_delete_nonexistent_task() {
275        let conn = setup_db();
276        let rows = delete_task(&conn, 999).unwrap();
277        assert_eq!(rows, 0);
278    }
279
280    #[test]
281    fn test_edit_task() {
282        let conn = setup_db();
283        create_task(&conn, "Original", Some(Priority::Low)).unwrap();
284
285        edit_task(&conn, 1, Some("Updated"), Some(Priority::High), None).unwrap();
286
287        let tasks = list_tasks_filtered(&conn, None, None).unwrap();
288        assert_eq!(tasks[0].title, "Updated");
289        assert_eq!(tasks[0].priority, Priority::High);
290    }
291
292    #[test]
293    fn test_edit_task_partial() {
294        let conn = setup_db();
295        create_task(&conn, "Original", Some(Priority::Low)).unwrap();
296
297        edit_task(&conn, 1, Some("New title"), None, None).unwrap();
298
299        let tasks = list_tasks_filtered(&conn, None, None).unwrap();
300        assert_eq!(tasks[0].title, "New title");
301        assert_eq!(tasks[0].priority, Priority::Low);
302    }
303
304    #[test]
305    fn test_edit_no_fields() {
306        let conn = setup_db();
307        create_task(&conn, "Original", None).unwrap();
308
309        let rows = edit_task(&conn, 1, None, None, None).unwrap();
310        assert_eq!(rows, 0);
311
312        let tasks = list_tasks_filtered(&conn, None, None).unwrap();
313        assert_eq!(tasks[0].title, "Original");
314    }
315}