Skip to main content

shipd_core/db/
tasks.rs

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