Skip to main content

aven_core/operations/
dependencies.rs

1use crate::ids::WorkspaceId;
2use std::collections::HashSet;
3
4use anyhow::{Result, bail};
5use sqlx::SqliteConnection;
6
7use crate::change_log::{ChangeEntity, ChangePayload, append_change, op_type};
8use crate::db::{Database, begin_immediate};
9use crate::ids::{TaskId, now};
10use crate::refs::get_task_in_workspace;
11use crate::undo::{UndoCommand, UndoContext, UndoPayload, record_tui_undo};
12use crate::workspaces::Workspace;
13
14impl Database {
15    pub async fn add_task_dependency(
16        &self,
17        workspace: &Workspace,
18        task_id: &TaskId,
19        depends_on_id: &TaskId,
20    ) -> Result<DependencyOutcome> {
21        self.add_task_dependency_with_undo(workspace, task_id, depends_on_id, UndoContext::None)
22            .await
23    }
24
25    pub async fn add_task_dependency_with_undo(
26        &self,
27        workspace: &Workspace,
28        task_id: &TaskId,
29        depends_on_id: &TaskId,
30        undo: UndoContext,
31    ) -> Result<DependencyOutcome> {
32        let mut conn = self.acquire().await?;
33        let mut tx = begin_immediate(&mut conn).await?;
34        let outcome =
35            add_task_dependency_in_transaction(&mut tx, workspace, task_id, depends_on_id).await?;
36        if outcome.changed
37            && let UndoContext::Tui { summary } = undo
38        {
39            record_tui_undo(
40                &mut tx,
41                &workspace.id,
42                &summary,
43                UndoPayload {
44                    commands: vec![UndoCommand::AddTaskDependency {
45                        task_id: outcome.task.id.clone(),
46                        depends_on_task_id: outcome.depends_on.id.clone(),
47                    }],
48                },
49            )
50            .await?;
51        }
52        tx.commit().await?;
53        Ok(outcome)
54    }
55
56    pub async fn remove_task_dependency(
57        &self,
58        workspace: &Workspace,
59        task_id: &TaskId,
60        depends_on_id: &TaskId,
61    ) -> Result<DependencyOutcome> {
62        self.remove_task_dependency_with_undo(workspace, task_id, depends_on_id, UndoContext::None)
63            .await
64    }
65
66    pub async fn remove_task_dependency_with_undo(
67        &self,
68        workspace: &Workspace,
69        task_id: &TaskId,
70        depends_on_id: &TaskId,
71        undo: UndoContext,
72    ) -> Result<DependencyOutcome> {
73        let mut conn = self.acquire().await?;
74        let mut tx = begin_immediate(&mut conn).await?;
75        let outcome =
76            remove_task_dependency_in_transaction(&mut tx, workspace, task_id, depends_on_id)
77                .await?;
78        if outcome.changed
79            && let UndoContext::Tui { summary } = undo
80        {
81            record_tui_undo(
82                &mut tx,
83                &workspace.id,
84                &summary,
85                UndoPayload {
86                    commands: vec![UndoCommand::RemoveTaskDependency {
87                        task_id: outcome.task.id.clone(),
88                        depends_on_task_id: outcome.depends_on.id.clone(),
89                    }],
90                },
91            )
92            .await?;
93        }
94        tx.commit().await?;
95        Ok(outcome)
96    }
97}
98
99pub struct DependencyOutcome {
100    pub task: crate::types::Task,
101    pub depends_on: crate::types::Task,
102    pub changed: bool,
103}
104
105struct DependencyPair {
106    task: crate::types::Task,
107    depends_on: crate::types::Task,
108}
109
110async fn load_dependency_pair(
111    conn: &mut SqliteConnection,
112    workspace: &Workspace,
113    task_id: &crate::ids::TaskId,
114    depends_on_id: &crate::ids::TaskId,
115) -> Result<DependencyPair> {
116    if task_id == depends_on_id {
117        bail!("error dependency-self task_id={task_id}");
118    }
119
120    let task = get_task_in_workspace(conn, workspace, task_id).await?;
121    let depends_on = get_task_in_workspace(conn, workspace, depends_on_id).await?;
122
123    Ok(DependencyPair { task, depends_on })
124}
125
126async fn record_dependency_change(
127    conn: &mut SqliteConnection,
128    workspace: &Workspace,
129    pair: &DependencyPair,
130    op_type: &'static str,
131) -> Result<()> {
132    append_change(
133        conn,
134        ChangeEntity::Task,
135        &pair.task.id,
136        Some("dependencies"),
137        op_type,
138        ChangePayload::workspace(workspace).set("depends_on_task_id", pair.depends_on.id.clone()),
139    )
140    .await?;
141    Ok(())
142}
143
144#[cfg(any(test, feature = "test-support"))]
145pub async fn add_task_dependency(
146    conn: &mut SqliteConnection,
147    workspace: &Workspace,
148    task_id: &crate::ids::TaskId,
149    depends_on_id: &crate::ids::TaskId,
150) -> Result<DependencyOutcome> {
151    let mut tx = begin_immediate(conn).await?;
152    let outcome =
153        add_task_dependency_in_transaction(&mut tx, workspace, task_id, depends_on_id).await?;
154    tx.commit().await?;
155    Ok(outcome)
156}
157
158async fn add_task_dependency_in_transaction(
159    conn: &mut SqliteConnection,
160    workspace: &Workspace,
161    task_id: &crate::ids::TaskId,
162    depends_on_id: &crate::ids::TaskId,
163) -> Result<DependencyOutcome> {
164    let pair = load_dependency_pair(conn, workspace, task_id, depends_on_id).await?;
165
166    if dependency_path_exists(
167        conn,
168        &pair.task.workspace_id,
169        &pair.depends_on.id,
170        &pair.task.id,
171    )
172    .await?
173    {
174        bail!("error dependency-cycle task_id={task_id} depends_on_task_id={depends_on_id}");
175    }
176
177    let created_at = now();
178    let changed = sqlx::query(
179        "INSERT OR IGNORE INTO task_dependencies(workspace_id, task_id, depends_on_task_id, created_at)
180         VALUES (?, ?, ?, ?)",
181    )
182    .bind(&pair.task.workspace_id)
183    .bind(&pair.task.id)
184    .bind(&pair.depends_on.id)
185    .bind(&created_at)
186    .execute(&mut *conn)
187    .await?
188    .rows_affected()
189        > 0;
190
191    if changed {
192        record_dependency_change(conn, workspace, &pair, op_type::DEPENDENCY_ADD).await?;
193    }
194
195    Ok(DependencyOutcome {
196        task: pair.task,
197        depends_on: pair.depends_on,
198        changed,
199    })
200}
201
202async fn remove_task_dependency_in_transaction(
203    conn: &mut SqliteConnection,
204    workspace: &Workspace,
205    task_id: &crate::ids::TaskId,
206    depends_on_id: &crate::ids::TaskId,
207) -> Result<DependencyOutcome> {
208    let pair = load_dependency_pair(conn, workspace, task_id, depends_on_id).await?;
209
210    let changed = sqlx::query(
211        "DELETE FROM task_dependencies
212         WHERE workspace_id = ? AND task_id = ? AND depends_on_task_id = ?",
213    )
214    .bind(&pair.task.workspace_id)
215    .bind(&pair.task.id)
216    .bind(&pair.depends_on.id)
217    .execute(&mut *conn)
218    .await?
219    .rows_affected()
220        > 0;
221
222    if changed {
223        record_dependency_change(conn, workspace, &pair, op_type::DEPENDENCY_REMOVE).await?;
224    }
225
226    Ok(DependencyOutcome {
227        task: pair.task,
228        depends_on: pair.depends_on,
229        changed,
230    })
231}
232
233pub async fn dependency_path_exists(
234    conn: &mut SqliteConnection,
235    workspace_id: &WorkspaceId,
236    from_task_id: &crate::ids::TaskId,
237    to_task_id: &crate::ids::TaskId,
238) -> Result<bool> {
239    let mut visited = HashSet::new();
240    let mut stack = vec![from_task_id.clone()];
241    while let Some(current) = stack.pop() {
242        if !visited.insert(current.clone()) {
243            continue;
244        }
245        if &current == to_task_id {
246            return Ok(true);
247        }
248        let next = sqlx::query_scalar::<_, crate::ids::TaskId>(
249            "SELECT depends_on_task_id
250             FROM task_dependencies
251             WHERE workspace_id = ? AND task_id = ?",
252        )
253        .bind(workspace_id)
254        .bind(&current)
255        .fetch_all(&mut *conn)
256        .await?;
257        stack.extend(next);
258    }
259    Ok(false)
260}