Files
ChuSQL/chusql-core/storage/tests/wal_test.rs
T
2026-10-11 17:32:12 +08:00

375 lines
12 KiB
Rust

use chusql_core_storage::protocol::Row;
use chusql_core_storage::wal::{committed_ops, records_after, Wal, WalOp};
use serde_json::json;
// WAL 测试:追加、读取、清空、截断尾巴、LSN 与提交标记。
/// 结构版本往返且旧隐藏列载荷可读
#[test]
fn schema_versions_round_trip_and_legacy_hide_decodes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
let ops = [
WalOp::HideColumn { table: "t".into(), column: "name".into(), schema_version: Some(u64::MAX) },
WalOp::DropColumn { table: "t".into(), column: "name".into(), rows: vec![], schema_version: Some(3) },
WalOp::ReplaceSchema { table: "t".into(), columns: vec![], rows: vec![], schema_version: Some(4) },
];
wal.append_group(&ops).unwrap();
assert_eq!(committed_ops(&wal.read_all().unwrap().records).into_iter().map(|(_, op)| op).collect::<Vec<_>>(), ops);
drop(wal);
let mut body = 1_u64.to_le_bytes().to_vec();
body.push(8);
body.extend_from_slice(&1_u16.to_le_bytes());
body.extend_from_slice(b"t");
body.extend_from_slice(br#""name""#);
let mut frame = (body.len() as u32).to_le_bytes().to_vec();
frame.extend_from_slice(&body);
std::fs::write(&path, frame).unwrap();
assert_eq!(Wal::new(&path).read().unwrap(), Some(WalOp::HideColumn {
table: "t".into(), column: "name".into(), schema_version: None,
}));
}
/// 建表编号完整往返且兼容旧载荷
#[test]
fn create_table_ids_round_trip_and_legacy_payloads_decode() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
let op = WalOp::CreateTable { table: "t".into(), columns: vec![], object_id: Some(42) };
wal.append(&op).unwrap();
assert_eq!(wal.read_all().unwrap().records[0].1, op);
drop(wal);
let mut body = 1_u64.to_le_bytes().to_vec();
body.push(11);
body.extend_from_slice(&1_u16.to_le_bytes());
body.extend_from_slice(b"t");
body.extend_from_slice(br#"{"columns":[]}"#);
let mut frame = (body.len() as u32).to_le_bytes().to_vec();
frame.extend_from_slice(&body);
std::fs::write(&path, frame).unwrap();
let wal = Wal::new(&path);
assert_eq!(wal.read_all().unwrap().records[0].1,
WalOp::CreateTable { table: "t".into(), columns: vec![], object_id: None });
}
/// 用键值对构造一行
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
m.insert((*k).to_string(), v.clone());
}
m
}
/// 三种操作写完都能原样读回
#[test]
fn append_and_read_round_trips() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
let ops = [
WalOp::Insert {
table: "users".into(),
row: row(&[("id", json!(1)), ("name", json!("Alice"))]),
},
WalOp::ReplaceAll {
table: "users".into(),
rows: vec![
row(&[("id", json!(1)), ("name", json!("Alice"))]),
row(&[("id", json!(2)), ("name", json!("Bob"))]),
],
},
WalOp::DropColumn {
table: "users".into(),
column: "age".into(),
schema_version: None,
rows: vec![
row(&[("id", json!(1)), ("name", json!("Alice"))]),
row(&[("id", json!(2)), ("name", json!("Bob"))]),
],
},
];
for op in ops {
wal.clear().unwrap();
wal.append(&op).unwrap();
assert_eq!(wal.read().unwrap().unwrap(), op);
}
}
/// 文件不存在或为空都返回 None
#[test]
fn read_without_content_returns_none() {
let dir = tempfile::tempdir().unwrap();
let missing = Wal::new(dir.path().join("missing.log"));
assert!(missing.read().unwrap().is_none());
let path = dir.path().join("wal.log");
std::fs::write(&path, b"").unwrap();
let empty = Wal::new(&path);
assert!(empty.read().unwrap().is_none());
}
/// 清空后读不到
#[test]
fn clear_truncates() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(1))]),
})
.unwrap();
assert!(wal.read().unwrap().is_some());
wal.clear().unwrap();
assert!(wal.read().unwrap().is_none());
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
/// 尾巴截断被忽略
#[test]
fn truncated_tail_is_ignored() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
wal.append(&WalOp::Insert {
table: "users".into(),
row: row(&[("id", json!(1)), ("name", json!("Alice"))]),
})
.unwrap();
let full = std::fs::read(&path).unwrap();
std::fs::write(&path, &full[..full.len() - 5]).unwrap();
assert!(wal.read().unwrap().is_none());
}
/// 每条记录带递增的 LSN,重开也从文件里的最大号往下走
#[test]
fn every_record_gets_an_increasing_lsn() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
for id in 1..=3 {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(id))]),
})
.unwrap();
}
let scan = wal.read_all().unwrap();
let lsns: Vec<u64> = scan.records.iter().map(|(lsn, _)| *lsn).collect();
assert_eq!(lsns, vec![1, 2, 3]);
assert_eq!(scan.good_len as u64, std::fs::metadata(&path).unwrap().len());
let reopened = Wal::new(&path);
reopened
.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(4))]),
})
.unwrap();
assert_eq!(reopened.read_all().unwrap().records.last().unwrap().0, 4);
}
/// 一组操作写完带提交标记,标记的 LSN 也在记录里
#[test]
fn append_group_writes_a_commit_marker() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
let group = [
WalOp::InsertBatch {
table: "t".into(),
rows: vec![row(&[("id", json!(2))])],
},
WalOp::DeleteKeys {
table: "t".into(),
keys: vec![1],
},
];
let commit_lsn = wal.append_group(&group).unwrap();
let scan = wal.read_all().unwrap();
assert_eq!(scan.records.len(), 3);
assert_eq!(scan.records.last().unwrap().1, WalOp::Commit);
assert_eq!(scan.records.last().unwrap().0, commit_lsn);
assert_eq!(wal.read().unwrap().unwrap(), group[0]);
assert_eq!(
committed_ops(&scan.records),
vec![(1, group[0].clone()), (2, group[1].clone())]
);
}
/// 没有提交标记的记录组算未提交,旧格式的单条仍算已提交
#[test]
fn records_without_a_commit_marker_stay_uncommitted() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
for id in 1..=2 {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(id))]),
})
.unwrap();
}
assert!(committed_ops(&wal.read_all().unwrap().records).is_empty());
wal.clear().unwrap();
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(9))]),
})
.unwrap();
assert_eq!(committed_ops(&wal.read_all().unwrap().records).len(), 1);
}
/// 撕成两半的尾巴能读出完好长度并截掉
#[test]
fn trailing_torn_frame_can_be_truncated() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
for id in 1..=2 {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(id))]),
})
.unwrap();
}
let full = std::fs::read(&path).unwrap();
std::fs::write(&path, &full[..full.len() - 3]).unwrap();
let scan = wal.read_all().unwrap();
assert_eq!(scan.records.len(), 1);
assert!(scan.good_len < full.len() - 3);
wal.truncate_to(scan.good_len).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), scan.good_len as u64);
assert_eq!(wal.read_all().unwrap().records.len(), 1);
}
/// 完整损坏帧返回错误而非截断成功
#[test]
fn complete_corrupt_frame_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
std::fs::write(&path, [1, 0, 0, 0, 255]).unwrap();
assert!(Wal::new(&path).read_all().is_err());
assert_eq!(std::fs::read(path).unwrap(), [1, 0, 0, 0, 255]);
}
/// 检查点损坏与替换失败不丢旧值
#[test]
fn checkpoint_corruption_and_write_failure_are_explicit() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
for bytes in [vec![], vec![1; 7], vec![1; 9]] {
std::fs::write(dir.path().join("wal.checkpoint"), bytes).unwrap();
assert!(wal.read_checkpoint().is_err());
}
wal.write_checkpoint(7).unwrap();
std::fs::create_dir(dir.path().join("wal.checkpoint.pending")).unwrap();
assert!(wal.write_checkpoint(9).is_err());
assert_eq!(wal.read_checkpoint().unwrap(), Some(7));
}
/// 检查点写读一致
#[test]
fn checkpoint_round_trips() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
assert_eq!(wal.read_checkpoint().unwrap(), None);
wal.write_checkpoint(7).unwrap();
assert_eq!(wal.read_checkpoint().unwrap(), Some(7));
}
/// 按 LSN 截断只丢边界以内的帧
#[test]
fn truncate_before_keeps_newer_records() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
let second_op = WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(2))]),
};
let first = wal
.append_group(&[WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(1))]),
}])
.unwrap();
let second = wal.append_group(std::slice::from_ref(&second_op)).unwrap();
assert!(second > first);
// 提交号是第一组的最后一条;边界以内的操作与提交标记都该丢掉
wal.truncate_before(first).unwrap();
let kept = wal.read_all().unwrap();
assert!(kept.records.iter().all(|(lsn, _)| *lsn > first));
let ops: Vec<&WalOp> = kept
.records
.iter()
.map(|(_, op)| op)
.filter(|op| **op != WalOp::Commit)
.collect();
assert_eq!(ops, vec![&second_op], "只剩第二组的操作");
// 边界划到最后一条之后,日志就空了
wal.truncate_before(second).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
assert!(wal.read_all().unwrap().records.is_empty());
}
/// 日志被截断后 LSN 也不回头
#[test]
fn lsn_continues_from_the_checkpoint() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let op = WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(1))]),
};
let first = {
let wal = Wal::new(&path);
let lsn = wal.append_group(std::slice::from_ref(&op)).unwrap();
wal.write_checkpoint(lsn).unwrap();
wal.truncate_before(lsn).unwrap();
lsn
};
// 重新打开:号从检查点续
let wal = Wal::new(&path);
let next = wal.append_group(std::slice::from_ref(&op)).unwrap();
assert!(next > first, "重启后 LSN 要接着检查点走: {next} vs {first}");
}
/// 过滤只留下检查点之后的记录
#[test]
fn records_after_keeps_only_the_tail() {
let records = vec![
(1u64, WalOp::Insert { table: "t".into(), row: row(&[("id", json!(1))]) }),
(2, WalOp::Commit),
(3, WalOp::Insert { table: "t".into(), row: row(&[("id", json!(2))]) }),
(4, WalOp::Commit),
];
let kept = records_after(&records, 2);
let lsn: Vec<u64> = kept.iter().map(|(lsn, _)| *lsn).collect();
assert_eq!(lsn, vec![3, 4]);
assert_eq!(records_after(&records, 4).len(), 0);
assert_eq!(records_after(&records, 0).len(), 4);
}