rm.rs

  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}