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}