use aedb::AedbInstance;
use aedb::catalog::DdlOperation;
use aedb::catalog::schema::ColumnDef;
use aedb::catalog::types::{ColumnType, Row, Value};
use aedb::commit::validation::Mutation;
use aedb::config::AedbConfig;
use aedb::offline;
use tempfile::tempdir;
#[tokio::test]
async fn snapshot_dump_restore_parity_and_invariants_are_deterministic() {
let data_dir = tempdir().expect("data");
let restore_dir = tempdir().expect("restore");
let dump_dir = tempdir().expect("dump");
let dump_path = dump_dir.path().join("snapshot_dump.json");
let config = AedbConfig::default();
let db = AedbInstance::open_anonymous(config.clone(), data_dir.path()).expect("open");
db.commit(Mutation::Ddl(DdlOperation::CreateProject {
project_id: "p".into(),
owner_id: None,
if_not_exists: true,
}))
.await
.expect("project");
db.commit(Mutation::Ddl(DdlOperation::CreateTable {
project_id: "p".into(),
scope_id: "app".into(),
table_name: "users".into(),
owner_id: None,
if_not_exists: false,
columns: vec![
ColumnDef {
name: "id".into(),
col_type: ColumnType::Integer,
nullable: false,
},
ColumnDef {
name: "name".into(),
col_type: ColumnType::Text,
nullable: false,
},
],
primary_key: vec!["id".into()],
}))
.await
.expect("table");
db.commit(Mutation::Upsert {
project_id: "p".into(),
scope_id: "app".into(),
table_name: "users".into(),
primary_key: vec![Value::Integer(1)],
row: Row {
values: vec![Value::Integer(1), Value::Text("alice".into())],
},
})
.await
.expect("row");
db.commit(Mutation::KvSet {
project_id: "p".into(),
scope_id: "app".into(),
key: b"user:1".to_vec(),
value: b"alice".to_vec(),
})
.await
.expect("kv");
db.shutdown().await.expect("shutdown");
let export =
offline::export_snapshot_dump(data_dir.path(), &config, &dump_path).expect("export");
assert!(export.current_seq >= 4);
assert!(!export.parity_checksum_hex.is_empty());
let restored =
offline::restore_snapshot_dump(&dump_path, restore_dir.path(), &config).expect("restore");
assert_eq!(restored.parity_checksum_hex, export.parity_checksum_hex);
let parity = offline::parity_report_against_data_dir(&dump_path, restore_dir.path(), &config)
.expect("parity");
assert!(parity.matches);
let invariants = offline::invariant_report(restore_dir.path(), &config).expect("invariants");
assert!(
invariants.ok,
"invariants violations: {:?}",
invariants.violations
);
}
#[tokio::test]
async fn verify_database_passes_on_clean_store_and_detects_corruption() {
let data_dir = tempdir().expect("data");
let config = AedbConfig::default();
let db = AedbInstance::open_anonymous(config.clone(), data_dir.path()).expect("open");
db.commit(Mutation::Ddl(DdlOperation::CreateProject {
project_id: "p".into(),
owner_id: None,
if_not_exists: true,
}))
.await
.expect("project");
db.commit(Mutation::KvSet {
project_id: "p".into(),
scope_id: "app".into(),
key: b"k".to_vec(),
value: b"v".to_vec(),
})
.await
.expect("kv");
db.shutdown().await.expect("shutdown");
drop(db);
let report = offline::verify_database(data_dir.path(), &config);
assert!(report.ok, "expected clean store to verify: {report:?}");
assert!(report.checks.iter().all(|c| c.ok));
assert!(
report
.checks
.iter()
.any(|c| c.name == "wal_checkpoint_manifest_integrity"),
"durability check must be present"
);
assert!(report.current_seq >= 2);
let seg = std::fs::read_dir(data_dir.path())
.expect("read dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.is_file()
&& p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("segment_") && n.ends_with(".aedbwal"))
})
.max()
.expect("a wal segment file");
let mut bytes = std::fs::read(&seg).expect("read segment");
assert!(!bytes.is_empty());
let flip_from = bytes.len().saturating_sub(16);
for b in &mut bytes[flip_from..] {
*b ^= 0xFF;
}
std::fs::write(&seg, &bytes).expect("write corrupted segment");
let corrupt = offline::verify_database(data_dir.path(), &config);
assert!(!corrupt.ok, "corrupted store must fail verification");
assert!(
corrupt.checks.iter().any(|c| !c.ok),
"at least one check must fail: {corrupt:?}"
);
assert!(!corrupt.violations.is_empty());
}
#[tokio::test]
async fn verify_database_handles_empty_table_without_false_violation() {
let data_dir = tempdir().expect("data");
let config = AedbConfig::default();
let db = AedbInstance::open_anonymous(config.clone(), data_dir.path()).expect("open");
db.commit(Mutation::Ddl(DdlOperation::CreateProject {
project_id: "p".into(),
owner_id: None,
if_not_exists: true,
}))
.await
.expect("project");
db.commit(Mutation::Ddl(DdlOperation::CreateTable {
project_id: "p".into(),
scope_id: "app".into(),
table_name: "empty".into(),
owner_id: None,
if_not_exists: false,
columns: vec![ColumnDef {
name: "id".into(),
col_type: ColumnType::Integer,
nullable: false,
}],
primary_key: vec!["id".into()],
}))
.await
.expect("table");
db.shutdown().await.expect("shutdown");
drop(db);
let report = offline::verify_database(data_dir.path(), &config);
assert!(
report.ok,
"empty table must not produce false integrity violations: {:?}",
report.violations
);
}