use rusqlite::{params, Connection, Result as SqlResult}; use chrono::NaiveDateTime; use crate::domain::models::*; pub struct Db { conn: Connection, } impl Db { pub fn new(path: &str) -> SqlResult { let conn = Connection::open(path)?; conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?; conn.execute_batch( "CREATE TABLE IF NOT EXISTS task_lists ( id TEXT PRIMARY KEY, title TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, list_id TEXT NOT NULL, title TEXT NOT NULL, notes TEXT, status TEXT NOT NULL DEFAULT 'needsAction', due TEXT, position INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL, FOREIGN KEY (list_id) REFERENCES task_lists(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS sync_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, action TEXT NOT NULL, task_id TEXT NOT NULL, list_id TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL );", )?; Ok(Self { conn }) } pub fn get_lists(&self) -> Vec { let mut stmt = self.conn .prepare("SELECT id, title FROM task_lists ORDER BY title") .unwrap(); stmt.query_map([], |row| { Ok(TaskList { id: row.get(0)?, title: row.get(1)?, }) }) .unwrap() .filter_map(|r| r.ok()) .collect() } pub fn insert_list(&self, list: &TaskList) -> SqlResult<()> { self.conn.execute( "INSERT OR REPLACE INTO task_lists (id, title) VALUES (?1, ?2)", params![list.id, list.title], )?; Ok(()) } pub fn delete_list(&self, list_id: &str) -> SqlResult<()> { self.conn.execute("DELETE FROM tasks WHERE list_id = ?1", params![list_id])?; self.conn.execute("DELETE FROM task_lists WHERE id = ?1", params![list_id])?; Ok(()) } pub fn get_tasks(&self, list_id: &str) -> Vec { let mut stmt = self.conn .prepare( "SELECT id, list_id, title, notes, status, due, position FROM tasks WHERE list_id = ?1 ORDER BY position ASC", ) .unwrap(); stmt.query_map(params![list_id], |row| { let due_str: Option = row.get(5)?; let due = due_str.and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M").ok()); Ok(Task { id: row.get(0)?, list_id: row.get(1)?, title: row.get(2)?, notes: row.get(3)?, status: match row.get::<_, String>(4)?.as_str() { "completed" => TaskStatus::Completed, _ => TaskStatus::NeedsAction, }, due, position: row.get(6)?, }) }) .unwrap() .filter_map(|r| r.ok()) .collect() } pub fn insert_task(&self, task: &Task) -> SqlResult<()> { let due_str = task.due.map(|d| d.format("%Y-%m-%d %H:%M").to_string()); let status_str = match task.status { TaskStatus::Completed => "completed", TaskStatus::NeedsAction => "needsAction", }; let position = if task.position == 0 { let max: i64 = self.conn .query_row( "SELECT COALESCE(MAX(position), -1) + 1 FROM tasks WHERE list_id = ?1", params![task.list_id], |row| row.get(0), ) .unwrap_or(0); max } else { task.position }; self.conn.execute( "INSERT OR REPLACE INTO tasks (id, list_id, title, notes, status, due, position, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![ task.id, task.list_id, task.title, task.notes, status_str, due_str, position, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), ], )?; Ok(()) } pub fn update_task(&self, task: &Task) -> SqlResult<()> { let due_str = task.due.map(|d| d.format("%Y-%m-%d %H:%M").to_string()); let status_str = match task.status { TaskStatus::Completed => "completed", TaskStatus::NeedsAction => "needsAction", }; self.conn.execute( "UPDATE tasks SET title=?1, notes=?2, status=?3, due=?4, position=?5, updated_at=?6 WHERE id=?7", params![ task.title, task.notes, status_str, due_str, task.position, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), task.id, ], )?; Ok(()) } pub fn delete_task(&self, task_id: &str) -> SqlResult<()> { self.conn.execute("DELETE FROM tasks WHERE id = ?1", params![task_id])?; Ok(()) } pub fn reorder_task(&self, task_id: &str, new_position: i64) -> SqlResult<()> { let (old_position, list_id): (i64, String) = self.conn.query_row( "SELECT position, list_id FROM tasks WHERE id = ?1", params![task_id], |row| Ok((row.get(0)?, row.get(1)?)), )?; if old_position == new_position { return Ok(()); } if new_position > old_position { self.conn.execute( "UPDATE tasks SET position = position - 1 WHERE list_id = ?1 AND position > ?2 AND position <= ?3", params![list_id, old_position, new_position], )?; } else { self.conn.execute( "UPDATE tasks SET position = position + 1 WHERE list_id = ?1 AND position >= ?2 AND position < ?3", params![list_id, new_position, old_position], )?; } self.conn.execute( "UPDATE tasks SET position = ?1 WHERE id = ?2", params![new_position, task_id], )?; Ok(()) } pub fn replace_all_lists(&self, lists: &[TaskList]) -> SqlResult<()> { self.conn.execute("DELETE FROM task_lists", [])?; for list in lists { self.insert_list(list)?; } Ok(()) } pub fn replace_all_tasks(&self, list_id: &str, tasks: &[Task]) -> SqlResult<()> { self.conn.execute("DELETE FROM tasks WHERE list_id = ?1", params![list_id])?; for (i, task) in tasks.iter().enumerate() { let mut t = task.clone(); if t.position == 0 { t.position = i as i64; } t.list_id = list_id.to_string(); self.insert_task(&t)?; } Ok(()) } pub fn push_sync(&self, action: SyncAction, task_id: &str, list_id: &str, payload: &str) -> SqlResult<()> { let action_str = serde_json::to_string(&action).unwrap_or_default(); self.conn.execute( "INSERT INTO sync_queue (action, task_id, list_id, payload, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", params![ action_str, task_id, list_id, payload, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), ], )?; Ok(()) } pub fn drain_sync(&self) -> Vec { let items: Vec = { let mut stmt = self.conn .prepare("SELECT id, action, task_id, list_id, payload, created_at FROM sync_queue ORDER BY id") .unwrap(); stmt.query_map([], |row| { let action_str: String = row.get(1)?; let action: SyncAction = serde_json::from_str(&action_str).unwrap_or(SyncAction::Update); Ok(SyncQueueItem { id: row.get(0)?, action, task_id: row.get(2)?, list_id: row.get(3)?, payload: row.get(4)?, created_at: row.get(5)?, }) }) .unwrap() .filter_map(|r| r.ok()) .collect() }; if !items.is_empty() { self.conn.execute("DELETE FROM sync_queue", []).unwrap(); } items } }