zeph_memory/sqlite/
mod.rs1mod history;
2mod messages;
3mod skills;
4mod summaries;
5mod trust;
6
7use sqlx::SqlitePool;
8use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
9use std::str::FromStr;
10
11use crate::error::MemoryError;
12
13pub use messages::role_str;
14pub use skills::{SkillMetricsRow, SkillUsageRow, SkillVersionRow};
15pub use trust::SkillTrustRow;
16
17#[derive(Debug, Clone)]
18pub struct SqliteStore {
19 pool: SqlitePool,
20}
21
22impl SqliteStore {
23 pub async fn new(path: &str) -> Result<Self, MemoryError> {
32 let url = if path == ":memory:" {
33 "sqlite::memory:".to_string()
34 } else {
35 if let Some(parent) = std::path::Path::new(path).parent()
36 && !parent.as_os_str().is_empty()
37 {
38 std::fs::create_dir_all(parent)?;
39 }
40 format!("sqlite:{path}?mode=rwc")
41 };
42
43 let opts = SqliteConnectOptions::from_str(&url)?
44 .create_if_missing(true)
45 .foreign_keys(true)
46 .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
47 .synchronous(sqlx::sqlite::SqliteSynchronous::Normal);
48
49 let pool = SqlitePoolOptions::new()
50 .max_connections(5)
51 .connect_with(opts)
52 .await?;
53
54 sqlx::migrate!("./migrations").run(&pool).await?;
55
56 Ok(Self { pool })
57 }
58
59 #[must_use]
61 pub fn pool(&self) -> &SqlitePool {
62 &self.pool
63 }
64
65 pub async fn run_migrations(pool: &SqlitePool) -> Result<(), MemoryError> {
71 sqlx::migrate!("./migrations").run(pool).await?;
72 Ok(())
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use tempfile::NamedTempFile;
80
81 #[tokio::test]
82 async fn wal_journal_mode_enabled_on_file_db() {
83 let file = NamedTempFile::new().expect("tempfile");
84 let path = file.path().to_str().expect("valid path");
85
86 let store = SqliteStore::new(path).await.expect("SqliteStore::new");
87
88 let mode: String = sqlx::query_scalar("PRAGMA journal_mode")
89 .fetch_one(store.pool())
90 .await
91 .expect("PRAGMA query");
92
93 assert_eq!(mode, "wal", "expected WAL journal mode, got: {mode}");
94 }
95
96 #[tokio::test]
97 async fn creates_parent_dirs() {
98 let dir = tempfile::tempdir().expect("tempdir");
99 let deep = dir.path().join("a/b/c/zeph.db");
100 let path = deep.to_str().expect("valid path");
101 let _store = SqliteStore::new(path).await.expect("SqliteStore::new");
102 assert!(deep.exists(), "database file should exist");
103 }
104}