1use rusqlite::Connection;
2use std::path::Path;
3
4use crate::{Result, queries, records::*, schema};
5
6pub struct Database {
7 conn: Connection,
8}
9
10impl Database {
11 pub fn open(db_path: &Path) -> Result<Self> {
12 let conn = Connection::open(db_path)?;
13 let db = Self { conn };
14 schema::init_schema(&db.conn)?;
15 Ok(db)
16 }
17
18 pub fn open_in_memory() -> Result<Self> {
19 let conn = Connection::open_in_memory()?;
20 let db = Self { conn };
21 schema::init_schema(&db.conn)?;
22 Ok(db)
23 }
24
25 pub fn insert_or_update_project(&self, project: &ProjectRecord) -> Result<()> {
27 queries::project::insert_or_update(&self.conn, project)
28 }
29
30 pub fn get_project(&self, hash: &str) -> Result<Option<ProjectRecord>> {
31 queries::project::get(&self.conn, hash)
32 }
33
34 pub fn list_projects(&self) -> Result<Vec<ProjectRecord>> {
35 queries::project::list(&self.conn)
36 }
37
38 pub fn count_sessions_for_project(&self, project_hash: &str) -> Result<usize> {
39 queries::project::count_sessions(&self.conn, project_hash)
40 }
41
42 pub fn insert_or_update_session(&self, session: &SessionRecord) -> Result<()> {
44 queries::session::insert_or_update(&self.conn, session)
45 }
46
47 pub fn get_session_by_id(&self, session_id: &str) -> Result<Option<SessionSummary>> {
48 queries::session::get_by_id(&self.conn, session_id)
49 }
50
51 pub fn list_sessions(
52 &self,
53 project_hash: Option<&agtrace_types::ProjectHash>,
54 provider: Option<&str>,
55 limit: Option<usize>,
56 ) -> Result<Vec<SessionSummary>> {
57 queries::session::list(&self.conn, project_hash, provider, limit)
58 }
59
60 pub fn find_session_by_prefix(&self, prefix: &str) -> Result<Option<String>> {
61 queries::session::find_by_prefix(&self.conn, prefix)
62 }
63
64 pub fn insert_or_update_log_file(&self, log_file: &LogFileRecord) -> Result<()> {
66 queries::log_file::insert_or_update(&self.conn, log_file)
67 }
68
69 pub fn get_session_files(&self, session_id: &str) -> Result<Vec<LogFileRecord>> {
70 queries::log_file::get_session_files(&self.conn, session_id)
71 }
72
73 pub fn get_all_log_files(&self) -> Result<Vec<LogFileRecord>> {
74 queries::log_file::get_all(&self.conn)
75 }
76
77 pub fn vacuum(&self) -> Result<()> {
79 self.conn.execute("VACUUM", [])?;
80 println!("Database vacuumed successfully");
81 Ok(())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::schema::SCHEMA_VERSION;
89 use rusqlite::params;
90
91 #[test]
92 fn test_schema_initialization() {
93 let db = Database::open_in_memory().unwrap();
94
95 let projects = db.list_projects().unwrap();
96 assert_eq!(projects.len(), 0);
97 }
98
99 #[test]
100 fn test_insert_project() {
101 let db = Database::open_in_memory().unwrap();
102
103 let project = ProjectRecord {
104 hash: agtrace_types::ProjectHash::from("abc123"),
105 root_path: Some("/path/to/project".to_string()),
106 last_scanned_at: Some("2025-12-10T10:00:00Z".to_string()),
107 };
108
109 db.insert_or_update_project(&project).unwrap();
110
111 let retrieved = db.get_project("abc123").unwrap().unwrap();
112 assert_eq!(retrieved.hash, agtrace_types::ProjectHash::from("abc123"));
113 assert_eq!(retrieved.root_path, Some("/path/to/project".to_string()));
114 }
115
116 #[test]
117 fn test_insert_session_with_fk() {
118 let db = Database::open_in_memory().unwrap();
119
120 let project = ProjectRecord {
121 hash: agtrace_types::ProjectHash::from("abc123"),
122 root_path: Some("/path/to/project".to_string()),
123 last_scanned_at: Some("2025-12-10T10:00:00Z".to_string()),
124 };
125 db.insert_or_update_project(&project).unwrap();
126
127 let session = SessionRecord {
128 id: "session-001".to_string(),
129 project_hash: agtrace_types::ProjectHash::from("abc123"),
130 provider: "claude".to_string(),
131 start_ts: Some("2025-12-10T10:05:00Z".to_string()),
132 end_ts: Some("2025-12-10T10:15:00Z".to_string()),
133 snippet: Some("Test session".to_string()),
134 is_valid: true,
135 };
136
137 db.insert_or_update_session(&session).unwrap();
138
139 let sessions = db
140 .list_sessions(
141 Some(&agtrace_types::ProjectHash::from("abc123")),
142 None,
143 Some(10),
144 )
145 .unwrap();
146 assert_eq!(sessions.len(), 1);
147 assert_eq!(sessions[0].id, "session-001");
148 assert_eq!(sessions[0].provider, "claude");
149 }
150
151 #[test]
152 fn test_insert_log_file() {
153 let db = Database::open_in_memory().unwrap();
154
155 let project = ProjectRecord {
156 hash: agtrace_types::ProjectHash::from("abc123"),
157 root_path: Some("/path/to/project".to_string()),
158 last_scanned_at: Some("2025-12-10T10:00:00Z".to_string()),
159 };
160 db.insert_or_update_project(&project).unwrap();
161
162 let session = SessionRecord {
163 id: "session-001".to_string(),
164 project_hash: agtrace_types::ProjectHash::from("abc123"),
165 provider: "claude".to_string(),
166 start_ts: Some("2025-12-10T10:05:00Z".to_string()),
167 end_ts: None,
168 snippet: None,
169 is_valid: true,
170 };
171 db.insert_or_update_session(&session).unwrap();
172
173 let log_file = LogFileRecord {
174 path: "/path/to/log.jsonl".to_string(),
175 session_id: "session-001".to_string(),
176 role: "main".to_string(),
177 file_size: Some(1024),
178 mod_time: Some("2025-12-10T10:05:00Z".to_string()),
179 };
180
181 db.insert_or_update_log_file(&log_file).unwrap();
182
183 let files = db.get_session_files("session-001").unwrap();
184 assert_eq!(files.len(), 1);
185 assert_eq!(files[0].path, "/path/to/log.jsonl");
186 assert_eq!(files[0].role, "main");
187 }
188
189 #[test]
190 fn test_list_sessions_query() {
191 let db = Database::open_in_memory().unwrap();
192
193 let project = ProjectRecord {
194 hash: agtrace_types::ProjectHash::from("abc123"),
195 root_path: Some("/path/to/project".to_string()),
196 last_scanned_at: Some("2025-12-10T10:00:00Z".to_string()),
197 };
198 db.insert_or_update_project(&project).unwrap();
199
200 for i in 1..=5 {
201 let session = SessionRecord {
202 id: format!("session-{:03}", i),
203 project_hash: agtrace_types::ProjectHash::from("abc123"),
204 provider: "claude".to_string(),
205 start_ts: Some(format!("2025-12-10T10:{:02}:00Z", i)),
206 end_ts: None,
207 snippet: Some(format!("Session {}", i)),
208 is_valid: true,
209 };
210 db.insert_or_update_session(&session).unwrap();
211 }
212
213 let sessions = db
214 .list_sessions(
215 Some(&agtrace_types::ProjectHash::from("abc123")),
216 None,
217 Some(10),
218 )
219 .unwrap();
220 assert_eq!(sessions.len(), 5);
221
222 let sessions_limited = db
223 .list_sessions(
224 Some(&agtrace_types::ProjectHash::from("abc123")),
225 None,
226 Some(3),
227 )
228 .unwrap();
229 assert_eq!(sessions_limited.len(), 3);
230 }
231
232 #[test]
233 fn test_count_sessions_for_project() {
234 let db = Database::open_in_memory().unwrap();
235
236 let project = ProjectRecord {
237 hash: agtrace_types::ProjectHash::from("abc123"),
238 root_path: Some("/path/to/project".to_string()),
239 last_scanned_at: Some("2025-12-10T10:00:00Z".to_string()),
240 };
241 db.insert_or_update_project(&project).unwrap();
242
243 for i in 1..=3 {
244 let session = SessionRecord {
245 id: format!("session-{:03}", i),
246 project_hash: agtrace_types::ProjectHash::from("abc123"),
247 provider: "claude".to_string(),
248 start_ts: Some(format!("2025-12-10T10:{:02}:00Z", i)),
249 end_ts: None,
250 snippet: None,
251 is_valid: true,
252 };
253 db.insert_or_update_session(&session).unwrap();
254 }
255
256 let count = db.count_sessions_for_project("abc123").unwrap();
257 assert_eq!(count, 3);
258 }
259
260 #[test]
261 fn test_schema_version_set_on_init() {
262 let db = Database::open_in_memory().unwrap();
263
264 let version: i32 = db
265 .conn
266 .query_row("PRAGMA user_version", [], |row| row.get(0))
267 .unwrap();
268
269 assert_eq!(version, SCHEMA_VERSION);
270 }
271
272 #[test]
273 fn test_schema_rebuild_on_version_mismatch() {
274 let conn = Connection::open_in_memory().unwrap();
275
276 conn.execute_batch(
277 r#"
278 CREATE TABLE projects (hash TEXT PRIMARY KEY);
279 CREATE TABLE sessions (id TEXT PRIMARY KEY);
280 PRAGMA user_version = 999;
281 "#,
282 )
283 .unwrap();
284
285 conn.execute(
286 "INSERT INTO projects (hash) VALUES (?1)",
287 params!["old_data"],
288 )
289 .unwrap();
290
291 let db = Database { conn };
292 schema::init_schema(&db.conn).unwrap();
293
294 let version: i32 = db
295 .conn
296 .query_row("PRAGMA user_version", [], |row| row.get(0))
297 .unwrap();
298 assert_eq!(version, SCHEMA_VERSION);
299
300 let count: i64 = db
301 .conn
302 .query_row("SELECT COUNT(*) FROM projects", [], |row| row.get(0))
303 .unwrap();
304 assert_eq!(count, 0);
305 }
306
307 #[test]
308 fn test_schema_preserved_on_version_match() {
309 let db = Database::open_in_memory().unwrap();
310
311 let project = ProjectRecord {
312 hash: agtrace_types::ProjectHash::from("abc123"),
313 root_path: Some("/path/to/project".to_string()),
314 last_scanned_at: Some("2025-12-10T10:00:00Z".to_string()),
315 };
316 db.insert_or_update_project(&project).unwrap();
317
318 schema::init_schema(&db.conn).unwrap();
319
320 let retrieved = db.get_project("abc123").unwrap();
321 assert!(retrieved.is_some());
322 assert_eq!(
323 retrieved.unwrap().hash,
324 agtrace_types::ProjectHash::from("abc123")
325 );
326 }
327}