use anyhow::Result;
use satoridb::SatoriDb;
#[test]
fn start_with_zero_workers_fails() {
let tmp = tempfile::tempdir().unwrap();
let result = SatoriDb::builder("test")
.workers(0)
.data_dir(tmp.path())
.build();
assert!(result.is_err(), "0 workers should fail");
let err = match result {
Ok(_) => panic!("should have error"),
Err(err) => err,
};
assert!(
err.to_string().contains("workers must be > 0"),
"error should mention workers"
);
}
#[test]
fn start_with_valid_config_succeeds() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
drop(db); Ok(())
}
#[test]
fn stats_ready_after_insert() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
let stats_before = db.stats();
assert!(
!stats_before.ready,
"router should not be ready before first insert"
);
db.insert(1, vec![1.0, 2.0])?;
let stats_after = db.stats();
assert!(
stats_after.ready,
"router should be ready after first insert"
);
Ok(())
}
#[test]
fn insert_and_query_round_trip() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![1.0, 2.0, 3.0])?;
let results = db.query(vec![1.0, 2.0, 3.0], 10)?;
assert!(!results.is_empty(), "should find the inserted vector");
assert_eq!(results[0].0, 42, "should find vector id 42");
Ok(())
}
#[test]
fn multiple_inserts_work() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(1, vec![1.0, 1.0])?;
db.insert(2, vec![1.1, 1.1])?;
db.insert(3, vec![1.2, 1.2])?;
let results = db.query(vec![1.0, 1.0], 10)?;
assert!(!results.is_empty(), "should find vectors");
Ok(())
}
#[test]
fn flush_completes() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(1, vec![1.0, 2.0])?;
db.flush()?;
Ok(())
}
#[test]
fn auto_shutdown_on_drop() -> Result<()> {
let tmp = tempfile::tempdir()?;
{
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(1, vec![1.0, 2.0])?;
}
Ok(())
}
#[test]
fn explicit_shutdown_works() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.shutdown()?;
Ok(())
}
#[test]
fn virtual_nodes_config() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(2)
.virtual_nodes(16)
.data_dir(tmp.path())
.build()?;
drop(db);
Ok(())
}
#[test]
fn query_empty_returns_error() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
let result = db.query(vec![1.0, 2.0], 10);
assert!(
result.is_err(),
"query on empty database should return error"
);
Ok(())
}
#[test]
fn insert_rejects_duplicate_id() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![1.0, 2.0, 3.0])?;
let result = db.insert(42, vec![4.0, 5.0, 6.0]);
assert!(result.is_err(), "duplicate id should be rejected");
let err = match result {
Ok(_) => panic!("should have error"),
Err(err) => err,
};
assert!(
err.to_string().contains("already exists"),
"error should mention id already exists: {}",
err
);
Ok(())
}
#[test]
fn insert_allows_different_ids() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(1, vec![1.0, 2.0])?;
db.insert(2, vec![2.0, 3.0])?;
db.insert(3, vec![3.0, 4.0])?;
let results = db.query(vec![2.0, 3.0], 10)?;
assert!(!results.is_empty(), "should find at least one vector");
Ok(())
}
#[test]
fn insert_rejects_duplicate_across_workers() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(2)
.data_dir(tmp.path())
.build()?;
db.insert(100, vec![1.0, 2.0])?;
let result = db.insert(100, vec![9.0, 9.0]);
assert!(result.is_err(), "duplicate id should be rejected");
Ok(())
}
#[test]
fn delete_allows_reinsert_of_same_id() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![1.0, 2.0, 3.0])?;
db.delete(42)?;
let result = db.insert(42, vec![4.0, 5.0, 6.0]);
assert!(result.is_ok(), "reinsert after delete should succeed");
Ok(())
}
#[test]
fn delete_nonexistent_id_succeeds() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(1, vec![1.0, 2.0])?;
let result = db.delete(9999);
assert!(result.is_ok(), "delete of non-existent id should succeed");
Ok(())
}
#[test]
fn query_does_not_find_deleted_vector() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![1.0, 2.0, 3.0])?;
let results = db.query(vec![1.0, 2.0, 3.0], 10)?;
assert!(
results.iter().any(|(id, _)| *id == 42),
"should find vector before delete"
);
db.delete(42)?;
db.insert(42, vec![9.0, 9.0, 9.0])?;
let results_after = db.query(vec![9.0, 9.0, 9.0], 10)?;
assert!(
results_after.iter().any(|(id, _)| *id == 42),
"should find reinserted vector"
);
Ok(())
}
#[test]
fn query_finds_reinserted_vector() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![0.1, 0.1, 0.1])?;
db.delete(42)?;
db.insert(42, vec![0.9, 0.9, 0.9])?;
let results = db.query(vec![0.9, 0.9, 0.9], 10)?;
assert!(
results.iter().any(|(id, _)| *id == 42),
"should find reinserted vector"
);
Ok(())
}
#[test]
fn multiple_delete_reinsert_cycles() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
for cycle in 0..5 {
let vec = vec![cycle as f32, cycle as f32 + 0.1];
db.insert(100, vec)?;
let dup_result = db.insert(100, vec![0.0, 0.0]);
assert!(
dup_result.is_err(),
"cycle {}: duplicate should be rejected",
cycle
);
db.delete(100)?;
}
db.insert(100, vec![9.9, 9.9])?;
Ok(())
}
#[test]
fn delete_reinsert_multiple_ids() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
let ids: [u64; 5] = [10, 20, 30, 40, 50];
for id in ids {
db.insert(id, vec![id as f32, id as f32])?;
}
for id in ids {
let result = db.insert(id, vec![0.0, 0.0]);
assert!(result.is_err(), "id {} should reject duplicate", id);
}
db.delete(20)?;
db.delete(40)?;
db.insert(20, vec![200.0, 200.0])?;
db.insert(40, vec![400.0, 400.0])?;
assert!(db.insert(10, vec![0.0, 0.0]).is_err());
assert!(db.insert(30, vec![0.0, 0.0]).is_err());
assert!(db.insert(50, vec![0.0, 0.0]).is_err());
assert!(db.insert(20, vec![0.0, 0.0]).is_err());
assert!(db.insert(40, vec![0.0, 0.0]).is_err());
Ok(())
}
#[test]
fn delete_cleans_vector_index() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![1.0, 2.0])?;
assert!(db.insert(42, vec![3.0, 4.0]).is_err());
db.delete(42)?;
assert!(
db.insert(42, vec![5.0, 6.0]).is_ok(),
"vector_index should be cleaned after delete"
);
Ok(())
}
#[test]
fn delete_cleans_bucket_index() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![1.0, 2.0])?;
db.delete(42)?;
db.insert(42, vec![1.0, 2.0])?;
Ok(())
}
#[test]
fn stress_rapid_delete_reinsert() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(2)
.data_dir(tmp.path())
.build()?;
for id in 1..=20u64 {
db.insert(id, vec![id as f32, id as f32])?;
}
for id in (1..=20u64).filter(|x| x % 2 == 0) {
db.delete(id)?;
}
for id in (1..=20u64).filter(|x| x % 2 == 0) {
db.insert(id, vec![id as f32 * 2.0, id as f32 * 2.0])?;
}
let results = db.query(vec![10.0, 10.0], 30)?;
assert!(results.len() >= 10, "should find many vectors");
Ok(())
}
#[test]
fn delete_with_multiple_workers() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(4)
.data_dir(tmp.path())
.build()?;
for id in 0..100u64 {
db.insert(id, vec![id as f32 * 0.1, id as f32 * 0.1])?;
}
for id in (0..100u64).filter(|x| x % 3 == 0) {
db.delete(id)?;
}
for id in (0..100u64).filter(|x| x % 3 == 0) {
db.insert(id, vec![id as f32 * 0.2, id as f32 * 0.2])?;
}
for id in 0..100u64 {
let result = db.insert(id, vec![0.0, 0.0]);
assert!(
result.is_err(),
"id {} should already exist and reject duplicate",
id
);
}
Ok(())
}
#[test]
fn delete_immediately_after_insert() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
for _ in 0..10 {
db.insert(999, vec![1.0, 1.0])?;
db.delete(999)?;
}
db.insert(999, vec![2.0, 2.0])?;
Ok(())
}
#[test]
fn delete_same_id_multiple_times() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(42, vec![1.0, 2.0])?;
db.delete(42)?;
db.delete(42)?;
db.delete(42)?;
db.insert(42, vec![3.0, 4.0])?;
Ok(())
}
#[test]
fn get_vectors_by_id() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(1, vec![1.0, 2.0, 3.0])?;
db.insert(2, vec![4.0, 5.0, 6.0])?;
let vectors = db.get(vec![1, 2, 999])?; assert_eq!(vectors.len(), 2, "should find 2 vectors");
Ok(())
}
#[test]
fn query_with_vectors_returns_data() -> Result<()> {
let tmp = tempfile::tempdir()?;
let db = SatoriDb::builder("test")
.workers(1)
.data_dir(tmp.path())
.build()?;
db.insert(1, vec![1.0, 2.0, 3.0])?;
let results = db.query_with_vectors(vec![1.0, 2.0, 3.0], 10)?;
assert!(!results.is_empty(), "should find vector");
assert_eq!(
results[0].2,
vec![1.0, 2.0, 3.0],
"should return vector data"
);
Ok(())
}