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