aven_core/query/
related.rs1use std::collections::HashMap;
2
3use anyhow::Result;
4use sqlx::{QueryBuilder, Row, Sqlite, SqliteConnection};
5
6use crate::choices::{TaskPriority, TaskStatus};
7use crate::ids::{TaskId, WorkspaceId};
8use crate::refs::DisplayRefContext;
9
10const RELATED_BIND_CHUNK_SIZE: usize = 449;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct TaskRelatedLink {
14 pub task_id: TaskId,
15 pub display_ref: String,
16 pub title: String,
17 pub status: TaskStatus,
18 pub priority: TaskPriority,
19 pub deleted: bool,
20 pub linked_at: String,
21}
22
23pub(crate) async fn related_links_for_tasks(
24 conn: &mut SqliteConnection,
25 workspace_id: &WorkspaceId,
26 task_ids: &[TaskId],
27 display_refs: &DisplayRefContext,
28) -> Result<HashMap<TaskId, Vec<TaskRelatedLink>>> {
29 let mut links_by_task = HashMap::new();
30 for chunk in task_ids.chunks(RELATED_BIND_CHUNK_SIZE) {
31 let mut query = QueryBuilder::<Sqlite>::new(
32 "SELECT r.task_a_id AS source_task_id,
33 t.id, t.title, t.status, t.priority, t.deleted,
34 p.prefix AS project_prefix, c.created_at AS linked_at
35 FROM task_related_links r
36 JOIN tasks t ON t.workspace_id = r.workspace_id AND t.id = r.task_b_id
37 JOIN projects p ON p.workspace_id = t.workspace_id AND p.id = t.project_id
38 JOIN changes c ON c.change_id = r.last_change_id
39 WHERE r.workspace_id = ",
40 );
41 query.push_bind(workspace_id);
42 query.push(" AND r.task_a_id IN (");
43 {
44 let mut separated = query.separated(", ");
45 for task_id in chunk {
46 separated.push_bind(task_id);
47 }
48 }
49 query.push(
50 ") AND r.linked = 1 UNION ALL SELECT r.task_b_id AS source_task_id,
51 t.id, t.title, t.status, t.priority, t.deleted,
52 p.prefix AS project_prefix, c.created_at AS linked_at
53 FROM task_related_links r
54 JOIN tasks t ON t.workspace_id = r.workspace_id AND t.id = r.task_a_id
55 JOIN projects p ON p.workspace_id = t.workspace_id AND p.id = t.project_id
56 JOIN changes c ON c.change_id = r.last_change_id
57 WHERE r.workspace_id = ",
58 );
59 query.push_bind(workspace_id);
60 query.push(" AND r.task_b_id IN (");
61 {
62 let mut separated = query.separated(", ");
63 for task_id in chunk {
64 separated.push_bind(task_id);
65 }
66 }
67 query.push(") AND r.linked = 1");
68
69 for row in query.build().fetch_all(&mut *conn).await? {
70 let source_task_id: TaskId = row.get("source_task_id");
71 let related_id: TaskId = row.get("id");
72 let project_prefix: String = row.get("project_prefix");
73 links_by_task
74 .entry(source_task_id)
75 .or_insert_with(Vec::new)
76 .push(TaskRelatedLink {
77 display_ref: display_refs.display_ref_for_id(
78 workspace_id,
79 &project_prefix,
80 &related_id,
81 ),
82 task_id: related_id,
83 title: row.get("title"),
84 status: TaskStatus::parse(&row.get::<String, _>("status"))?,
85 priority: TaskPriority::parse(&row.get::<String, _>("priority"))?,
86 deleted: row.get::<i64, _>("deleted") != 0,
87 linked_at: row.get("linked_at"),
88 });
89 }
90 }
91
92 for links in links_by_task.values_mut() {
93 links.sort_by(|left, right| {
94 left.deleted
95 .cmp(&right.deleted)
96 .then_with(|| {
97 super::status_order(left.status).cmp(&super::status_order(right.status))
98 })
99 .then_with(|| left.title.cmp(&right.title))
100 .then_with(|| left.linked_at.cmp(&right.linked_at))
101 .then_with(|| left.task_id.cmp(&right.task_id))
102 });
103 }
104 Ok(links_by_task)
105}
106
107pub(crate) async fn task_related_links(
108 conn: &mut SqliteConnection,
109 workspace_id: &WorkspaceId,
110 task_id: &TaskId,
111 display_refs: Option<&DisplayRefContext>,
112) -> Result<Vec<TaskRelatedLink>> {
113 let owned_refs;
114 let display_refs = match display_refs {
115 Some(refs) => refs,
116 None => {
117 owned_refs = DisplayRefContext::for_workspace(conn, workspace_id).await?;
118 &owned_refs
119 }
120 };
121 Ok(related_links_for_tasks(
122 conn,
123 workspace_id,
124 std::slice::from_ref(task_id),
125 display_refs,
126 )
127 .await?
128 .remove(task_id)
129 .unwrap_or_default())
130}