1use anyhow::{bail, Result};
2use serde::Serialize;
3use std::collections::BTreeSet;
4use std::path::Path;
5
6use crate::db;
7
8#[derive(Serialize)]
9struct RmResult {
10 requested_ids: Vec<String>,
11 deleted_ids: Vec<String>,
12 unblocked_ids: Vec<String>,
13}
14
15pub fn run(root: &Path, ids: &[String], recursive: bool, force: bool, json: bool) -> Result<()> {
16 let mut conn = db::open(root)?;
17 let tx = conn.transaction()?;
18
19 let mut to_delete = BTreeSet::new();
20 for id in ids {
21 if !db::task_exists(&tx, id)? {
22 bail!("task '{id}' not found");
23 }
24
25 if recursive {
26 for subtree_id in load_subtree_ids(&tx, id)? {
27 to_delete.insert(subtree_id);
28 }
29 } else {
30 let child_count: i64 = tx.query_row(
31 "SELECT COUNT(*) FROM tasks WHERE parent = ?1",
32 [id],
33 |row| row.get(0),
34 )?;
35 if child_count > 0 {
36 bail!("task '{id}' has children; use --recursive to delete subtree");
37 }
38 to_delete.insert(id.clone());
39 }
40 }
41
42 let deleted_ids: Vec<String> = to_delete.into_iter().collect();
43 let unblocked_ids = detach_dependents(&tx, &deleted_ids)?;
44
45 if !deleted_ids.is_empty() {
46 delete_tasks(&tx, &deleted_ids)?;
47 }
48
49 tx.commit()?;
50
51 if !force && !unblocked_ids.is_empty() {
52 eprintln!(
53 "warning: removed blockers from {}",
54 unblocked_ids.join(", ")
55 );
56 }
57
58 if json {
59 let out = RmResult {
60 requested_ids: ids.to_vec(),
61 deleted_ids,
62 unblocked_ids,
63 };
64 println!("{}", serde_json::to_string(&out)?);
65 } else {
66 let c = crate::color::stdout_theme();
67 for id in &deleted_ids {
68 println!("{}deleted{} {id}", c.green, c.reset);
69 }
70 }
71
72 Ok(())
73}
74
75fn load_subtree_ids(tx: &rusqlite::Transaction, root_id: &str) -> Result<Vec<String>> {
76 let mut stmt = tx.prepare(
77 "WITH RECURSIVE subtree(id) AS (
78 SELECT id FROM tasks WHERE id = ?1
79 UNION ALL
80 SELECT tasks.id
81 FROM tasks
82 JOIN subtree ON tasks.parent = subtree.id
83 )
84 SELECT id FROM subtree",
85 )?;
86 let ids = stmt
87 .query_map([root_id], |row| row.get(0))?
88 .collect::<rusqlite::Result<Vec<String>>>()?;
89 Ok(ids)
90}
91
92fn detach_dependents(tx: &rusqlite::Transaction, deleted_ids: &[String]) -> Result<Vec<String>> {
93 if deleted_ids.is_empty() {
94 return Ok(Vec::new());
95 }
96
97 let in_placeholders = vec!["?"; deleted_ids.len()].join(", ");
98 let sql = format!(
99 "SELECT DISTINCT task_id
100 FROM blockers
101 WHERE blocker_id IN ({in_placeholders})
102 AND task_id NOT IN ({in_placeholders})
103 ORDER BY task_id"
104 );
105 let params = deleted_ids.iter().chain(deleted_ids.iter());
106 let mut stmt = tx.prepare(&sql)?;
107 let unblocked_ids = stmt
108 .query_map(rusqlite::params_from_iter(params), |row| row.get(0))?
109 .collect::<rusqlite::Result<Vec<String>>>()?;
110
111 if unblocked_ids.is_empty() {
112 return Ok(unblocked_ids);
113 }
114
115 let delete_sql = format!(
116 "DELETE FROM blockers
117 WHERE blocker_id IN ({in_placeholders})
118 AND task_id NOT IN ({in_placeholders})"
119 );
120 let delete_params = deleted_ids.iter().chain(deleted_ids.iter());
121 tx.execute(&delete_sql, rusqlite::params_from_iter(delete_params))?;
122
123 let update_placeholders = vec!["?"; unblocked_ids.len()].join(", ");
124 let update_sql = format!(
125 "UPDATE tasks
126 SET updated = ?1
127 WHERE id IN ({update_placeholders})"
128 );
129 let ts = db::now_utc();
130 let update_params = std::iter::once(&ts).chain(unblocked_ids.iter());
131 tx.execute(&update_sql, rusqlite::params_from_iter(update_params))?;
132
133 Ok(unblocked_ids)
134}
135
136fn delete_tasks(tx: &rusqlite::Transaction, deleted_ids: &[String]) -> Result<()> {
137 let in_placeholders = vec!["?"; deleted_ids.len()].join(", ");
138 let sql = format!("DELETE FROM tasks WHERE id IN ({in_placeholders})");
139 tx.execute(&sql, rusqlite::params_from_iter(deleted_ids.iter()))?;
140 Ok(())
141}