Skip to main content

kernex_memory/store/
facts.rs

1//! User facts, cross-channel aliases, and limitations.
2
3use super::Store;
4use crate::error::MemoryError;
5use uuid::Uuid;
6
7impl Store {
8    /// Store a fact (upsert by sender_id + key). If the row was previously
9    /// soft-deleted, re-storing clears `deleted_at` so the value is visible
10    /// again to default-filtered reads.
11    pub async fn store_fact(
12        &self,
13        sender_id: &str,
14        key: &str,
15        value: &str,
16    ) -> Result<(), MemoryError> {
17        let id = Uuid::new_v4().to_string();
18        sqlx::query(
19            "INSERT INTO facts (id, sender_id, key, value) VALUES (?, ?, ?, ?) \
20             ON CONFLICT(sender_id, key) DO UPDATE SET \
21                value = excluded.value, \
22                updated_at = datetime('now'), \
23                deleted_at = NULL",
24        )
25        .bind(&id)
26        .bind(sender_id)
27        .bind(key)
28        .bind(value)
29        .execute(&self.pool)
30        .await
31        .map_err(|e| MemoryError::sqlite("upsert fact failed", e))?;
32
33        Ok(())
34    }
35
36    /// Get a single fact by sender and key. Returns `None` if the row is
37    /// soft-deleted.
38    pub async fn get_fact(
39        &self,
40        sender_id: &str,
41        key: &str,
42    ) -> Result<Option<String>, MemoryError> {
43        let row: Option<(String,)> = sqlx::query_as(
44            "SELECT value FROM facts \
45             WHERE sender_id = ? AND key = ? AND deleted_at IS NULL",
46        )
47        .bind(sender_id)
48        .bind(key)
49        .fetch_optional(&self.pool)
50        .await
51        .map_err(|e| MemoryError::sqlite("query failed", e))?;
52
53        Ok(row.map(|(v,)| v))
54    }
55
56    /// Hard-delete a single fact by sender and key. Returns `true` if a row
57    /// was deleted. Emergency cleanup only — not exposed on the
58    /// `MemoryStore` trait. Default consumer paths should call
59    /// [`Self::soft_delete_fact`].
60    pub async fn delete_fact(&self, sender_id: &str, key: &str) -> Result<bool, MemoryError> {
61        let result = sqlx::query("DELETE FROM facts WHERE sender_id = ? AND key = ?")
62            .bind(sender_id)
63            .bind(key)
64            .execute(&self.pool)
65            .await
66            .map_err(|e| MemoryError::sqlite("delete failed", e))?;
67
68        Ok(result.rows_affected() > 0)
69    }
70
71    /// Soft-delete a single fact by setting its `deleted_at` timestamp.
72    /// Returns `true` if a row transitioned from active to deleted.
73    pub async fn soft_delete_fact(&self, sender_id: &str, key: &str) -> Result<bool, MemoryError> {
74        let result = sqlx::query(
75            "UPDATE facts SET deleted_at = datetime('now') \
76             WHERE sender_id = ? AND key = ? AND deleted_at IS NULL",
77        )
78        .bind(sender_id)
79        .bind(key)
80        .execute(&self.pool)
81        .await
82        .map_err(|e| MemoryError::sqlite("soft delete failed", e))?;
83
84        Ok(result.rows_affected() > 0)
85    }
86
87    /// Get all active (not soft-deleted) facts for a sender.
88    pub async fn get_facts(&self, sender_id: &str) -> Result<Vec<(String, String)>, MemoryError> {
89        let rows: Vec<(String, String)> = sqlx::query_as(
90            "SELECT key, value FROM facts \
91             WHERE sender_id = ? AND deleted_at IS NULL ORDER BY key",
92        )
93        .bind(sender_id)
94        .fetch_all(&self.pool)
95        .await
96        .map_err(|e| MemoryError::sqlite("query failed", e))?;
97
98        Ok(rows)
99    }
100
101    /// Hard-delete facts for a sender — all if `key` is `None`, specific
102    /// fact if `key` is `Some`. Emergency cleanup only — not exposed on the
103    /// `MemoryStore` trait. Default consumer paths should call
104    /// [`Self::soft_delete_facts`].
105    pub async fn delete_facts(
106        &self,
107        sender_id: &str,
108        key: Option<&str>,
109    ) -> Result<u64, MemoryError> {
110        let result = if let Some(k) = key {
111            sqlx::query("DELETE FROM facts WHERE sender_id = ? AND key = ?")
112                .bind(sender_id)
113                .bind(k)
114                .execute(&self.pool)
115                .await
116        } else {
117            sqlx::query("DELETE FROM facts WHERE sender_id = ?")
118                .bind(sender_id)
119                .execute(&self.pool)
120                .await
121        };
122
123        result
124            .map(|r| r.rows_affected())
125            .map_err(|e| MemoryError::sqlite("delete failed", e))
126    }
127
128    /// Soft-delete facts for a sender — every active fact if `key` is
129    /// `None`, only the matching active fact if `key` is `Some`. Returns
130    /// the count of rows that transitioned from active to deleted.
131    pub async fn soft_delete_facts(
132        &self,
133        sender_id: &str,
134        key: Option<&str>,
135    ) -> Result<u64, MemoryError> {
136        let result = if let Some(k) = key {
137            sqlx::query(
138                "UPDATE facts SET deleted_at = datetime('now') \
139                 WHERE sender_id = ? AND key = ? AND deleted_at IS NULL",
140            )
141            .bind(sender_id)
142            .bind(k)
143            .execute(&self.pool)
144            .await
145        } else {
146            sqlx::query(
147                "UPDATE facts SET deleted_at = datetime('now') \
148                 WHERE sender_id = ? AND deleted_at IS NULL",
149            )
150            .bind(sender_id)
151            .execute(&self.pool)
152            .await
153        };
154
155        result
156            .map(|r| r.rows_affected())
157            .map_err(|e| MemoryError::sqlite("soft delete failed", e))
158    }
159
160    /// Read soft-deleted facts for a sender (debug / recovery helper).
161    /// Returns `(key, value, deleted_at)` rows ordered by deletion time
162    /// descending.
163    pub async fn list_soft_deleted_facts(
164        &self,
165        sender_id: &str,
166    ) -> Result<Vec<(String, String, String)>, MemoryError> {
167        let rows: Vec<(String, String, String)> = sqlx::query_as(
168            "SELECT key, value, deleted_at FROM facts \
169             WHERE sender_id = ? AND deleted_at IS NOT NULL \
170             ORDER BY deleted_at DESC",
171        )
172        .bind(sender_id)
173        .fetch_all(&self.pool)
174        .await
175        .map_err(|e| MemoryError::sqlite("query failed", e))?;
176
177        Ok(rows)
178    }
179
180    /// Get all active facts across all users (excluding the `welcomed`
181    /// marker key).
182    pub async fn get_all_facts(&self) -> Result<Vec<(String, String)>, MemoryError> {
183        let rows: Vec<(String, String)> = sqlx::query_as(
184            "SELECT key, value FROM facts \
185             WHERE key != 'welcomed' AND deleted_at IS NULL ORDER BY key",
186        )
187        .fetch_all(&self.pool)
188        .await
189        .map_err(|e| MemoryError::sqlite("query failed", e))?;
190
191        Ok(rows)
192    }
193
194    /// Get all active `(sender_id, value)` pairs for a given fact key
195    /// across all users.
196    pub async fn get_all_facts_by_key(
197        &self,
198        key: &str,
199    ) -> Result<Vec<(String, String)>, MemoryError> {
200        let rows: Vec<(String, String)> = sqlx::query_as(
201            "SELECT sender_id, value FROM facts \
202             WHERE key = ? AND deleted_at IS NULL ORDER BY sender_id",
203        )
204        .bind(key)
205        .fetch_all(&self.pool)
206        .await
207        .map_err(|e| MemoryError::sqlite("get facts by key failed", e))?;
208        Ok(rows)
209    }
210
211    /// Check if a sender has never been welcomed (no active `welcomed`
212    /// fact).
213    pub async fn is_new_user(&self, sender_id: &str) -> Result<bool, MemoryError> {
214        let row: Option<(String,)> = sqlx::query_as(
215            "SELECT value FROM facts \
216             WHERE sender_id = ? AND key = 'welcomed' AND deleted_at IS NULL",
217        )
218        .bind(sender_id)
219        .fetch_optional(&self.pool)
220        .await
221        .map_err(|e| MemoryError::sqlite("query failed", e))?;
222
223        Ok(row.is_none())
224    }
225
226    // --- Aliases ---
227
228    /// Resolve a sender_id to its canonical form via the user_aliases table.
229    pub async fn resolve_sender_id(&self, sender_id: &str) -> Result<String, MemoryError> {
230        let row: Option<(String,)> = sqlx::query_as(
231            "SELECT canonical_sender_id FROM user_aliases WHERE alias_sender_id = ?",
232        )
233        .bind(sender_id)
234        .fetch_optional(&self.pool)
235        .await
236        .map_err(|e| MemoryError::sqlite("resolve alias failed", e))?;
237
238        Ok(row.map(|(id,)| id).unwrap_or_else(|| sender_id.to_string()))
239    }
240
241    /// Create an alias mapping: alias_id → canonical_id.
242    pub async fn create_alias(
243        &self,
244        alias_id: &str,
245        canonical_id: &str,
246    ) -> Result<(), MemoryError> {
247        sqlx::query(
248            "INSERT OR IGNORE INTO user_aliases (alias_sender_id, canonical_sender_id) \
249             VALUES (?, ?)",
250        )
251        .bind(alias_id)
252        .bind(canonical_id)
253        .execute(&self.pool)
254        .await
255        .map_err(|e| MemoryError::sqlite("create alias failed", e))?;
256
257        Ok(())
258    }
259
260    /// Find an existing welcomed user different from `sender_id`. Skips
261    /// soft-deleted `welcomed` markers.
262    pub async fn find_canonical_user(
263        &self,
264        exclude_sender_id: &str,
265    ) -> Result<Option<String>, MemoryError> {
266        let row: Option<(String,)> = sqlx::query_as(
267            "SELECT sender_id FROM facts \
268             WHERE key = 'welcomed' AND sender_id != ? AND deleted_at IS NULL LIMIT 1",
269        )
270        .bind(exclude_sender_id)
271        .fetch_optional(&self.pool)
272        .await
273        .map_err(|e| MemoryError::sqlite("query failed", e))?;
274
275        Ok(row.map(|(id,)| id))
276    }
277
278    // --- Limitations ---
279
280    /// Store a limitation (deduplicates by title, case-insensitive).
281    pub async fn store_limitation(
282        &self,
283        title: &str,
284        description: &str,
285        proposed_plan: &str,
286    ) -> Result<bool, MemoryError> {
287        let id = Uuid::new_v4().to_string();
288        let result = sqlx::query(
289            "INSERT OR IGNORE INTO limitations (id, title, description, proposed_plan) \
290             VALUES (?, ?, ?, ?)",
291        )
292        .bind(&id)
293        .bind(title)
294        .bind(description)
295        .bind(proposed_plan)
296        .execute(&self.pool)
297        .await
298        .map_err(|e| MemoryError::sqlite("store limitation failed", e))?;
299
300        Ok(result.rows_affected() > 0)
301    }
302
303    /// Get all open limitations: (title, description, proposed_plan).
304    pub async fn get_open_limitations(&self) -> Result<Vec<(String, String, String)>, MemoryError> {
305        let rows: Vec<(String, String, String)> = sqlx::query_as(
306            "SELECT title, description, proposed_plan FROM limitations \
307             WHERE status = 'open' ORDER BY created_at ASC",
308        )
309        .fetch_all(&self.pool)
310        .await
311        .map_err(|e| MemoryError::sqlite("get open limitations failed", e))?;
312
313        Ok(rows)
314    }
315}