1use crate::ids::WorkspaceId;
2use anyhow::{Result, bail};
3use sqlx::{Row, SqliteConnection};
4
5use crate::db::Database;
6use crate::matching::is_near;
7use crate::operations::LabelOutcome;
8use crate::projects::normalize_key;
9use crate::workspaces::Workspace;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct LabelUsage {
13 pub name: String,
14 pub task_count: usize,
15 pub series_count: usize,
16}
17
18#[derive(Debug, Clone)]
19pub(crate) struct CreatedLabel {
20 pub(crate) name: String,
21 pub(crate) change_id: String,
22}
23
24#[derive(Debug, Default)]
25pub(crate) struct LabelResolution {
26 pub(crate) names: Vec<String>,
27 pub(crate) created: Vec<CreatedLabel>,
28}
29
30impl Database {
31 pub async fn label_usage(&self, workspace_id: &WorkspaceId) -> Result<Vec<LabelUsage>> {
32 let mut conn = self.acquire_reader().await?;
33 let rows = sqlx::query(
34 "SELECT l.name,
35 (SELECT count(*) FROM task_labels tl
36 WHERE tl.workspace_id = l.workspace_id AND tl.label = l.name) AS task_count,
37 (SELECT count(*) FROM recurrence_series_labels sl
38 WHERE sl.workspace_id = l.workspace_id AND sl.label = l.name) AS series_count
39 FROM labels l
40 WHERE l.workspace_id = ?
41 ORDER BY l.name",
42 )
43 .bind(workspace_id)
44 .fetch_all(&mut *conn)
45 .await?;
46 Ok(rows
47 .into_iter()
48 .map(|row| LabelUsage {
49 name: row.get("name"),
50 task_count: usize::try_from(row.get::<i64, _>("task_count")).unwrap_or(usize::MAX),
51 series_count: usize::try_from(row.get::<i64, _>("series_count"))
52 .unwrap_or(usize::MAX),
53 })
54 .collect())
55 }
56
57 pub async fn list_labels(
58 &self,
59 workspace_id: &WorkspaceId,
60 search: Option<&str>,
61 ) -> Result<Vec<String>> {
62 let mut conn = self.acquire_reader().await?;
63 list_labels_in_workspace(&mut conn, workspace_id, search).await
64 }
65
66 pub async fn resolve_labels(
67 &self,
68 workspace_id: &WorkspaceId,
69 labels: &[String],
70 ) -> Result<Vec<String>> {
71 let mut conn = self.acquire_reader().await?;
72 resolve_labels_in_workspace(&mut conn, workspace_id, labels).await
73 }
74}
75
76pub fn normalize_label(input: &str) -> String {
77 normalize_key(input)
78}
79
80async fn near_labels(
81 conn: &mut SqliteConnection,
82 workspace_id: &WorkspaceId,
83 input: &str,
84) -> Result<Vec<String>> {
85 let needle = normalize_label(input);
86 Ok(list_labels_in_workspace(conn, workspace_id, None)
87 .await?
88 .into_iter()
89 .filter(|label| is_near(&needle, label))
90 .collect())
91}
92
93pub(crate) async fn list_labels_in_workspace(
94 conn: &mut SqliteConnection,
95 workspace_id: &WorkspaceId,
96 search: Option<&str>,
97) -> Result<Vec<String>> {
98 let search = search.map(normalize_label);
99 let labels = sqlx::query_scalar::<_, String>(
100 "SELECT name FROM labels WHERE workspace_id = ? ORDER BY name",
101 )
102 .bind(workspace_id)
103 .fetch_all(&mut *conn)
104 .await?;
105 Ok(labels
106 .into_iter()
107 .filter(|label| {
108 search
109 .as_deref()
110 .is_none_or(|search| label.contains(search))
111 })
112 .collect())
113}
114
115pub(crate) async fn ensure_label_exists_in_workspace(
116 conn: &mut SqliteConnection,
117 workspace_id: &WorkspaceId,
118 label: &str,
119) -> Result<String> {
120 let label = normalize_label(label);
121 if sqlx::query_scalar::<_, i64>(
122 "SELECT count(*) FROM labels WHERE workspace_id = ? AND name = ?",
123 )
124 .bind(workspace_id)
125 .bind(&label)
126 .fetch_one(&mut *conn)
127 .await?
128 > 0
129 {
130 Ok(label)
131 } else {
132 let choices = near_labels(conn, workspace_id, &label).await?;
133 eprintln!("error unknown-label input={}", label);
134 for choice in choices {
135 eprintln!("choice {choice}");
136 }
137 eprintln!("hint \"create the label explicitly\"");
138 bail!("unknown label");
139 }
140}
141
142pub(crate) async fn resolve_labels_in_workspace(
143 conn: &mut SqliteConnection,
144 workspace_id: &WorkspaceId,
145 labels: &[String],
146) -> Result<Vec<String>> {
147 let mut resolved = Vec::with_capacity(labels.len());
148 for label in labels {
149 resolved.push(ensure_label_exists_in_workspace(conn, workspace_id, label).await?);
150 }
151 Ok(resolved)
152}
153
154pub(crate) async fn resolve_or_create_labels_in_workspace(
155 conn: &mut SqliteConnection,
156 workspace: &Workspace,
157 labels: &[String],
158) -> Result<LabelResolution> {
159 let mut resolution = LabelResolution {
160 names: Vec::with_capacity(labels.len()),
161 created: Vec::new(),
162 };
163 for label in labels {
164 let outcome: LabelOutcome =
165 crate::operations::create_label_operation(conn, workspace, label).await?;
166 if outcome.created {
167 let change_id = outcome
168 .change_id
169 .ok_or_else(|| anyhow::anyhow!("created label missing change id"))?;
170 resolution.created.push(CreatedLabel {
171 name: outcome.name.clone(),
172 change_id,
173 });
174 }
175 resolution.names.push(outcome.name);
176 }
177 Ok(resolution)
178}