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 read_btree_index_entry(
515 &self,
516 _table: &str,
517 _field: &crate::ValueIndexKey,
518 _doc_id: DocId,
519 ) -> StorageBackendResult<crate::ValueIndexEntry> {
520 if self.persists_btree_indexes() {
521 return Err(crate::StorageBackendError::Other(
522 "point reads of stored B-tree entries are not implemented for this backend".into(),
523 ));
524 }
525 Ok(crate::ValueIndexEntry::Unbuilt)
526 }
527
528 fn btree_index_repairs(&self) -> StorageBackendResult<Vec<(String, crate::ValueIndexKey)>> {
532 Ok(Vec::new())
533 }
534
535 fn clear_btree_index_repair(
536 &self,
537 _table: &str,
538 _field: &crate::ValueIndexKey,
539 ) -> StorageBackendResult<()> {
540 Ok(())
541 }
542
543 fn replace_btree_index(
544 &self,
545 _table: &str,
546 _field: &crate::ValueIndexKey,
547 _values: &[(DocId, Value)],
548 ) -> StorageBackendResult<()> {
549 Ok(())
550 }
551
552 fn repair_btree_index(
556 &self,
557 table: &str,
558 field: &crate::ValueIndexKey,
559 complete: &[(DocId, Value)],
560 _stale_doc_ids: &[DocId],
561 _missing: &[(DocId, Value)],
562 ) -> StorageBackendResult<()> {
563 self.replace_btree_index(table, field, complete)
564 }
565
566 fn replace_btree_indexes(
569 &self,
570 table: &str,
571 indexes: &[(&crate::ValueIndexKey, &[(DocId, Value)])],
572 ) -> StorageBackendResult<()> {
573 for (field, values) in indexes {
574 self.replace_btree_index(table, field, values)?;
575 }
576 Ok(())
577 }
578
579 fn apply_btree_index_write(
580 &self,
581 _table: &str,
582 _doc_id: DocId,
583 _values: Option<&BTreeMap<crate::ValueIndexKey, Value>>,
584 ) -> StorageBackendResult<()> {
585 Ok(())
586 }
587
588 fn apply_unused_btree_index_write(
590 &self,
591 table: &str,
592 doc_id: DocId,
593 values: &BTreeMap<crate::ValueIndexKey, Value>,
594 namespace: crate::document_store::identifiers::DocumentIdNamespace,
595 ) -> StorageBackendResult<()> {
596 let _ = namespace;
597 self.apply_btree_index_write(table, doc_id, Some(values))
598 }
599
600 fn drop_btree_index(
601 &self,
602 _table: &str,
603 _field: &crate::ValueIndexKey,
604 ) -> StorageBackendResult<()> {
605 Ok(())
606 }
607
608 fn clear_btree_indexes(&self, _table: &str) -> StorageBackendResult<()> {
609 Ok(())
610 }
611
612 fn reclaim_obsolete(&self) -> StorageBackendResult<()> {
614 if self.transaction_model().is_versioned() {
615 return Err(StorageBackendError::Other(
616 "versioned storage reclamation is not implemented by this backend".into(),
617 ));
618 }
619 Ok(())
620 }
621
622 fn vacuum(&self) -> StorageBackendResult<()> {
624 Ok(())
625 }
626
627 fn begin_transaction(&self) -> StorageBackendResult<()>;
628
629 fn begin_read_transaction(&self) -> StorageBackendResult<()> {
633 self.begin_transaction()
634 }
635
636 fn begin_upgradeable_transaction(&self) -> StorageBackendResult<()> {
638 self.begin_transaction()
639 }
640
641 fn refresh_transaction_snapshot(
643 &self,
644 _cancellation: &uqa_core::CancellationToken,
645 ) -> StorageBackendResult<()> {
646 Err(StorageBackendError::Other(
647 "transaction snapshot refresh is not supported by this backend".into(),
648 ))
649 }
650
651 fn in_transaction(&self) -> bool;
653
654 fn transaction_has_written(&self) -> StorageBackendResult<bool>;
656
657 fn change_version(&self) -> StorageBackendResult<Option<u64>> {
660 Ok(None)
661 }
662
663 fn read_view_revision(
665 &self,
666 ) -> StorageBackendResult<Option<crate::key_value::KeyValueReadRevision>> {
667 Ok(None)
668 }
669
670 fn committed_data_revision(&self) -> StorageBackendResult<Option<CommittedDataRevision>> {
672 Ok(None)
673 }
674
675 fn commit_monitor_version(&self) -> StorageBackendResult<Option<u64>> {
677 Ok(None)
678 }
679
680 fn change_version_monitor_is_nonblocking(&self) -> StorageBackendResult<bool> {
682 Ok(true)
683 }
684
685 fn pin_transaction_snapshot(&self) -> StorageBackendResult<()> {
687 Ok(())
688 }
689
690 fn commit_transaction(&self) -> StorageBackendResult<()>;
691
692 fn rollback_transaction(&self) -> StorageBackendResult<()>;
693
694 fn savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
695
696 fn release_savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
697
698 fn rollback_to_savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
699}
700
701#[cfg(all(test, unix))]
702mod identity_tests {
703 use super::*;
704
705 #[test]
706 fn dangling_database_symlink_keeps_the_target_identity_after_creation() {
707 use std::os::unix::fs::symlink;
708
709 let directory = tempfile::tempdir().unwrap();
710 let target = directory.path().join("target.db");
711 let link = directory.path().join("database.db");
712 symlink("target.db", &link).unwrap();
713
714 let before = PersistentStorageIdentity::for_database_path(&link).unwrap();
715 std::fs::File::create(&target).unwrap();
716 let after = PersistentStorageIdentity::for_database_path(&link).unwrap();
717
718 assert_eq!(before, after);
719 assert_eq!(
720 after,
721 PersistentStorageIdentity::File(target.canonicalize().unwrap())
722 );
723 }
724}