From 8c9c9390d88bd24fa81bed76e536856eaf2942f0 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 19 Apr 2026 11:00:37 +0100 Subject: [PATCH] fix(storage): PRAGMA foreign_keys=ON; atomic transaction in complete_subtask_and_check_parent; uncomplete_task moved to storage layer --- crates/storage/src/database.rs | 55 +++++++++++++++++++++++---------- crates/storage/src/lib.rs | 2 +- src-tauri/src/commands/tasks.rs | 8 ++--- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/crates/storage/src/database.rs b/crates/storage/src/database.rs index 97e40b6..fe1bcae 100644 --- a/crates/storage/src/database.rs +++ b/crates/storage/src/database.rs @@ -21,6 +21,11 @@ pub async fn init(db_path: &Path) -> Result { .await .map_err(|e| KonError::StorageError(format!("Database connect failed: {e}")))?; + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&pool) + .await + .map_err(|e| KonError::StorageError(format!("foreign_keys pragma failed: {e}")))?; + run_migrations(&pool).await?; Ok(pool) @@ -343,10 +348,14 @@ pub async fn list_subtasks(pool: &SqlitePool, parent_id: &str) -> Result Result<()> { + let mut tx = pool.begin().await + .map_err(|e| KonError::StorageError(format!("Begin transaction failed: {e}")))?; + sqlx::query("UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?") .bind(subtask_id) - .execute(pool) + .execute(&mut *tx) .await .map_err(|e| KonError::StorageError(format!("Complete subtask failed: {e}")))?; @@ -354,28 +363,31 @@ pub async fn complete_subtask_and_check_parent(pool: &SqlitePool, subtask_id: &s "SELECT parent_task_id FROM tasks WHERE id = ?" ) .bind(subtask_id) - .fetch_one(pool) + .fetch_one(&mut *tx) .await .map_err(|e| KonError::StorageError(format!("Get parent_task_id failed: {e}")))?; - let Some(pid) = parent_id else { return Ok(()); }; + if let Some(pid) = parent_id { + let pending: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM tasks WHERE parent_task_id = ? AND done = 0" + ) + .bind(&pid) + .fetch_one(&mut *tx) + .await + .map_err(|e| KonError::StorageError(format!("Count pending subtasks failed: {e}")))?; - let pending: i64 = sqlx::query_scalar( - "SELECT COUNT(*) FROM tasks WHERE parent_task_id = ? AND done = 0" - ) - .bind(&pid) - .fetch_one(pool) - .await - .map_err(|e| KonError::StorageError(format!("Count pending subtasks failed: {e}")))?; - - if pending == 0 { - sqlx::query("UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?") - .bind(&pid) - .execute(pool) - .await - .map_err(|e| KonError::StorageError(format!("Auto-complete parent failed: {e}")))?; + if pending == 0 { + sqlx::query("UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?") + .bind(&pid) + .execute(&mut *tx) + .await + .map_err(|e| KonError::StorageError(format!("Auto-complete parent failed: {e}")))?; + } } + tx.commit().await + .map_err(|e| KonError::StorageError(format!("Commit transaction failed: {e}")))?; + Ok(()) } @@ -388,6 +400,15 @@ pub async fn complete_task(pool: &SqlitePool, id: &str) -> Result<()> { Ok(()) } +pub async fn uncomplete_task(pool: &SqlitePool, id: &str) -> Result<()> { + sqlx::query("UPDATE tasks SET done = 0, done_at = NULL WHERE id = ?") + .bind(id) + .execute(pool) + .await + .map_err(|e| KonError::StorageError(format!("Uncomplete task failed: {e}")))?; + Ok(()) +} + pub async fn delete_task(pool: &SqlitePool, id: &str) -> Result<()> { sqlx::query("DELETE FROM tasks WHERE id = ?") .bind(id) diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index e17f398..23b132a 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -4,7 +4,7 @@ pub mod migrations; pub use database::{ add_dictionary_entry, complete_subtask_and_check_parent, complete_task, count_transcripts, - delete_dictionary_entry, delete_task, delete_transcript, get_setting, get_task_by_id, + delete_dictionary_entry, delete_task, delete_transcript, get_setting, get_task_by_id, uncomplete_task, get_transcript, init, insert_subtask, insert_task, insert_transcript, list_dictionary, list_recent_errors, list_subtasks, list_tasks, list_transcripts, list_transcripts_paged, log_error, search_transcripts, set_setting, update_transcript, DictionaryEntry, ErrorLogRow, diff --git a/src-tauri/src/commands/tasks.rs b/src-tauri/src/commands/tasks.rs index baf3bd4..2ee7033 100644 --- a/src-tauri/src/commands/tasks.rs +++ b/src-tauri/src/commands/tasks.rs @@ -15,6 +15,7 @@ use kon_storage::{ insert_task as db_insert_task, list_subtasks as db_list_subtasks, list_tasks as db_list_tasks, + uncomplete_task as db_uncomplete_task, TaskRow, }; @@ -113,12 +114,9 @@ pub async fn uncomplete_task_cmd( state: tauri::State<'_, AppState>, id: String, ) -> Result<(), String> { - sqlx::query("UPDATE tasks SET done = 0, done_at = NULL WHERE id = ?") - .bind(&id) - .execute(&state.db) + db_uncomplete_task(&state.db, &id) .await - .map_err(|e| format!("Uncomplete task failed: {e}")) - .map(|_| ()) + .map_err(|e| e.to_string()) } #[tauri::command]