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