fix(storage): PRAGMA foreign_keys=ON; atomic transaction in complete_subtask_and_check_parent; uncomplete_task moved to storage layer
This commit is contained in:
@@ -21,6 +21,11 @@ pub async fn init(db_path: &Path) -> Result<SqlitePool> {
|
||||
.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<Vec<Tas
|
||||
}
|
||||
|
||||
/// Mark a subtask done. If all siblings are now done, auto-complete the parent.
|
||||
/// Runs in a transaction so concurrent completions see consistent sibling counts.
|
||||
pub async fn complete_subtask_and_check_parent(pool: &SqlitePool, subtask_id: &str) -> 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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user