1use chrono::{DateTime, Utc};
2use uuid::Uuid;
3
4use crate::{Db, DbError};
5
6#[derive(Debug, Clone)]
7pub struct Session {
8 pub id: String,
9 pub title: String,
10 pub created_at: DateTime<Utc>,
11 pub updated_at: DateTime<Utc>,
12 pub metadata: Option<String>,
13 pub tags: Vec<String>,
14}
15
16impl Db {
17 pub fn create_session(&self, title: &str) -> Result<Session, DbError> {
18 let id = Uuid::new_v4().to_string();
19 let now = Utc::now();
20 let now_str = now.to_rfc3339();
21
22 self.conn.execute(
23 "INSERT INTO sessions (id, title, created_at, updated_at) VALUES (?1, ?2, ?3, ?4)",
24 rusqlite::params![id, title, now_str, now_str],
25 )?;
26
27 Ok(Session {
28 id,
29 title: title.to_string(),
30 created_at: now,
31 updated_at: now,
32 metadata: None,
33 tags: Vec::new(),
34 })
35 }
36
37 pub fn get_session(&self, id: &str) -> Result<Session, DbError> {
38 let mut stmt = self.conn.prepare(
39 "SELECT id, title, created_at, updated_at, metadata FROM sessions WHERE id = ?1",
40 )?;
41
42 let session = stmt
43 .query_row(rusqlite::params![id], |row| {
44 let created_str: String = row.get(2)?;
45 let updated_str: String = row.get(3)?;
46 Ok((
47 row.get::<_, String>(0)?,
48 row.get::<_, String>(1)?,
49 created_str,
50 updated_str,
51 row.get::<_, Option<String>>(4)?,
52 ))
53 })
54 .map_err(|e| match e {
55 rusqlite::Error::QueryReturnedNoRows => DbError::NotFound(format!("session {id}")),
56 other => DbError::Sqlite(other),
57 })?;
58
59 let tags = self.get_session_tags(&session.0)?;
60
61 Ok(Session {
62 id: session.0,
63 title: session.1,
64 created_at: DateTime::parse_from_rfc3339(&session.2)
65 .unwrap_or_default()
66 .with_timezone(&Utc),
67 updated_at: DateTime::parse_from_rfc3339(&session.3)
68 .unwrap_or_default()
69 .with_timezone(&Utc),
70 metadata: session.4,
71 tags,
72 })
73 }
74
75 pub fn list_sessions(&self) -> Result<Vec<Session>, DbError> {
76 let mut stmt = self.conn.prepare(
77 "SELECT id, title, created_at, updated_at, metadata FROM sessions ORDER BY updated_at DESC",
78 )?;
79
80 let rows = stmt.query_map([], |row| {
81 let created_str: String = row.get(2)?;
82 let updated_str: String = row.get(3)?;
83 Ok((
84 row.get::<_, String>(0)?,
85 row.get::<_, String>(1)?,
86 created_str,
87 updated_str,
88 row.get::<_, Option<String>>(4)?,
89 ))
90 })?;
91
92 let mut sessions = Vec::new();
93 for row in rows {
94 let r = row?;
95 let tags = self.get_session_tags(&r.0)?;
96 sessions.push(Session {
97 id: r.0,
98 title: r.1,
99 created_at: DateTime::parse_from_rfc3339(&r.2)
100 .unwrap_or_default()
101 .with_timezone(&Utc),
102 updated_at: DateTime::parse_from_rfc3339(&r.3)
103 .unwrap_or_default()
104 .with_timezone(&Utc),
105 metadata: r.4,
106 tags,
107 });
108 }
109
110 Ok(sessions)
111 }
112
113 pub fn update_session_title(&self, id: &str, title: &str) -> Result<(), DbError> {
114 let now = Utc::now().to_rfc3339();
115 let rows = self.conn.execute(
116 "UPDATE sessions SET title = ?1, updated_at = ?2 WHERE id = ?3",
117 rusqlite::params![title, now, id],
118 )?;
119 if rows == 0 {
120 return Err(DbError::NotFound(format!("session {id}")));
121 }
122 Ok(())
123 }
124
125 pub fn update_session_metadata(&self, id: &str, metadata: &str) -> Result<(), DbError> {
126 let now = Utc::now().to_rfc3339();
127 self.conn.execute(
128 "UPDATE sessions SET metadata = ?1, updated_at = ?2 WHERE id = ?3",
129 rusqlite::params![metadata, now, id],
130 )?;
131 Ok(())
132 }
133
134 pub fn list_sessions_with_preview(&self) -> Result<Vec<(Session, Option<String>)>, DbError> {
137 let mut stmt = self.conn.prepare(
138 "SELECT s.id, s.title, s.created_at, s.updated_at, s.metadata, \
139 (SELECT m.content FROM messages m WHERE m.session_id = s.id ORDER BY m.created_at DESC LIMIT 1) as last_message \
140 FROM sessions s ORDER BY s.updated_at DESC",
141 )?;
142
143 let rows = stmt.query_map([], |row| {
144 let created_str: String = row.get(2)?;
145 let updated_str: String = row.get(3)?;
146 Ok((
147 row.get::<_, String>(0)?,
148 row.get::<_, String>(1)?,
149 created_str,
150 updated_str,
151 row.get::<_, Option<String>>(4)?,
152 row.get::<_, Option<String>>(5)?,
153 ))
154 })?;
155
156 let mut results = Vec::new();
157 for row in rows {
158 let r = row?;
159 let tags = self.get_session_tags(&r.0)?;
160 let session = Session {
161 id: r.0,
162 title: r.1,
163 created_at: DateTime::parse_from_rfc3339(&r.2)
164 .unwrap_or_default()
165 .with_timezone(&Utc),
166 updated_at: DateTime::parse_from_rfc3339(&r.3)
167 .unwrap_or_default()
168 .with_timezone(&Utc),
169 metadata: r.4,
170 tags,
171 };
172 results.push((session, r.5));
173 }
174
175 Ok(results)
176 }
177
178 pub fn delete_session(&self, id: &str) -> Result<(), DbError> {
179 let rows = self
180 .conn
181 .execute("DELETE FROM sessions WHERE id = ?1", rusqlite::params![id])?;
182 if rows == 0 {
183 return Err(DbError::NotFound(format!("session {id}")));
184 }
185 Ok(())
186 }
187
188 pub fn add_session_tag(&self, session_id: &str, tag: &str) -> Result<(), DbError> {
189 self.conn.execute(
190 "INSERT OR IGNORE INTO session_tags (session_id, tag) VALUES (?1, ?2)",
191 rusqlite::params![session_id, tag],
192 )?;
193 Ok(())
194 }
195
196 pub fn remove_session_tag(&self, session_id: &str, tag: &str) -> Result<(), DbError> {
197 self.conn.execute(
198 "DELETE FROM session_tags WHERE session_id = ?1 AND tag = ?2",
199 rusqlite::params![session_id, tag],
200 )?;
201 Ok(())
202 }
203
204 fn get_session_tags(&self, session_id: &str) -> Result<Vec<String>, DbError> {
205 let mut stmt = self
206 .conn
207 .prepare("SELECT tag FROM session_tags WHERE session_id = ?1 ORDER BY tag")?;
208 let tags = stmt
209 .query_map(rusqlite::params![session_id], |row| row.get(0))?
210 .collect::<Result<Vec<String>, _>>()?;
211 Ok(tags)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use crate::Db;
218
219 #[test]
220 fn test_create_and_get_session() {
221 let db = Db::open_in_memory().unwrap();
222 let session = db.create_session("Test Session").unwrap();
223 assert_eq!(session.title, "Test Session");
224
225 let fetched = db.get_session(&session.id).unwrap();
226 assert_eq!(fetched.id, session.id);
227 assert_eq!(fetched.title, "Test Session");
228 }
229
230 #[test]
231 fn test_list_sessions_ordered_by_updated() {
232 let db = Db::open_in_memory().unwrap();
233 let s1 = db.create_session("First").unwrap();
234 let _s2 = db.create_session("Second").unwrap();
235
236 db.update_session_title(&s1.id, "First Updated").unwrap();
238
239 let sessions = db.list_sessions().unwrap();
240 assert_eq!(sessions.len(), 2);
241 assert_eq!(sessions[0].title, "First Updated");
242 assert_eq!(sessions[1].title, "Second");
243 }
244
245 #[test]
246 fn test_session_tags() {
247 let db = Db::open_in_memory().unwrap();
248 let session = db.create_session("Tagged").unwrap();
249
250 db.add_session_tag(&session.id, "rust").unwrap();
251 db.add_session_tag(&session.id, "coding").unwrap();
252
253 db.add_session_tag(&session.id, "rust").unwrap();
255
256 let fetched = db.get_session(&session.id).unwrap();
257 assert_eq!(fetched.tags.len(), 2);
258 assert!(fetched.tags.contains(&"rust".to_string()));
259 assert!(fetched.tags.contains(&"coding".to_string()));
260
261 db.remove_session_tag(&session.id, "rust").unwrap();
263 let fetched = db.get_session(&session.id).unwrap();
264 assert_eq!(fetched.tags.len(), 1);
265 assert!(fetched.tags.contains(&"coding".to_string()));
266 }
267
268 #[test]
269 fn test_delete_session() {
270 let db = Db::open_in_memory().unwrap();
271 let session = db.create_session("To Delete").unwrap();
272 db.delete_session(&session.id).unwrap();
273
274 let result = db.get_session(&session.id);
275 assert!(result.is_err());
276 }
277
278 #[test]
279 fn test_delete_nonexistent_session() {
280 let db = Db::open_in_memory().unwrap();
281 let result = db.delete_session("nonexistent-id");
282 assert!(result.is_err());
283 }
284}