修bug,完善功能
build / build (push) Blocked by required conditions
tests / build (push) Waiting to run
tests / tests (push) Waiting to run

This commit is contained in:
ChuNa7a committed 2026-10-16 23:52:12 +08:00
1 parent 30364b1c4b
commit 6996e19694
125 files changed
+15343 -3746

No files matched your search

+125 -12
View File
@@ -1,4 +1,4 @@
use chusql_core_storage::btree::{fits_in_page, DiskBTree, MAX_KEY_BYTES};
use chusql_core_storage::btree::{DiskBTree, MAX_KEY_BYTES, fits_in_page};
use chusql_core_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
use chusql_core_storage::page::DEFAULT_POOL_SIZE;
use std::io::{ErrorKind, Seek, SeekFrom, Write};
@@ -16,20 +16,28 @@ fn malformed_node_fields_return_invalid_data() {
tree.insert(b"y", 2).unwrap();
}
let original = std::fs::read(&path).unwrap();
let cell = u16::from_le_bytes([original[DEFAULT_PAGE_SIZE + 11], original[DEFAULT_PAGE_SIZE + 12]]) as usize;
let cell = u16::from_le_bytes([
original[DEFAULT_PAGE_SIZE + 11],
original[DEFAULT_PAGE_SIZE + 12],
]) as usize;
for (name, offset, bytes) in [
("slot count", 1, vec![255, 255]),
("node kind", 0, vec![2]),
("cell overlaps slots", 11, vec![0, 0]),
("cell beyond page", 11, vec![255, 15]),
("overlapping cells", 13, (cell as u16).to_le_bytes().to_vec()),
(
"overlapping cells",
13,
(cell as u16).to_le_bytes().to_vec(),
),
("key length", cell, vec![255, 255]),
("key order", cell + 2, vec![b'z']),
("zero child", 0, vec![1]),
] {
std::fs::write(&path, &original).unwrap();
let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
file.seek(SeekFrom::Start((DEFAULT_PAGE_SIZE + offset) as u64)).unwrap();
file.seek(SeekFrom::Start((DEFAULT_PAGE_SIZE + offset) as u64))
.unwrap();
file.write_all(&bytes).unwrap();
drop(file);
let mut tree = open(dir.path(), "corrupt.idx");
@@ -42,7 +50,13 @@ fn malformed_node_fields_return_invalid_data() {
/// 按默认参数打开指定索引文件
fn open(dir: &std::path::Path, name: &str) -> DiskBTree {
DiskBTree::open(dir.join(name), DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap()
DiskBTree::open(
dir.join(name),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap()
}
/// 整数键编码成保序的 8 字节(与堆表一致)
@@ -101,12 +115,24 @@ fn reopen_persists() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.idx");
{
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
let mut t = DiskBTree::open(
&path,
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in 0..50 {
t.insert(&key(i), i as u64).unwrap();
}
}
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
let mut t = DiskBTree::open(
&path,
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in 0..50 {
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64));
}
@@ -148,12 +174,26 @@ fn config_mismatch_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.idx");
{
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
let mut t = DiskBTree::open(
&path,
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
t.insert(&key(1), 1).unwrap();
}
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 8, DEFAULT_POOL_SIZE).is_err());
assert!(DiskBTree::open(&path, 8192, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_ok());
assert!(
DiskBTree::open(
&path,
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE
)
.is_ok()
);
}
/// 不是索引文件要报错
@@ -162,7 +202,15 @@ fn not_an_index_file_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("junk.idx");
std::fs::write(&path, vec![7u8; DEFAULT_PAGE_SIZE]).unwrap();
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
assert!(
DiskBTree::open(
&path,
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE
)
.is_err()
);
}
/// order 放不进一页要报错
@@ -182,7 +230,10 @@ fn delete_removes_only_that_key() {
t.insert(&key(i), i as u64 + 1).unwrap();
}
assert!(t.delete(&key(50), 51).unwrap(), "deleting an existing entry should return true");
assert!(
t.delete(&key(50), 51).unwrap(),
"deleting an existing entry should return true"
);
assert_eq!(t.get(&key(50)).unwrap(), None);
assert_eq!(t.get(&key(49)).unwrap(), Some(50));
assert_eq!(t.get(&key(51)).unwrap(), Some(52));
@@ -262,7 +313,11 @@ fn splits_keep_keys_sorted() {
let all: Vec<Vec<u8>> = t.iter_all().unwrap().into_iter().map(|(k, _)| k).collect();
assert_eq!(all, expected);
for k in &expected {
assert!(t.get(k).unwrap().is_some(), "missing {:?}", String::from_utf8_lossy(k));
assert!(
t.get(k).unwrap().is_some(),
"missing {:?}",
String::from_utf8_lossy(k)
);
}
}
@@ -314,3 +369,61 @@ fn clear_resets_the_tree() {
t.insert(b"c", 3).unwrap();
assert_eq!(t.get(b"c").unwrap(), Some(3));
}
/// 长键跨页后可重启查询并保留旧配置
#[test]
fn long_keys_split_reopen_and_preserve_dense_configuration() {
use chusql_core_storage::btree::key_limit;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("long.idx");
let mut tree = DiskBTree::open(&path, 512, 4, DEFAULT_POOL_SIZE).unwrap();
for n in 0..80_u64 {
let key = format!("{n:03}{}", "a".repeat(125));
tree.insert(key.as_bytes(), n).unwrap();
}
drop(tree);
let mut tree = DiskBTree::open(&path, 512, 4, DEFAULT_POOL_SIZE).unwrap();
assert_eq!(tree.scan(None, None).unwrap().len(), 80);
for n in 0..80_u64 {
let key = format!("{n:03}{}", "a".repeat(125));
assert_eq!(tree.get(key.as_bytes()).unwrap(), Some(n));
}
let dense_path = dir.path().join("dense.idx");
let mut dense = DiskBTree::open(&dense_path, 512, 7, DEFAULT_POOL_SIZE).unwrap();
let limit = key_limit(512, 7);
assert!((64..MAX_KEY_BYTES).contains(&limit));
dense.insert(&vec![b'a'; limit], 1).unwrap();
assert_eq!(
dense.insert(&vec![b'b'; limit + 1], 2).unwrap_err().kind(),
ErrorKind::InvalidInput
);
assert_eq!(dense.get(&vec![b'a'; limit]).unwrap(), Some(1));
}
/// 旧文件头可无损打开并首次写入长键
#[test]
fn legacy_key_header_upgrades_only_when_long_keys_are_written() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("legacy.idx");
let mut tree = open(dir.path(), "legacy.idx");
tree.insert(b"old", 1).unwrap();
drop(tree);
let mut bytes = std::fs::read(&path).unwrap();
bytes[8..10].copy_from_slice(&64_u16.to_le_bytes());
std::fs::write(&path, &bytes).unwrap();
let mut tree = open(dir.path(), "legacy.idx");
assert_eq!(tree.get(b"old").unwrap(), Some(1));
drop(tree);
assert_eq!(std::fs::read(&path).unwrap(), bytes);
let mut tree = open(dir.path(), "legacy.idx");
let key = "author/".to_string() + &"p".repeat(80);
tree.insert(key.as_bytes(), 2).unwrap();
drop(tree);
let mut tree = open(dir.path(), "legacy.idx");
assert_eq!(tree.get(key.as_bytes()).unwrap(), Some(2));
assert_eq!(tree.get(b"old").unwrap(), Some(1));
assert_eq!(
&std::fs::read(&path).unwrap()[8..10],
&128_u16.to_le_bytes()
);
}
+83 -31
View File
@@ -33,9 +33,14 @@ fn schema_versions_track_structure_and_survive_reload() {
fn legacy_schema_version_remains_unknown_until_change() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
std::fs::write(&path, json!({"tables": {
"t": {"object_id": 42, "columns": []}
}}).to_string()).unwrap();
std::fs::write(
&path,
json!({"tables": {
"t": {"object_id": 42, "columns": []}
}})
.to_string(),
)
.unwrap();
let mut c = Catalog::load(&path).unwrap();
assert_eq!(c.describe("t").unwrap().schema_version, 0);
c.save(&path).unwrap();
@@ -51,10 +56,15 @@ fn legacy_schema_version_remains_unknown_until_change() {
fn exhausted_schema_version_preserves_structure() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
std::fs::write(&path, json!({"tables": {
"t": {"object_id": 42, "schema_version": u64::MAX,
"columns": [{"name": "id", "ty": "int"}]}
}}).to_string()).unwrap();
std::fs::write(
&path,
json!({"tables": {
"t": {"object_id": 42, "schema_version": u64::MAX,
"columns": [{"name": "id", "ty": "int"}]}
}})
.to_string(),
)
.unwrap();
let mut c = Catalog::load(&path).unwrap();
assert!(c.remove_column("t", "id").is_err());
assert!(c.set_columns("t", vec![]).is_err());
@@ -90,20 +100,42 @@ fn object_ids_survive_reload_and_are_not_reused() {
fn legacy_object_ids_upgrade_without_collisions() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
std::fs::write(&path, json!({"tables": {
"a": {"columns": [], "object_id": 42},
"b": {"columns": []}
}}).to_string()).unwrap();
std::fs::write(
&path,
json!({"tables": {
"a": {"columns": [], "object_id": 42},
"b": {"columns": []}
}})
.to_string(),
)
.unwrap();
let catalog = Catalog::load(&path).unwrap();
assert_eq!(catalog.describe("a").unwrap().object_id, 42);
assert_eq!(catalog.describe("b").unwrap().object_id, 43);
catalog.save(&path).unwrap();
assert_eq!(Catalog::load(&path).unwrap().describe("b").unwrap().object_id, 43);
std::fs::write(&path, json!({"tables": {
"a": {"columns": [], "object_id": 42},
"b": {"columns": [], "object_id": 42}
}}).to_string()).unwrap();
assert!(Catalog::load(&path).unwrap_err().to_string().contains("duplicate"));
assert_eq!(
Catalog::load(&path)
.unwrap()
.describe("b")
.unwrap()
.object_id,
43
);
std::fs::write(
&path,
json!({"tables": {
"a": {"columns": [], "object_id": 42},
"b": {"columns": [], "object_id": 42}
}})
.to_string(),
)
.unwrap();
assert!(
Catalog::load(&path)
.unwrap_err()
.to_string()
.contains("duplicate")
);
}
/// 编号耗尽明确失败且不建立对象
@@ -115,8 +147,6 @@ fn exhausted_object_ids_fail_without_creating_table() {
assert!(catalog.describe("t").is_none());
}
/// 用键值对构造一行
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
@@ -128,7 +158,8 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
/// 空文件得到空字典,未知表返回 None
#[test]
fn empty_file_gives_empty_catalog_and_unknown_table_returns_none() { let dir = tempfile::tempdir().unwrap();
fn empty_file_gives_empty_catalog_and_unknown_table_returns_none() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
let c = Catalog::load(&path).unwrap();
assert_eq!(c.table_names().len(), 0);
@@ -142,7 +173,8 @@ fn ensure_then_describe() {
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1)), ("name", json!("Alice"))])).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1)), ("name", json!("Alice"))]))
.unwrap();
c.save(&path).unwrap();
let c2 = Catalog::load(&path).unwrap();
@@ -164,8 +196,10 @@ fn second_insert_does_not_duplicate_columns() {
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1))])).unwrap();
c.ensure_columns("users", &row(&[("id", json!(2))])).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1))]))
.unwrap();
c.ensure_columns("users", &row(&[("id", json!(2))]))
.unwrap();
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
}
@@ -176,9 +210,11 @@ fn remove_column_drops_definition_stats_and_index() {
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("t", &row(&[("id", json!(1)), ("code", json!(5))])).unwrap();
c.ensure_columns("t", &row(&[("id", json!(1)), ("code", json!(5))]))
.unwrap();
c.add_index("t", "code").unwrap();
c.rebuild_stats("t", &[row(&[("id", json!(1)), ("code", json!(5))])]).unwrap();
c.rebuild_stats("t", &[row(&[("id", json!(1)), ("code", json!(5))])])
.unwrap();
let removed = c.remove_column("t", "code").unwrap();
assert!(removed, "first remove should hit the column");
@@ -186,7 +222,10 @@ fn remove_column_drops_definition_stats_and_index() {
assert_eq!(s.columns.len(), 1);
assert_eq!(s.columns[0].name, "id");
assert!(s.indexes.is_empty(), "index on code should be gone");
assert!(!s.stats.contains_key("code"), "stats for code should be gone");
assert!(
!s.stats.contains_key("code"),
"stats for code should be gone"
);
let again = c.remove_column("t", "code").unwrap();
assert!(!again);
@@ -217,7 +256,8 @@ fn stats_track_a_histogram_for_numeric_columns() {
assert!(name.hist.is_empty(), "字符串列不进直方图");
assert_eq!(name.lo, None);
c.record_insert("t", &row(&[("id", json!(1000)), ("name", json!("x"))])).unwrap();
c.record_insert("t", &row(&[("id", json!(1000)), ("name", json!("x"))]))
.unwrap();
let s = c.describe("t").unwrap();
let id = s.stats.get("id").unwrap();
assert_eq!(id.lo, Some(0.0));
@@ -240,7 +280,9 @@ fn stats_track_a_histogram_for_numeric_columns() {
fn duplicate_deletion_keeps_distinct_until_last_occurrence() {
let mut catalog = Catalog::default();
let duplicate = row(&[("id", json!(1)), ("v", json!(7))]);
catalog.rebuild_stats("t", &[duplicate.clone(), duplicate.clone()]).unwrap();
catalog
.rebuild_stats("t", &[duplicate.clone(), duplicate.clone()])
.unwrap();
catalog.record_delete("t", std::slice::from_ref(&duplicate));
assert_eq!(catalog.describe("t").unwrap().stats["v"].distinct, 1);
catalog.record_delete("t", &[duplicate]);
@@ -251,7 +293,9 @@ fn duplicate_deletion_keeps_distinct_until_last_occurrence() {
#[test]
fn capped_distinct_keeps_histogram_and_other_columns_accurate() {
let mut catalog = Catalog::default();
let rows: Vec<Row> = (0..5000).map(|n| row(&[("id", json!(n)), ("v", json!(n % 2))])).collect();
let rows: Vec<Row> = (0..5000)
.map(|n| row(&[("id", json!(n)), ("v", json!(n % 2))]))
.collect();
catalog.rebuild_stats("t", &rows).unwrap();
let schema = catalog.describe("t").unwrap();
assert!(schema.stats["id"].capped);
@@ -277,7 +321,9 @@ fn rebuilt_histogram_has_exact_buckets_in_any_order() {
let stat = &forward.describe("t").unwrap().stats["v"];
assert_eq!(stat.hist, vec![10; 16]);
assert_eq!(stat.hist, reverse.describe("t").unwrap().stats["v"].hist);
forward.record_insert("t", &row(&[("v", json!(10000))])).unwrap();
forward
.record_insert("t", &row(&[("v", json!(10000))]))
.unwrap();
forward.record_delete("t", &[row(&[("v", json!(10000))])]);
forward.rebuild_stats("t", &rows[80..]).unwrap();
let repaired = &forward.describe("t").unwrap().stats["v"];
@@ -316,7 +362,13 @@ fn legacy_catalog_loads_and_normalizes() {
assert_eq!(s.columns.len(), 2);
assert!(s.columns[0].nullable, "老列默认可空");
assert!(c.normalize_types().unwrap());
let types: Vec<&str> = c.describe("legacy").unwrap().columns.iter().map(|col| col.ty.as_str()).collect();
let types: Vec<&str> = c
.describe("legacy")
.unwrap()
.columns
.iter()
.map(|col| col.ty.as_str())
.collect();
assert_eq!(types, vec!["int", "str"]);
assert!(!c.normalize_types().unwrap(), "第二次没有可改的");
}
+21 -6
View File
@@ -36,8 +36,7 @@ fn defaults_without_file() {
fn file_overrides_only_written_keys() {
let _guard = env_guard();
let path = PathBuf::from("t.toml");
let loaded = config::resolve(Some("[page]\nsize = 8192\n"), Some(path.clone()))
.unwrap();
let loaded = config::resolve(Some("[page]\nsize = 8192\n"), Some(path.clone())).unwrap();
assert_eq!(loaded.config.page_size, 8192);
assert_eq!(loaded.config.btree_order, DEFAULT_BTREE_ORDER);
@@ -45,9 +44,17 @@ fn file_overrides_only_written_keys() {
assert_eq!(loaded.config.log_files, PathBuf::from(DEFAULT_LOG_FILES));
assert_eq!(loaded.config_path, Some(path.clone()));
let page = loaded.origins.iter().find(|(n, _, _)| *n == "page.size").unwrap();
let page = loaded
.origins
.iter()
.find(|(n, _, _)| *n == "page.size")
.unwrap();
assert_eq!(page.2, Origin::File(path));
let order = loaded.origins.iter().find(|(n, _, _)| *n == "btree.order").unwrap();
let order = loaded
.origins
.iter()
.find(|(n, _, _)| *n == "btree.order")
.unwrap();
assert_eq!(order.2, Origin::Default);
}
@@ -110,7 +117,11 @@ fn default_config_path_follows_system_convention() {
let path = config::default_config_path();
assert_eq!(path.file_name().unwrap(), "settings.toml");
assert_eq!(path.parent().unwrap().file_name().unwrap(), "ChuSQL");
assert!(path.ends_with(format!("ChuSQL{}settings.toml", std::path::MAIN_SEPARATOR)), "got {}", path.display());
assert!(
path.ends_with(format!("ChuSQL{}settings.toml", std::path::MAIN_SEPARATOR)),
"got {}",
path.display()
);
// 本机 APPDATA 存在时必须是 %APPDATA%\ChuSQL\settings.toml
#[cfg(windows)]
@@ -168,7 +179,11 @@ fn explicit_missing_path_is_an_error() {
"got {}",
err
);
assert!(err.contains("chusql-no-such-config-9f21.toml"), "got {}", err);
assert!(
err.contains("chusql-no-such-config-9f21.toml"),
"got {}",
err
);
}
/// 不给 --config 且无配置文件时用内置默认
+64 -10
View File
@@ -26,16 +26,40 @@ fn maintenance_rejects_invalid_arguments_and_resets_outputs() {
let mut len = 99;
// SAFETY: 输出均可写;无效输入应在任何读取之前被拒绝。
unsafe {
assert_eq!(chusql_storage_maintenance(ptr::null(), ptr::null(), 0, &mut out, &mut len), -1);
assert_eq!(
chusql_storage_maintenance(ptr::null(), ptr::null(), 0, &mut out, &mut len),
-1
);
assert!(out.is_null());
assert_eq!(len, 0);
len = 99;
assert_eq!(chusql_storage_maintenance(ptr::null(), input.as_ptr(), input.len(), ptr::null_mut(), &mut len), -1);
assert_eq!(
chusql_storage_maintenance(
ptr::null(),
input.as_ptr(),
input.len(),
ptr::null_mut(),
&mut len
),
-1
);
assert_eq!(len, 0);
out = input.as_ptr() as *mut u8;
assert_eq!(chusql_storage_maintenance(ptr::null(), input.as_ptr(), input.len(), &mut out, ptr::null_mut()), -1);
assert_eq!(
chusql_storage_maintenance(
ptr::null(),
input.as_ptr(),
input.len(),
&mut out,
ptr::null_mut()
),
-1
);
assert!(out.is_null());
assert_eq!(chusql_storage_maintenance(ptr::null(), input.as_ptr(), usize::MAX, &mut out, &mut len), -1);
assert_eq!(
chusql_storage_maintenance(ptr::null(), input.as_ptr(), usize::MAX, &mut out, &mut len),
-1
);
}
assert!(out.is_null());
assert_eq!(len, 0);
@@ -47,20 +71,36 @@ fn maintenance_rejects_invalid_arguments_and_resets_outputs() {
fn request_validates_input_and_returns_owned_response() -> Result<(), Box<dyn std::error::Error>> {
let temp = tempfile::tempdir()?;
let config = temp.path().join("settings.toml");
std::fs::write(&config, format!("[storage]\ndata_dir = '{}'\nlog_files = '{}'\n[log]\nlevel = 'off'\n", temp.path().join("data").display(), temp.path().join("logs").display()))?;
std::fs::write(
&config,
format!(
"[storage]\ndata_dir = '{}'\nlog_files = '{}'\n[log]\nlevel = 'off'\n",
temp.path().join("data").display(),
temp.path().join("logs").display()
),
)?;
let storage = Storage::open(config.to_str())?;
let input = br#"{"method":"ping"}"#;
let mut out = ptr::null_mut();
let mut len = 0;
// SAFETY: 句柄和实际请求有效;超长长度必须在构造切片前被拒绝。
unsafe {
assert_eq!(chusql_storage_request(&storage, input.as_ptr(), usize::MAX, &mut out, &mut len), -1);
assert_eq!(
chusql_storage_request(&storage, input.as_ptr(), usize::MAX, &mut out, &mut len),
-1
);
assert!(out.is_null());
assert_eq!(len, 0);
assert_eq!(last_error(), "invalid argument");
assert_eq!(chusql_storage_request(&storage, [0xff].as_ptr(), 1, &mut out, &mut len), -1);
assert_eq!(
chusql_storage_request(&storage, [0xff].as_ptr(), 1, &mut out, &mut len),
-1
);
assert!(last_error().starts_with("request is not valid UTF-8:"));
assert_eq!(chusql_storage_request(&storage, input.as_ptr(), input.len(), &mut out, &mut len), 0);
assert_eq!(
chusql_storage_request(&storage, input.as_ptr(), input.len(), &mut out, &mut len),
0
);
assert!(!out.is_null());
let response = slice::from_raw_parts(out, len).to_vec();
chusql_storage_free(out, len);
@@ -71,7 +111,10 @@ fn request_validates_input_and_returns_owned_response() -> Result<(), Box<dyn st
let config = CString::new(config.to_str().unwrap())?;
// SAFETY: 路径与输出有效;失败不产生需要释放的缓冲。
unsafe {
assert_eq!(chusql_storage_maintenance(config.as_ptr(), [0xff].as_ptr(), 1, &mut out, &mut len), -1);
assert_eq!(
chusql_storage_maintenance(config.as_ptr(), [0xff].as_ptr(), 1, &mut out, &mut len),
-1
);
}
assert!(out.is_null());
assert_eq!(len, 0);
@@ -79,7 +122,18 @@ fn request_validates_input_and_returns_owned_response() -> Result<(), Box<dyn st
let maintenance = br#"{"mode":"reset","user":"root","password_hash":"hash","types":[{"name":"int","base_type":"int","parameterized":false}]}"#;
// SAFETY: 维护仅使用本测试临时目录;成功响应复制后由配套函数释放。
unsafe {
assert_eq!(chusql_storage_maintenance(config.as_ptr(), maintenance.as_ptr(), maintenance.len(), &mut out, &mut len), 0, "{}", last_error());
assert_eq!(
chusql_storage_maintenance(
config.as_ptr(),
maintenance.as_ptr(),
maintenance.len(),
&mut out,
&mut len
),
0,
"{}",
last_error()
);
assert!(!out.is_null());
let response = slice::from_raw_parts(out, len).to_vec();
chusql_storage_free(out, len);
+135 -37
View File
@@ -25,20 +25,61 @@ fn delete_propagates_index_read_errors() {
let original = row(&[("id", json!(1))]);
t.insert_row(&original).unwrap();
t.flush().unwrap();
let file = std::fs::OpenOptions::new().write(true).open(&index).unwrap();
let file = std::fs::OpenOptions::new()
.write(true)
.open(&index)
.unwrap();
file.set_len(DEFAULT_PAGE_SIZE as u64).unwrap();
let err = t.delete_by_keys(&[1]).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
assert_eq!(t.scan().unwrap(), vec![original]);
}
/// 索引键失败不得留下堆行或清空旧表
#[test]
fn invalid_index_keys_leave_heap_and_indexes_unchanged() {
let dir = tempfile::tempdir().unwrap();
let mut table = HeapTable::open(dir.path().join("t.db"), DEFAULT_PAGE_SIZE, 1).unwrap();
let original = row(&[("id", json!(1)), ("name", json!("old"))]);
table.insert_row(&original).unwrap();
table
.build_index(
"name",
&dir.path().join("t.name.idx"),
DEFAULT_PAGE_SIZE,
4,
1,
)
.unwrap();
let invalid = row(&[("id", json!(2)), ("name", json!("x".repeat(129)))]);
assert!(table.insert_row(&invalid).is_err());
assert_eq!(table.scan().unwrap(), vec![original.clone()]);
assert!(table.replace_all(&[invalid]).is_err());
assert_eq!(table.scan().unwrap(), vec![original.clone()]);
assert_eq!(
table.get_by_string_key("name", "old").unwrap(),
Some(original)
);
assert!(
table
.replace_all(&[row(&[("name", json!("x".repeat(10_000)))])])
.is_err()
);
assert_eq!(table.scan().unwrap().len(), 1);
}
/// 跨页投影与分片保持行序及隐藏列
#[test]
fn projected_shards_preserve_order_and_hidden_columns() {
let dir = tempfile::tempdir().unwrap();
let mut t = HeapTable::open(dir.path().join("t.db"), 256, 1).unwrap();
for i in 0..40 {
t.insert_row(&row(&[("id", json!(i)), ("name", json!("alice")), ("secret", json!(i))])).unwrap();
t.insert_row(&row(&[
("id", json!(i)),
("name", json!("alice")),
("secret", json!(i)),
]))
.unwrap();
}
t.set_hidden_columns(["secret".to_string()].into_iter().collect());
let columns = vec!["id".to_string(), "secret".to_string()];
@@ -47,9 +88,15 @@ fn projected_shards_preserve_order_and_hidden_columns() {
let pages = t.num_pages().unwrap();
assert!(pages > 1);
let mut shards = t.scan_columns_pages(Some(&columns), 0, pages / 2).unwrap();
shards.extend(t.scan_columns_pages(Some(&columns), pages / 2, pages + 1).unwrap());
shards.extend(
t.scan_columns_pages(Some(&columns), pages / 2, pages + 1)
.unwrap(),
);
assert_eq!(shards, expected);
assert_eq!(t.read_at(0, 0).unwrap().unwrap(), row(&[("id", json!(0)), ("name", json!("alice"))]));
assert_eq!(
t.read_at(0, 0).unwrap().unwrap(),
row(&[("id", json!(0)), ("name", json!("alice"))])
);
}
/// 扫描与点读均返回行解码错误
@@ -78,15 +125,20 @@ fn empty_index_file_is_rebuilt_from_rows() {
let data = dir.path().join("t.db");
let index = dir.path().join("t.idx");
{
let mut t = HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
let mut t = HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE)
.unwrap();
for i in 1..=5 {
t.insert_row(&row(&[("id", json!(i))])).unwrap();
}
t.flush().unwrap();
}
std::fs::write(&index, b"").unwrap();
let mut t = HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
assert!(t.get_by_column_key("id", 3).unwrap().is_some(), "空索引文件要从堆数据重建");
let mut t =
HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
assert!(
t.get_by_column_key("id", 3).unwrap().is_some(),
"空索引文件要从堆数据重建"
);
assert!(t.get_by_column_key("id", 9).unwrap().is_none());
}
@@ -97,14 +149,24 @@ fn string_index_looks_up_by_value() {
let data = dir.path().join("s.db");
let index = dir.path().join("s.idx");
let mut t = HeapTable::open(&data, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert_row(&row(&[("id", json!(1)), ("name", json!("alice"))])).unwrap();
t.insert_row(&row(&[("id", json!(2)), ("name", json!("bob"))])).unwrap();
t.insert_row(&row(&[("id", json!(3)), ("name", json!(null))])).unwrap();
t.build_index("name", &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
t.insert_row(&row(&[("id", json!(1)), ("name", json!("alice"))]))
.unwrap();
t.insert_row(&row(&[("id", json!(2)), ("name", json!("bob"))]))
.unwrap();
t.insert_row(&row(&[("id", json!(3)), ("name", json!(null))]))
.unwrap();
t.build_index("name", &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE)
.unwrap();
assert_eq!(t.get_by_string_key("name", "bob").unwrap().unwrap()["id"], json!(2));
assert_eq!(
t.get_by_string_key("name", "bob").unwrap().unwrap()["id"],
json!(2)
);
assert!(t.get_by_string_key("name", "dave").unwrap().is_none());
assert!(t.get_by_string_key("name", "").unwrap().is_none(), "NULL 不入索引");
assert!(
t.get_by_string_key("name", "").unwrap().is_none(),
"NULL 不入索引"
);
}
/// 允许重复值建索引:点查把重复的行都取回来
@@ -114,9 +176,12 @@ fn duplicate_values_are_indexed_and_all_returned() {
let data = dir.path().join("d.db");
let index = dir.path().join("d.idx");
let mut t = HeapTable::open(&data, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert_row(&row(&[("id", json!(1)), ("name", json!("alice"))])).unwrap();
t.insert_row(&row(&[("id", json!(2)), ("name", json!("alice"))])).unwrap();
t.build_index("name", &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
t.insert_row(&row(&[("id", json!(1)), ("name", json!("alice"))]))
.unwrap();
t.insert_row(&row(&[("id", json!(2)), ("name", json!("alice"))]))
.unwrap();
t.build_index("name", &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE)
.unwrap();
let mut ids: Vec<i64> = t
.get_all_by_string_key("name", "alice")
@@ -218,9 +283,15 @@ fn replace_all_rebuilds_index() {
])
.unwrap();
assert!(t.get_by_key(1).unwrap().is_none(), "old key must be invalidated");
assert!(
t.get_by_key(1).unwrap().is_none(),
"old key must be invalidated"
);
assert!(t.get_by_key(2).unwrap().is_none());
let got = t.get_by_key(7).unwrap().expect("new row should be indexed by id");
let got = t
.get_by_key(7)
.unwrap()
.expect("new row should be indexed by id");
assert_eq!(got["name"], json!("Zoe"));
assert_eq!(t.scan().unwrap().len(), 2);
}
@@ -239,8 +310,11 @@ fn delete_by_keys_removes_rows() {
.unwrap();
for i in 1..=5 {
t.insert_row(&row(&[("id", json!(i)), ("name", json!(format!("u{}", i)))]))
.unwrap();
t.insert_row(&row(&[
("id", json!(i)),
("name", json!(format!("u{}", i))),
]))
.unwrap();
}
let deleted = t.delete_by_keys(&[2, 4, 99]).unwrap();
@@ -254,7 +328,10 @@ fn delete_by_keys_removes_rows() {
.collect();
assert_eq!(ids, vec![1, 3, 5], "remaining rows must keep their order");
assert!(t.get_by_key(2).unwrap().is_none(), "index entry should be deleted too");
assert!(
t.get_by_key(2).unwrap().is_none(),
"index entry should be deleted too"
);
assert!(t.get_by_key(4).unwrap().is_none());
assert!(t.get_by_key(3).unwrap().is_some());
@@ -284,10 +361,7 @@ fn insert_rows_batch() {
assert_eq!(t.scan().unwrap().len(), 20);
assert_eq!(t.get_by_key(20).unwrap().unwrap()["name"], json!("u20"));
let dup = vec![
row(&[("id", json!(100))]),
row(&[("id", json!(100))]),
];
let dup = vec![row(&[("id", json!(100))]), row(&[("id", json!(100))])];
assert!(t.insert_rows(&dup).is_err());
}
@@ -320,16 +394,23 @@ fn secondary_index_is_maintained() {
)
.unwrap();
assert!(t.has_index("code"));
assert_eq!(t.get_by_column_key("code", 7002).unwrap().unwrap()["id"], json!(2));
assert_eq!(
t.get_by_column_key("code", 7002).unwrap().unwrap()["id"],
json!(2)
);
t.insert_row(&row(&[("id", json!(3)), ("code", json!(7003))]))
.unwrap();
assert_eq!(t.get_by_column_key("code", 7003).unwrap().unwrap()["id"], json!(3));
assert_eq!(
t.get_by_column_key("code", 7003).unwrap().unwrap()["id"],
json!(3)
);
t.delete_by_keys(&[2]).unwrap();
assert!(t.get_by_column_key("code", 7002).unwrap().is_none());
t.insert_row(&row(&[("id", json!(4)), ("code", json!(7001))])).unwrap();
t.insert_row(&row(&[("id", json!(4)), ("code", json!(7001))]))
.unwrap();
let mut dup_ids: Vec<i64> = t
.get_all_by_column_key("code", 7001)
.unwrap()
@@ -339,7 +420,12 @@ fn secondary_index_is_maintained() {
dup_ids.sort_unstable();
assert_eq!(dup_ids, vec![1, 4], "duplicate index values keep both rows");
let mut t2 = HeapTable::open(dir.path().join("d.db"), DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let mut t2 = HeapTable::open(
dir.path().join("d.db"),
DEFAULT_PAGE_SIZE,
DEFAULT_POOL_SIZE,
)
.unwrap();
t2.insert_rows(&[
row(&[("id", json!(1)), ("age", json!(30))]),
row(&[("id", json!(2)), ("age", json!(30))]),
@@ -360,7 +446,6 @@ fn secondary_index_is_maintained() {
);
}
/// 整表整理要收回删行留下的页内空洞
#[test]
fn compact_pages_reclaims_dead_space() {
@@ -370,7 +455,8 @@ fn compact_pages_reclaims_dead_space() {
let pad = "x".repeat(200);
for i in 0..10 {
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))]))
.unwrap();
}
let pages = t.num_pages().unwrap();
@@ -395,7 +481,8 @@ fn deletes_compact_pages_so_space_is_reused() {
let pad = "y".repeat(80);
for i in 0..30 {
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))]))
.unwrap();
}
assert_eq!(t.num_pages().unwrap(), 1, "30 行要先塞进一页");
@@ -404,7 +491,8 @@ fn deletes_compact_pages_so_space_is_reused() {
t.delete_by_keys(&keys).unwrap();
for i in 100..119 {
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))]))
.unwrap();
}
assert_eq!(t.num_pages().unwrap(), 1, "删掉的空间要能被复用");
assert_eq!(t.scan().unwrap().len(), 30);
@@ -416,11 +504,13 @@ fn compact_pages_keeps_index_positions_valid() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("t.db");
let index = dir.path().join("t.idx");
let mut t = HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
let mut t =
HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
let pad = "z".repeat(80);
for i in 0..30 {
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))])).unwrap();
t.insert_row(&row(&[("id", json!(i)), ("pad", json!(pad))]))
.unwrap();
}
let keys: Vec<i64> = (5..25).collect();
t.delete_by_keys(&keys).unwrap();
@@ -428,9 +518,17 @@ fn compact_pages_keeps_index_positions_valid() {
for i in [0, 1, 2, 3, 4, 25, 26, 27, 28, 29] {
let found = t.get_by_column_key("id", i).unwrap();
assert_eq!(found.as_ref().map(|r| r["pad"].clone()), Some(json!(pad)), "整理后 id={} 仍要能点查", i);
assert_eq!(
found.as_ref().map(|r| r["pad"].clone()),
Some(json!(pad)),
"整理后 id={} 仍要能点查",
i
);
}
for i in 5..25 {
assert!(t.get_by_column_key("id", i).unwrap().is_none(), "删掉的行不该再查出来");
assert!(
t.get_by_column_key("id", i).unwrap().is_none(),
"删掉的行不该再查出来"
);
}
}
+144
View File
@@ -142,3 +142,147 @@ fn dirty_page_flushed_on_drop() {
let p = pf2.read_page(0).unwrap();
assert_eq!(p.data[0], 42);
}
/// 已同步镜像恢复撕裂页且页 LSN 持久保留
#[test]
fn full_page_image_repairs_torn_write_before_read() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("torn.db");
let metadata = dir.path().join("torn.db.pages");
let journal = dir.path().join("torn.db.page-wal");
let mut file = PageFile::with_options(&path, 512, 1).unwrap();
file.set_lsn(7);
file.write_page(&Page::new(0, 512)).unwrap();
file.flush().unwrap();
let old_metadata = std::fs::read(&metadata).unwrap();
file.set_lsn(13);
let mut changed = Page::new(0, 512);
changed.data.fill(0x5a);
file.write_page(&changed).unwrap();
file.append_page().unwrap(); // 淘汰页零前镜像已同步
let image = std::fs::read(&journal).unwrap();
assert!(!image.is_empty());
drop(file);
std::fs::write(&metadata, old_metadata).unwrap();
std::fs::write(&journal, &image).unwrap();
std::fs::write(&path, [0x11; 213]).unwrap();
let mut reopened = PageFile::open(&path, 512).unwrap();
assert_eq!(reopened.read_page(0).unwrap().data, changed.data);
assert_eq!(reopened.page_lsn(0), Some(13));
drop(reopened);
assert_eq!(std::fs::metadata(&journal).unwrap().len(), 0);
let mut reopened = PageFile::open(&path, 512).unwrap();
assert_eq!(reopened.read_page(0).unwrap().data, changed.data);
assert_eq!(reopened.page_lsn(0), Some(13));
}
/// 撕裂日志尾不发布,完整损坏帧明确拒绝
#[test]
fn page_image_tail_and_corruption_are_distinguished() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tail.db");
let metadata = dir.path().join("tail.db.pages");
let journal = dir.path().join("tail.db.page-wal");
let mut file = PageFile::with_options(&path, 512, 1).unwrap();
file.write_page(&Page::new(0, 512)).unwrap();
file.flush().unwrap();
let old_metadata = std::fs::read(&metadata).unwrap();
let old_data = std::fs::read(&path).unwrap();
file.set_lsn(20);
let mut changed = Page::new(0, 512);
changed.data.fill(9);
file.write_page(&changed).unwrap();
file.append_page().unwrap();
let image = std::fs::read(&journal).unwrap();
drop(file);
for end in [1, 39, image.len() - 1] {
std::fs::write(&metadata, &old_metadata).unwrap();
std::fs::write(&path, &old_data).unwrap();
std::fs::write(&journal, &image[..end]).unwrap();
let mut reopened = PageFile::open(&path, 512).unwrap();
assert_eq!(reopened.read_page(0).unwrap().data, vec![0; 512]);
}
let mut corrupt = image;
corrupt[40] ^= 1;
std::fs::write(&metadata, &old_metadata).unwrap();
std::fs::write(&journal, &corrupt).unwrap();
assert!(
PageFile::open(&path, 512)
.unwrap_err()
.to_string()
.contains("checksum")
);
assert_eq!(std::fs::read(&journal).unwrap(), corrupt);
}
/// 截断标记阻止旧页重放后复活
#[test]
fn truncate_clears_page_versions_and_survives_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("reset.db");
let mut file = PageFile::open(&path, 512).unwrap();
file.set_lsn(5);
for _ in 0..3 {
file.append_page().unwrap();
}
file.flush().unwrap();
file.set_lsn(8);
file.truncate().unwrap();
file.append_page().unwrap();
file.flush().unwrap();
assert_eq!(file.page_lsn(0), Some(8));
assert_eq!(file.page_lsn(2), None);
drop(file);
let mut file = PageFile::open(&path, 512).unwrap();
assert_eq!(file.num_pages().unwrap(), 1);
assert_eq!(file.page_lsn(0), Some(8));
assert_eq!(file.page_lsn(2), None);
}
/// 镜像写入失败后禁止析构或重试绕过恢复
#[test]
fn page_journal_error_fences_further_flushes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fenced.db");
let journal = dir.path().join("fenced.db.page-wal");
std::fs::write(&path, [0; 512]).unwrap();
let mut file = PageFile::open(&path, 512).unwrap();
let mut page = Page::new(0, 512);
page.data.fill(7);
file.write_page(&page).unwrap();
std::fs::create_dir(&journal).unwrap();
assert!(file.flush().is_err());
std::fs::remove_dir(&journal).unwrap();
assert!(
file.flush()
.unwrap_err()
.to_string()
.contains("requires recovery")
);
drop(file);
let mut file = PageFile::open(&path, 512).unwrap();
assert_eq!(file.read_page(0).unwrap().data, vec![0; 512]);
}
/// 小缓存淘汰不会反复记录同一批未改页
#[test]
fn eviction_batches_images_without_pool_sized_write_amplification() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bounded.db");
let journal = dir.path().join("bounded.db.page-wal");
let mut file = PageFile::with_options(&path, 512, 2).unwrap();
file.set_lsn(9);
for id in 0..100 {
let mut page = Page::new(id, 512);
page.data.fill(id as u8);
file.write_page(&page).unwrap();
}
assert!(std::fs::metadata(&journal).unwrap().len() <= 100 * (512 + 40));
file.flush().unwrap();
drop(file);
let mut file = PageFile::open(&path, 512).unwrap();
for id in 0..100 {
assert_eq!(file.read_page(id).unwrap().data, vec![id as u8; 512]);
assert_eq!(file.page_lsn(id), Some(9));
}
}
File diff suppressed because it is too large. Load diff
+68 -17
View File
@@ -1,5 +1,5 @@
use chusql_core_storage::protocol::Row;
use chusql_core_storage::wal::{committed_ops, records_after, Wal, WalOp};
use chusql_core_storage::wal::{Wal, WalOp, committed_ops, records_after};
use serde_json::json;
// WAL 测试:追加、读取、清空、截断尾巴、LSN 与提交标记。
@@ -11,12 +11,32 @@ fn schema_versions_round_trip_and_legacy_hide_decodes() {
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) },
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);
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);
@@ -26,9 +46,14 @@ fn schema_versions_round_trip_and_legacy_hide_decodes() {
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,
}));
assert_eq!(
Wal::new(&path).read().unwrap(),
Some(WalOp::HideColumn {
table: "t".into(),
column: "name".into(),
schema_version: None,
})
);
}
/// 建表编号完整往返且兼容旧载荷
@@ -37,7 +62,11 @@ 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) };
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);
@@ -50,11 +79,16 @@ fn create_table_ids_round_trip_and_legacy_payloads_decode() {
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 });
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();
@@ -167,7 +201,10 @@ fn every_record_gets_an_increasing_lsn() {
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());
assert_eq!(
scan.good_len as u64,
std::fs::metadata(&path).unwrap().len()
);
let reopened = Wal::new(&path);
reopened
@@ -254,7 +291,10 @@ fn trailing_torn_frame_can_be_truncated() {
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!(
std::fs::metadata(&path).unwrap().len(),
scan.good_len as u64
);
assert_eq!(wal.read_all().unwrap().records.len(), 1);
}
@@ -360,9 +400,21 @@ fn lsn_continues_from_the_checkpoint() {
#[test]
fn records_after_keeps_only_the_tail() {
let records = vec![
(1u64, WalOp::Insert { table: "t".into(), row: row(&[("id", json!(1))]) }),
(
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))]) }),
(
3,
WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(2))]),
},
),
(4, WalOp::Commit),
];
let kept = records_after(&records, 2);
@@ -371,4 +423,3 @@ fn records_after_keeps_only_the_tail() {
assert_eq!(records_after(&records, 4).len(), 0);
assert_eq!(records_after(&records, 0).len(), 4);
}