1use crate::scenes::MemScene;
7use crate::types::{MemSceneId, MessageId};
8#[allow(unused_imports)]
9use zeph_db::sql;
10
11use zeph_db::ActiveDialect;
12
13use crate::error::MemoryError;
14
15use super::SqliteStore;
16
17impl SqliteStore {
18 pub async fn find_unscened_semantic_messages(
26 &self,
27 limit: usize,
28 ) -> Result<Vec<(MessageId, String)>, MemoryError> {
29 let limit_i64 = i64::try_from(limit).unwrap_or(i64::MAX);
30 let rows: Vec<(i64, String)> = zeph_db::query_as(sql!(
31 "SELECT m.id, m.content
32 FROM messages m
33 WHERE m.tier = 'semantic'
34 AND m.deleted_at IS NULL
35 AND m.id NOT IN (SELECT message_id FROM mem_scene_members)
36 ORDER BY m.id ASC
37 LIMIT ?"
38 ))
39 .bind(limit_i64)
40 .fetch_all(&self.pool)
41 .await?;
42
43 Ok(rows
44 .into_iter()
45 .map(|(id, content)| (MessageId(id), content))
46 .collect())
47 }
48
49 pub async fn insert_mem_scene(
57 &self,
58 label: &str,
59 profile: &str,
60 member_ids: &[MessageId],
61 ) -> Result<MemSceneId, MemoryError> {
62 let member_count = i64::try_from(member_ids.len()).unwrap_or(0);
63 let mut tx = self.pool.begin().await?;
64
65 let row: (i64,) = zeph_db::query_as(sql!(
66 "INSERT INTO mem_scenes (label, profile, member_count) VALUES (?, ?, ?) RETURNING id"
67 ))
68 .bind(label)
69 .bind(profile)
70 .bind(member_count)
71 .fetch_one(&mut *tx)
72 .await?;
73 let scene_id = row.0;
74
75 let member_sql = zeph_db::rewrite_placeholders(&format!(
76 "{} INTO mem_scene_members (scene_id, message_id) VALUES (?, ?){}",
77 <ActiveDialect as zeph_db::dialect::Dialect>::INSERT_IGNORE,
78 <ActiveDialect as zeph_db::dialect::Dialect>::CONFLICT_NOTHING,
79 ));
80 for &msg_id in member_ids {
81 zeph_db::query(sqlx::AssertSqlSafe(member_sql.as_str()))
82 .bind(scene_id)
83 .bind(msg_id.0)
84 .execute(&mut *tx)
85 .await?;
86 }
87
88 tx.commit().await?;
89 Ok(MemSceneId(scene_id))
90 }
91
92 pub async fn list_mem_scenes(&self) -> Result<Vec<MemScene>, MemoryError> {
98 let rows: Vec<(i64, String, String, i64, i64, i64)> = zeph_db::query_as(sql!(
99 "SELECT id, label, profile, member_count, created_at, updated_at \
100 FROM mem_scenes ORDER BY created_at DESC"
101 ))
102 .fetch_all(&self.pool)
103 .await?;
104
105 Ok(rows
106 .into_iter()
107 .map(
108 |(id, label, profile, member_count, created_at, updated_at)| MemScene {
109 id: MemSceneId(id),
110 label,
111 profile,
112 member_count: u32::try_from(member_count).unwrap_or(0),
113 created_at,
114 updated_at,
115 },
116 )
117 .collect())
118 }
119
120 pub async fn scene_member_ids(
126 &self,
127 scene_id: MemSceneId,
128 ) -> Result<Vec<MessageId>, MemoryError> {
129 let rows: Vec<(i64,)> = zeph_db::query_as(sql!(
130 "SELECT message_id FROM mem_scene_members WHERE scene_id = ?"
131 ))
132 .bind(scene_id.0)
133 .fetch_all(&self.pool)
134 .await?;
135
136 Ok(rows.into_iter().map(|(id,)| MessageId(id)).collect())
137 }
138
139 pub async fn reset_mem_scenes(&self) -> Result<u64, MemoryError> {
145 let result = zeph_db::query(sql!("DELETE FROM mem_scenes"))
146 .execute(&self.pool)
147 .await?;
148 Ok(result.rows_affected())
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 async fn make_store() -> SqliteStore {
157 SqliteStore::with_pool_size(":memory:", 1).await.unwrap()
161 }
162
163 async fn seed_messages(store: &SqliteStore, n: usize) -> Vec<MessageId> {
165 let cid = store.create_conversation().await.unwrap();
166 let mut ids = Vec::with_capacity(n);
167 for i in 0..n {
168 let id = store
169 .save_message(cid, "user", &format!("msg {i}"))
170 .await
171 .unwrap();
172 ids.push(id);
173 }
174 ids
175 }
176
177 #[tokio::test]
179 async fn insert_and_list_scene() {
180 let store = make_store().await;
181 let ids = seed_messages(&store, 2).await;
182
183 let scene_id = store
184 .insert_mem_scene("Rust Auth", "JWT tokens used for RS256.", &ids)
185 .await
186 .unwrap();
187 assert!(scene_id.0 > 0, "scene id must be positive");
188
189 let scenes = store.list_mem_scenes().await.unwrap();
190 assert_eq!(scenes.len(), 1);
191 assert_eq!(scenes[0].label, "Rust Auth");
192 assert_eq!(scenes[0].member_count, 2);
193 }
194
195 #[tokio::test]
197 async fn scene_member_ids_expansion() {
198 let store = make_store().await;
199 let ids = seed_messages(&store, 3).await;
200
201 let scene_id = store
202 .insert_mem_scene("Topic A", "Profile text.", &ids)
203 .await
204 .unwrap();
205
206 let members = store.scene_member_ids(scene_id).await.unwrap();
207 assert_eq!(members.len(), 3);
208 for id in &ids {
209 assert!(members.contains(id), "member {id} must be in expansion");
210 }
211 }
212
213 #[tokio::test]
215 async fn find_unscened_excludes_assigned_members() {
216 let store = make_store().await;
217 let ids = seed_messages(&store, 3).await;
218
219 for id in &ids {
221 zeph_db::query(sql!("UPDATE messages SET tier = 'semantic' WHERE id = ?"))
222 .bind(id.0)
223 .execute(store.pool())
224 .await
225 .unwrap();
226 }
227
228 let unscened = store.find_unscened_semantic_messages(100).await.unwrap();
230 assert_eq!(unscened.len(), 3);
231
232 store
234 .insert_mem_scene("Partial Scene", "Some profile", &ids[..2])
235 .await
236 .unwrap();
237
238 let unscened_after = store.find_unscened_semantic_messages(100).await.unwrap();
240 assert_eq!(unscened_after.len(), 1);
241 assert_eq!(unscened_after[0].0, ids[2]);
242 }
243
244 #[tokio::test]
246 async fn reset_scenes_clears_all() {
247 let store = make_store().await;
248 let ids1 = seed_messages(&store, 1).await;
249 let ids2 = seed_messages(&store, 1).await;
250
251 store
252 .insert_mem_scene("Scene 1", "Profile 1", &ids1)
253 .await
254 .unwrap();
255 store
256 .insert_mem_scene("Scene 2", "Profile 2", &ids2)
257 .await
258 .unwrap();
259
260 let deleted = store.reset_mem_scenes().await.unwrap();
261 assert_eq!(deleted, 2);
262
263 let scenes = store.list_mem_scenes().await.unwrap();
264 assert!(scenes.is_empty());
265 }
266
267 #[tokio::test]
271 async fn list_scenes_ordered_newest_first() {
272 let store = make_store().await;
273 let ids1 = seed_messages(&store, 1).await;
274 let ids2 = seed_messages(&store, 1).await;
275
276 zeph_db::query(
278 sql!("INSERT INTO mem_scenes (label, profile, member_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"),
279 )
280 .bind("First")
281 .bind("Profile first")
282 .bind(1i64)
283 .bind(1_000_000i64)
284 .bind(1_000_000i64)
285 .execute(store.pool())
286 .await
287 .unwrap();
288 let scene1_id: (i64,) = zeph_db::query_as(sql!("SELECT last_insert_rowid()"))
289 .fetch_one(store.pool())
290 .await
291 .unwrap();
292
293 zeph_db::query(
294 sql!("INSERT INTO mem_scenes (label, profile, member_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"),
295 )
296 .bind("Second")
297 .bind("Profile second")
298 .bind(1i64)
299 .bind(2_000_000i64)
300 .bind(2_000_000i64)
301 .execute(store.pool())
302 .await
303 .unwrap();
304 let scene2_id: (i64,) = zeph_db::query_as(sql!("SELECT last_insert_rowid()"))
305 .fetch_one(store.pool())
306 .await
307 .unwrap();
308
309 zeph_db::query(sql!(
311 "INSERT INTO mem_scene_members (scene_id, message_id) VALUES (?, ?)"
312 ))
313 .bind(scene1_id.0)
314 .bind(ids1[0].0)
315 .execute(store.pool())
316 .await
317 .unwrap();
318 zeph_db::query(sql!(
319 "INSERT INTO mem_scene_members (scene_id, message_id) VALUES (?, ?)"
320 ))
321 .bind(scene2_id.0)
322 .bind(ids2[0].0)
323 .execute(store.pool())
324 .await
325 .unwrap();
326
327 let scenes = store.list_mem_scenes().await.unwrap();
328 assert_eq!(scenes.len(), 2);
330 assert_eq!(scenes[0].label, "Second");
331 assert_eq!(scenes[1].label, "First");
332 }
333}