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