1mod acp_sessions;
30pub mod admission_training;
31pub mod compression_guidelines;
32pub mod compression_predictor;
33pub mod corrections;
34pub mod experiments;
35pub mod graph_store;
36mod history;
37mod mem_scenes;
38pub mod memory_tree;
39pub(crate) mod messages;
40pub mod overflow;
41pub mod persona;
42pub mod preferences;
43pub mod session_digest;
44mod skills;
45mod summaries;
46pub mod trajectory;
47mod trust;
48
49#[allow(unused_imports)]
50use zeph_db::sql;
51use zeph_db::{DbConfig, DbPool};
52
53use crate::error::MemoryError;
54
55pub use acp_sessions::{AcpSessionEvent, AcpSessionInfo};
56pub use memory_tree::MemoryTreeRow;
57pub use messages::role_str;
58pub use persona::PersonaFactRow;
59pub use skills::{SkillMetricsRow, SkillUsageRow, SkillVersionRow};
60pub use trajectory::{NewTrajectoryEntry, TrajectoryEntryRow};
61pub use trust::{SkillTrustRow, SourceKind};
62
63pub type SqliteStore = DbStore;
65
66#[derive(Debug, Clone)]
82pub struct DbStore {
83 pool: DbPool,
84}
85
86impl DbStore {
87 pub async fn new(path: &str) -> Result<Self, MemoryError> {
93 Self::with_pool_size(path, 5).await
94 }
95
96 pub async fn with_pool_size(path: &str, pool_size: u32) -> Result<Self, MemoryError> {
102 let pool = DbConfig {
103 url: path.to_string(),
104 max_connections: pool_size,
105 pool_size,
106 }
107 .connect()
108 .await?;
109
110 Ok(Self { pool })
111 }
112
113 #[must_use]
115 pub fn pool(&self) -> &DbPool {
116 &self.pool
117 }
118
119 pub async fn run_migrations(pool: &DbPool) -> Result<(), MemoryError> {
125 zeph_db::run_migrations(pool).await?;
126 Ok(())
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use tempfile::NamedTempFile;
134
135 const DEFAULT_BUSY_TIMEOUT_MS: i64 = 5_000;
137
138 #[tokio::test]
139 async fn wal_journal_mode_enabled_on_file_db() {
140 let file = NamedTempFile::new().expect("tempfile");
141 let path = file.path().to_str().expect("valid path");
142
143 let store = DbStore::new(path).await.expect("DbStore::new");
144
145 let mode: String = zeph_db::query_scalar(sql!("PRAGMA journal_mode"))
146 .fetch_one(store.pool())
147 .await
148 .expect("PRAGMA query");
149
150 assert_eq!(mode, "wal", "expected WAL journal mode, got: {mode}");
151 }
152
153 #[tokio::test]
154 async fn busy_timeout_enabled_on_file_db() {
155 let file = NamedTempFile::new().expect("tempfile");
156 let path = file.path().to_str().expect("valid path");
157
158 let store = DbStore::new(path).await.expect("DbStore::new");
159
160 let timeout_ms: i64 = zeph_db::query_scalar(sql!("PRAGMA busy_timeout"))
161 .fetch_one(store.pool())
162 .await
163 .expect("PRAGMA query");
164
165 assert_eq!(
166 timeout_ms, DEFAULT_BUSY_TIMEOUT_MS,
167 "expected busy_timeout pragma to match configured timeout"
168 );
169 }
170
171 #[tokio::test]
172 async fn creates_parent_dirs() {
173 let dir = tempfile::tempdir().expect("tempdir");
174 let deep = dir.path().join("a/b/c/zeph.db");
175 let path = deep.to_str().expect("valid path");
176 let _store = DbStore::new(path).await.expect("DbStore::new");
177 assert!(deep.exists(), "database file should exist");
178 }
179
180 #[tokio::test]
181 async fn with_pool_size_accepts_custom_size() {
182 let store = DbStore::with_pool_size(":memory:", 2)
183 .await
184 .expect("with_pool_size");
185 let _cid = store
187 .create_conversation()
188 .await
189 .expect("create_conversation");
190 }
191}