use sqlitegraph::backend::native::{
graph_file::GraphFile,
types::{NativeBackendError, NativeResult},
v2::{
export::{ExportFactory, SnapshotExportConfig, SnapshotExporter},
import::{SnapshotImportConfig, SnapshotImporter},
planner::{ExportPlanner, PlannerDecision},
},
};
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::{NamedTempFile, TempDir};
fn create_stable_test_graph() -> NativeResult<(GraphFile, PathBuf)> {
let temp_file = NamedTempFile::new().map_err(|e| NativeBackendError::Io(e))?;
let path = temp_file.path().to_path_buf();
let _ = temp_file.into_temp_path().keep().unwrap();
let mut graph_file = GraphFile::create(&path)?;
if graph_file.is_transaction_active() {
graph_file.commit_transaction()?;
}
graph_file.flush()?;
graph_file.sync()?;
Ok((graph_file, path))
}
#[test]
fn test_snapshot_export_import_chain() -> NativeResult<()> {
let (original_graph, original_path) = create_stable_test_graph()?;
let original_header = original_graph.persistent_header().clone();
let planner_decision = ExportPlanner::analyze_export_strategy(&original_path)?;
assert!(
planner_decision.export_mode
== sqlitegraph::backend::native::v2::export::ExportMode::Snapshot
);
assert!(planner_decision.graph_stable);
let export_dir = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let snapshot_config = SnapshotExportConfig {
export_path: export_dir.path().join("snapshot"),
snapshot_id: "test_chain_snapshot".to_string(),
include_statistics: true,
min_stable_duration: std::time::Duration::from_secs(0),
checksum_validation: true,
};
let mut exporter = SnapshotExporter::new(&original_path, snapshot_config)?;
let export_result = exporter.export_snapshot()?;
assert!(export_result.snapshot_path.exists());
assert!(export_result.manifest_path.exists());
assert!(export_result.snapshot_size_bytes > 0);
drop(original_graph);
fs::remove_file(&original_path)?;
let import_path = export_dir.path().join("restored.v2");
let import_config = SnapshotImportConfig {
target_graph_path: import_path.clone(),
export_dir_path: export_dir.path().to_path_buf(),
import_mode: sqlitegraph::backend::native::v2::import::ImportMode::Fresh,
validate_manifest: true,
verify_checksum: true,
overwrite_existing: false,
};
let importer =
SnapshotImporter::from_export_dir(export_dir.path(), &import_path, import_config)?;
let import_result = importer.import()?;
assert!(import_path.exists());
assert!(import_result.records_imported > 0);
assert!(import_result.validation_passed);
let mut restored_graph = GraphFile::open(&import_path)?;
let restored_header = restored_graph.persistent_header().clone();
assert_eq!(original_header.magic, restored_header.magic);
assert_eq!(original_header.version, restored_header.version);
assert_eq!(original_header.node_count, restored_header.node_count);
assert_eq!(original_header.edge_count, restored_header.edge_count);
let wal_path = import_path.with_extension("wal");
assert!(
!wal_path.exists(),
"WAL should not exist after snapshot import"
);
assert!(!restored_graph.is_transaction_active());
assert!(restored_graph.validate_file_size().is_ok());
assert!(restored_graph.verify_commit_marker().is_ok());
Ok(())
}
#[test]
fn test_multiple_snapshot_cycles_consistency() -> NativeResult<()> {
let mut current_path = {
let (_, path) = create_stable_test_graph()?;
path
};
for cycle in 1..=3 {
let export_dir = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let snapshot_config = SnapshotExportConfig {
export_path: export_dir.path().join("snapshot"),
snapshot_id: format!("cycle_{}_snapshot", cycle),
include_statistics: true,
min_stable_duration: std::time::Duration::from_secs(0),
checksum_validation: true,
};
let mut exporter = SnapshotExporter::new(¤t_path, snapshot_config)?;
let export_result = exporter.export_snapshot()?;
fs::remove_file(¤t_path)?;
let import_path = export_dir
.path()
.join(format!("cycle_{}_restored.v2", cycle));
let import_config = SnapshotImportConfig {
target_graph_path: import_path.clone(),
export_dir_path: export_dir.path().to_path_buf(),
import_mode: sqlitegraph::backend::native::v2::import::ImportMode::Fresh,
validate_manifest: true,
verify_checksum: true,
overwrite_existing: false,
};
let importer =
SnapshotImporter::from_export_dir(export_dir.path(), &import_path, import_config)?;
let import_result = importer.import()?;
assert!(import_path.exists());
assert!(import_result.validation_passed);
current_path = import_path;
}
let mut final_graph = GraphFile::open(¤t_path)?;
assert!(final_graph.validate_file_size().is_ok());
assert!(final_graph.verify_commit_marker().is_ok());
Ok(())
}
#[test]
fn test_wal_export_paths_unmodified() -> NativeResult<()> {
let (graph_file, graph_path) = create_stable_test_graph()?;
let export_dir = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let checkpoint_result =
ExportFactory::create_checkpoint_aligned_exporter(&graph_path, export_dir.path());
let full_result = ExportFactory::create_full_exporter(&graph_path, export_dir.path());
drop(graph_file);
Ok(())
}
#[test]
fn test_planner_deterministic_behavior() -> NativeResult<()> {
let (graph_file, graph_path) = create_stable_test_graph()?;
let decision1 = ExportPlanner::analyze_export_strategy(&graph_path)?;
let decision2 = ExportPlanner::analyze_export_strategy(&graph_path)?;
let decision3 = ExportPlanner::analyze_export_strategy(&graph_path)?;
assert_eq!(decision1.export_mode, decision2.export_mode);
assert_eq!(decision2.export_mode, decision3.export_mode);
assert_eq!(decision1.reasoning, decision2.reasoning);
assert_eq!(decision2.reasoning, decision3.reasoning);
let snapshot_advisable = ExportPlanner::is_snapshot_advisable(&graph_path)?;
let should_be_snapshot = matches!(
decision1.export_mode,
sqlitegraph::backend::native::v2::export::ExportMode::Snapshot
);
assert_eq!(snapshot_advisable, should_be_snapshot);
drop(graph_file);
Ok(())
}
#[test]
fn test_snapshot_import_bypasses_wal_recovery() -> NativeResult<()> {
let (_, graph_path) = create_stable_test_graph()?;
let export_dir = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let snapshot_config = SnapshotExportConfig::default();
let mut exporter = SnapshotExporter::new(&graph_path, snapshot_config)?;
let export_result = exporter.export_snapshot()?;
let import_path = TempDir::new()
.map_err(|e| NativeBackendError::Io(e))?
.path()
.join("imported.v2");
let import_config = SnapshotImportConfig {
target_graph_path: import_path.clone(),
export_dir_path: export_dir.path().to_path_buf(),
import_mode: sqlitegraph::backend::native::v2::import::ImportMode::Fresh,
validate_manifest: true,
verify_checksum: true,
overwrite_existing: false,
};
let importer =
SnapshotImporter::from_export_dir(export_dir.path(), &import_path, import_config)?;
let import_result = importer.import()?;
let mut imported_graph = GraphFile::open(&import_path)?;
let wal_path = import_path.with_extension("wal");
assert!(
!wal_path.exists(),
"WAL file should not exist after snapshot import"
);
assert!(import_result.final_recovery_state == sqlitegraph::backend::native::v2::wal::recovery::states::RecoveryState::CleanShutdown);
assert!(!imported_graph.is_transaction_active());
assert!(imported_graph.validate_file_size().is_ok());
assert!(imported_graph.verify_commit_marker().is_ok());
Ok(())
}
#[test]
fn test_snapshot_export_requires_stable_state() -> NativeResult<()> {
let (mut graph_file, graph_path) = create_stable_test_graph()?;
graph_file.begin_transaction()?;
let planner_decision = ExportPlanner::analyze_export_strategy(&graph_path)?;
assert!(!matches!(
planner_decision.export_mode,
sqlitegraph::backend::native::v2::export::ExportMode::Snapshot
));
let snapshot_advisable = ExportPlanner::is_snapshot_advisable(&graph_path)?;
assert!(!snapshot_advisable);
graph_file.rollback_transaction()?;
drop(graph_file);
Ok(())
}
#[test]
fn test_snapshot_error_handling() -> NativeResult<()> {
let empty_dir = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let import_path = empty_dir.path().join("imported.v2");
let import_config = SnapshotImportConfig::default();
let import_result =
SnapshotImporter::from_export_dir(empty_dir.path(), &import_path, import_config);
assert!(import_result.is_err());
Ok(())
}
#[test]
fn test_snapshot_large_file_handling() -> NativeResult<()> {
let (_, graph_path) = create_stable_test_graph()?;
assert!(graph_path.exists());
let file_size = fs::metadata(&graph_path)?.len();
assert!(file_size > 0);
let export_dir = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let snapshot_config = SnapshotExportConfig::default();
let mut exporter = SnapshotExporter::new(&graph_path, snapshot_config)?;
let export_result = exporter.export_snapshot()?;
assert!(export_result.snapshot_size_bytes > 0);
let import_path = export_dir.path().join("large_import.v2");
let import_config = SnapshotImportConfig {
target_graph_path: import_path.clone(),
export_dir_path: export_dir.path().to_path_buf(),
import_mode: sqlitegraph::backend::native::v2::import::ImportMode::Fresh,
validate_manifest: true,
verify_checksum: true,
overwrite_existing: false,
};
let importer =
SnapshotImporter::from_export_dir(export_dir.path(), &import_path, import_config)?;
let import_result = importer.import()?;
assert!(import_result.snapshot_size_bytes == export_result.snapshot_size_bytes);
assert!(import_path.exists());
let imported_size = fs::metadata(&import_path)?.len();
assert_eq!(imported_size, file_size);
Ok(())
}
#[test]
fn test_snapshot_concurrent_access() -> NativeResult<()> {
let (_, graph_path) = create_stable_test_graph()?;
let export_dir1 = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let export_dir2 = TempDir::new().map_err(|e| NativeBackendError::Io(e))?;
let snapshot_config1 = SnapshotExportConfig {
snapshot_id: "concurrent_1".to_string(),
export_path: export_dir1.path().join("snapshot"),
..Default::default()
};
let snapshot_config2 = SnapshotExportConfig {
snapshot_id: "concurrent_2".to_string(),
export_path: export_dir2.path().join("snapshot"),
..Default::default()
};
let mut exporter1 = SnapshotExporter::new(&graph_path, snapshot_config1)?;
let mut exporter2 = SnapshotExporter::new(&graph_path, snapshot_config2)?;
let export_result1 = exporter1.export_snapshot()?;
let export_result2 = exporter2.export_snapshot()?;
assert!(export_result1.snapshot_path.exists());
assert!(export_result2.snapshot_path.exists());
assert_eq!(
export_result1.snapshot_size_bytes,
export_result2.snapshot_size_bytes
);
Ok(())
}