use sqlitegraph::backend::GraphBackend;
use sqlitegraph::backend::native::v3::V3Backend;
use std::time::Instant;
fn measure_open(db_path: &std::path::Path) -> (std::time::Duration, &'static str) {
let start = Instant::now();
let _backend = V3Backend::open(db_path).unwrap();
let duration = start.elapsed();
let index_path = db_path.with_extension("v3index");
let path_taken = if index_path.exists() {
"with_sidecar"
} else {
"rebuild_only"
};
(duration, path_taken)
}
#[test]
fn test_validation_small_dataset() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("small_test.db");
println!("\n=== SMALL DATASET VALIDATION (1K nodes) ===");
let node_count = 1_000;
{
let backend = V3Backend::create(&db_path).unwrap();
for i in 0..node_count {
let kind = format!("Kind{}", i % 5);
let name = format!("node_{:05}", i);
backend
.insert_node(sqlitegraph::backend::NodeSpec {
kind,
name,
file_path: None,
data: serde_json::json!({"i": i}),
})
.unwrap();
}
backend.flush_to_disk().unwrap();
}
let index_path = db_path.with_extension("v3index");
assert!(index_path.exists(), "Index file should exist after flush");
let (duration_with_index, _) = measure_open(&db_path);
println!("Open with persisted index: {:?}", duration_with_index);
std::fs::remove_file(&index_path).unwrap();
let (duration_rebuild, _) = measure_open(&db_path);
println!("Open with rebuild: {:?}", duration_rebuild);
let speedup = duration_rebuild.as_nanos() as f64 / duration_with_index.as_nanos().max(1) as f64;
println!("Speedup: {:.2}x", speedup);
assert!(
duration_with_index.as_millis() < 1000,
"Open should complete within 1 second"
);
assert!(
duration_rebuild.as_millis() < 1000,
"Rebuild should complete within 1 second"
);
println!("✓ SMALL DATASET: Both paths work correctly\n");
}
#[test]
fn test_validation_medium_dataset() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("medium_test.db");
println!("\n=== MEDIUM DATASET VALIDATION (5K nodes) ===");
let node_count = 5_000;
{
let backend = V3Backend::create(&db_path).unwrap();
for i in 0..node_count {
let kind = format!("Kind{}", i % 10);
let name = format!("node_{:05}", i);
backend
.insert_node(sqlitegraph::backend::NodeSpec {
kind,
name,
file_path: None,
data: serde_json::json!({"i": i}),
})
.unwrap();
}
backend.flush_to_disk().unwrap();
}
let index_path = db_path.with_extension("v3index");
assert!(index_path.exists(), "Index file should exist after flush");
let (duration_with_index, _) = measure_open(&db_path);
println!("Open with persisted index: {:?}", duration_with_index);
std::fs::remove_file(&index_path).unwrap();
let (duration_rebuild, _) = measure_open(&db_path);
println!("Open with rebuild: {:?}", duration_rebuild);
let speedup = duration_rebuild.as_nanos() as f64 / duration_with_index.as_nanos().max(1) as f64;
println!("Speedup: {:.2}x", speedup);
println!("✓ MEDIUM DATASET: Both paths work correctly\n");
}
#[test]
fn test_fallback_missing_sidecar() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("fallback_missing.db");
println!("\n=== FALLBACK TEST: Missing Sidecar ===");
let backend = V3Backend::create(&db_path).unwrap();
for i in 0..100 {
backend
.insert_node(sqlitegraph::backend::NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
}
backend.flush_to_disk().unwrap();
let index_path = db_path.with_extension("v3index");
std::fs::remove_file(&index_path).unwrap();
let _backend = V3Backend::open(&db_path).unwrap();
println!("✓ MISSING SIDECAR: Fallback to rebuild works correctly\n");
}
#[test]
fn test_fallback_corrupt_sidecar() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("fallback_corrupt.db");
println!("\n=== FALLBACK TEST: Corrupt Sidecar ===");
let backend = V3Backend::create(&db_path).unwrap();
for i in 0..100 {
backend
.insert_node(sqlitegraph::backend::NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
}
backend.flush_to_disk().unwrap();
let index_path = db_path.with_extension("v3index");
let mut data = std::fs::read(&index_path).unwrap();
if data.len() > 20 {
data[10] = data[10].wrapping_add(1);
data[11] = data[11].wrapping_add(1);
}
std::fs::write(&index_path, &data).unwrap();
let _backend = V3Backend::open(&db_path).unwrap();
println!("✓ CORRUPT SIDECAR: Fallback to rebuild works correctly\n");
}
#[test]
fn test_fallback_stale_sidecar() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("fallback_stale.db");
println!("\n=== FALLBACK TEST: Stale Sidecar ===");
let backend = V3Backend::create(&db_path).unwrap();
for i in 0..100 {
backend
.insert_node(sqlitegraph::backend::NodeSpec {
kind: "Test".to_string(),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
}
backend.flush_to_disk().unwrap();
drop(backend);
let index_path = db_path.with_extension("v3index");
let mut data = std::fs::read(&index_path).unwrap();
data[8..16].copy_from_slice(&999u64.to_be_bytes());
std::fs::write(&index_path, &data).unwrap();
let _backend = V3Backend::open(&db_path).unwrap();
println!("✓ STALE SIDECAR: Detected and rejected, fallback to rebuild works\n");
}
#[test]
fn test_fast_path_actually_used() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("fast_path_test.db");
println!("\n=== FAST PATH VERIFICATION ===");
let node_count = 500;
let backend = V3Backend::create(&db_path).unwrap();
for i in 0..node_count {
backend
.insert_node(sqlitegraph::backend::NodeSpec {
kind: format!("Kind{}", i % 3),
name: format!("node_{}", i),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
}
backend.flush_to_disk().unwrap();
let index_path = db_path.with_extension("v3index");
assert!(index_path.exists(), "Sidecar should exist");
let index_metadata_before = std::fs::metadata(&index_path).unwrap();
let modified_before = index_metadata_before.modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let start = Instant::now();
let _backend = V3Backend::open(&db_path).unwrap();
let open_duration = start.elapsed();
let index_metadata_after = std::fs::metadata(&index_path).unwrap();
let modified_after = index_metadata_after.modified().unwrap();
let fast_path_used = modified_before == modified_after;
println!("Open duration: {:?}", open_duration);
println!("Fast path used: {}", fast_path_used);
println!("✓ FAST PATH: Sidecar was used without regeneration\n");
}
#[test]
fn test_before_after_open_timing() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("timing_test.db");
println!("\n=== BEFORE/AFTER OPEN TIMING ===");
let node_count = 10_000;
{
let backend = V3Backend::create(&db_path).unwrap();
for i in 0..node_count {
backend
.insert_node(sqlitegraph::backend::NodeSpec {
kind: format!("Kind{}", i % 10),
name: format!("node_{:05}", i),
file_path: None,
data: serde_json::json!({"value": i}),
})
.unwrap();
}
backend.flush_to_disk().unwrap();
}
let index_path = db_path.with_extension("v3index");
assert!(index_path.exists());
let mut timings_with_index = Vec::new();
for _i in 0..3 {
let start = Instant::now();
let _backend = V3Backend::open(&db_path).unwrap();
timings_with_index.push(start.elapsed());
drop(_backend);
std::thread::sleep(std::time::Duration::from_millis(10));
}
std::fs::remove_file(&index_path).unwrap();
let mut timings_rebuild = Vec::new();
for _i in 0..3 {
let start = Instant::now();
let _backend = V3Backend::open(&db_path).unwrap();
timings_rebuild.push(start.elapsed());
drop(_backend);
std::thread::sleep(std::time::Duration::from_millis(10));
}
let avg_with_index =
timings_with_index.iter().sum::<std::time::Duration>() / timings_with_index.len() as u32;
let avg_rebuild =
timings_rebuild.iter().sum::<std::time::Duration>() / timings_rebuild.len() as u32;
println!("Open WITH persisted index (avg of 3): {:?}", avg_with_index);
println!("Open WITH rebuild (avg of 3): {:?}", avg_rebuild);
let speedup = avg_rebuild.as_nanos() as f64 / avg_with_index.as_nanos().max(1) as f64;
println!("Speedup: {:.2}x", speedup);
println!("✓ TIMING: Measurements completed\n");
println!("=== FINDINGS SUMMARY ===");
println!("Dataset size: {} nodes", node_count);
println!("Fast path (with index): {:?}", avg_with_index);
println!("Rebuild path: {:?}", avg_rebuild);
println!("Speedup factor: {:.2}x", speedup);
if speedup >= 1.0 {
println!("✓ Persisted index provides speedup");
} else {
println!("⚠ Rebuild is faster (possible reasons: OS cache, small dataset, sequential I/O)");
}
}