feat(storage): add parent_task_id migration for micro-stepping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-21 14:43:06 +00:00
parent b1fa2739b7
commit c61f0585e0
3 changed files with 60 additions and 9 deletions

View File

@@ -194,7 +194,7 @@ pub async fn insert_task(
pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
let rows =
sqlx::query("SELECT id, text, bucket, list_id, effort, done, done_at, created_at, source_transcript_id, priority, project, status, updated_at, sort_order, notes FROM tasks ORDER BY sort_order ASC, created_at DESC")
sqlx::query("SELECT id, text, bucket, list_id, effort, done, done_at, created_at, source_transcript_id, priority, project, status, updated_at, sort_order, notes, parent_task_id FROM tasks ORDER BY sort_order ASC, created_at DESC")
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List tasks failed: {e}")))?;
@@ -303,7 +303,7 @@ pub async fn list_tasks_by_status(
limit: i64,
) -> Result<Vec<TaskRow>> {
let rows = sqlx::query(
"SELECT id, text, bucket, list_id, effort, done, done_at, created_at, source_transcript_id, priority, project, status, updated_at, sort_order, notes
"SELECT id, text, bucket, list_id, effort, done, done_at, created_at, source_transcript_id, priority, project, status, updated_at, sort_order, notes, parent_task_id
FROM tasks WHERE status = ?
ORDER BY sort_order ASC, created_at DESC
LIMIT ?",
@@ -317,6 +317,49 @@ pub async fn list_tasks_by_status(
Ok(rows.into_iter().map(task_row_from).collect())
}
// --- Subtask (micro-step) queries ---
/// List subtasks (children) of a parent task, ordered by sort_order.
pub async fn list_subtasks(pool: &SqlitePool, parent_task_id: &str) -> Result<Vec<TaskRow>> {
let rows = sqlx::query(
"SELECT id, text, bucket, list_id, effort, done, done_at, created_at, source_transcript_id, priority, project, status, updated_at, sort_order, notes, parent_task_id
FROM tasks WHERE parent_task_id = ?
ORDER BY sort_order ASC",
)
.bind(parent_task_id)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("List subtasks failed: {e}")))?;
Ok(rows.into_iter().map(task_row_from).collect())
}
/// Check whether all subtasks of a parent are done.
pub async fn all_subtasks_done(pool: &SqlitePool, parent_task_id: &str) -> Result<bool> {
let count: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ? AND done = 0",
)
.bind(parent_task_id)
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Check subtasks failed: {e}")))?;
Ok(count == 0)
}
/// Check whether a task has any subtasks.
pub async fn has_subtasks(pool: &SqlitePool, task_id: &str) -> Result<bool> {
let count: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ?",
)
.bind(task_id)
.fetch_one(pool)
.await
.map_err(|e| KonError::StorageError(format!("Check has subtasks failed: {e}")))?;
Ok(count > 0)
}
// --- FTS5 Search ---
/// Full-text search across transcripts using the FTS5 virtual table.
@@ -455,6 +498,8 @@ pub struct TaskRow {
pub updated_at: String,
pub sort_order: i64,
pub notes: String,
// v3 field
pub parent_task_id: Option<String>,
}
#[derive(Debug, Clone)]
@@ -484,6 +529,7 @@ fn task_row_from(r: sqlx::sqlite::SqliteRow) -> TaskRow {
updated_at: r.get("updated_at"),
sort_order: r.get("sort_order"),
notes: r.get("notes"),
parent_task_id: r.get("parent_task_id"),
}
}

View File

@@ -3,10 +3,11 @@ pub mod file_storage;
pub mod migrations;
pub use database::{
clear_timer_state, complete_task, delete_task, delete_transcript, get_segments,
get_setting, get_timer_state, get_transcript, init, insert_segments, insert_task,
insert_task_v2, insert_transcript, list_tasks, list_tasks_by_status, list_transcripts,
log_error, reorder_tasks, save_timer_state, search_transcripts, set_setting,
update_task_v2, InsertTranscriptParams, SegmentRow, TaskRow, TimerStateRow, TranscriptRow,
all_subtasks_done, clear_timer_state, complete_task, delete_task, delete_transcript,
get_segments, get_setting, get_timer_state, get_transcript, has_subtasks, init,
insert_segments, insert_task, insert_task_v2, insert_transcript, list_subtasks, list_tasks,
list_tasks_by_status, list_transcripts, log_error, reorder_tasks, save_timer_state,
search_transcripts, set_setting, update_task_v2, InsertTranscriptParams, SegmentRow,
TaskRow, TimerStateRow, TranscriptRow,
};
pub use file_storage::{app_data_dir, database_path, recordings_dir};

View File

@@ -118,6 +118,10 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project)
"#),
(3, "phase 2b — parent_task_id for micro-stepping", r#"
ALTER TABLE tasks ADD COLUMN parent_task_id TEXT;
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id)
"#),
];
/// Split SQL text into individual statements, respecting BEGIN...END blocks
@@ -236,7 +240,7 @@ mod tests {
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 2);
assert_eq!(count, 3);
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
.execute(&pool)
@@ -279,6 +283,6 @@ mod tests {
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 2);
assert_eq!(count, 3);
}
}