1use crate::ids::WorkspaceId;
2use anyhow::{Result, bail};
3use sqlx::SqliteConnection;
4
5use crate::db::Database;
6use crate::matching::is_near;
7use crate::projects::normalize_key;
8
9impl Database {
10 pub async fn list_labels(
11 &self,
12 workspace_id: &WorkspaceId,
13 search: Option<&str>,
14 ) -> Result<Vec<String>> {
15 let mut conn = self.acquire().await?;
16 list_labels_in_workspace(&mut conn, workspace_id, search).await
17 }
18
19 pub async fn resolve_labels(
20 &self,
21 workspace_id: &WorkspaceId,
22 labels: &[String],
23 ) -> Result<Vec<String>> {
24 let mut conn = self.acquire().await?;
25 resolve_labels_in_workspace(&mut conn, workspace_id, labels).await
26 }
27}
28
29pub fn normalize_label(input: &str) -> String {
30 normalize_key(input)
31}
32
33async fn near_labels(
34 conn: &mut SqliteConnection,
35 workspace_id: &WorkspaceId,
36 input: &str,
37) -> Result<Vec<String>> {
38 let needle = normalize_label(input);
39 Ok(list_labels_in_workspace(conn, workspace_id, None)
40 .await?
41 .into_iter()
42 .filter(|label| is_near(&needle, label))
43 .collect())
44}
45
46pub(crate) async fn list_labels_in_workspace(
47 conn: &mut SqliteConnection,
48 workspace_id: &WorkspaceId,
49 search: Option<&str>,
50) -> Result<Vec<String>> {
51 let search = search.map(normalize_label);
52 let labels = sqlx::query_scalar::<_, String>(
53 "SELECT name FROM labels WHERE workspace_id = ? ORDER BY name",
54 )
55 .bind(workspace_id)
56 .fetch_all(&mut *conn)
57 .await?;
58 Ok(labels
59 .into_iter()
60 .filter(|label| {
61 search
62 .as_deref()
63 .is_none_or(|search| label.contains(search))
64 })
65 .collect())
66}
67
68pub(crate) async fn ensure_label_exists_in_workspace(
69 conn: &mut SqliteConnection,
70 workspace_id: &WorkspaceId,
71 label: &str,
72) -> Result<String> {
73 let label = normalize_label(label);
74 if sqlx::query_scalar::<_, i64>(
75 "SELECT count(*) FROM labels WHERE workspace_id = ? AND name = ?",
76 )
77 .bind(workspace_id)
78 .bind(&label)
79 .fetch_one(&mut *conn)
80 .await?
81 > 0
82 {
83 Ok(label)
84 } else {
85 let choices = near_labels(conn, workspace_id, &label).await?;
86 eprintln!("error unknown-label input={}", label);
87 for choice in choices {
88 eprintln!("choice {choice}");
89 }
90 eprintln!("hint \"create the label explicitly\"");
91 bail!("unknown label");
92 }
93}
94
95pub(crate) async fn resolve_labels_in_workspace(
96 conn: &mut SqliteConnection,
97 workspace_id: &WorkspaceId,
98 labels: &[String],
99) -> Result<Vec<String>> {
100 let mut resolved = Vec::with_capacity(labels.len());
101 for label in labels {
102 resolved.push(ensure_label_exists_in_workspace(conn, workspace_id, label).await?);
103 }
104 Ok(resolved)
105}