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