diff --git a/crates/storage/src/migrations.rs b/crates/storage/src/migrations.rs index cd01292..cf362b5 100644 --- a/crates/storage/src/migrations.rs +++ b/crates/storage/src/migrations.rs @@ -108,6 +108,10 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[ CREATE INDEX IF NOT EXISTS idx_dictionary_term ON dictionary(term) "#), + (3, "micro-stepping: parent_task_id on tasks", r#" + ALTER TABLE tasks ADD COLUMN parent_task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE; + CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id) + "#), ]; /// Split SQL into individual statements, respecting BEGIN...END trigger blocks. @@ -207,7 +211,7 @@ mod tests { .fetch_one(&pool) .await .unwrap(); - assert_eq!(count, 1); + assert_eq!(count, 3); sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')") .execute(&pool) @@ -229,6 +233,40 @@ mod tests { .fetch_one(&pool) .await .unwrap(); - assert_eq!(count, 1); + assert_eq!(count, 3); + } + + #[tokio::test] + async fn test_parent_task_id_cascade_delete() { + let pool = SqlitePoolOptions::new() + .connect("sqlite::memory:") + .await + .unwrap(); + + run_migrations(&pool).await.unwrap(); + + // Insert parent task + sqlx::query("INSERT INTO tasks (id, text) VALUES ('parent-1', 'Parent task')") + .execute(&pool) + .await + .unwrap(); + + // Insert child task with parent_task_id + sqlx::query("INSERT INTO tasks (id, text, parent_task_id) VALUES ('child-1', 'Child task', 'parent-1')") + .execute(&pool) + .await + .unwrap(); + + // Delete parent — child should cascade + sqlx::query("DELETE FROM tasks WHERE id = 'parent-1'") + .execute(&pool) + .await + .unwrap(); + + let remaining: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM tasks") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(remaining, 0, "cascade delete should remove child when parent is deleted"); } }