use std::path::Path;
use crate::{
error::MemoryError,
types::{Scope, ScopeFilter},
};
pub mod in_memory;
pub mod usearch;
pub use in_memory::InMemoryStore;
pub use usearch::UsearchStore;
pub(crate) mod sealed {
pub trait Sealed {}
}
pub trait VectorStore: Send + Sync + sealed::Sealed {
fn add(
&self,
scope: &Scope,
vector: &[f32],
qualified_name: String,
) -> Result<u64, MemoryError>;
fn remove(&self, scope: &Scope, qualified_name: &str) -> Result<(), MemoryError>;
fn search(
&self,
filter: &ScopeFilter,
query: &[f32],
limit: usize,
) -> Result<Vec<(u64, String, f32)>, MemoryError>;
fn find_by_name(&self, qualified_name: &str) -> Option<u64>;
fn save(&self, dir: &Path) -> Result<(), MemoryError>;
fn is_ready(&self) -> bool;
fn dimensions(&self) -> usize;
fn commit_sha(&self) -> Option<String>;
fn set_commit_sha(&self, sha: Option<&str>);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::index::{InMemoryStore, UsearchStore};
fn vec_a() -> Vec<f32> {
vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
}
fn vec_b() -> Vec<f32> {
vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
}
fn check_contract(store: &dyn VectorStore) {
let scope = Scope::Global;
let name = "global/contract-test".to_string();
store
.add(&scope, &vec_a(), name.clone())
.expect("add should succeed");
assert!(
store.find_by_name(&name).is_some(),
"TC-02a: find_by_name should return Some after add"
);
let results = store
.search(&ScopeFilter::GlobalOnly, &vec_a(), 5)
.expect("search should succeed");
assert!(
results.iter().any(|(_, n, _)| n == &name),
"TC-02a: search should return added entry"
);
store
.add(&scope, &vec_b(), name.clone())
.expect("upsert should succeed");
let results = store
.search(&ScopeFilter::All, &vec_b(), 10)
.expect("search after upsert should succeed");
assert_eq!(
results.iter().filter(|(_, n, _)| n == &name).count(),
1,
"TC-02c: upsert should leave exactly one entry"
);
store.remove(&scope, &name).expect("remove should succeed");
assert!(
store.find_by_name(&name).is_none(),
"TC-02b: find_by_name should return None after remove"
);
let results_after = store
.search(&ScopeFilter::GlobalOnly, &vec_a(), 5)
.expect("search after remove should succeed");
assert!(
!results_after.iter().any(|(_, n, _)| n == &name),
"TC-02b: search should not return removed entry"
);
let proj_scope = Scope::Project("testproj".to_string());
store
.add(
&Scope::Global,
&vec_a(),
"global/contract-global".to_string(),
)
.expect("re-add global entry for TC-02d");
store
.add(
&proj_scope,
&vec_b(),
"projects/testproj/contract-proj".to_string(),
)
.expect("add project entry should succeed");
let pag_results = store
.search(
&ScopeFilter::ProjectAndGlobal("testproj".to_string()),
&vec_a(),
10,
)
.expect("ProjectAndGlobal search should succeed");
let pag_names: Vec<&str> = pag_results.iter().map(|(_, n, _)| n.as_str()).collect();
assert!(
pag_names.contains(&"projects/testproj/contract-proj"),
"TC-02d: ProjectAndGlobal should include project entries"
);
assert!(
pag_names.contains(&"global/contract-global"),
"TC-02d: ProjectAndGlobal should include global entries"
);
store
.remove(&proj_scope, "projects/testproj/contract-proj")
.expect("remove project entry");
store
.remove(&Scope::Global, "global/contract-global")
.expect("remove global entry");
assert!(
store.is_ready(),
"TC-06: is_ready() should return true for a functioning store"
);
assert_eq!(
store.dimensions(),
8,
"dimensions() should return 8 (the value passed to new)"
);
assert!(
store.commit_sha().is_none(),
"commit_sha() should be None on a fresh store"
);
store.set_commit_sha(Some("deadbeef"));
assert_eq!(
store.commit_sha(),
Some("deadbeef".to_string()),
"commit_sha() should reflect set_commit_sha(Some(...))"
);
store.set_commit_sha(None);
assert!(
store.commit_sha().is_none(),
"commit_sha() should be None after set_commit_sha(None)"
);
}
#[test]
fn trait_contract_usearch_store() {
let store = UsearchStore::new(8).expect("create UsearchStore");
check_contract(&store);
}
#[test]
fn trait_contract_in_memory_store() {
let store = InMemoryStore::new(8);
check_contract(&store);
}
}