1mod acp_sessions;
5pub mod admission_training;
6pub mod compression_guidelines;
7pub mod compression_predictor;
8pub mod corrections;
9pub mod experiments;
10pub mod graph_store;
11mod history;
12mod mem_scenes;
13pub mod memory_tree;
14pub(crate) mod messages;
15pub mod overflow;
16pub mod persona;
17pub mod preferences;
18pub mod session_digest;
19mod skills;
20mod summaries;
21pub mod trajectory;
22mod trust;
23
24#[allow(unused_imports)]
25use zeph_db::sql;
26use zeph_db::{DbConfig, DbPool};
27
28use crate::error::MemoryError;
29
30pub use acp_sessions::{AcpSessionEvent, AcpSessionInfo};
31pub use memory_tree::MemoryTreeRow;
32pub use messages::role_str;
33pub use persona::PersonaFactRow;
34pub use skills::{SkillMetricsRow, SkillUsageRow, SkillVersionRow};
35pub use trajectory::{NewTrajectoryEntry, TrajectoryEntryRow};
36pub use trust::{SkillTrustRow, SourceKind};
37
38pub type SqliteStore = DbStore;
40
41#[derive(Debug, Clone)]
42pub struct DbStore {
43 pool: DbPool,
44}
45
46impl DbStore {
47 pub async fn new(path: &str) -> Result<Self, MemoryError> {
53 Self::with_pool_size(path, 5).await
54 }
55
56 pub async fn with_pool_size(path: &str, pool_size: u32) -> Result<Self, MemoryError> {
62 let pool = DbConfig {
63 url: path.to_string(),
64 max_connections: pool_size,
65 pool_size,
66 }
67 .connect()
68 .await?;
69
70 Ok(Self { pool })
71 }
72
73 #[must_use]
75 pub fn pool(&self) -> &DbPool {
76 &self.pool
77 }
78
79 pub async fn run_migrations(pool: &DbPool) -> Result<(), MemoryError> {
85 zeph_db::run_migrations(pool).await?;
86 Ok(())
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use tempfile::NamedTempFile;
94
95 const DEFAULT_BUSY_TIMEOUT_MS: i64 = 5_000;
97
98 #[tokio::test]
99 async fn wal_journal_mode_enabled_on_file_db() {
100 let file = NamedTempFile::new().expect("tempfile");
101 let path = file.path().to_str().expect("valid path");
102
103 let store = DbStore::new(path).await.expect("DbStore::new");
104
105 let mode: String = zeph_db::query_scalar(sql!("PRAGMA journal_mode"))
106 .fetch_one(store.pool())
107 .await
108 .expect("PRAGMA query");
109
110 assert_eq!(mode, "wal", "expected WAL journal mode, got: {mode}");
111 }
112
113 #[tokio::test]
114 async fn busy_timeout_enabled_on_file_db() {
115 let file = NamedTempFile::new().expect("tempfile");
116 let path = file.path().to_str().expect("valid path");
117
118 let store = DbStore::new(path).await.expect("DbStore::new");
119
120 let timeout_ms: i64 = zeph_db::query_scalar(sql!("PRAGMA busy_timeout"))
121 .fetch_one(store.pool())
122 .await
123 .expect("PRAGMA query");
124
125 assert_eq!(
126 timeout_ms, DEFAULT_BUSY_TIMEOUT_MS,
127 "expected busy_timeout pragma to match configured timeout"
128 );
129 }
130
131 #[tokio::test]
132 async fn creates_parent_dirs() {
133 let dir = tempfile::tempdir().expect("tempdir");
134 let deep = dir.path().join("a/b/c/zeph.db");
135 let path = deep.to_str().expect("valid path");
136 let _store = DbStore::new(path).await.expect("DbStore::new");
137 assert!(deep.exists(), "database file should exist");
138 }
139
140 #[tokio::test]
141 async fn with_pool_size_accepts_custom_size() {
142 let store = DbStore::with_pool_size(":memory:", 2)
143 .await
144 .expect("with_pool_size");
145 let _cid = store
147 .create_conversation()
148 .await
149 .expect("create_conversation");
150 }
151}