Skip to main content

modo/db/
config.rs

1use serde::Deserialize;
2
3/// Database configuration with sensible defaults for SQLite/libsql.
4///
5/// All fields are optional when deserializing from YAML. Defaults produce
6/// a WAL-mode database at `data/app.db` with foreign keys enabled.
7///
8/// If [`migrations`](Self::migrations) is set, SQL migrations from that
9/// directory are applied automatically on [`connect`](super::connect).
10#[derive(Debug, Clone, Deserialize)]
11pub struct Config {
12    /// Database file path.
13    #[serde(default = "defaults::path")]
14    pub path: String,
15
16    /// Migration directory. If set, migrations run on connect.
17    #[serde(default)]
18    pub migrations: Option<String>,
19
20    /// Busy timeout in milliseconds.
21    #[serde(default = "defaults::busy_timeout")]
22    pub busy_timeout: u64,
23
24    /// Cache size in KB (applied as PRAGMA cache_size = -N).
25    #[serde(default = "defaults::cache_size")]
26    pub cache_size: i64,
27
28    /// Memory-mapped I/O size in bytes.
29    #[serde(default = "defaults::mmap_size")]
30    pub mmap_size: u64,
31
32    /// WAL journal mode.
33    #[serde(default = "defaults::journal_mode")]
34    pub journal_mode: JournalMode,
35
36    /// Synchronous mode.
37    #[serde(default = "defaults::synchronous")]
38    pub synchronous: SynchronousMode,
39
40    /// Foreign key enforcement.
41    #[serde(default = "defaults::foreign_keys")]
42    pub foreign_keys: bool,
43
44    /// Temp store location.
45    #[serde(default = "defaults::temp_store")]
46    pub temp_store: TempStore,
47
48    /// Optional pool configuration for multi-database sharding.
49    /// When set, [`DatabasePool::new`](super::DatabasePool::new) can be used
50    /// to manage shard databases that share this config's PRAGMAs and migrations.
51    #[serde(default)]
52    pub pool: Option<PoolConfig>,
53}
54
55impl Default for Config {
56    fn default() -> Self {
57        Self {
58            path: defaults::path(),
59            migrations: None,
60            busy_timeout: defaults::busy_timeout(),
61            cache_size: defaults::cache_size(),
62            mmap_size: defaults::mmap_size(),
63            journal_mode: defaults::journal_mode(),
64            synchronous: defaults::synchronous(),
65            foreign_keys: defaults::foreign_keys(),
66            temp_store: defaults::temp_store(),
67            pool: None,
68        }
69    }
70}
71
72/// SQLite journal mode.
73///
74/// Controls how the database writes transactions. Default is [`Wal`](Self::Wal).
75#[derive(Debug, Clone, Copy, Deserialize, Default)]
76#[serde(rename_all = "lowercase")]
77pub enum JournalMode {
78    #[default]
79    Wal,
80    Delete,
81    Truncate,
82    Memory,
83    Off,
84}
85
86impl JournalMode {
87    /// Returns the PRAGMA-compatible string representation.
88    pub fn as_str(self) -> &'static str {
89        match self {
90            Self::Wal => "WAL",
91            Self::Delete => "DELETE",
92            Self::Truncate => "TRUNCATE",
93            Self::Memory => "MEMORY",
94            Self::Off => "OFF",
95        }
96    }
97}
98
99/// SQLite synchronous mode.
100///
101/// Controls the trade-off between durability and write performance.
102/// Default is [`Normal`](Self::Normal).
103#[derive(Debug, Clone, Copy, Deserialize, Default)]
104#[serde(rename_all = "lowercase")]
105pub enum SynchronousMode {
106    Off,
107    #[default]
108    Normal,
109    Full,
110    Extra,
111}
112
113impl SynchronousMode {
114    /// Returns the PRAGMA-compatible string representation.
115    pub fn as_str(self) -> &'static str {
116        match self {
117            Self::Off => "OFF",
118            Self::Normal => "NORMAL",
119            Self::Full => "FULL",
120            Self::Extra => "EXTRA",
121        }
122    }
123}
124
125/// SQLite temp store location.
126///
127/// Controls where temporary tables and indices are stored.
128/// Default is [`Memory`](Self::Memory).
129#[derive(Debug, Clone, Copy, Deserialize, Default)]
130#[serde(rename_all = "lowercase")]
131pub enum TempStore {
132    Default,
133    File,
134    #[default]
135    Memory,
136}
137
138impl TempStore {
139    /// Returns the PRAGMA-compatible string representation.
140    pub fn as_str(self) -> &'static str {
141        match self {
142            Self::Default => "DEFAULT",
143            Self::File => "FILE",
144            Self::Memory => "MEMORY",
145        }
146    }
147}
148
149/// Pool configuration for multi-database sharding.
150///
151/// When nested inside [`Config`], enables [`DatabasePool`](super::DatabasePool)
152/// to manage lazily-opened shard databases that share the parent config's
153/// PRAGMAs and migrations.
154#[derive(Debug, Clone, Deserialize)]
155pub struct PoolConfig {
156    /// Directory where shard databases are stored.
157    /// Each shard creates `{base_path}/{shard_name}.db`.
158    #[serde(default = "defaults::base_path")]
159    pub base_path: String,
160
161    /// Number of internal lock shards for the connection map.
162    /// Controls lock contention parallelism, not the number of tenant databases.
163    #[serde(default = "defaults::lock_shards")]
164    pub lock_shards: usize,
165}
166
167impl Default for PoolConfig {
168    fn default() -> Self {
169        Self {
170            base_path: defaults::base_path(),
171            lock_shards: defaults::lock_shards(),
172        }
173    }
174}
175
176mod defaults {
177    use super::*;
178
179    pub fn path() -> String {
180        "data/app.db".to_string()
181    }
182
183    pub fn busy_timeout() -> u64 {
184        5000
185    }
186
187    pub fn cache_size() -> i64 {
188        16384
189    }
190
191    pub fn mmap_size() -> u64 {
192        268_435_456 // 256 MB
193    }
194
195    pub fn journal_mode() -> JournalMode {
196        JournalMode::Wal
197    }
198
199    pub fn synchronous() -> SynchronousMode {
200        SynchronousMode::Normal
201    }
202
203    pub fn foreign_keys() -> bool {
204        true
205    }
206
207    pub fn temp_store() -> TempStore {
208        TempStore::Memory
209    }
210
211    pub fn base_path() -> String {
212        "data/shards".to_string()
213    }
214
215    pub fn lock_shards() -> usize {
216        16
217    }
218}