Skip to main content

ruvector_core/
storage.rs

1//! Storage layer with redb for metadata and memory-mapped vectors
2//!
3//! This module is only available when the "storage" feature is enabled.
4//! For WASM builds, use the in-memory storage backend instead.
5
6#[cfg(feature = "storage")]
7use crate::error::{Result, RuvectorError};
8#[cfg(feature = "storage")]
9use crate::types::{DbOptions, VectorEntry, VectorId};
10#[cfg(feature = "storage")]
11use bincode::config;
12#[cfg(feature = "storage")]
13use once_cell::sync::Lazy;
14#[cfg(feature = "storage")]
15use parking_lot::Mutex;
16#[cfg(feature = "storage")]
17use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};
18#[cfg(feature = "storage")]
19use serde_json;
20#[cfg(feature = "storage")]
21use std::collections::HashMap;
22#[cfg(feature = "storage")]
23use std::path::{Path, PathBuf};
24#[cfg(feature = "storage")]
25use std::sync::{Arc, Weak};
26
27#[cfg(feature = "storage")]
28const VECTORS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("vectors");
29const METADATA_TABLE: TableDefinition<&str, &str> = TableDefinition::new("metadata");
30const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
31
32/// Key used to store database configuration in CONFIG_TABLE
33const DB_CONFIG_KEY: &str = "__ruvector_db_config__";
34
35/// Per-path guard around the pooled database handle.
36///
37/// The `Weak` (rather than a strong `Arc`) is what stops an erased-and-recreated
38/// path from handing out a `Database` that still points at the unlinked inode:
39/// the pool never keeps a database alive on its own. Opening and closing a
40/// database both happen while this guard is held, so a close (redb's final write
41/// transaction, fsync, and file-lock release, all of which run in
42/// `Database::drop`) is never observed half-finished by a concurrent open.
43type PathSlot = Mutex<Option<Weak<Database>>>;
44
45// Global database connection pool to allow multiple VectorDB instances
46// to share the same underlying database file. The global lock only guards the
47// map itself; the slow work happens under the per-path slot lock so an fsync on
48// one path never blocks opens of unrelated paths.
49static DB_POOL: Lazy<Mutex<HashMap<PathBuf, Arc<PathSlot>>>> =
50    Lazy::new(|| Mutex::new(HashMap::new()));
51
52/// The pooled database and the guard that owns its lifecycle.
53struct Pooled {
54    db: Arc<Database>,
55    slot: Arc<PathSlot>,
56}
57
58/// Storage backend for vector database
59pub struct VectorStorage {
60    /// Always `Some` for a live handle; only `Drop` takes it.
61    pooled: Option<Pooled>,
62    path: PathBuf,
63    dimensions: usize,
64}
65
66impl VectorStorage {
67    /// Create or open a vector storage at the given path
68    ///
69    /// This method uses a global connection pool to allow multiple VectorDB
70    /// instances to share the same underlying database file, fixing the
71    /// "Database already open. Cannot acquire lock" error.
72    pub fn new<P: AsRef<Path>>(path: P, dimensions: usize) -> Result<Self> {
73        // SECURITY: Validate path to prevent directory traversal attacks
74        let path_ref = path.as_ref();
75
76        // Create parent directories if they don't exist (needed for canonicalize)
77        if let Some(parent) = path_ref.parent() {
78            if !parent.as_os_str().is_empty() && !parent.exists() {
79                std::fs::create_dir_all(parent).map_err(|e| {
80                    RuvectorError::InvalidPath(format!("Failed to create directory: {}", e))
81                })?;
82            }
83        }
84
85        // Convert to absolute path first, then validate
86        let path_buf = if path_ref.is_absolute() {
87            path_ref.to_path_buf()
88        } else {
89            std::env::current_dir()
90                .map_err(|e| RuvectorError::InvalidPath(format!("Failed to get cwd: {}", e)))?
91                .join(path_ref)
92        };
93
94        // SECURITY: Check for path traversal attempts (e.g., "../../../etc/passwd")
95        // Only reject paths that contain ".." components trying to escape
96        let path_str = path_ref.to_string_lossy();
97        if path_str.contains("..") {
98            // Verify the resolved path doesn't escape intended boundaries
99            // For absolute paths, we allow them as-is (user explicitly specified)
100            // For relative paths with "..", check they don't escape cwd
101            if !path_ref.is_absolute() {
102                if let Ok(cwd) = std::env::current_dir() {
103                    // Normalize the path by resolving .. components
104                    let mut normalized = cwd.clone();
105                    for component in path_ref.components() {
106                        match component {
107                            std::path::Component::ParentDir => {
108                                if !normalized.pop() || !normalized.starts_with(&cwd) {
109                                    return Err(RuvectorError::InvalidPath(
110                                        "Path traversal attempt detected".to_string(),
111                                    ));
112                                }
113                            }
114                            std::path::Component::Normal(c) => normalized.push(c),
115                            _ => {}
116                        }
117                    }
118                }
119            }
120        }
121
122        // Claim this path's slot. Slot handles are only ever cloned while the
123        // pool lock is held, which is what makes the reference-count check in
124        // `release_slot_if_unused` sound.
125        let slot = {
126            let mut pool = DB_POOL.lock();
127            Arc::clone(pool.entry(path_buf.clone()).or_default())
128        };
129
130        let db = match Self::open_pooled(&slot, &path_buf) {
131            Ok(db) => db,
132            Err(e) => {
133                // Nothing was pooled, so this slot may now be garbage.
134                Self::release_slot_if_unused(&path_buf, slot);
135                return Err(e);
136            }
137        };
138
139        Ok(Self {
140            pooled: Some(Pooled { db, slot }),
141            path: path_buf,
142            dimensions,
143        })
144    }
145
146    /// Reuse this path's pooled database, or open a fresh one under its guard.
147    fn open_pooled(slot: &Arc<PathSlot>, path: &Path) -> Result<Arc<Database>> {
148        let mut guard = slot.lock();
149
150        if let Some(existing_db) = guard.as_ref().and_then(Weak::upgrade) {
151            // Reuse existing database connection
152            return Ok(existing_db);
153        }
154
155        // Create new database and publish it to the pool. On any failure below,
156        // `new_db` is dropped before `guard`, so the file lock is released while
157        // the slot is still held.
158        let new_db = Arc::new(Database::create(path)?);
159
160        // Initialize tables
161        let write_txn = new_db.begin_write()?;
162        {
163            let _ = write_txn.open_table(VECTORS_TABLE)?;
164            let _ = write_txn.open_table(METADATA_TABLE)?;
165            let _ = write_txn.open_table(CONFIG_TABLE)?;
166        }
167        write_txn.commit()?;
168
169        *guard = Some(Arc::downgrade(&new_db));
170        Ok(new_db)
171    }
172
173    /// Drop this path's pool entry once nothing can reach it any more.
174    ///
175    /// Leaving the entry in place would leak one map entry per opened path.
176    /// `slot` is consumed and released *under* the pool lock, so a remaining
177    /// strong count of one proves the pool's own entry is the last handle: new
178    /// handles are only ever cloned under this same lock, so no other thread can
179    /// be mid-`new` for this path.
180    fn release_slot_if_unused(path: &Path, slot: Arc<PathSlot>) {
181        let mut pool = DB_POOL.lock();
182        let is_ours = matches!(pool.get(path), Some(pooled) if Arc::ptr_eq(pooled, &slot));
183        drop(slot);
184
185        if is_ours && matches!(pool.get(path), Some(pooled) if Arc::strong_count(pooled) == 1) {
186            pool.remove(path);
187        }
188    }
189
190    /// Borrow the pooled database.
191    ///
192    /// Handing out `&Database` rather than the `Arc` keeps the strong count
193    /// under this handle's control, which `Drop` relies on to decide whether it
194    /// is closing the last reference.
195    #[inline]
196    fn db(&self) -> &Database {
197        &self
198            .pooled
199            .as_ref()
200            .expect("database handle used after drop")
201            .db
202    }
203
204    /// Insert a vector entry
205    pub fn insert(&self, entry: &VectorEntry) -> Result<VectorId> {
206        if entry.vector.len() != self.dimensions {
207            return Err(RuvectorError::DimensionMismatch {
208                expected: self.dimensions,
209                actual: entry.vector.len(),
210            });
211        }
212
213        let id = entry
214            .id
215            .clone()
216            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
217
218        let write_txn = self.db().begin_write()?;
219        {
220            let mut table = write_txn.open_table(VECTORS_TABLE)?;
221
222            // Serialize vector data
223            let vector_data = bincode::encode_to_vec(&entry.vector, config::standard())
224                .map_err(|e| RuvectorError::SerializationError(e.to_string()))?;
225
226            table.insert(id.as_str(), vector_data.as_slice())?;
227
228            // Store metadata if present
229            if let Some(metadata) = &entry.metadata {
230                let mut meta_table = write_txn.open_table(METADATA_TABLE)?;
231                let metadata_json = serde_json::to_string(metadata)
232                    .map_err(|e| RuvectorError::SerializationError(e.to_string()))?;
233                meta_table.insert(id.as_str(), metadata_json.as_str())?;
234            }
235        }
236        write_txn.commit()?;
237
238        Ok(id)
239    }
240
241    /// Insert multiple vectors in a batch
242    pub fn insert_batch(&self, entries: &[VectorEntry]) -> Result<Vec<VectorId>> {
243        let write_txn = self.db().begin_write()?;
244        let mut ids = Vec::with_capacity(entries.len());
245
246        {
247            let mut table = write_txn.open_table(VECTORS_TABLE)?;
248            let mut meta_table = write_txn.open_table(METADATA_TABLE)?;
249
250            for entry in entries {
251                if entry.vector.len() != self.dimensions {
252                    return Err(RuvectorError::DimensionMismatch {
253                        expected: self.dimensions,
254                        actual: entry.vector.len(),
255                    });
256                }
257
258                let id = entry
259                    .id
260                    .clone()
261                    .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
262
263                // Serialize and insert vector
264                let vector_data = bincode::encode_to_vec(&entry.vector, config::standard())
265                    .map_err(|e| RuvectorError::SerializationError(e.to_string()))?;
266                table.insert(id.as_str(), vector_data.as_slice())?;
267
268                // Insert metadata if present
269                if let Some(metadata) = &entry.metadata {
270                    let metadata_json = serde_json::to_string(metadata)
271                        .map_err(|e| RuvectorError::SerializationError(e.to_string()))?;
272                    meta_table.insert(id.as_str(), metadata_json.as_str())?;
273                }
274
275                ids.push(id);
276            }
277        }
278
279        write_txn.commit()?;
280        Ok(ids)
281    }
282
283    /// Get a vector by ID
284    pub fn get(&self, id: &str) -> Result<Option<VectorEntry>> {
285        let read_txn = self.db().begin_read()?;
286        let table = read_txn.open_table(VECTORS_TABLE)?;
287
288        let Some(vector_data) = table.get(id)? else {
289            return Ok(None);
290        };
291
292        let (vector, _): (Vec<f32>, usize) =
293            bincode::decode_from_slice(vector_data.value(), config::standard())
294                .map_err(|e| RuvectorError::SerializationError(e.to_string()))?;
295
296        // Try to get metadata
297        let meta_table = read_txn.open_table(METADATA_TABLE)?;
298        let metadata = if let Some(meta_data) = meta_table.get(id)? {
299            let meta_str = meta_data.value();
300            Some(
301                serde_json::from_str(meta_str)
302                    .map_err(|e| RuvectorError::SerializationError(e.to_string()))?,
303            )
304        } else {
305            None
306        };
307
308        Ok(Some(VectorEntry {
309            id: Some(id.to_string()),
310            vector,
311            metadata,
312        }))
313    }
314
315    /// Delete a vector by ID
316    pub fn delete(&self, id: &str) -> Result<bool> {
317        let write_txn = self.db().begin_write()?;
318        let deleted;
319
320        {
321            let mut table = write_txn.open_table(VECTORS_TABLE)?;
322            deleted = table.remove(id)?.is_some();
323
324            let mut meta_table = write_txn.open_table(METADATA_TABLE)?;
325            let _ = meta_table.remove(id)?;
326        }
327
328        write_txn.commit()?;
329        Ok(deleted)
330    }
331
332    /// Get the number of vectors stored
333    pub fn len(&self) -> Result<usize> {
334        let read_txn = self.db().begin_read()?;
335        let table = read_txn.open_table(VECTORS_TABLE)?;
336        Ok(table.len()? as usize)
337    }
338
339    /// Check if storage is empty
340    pub fn is_empty(&self) -> Result<bool> {
341        Ok(self.len()? == 0)
342    }
343
344    /// Get all vector IDs
345    pub fn all_ids(&self) -> Result<Vec<VectorId>> {
346        let read_txn = self.db().begin_read()?;
347        let table = read_txn.open_table(VECTORS_TABLE)?;
348
349        let mut ids = Vec::new();
350        let iter = table.iter()?;
351        for item in iter {
352            let (key, _) = item?;
353            ids.push(key.value().to_string());
354        }
355
356        Ok(ids)
357    }
358
359    /// Save database configuration to persistent storage
360    pub fn save_config(&self, options: &DbOptions) -> Result<()> {
361        let config_json = serde_json::to_string(options)
362            .map_err(|e| RuvectorError::SerializationError(e.to_string()))?;
363
364        let write_txn = self.db().begin_write()?;
365        {
366            let mut table = write_txn.open_table(CONFIG_TABLE)?;
367            table.insert(DB_CONFIG_KEY, config_json.as_str())?;
368        }
369        write_txn.commit()?;
370
371        Ok(())
372    }
373
374    /// Load database configuration from persistent storage
375    pub fn load_config(&self) -> Result<Option<DbOptions>> {
376        let read_txn = self.db().begin_read()?;
377
378        // Try to open config table - may not exist in older databases
379        let table = match read_txn.open_table(CONFIG_TABLE) {
380            Ok(t) => t,
381            Err(_) => return Ok(None),
382        };
383
384        let Some(config_data) = table.get(DB_CONFIG_KEY)? else {
385            return Ok(None);
386        };
387
388        let config: DbOptions = serde_json::from_str(config_data.value())
389            .map_err(|e| RuvectorError::SerializationError(e.to_string()))?;
390
391        Ok(Some(config))
392    }
393
394    /// Store an arbitrary provenance value alongside the vectors themselves.
395    ///
396    /// Lives in the same file as the vectors on purpose: provenance that a
397    /// caller can delete without deleting the data it describes is not a
398    /// safety property. See [`AgenticDB::with_embedding_provider`](crate::AgenticDB::with_embedding_provider).
399    pub fn save_config_value(&self, key: &str, value: &str) -> Result<()> {
400        let write_txn = self.db().begin_write()?;
401        {
402            let mut table = write_txn.open_table(CONFIG_TABLE)?;
403            table.insert(key, value)?;
404        }
405        write_txn.commit()?;
406        Ok(())
407    }
408
409    /// Read a value written by [`save_config_value`](Self::save_config_value).
410    /// A database created before the key existed returns `None`.
411    pub fn load_config_value(&self, key: &str) -> Result<Option<String>> {
412        let read_txn = self.db().begin_read()?;
413        let table = match read_txn.open_table(CONFIG_TABLE) {
414            Ok(t) => t,
415            Err(_) => return Ok(None),
416        };
417        Ok(table.get(key)?.map(|v| v.value().to_string()))
418    }
419
420    /// Get the stored dimensions
421    pub fn dimensions(&self) -> usize {
422        self.dimensions
423    }
424}
425
426impl Drop for VectorStorage {
427    fn drop(&mut self) {
428        let Some(Pooled { db, slot }) = self.pooled.take() else {
429            return;
430        };
431
432        {
433            let mut guard = slot.lock();
434
435            // Evicting under the guard is what keeps a concurrent `new` for this
436            // path correct: `Weak::upgrade` starts failing the moment the strong
437            // count hits zero, but redb only commits, fsyncs, and unlocks the
438            // file later, inside `Database::drop`. Anyone racing us blocks on the
439            // guard instead of calling `Database::create` against a held lock.
440            if Arc::strong_count(&db) == 1 {
441                *guard = None;
442            }
443            drop(db);
444        }
445
446        Self::release_slot_if_unused(&self.path, slot);
447    }
448}
449
450// Add uuid dependency
451use uuid;
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use tempfile::tempdir;
457
458    #[test]
459    fn test_insert_and_get() -> Result<()> {
460        let dir = tempdir().unwrap();
461        let storage = VectorStorage::new(dir.path().join("test.db"), 3)?;
462
463        let entry = VectorEntry {
464            id: Some("test1".to_string()),
465            vector: vec![1.0, 2.0, 3.0],
466            metadata: None,
467        };
468
469        let id = storage.insert(&entry)?;
470        assert_eq!(id, "test1");
471
472        let retrieved = storage.get("test1")?;
473        assert!(retrieved.is_some());
474        let retrieved = retrieved.unwrap();
475        assert_eq!(retrieved.vector, vec![1.0, 2.0, 3.0]);
476
477        Ok(())
478    }
479
480    #[test]
481    fn test_batch_insert() -> Result<()> {
482        let dir = tempdir().unwrap();
483        let storage = VectorStorage::new(dir.path().join("test.db"), 3)?;
484
485        let entries = vec![
486            VectorEntry {
487                id: None,
488                vector: vec![1.0, 2.0, 3.0],
489                metadata: None,
490            },
491            VectorEntry {
492                id: None,
493                vector: vec![4.0, 5.0, 6.0],
494                metadata: None,
495            },
496        ];
497
498        let ids = storage.insert_batch(&entries)?;
499        assert_eq!(ids.len(), 2);
500        assert_eq!(storage.len()?, 2);
501
502        Ok(())
503    }
504
505    #[test]
506    fn test_delete() -> Result<()> {
507        let dir = tempdir().unwrap();
508        let storage = VectorStorage::new(dir.path().join("test.db"), 3)?;
509
510        let entry = VectorEntry {
511            id: Some("test1".to_string()),
512            vector: vec![1.0, 2.0, 3.0],
513            metadata: None,
514        };
515
516        storage.insert(&entry)?;
517        assert_eq!(storage.len()?, 1);
518
519        let deleted = storage.delete("test1")?;
520        assert!(deleted);
521        assert_eq!(storage.len()?, 0);
522
523        Ok(())
524    }
525
526    #[test]
527    fn test_multiple_instances_same_path() -> Result<()> {
528        // This test verifies the fix for the database locking bug
529        // Multiple VectorStorage instances should be able to share the same database file
530        let dir = tempdir().unwrap();
531        let db_path = dir.path().join("shared.db");
532
533        // Create first instance
534        let storage1 = VectorStorage::new(&db_path, 3)?;
535
536        // Insert data with first instance
537        storage1.insert(&VectorEntry {
538            id: Some("test1".to_string()),
539            vector: vec![1.0, 2.0, 3.0],
540            metadata: None,
541        })?;
542
543        // Create second instance with SAME path - this should NOT fail
544        let storage2 = VectorStorage::new(&db_path, 3)?;
545
546        // Both instances should see the same data
547        assert_eq!(storage1.len()?, 1);
548        assert_eq!(storage2.len()?, 1);
549
550        // Insert with second instance
551        storage2.insert(&VectorEntry {
552            id: Some("test2".to_string()),
553            vector: vec![4.0, 5.0, 6.0],
554            metadata: None,
555        })?;
556
557        // Both instances should see both records
558        assert_eq!(storage1.len()?, 2);
559        assert_eq!(storage2.len()?, 2);
560
561        // Verify data integrity
562        let retrieved1 = storage1.get("test1")?;
563        assert!(retrieved1.is_some());
564
565        let retrieved2 = storage2.get("test2")?;
566        assert!(retrieved2.is_some());
567
568        Ok(())
569    }
570
571    #[test]
572    fn reopening_after_the_last_handle_drops_preserves_data() -> Result<()> {
573        let dir = tempdir().unwrap();
574        let db_path = dir.path().join("reopen.db");
575
576        {
577            let storage = VectorStorage::new(&db_path, 3)?;
578            storage.insert(&VectorEntry {
579                id: Some("kept".to_string()),
580                vector: vec![1.0, 2.0, 3.0],
581                metadata: None,
582            })?;
583        }
584
585        // The pool entry must be reaped once the last handle is gone, otherwise
586        // long-running processes accumulate one dead entry per opened path.
587        assert!(
588            !DB_POOL.lock().contains_key(&db_path),
589            "pool kept a dead entry for {}",
590            db_path.display()
591        );
592
593        let reopened = VectorStorage::new(&db_path, 3)?;
594        assert_eq!(reopened.len()?, 1);
595        assert_eq!(
596            reopened.get("kept")?.expect("entry survived reopen").vector,
597            vec![1.0, 2.0, 3.0]
598        );
599
600        Ok(())
601    }
602
603    #[test]
604    fn concurrently_dropping_two_handles_still_reaps_the_pool_entry() {
605        use std::sync::Barrier;
606
607        let dir = tempdir().unwrap();
608        let db_path = dir.path().join("double-drop.db");
609
610        for _ in 0..50 {
611            let first = VectorStorage::new(&db_path, 3).expect("first open");
612            let second = VectorStorage::new(&db_path, 3).expect("second open");
613            let barrier = Arc::new(Barrier::new(2));
614
615            let threads: Vec<_> = [first, second]
616                .into_iter()
617                .map(|handle| {
618                    let barrier = Arc::clone(&barrier);
619                    std::thread::spawn(move || {
620                        barrier.wait();
621                        drop(handle);
622                    })
623                })
624                .collect();
625
626            for thread in threads {
627                thread.join().unwrap();
628            }
629
630            assert!(
631                !DB_POOL.lock().contains_key(&db_path),
632                "pool kept a dead entry after both handles dropped"
633            );
634        }
635    }
636
637    #[test]
638    fn concurrent_drop_and_open_never_race_the_file_lock() {
639        use std::sync::atomic::{AtomicUsize, Ordering};
640        use std::sync::Barrier;
641
642        // The unfixed pool failed this race on ~97.5% of attempts (195/200
643        // when measured), so a handful of iterations already makes a
644        // regression a near-certainty to catch: missing it 40 times running
645        // is about 0.025^40. Each iteration is a real `Database::create` plus
646        // redb's fsync-bearing drop, so this is kept small deliberately —
647        // 200 iterations cost ~26s and would dominate the crate's suite.
648        const ITERATIONS: usize = 40;
649
650        let dir = tempdir().unwrap();
651        let db_path = dir.path().join("race.db");
652        let failures = Arc::new(AtomicUsize::new(0));
653        let last_error = Arc::new(Mutex::new(None::<String>));
654
655        for _ in 0..ITERATIONS {
656            // The only live handle for this path; dropping it releases the
657            // underlying redb file lock.
658            let holder = VectorStorage::new(&db_path, 3).expect("initial open");
659            let barrier = Arc::new(Barrier::new(2));
660
661            let dropper = {
662                let barrier = Arc::clone(&barrier);
663                std::thread::spawn(move || {
664                    barrier.wait();
665                    drop(holder);
666                })
667            };
668
669            let opener = {
670                let barrier = Arc::clone(&barrier);
671                let path = db_path.clone();
672                let failures = Arc::clone(&failures);
673                let last_error = Arc::clone(&last_error);
674                std::thread::spawn(move || {
675                    barrier.wait();
676                    match VectorStorage::new(&path, 3) {
677                        Ok(storage) => drop(storage),
678                        Err(e) => {
679                            failures.fetch_add(1, Ordering::Relaxed);
680                            *last_error.lock() = Some(e.to_string());
681                        }
682                    }
683                })
684            };
685
686            dropper.join().unwrap();
687            opener.join().unwrap();
688        }
689
690        let failed = failures.load(Ordering::Relaxed);
691        assert_eq!(
692            failed,
693            0,
694            "{failed}/{ITERATIONS} concurrent opens raced the drop of the last \
695             handle; last error: {:?}",
696            last_error.lock().as_deref()
697        );
698    }
699}