1use crate::integrity::validate_internal_records;
12use crate::schema;
13use crate::{
14 SqliteReplicationLog, SqliteSyncCheckpointStore, SqliteSyncConfig, SqliteSyncError,
15 SqliteSyncOutbox, SqliteSyncResult, SqliteSyncTombstoneStore,
16};
17use appcore_contracts::ProviderId;
18use appcore_storage::{
19 StorageCapabilityDescriptorV1, StorageCapabilityProviderV1, StorageCapabilityV1,
20};
21use parking_lot::{Condvar, Mutex};
22use rusqlite::limits::Limit;
23use rusqlite::{Connection, OpenFlags};
24use std::fmt;
25use std::fs;
26use std::path::Path;
27use std::sync::Arc;
28use std::time::{Duration, Instant};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct SqliteSyncHealth {
33 pub schema_version: u32,
35 pub page_count: u64,
37 pub max_page_count: u64,
39}
40
41struct SqliteSyncInner {
42 pool: Mutex<ConnectionPool>,
43 available: Condvar,
44 config: SqliteSyncConfig,
45}
46
47struct ConnectionPool {
48 idle: Vec<Connection>,
49 total: usize,
50}
51
52struct ConnectionGuard<'a> {
53 inner: &'a SqliteSyncInner,
54 connection: Option<Connection>,
55}
56
57#[derive(Clone)]
59pub struct SqliteSyncStore {
60 inner: Arc<SqliteSyncInner>,
61}
62
63impl SqliteSyncStore {
64 pub fn open(mut config: SqliteSyncConfig) -> SqliteSyncResult<Self> {
66 config.validate()?;
67 config.path = normalize_path(&config.path)?;
68 let flags = OpenFlags::SQLITE_OPEN_READ_WRITE
69 | OpenFlags::SQLITE_OPEN_CREATE
70 | OpenFlags::SQLITE_OPEN_FULL_MUTEX;
71 let mut connection =
72 Connection::open_with_flags(&config.path, flags).map_err(SqliteSyncError::database)?;
73 configure(&connection, &config)?;
74 schema::migrate(&mut connection)?;
75 integrity_check(&connection, &config)?;
76 Ok(Self {
77 inner: Arc::new(SqliteSyncInner {
78 pool: Mutex::new(ConnectionPool {
79 idle: vec![connection],
80 total: 1,
81 }),
82 available: Condvar::new(),
83 config,
84 }),
85 })
86 }
87
88 pub fn health(&self) -> SqliteSyncResult<SqliteSyncHealth> {
90 self.with_connection(|connection| {
91 integrity_check(connection, &self.inner.config)?;
92 let schema_version = connection
93 .pragma_query_value(None, "user_version", |row| row.get(0))
94 .map_err(SqliteSyncError::database)?;
95 let page_count: i64 = connection
96 .pragma_query_value(None, "page_count", |row| row.get(0))
97 .map_err(SqliteSyncError::database)?;
98 let max_page_count: i64 = connection
99 .pragma_query_value(None, "max_page_count", |row| row.get(0))
100 .map_err(SqliteSyncError::database)?;
101 Ok(SqliteSyncHealth {
102 schema_version,
103 page_count: u64::try_from(page_count)
104 .map_err(|_| SqliteSyncError::CorruptRecord("page count"))?,
105 max_page_count: u64::try_from(max_page_count)
106 .map_err(|_| SqliteSyncError::CorruptRecord("page limit"))?,
107 })
108 })
109 }
110
111 pub fn config(&self) -> &SqliteSyncConfig {
113 &self.inner.config
114 }
115
116 pub fn replication_log(&self) -> SqliteReplicationLog {
118 SqliteReplicationLog::new(self.clone())
119 }
120
121 pub fn checkpoint_store(&self) -> SqliteSyncCheckpointStore {
123 SqliteSyncCheckpointStore::new(self.clone())
124 }
125
126 pub fn outbox(&self) -> SqliteSyncOutbox {
128 SqliteSyncOutbox::new(self.clone())
129 }
130
131 pub fn tombstone_store(&self) -> SqliteSyncTombstoneStore {
133 SqliteSyncTombstoneStore::new(self.clone())
134 }
135
136 pub(crate) fn with_connection<T>(
137 &self,
138 action: impl FnOnce(&mut Connection) -> SqliteSyncResult<T>,
139 ) -> SqliteSyncResult<T> {
140 let mut connection = self.acquire_connection()?;
141 action(connection.connection_mut()?)
142 }
143
144 fn acquire_connection(&self) -> SqliteSyncResult<ConnectionGuard<'_>> {
145 let deadline = Instant::now() + Duration::from_millis(self.inner.config.busy_timeout_ms);
146 let mut pool = self.inner.pool.lock();
147 loop {
148 if let Some(connection) = pool.idle.pop() {
149 return Ok(ConnectionGuard {
150 inner: &self.inner,
151 connection: Some(connection),
152 });
153 }
154 if pool.total < self.inner.config.max_connections {
155 pool.total += 1;
156 drop(pool);
157 return match open_connection(&self.inner.config) {
158 Ok(connection) => Ok(ConnectionGuard {
159 inner: &self.inner,
160 connection: Some(connection),
161 }),
162 Err(error) => {
163 let mut pool = self.inner.pool.lock();
164 pool.total = pool.total.saturating_sub(1);
165 self.inner.available.notify_one();
166 Err(error)
167 }
168 };
169 }
170 let now = Instant::now();
171 if now >= deadline {
172 return Err(SqliteSyncError::CapacityExceeded("connection"));
173 }
174 self.inner.available.wait_for(&mut pool, deadline - now);
175 }
176 }
177}
178
179impl ConnectionGuard<'_> {
180 fn connection_mut(&mut self) -> SqliteSyncResult<&mut Connection> {
181 self.connection
182 .as_mut()
183 .ok_or(SqliteSyncError::DatabaseOperation)
184 }
185}
186
187impl Drop for ConnectionGuard<'_> {
188 fn drop(&mut self) {
189 if let Some(connection) = self.connection.take() {
190 self.inner.pool.lock().idle.push(connection);
191 self.inner.available.notify_one();
192 }
193 }
194}
195
196impl fmt::Debug for SqliteSyncStore {
197 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198 formatter
199 .debug_struct("SqliteSyncStore")
200 .field("config", &self.inner.config)
201 .finish_non_exhaustive()
202 }
203}
204
205impl StorageCapabilityProviderV1 for SqliteSyncStore {
206 fn storage_capabilities_v1(
207 &self,
208 ) -> Result<StorageCapabilityDescriptorV1, appcore_storage::StorageCapabilityError> {
209 sqlite_sync_capability_descriptor_v1()
210 }
211}
212
213pub fn sqlite_sync_capability_descriptor_v1(
215) -> Result<StorageCapabilityDescriptorV1, appcore_storage::StorageCapabilityError> {
216 let provider_id = ProviderId::new("sqlite-sync")
217 .map_err(|_| appcore_storage::StorageCapabilityError::InvalidDescriptor)?;
218 Ok(StorageCapabilityDescriptorV1::new(
219 provider_id,
220 [
221 StorageCapabilityV1::Transactions,
222 StorageCapabilityV1::Locking,
223 StorageCapabilityV1::Snapshot,
224 StorageCapabilityV1::OnlineBackup,
225 StorageCapabilityV1::MultiProcess,
226 ],
227 ))
228}
229
230fn configure(connection: &Connection, config: &SqliteSyncConfig) -> SqliteSyncResult<()> {
231 connection
232 .busy_timeout(Duration::from_millis(config.busy_timeout_ms))
233 .map_err(SqliteSyncError::database)?;
234 connection
235 .pragma_update_and_check(None, "journal_mode", "WAL", |row| row.get::<_, String>(0))
236 .map_err(SqliteSyncError::database)
237 .and_then(|mode| {
238 if mode.eq_ignore_ascii_case("wal") {
239 Ok(())
240 } else {
241 Err(SqliteSyncError::DatabaseOperation)
242 }
243 })?;
244 connection
245 .pragma_update(None, "synchronous", "FULL")
246 .and_then(|_| connection.pragma_update(None, "foreign_keys", true))
247 .and_then(|_| connection.pragma_update(None, "trusted_schema", false))
248 .and_then(|_| connection.pragma_update(None, "wal_autocheckpoint", 1_000))
249 .map_err(SqliteSyncError::database)?;
250 let max_length = i32::try_from(config.max_outbox_record_bytes)
251 .map_err(|_| SqliteSyncError::InvalidConfiguration("outbox record bound overflow"))?;
252 connection
253 .set_limit(Limit::SQLITE_LIMIT_LENGTH, max_length)
254 .and_then(|_| connection.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0))
255 .and_then(|_| connection.set_limit(Limit::SQLITE_LIMIT_WORKER_THREADS, 0))
256 .map_err(SqliteSyncError::database)?;
257 let page_bytes: i64 = connection
258 .pragma_query_value(None, "page_size", |row| row.get(0))
259 .map_err(SqliteSyncError::database)?;
260 let page_bytes =
261 u64::try_from(page_bytes).map_err(|_| SqliteSyncError::CorruptRecord("page size"))?;
262 if page_bytes == 0 {
263 return Err(SqliteSyncError::CorruptRecord("page size"));
264 }
265 let max_pages = i64::try_from(config.max_database_bytes / page_bytes)
266 .map_err(|_| SqliteSyncError::InvalidConfiguration("database page bound overflow"))?;
267 connection
268 .pragma_update(None, "max_page_count", max_pages)
269 .map_err(SqliteSyncError::database)
270}
271
272fn open_connection(config: &SqliteSyncConfig) -> SqliteSyncResult<Connection> {
273 let flags = OpenFlags::SQLITE_OPEN_READ_WRITE
274 | OpenFlags::SQLITE_OPEN_CREATE
275 | OpenFlags::SQLITE_OPEN_FULL_MUTEX;
276 let connection =
277 Connection::open_with_flags(&config.path, flags).map_err(SqliteSyncError::database)?;
278 configure(&connection, config)?;
279 Ok(connection)
280}
281
282fn integrity_check(connection: &Connection, config: &SqliteSyncConfig) -> SqliteSyncResult<()> {
283 let result: String = connection
284 .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
285 .map_err(SqliteSyncError::database)?;
286 if result != "ok" {
287 return Err(SqliteSyncError::IntegrityFailed);
288 }
289 validate_internal_records(connection, config)
290}
291
292pub(crate) fn normalize_path(path: &Path) -> SqliteSyncResult<std::path::PathBuf> {
293 let file_name = path.file_name().ok_or(SqliteSyncError::UnsafePath)?;
294 match fs::symlink_metadata(path) {
295 Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
296 return Err(SqliteSyncError::UnsafePath);
297 }
298 Ok(_) => {}
299 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
300 Err(_) => return Err(SqliteSyncError::UnsafePath),
301 }
302 let parent = path.parent().unwrap_or_else(|| Path::new("."));
303 fs::create_dir_all(parent).map_err(|_| SqliteSyncError::UnsafePath)?;
304 let canonical_parent = fs::canonicalize(parent).map_err(|_| SqliteSyncError::UnsafePath)?;
305 if !canonical_parent.is_dir() {
306 return Err(SqliteSyncError::UnsafePath);
307 }
308 Ok(canonical_parent.join(file_name))
309}