1use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19use parking_lot::{Condvar, Mutex, RwLock};
20use rusqlite::{Connection, OpenFlags};
21
22use crate::compressed_vfs::{self, SQLiteCompressedContainerAnchor, SQLiteCompressionOptions};
23
24#[derive(Debug, thiserror::Error)]
25pub enum SQLiteError {
26 #[error(transparent)]
27 Memory(#[from] uqa_core::memory::MemoryError),
28 #[error(transparent)]
29 Cancelled(#[from] uqa_core::QueryCancelled),
30 #[error("text analysis failed: {0}")]
31 Analysis(#[from] uqa_analysis::AnalysisError),
32 #[error("sqlite error: {0}")]
33 SQLite(#[from] rusqlite::Error),
34 #[error("encryption key must not be empty")]
35 EmptyEncryptionKey,
36 #[error("database requires an encryption key")]
37 EncryptionKeyRequired,
38 #[error("database is not encrypted but an encryption key was provided")]
39 NotEncrypted,
40 #[error("compressed sqlite container error: {0}")]
41 CompressedContainer(String),
42 #[error("io error: {0}")]
43 Io(#[from] std::io::Error),
44 #[error("catalog migration {version} failed: {source}")]
45 Migration {
46 version: u32,
47 #[source]
48 source: rusqlite::Error,
49 },
50 #[error("invalid persisted catalog schema version `{0}`")]
51 InvalidSchemaVersion(String),
52 #[error("catalog schema version {found} is newer than this engine supports ({supported})")]
53 UnsupportedSchemaVersion { found: u32, supported: u32 },
54 #[error("corrupt document blob for `{table}` doc {doc_id} field `{field}`: {reason}")]
55 CorruptDocumentBlob {
56 table: String,
57 doc_id: u64,
58 field: String,
59 reason: String,
60 },
61 #[error("payload serialization failed: {0}")]
62 Serde(#[from] serde_json::Error),
63 #[error("storage backend error: {0}")]
64 StorageBackend(String),
65 #[error("transaction already active for this sqlite session")]
66 TransactionAlreadyActive,
67 #[error("no active transaction for this sqlite session")]
68 NoActiveTransaction,
69 #[error("sqlite transaction was aborted by an earlier storage error: {0}")]
70 TransactionAborted(String),
71 #[error("sqlite session cleanup failed: {0}")]
72 SessionCleanupFailed(String),
73 #[error("sqlite connection-pool checkout lost its connection")]
74 MissingCheckedOutConnection,
75}
76
77pub type Result<T> = std::result::Result<T, SQLiteError>;
78
79const MIN_POOL_CONNECTIONS: usize = 4;
80const MAX_POOL_CONNECTIONS: usize = 32;
81
82#[derive(Clone)]
83enum ConnectionSpec {
84 File {
85 path: PathBuf,
86 key: Option<Arc<str>>,
87 },
88 Compressed {
89 path: PathBuf,
90 compression: SQLiteCompressionOptions,
91 },
92 Memory,
93}
94
95impl ConnectionSpec {
96 fn open(&self, initialize_database: bool) -> Result<Connection> {
97 match self {
98 Self::File { path, key } => {
99 let conn = Connection::open(path)?;
100 if let Some(key) = key {
101 ManagedConnection::apply_encryption_key(&conn, key)?;
102 }
103 if initialize_database {
104 ManagedConnection::enable_wal(&conn)?;
105 }
106 ManagedConnection::configure_wal_connection(&conn)?;
107 Ok(conn)
108 }
109 Self::Compressed { path, compression } => {
110 let conn = Connection::open_with_flags_and_vfs(
111 path,
112 OpenFlags::default(),
113 compressed_vfs::VFS_NAME,
114 )?;
115 if initialize_database {
116 conn.pragma_update(None, "page_size", compression.page_size)?;
117 ManagedConnection::enable_compressed_journal(&conn)?;
118 }
119 ManagedConnection::configure_compressed_connection(&conn)?;
120 Ok(conn)
121 }
122 Self::Memory => {
123 let conn = Connection::open_in_memory()?;
124 if initialize_database {
125 ManagedConnection::enable_wal(&conn)?;
126 }
127 ManagedConnection::configure_wal_connection(&conn)?;
128 Ok(conn)
129 }
130 }
131 }
132}
133
134struct PoolState {
135 idle: Vec<Connection>,
136 open: usize,
137}
138
139struct ConnectionPool {
140 spec: ConnectionSpec,
141 max_connections: usize,
142 state: Mutex<PoolState>,
143 available: Condvar,
144 data_version_monitor: Mutex<Option<Connection>>,
149}
150
151impl ConnectionPool {
152 fn new(spec: ConnectionSpec, initial: Connection, max_connections: usize) -> Arc<Self> {
153 Arc::new(Self {
154 spec,
155 max_connections: max_connections.max(1),
156 state: Mutex::new(PoolState {
157 idle: vec![initial],
158 open: 1,
159 }),
160 available: Condvar::new(),
161 data_version_monitor: Mutex::new(None),
162 })
163 }
164
165 fn checkout(self: &Arc<Self>) -> Result<PooledConnection> {
166 loop {
167 let mut state = self.state.lock();
168 if let Some(connection) = state.idle.pop() {
169 return Ok(PooledConnection {
170 pool: Arc::clone(self),
171 connection: Some(connection),
172 });
173 }
174 if state.open < self.max_connections {
175 state.open += 1;
176 drop(state);
177 return match self.spec.open(false) {
178 Ok(connection) => Ok(PooledConnection {
179 pool: Arc::clone(self),
180 connection: Some(connection),
181 }),
182 Err(error) => {
183 let mut state = self.state.lock();
184 state.open -= 1;
185 self.available.notify_one();
186 Err(error)
187 }
188 };
189 }
190 self.available.wait(&mut state);
191 }
192 }
193
194 fn checkin(&self, connection: Connection) {
195 self.state.lock().idle.push(connection);
196 self.available.notify_one();
197 }
198
199 fn discard(&self) {
200 let mut state = self.state.lock();
201 state.open -= 1;
202 self.available.notify_one();
203 }
204}
205
206struct PooledConnection {
207 pool: Arc<ConnectionPool>,
208 connection: Option<Connection>,
209}
210
211impl PooledConnection {
212 fn connection(&self) -> Result<&Connection> {
213 self.connection
214 .as_ref()
215 .ok_or(SQLiteError::MissingCheckedOutConnection)
216 }
217
218 fn connection_mut(&mut self) -> Result<&mut Connection> {
219 self.connection
220 .as_mut()
221 .ok_or(SQLiteError::MissingCheckedOutConnection)
222 }
223}
224
225impl Drop for PooledConnection {
226 fn drop(&mut self) {
227 let Some(connection) = self.connection.take() else {
228 return;
229 };
230 let reusable = connection.is_autocommit() || connection.execute_batch("ROLLBACK").is_ok();
231 if reusable {
232 self.pool.checkin(connection);
233 } else {
234 self.pool.discard();
235 }
236 }
237}
238
239struct SessionState {
240 gate: RwLock<()>,
244 transaction: Mutex<Option<PooledConnection>>,
245 transaction_failure: Mutex<Option<String>>,
246 cleanup_failure: Mutex<Option<String>>,
247}
248
249impl SessionState {
250 fn new() -> Self {
251 Self {
252 gate: RwLock::new(()),
253 transaction: Mutex::new(None),
254 transaction_failure: Mutex::new(None),
255 cleanup_failure: Mutex::new(None),
256 }
257 }
258}
259
260impl Drop for SessionState {
261 fn drop(&mut self) {
262 self.transaction.get_mut().take();
266 }
267}
268
269#[derive(Clone)]
273pub struct ManagedConnection {
274 pool: Arc<ConnectionPool>,
275 session: Arc<SessionState>,
276}
277
278impl ManagedConnection {
279 fn surface_cleanup_failure(&self) -> Result<()> {
280 if let Some(error) = self.session.cleanup_failure.lock().take() {
281 return Err(SQLiteError::SessionCleanupFailed(error));
282 }
283 Ok(())
284 }
285
286 pub fn open(path: &Path) -> Result<Self> {
287 if path == Path::new(":memory:") {
288 return Self::open_in_memory();
289 }
290 Self::open_with_optional_key(path, None)
291 }
292
293 #[must_use]
295 pub fn database_path(&self) -> Option<&Path> {
296 match &self.pool.spec {
297 ConnectionSpec::File { path, .. } | ConnectionSpec::Compressed { path, .. } => {
298 Some(path)
299 }
300 ConnectionSpec::Memory => None,
301 }
302 }
303
304 pub fn open_encrypted(path: &Path, key: &str) -> Result<Self> {
305 Self::open_with_optional_key(path, Some(key))
306 }
307
308 pub fn open_compressed(path: &Path, compression: SQLiteCompressionOptions) -> Result<Self> {
309 Self::open_compressed_with_optional_key(path, compression, None, None)
310 }
311
312 pub fn open_compressed_encrypted(
313 path: &Path,
314 key: &str,
315 compression: SQLiteCompressionOptions,
316 ) -> Result<Self> {
317 if key.is_empty() {
318 return Err(SQLiteError::EmptyEncryptionKey);
319 }
320 Self::open_compressed_with_optional_key(path, compression, Some(key), None)
321 }
322
323 pub fn open_compressed_encrypted_with_anchor(
326 path: &Path,
327 key: &str,
328 compression: SQLiteCompressionOptions,
329 trusted_anchor: SQLiteCompressedContainerAnchor,
330 ) -> Result<Self> {
331 if key.is_empty() {
332 return Err(SQLiteError::EmptyEncryptionKey);
333 }
334 Self::open_compressed_with_optional_key(path, compression, Some(key), Some(trusted_anchor))
335 }
336
337 fn open_with_optional_key(path: &Path, key: Option<&str>) -> Result<Self> {
338 let spec = ConnectionSpec::File {
339 path: path.to_path_buf(),
340 key: key.map(Arc::from),
341 };
342 Self::from_spec(spec, default_pool_connections())
343 }
344
345 fn open_compressed_with_optional_key(
346 path: &Path,
347 compression: SQLiteCompressionOptions,
348 key: Option<&str>,
349 trusted_anchor: Option<SQLiteCompressedContainerAnchor>,
350 ) -> Result<Self> {
351 let compression = compression
352 .validate()
353 .map_err(SQLiteError::CompressedContainer)?;
354 match trusted_anchor {
355 Some(anchor) => compressed_vfs::register_database_with_anchor(
356 path,
357 compression,
358 key.ok_or(SQLiteError::EncryptionKeyRequired)?,
359 anchor,
360 ),
361 None => compressed_vfs::register_database(path, compression, key),
362 }
363 .map_err(SQLiteError::CompressedContainer)?;
364 let spec = ConnectionSpec::Compressed {
365 path: path.to_path_buf(),
366 compression,
367 };
368 Self::from_spec(spec, default_pool_connections())
369 }
370
371 pub fn open_in_memory() -> Result<Self> {
372 Self::from_spec(ConnectionSpec::Memory, 1)
376 }
377
378 fn from_spec(spec: ConnectionSpec, max_connections: usize) -> Result<Self> {
379 let initial = spec.open(true)?;
380 Ok(Self {
381 pool: ConnectionPool::new(spec, initial, max_connections),
382 session: Arc::new(SessionState::new()),
383 })
384 }
385
386 fn apply_encryption_key(conn: &Connection, key: &str) -> Result<()> {
387 if key.is_empty() {
388 return Err(SQLiteError::EmptyEncryptionKey);
389 }
390 conn.pragma_update(None, "key", key)?;
391 Ok(())
392 }
393
394 fn enable_wal(conn: &Connection) -> Result<()> {
395 conn.pragma_update(None, "journal_mode", "WAL")?;
396 Ok(())
397 }
398
399 fn configure_wal_connection(conn: &Connection) -> Result<()> {
400 conn.busy_timeout(std::time::Duration::from_secs(5))?;
403 conn.pragma_update(None, "synchronous", "NORMAL")?;
406 conn.pragma_update(None, "foreign_keys", "ON")?;
409 Ok(())
410 }
411
412 fn enable_compressed_journal(conn: &Connection) -> Result<()> {
413 conn.pragma_update(None, "journal_mode", "DELETE")?;
419 Ok(())
420 }
421
422 fn configure_compressed_connection(conn: &Connection) -> Result<()> {
423 conn.busy_timeout(std::time::Duration::from_secs(5))?;
424 conn.pragma_update(None, "synchronous", "FULL")?;
425 conn.pragma_update(None, "temp_store", "MEMORY")?;
426 conn.pragma_update(None, "foreign_keys", "ON")?;
427 Ok(())
428 }
429
430 #[must_use]
434 pub fn new_session(&self) -> Self {
435 Self {
436 pool: Arc::clone(&self.pool),
437 session: Arc::new(SessionState::new()),
438 }
439 }
440
441 #[must_use]
444 pub fn in_transaction(&self) -> bool {
445 let _gate = self.session.gate.read();
446 self.session.transaction.lock().is_some()
447 }
448
449 pub fn transaction_has_written(&self) -> Result<bool> {
453 let _gate = self.session.gate.read();
454 let transaction = self.session.transaction.lock();
455 let transaction = transaction
456 .as_ref()
457 .ok_or(SQLiteError::NoActiveTransaction)?;
458 Ok(matches!(
459 transaction.connection()?.transaction_state(Some("main"))?,
460 rusqlite::TransactionState::Write
461 ))
462 }
463
464 pub fn data_version_monitor_is_nonblocking(&self) -> Result<bool> {
469 if !matches!(&self.pool.spec, ConnectionSpec::Compressed { .. }) {
470 return Ok(true);
471 }
472 let _gate = self.session.gate.read();
473 Ok(self.session.transaction.lock().is_none())
474 }
475
476 #[must_use]
478 pub fn supports_concurrent_pinned_read_and_write(&self) -> bool {
479 !matches!(&self.pool.spec, ConnectionSpec::Compressed { .. })
480 }
481
482 pub fn data_version(&self) -> Result<Option<u64>> {
487 if matches!(&self.pool.spec, ConnectionSpec::Memory) {
488 return Ok(None);
489 }
490 let mut monitor = self.pool.data_version_monitor.lock();
491 if monitor.is_none() {
492 *monitor = Some(self.pool.spec.open(false)?);
493 }
494 let monitor = monitor.as_ref().ok_or_else(|| {
495 SQLiteError::StorageBackend(
496 "data-version monitor was not initialized after opening it".into(),
497 )
498 })?;
499 let version: i64 = monitor.pragma_query_value(None, "data_version", |row| row.get(0))?;
500 let version = u64::try_from(version).map_err(|_| {
501 SQLiteError::StorageBackend(format!(
502 "SQLite returned a negative PRAGMA data_version: {version}"
503 ))
504 })?;
505 Ok(Some(version))
506 }
507
508 pub fn pin_transaction_snapshot(&self) -> Result<()> {
515 self.with(|connection| {
516 if connection.is_autocommit() {
517 return Err(SQLiteError::NoActiveTransaction);
518 }
519 let _: i64 =
520 connection.query_row("SELECT COUNT(*) FROM sqlite_schema", [], |row| row.get(0))?;
521 Ok(())
522 })
523 }
524
525 pub fn with<R>(&self, f: impl FnOnce(&Connection) -> Result<R>) -> Result<R> {
529 self.surface_cleanup_failure()?;
530 let _gate = self.session.gate.read();
531 let transaction = self.session.transaction.lock();
532 if let Some(connection) = transaction.as_ref() {
533 if let Some(error) = self.session.transaction_failure.lock().as_ref() {
534 return Err(SQLiteError::TransactionAborted(error.clone()));
535 }
536 let result = f(connection.connection()?);
537 if let Err(error) = &result {
538 let mut failure = self.session.transaction_failure.lock();
539 if failure.is_none() {
540 *failure = Some(error.to_string());
541 }
542 }
543 return result;
544 }
545 drop(transaction);
546 let connection = self.pool.checkout()?;
547 f(connection.connection()?)
548 }
549
550 pub fn with_mut<R>(&self, f: impl FnOnce(&mut Connection) -> Result<R>) -> Result<R> {
551 self.surface_cleanup_failure()?;
552 let _gate = self.session.gate.read();
553 let mut transaction = self.session.transaction.lock();
554 if let Some(connection) = transaction.as_mut() {
555 if let Some(error) = self.session.transaction_failure.lock().as_ref() {
556 return Err(SQLiteError::TransactionAborted(error.clone()));
557 }
558 let result = f(connection.connection_mut()?);
559 if let Err(error) = &result {
560 let mut failure = self.session.transaction_failure.lock();
561 if failure.is_none() {
562 *failure = Some(error.to_string());
563 }
564 }
565 return result;
566 }
567 drop(transaction);
568 let mut connection = self.pool.checkout()?;
569 f(connection.connection_mut()?)
570 }
571
572 pub fn vacuum(&self) -> Result<()> {
574 self.surface_cleanup_failure()?;
575 let _gate = self.session.gate.write();
576 if self.session.transaction.lock().is_some() {
577 return Err(SQLiteError::TransactionAlreadyActive);
578 }
579 let connection = self.pool.checkout()?;
580 connection.connection()?.execute_batch("VACUUM")?;
581 Ok(())
582 }
583
584 pub fn begin_transaction(&self) -> Result<()> {
591 self.begin_transaction_with("BEGIN IMMEDIATE")
592 }
593
594 pub fn begin_deferred_transaction(&self) -> Result<()> {
599 self.begin_transaction_with("BEGIN DEFERRED")
600 }
601
602 fn begin_transaction_with(&self, statement: &str) -> Result<()> {
603 self.surface_cleanup_failure()?;
604 let _gate = self.session.gate.write();
605 let mut transaction = self.session.transaction.lock();
606 if transaction.is_some() {
607 return Err(SQLiteError::TransactionAlreadyActive);
608 }
609 let connection = self.pool.checkout()?;
610 connection.connection()?.execute_batch(statement)?;
611 self.session.transaction_failure.lock().take();
612 *transaction = Some(connection);
613 Ok(())
614 }
615
616 pub fn commit_transaction(&self) -> Result<()> {
617 self.surface_cleanup_failure()?;
618 self.finish_transaction("COMMIT")
619 }
620
621 pub fn rollback_transaction(&self) -> Result<()> {
622 self.surface_cleanup_failure()?;
623 self.finish_transaction("ROLLBACK")
624 }
625
626 pub(crate) fn rollback_transaction_on_drop(&self) {
630 if let Err(error) = self.finish_transaction("ROLLBACK") {
631 let mut failure = self.session.cleanup_failure.lock();
632 if failure.is_none() {
633 *failure = Some(error.to_string());
634 }
635 }
636 }
637
638 fn finish_transaction(&self, statement: &str) -> Result<()> {
639 let _gate = self.session.gate.write();
640 let mut transaction = self.session.transaction.lock();
641 let connection = transaction
642 .as_ref()
643 .ok_or(SQLiteError::NoActiveTransaction)?;
644 if statement == "COMMIT" {
645 let transaction_failure = self.session.transaction_failure.lock().clone();
649 if let Some(error) = transaction_failure {
650 if let Err(rollback_error) = connection.connection()?.execute_batch("ROLLBACK") {
651 transaction.take();
652 self.session.transaction_failure.lock().take();
653 return Err(SQLiteError::SQLite(rollback_error));
654 }
655 transaction.take();
656 self.session.transaction_failure.lock().take();
657 return Err(SQLiteError::TransactionAborted(error));
658 }
659 }
660 if let Err(error) = connection.connection()?.execute_batch(statement) {
661 transaction.take();
662 self.session.transaction_failure.lock().take();
663 return Err(SQLiteError::SQLite(error));
664 }
665 transaction.take();
666 self.session.transaction_failure.lock().take();
667 Ok(())
668 }
669
670 fn with_transaction<R>(&self, f: impl FnOnce(&Connection) -> Result<R>) -> Result<R> {
671 let _gate = self.session.gate.read();
672 let transaction = self.session.transaction.lock();
673 let connection = transaction
674 .as_ref()
675 .ok_or(SQLiteError::NoActiveTransaction)?;
676 f(connection.connection()?)
677 }
678
679 pub fn savepoint(&self, name: &str) -> Result<()> {
680 self.surface_cleanup_failure()?;
681 let stmt = format!("SAVEPOINT \"{}\"", name.replace('"', "\"\""));
682 self.with_transaction(|c| {
683 c.execute_batch(&stmt)?;
684 Ok(())
685 })
686 }
687
688 pub fn release_savepoint(&self, name: &str) -> Result<()> {
689 self.surface_cleanup_failure()?;
690 let stmt = format!("RELEASE SAVEPOINT \"{}\"", name.replace('"', "\"\""));
691 self.with_transaction(|c| {
692 c.execute_batch(&stmt)?;
693 Ok(())
694 })
695 }
696
697 pub fn rollback_to_savepoint(&self, name: &str) -> Result<()> {
698 self.surface_cleanup_failure()?;
699 let stmt = format!("ROLLBACK TO SAVEPOINT \"{}\"", name.replace('"', "\"\""));
700 self.with_transaction(|c| {
701 c.execute_batch(&stmt)?;
702 Ok(())
703 })?;
704 self.session.transaction_failure.lock().take();
705 Ok(())
706 }
707}
708
709fn default_pool_connections() -> usize {
710 std::thread::available_parallelism()
711 .map_or(MIN_POOL_CONNECTIONS, |parallelism| parallelism.get() * 2)
712 .clamp(MIN_POOL_CONNECTIONS, MAX_POOL_CONNECTIONS)
713}
714
715#[cfg(test)]
716mod tests;
717
718impl From<SQLiteError> for uqa_storage::StorageBackendError {
719 fn from(source: SQLiteError) -> Self {
720 match source {
721 SQLiteError::Memory(error) => Self::Memory(error),
722 SQLiteError::Cancelled(error) => Self::Cancelled(error),
723 source => Self::backend("SQLite", source),
724 }
725 }
726}
727
728impl From<SQLiteError> for uqa_storage::TransactionError {
729 fn from(source: SQLiteError) -> Self {
730 Self::Storage(source.into())
731 }
732}
733
734impl From<uqa_storage::StorageBackendError> for SQLiteError {
735 fn from(error: uqa_storage::StorageBackendError) -> Self {
736 use uqa_storage::StorageBackendError;
737 match error {
738 StorageBackendError::Memory(error) => Self::Memory(error),
739 StorageBackendError::Cancelled(error) => Self::Cancelled(error),
740 StorageBackendError::Analysis(error) => Self::Analysis(error),
741 StorageBackendError::Serde(error) => Self::Serde(error),
742 StorageBackendError::Backend { backend, source } => match source.downcast::<Self>() {
743 Ok(error) => *error,
744 Err(source) => Self::StorageBackend(format!("{backend} storage failed: {source}")),
745 },
746 StorageBackendError::Other(message) => Self::StorageBackend(message),
747 }
748 }
749}