Skip to main content

uqa_storage/
key_value.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Backend-neutral Key/Value storage.
8//!
9//! This module is the logical storage boundary for non-relational
10//! persistence. Concrete stores only need ordered byte keys, atomic
11//! batches, prefix scans, and transaction hooks. Catalog, document,
12//! inverted-index, and vector-index behavior stays above that boundary.
13
14use std::collections::{BTreeMap, BTreeSet};
15use std::sync::Arc;
16
17use parking_lot::Mutex;
18use serde::{Deserialize, Serialize};
19use uqa_analysis::Analyzer;
20use uqa_core::{DocId, FieldName, IndexStats, Payload, PostingEntry, PostingList, Value};
21
22use crate::backend::{PersistentStorageBackend, PersistentStorageIdentity};
23use crate::document_store::{Document, DocumentStore};
24use crate::inverted_index::{AnalyzerPhase, InvertedIndex};
25use crate::vector_index::{
26    validate_vector_values, VectorIndex, VectorIndexOpenMode, VectorIndexSpec,
27};
28use crate::{StorageBackendError, StorageBackendResult};
29
30mod catalog;
31pub use catalog::KeyValueCatalog;
32mod graph_commit;
33mod table_owners;
34pub use graph_commit::KeyValueGraphRecords;
35mod hnsw_records;
36mod index_view;
37mod ivf_records;
38mod maintenance_records;
39mod notification_records;
40pub use notification_records::KeyValueNotificationRecords;
41pub(crate) mod record_json;
42pub use maintenance_records::KeyValueMaintenanceRecords;
43mod vector_records;
44pub use hnsw_records::KeyValueHNSWRecords;
45pub use ivf_records::KeyValueIVFRecords;
46pub(crate) mod occurrence_records;
47pub use occurrence_records::KeyValueOccurrenceRecords;
48mod view;
49pub use view::{KeyValueMutation, KeyValueRead, KeyValueReadRevision, KeyValueReadScope};
50
51const TAG_METADATA: u8 = b'm';
52const TAG_TABLE: u8 = b't';
53const TAG_MODEL: u8 = b'M';
54const TAG_SCORING_PARAMS: u8 = b'S';
55const TAG_NAMED_GRAPH: u8 = b'g';
56const TAG_VERTEX: u8 = b'V';
57const TAG_EDGE: u8 = b'E';
58const TAG_GRAPH_MEMBERSHIP: u8 = b'G';
59const TAG_GRAPH_LOOKUP: u8 = b'J';
60const TAG_ANALYZER: u8 = b'a';
61const TAG_ANALYZER_DESCRIPTOR: u8 = b'D';
62const TAG_FIELD_ANALYZER_BINDING: u8 = b'U';
63const TAG_TABLE_FIELD_ANALYZER: u8 = b'A';
64const TAG_FOREIGN_SERVER: u8 = b'F';
65const TAG_FOREIGN_TABLE: u8 = b'T';
66const TAG_CATALOG_INDEX: u8 = b'C';
67const TAG_PATH_INDEX: u8 = b'P';
68const TAG_PATH_INDEX_DATA: u8 = b'Q';
69const TAG_COLUMN_STATS: u8 = b'c';
70const TAG_SCHEMA: u8 = b's';
71const TAG_SEQUENCE: u8 = b'q';
72const TAG_RELATION: u8 = b'R';
73const TAG_VIEW: u8 = b'w';
74const TAG_DOCUMENT: u8 = b'd';
75const TAG_POSTING: u8 = b'p';
76const TAG_OCCURRENCE_INDEX: u8 = b'e';
77const TAG_POSTING_CLUSTER_SCORE: u8 = b'k';
78const TAG_POSTING_CLUSTER_POSITIONS: u8 = b'o';
79const TAG_POSTING_DOCUMENT: u8 = b'x';
80const TAG_DOC_LENGTH: u8 = b'l';
81const TAG_FIELD_STATS: u8 = b'f';
82const TAG_REVERSE_POSTING: u8 = b'r';
83const TAG_VECTOR: u8 = b'v';
84const TAG_BTREE_INDEX: u8 = b'B';
85const TAG_BTREE_ENTRY: u8 = b'b';
86const TAG_NAMED_BTREE_INDEX: u8 = b'N';
87const TAG_NAMED_BTREE_ENTRY: u8 = b'n';
88const TAG_IVF_METADATA: u8 = b'I';
89const TAG_IVF_CENTROID: u8 = b'i';
90const TAG_IVF_ASSIGNMENT: u8 = b'j';
91const TAG_HNSW_METADATA: u8 = b'H';
92const TAG_HNSW_NODE: u8 = b'h';
93
94/// Prefix for the unambiguous document encoding introduced after JSON arrays
95/// became ordinary [`Value::List`] values. A legacy document is plain JSON and
96/// therefore cannot start with NUL; the prefix lets reads preserve the old
97/// `Bytes`-before-`List` interpretation without misreading newly written lists.
98const DOCUMENT_VALUE_V1_PREFIX: &[u8] = b"\0uqa-document-json-v1\0";
99const DOCUMENT_VALUE_V2_PREFIX: &[u8] = b"\0uqa-document-record-v2\0";
100
101#[derive(Debug, Clone)]
102enum KeyValueBatchOperation {
103    Put(Vec<u8>, Vec<u8>),
104    Delete(Vec<u8>),
105    DeletePrefix(Vec<u8>),
106}
107
108/// Atomic mutation buffer for a [`KeyValueStore`].
109pub trait KeyValueBatch {
110    /// Original SSI participant captured with this evaluated mutation. Wrappers must forward this capability; a batch cannot move to another participant before publication.
111    fn serializable_participant(&self) -> Option<crate::mvcc::SerializableTransactionId> {
112        None
113    }
114    /// Stage a logical write with the evaluated records. Failed application discards its new intents and records together while retaining already established dependencies. Providers without this capability reject the request.
115    fn observe_serializable_write(
116        &mut self,
117        _predicate: crate::mvcc::SerializablePredicate<'_>,
118    ) -> StorageBackendResult<()> {
119        Err(StorageBackendError::Other(
120            "atomic serializable write observations are not supported".into(),
121        ))
122    }
123    /// Require this record's original committed revision at publication without replacing it. This permits independent data writers to share a definition. Stores without commit-time read validation reject this operation; capable wrappers must forward it.
124    fn require_unchanged(&mut self, _key: &[u8]) -> StorageBackendResult<()> {
125        Err(StorageBackendError::Other(
126            "commit-time record requirements are not supported".into(),
127        ))
128    }
129    /// Publish a new revision of an immutable marker, merging concurrent touches of the same value. Structural changes fence the marker and replace their definition; data changes require the definition and touch the marker. The payload must contain only immutable owner/format data. Capable wrappers must forward this operation.
130    fn touch_marker(&mut self, _key: &[u8], _value: &[u8]) -> StorageBackendResult<()> {
131        Err(StorageBackendError::Other(
132            "mergeable revision markers are not supported".into(),
133        ))
134    }
135    /// Stage a durable identifier observation before publishing this batch's records. Successful observations survive later transaction/savepoint rollback. Dropping an unevaluated batch consumes nothing. Stores without autonomous allocation reject this operation; capable wrappers must forward it.
136    fn observe_identifier(&mut self, _namespace: &[u8], _value: u64) -> StorageBackendResult<()> {
137        Err(StorageBackendError::Other(
138            "durable identifier observations are not supported".into(),
139        ))
140    }
141    /// Carry the source's durable identifier watermark into the target before this batch publishes its records. Successful watermark updates survive undo, just like observations. Owning lifecycle code must coordinate the accompanying definition change with data writers. Capable wrappers must forward this namespace operation.
142    fn inherit_identifiers(&mut self, _from: &[u8], _to: &[u8]) -> StorageBackendResult<()> {
143        Err(StorageBackendError::Other(
144            "durable identifier inheritance is not supported".into(),
145        ))
146    }
147    fn put(&mut self, key: &[u8], value: &[u8]) -> StorageBackendResult<()>;
148    fn delete(&mut self, key: &[u8]) -> StorageBackendResult<()>;
149    fn delete_prefix(&mut self, prefix: &[u8]) -> StorageBackendResult<()>;
150    /// Retain already evaluated document input in the same atomic batch as its canonical values and IVF preview. Concurrent wrappers must forward this call.
151    fn ivf_mutation(
152        &mut self,
153        _metadata: &[u8],
154        _mutation: crate::ivf_index::IVFMutation<'_>,
155    ) -> StorageBackendResult<()> {
156        Ok(())
157    }
158    /// The immutable preview serves private reads; concurrent commit preparation may replace it with an internally recalculated generation.
159    fn preview_ivf_record(&mut self, key: &[u8], value: Option<&[u8]>) -> StorageBackendResult<()> {
160        match value {
161            Some(value) => self.put(key, value),
162            None => self.delete(key),
163        }
164    }
165    fn preview_ivf_prefix(&mut self, prefix: &[u8]) -> StorageBackendResult<()> {
166        self.delete_prefix(prefix)
167    }
168    /// Fence each existing IVF definition under this metadata prefix, including definitions with no vectors. Serialized stores already exclude competing writers.
169    fn fence_ivf_prefix(&mut self, _prefix: &[u8]) -> StorageBackendResult<()> {
170        Ok(())
171    }
172    /// Retain already evaluated document input in the same atomic batch as its canonical values and HNSW preview. Concurrent wrappers must forward this call.
173    fn hnsw_mutation(
174        &mut self,
175        _metadata: &[u8],
176        _mutation: crate::hnsw_index::HNSWMutation<'_>,
177    ) -> StorageBackendResult<()> {
178        Ok(())
179    }
180    /// The immutable preview serves private reads; concurrent commit preparation may replace it with an internally recalculated generation.
181    fn preview_hnsw_record(
182        &mut self,
183        key: &[u8],
184        value: Option<&[u8]>,
185    ) -> StorageBackendResult<()> {
186        match value {
187            Some(value) => self.put(key, value),
188            None => self.delete(key),
189        }
190    }
191    fn preview_hnsw_prefix(&mut self, prefix: &[u8]) -> StorageBackendResult<()> {
192        self.delete_prefix(prefix)
193    }
194    /// Fence each existing HNSW definition under this metadata prefix, including definitions with no vectors. Serialized stores already exclude competing writers.
195    fn fence_hnsw_prefix(&mut self, _prefix: &[u8]) -> StorageBackendResult<()> {
196        Ok(())
197    }
198    /// Stage an evaluated occurrence cluster, field total or source marker using the persistence's declared record layout. Source mutations must include their format marker and document guards. Concurrent stores merge only these explicitly typed replacements; ordinary byte writes remain conditional replacements.
199    fn replace_occurrence_record(
200        &mut self,
201        key: &[u8],
202        value: Option<&[u8]>,
203    ) -> StorageBackendResult<()> {
204        match value {
205            Some(value) => self.put(key, value),
206            None => self.delete(key),
207        }
208    }
209    /// Stage source-owned occurrence-cache invalidation. The source marker in the same batch also drives discovery of caches published after the retained source view.
210    fn invalidate_occurrence_prefix(&mut self, prefix: &[u8]) -> StorageBackendResult<()> {
211        self.delete_prefix(prefix)
212    }
213    /// Protect whole-document replacement even when two writers select different fields of an originally absent document.
214    fn occurrence_document(&mut self, _table: &str, _document: DocId) -> StorageBackendResult<()> {
215        Ok(())
216    }
217    /// Fence a structural occurrence change, including an empty rebuild, drop, purge or rename. Serialized projections already retain their native mutation boundary.
218    fn reset_occurrences(&mut self, _table: &str) -> StorageBackendResult<()> {
219        Ok(())
220    }
221    /// Retain the original precondition and current value of a record, including an absent tombstone, before later replacements. Concurrent wrappers must forward this call; serialized stores already exclude intervening writers.
222    fn fence_record(&mut self, _key: &[u8]) -> StorageBackendResult<()> {
223        Ok(())
224    }
225    /// Stage an evaluated statistics-maintenance replacement using the provider's declared layout. Keep ordinary metadata writes canonical.
226    fn replace_statistics_maintenance(
227        &mut self,
228        key: &[u8],
229        value: &[u8],
230    ) -> StorageBackendResult<()> {
231        self.put(key, value)
232    }
233    /// Record graph-cache dependencies in the same atomic batch. Concurrent MVCC stores must resolve these logical effects before admitting a commit; serialized legacy stores use the ordinary preview writes already included by the catalog.
234    fn graph_mutation(
235        &mut self,
236        _mutation: crate::mvcc::GraphMutation<'_>,
237    ) -> StorageBackendResult<()> {
238        Ok(())
239    }
240    /// Maintain read-your-writes for a graph invalidation. MVCC replaces this preview with effects selected from current graph ownership at commit.
241    fn preview_graph_invalidation(
242        &mut self,
243        key: &[u8],
244        value: Option<&[u8]>,
245    ) -> StorageBackendResult<()> {
246        match value {
247            Some(value) => self.put(key, value),
248            None => self.delete(key),
249        }
250    }
251    /// Replace or clear derived graph cache state. Publishing a completed build must also record `GraphMutation::PublishPath` so its source dependencies are validated.
252    fn replace_graph_cache(
253        &mut self,
254        key: &[u8],
255        value: Option<&[u8]>,
256    ) -> StorageBackendResult<()> {
257        match value {
258            Some(value) => self.put(key, value),
259            None => self.delete(key),
260        }
261    }
262    fn commit(self: Box<Self>) -> StorageBackendResult<()>;
263}
264
265/// Ordered byte-key storage used by Key/Value catalog and index backends.
266pub trait KeyValueStore: Send + Sync {
267    /// Credential for database-owned auxiliary files. Wrappers over an encrypted store must forward it; `None` permits unencrypted auxiliary storage.
268    fn auxiliary_encryption_key(&self) -> Option<crate::StorageEncryptionKey> {
269        None
270    }
271
272    /// Shared session allowance, including resources retained by readers after their producer finishes. Versioned wrappers must forward this capability.
273    fn retention_control(&self) -> Option<crate::read_control::StorageReadControl> {
274        None
275    }
276
277    /// Atomic auxiliary notification publication and fresh committed recovery. Session wrappers must forward this capability.
278    fn notification_publications(
279        &self,
280    ) -> Option<&dyn crate::notifications::NotificationPublicationStore> {
281        None
282    }
283
284    /// Original participant admission and logical observations. Wrappers must forward this capability, including original attribution on retained read-only sessions; capability presence does not establish complete SQL SSI support.
285    fn serializable_session(&self) -> Option<&dyn crate::mvcc::SerializableSession> {
286        None
287    }
288
289    /// Reclaim committed history outside this session's transaction. Versioned wrappers must forward maintenance; serialized stores that eagerly remove obsolete values may keep the default.
290    fn vacuum(&self) -> StorageBackendResult<()> {
291        if self.transaction_model().is_versioned() {
292            return Err(StorageBackendError::Other(
293                "versioned KeyValue maintenance is not implemented by this store".into(),
294            ));
295        }
296        Ok(())
297    }
298
299    /// Token shared with the execution that owns this session's writes. Cancellation must not prevent rollback cleanup or diagnostic reads. Versioned wrappers must forward this capability.
300    fn write_cancellation(&self) -> Option<uqa_core::CancellationToken> {
301        None
302    }
303
304    /// Create a transaction-isolated session with the caller's write cancellation and a fresh retention budget.
305    fn open_session_with_cancellation(
306        &self,
307        cancellation: &uqa_core::CancellationToken,
308    ) -> StorageBackendResult<Arc<dyn KeyValueStore>> {
309        cancellation.check()?;
310        if self.transaction_model().is_versioned() {
311            return Err(StorageBackendError::Other(
312                "cancellable independent KeyValue sessions are not implemented by this store"
313                    .into(),
314            ));
315        }
316        self.open_session()
317    }
318
319    /// Retain this exact committed/private view in a separate read-only session, including its original logical reader attribution. The returned session cannot publish records or complete the source transaction. Versioned wrappers must forward this capability; opening a newer independent snapshot is not equivalent.
320    fn open_retained_read_session(
321        &self,
322        cancellation: &uqa_core::CancellationToken,
323    ) -> StorageBackendResult<Arc<dyn KeyValueStore>> {
324        cancellation.check()?;
325        Err(StorageBackendError::Other(
326            "retained read sessions are not implemented by this KeyValue store".into(),
327        ))
328    }
329
330    /// Whether writes remain private while independent sessions read and write. Versioned stores must provide command refresh, retained reads, savepoint undo and conditional publication under one reported affinity.
331    fn transaction_model(&self) -> crate::StorageTransactionModel {
332        crate::StorageTransactionModel::ProviderSerialized
333    }
334
335    /// Durable reservations outside private record undo. Serialized custom stores may omit this capability; wrappers over a capable store must forward it.
336    fn identifier_allocator(&self) -> Option<&dyn crate::mvcc::IdentifierAllocator> {
337        None
338    }
339
340    /// Evaluate a compound read once against one fixed committed/private view. The callback must use the supplied reader and must not reenter this session. Stores without this capability reject it explicitly.
341    fn with_read_view(&self, _read: &mut KeyValueReadScope<'_>) -> StorageBackendResult<()> {
342        Err(StorageBackendError::Other(
343            "compound KeyValue reads are not supported by this store".into(),
344        ))
345    }
346
347    /// Evaluate once against the same view that supplies write preconditions, then atomically stage the batch. Errors and unwinds discard that batch; failed publication retains the evaluated attempt. The callback must not reenter this session or perform transaction control.
348    fn with_mutation(&self, _mutate: &mut KeyValueMutation<'_>) -> StorageBackendResult<()> {
349        Err(StorageBackendError::Other(
350            "atomic KeyValue evaluation is not supported by this store".into(),
351        ))
352    }
353
354    /// Identity of the transaction context shared by this store's catalog and data handles. Independent sessions must report different identities, even over the same file.
355    fn transaction_affinity(&self) -> Option<crate::StorageSessionAffinity> {
356        None
357    }
358
359    fn storage_identity(&self) -> StorageBackendResult<Option<PersistentStorageIdentity>> {
360        Ok(None)
361    }
362
363    /// Open an independent transaction session over the same logical store. The default keeps simple test/custom stores source-compatible while making the missing MVCC capability explicit when a persistent engine needs a committed reader alongside a pinned statement snapshot.
364    fn open_session(&self) -> StorageBackendResult<Arc<dyn KeyValueStore>> {
365        Err(StorageBackendError::Other(
366            "independent sessions are not implemented for this KeyValue store".into(),
367        ))
368    }
369
370    fn get(&self, key: &[u8]) -> StorageBackendResult<Option<Vec<u8>>>;
371    /// Visit one borrowed value under the retained read. Providers reserve any temporary encoded payload before fetching it; callbacks must not reenter the store.
372    fn visit_value(
373        &self,
374        _key: &[u8],
375        control: &crate::read_control::StorageReadControl,
376        _visit: &mut crate::read_control::ValueReadVisitor<'_>,
377    ) -> StorageBackendResult<()> {
378        control.check()?;
379        Err(StorageBackendError::Other(
380            "controlled value reads are not supported by this KeyValue store".into(),
381        ))
382    }
383    /// Visit at most `limit` entries in key order, strictly after `after` when supplied. Encoded values remain borrowed from their provider owner and callbacks must not reenter the store.
384    fn visit_prefix_after(
385        &self,
386        _prefix: &[u8],
387        _after: Option<&[u8]>,
388        _limit: usize,
389        control: &crate::read_control::StorageReadControl,
390        _visit: &mut crate::read_control::KeyValueReadVisitor<'_>,
391    ) -> StorageBackendResult<()> {
392        control.check()?;
393        Err(StorageBackendError::Other(
394            "controlled prefix reads are not supported by this KeyValue store".into(),
395        ))
396    }
397    /// Test prefix existence under the read allowance. Providers that materialize values should implement a key-only probe.
398    fn contains_prefix_budgeted(
399        &self,
400        prefix: &[u8],
401        control: &crate::read_control::StorageReadControl,
402    ) -> StorageBackendResult<bool> {
403        let mut found = false;
404        self.visit_prefix_after(prefix, None, 1, control, &mut |_, _| {
405            found = true;
406            Ok(())
407        })?;
408        control.check()?;
409        Ok(found)
410    }
411
412    fn contains_key(&self, key: &[u8]) -> StorageBackendResult<bool> {
413        self.get(key).map(|value| value.is_some())
414    }
415    fn put(&self, key: &[u8], value: &[u8]) -> StorageBackendResult<()>;
416    fn delete(&self, key: &[u8]) -> StorageBackendResult<()>;
417    fn scan_prefix(&self, prefix: &[u8]) -> StorageBackendResult<Vec<(Vec<u8>, Vec<u8>)>>;
418    /// Return at most `limit` key/value pairs in key order, strictly after
419    /// `after` when supplied. This is the bounded value cursor used by large
420    /// format migrations and other paged consumers.
421    fn scan_prefix_after(
422        &self,
423        prefix: &[u8],
424        after: Option<&[u8]>,
425        limit: usize,
426    ) -> StorageBackendResult<Vec<(Vec<u8>, Vec<u8>)>> {
427        if limit == 0 {
428            return Ok(Vec::new());
429        }
430        Ok(self
431            .scan_prefix(prefix)?
432            .into_iter()
433            .filter(|(key, _)| after.is_none_or(|after| key.as_slice() > after))
434            .take(limit)
435            .collect())
436    }
437    /// Return at most `limit` keys in key order, strictly after `after` when
438    /// it is present. Backends should override this method with a key-only,
439    /// bounded range scan so cursor consumers neither materialize the entire
440    /// prefix nor read values they do not need on every page.
441    fn scan_prefix_keys_after(
442        &self,
443        prefix: &[u8],
444        after: Option<&[u8]>,
445        limit: usize,
446    ) -> StorageBackendResult<Vec<Vec<u8>>> {
447        if limit == 0 {
448            return Ok(Vec::new());
449        }
450        Ok(self
451            .scan_prefix(prefix)?
452            .into_iter()
453            .filter(|(key, _)| after.is_none_or(|after| key.as_slice() > after))
454            .take(limit)
455            .map(|(key, _)| key)
456            .collect())
457    }
458    fn first_prefix_after(
459        &self,
460        prefix: &[u8],
461        after: Option<&[u8]>,
462    ) -> StorageBackendResult<Option<(Vec<u8>, Vec<u8>)>> {
463        Ok(self
464            .scan_prefix(prefix)?
465            .into_iter()
466            .find(|(key, _)| after.is_none_or(|after| key.as_slice() > after)))
467    }
468    fn delete_prefix(&self, prefix: &[u8]) -> StorageBackendResult<usize>;
469    fn batch(&self) -> Box<dyn KeyValueBatch + '_>;
470
471    fn begin_transaction(&self) -> StorageBackendResult<()> {
472        Err(StorageBackendError::Other(
473            "KeyValue transaction begin is not implemented for this store".into(),
474        ))
475    }
476
477    fn begin_read_transaction(&self) -> StorageBackendResult<()> {
478        self.begin_transaction()
479    }
480
481    fn begin_upgradeable_transaction(&self) -> StorageBackendResult<()> {
482        self.begin_transaction()
483    }
484
485    fn in_transaction(&self) -> bool;
486
487    /// Report ordinary record and derived-index writes, excluding typed auxiliary notification effects. Ordinary writes remain forbidden in read-only transactions.
488    fn transaction_has_written(&self) -> StorageBackendResult<bool>;
489
490    fn change_version(&self) -> StorageBackendResult<Option<u64>> {
491        Ok(None)
492    }
493
494    fn change_version_monitor_is_nonblocking(&self) -> StorageBackendResult<bool> {
495        Ok(true)
496    }
497
498    fn pin_transaction_snapshot(&self) -> StorageBackendResult<()> {
499        Ok(())
500    }
501
502    /// Advance an active logical transaction's committed view while preserving its private writes and retained readers. The caller chooses command boundaries allowed by SQL isolation. Stores without this capability must reject the request; ending or replaying the caller's transaction is not a valid fallback.
503    fn refresh_transaction_snapshot(
504        &self,
505        _cancellation: &uqa_core::CancellationToken,
506    ) -> StorageBackendResult<()> {
507        Err(StorageBackendError::Other(
508            "transaction snapshot refresh is not supported by this KeyValue store".into(),
509        ))
510    }
511
512    fn commit_transaction(&self) -> StorageBackendResult<()> {
513        Err(StorageBackendError::Other(
514            "KeyValue transaction commit is not implemented for this store".into(),
515        ))
516    }
517
518    fn rollback_transaction(&self) -> StorageBackendResult<()> {
519        Err(StorageBackendError::Other(
520            "KeyValue transaction rollback is not implemented for this store".into(),
521        ))
522    }
523
524    fn savepoint(&self, _name: &str) -> StorageBackendResult<()> {
525        Err(StorageBackendError::Other(
526            "KeyValue savepoints are not implemented for this store".into(),
527        ))
528    }
529
530    fn release_savepoint(&self, _name: &str) -> StorageBackendResult<()> {
531        Err(StorageBackendError::Other(
532            "KeyValue savepoint release is not implemented for this store".into(),
533        ))
534    }
535
536    fn rollback_to_savepoint(&self, _name: &str) -> StorageBackendResult<()> {
537        Err(StorageBackendError::Other(
538            "KeyValue savepoint rollback is not implemented for this store".into(),
539        ))
540    }
541}
542
543mod btree_index;
544mod codec;
545pub mod conformance;
546mod document_store;
547mod hnsw_index;
548mod hnsw_persistence;
549mod index_keys;
550mod inverted_index;
551mod ivf_index;
552mod ivf_persistence;
553mod memory_store;
554pub mod occurrence_format;
555mod occurrence_keys;
556mod storage_backend;
557mod vector_index;
558
559pub use codec::prefix_upper_bound;
560pub use document_store::KeyValueDocumentStore;
561pub use hnsw_index::KeyValueHNSWIndex;
562pub use inverted_index::{KeyValueInvertedIndex, OccurrenceStorage};
563pub use ivf_index::KeyValueIVFIndex;
564pub use memory_store::MemoryKeyValueStore;
565pub use storage_backend::KeyValueStorageBackend;
566pub use vector_index::KeyValueVectorIndex;
567
568#[cfg(test)]
569mod tests;