use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use iqdb::{DistanceMetric, Error, Iqdb, Payload, PayloadValue, Record, RecordId, Vector};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
fn tempdir() -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let n = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("iqdb-int-{nanos}-{n}"));
std::fs::create_dir_all(&dir).expect("mkdir");
dir
}
fn cleanup(path: &PathBuf) {
let _ = std::fs::remove_dir_all(path);
}
fn record(id: u64, components: Vec<f32>) -> Record {
Record::new(RecordId::new(id), Vector::new(components).unwrap())
}
#[test]
fn open_on_fresh_directory_creates_empty_database() {
let dir = tempdir();
let child = dir.join("nested");
{
let db = Iqdb::open(&child).expect("open");
assert!(db.is_empty());
assert_eq!(db.len(), 0);
}
assert!(child.is_dir());
cleanup(&dir);
}
#[test]
fn upserted_records_survive_close_and_reopen() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(1, vec![0.1, 0.2, 0.3])).unwrap();
db.upsert(record(2, vec![1.0, 0.0, 0.0])).unwrap();
db.upsert(record(3, vec![0.0, 1.0, 0.0])).unwrap();
db.close().unwrap();
}
let db = Iqdb::open(&dir).unwrap();
assert_eq!(db.len(), 3);
let hit = db.get(RecordId::new(2)).unwrap().expect("present");
assert_eq!(hit.vector().as_slice(), &[1.0, 0.0, 0.0]);
cleanup(&dir);
}
#[test]
fn deletes_survive_close_and_reopen() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(1, vec![1.0, 0.0])).unwrap();
db.upsert(record(2, vec![0.0, 1.0])).unwrap();
let _ = db.delete(RecordId::new(1)).unwrap();
db.close().unwrap();
}
let db = Iqdb::open(&dir).unwrap();
assert_eq!(db.len(), 1);
assert!(db.get(RecordId::new(1)).unwrap().is_none());
assert!(db.get(RecordId::new(2)).unwrap().is_some());
cleanup(&dir);
}
#[test]
fn payload_round_trips_through_persistence() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
let mut p = Payload::new();
let _ = p.insert("kind", "doc");
let _ = p.insert("year", 2026_i64);
let _ = p.insert("score", 0.97_f64);
let _ = p.insert("verified", true);
let _ = p.insert("blob", PayloadValue::Bytes(vec![1, 2, 3, 4]));
db.upsert(Record::with_payload(
RecordId::new(7),
Vector::new(vec![0.5, 0.5, 0.5]).unwrap(),
p,
))
.unwrap();
db.close().unwrap();
}
let db = Iqdb::open(&dir).unwrap();
let hit = db.get(RecordId::new(7)).unwrap().expect("present");
let payload = hit.payload().expect("payload preserved");
assert_eq!(
payload.get("kind").and_then(PayloadValue::as_text),
Some("doc")
);
assert_eq!(
payload.get("year").and_then(PayloadValue::as_int),
Some(2026)
);
assert!(payload
.get("score")
.and_then(PayloadValue::as_float)
.map(|f| (f - 0.97).abs() < 1e-9)
.unwrap_or(false));
assert_eq!(
payload.get("verified").and_then(PayloadValue::as_bool),
Some(true)
);
assert_eq!(
payload
.get("blob")
.and_then(PayloadValue::as_bytes)
.map(<[u8]>::to_vec),
Some(vec![1, 2, 3, 4])
);
cleanup(&dir);
}
#[test]
fn flush_without_close_recovers_via_wal_replay() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(1, vec![1.0, 0.0])).unwrap();
db.upsert(record(2, vec![0.0, 1.0])).unwrap();
db.flush().unwrap();
}
let db = Iqdb::open(&dir).unwrap();
assert_eq!(db.len(), 2);
assert!(db.get(RecordId::new(1)).unwrap().is_some());
assert!(db.get(RecordId::new(2)).unwrap().is_some());
cleanup(&dir);
}
#[test]
fn upsert_without_flush_or_close_is_still_replayed_on_reopen() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(1, vec![1.0, 0.0])).unwrap();
}
let db = Iqdb::open(&dir).unwrap();
assert_eq!(db.len(), 1);
assert_eq!(
db.get(RecordId::new(1)).unwrap().map(|r| r.id().get()),
Some(1)
);
cleanup(&dir);
}
#[test]
fn open_rejects_existing_file_path() {
let dir = tempdir();
let file_path = dir.join("not-a-directory");
std::fs::write(&file_path, b"data").unwrap();
let err = Iqdb::open(&file_path).unwrap_err();
assert!(matches!(err, Error::InvalidConfig(_)));
cleanup(&dir);
}
#[test]
fn search_works_against_recovered_data() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(1, vec![1.0, 0.0, 0.0])).unwrap();
db.upsert(record(2, vec![0.0, 1.0, 0.0])).unwrap();
db.upsert(record(3, vec![0.0, 0.0, 1.0])).unwrap();
db.close().unwrap();
}
let db = Iqdb::open(&dir).unwrap();
let probe = Vector::new(vec![1.0, 0.0, 0.0]).unwrap();
let hits = db.search(&probe, 2, DistanceMetric::Cosine).unwrap();
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].id, RecordId::new(1));
cleanup(&dir);
}
#[test]
fn multiple_reopen_cycles_preserve_state() {
let dir = tempdir();
for round in 0..5_u64 {
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(round, vec![round as f32, 0.0, 0.0]))
.unwrap();
db.close().unwrap();
}
let db = Iqdb::open(&dir).unwrap();
assert_eq!(db.len(), 5);
for round in 0..5_u64 {
assert!(db.get(RecordId::new(round)).unwrap().is_some());
}
cleanup(&dir);
}
#[test]
fn close_truncates_wal_to_zero_bytes() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
for id in 0..50_u64 {
db.upsert(record(id, vec![id as f32, 0.0])).unwrap();
}
db.close().unwrap();
}
let wal_len = std::fs::metadata(dir.join("wal")).unwrap().len();
assert_eq!(wal_len, 0, "WAL must be empty after compaction");
let snap_len = std::fs::metadata(dir.join("snap")).unwrap().len();
assert!(snap_len > 0, "snapshot must contain the records");
cleanup(&dir);
}
#[test]
fn corrupt_snapshot_surfaces_corrupt_error() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(1, vec![1.0, 0.0])).unwrap();
db.close().unwrap();
}
std::fs::write(dir.join("snap"), b"NOPE\x01\x00\x00\x00").unwrap();
let err = Iqdb::open(&dir).unwrap_err();
assert!(matches!(err, Error::Corrupt { .. }));
cleanup(&dir);
}
#[test]
fn corrupt_wal_tail_is_truncated_silently() {
let dir = tempdir();
{
let db = Iqdb::open(&dir).unwrap();
db.upsert(record(1, vec![1.0, 0.0])).unwrap();
db.flush().unwrap();
}
use std::io::Write;
let mut wal = std::fs::OpenOptions::new()
.append(true)
.open(dir.join("wal"))
.unwrap();
wal.write_all(&[0xFFu8; 64]).unwrap();
wal.sync_all().unwrap();
drop(wal);
let db = Iqdb::open(&dir).unwrap();
assert_eq!(db.len(), 1);
db.upsert(record(2, vec![0.0, 1.0])).unwrap();
db.close().unwrap();
cleanup(&dir);
}