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