Skip to main content

kernex_memory/store/
facts.rs

1//! User facts, cross-channel aliases, and limitations.
2
3use super::Store;
4use kernex_core::error::KernexError;
5use uuid::Uuid;
6
7impl Store {
8    /// Store a fact (upsert by sender_id + key).
9    pub async fn store_fact(
10        &self,
11        sender_id: &str,
12        key: &str,
13        value: &str,
14    ) -> Result<(), KernexError> {
15        let id = Uuid::new_v4().to_string();
16        sqlx::query(
17            "INSERT INTO facts (id, sender_id, key, value) VALUES (?, ?, ?, ?) \
18             ON CONFLICT(sender_id, key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')",
19        )
20        .bind(&id)
21        .bind(sender_id)
22        .bind(key)
23        .bind(value)
24        .execute(&self.pool)
25        .await
26        .map_err(|e| KernexError::Store(format!("upsert fact failed: {e}")))?;
27
28        Ok(())
29    }
30
31    /// Get a single fact by sender and key.
32    pub async fn get_fact(
33        &self,
34        sender_id: &str,
35        key: &str,
36    ) -> Result<Option<String>, KernexError> {
37        let row: Option<(String,)> =
38            sqlx::query_as("SELECT value FROM facts WHERE sender_id = ? AND key = ?")
39                .bind(sender_id)
40                .bind(key)
41                .fetch_optional(&self.pool)
42                .await
43                .map_err(|e| KernexError::Store(format!("query failed: {e}")))?;
44
45        Ok(row.map(|(v,)| v))
46    }
47
48    /// Delete a single fact by sender and key. Returns `true` if a row was deleted.
49    pub async fn delete_fact(&self, sender_id: &str, key: &str) -> Result<bool, KernexError> {
50        let result = sqlx::query("DELETE FROM facts WHERE sender_id = ? AND key = ?")
51            .bind(sender_id)
52            .bind(key)
53            .execute(&self.pool)
54            .await
55            .map_err(|e| KernexError::Store(format!("delete failed: {e}")))?;
56
57        Ok(result.rows_affected() > 0)
58    }
59
60    /// Get all facts for a sender.
61    pub async fn get_facts(&self, sender_id: &str) -> Result<Vec<(String, String)>, KernexError> {
62        let rows: Vec<(String, String)> =
63            sqlx::query_as("SELECT key, value FROM facts WHERE sender_id = ? ORDER BY key")
64                .bind(sender_id)
65                .fetch_all(&self.pool)
66                .await
67                .map_err(|e| KernexError::Store(format!("query failed: {e}")))?;
68
69        Ok(rows)
70    }
71
72    /// Delete facts for a sender — all facts if key is None, specific fact if key is Some.
73    pub async fn delete_facts(
74        &self,
75        sender_id: &str,
76        key: Option<&str>,
77    ) -> Result<u64, KernexError> {
78        let result = if let Some(k) = key {
79            sqlx::query("DELETE FROM facts WHERE sender_id = ? AND key = ?")
80                .bind(sender_id)
81                .bind(k)
82                .execute(&self.pool)
83                .await
84        } else {
85            sqlx::query("DELETE FROM facts WHERE sender_id = ?")
86                .bind(sender_id)
87                .execute(&self.pool)
88                .await
89        };
90
91        result
92            .map(|r| r.rows_affected())
93            .map_err(|e| KernexError::Store(format!("delete failed: {e}")))
94    }
95
96    /// Get all facts across all users.
97    pub async fn get_all_facts(&self) -> Result<Vec<(String, String)>, KernexError> {
98        let rows: Vec<(String, String)> =
99            sqlx::query_as("SELECT key, value FROM facts WHERE key != 'welcomed' ORDER BY key")
100                .fetch_all(&self.pool)
101                .await
102                .map_err(|e| KernexError::Store(format!("query failed: {e}")))?;
103
104        Ok(rows)
105    }
106
107    /// Get all `(sender_id, value)` pairs for a given fact key across all users.
108    pub async fn get_all_facts_by_key(
109        &self,
110        key: &str,
111    ) -> Result<Vec<(String, String)>, KernexError> {
112        let rows: Vec<(String, String)> =
113            sqlx::query_as("SELECT sender_id, value FROM facts WHERE key = ? ORDER BY sender_id")
114                .bind(key)
115                .fetch_all(&self.pool)
116                .await
117                .map_err(|e| KernexError::Store(format!("get facts by key failed: {e}")))?;
118        Ok(rows)
119    }
120
121    /// Check if a sender has never been welcomed (no `welcomed` fact).
122    pub async fn is_new_user(&self, sender_id: &str) -> Result<bool, KernexError> {
123        let row: Option<(String,)> =
124            sqlx::query_as("SELECT value FROM facts WHERE sender_id = ? AND key = 'welcomed'")
125                .bind(sender_id)
126                .fetch_optional(&self.pool)
127                .await
128                .map_err(|e| KernexError::Store(format!("query failed: {e}")))?;
129
130        Ok(row.is_none())
131    }
132
133    // --- Aliases ---
134
135    /// Resolve a sender_id to its canonical form via the user_aliases table.
136    pub async fn resolve_sender_id(&self, sender_id: &str) -> Result<String, KernexError> {
137        let row: Option<(String,)> = sqlx::query_as(
138            "SELECT canonical_sender_id FROM user_aliases WHERE alias_sender_id = ?",
139        )
140        .bind(sender_id)
141        .fetch_optional(&self.pool)
142        .await
143        .map_err(|e| KernexError::Store(format!("resolve alias failed: {e}")))?;
144
145        Ok(row.map(|(id,)| id).unwrap_or_else(|| sender_id.to_string()))
146    }
147
148    /// Create an alias mapping: alias_id → canonical_id.
149    pub async fn create_alias(
150        &self,
151        alias_id: &str,
152        canonical_id: &str,
153    ) -> Result<(), KernexError> {
154        sqlx::query(
155            "INSERT OR IGNORE INTO user_aliases (alias_sender_id, canonical_sender_id) \
156             VALUES (?, ?)",
157        )
158        .bind(alias_id)
159        .bind(canonical_id)
160        .execute(&self.pool)
161        .await
162        .map_err(|e| KernexError::Store(format!("create alias failed: {e}")))?;
163
164        Ok(())
165    }
166
167    /// Find an existing welcomed user different from `sender_id`.
168    pub async fn find_canonical_user(
169        &self,
170        exclude_sender_id: &str,
171    ) -> Result<Option<String>, KernexError> {
172        let row: Option<(String,)> = sqlx::query_as(
173            "SELECT sender_id FROM facts WHERE key = 'welcomed' AND sender_id != ? LIMIT 1",
174        )
175        .bind(exclude_sender_id)
176        .fetch_optional(&self.pool)
177        .await
178        .map_err(|e| KernexError::Store(format!("query failed: {e}")))?;
179
180        Ok(row.map(|(id,)| id))
181    }
182
183    // --- Limitations ---
184
185    /// Store a limitation (deduplicates by title, case-insensitive).
186    pub async fn store_limitation(
187        &self,
188        title: &str,
189        description: &str,
190        proposed_plan: &str,
191    ) -> Result<bool, KernexError> {
192        let id = Uuid::new_v4().to_string();
193        let result = sqlx::query(
194            "INSERT OR IGNORE INTO limitations (id, title, description, proposed_plan) \
195             VALUES (?, ?, ?, ?)",
196        )
197        .bind(&id)
198        .bind(title)
199        .bind(description)
200        .bind(proposed_plan)
201        .execute(&self.pool)
202        .await
203        .map_err(|e| KernexError::Store(format!("store limitation failed: {e}")))?;
204
205        Ok(result.rows_affected() > 0)
206    }
207
208    /// Get all open limitations: (title, description, proposed_plan).
209    pub async fn get_open_limitations(&self) -> Result<Vec<(String, String, String)>, KernexError> {
210        let rows: Vec<(String, String, String)> = sqlx::query_as(
211            "SELECT title, description, proposed_plan FROM limitations \
212             WHERE status = 'open' ORDER BY created_at ASC",
213        )
214        .fetch_all(&self.pool)
215        .await
216        .map_err(|e| KernexError::Store(format!("get open limitations failed: {e}")))?;
217
218        Ok(rows)
219    }
220}