1pub mod messages;
2pub mod search;
3pub mod sessions;
4
5use rusqlite::Connection;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum DbError {
10 #[error("sqlite error: {0}")]
11 Sqlite(#[from] rusqlite::Error),
12 #[error("migration error: {0}")]
13 Migration(String),
14 #[error("not found: {0}")]
15 NotFound(String),
16}
17
18pub struct Db {
19 conn: Connection,
20}
21
22impl Db {
23 pub fn open(path: &str) -> Result<Self, DbError> {
24 let conn = Connection::open(path)?;
25 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
26 let db = Self { conn };
27 db.migrate()?;
28 Ok(db)
29 }
30
31 pub fn open_in_memory() -> Result<Self, DbError> {
32 let conn = Connection::open_in_memory()?;
33 conn.execute_batch("PRAGMA foreign_keys=ON;")?;
34 let db = Self { conn };
35 db.migrate()?;
36 Ok(db)
37 }
38
39 pub fn conn(&self) -> &Connection {
40 &self.conn
41 }
42
43 fn migrate(&self) -> Result<(), DbError> {
44 let current_version = self.current_schema_version();
45 if current_version < 1 {
46 let sql = include_str!("../migrations/001_initial.sql");
47 self.conn.execute_batch(sql)?;
48 }
49 Ok(())
50 }
51
52 fn current_schema_version(&self) -> i64 {
53 self.conn
54 .query_row("SELECT MAX(version) FROM schema_version", [], |row| {
55 row.get(0)
56 })
57 .unwrap_or(0)
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn test_open_in_memory() {
67 let db = Db::open_in_memory().expect("should open in-memory db");
68 let version: i64 = db
69 .conn()
70 .query_row("SELECT MAX(version) FROM schema_version", [], |row| {
71 row.get(0)
72 })
73 .unwrap();
74 assert_eq!(version, 1);
75 }
76
77 #[test]
78 fn test_migrate_idempotent() {
79 let db = Db::open_in_memory().expect("should open in-memory db");
80 db.migrate().expect("idempotent migration should succeed");
82 let version: i64 = db
83 .conn()
84 .query_row("SELECT MAX(version) FROM schema_version", [], |row| {
85 row.get(0)
86 })
87 .unwrap();
88 assert_eq!(version, 1);
89 }
90}