Skip to main content

mxr_store/
label.rs

1use mxr_core::id::*;
2use mxr_core::types::*;
3
4impl super::Store {
5    pub async fn upsert_label(&self, label: &Label) -> Result<(), sqlx::Error> {
6        let id = label.id.as_str();
7        let account_id = label.account_id.as_str();
8        let kind = match label.kind {
9            LabelKind::System => "system",
10            LabelKind::Folder => "folder",
11            LabelKind::User => "user",
12        };
13        let unread_count = label.unread_count as i64;
14        let total_count = label.total_count as i64;
15
16        // On conflict, do NOT overwrite counts — those are managed by
17        // recalculate_label_counts() from the junction table. Only update
18        // metadata (name, kind, color, provider_id).
19        sqlx::query!(
20            "INSERT INTO labels (id, account_id, name, kind, color, provider_id, unread_count, total_count)
21             VALUES (?, ?, ?, ?, ?, ?, ?, ?)
22             ON CONFLICT(id) DO UPDATE SET
23                name = excluded.name,
24                kind = excluded.kind,
25                color = excluded.color,
26                provider_id = excluded.provider_id",
27            id,
28            account_id,
29            label.name,
30            kind,
31            label.color,
32            label.provider_id,
33            unread_count,
34            total_count,
35        )
36        .execute(self.writer())
37        .await?;
38
39        Ok(())
40    }
41
42    pub async fn list_labels_by_account(
43        &self,
44        account_id: &AccountId,
45    ) -> Result<Vec<Label>, sqlx::Error> {
46        let aid = account_id.as_str();
47        let rows = sqlx::query!(
48            r#"SELECT id as "id!", account_id as "account_id!", name as "name!",
49                      kind as "kind!", color, provider_id as "provider_id!",
50                      unread_count as "unread_count!", total_count as "total_count!"
51               FROM labels WHERE account_id = ?"#,
52            aid,
53        )
54        .fetch_all(self.reader())
55        .await?;
56
57        Ok(rows
58            .into_iter()
59            .map(|r| Label {
60                id: LabelId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap()),
61                account_id: AccountId::from_uuid(uuid::Uuid::parse_str(&r.account_id).unwrap()),
62                name: r.name,
63                kind: match r.kind.as_str() {
64                    "system" => LabelKind::System,
65                    "folder" => LabelKind::Folder,
66                    _ => LabelKind::User,
67                },
68                color: r.color,
69                provider_id: r.provider_id,
70                unread_count: r.unread_count as u32,
71                total_count: r.total_count as u32,
72            })
73            .collect())
74    }
75
76    pub async fn update_label_counts(
77        &self,
78        label_id: &LabelId,
79        unread_count: u32,
80        total_count: u32,
81    ) -> Result<(), sqlx::Error> {
82        let lid = label_id.as_str();
83        let unread = unread_count as i64;
84        let total = total_count as i64;
85        sqlx::query!(
86            "UPDATE labels SET unread_count = ?, total_count = ? WHERE id = ?",
87            unread,
88            total,
89            lid,
90        )
91        .execute(self.writer())
92        .await?;
93        Ok(())
94    }
95
96    pub async fn delete_label(&self, label_id: &LabelId) -> Result<(), sqlx::Error> {
97        sqlx::query("DELETE FROM labels WHERE id = ?")
98            .bind(label_id.as_str())
99            .execute(self.writer())
100            .await?;
101        Ok(())
102    }
103
104    pub async fn replace_label(
105        &self,
106        old_label_id: &LabelId,
107        new_label: &Label,
108    ) -> Result<(), sqlx::Error> {
109        let mut tx = self.writer().begin().await?;
110
111        let existing: Option<(i64, i64)> =
112            sqlx::query_as("SELECT unread_count, total_count FROM labels WHERE id = ?")
113                .bind(old_label_id.as_str())
114                .fetch_optional(&mut *tx)
115                .await?;
116
117        let (unread_count, total_count) = existing.unwrap_or((0, 0));
118        let kind = match new_label.kind {
119            LabelKind::System => "system",
120            LabelKind::Folder => "folder",
121            LabelKind::User => "user",
122        };
123
124        sqlx::query(
125            "INSERT INTO labels (id, account_id, name, kind, color, provider_id, unread_count, total_count)
126             VALUES (?, ?, ?, ?, ?, ?, ?, ?)
127             ON CONFLICT(id) DO UPDATE SET
128                account_id = excluded.account_id,
129                name = excluded.name,
130                kind = excluded.kind,
131                color = excluded.color,
132                provider_id = excluded.provider_id,
133                unread_count = excluded.unread_count,
134                total_count = excluded.total_count",
135        )
136        .bind(new_label.id.as_str())
137        .bind(new_label.account_id.as_str())
138        .bind(&new_label.name)
139        .bind(kind)
140        .bind(&new_label.color)
141        .bind(&new_label.provider_id)
142        .bind(unread_count)
143        .bind(total_count)
144        .execute(&mut *tx)
145        .await?;
146
147        if old_label_id != &new_label.id {
148            sqlx::query("UPDATE message_labels SET label_id = ? WHERE label_id = ?")
149                .bind(new_label.id.as_str())
150                .bind(old_label_id.as_str())
151                .execute(&mut *tx)
152                .await?;
153
154            sqlx::query("DELETE FROM labels WHERE id = ?")
155                .bind(old_label_id.as_str())
156                .execute(&mut *tx)
157                .await?;
158        }
159
160        tx.commit().await?;
161        Ok(())
162    }
163
164    pub async fn recalculate_label_counts(
165        &self,
166        account_id: &AccountId,
167    ) -> Result<(), sqlx::Error> {
168        let aid = account_id.as_str();
169        sqlx::query!(
170            "UPDATE labels SET
171                total_count = (SELECT COUNT(*) FROM message_labels WHERE label_id = labels.id),
172                unread_count = (SELECT COUNT(*) FROM message_labels ml
173                    JOIN messages m ON m.id = ml.message_id
174                    WHERE ml.label_id = labels.id AND (m.flags & 1) = 0)
175            WHERE account_id = ?",
176            aid,
177        )
178        .execute(self.writer())
179        .await?;
180        Ok(())
181    }
182
183    /// Look up LabelIds from a list of provider_ids (e.g. ["INBOX", "SENT"]).
184    /// Returns only the IDs for labels that exist in the store.
185    pub async fn find_labels_by_provider_ids(
186        &self,
187        account_id: &AccountId,
188        provider_ids: &[String],
189    ) -> Result<Vec<LabelId>, sqlx::Error> {
190        if provider_ids.is_empty() {
191            return Ok(vec![]);
192        }
193        let aid = account_id.as_str();
194        let placeholders: Vec<String> = provider_ids.iter().map(|_| "?".to_string()).collect();
195        let sql = format!(
196            "SELECT id FROM labels WHERE account_id = ? AND provider_id IN ({})",
197            placeholders.join(", ")
198        );
199        let mut query = sqlx::query_scalar::<_, String>(&sql).bind(aid);
200        for pid in provider_ids {
201            query = query.bind(pid);
202        }
203        let rows = query.fetch_all(self.reader()).await?;
204        Ok(rows
205            .into_iter()
206            .map(|id| LabelId::from_uuid(uuid::Uuid::parse_str(&id).unwrap()))
207            .collect())
208    }
209
210    pub async fn find_label_by_provider_id(
211        &self,
212        account_id: &AccountId,
213        provider_id: &str,
214    ) -> Result<Option<Label>, sqlx::Error> {
215        let aid = account_id.as_str();
216        let row = sqlx::query!(
217            r#"SELECT id as "id!", account_id as "account_id!", name as "name!",
218                      kind as "kind!", color, provider_id as "provider_id!",
219                      unread_count as "unread_count!", total_count as "total_count!"
220               FROM labels WHERE account_id = ? AND provider_id = ?"#,
221            aid,
222            provider_id,
223        )
224        .fetch_optional(self.reader())
225        .await?;
226
227        Ok(row.map(|r| Label {
228            id: LabelId::from_uuid(uuid::Uuid::parse_str(&r.id).unwrap()),
229            account_id: AccountId::from_uuid(uuid::Uuid::parse_str(&r.account_id).unwrap()),
230            name: r.name,
231            kind: match r.kind.as_str() {
232                "system" => LabelKind::System,
233                "folder" => LabelKind::Folder,
234                _ => LabelKind::User,
235            },
236            color: r.color,
237            provider_id: r.provider_id,
238            unread_count: r.unread_count as u32,
239            total_count: r.total_count as u32,
240        }))
241    }
242}