Skip to main content

appcore_sync_sqlite/
config.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: config.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/26 00:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/26 00:00:00 by dnettoRaw
8//      ###########      S: 2.0.0
9// =============================================================================
10
11use crate::{SqliteSyncError, SqliteSyncResult};
12use std::fmt;
13use std::path::{Path, PathBuf};
14
15const MIB: u64 = 1024 * 1024;
16
17/// Bounded configuration for one SQLite sync database.
18#[derive(Clone, PartialEq, Eq)]
19pub struct SqliteSyncConfig {
20    pub(crate) path: PathBuf,
21    pub(crate) max_database_bytes: u64,
22    pub(crate) max_outbox_entries: usize,
23    pub(crate) max_checkpoints: usize,
24    pub(crate) max_outbox_record_bytes: usize,
25    pub(crate) max_read_records: usize,
26    pub(crate) max_read_bytes: usize,
27    pub(crate) max_tombstones: usize,
28    pub(crate) max_connections: usize,
29    pub(crate) busy_timeout_ms: u64,
30    pub(crate) backup_pages_per_step: i32,
31}
32
33impl SqliteSyncConfig {
34    /// Creates a configuration with conservative production bounds.
35    pub fn new(path: impl Into<PathBuf>) -> Self {
36        Self {
37            path: path.into(),
38            max_database_bytes: 512 * MIB,
39            max_outbox_entries: 10_000,
40            max_checkpoints: 10_000,
41            max_outbox_record_bytes: 48 * MIB as usize,
42            max_read_records: 4_096,
43            max_read_bytes: 16 * MIB as usize,
44            max_tombstones: 100_000,
45            max_connections: 8,
46            busy_timeout_ms: 5_000,
47            backup_pages_per_step: 128,
48        }
49    }
50
51    /// Selects the maximum database size in bytes.
52    pub fn with_max_database_bytes(mut self, value: u64) -> Self {
53        self.max_database_bytes = value;
54        self
55    }
56
57    /// Selects the maximum number of pending outbox entries.
58    pub fn with_max_outbox_entries(mut self, value: usize) -> Self {
59        self.max_outbox_entries = value;
60        self
61    }
62
63    /// Selects the maximum number of peer checkpoints.
64    pub fn with_max_checkpoints(mut self, value: usize) -> Self {
65        self.max_checkpoints = value;
66        self
67    }
68
69    /// Selects the maximum encoded size of one outbox entry.
70    pub fn with_max_outbox_record_bytes(mut self, value: usize) -> Self {
71        self.max_outbox_record_bytes = value;
72        self
73    }
74
75    /// Selects the maximum record count returned by one log read.
76    pub fn with_max_read_records(mut self, value: usize) -> Self {
77        self.max_read_records = value;
78        self
79    }
80
81    /// Selects the maximum payload bytes returned by one log read.
82    pub fn with_max_read_bytes(mut self, value: usize) -> Self {
83        self.max_read_bytes = value;
84        self
85    }
86
87    /// Selects the maximum retained tombstone count.
88    pub fn with_max_tombstones(mut self, value: usize) -> Self {
89        self.max_tombstones = value;
90        self
91    }
92
93    /// Selects the maximum number of simultaneously open SQLite connections.
94    pub fn with_max_connections(mut self, value: usize) -> Self {
95        self.max_connections = value;
96        self
97    }
98
99    /// Selects the SQLite writer-admission timeout in milliseconds.
100    pub fn with_busy_timeout_ms(mut self, value: u64) -> Self {
101        self.busy_timeout_ms = value;
102        self
103    }
104
105    /// Selects the number of pages copied by each online-backup step.
106    pub fn with_backup_pages_per_step(mut self, value: i32) -> Self {
107        self.backup_pages_per_step = value;
108        self
109    }
110
111    /// Returns the configured database path.
112    pub fn path(&self) -> &Path {
113        &self.path
114    }
115
116    pub(crate) fn validate(&self) -> SqliteSyncResult<()> {
117        if self.path.as_os_str().is_empty() {
118            return Err(SqliteSyncError::InvalidConfiguration("empty path"));
119        }
120        if !(8 * MIB..=8 * 1024 * MIB).contains(&self.max_database_bytes) {
121            return Err(SqliteSyncError::InvalidConfiguration(
122                "database byte bound is outside 8 MiB..=8 GiB",
123            ));
124        }
125        if !(1..=100_000).contains(&self.max_outbox_entries) {
126            return Err(SqliteSyncError::InvalidConfiguration(
127                "outbox entry bound is outside 1..=100000",
128            ));
129        }
130        if !(1..=100_000).contains(&self.max_checkpoints) {
131            return Err(SqliteSyncError::InvalidConfiguration(
132                "checkpoint bound is outside 1..=100000",
133            ));
134        }
135        if !(MIB as usize..=48 * MIB as usize).contains(&self.max_outbox_record_bytes) {
136            return Err(SqliteSyncError::InvalidConfiguration(
137                "outbox record bound is outside 1 MiB..=48 MiB",
138            ));
139        }
140        if !(1..=10_000).contains(&self.max_read_records) {
141            return Err(SqliteSyncError::InvalidConfiguration(
142                "read record bound is outside 1..=10000",
143            ));
144        }
145        if !(MIB as usize..=64 * MIB as usize).contains(&self.max_read_bytes) {
146            return Err(SqliteSyncError::InvalidConfiguration(
147                "read byte bound is outside 1 MiB..=64 MiB",
148            ));
149        }
150        if !(1..=1_000_000).contains(&self.max_tombstones) {
151            return Err(SqliteSyncError::InvalidConfiguration(
152                "tombstone bound is outside 1..=1000000",
153            ));
154        }
155        if !(1..=32).contains(&self.max_connections) {
156            return Err(SqliteSyncError::InvalidConfiguration(
157                "connection bound is outside 1..=32",
158            ));
159        }
160        if !(1..=60_000).contains(&self.busy_timeout_ms) {
161            return Err(SqliteSyncError::InvalidConfiguration(
162                "busy timeout is outside 1..=60000 ms",
163            ));
164        }
165        if !(1..=4_096).contains(&self.backup_pages_per_step) {
166            return Err(SqliteSyncError::InvalidConfiguration(
167                "backup step is outside 1..=4096 pages",
168            ));
169        }
170        Ok(())
171    }
172}
173
174impl fmt::Debug for SqliteSyncConfig {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        formatter
177            .debug_struct("SqliteSyncConfig")
178            .field("path_configured", &!self.path.as_os_str().is_empty())
179            .field("max_database_bytes", &self.max_database_bytes)
180            .field("max_outbox_entries", &self.max_outbox_entries)
181            .field("max_checkpoints", &self.max_checkpoints)
182            .field("max_outbox_record_bytes", &self.max_outbox_record_bytes)
183            .field("max_read_records", &self.max_read_records)
184            .field("max_read_bytes", &self.max_read_bytes)
185            .field("max_tombstones", &self.max_tombstones)
186            .field("max_connections", &self.max_connections)
187            .field("busy_timeout_ms", &self.busy_timeout_ms)
188            .field("backup_pages_per_step", &self.backup_pages_per_step)
189            .finish()
190    }
191}