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(Debug, thiserror::Error)]
27pub enum StorageBackendError {
28 #[error(transparent)]
29 Memory(#[from] uqa_core::memory::MemoryError),
30 #[error(transparent)]
31 Cancelled(#[from] uqa_core::QueryCancelled),
32 #[error("text analysis failed: {0}")]
33 Analysis(#[from] uqa_analysis::AnalysisError),
34 #[error("payload serialization failed: {0}")]
35 Serde(#[from] serde_json::Error),
36 #[error("{backend} storage failed: {source}")]
37 Backend {
38 backend: &'static str,
39 #[source]
40 source: Box<dyn std::error::Error + Send + Sync>,
41 },
42 #[error("{0}")]
43 Other(String),
44}
45
46impl StorageBackendError {
47 pub fn backend(
48 backend: &'static str,
49 source: impl std::error::Error + Send + Sync + 'static,
50 ) -> Self {
51 Self::Backend {
52 backend,
53 source: Box::new(source),
54 }
55 }
56}
57
58pub type StorageBackendResult<T> = std::result::Result<T, StorageBackendError>;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct StorageSavepointId(u64);
63
64impl StorageSavepointId {
65 #[must_use]
66 pub fn allocate() -> Self {
67 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
68 let id = NEXT_ID
69 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
70 current.checked_add(1)
71 })
72 .expect("storage savepoint identity space exhausted");
73 Self(id)
74 }
75
76 pub fn backend_name(self) -> String {
77 self.0.to_string()
78 }
79}
80
81pub struct PersistentStorageSession {
87 pub catalog: Arc<dyn CatalogFacade>,
88 pub backend: Arc<dyn PersistentStorageBackend>,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Hash)]
93pub enum PersistentStorageIdentity {
94 File(PathBuf),
95 Opaque(String),
96}
97
98impl PersistentStorageIdentity {
99 pub fn for_database_path(path: &Path) -> StorageBackendResult<Self> {
101 let path = resolve_final_symlinks(path)?;
102 match std::fs::canonicalize(&path) {
103 Ok(canonical) => Ok(Self::File(canonical)),
104 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
105 let file_name = path.file_name().ok_or_else(|| {
106 StorageBackendError::Other(format!(
107 "database path `{}` has no file name",
108 path.display()
109 ))
110 })?;
111 let parent = path
112 .parent()
113 .filter(|parent| !parent.as_os_str().is_empty())
114 .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
115 let parent = std::fs::canonicalize(&parent).map_err(|error| {
116 StorageBackendError::Other(format!(
117 "canonicalize database directory `{}`: {error}",
118 parent.display()
119 ))
120 })?;
121 Ok(Self::File(parent.join(file_name)))
122 }
123 Err(error) => Err(StorageBackendError::Other(format!(
124 "canonicalize database `{}`: {error}",
125 path.display()
126 ))),
127 }
128 }
129}
130
131fn resolve_final_symlinks(path: &Path) -> StorageBackendResult<PathBuf> {
132 let mut current = path.to_path_buf();
133 let mut followed = 0usize;
134 loop {
135 match std::fs::symlink_metadata(¤t) {
136 Ok(metadata) if metadata.file_type().is_symlink() => {
137 followed += 1;
138 if followed > 40 {
139 return Err(StorageBackendError::Other(format!(
140 "database path `{}` has too many symbolic-link levels",
141 path.display()
142 )));
143 }
144 let target = std::fs::read_link(¤t).map_err(|error| {
145 StorageBackendError::Other(format!(
146 "read database symbolic link `{}`: {error}",
147 current.display()
148 ))
149 })?;
150 current = if target.is_absolute() {
151 target
152 } else {
153 current
154 .parent()
155 .filter(|parent| !parent.as_os_str().is_empty())
156 .unwrap_or_else(|| Path::new("."))
157 .join(target)
158 };
159 }
160 Ok(_) => return Ok(current),
161 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(current),
162 Err(error) => {
163 return Err(StorageBackendError::Other(format!(
164 "inspect database path `{}`: {error}",
165 current.display()
166 )))
167 }
168 }
169 }
170}
171
172impl PersistentStorageSession {
173 pub fn new(
174 catalog: Arc<dyn CatalogFacade>,
175 backend: Arc<dyn PersistentStorageBackend>,
176 ) -> Self {
177 Self { catalog, backend }
178 }
179}
180
181pub trait PersistentStorageProvider: Send + Sync {
187 fn open_session(&self) -> StorageBackendResult<PersistentStorageSession>;
188
189 fn open_initial_session(&self) -> StorageBackendResult<PersistentStorageSession> {
191 self.open_session()
192 }
193
194 fn storage_identity(&self) -> StorageBackendResult<Option<PersistentStorageIdentity>> {
196 Ok(None)
197 }
198}
199
200pub trait PersistentStorageBackend: Send + Sync {
202 fn storage_identity(&self) -> StorageBackendResult<Option<PersistentStorageIdentity>> {
204 Ok(None)
205 }
206
207 fn open_session(&self) -> StorageBackendResult<PersistentStorageSession> {
209 Err(StorageBackendError::Other(
210 "independent sessions are not implemented for this persistent backend".into(),
211 ))
212 }
213
214 fn supports_concurrent_pinned_read_and_write(&self) -> bool {
216 false
217 }
218
219 fn document_store(&self, table: &str) -> Box<dyn DocumentStore>;
220
221 fn migrate_document_storage(&self) -> StorageBackendResult<()> {
223 Ok(())
224 }
225
226 fn inverted_index(&self, table: &str, analyzer: Analyzer) -> Box<dyn InvertedIndex>;
227
228 fn migrate_inverted_index_storage(&self) -> StorageBackendResult<()> {
231 Ok(())
232 }
233
234 fn vector_index(
235 &self,
236 table: &str,
237 field: &str,
238 dimensions: u32,
239 spec: VectorIndexSpec,
240 mode: VectorIndexOpenMode,
241 ) -> StorageBackendResult<Box<dyn VectorIndex>>;
242
243 fn drop_vector_index_metadata(&self, _table: &str, _field: &str) -> StorageBackendResult<()> {
244 Ok(())
245 }
246
247 fn persists_btree_indexes(&self) -> bool {
249 false
250 }
251
252 fn load_btree_index(
255 &self,
256 _table: &str,
257 _field: &crate::ValueIndexKey,
258 ) -> StorageBackendResult<Option<Vec<(DocId, Value)>>> {
259 Ok(None)
260 }
261
262 fn btree_index_fields(&self, _table: &str) -> StorageBackendResult<Vec<crate::ValueIndexKey>> {
263 Ok(Vec::new())
264 }
265
266 fn btree_index_repairs(&self) -> StorageBackendResult<Vec<(String, crate::ValueIndexKey)>> {
270 Ok(Vec::new())
271 }
272
273 fn clear_btree_index_repair(
274 &self,
275 _table: &str,
276 _field: &crate::ValueIndexKey,
277 ) -> StorageBackendResult<()> {
278 Ok(())
279 }
280
281 fn replace_btree_index(
282 &self,
283 _table: &str,
284 _field: &crate::ValueIndexKey,
285 _values: &[(DocId, Value)],
286 ) -> StorageBackendResult<()> {
287 Ok(())
288 }
289
290 fn repair_btree_index(
294 &self,
295 table: &str,
296 field: &crate::ValueIndexKey,
297 complete: &[(DocId, Value)],
298 _stale_doc_ids: &[DocId],
299 _missing: &[(DocId, Value)],
300 ) -> StorageBackendResult<()> {
301 self.replace_btree_index(table, field, complete)
302 }
303
304 fn replace_btree_indexes(
307 &self,
308 table: &str,
309 indexes: &[(&crate::ValueIndexKey, &[(DocId, Value)])],
310 ) -> StorageBackendResult<()> {
311 for (field, values) in indexes {
312 self.replace_btree_index(table, field, values)?;
313 }
314 Ok(())
315 }
316
317 fn apply_btree_index_write(
318 &self,
319 _table: &str,
320 _doc_id: DocId,
321 _values: Option<&BTreeMap<crate::ValueIndexKey, Value>>,
322 ) -> StorageBackendResult<()> {
323 Ok(())
324 }
325
326 fn drop_btree_index(
327 &self,
328 _table: &str,
329 _field: &crate::ValueIndexKey,
330 ) -> StorageBackendResult<()> {
331 Ok(())
332 }
333
334 fn clear_btree_indexes(&self, _table: &str) -> StorageBackendResult<()> {
335 Ok(())
336 }
337
338 fn vacuum(&self) -> StorageBackendResult<()> {
340 Ok(())
341 }
342
343 fn begin_transaction(&self) -> StorageBackendResult<()>;
344
345 fn begin_read_transaction(&self) -> StorageBackendResult<()> {
349 self.begin_transaction()
350 }
351
352 fn begin_upgradeable_transaction(&self) -> StorageBackendResult<()> {
354 self.begin_transaction()
355 }
356
357 fn in_transaction(&self) -> bool;
359
360 fn transaction_has_written(&self) -> StorageBackendResult<bool>;
365
366 fn change_version(&self) -> StorageBackendResult<Option<u64>> {
369 Ok(None)
370 }
371
372 fn change_version_monitor_is_nonblocking(&self) -> StorageBackendResult<bool> {
374 Ok(true)
375 }
376
377 fn pin_transaction_snapshot(&self) -> StorageBackendResult<()> {
379 Ok(())
380 }
381
382 fn commit_transaction(&self) -> StorageBackendResult<()>;
383
384 fn rollback_transaction(&self) -> StorageBackendResult<()>;
385
386 fn savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
387
388 fn release_savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
389
390 fn rollback_to_savepoint(&self, id: StorageSavepointId) -> StorageBackendResult<()>;
391}
392
393#[cfg(test)]
394mod identity_tests {
395 use super::*;
396
397 #[cfg(unix)]
398 #[test]
399 fn dangling_database_symlink_keeps_the_target_identity_after_creation() {
400 use std::os::unix::fs::symlink;
401
402 let directory = tempfile::tempdir().unwrap();
403 let target = directory.path().join("target.db");
404 let link = directory.path().join("database.db");
405 symlink("target.db", &link).unwrap();
406
407 let before = PersistentStorageIdentity::for_database_path(&link).unwrap();
408 std::fs::File::create(&target).unwrap();
409 let after = PersistentStorageIdentity::for_database_path(&link).unwrap();
410
411 assert_eq!(before, after);
412 assert_eq!(
413 after,
414 PersistentStorageIdentity::File(target.canonicalize().unwrap())
415 );
416 }
417}