1use std::collections::BTreeMap;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::Arc;
17
18use uqa_analysis::Analyzer;
19use uqa_core::{DocId, Value};
20
21use crate::document_store::DocumentStore;
22use crate::inverted_index::InvertedIndex;
23use crate::vector_index::{VectorIndex, VectorIndexOpenMode, VectorIndexSpec};
24use crate::CatalogFacade;
25
26#[derive(Clone)]
28pub struct CommittedDataRevision {
29 pub revision: crate::key_value::KeyValueReadRevision,
30 pub change_version: u64,
31}
32
33impl CommittedDataRevision {
34 pub fn from_receipt(receipt: crate::mvcc::CommitReceipt) -> Self {
35 Self {
36 revision: crate::key_value::KeyValueReadRevision::records(
37 receipt.transaction.database(),
38 receipt.sequence,
39 None,
40 ),
41 change_version: receipt.sequence.as_u64(),
42 }
43 }
44}
45
46#[derive(Debug, thiserror::Error)]
47pub enum StorageBackendError {
48 #[error(transparent)]
49 Memory(#[from] uqa_core::memory::MemoryError),
50 #[error(transparent)]
51 Cancelled(#[from] uqa_core::QueryCancelled),
52 #[error("text analysis failed: {0}")]
53 Analysis(#[from] uqa_analysis::AnalysisError),
54 #[error("payload serialization failed: {0}")]
55 Serde(#[from] serde_json::Error),
56 #[error("{backend} storage failed: {source}")]
57 Backend {
58 backend: &'static str,
59 #[source]
60 source: Box<dyn std::error::Error + Send + Sync>,
61 },
62 #[error("{0}")]
63 Other(String),
64}
65
66impl StorageBackendError {
67 pub fn backend(
68 backend: &'static str,
69 source: impl std::error::Error + Send + Sync + 'static,
70 ) -> Self {
71 Self::Backend {
72 backend,
73 source: Box::new(source),
74 }
75 }
76}
77
78pub type StorageBackendResult<T> = std::result::Result<T, StorageBackendError>;
79
80#[derive(Clone, Debug)]
82pub struct StorageSessionAffinity(Arc<()>);
83
84impl StorageSessionAffinity {
85 #[must_use]
86 pub fn new() -> Self {
87 Self(Arc::new(()))
88 }
89}
90
91impl Default for StorageSessionAffinity {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl PartialEq for StorageSessionAffinity {
98 fn eq(&self, other: &Self) -> bool {
99 Arc::ptr_eq(&self.0, &other.0)
100 }
101}
102
103impl Eq for StorageSessionAffinity {}
104
105#[derive(Debug, thiserror::Error)]
106#[error("catalog and data backend must share one storage transaction context")]
107pub struct StorageSessionMismatch;
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
111pub struct StorageSavepointId(u64);
112
113impl StorageSavepointId {
114 #[must_use]
115 pub fn allocate() -> Self {
116 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
117 let id = uqa_core::atomic::try_update_u64(
118 &NEXT_ID,
119 Ordering::Relaxed,
120 Ordering::Relaxed,
121 |current| current.checked_add(1),
122 )
123 .expect("storage savepoint identity space exhausted");
124 Self(id)
125 }
126
127 pub fn backend_name(self) -> String {
128 self.0.to_string()
129 }
130}
131
132pub struct PersistentStorageSession {
138 pub catalog: Arc<dyn CatalogFacade>,
139 pub backend: Arc<dyn PersistentStorageBackend>,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq, Hash)]
144pub enum PersistentStorageIdentity {
145 File(PathBuf),
146 Opaque(String),
147}
148
149impl PersistentStorageIdentity {
150 pub fn for_database_path(path: &Path) -> StorageBackendResult<Self> {
152 let path = resolve_final_symlinks(path)?;
153 match std::fs::canonicalize(&path) {
154 Ok(canonical) => Ok(Self::File(canonical)),
155 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
156 let file_name = path.file_name().ok_or_else(|| {
157 StorageBackendError::Other(format!(
158 "database path `{}` has no file name",
159 path.display()
160 ))
161 })?;
162 let parent = path
163 .parent()
164 .filter(|parent| !parent.as_os_str().is_empty())
165 .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
166 let parent = std::fs::canonicalize(&parent).map_err(|error| {
167 StorageBackendError::Other(format!(
168 "canonicalize database directory `{}`: {error}",
169 parent.display()
170 ))
171 })?;
172 Ok(Self::File(parent.join(file_name)))
173 }
174 Err(error) => Err(StorageBackendError::Other(format!(
175 "canonicalize database `{}`: {error}",
176 path.display()
177 ))),
178 }
179 }
180}
181
182fn resolve_final_symlinks(path: &Path) -> StorageBackendResult<PathBuf> {
183 let mut current = path.to_path_buf();
184 let mut followed = 0usize;
185 loop {
186 match std::fs::symlink_metadata(¤t) {
187 Ok(metadata) if metadata.file_type().is_symlink() => {
188 followed += 1;
189 if followed > 40 {
190 return Err(StorageBackendError::Other(format!(
191 "database path `{}` has too many symbolic-link levels",
192 path.display()
193 )));
194 }
195 let target = std::fs::read_link(¤t).map_err(|error| {
196 StorageBackendError::Other(format!(
197 "read database symbolic link `{}`: {error}",
198 current.display()
199 ))
200 })?;
201 current = if target.is_absolute() {
202 target
203 } else {
204 current
205 .parent()
206 .filter(|parent| !parent.as_os_str().is_empty())
207 .unwrap_or_else(|| Path::new("."))
208 .join(target)
209 };
210 }
211 Ok(_) => return Ok(current),
212 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(current),
213 Err(error) => {
214 return Err(StorageBackendError::Other(format!(
215 "inspect database path `{}`: {error}",
216 current.display()
217 )))
218 }
219 }
220 }
221}
222
223#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
225pub enum StorageTransactionModel {
226 #[default]
227 ProviderSerialized,
228 VersionedConcurrent { database: crate::mvcc::DatabaseId },
230}
231
232impl StorageTransactionModel {
233 pub fn is_versioned(self) -> bool {
234 matches!(self, Self::VersionedConcurrent { .. })
235 }
236}
237
238impl PersistentStorageSession {
239 pub fn new(
240 catalog: Arc<dyn CatalogFacade>,
241 backend: Arc<dyn PersistentStorageBackend>,
242 ) -> Self {
243 Self { catalog, backend }
244 }
245
246 pub fn validate_transaction_affinity(&self) -> StorageBackendResult<()> {
248 let affinity = self.backend.transaction_affinity();
249 let model = self.backend.transaction_model();
250 if self.catalog.transaction_affinity() != affinity
251 || self.catalog.transaction_model() != model
252 || (model.is_versioned() && affinity.is_none())
253 {
254 return Err(StorageBackendError::backend(
255 "session",
256 StorageSessionMismatch,
257 ));
258 }
259 if model.is_versioned() && self.backend.write_cancellation().is_none() {
260 return Err(StorageBackendError::Other(
261 "versioned storage backend does not expose write cancellation".into(),
262 ));
263 }
264 Ok(())
265 }
266}
267
268pub trait PersistentStorageProvider: Send + Sync {
274 fn open_session(&self) -> StorageBackendResult<PersistentStorageSession>;
275
276 fn open_session_with_cancellation(
278 &self,
279 cancellation: &uqa_core::CancellationToken,
280 ) -> StorageBackendResult<PersistentStorageSession> {
281 cancellation.check()?;
282 let session = self.open_session()?;
283 if session.backend.transaction_model().is_versioned() {
284 return Err(StorageBackendError::Other(
285 "cancellable independent storage sessions are not implemented by this provider"
286 .into(),
287 ));
288 }
289 Ok(session)
290 }
291
292 fn open_initial_session(&self) -> StorageBackendResult<PersistentStorageSession> {
294 self.open_session()
295 }
296
297 fn storage_identity(&self) -> StorageBackendResult<Option<PersistentStorageIdentity>> {
299 Ok(None)
300 }
301
302 fn auxiliary_encryption_key(&self) -> Option<crate::StorageEncryptionKey> {
306 None
307 }
308}
309
310pub trait PersistentStorageBackend: Send + Sync {
312 fn require_sequence_value_durability(&self) -> StorageBackendResult<()> {
314 Ok(())
315 }
316
317 fn open_sequence_value_session(
319 &self,
320 cancellation: &uqa_core::CancellationToken,
321 ) -> StorageBackendResult<PersistentStorageSession> {
322 self.open_session_with_cancellation(cancellation)
323 }
324
325 fn retention_control(&self) -> Option<crate::read_control::StorageReadControl> {
327 None
328 }
329
330 fn notification_publications(
332 &self,
333 ) -> Option<&dyn crate::notifications::NotificationPublicationStore> {
334 None
335 }
336
337 fn serializable_session(&self) -> Option<&dyn crate::mvcc::SerializableSession> {
339 None
340 }
341
342 fn write_cancellation(&self) -> Option<uqa_core::CancellationToken> {
344 None
345 }
346
347 fn open_session_with_cancellation(
349 &self,
350 cancellation: &uqa_core::CancellationToken,
351 ) -> StorageBackendResult<PersistentStorageSession> {
352 cancellation.check()?;
353 if self.transaction_model().is_versioned() {
354 return Err(StorageBackendError::Other(
355 "cancellable independent storage sessions are not implemented by this backend"
356 .into(),
357 ));
358 }
359 self.open_session()
360 }
361
362 fn open_controlled_session(
364 &self,
365 _control: &crate::read_control::StorageReadControl,
366 ) -> StorageBackendResult<PersistentStorageSession> {
367 Err(StorageBackendError::Other(
368 "controlled independent sessions are not implemented by this backend".into(),
369 ))
370 }
371
372 fn open_retained_read_session(
374 &self,
375 cancellation: &uqa_core::CancellationToken,
376 ) -> StorageBackendResult<PersistentStorageSession> {
377 cancellation.check()?;
378 Err(StorageBackendError::Other(
379 "retained read sessions are not implemented by this storage backend".into(),
380 ))
381 }
382
383 fn transaction_model(&self) -> StorageTransactionModel {
385 StorageTransactionModel::ProviderSerialized
386 }
387
388 fn identifier_allocator(&self) -> Option<&dyn crate::mvcc::IdentifierAllocator> {
390 None
391 }
392
393 fn transaction_affinity(&self) -> Option<StorageSessionAffinity> {
395 None
396 }
397
398 fn storage_identity(&self) -> StorageBackendResult<Option<PersistentStorageIdentity>> {
400 Ok(None)
401 }
402
403 fn auxiliary_encryption_key(&self) -> Option<crate::StorageEncryptionKey> {
406 None
407 }
408
409 fn open_session(&self) -> StorageBackendResult<PersistentStorageSession> {
411 Err(StorageBackendError::Other(
412 "independent sessions are not implemented for this persistent backend".into(),
413 ))
414 }
415
416 fn supports_concurrent_pinned_read_and_write(&self) -> bool {
418 false
419 }
420
421 fn document_store(&self, table: &str) -> Box<dyn DocumentStore>;
422
423 fn migrate_document_storage(&self) -> StorageBackendResult<()> {
425 Ok(())
426 }
427
428 fn inverted_index(&self, table: &str, analyzer: Analyzer) -> Box<dyn InvertedIndex>;
429
430 fn migrate_inverted_index_storage(&self) -> StorageBackendResult<()> {
433 Ok(())
434 }
435
436 fn vector_index(
437 &self,
438 table: &str,
439 field: &str,
440 dimensions: u32,
441 spec: VectorIndexSpec,
442 mode: VectorIndexOpenMode,
443 ) -> StorageBackendResult<Box<dyn VectorIndex>>;
444
445 fn diskann_index(
447 &self,
448 _binding: crate::diskann_index::DiskANNIndexBinding<'_>,
449 _options: crate::diskann_index::DiskANNIndexOptions,
450 _temporary: &crate::diskann_index::build::DiskANNTemporaryBudget,
451 _mode: VectorIndexOpenMode,
452 ) -> StorageBackendResult<Box<dyn VectorIndex>> {
453 Err(StorageBackendError::Other(
454 "this backend does not support catalog-bound DiskANN indexes".into(),
455 ))
456 }
457
458 fn diskann_journal_pruner(
460 &self,
461 _binding: crate::diskann_index::DiskANNIndexBinding<'_>,
462 _max_record_bytes: usize,
463 ) -> StorageBackendResult<Box<dyn crate::diskann_index::changes::DiskANNJournalPruner>> {
464 Err(StorageBackendError::Other(
465 "this backend does not support DiskANN journal maintenance".into(),
466 ))
467 }
468
469 fn diskann_maintenance_source(
471 &self,
472 _binding: crate::diskann_index::DiskANNIndexBinding<'_>,
473 _max_record_bytes: usize,
474 ) -> StorageBackendResult<Box<dyn crate::diskann_index::maintenance::DiskANNMaintenanceSource>>
475 {
476 Err(StorageBackendError::Other(
477 "this backend does not support DiskANN rebuild maintenance".into(),
478 ))
479 }
480
481 fn retire_diskann_index(
482 &self,
483 _binding: crate::diskann_index::DiskANNIndexBinding<'_>,
484 ) -> StorageBackendResult<()> {
485 Err(StorageBackendError::Other(
486 "this backend does not support DiskANN retirement".into(),
487 ))
488 }
489
490 fn drop_vector_index_metadata(&self, _table: &str, _field: &str) -> StorageBackendResult<()> {
491 Ok(())
492 }
493
494 fn persists_btree_indexes(&self) -> bool {
496 false
497 }
498
499 fn load_btree_index(
502 &self,
503 _table: &str,
504 _field: &crate::ValueIndexKey,
505 ) -> StorageBackendResult<Option<Vec<(DocId, Value)>>> {
506 Ok(None)
507 }
508
509 fn btree_index_fields(&self, _table: &str) -> StorageBackendResult<Vec<crate::ValueIndexKey>> {
510 Ok(Vec::new())
511 }
512
513 fn probe_btree_index_equal(
515 &self,
516 _table: &str,
517 _field: &crate::ValueIndexKey,
518 _value: &Value,
519 ) -> StorageBackendResult<Option<Vec<DocId>>> {
520 Ok(None)
521 }
522
523 fn read_btree_index_entry(
525 &self,
526 _table: &str,
527 _field: &crate::ValueIndexKey,
528 _doc_id: DocId,
529 ) -> StorageBackendResult<crate::ValueIndexEntry> {
530 if self.persists_btree_indexes() {
531 return Err(crate::StorageBackendError::Other(
532 "point reads of stored B-tree entries are not implemented for this backend".into(),
533 ));
534 }
535 Ok(crate::ValueIndexEntry::Unbuilt)
536 }
537
538 fn btree_index_repairs(&self) -> StorageBackendResult<Vec<(String, crate::ValueIndexKey)>> {
542 Ok(Vec::new())
543 }
544
545 fn clear_btree_index_repair(
546 &self,
547 _table: &str,
548 _field: &crate::ValueIndexKey,
549 ) -> StorageBackendResult<()> {
550 Ok(())
551 }
552
553 fn replace_btree_index(
554 &self,
555 _table: &str,
556 _field: &crate::ValueIndexKey,
557 _values: &[(DocId, Value)],
558 ) -> StorageBackendResult<()> {
559 Ok(())
560 }
561
562 fn repair_btree_index(
566 &self,
567 table: &str,
568 field: &crate::ValueIndexKey,
569 complete: &[(DocId, Value)],
570 _stale_doc_ids: &[DocId],
571 _missing: &[(DocId, Value)],
572 ) -> StorageBackendResult<()> {
573 self.replace_btree_index(table, field, complete)
574 }
575
576 fn replace_btree_indexes(
579 &self,
580 table: &str,
581 indexes: &[(&crate::ValueIndexKey, &[(DocId, Value)])],
582 ) -> StorageBackendResult<()> {
583 for (field, values) in indexes {
584 self.replace_btree_index(table, field, values)?;
585 }
586 Ok(())
587 }
588
589 fn apply_btree_index_write(
590 &self,
591 _table: &str,
592 _doc_id: DocId,
593 _values: Option<&BTreeMap<crate::ValueIndexKey, Value>>,
594 ) -> StorageBackendResult<()> {
595 Ok(())
596 }
597
598 fn apply_unused_btree_index_write(
600 &self,
601 table: &str,
602 doc_id: DocId,
603 values: &BTreeMap<crate::ValueIndexKey, Value>,
604 namespace: crate::document_store::identifiers::DocumentIdNamespace,
605 ) -> StorageBackendResult<()> {
606 let _ = namespace;
607 self.apply_btree_index_write(table, doc_id, Some(values))
608 }
609
610 fn drop_btree_index(
611 &self,
612 _table: &str,
613 _field: &crate::ValueIndexKey,
614 ) -> StorageBackendResult<()> {
615 Ok(())
616 }
617
618 fn clear_btree_indexes(&self, _table: &str) -> StorageBackendResult<()> {
619 Ok(())
620 }
621
622 fn reclaim_obsolete(&self) -> StorageBackendResult<()> {
624 if self.transaction_model().is_versioned() {
625 return Err(StorageBackendError::Other(
626 "versioned storage reclamation is not implemented by this backend".into(),
627 ));
628 }
629 Ok(())
630 }
631
632 fn vacuum(&self) -> StorageBackendResult<()> {
634 Ok(())
635 }
636
637 fn begin_transaction(&self) -> StorageBackendResult<()>;
638
639 fn begin_read_transaction(&self) -> StorageBackendResult<()> {
643 self.begin_transaction()
644 }
645
646 fn begin_upgradeable_transaction(&self) -> StorageBackendResult<()> {
648 self.begin_transaction()
649 }
650
651 fn refresh_transaction_snapshot(
653 &self,
654 _cancellation: &uqa_core::CancellationToken,
655 ) -> StorageBackendResult<()> {
656 Err(StorageBackendError::Other(
657 "transaction snapshot refresh is not supported by this backend".into(),
658 ))
659 }
660
661 fn in_transaction(&self) -> bool;
663
664 fn transaction_has_written(&self) -> StorageBackendResult<bool>;
666
667 fn change_version(&self) -> StorageBackendResult<Option<u64>> {
670 Ok(None)
671 }
672
673 fn read_view_revision(
675 &self,
676 ) -> StorageBackendResult<Option<crate::key_value::KeyValueReadRevision>> {
677 Ok(None)
678 }
679
680 fn committed_data_revision(&self) -> StorageBackendResult<Option<CommittedDataRevision>> {
682 Ok(None)
683 }
684
685 fn commit_monitor_version(&self) -> StorageBackendResult<Option<u64>> {
687 Ok(None)
688 }
689
690 fn change_version_monitor_is_nonblocking(&self) -> StorageBackendResult<bool> {
692 Ok(true)
693 }
694
695 fn pin_transaction_snapshot(&self) -> StorageBackendResult<()> {
697 Ok(())
698 }
699
700 fn commit_transaction(&self) -> StorageBackendResult<()>;
701
702 fn rollback_transaction(&self) -> StorageBackendResult<()>;
703
704 fn savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
705
706 fn release_savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
707
708 fn rollback_to_savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
709}
710
711#[cfg(all(test, unix))]
712mod identity_tests {
713 use super::*;
714
715 #[test]
716 fn dangling_database_symlink_keeps_the_target_identity_after_creation() {
717 use std::os::unix::fs::symlink;
718
719 let directory = tempfile::tempdir().unwrap();
720 let target = directory.path().join("target.db");
721 let link = directory.path().join("database.db");
722 symlink("target.db", &link).unwrap();
723
724 let before = PersistentStorageIdentity::for_database_path(&link).unwrap();
725 std::fs::File::create(&target).unwrap();
726 let after = PersistentStorageIdentity::for_database_path(&link).unwrap();
727
728 assert_eq!(before, after);
729 assert_eq!(
730 after,
731 PersistentStorageIdentity::File(target.canonicalize().unwrap())
732 );
733 }
734}