大改,整理项目结构,完成rust ffi调用,写好tcp接口
This commit is contained in:
1 parent
df6d348995
commit
9758439b82
118 files changed
+8103
-6321
No files matched your search
@@ -0,0 +1,257 @@
|
||||
use chusql_core_storage::btree::{fits_in_page, DiskBTree, MAX_KEY_BYTES};
|
||||
use chusql_core_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
|
||||
use chusql_core_storage::page::DEFAULT_POOL_SIZE;
|
||||
|
||||
// B+ 树测试:字节键、重复键、范围扫描、重开与配置校验。
|
||||
|
||||
/// 按默认参数打开指定索引文件
|
||||
fn open(dir: &std::path::Path, name: &str) -> DiskBTree {
|
||||
DiskBTree::open(dir.join(name), DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap()
|
||||
}
|
||||
|
||||
/// 整数键编码成保序的 8 字节(与堆表一致)
|
||||
fn key(n: i64) -> [u8; 8] {
|
||||
(n as u64 ^ (1u64 << 63)).to_be_bytes()
|
||||
}
|
||||
|
||||
/// 空树查不到,遍历为空
|
||||
#[test]
|
||||
fn empty_tree_get() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
assert_eq!(t.get(&key(1)).unwrap(), None);
|
||||
assert_eq!(t.iter_all().unwrap(), vec![]);
|
||||
}
|
||||
|
||||
/// 插 100 个键都能查回
|
||||
#[test]
|
||||
fn insert_then_get() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in 0..100 {
|
||||
t.insert(&key(i), i as u64 * 10).unwrap();
|
||||
}
|
||||
for i in 0..100 {
|
||||
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64 * 10));
|
||||
}
|
||||
}
|
||||
|
||||
/// 重开文件数据还在
|
||||
#[test]
|
||||
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();
|
||||
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();
|
||||
for i in 0..50 {
|
||||
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64));
|
||||
}
|
||||
}
|
||||
|
||||
/// 同一个键可以挂多个位置,按值升序返回
|
||||
#[test]
|
||||
fn duplicate_keys_keep_every_value() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "dup.idx");
|
||||
t.insert(b"same", 30).unwrap();
|
||||
t.insert(b"same", 10).unwrap();
|
||||
t.insert(b"same", 20).unwrap();
|
||||
assert_eq!(t.get_all(b"same").unwrap(), vec![10, 20, 30]);
|
||||
assert_eq!(t.get(b"same").unwrap(), Some(10));
|
||||
assert!(t.delete(b"same", 10).unwrap());
|
||||
assert_eq!(t.get_all(b"same").unwrap(), vec![20, 30]);
|
||||
assert!(!t.delete(b"same", 10).unwrap());
|
||||
}
|
||||
|
||||
/// 倒序插入后遍历仍升序
|
||||
#[test]
|
||||
fn iter_all_sorted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in (0..50).rev() {
|
||||
t.insert(&key(i), i as u64).unwrap();
|
||||
}
|
||||
let all = t.iter_all().unwrap();
|
||||
assert_eq!(all.len(), 50);
|
||||
for (i, (k, _)) in all.iter().enumerate() {
|
||||
assert_eq!(k.as_slice(), key(i as i64).as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
/// 页大小或 order 不符要报错
|
||||
#[test]
|
||||
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();
|
||||
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());
|
||||
}
|
||||
|
||||
/// 不是索引文件要报错
|
||||
#[test]
|
||||
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());
|
||||
}
|
||||
|
||||
/// order 放不进一页要报错
|
||||
#[test]
|
||||
fn order_too_large_for_page_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300, DEFAULT_POOL_SIZE).is_err());
|
||||
assert!(!fits_in_page(64, DEFAULT_BTREE_ORDER));
|
||||
}
|
||||
|
||||
/// 删掉一个键值之后查不到,别人不受影响
|
||||
#[test]
|
||||
fn delete_removes_only_that_key() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in 0..200 {
|
||||
t.insert(&key(i), i as u64 + 1).unwrap();
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
assert!(!t.delete(&key(50), 51).unwrap());
|
||||
|
||||
let all = t.iter_all().unwrap();
|
||||
assert_eq!(all.len(), 199);
|
||||
assert!(all.iter().all(|(k, _)| k.as_slice() != key(50).as_slice()));
|
||||
}
|
||||
|
||||
/// 删过之后树还能继续插
|
||||
#[test]
|
||||
fn insert_after_delete_still_works() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in 0..300 {
|
||||
t.insert(&key(i), i as u64).unwrap();
|
||||
}
|
||||
for i in (0..300).step_by(2) {
|
||||
assert!(t.delete(&key(i), i as u64).unwrap());
|
||||
}
|
||||
for i in 1000..1100 {
|
||||
t.insert(&key(i), i as u64).unwrap();
|
||||
}
|
||||
for i in 0..300 {
|
||||
let want = if i % 2 == 0 { None } else { Some(i as u64) };
|
||||
assert_eq!(t.get(&key(i)).unwrap(), want, "key {}", i);
|
||||
}
|
||||
for i in 1000..1100 {
|
||||
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64));
|
||||
}
|
||||
}
|
||||
|
||||
/// 变长键能插能查,最长键也支持
|
||||
#[test]
|
||||
fn variable_length_keys_round_trip() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "var.idx");
|
||||
t.insert(b"a", 1).unwrap();
|
||||
t.insert(b"alice", 2).unwrap();
|
||||
t.insert(&[b'z'; MAX_KEY_BYTES], 3).unwrap();
|
||||
t.flush().unwrap();
|
||||
|
||||
assert_eq!(t.get(b"a").unwrap(), Some(1));
|
||||
assert_eq!(t.get(b"alice").unwrap(), Some(2));
|
||||
assert_eq!(t.get(b"bob").unwrap(), None);
|
||||
assert_eq!(t.get(&[b'z'; MAX_KEY_BYTES]).unwrap(), Some(3));
|
||||
assert_eq!(t.get(&[b'z'; MAX_KEY_BYTES - 1]).unwrap(), None);
|
||||
}
|
||||
|
||||
/// 超出键长上限直接报错,不写坏节点
|
||||
#[test]
|
||||
fn oversized_key_is_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "long.idx");
|
||||
let too_long = vec![b'k'; MAX_KEY_BYTES + 1];
|
||||
assert!(t.insert(&too_long, 1).is_err());
|
||||
assert_eq!(t.get(&too_long).unwrap(), None);
|
||||
}
|
||||
|
||||
/// 插满一页后会分裂,顺序仍然正确
|
||||
#[test]
|
||||
fn splits_keep_keys_sorted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "split.idx");
|
||||
|
||||
let mut expected: Vec<Vec<u8>> = Vec::new();
|
||||
for i in 0..600 {
|
||||
let k = format!("key-{:04}", (i * 7919) % 600).into_bytes();
|
||||
t.insert(&k, i as u64).unwrap();
|
||||
expected.push(k);
|
||||
}
|
||||
expected.sort();
|
||||
expected.dedup();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// 范围扫描走叶子链,两端都是闭区间
|
||||
#[test]
|
||||
fn range_scan_walks_the_leaf_chain() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "range.idx");
|
||||
for word in ["apple", "banana", "cherry", "date", "elderberry"] {
|
||||
t.insert(word.as_bytes(), word.len() as u64).unwrap();
|
||||
}
|
||||
|
||||
let mid: Vec<String> = t
|
||||
.scan(Some(b"banana"), Some(b"date"))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(k, _)| String::from_utf8(k).unwrap())
|
||||
.collect();
|
||||
assert_eq!(mid, vec!["banana", "cherry", "date"]);
|
||||
|
||||
let from: Vec<String> = t
|
||||
.scan(Some(b"cherry"), None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(k, _)| String::from_utf8(k).unwrap())
|
||||
.collect();
|
||||
assert_eq!(from, vec!["cherry", "date", "elderberry"]);
|
||||
|
||||
let until: Vec<String> = t
|
||||
.scan(None, Some(b"banana"))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(k, _)| String::from_utf8(k).unwrap())
|
||||
.collect();
|
||||
assert_eq!(until, vec!["apple", "banana"]);
|
||||
|
||||
assert!(t.scan(Some(b"z"), None).unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// 清空之后还能接着用
|
||||
#[test]
|
||||
fn clear_resets_the_tree() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = open(dir.path(), "clear.idx");
|
||||
t.insert(b"a", 1).unwrap();
|
||||
t.insert(b"b", 2).unwrap();
|
||||
t.clear().unwrap();
|
||||
assert!(t.iter_all().unwrap().is_empty());
|
||||
t.insert(b"c", 3).unwrap();
|
||||
assert_eq!(t.get(b"c").unwrap(), Some(3));
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use chusql_core_storage::catalog::Catalog;
|
||||
use chusql_core_storage::protocol::Row;
|
||||
use serde_json::json;
|
||||
|
||||
// 数据字典测试:补列、去重、未知表。
|
||||
|
||||
|
||||
|
||||
/// 用键值对构造一行
|
||||
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
|
||||
}
|
||||
|
||||
/// 空文件得到空字典,未知表返回 None
|
||||
#[test]
|
||||
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);
|
||||
assert!(c.describe("nope").is_none());
|
||||
}
|
||||
|
||||
/// 补列之后能查到 schema
|
||||
#[test]
|
||||
fn ensure_then_describe() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
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"))]));
|
||||
c.save(&path).unwrap();
|
||||
|
||||
let c2 = Catalog::load(&path).unwrap();
|
||||
let s = c2.describe("users").unwrap();
|
||||
assert_eq!(s.columns.len(), 2);
|
||||
let names: Vec<&str> = s.columns.iter().map(|c| c.name.as_str()).collect();
|
||||
assert!(names.contains(&"id"));
|
||||
assert!(names.contains(&"name"));
|
||||
let id_col = s.columns.iter().find(|c| c.name == "id").unwrap();
|
||||
assert_eq!(id_col.ty, "int");
|
||||
let name_col = s.columns.iter().find(|c| c.name == "name").unwrap();
|
||||
assert_eq!(name_col.ty, "str");
|
||||
}
|
||||
|
||||
/// 再插入不会重复列
|
||||
#[test]
|
||||
fn second_insert_does_not_duplicate_columns() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("catalog.json");
|
||||
|
||||
let mut c = Catalog::load(&path).unwrap();
|
||||
c.ensure_columns("users", &row(&[("id", json!(1))]));
|
||||
c.ensure_columns("users", &row(&[("id", json!(2))]));
|
||||
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
|
||||
}
|
||||
|
||||
/// 删列同时清掉统计与索引
|
||||
#[test]
|
||||
fn remove_column_drops_definition_stats_and_index() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
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))]));
|
||||
c.add_index("t", "code").unwrap();
|
||||
c.rebuild_stats("t", &[row(&[("id", json!(1)), ("code", json!(5))])]);
|
||||
|
||||
let removed = c.remove_column("t", "code").unwrap();
|
||||
assert!(removed, "first remove should hit the column");
|
||||
let s = c.describe("t").unwrap();
|
||||
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");
|
||||
|
||||
let again = c.remove_column("t", "code").unwrap();
|
||||
assert!(!again);
|
||||
|
||||
assert!(c.remove_column("nope", "code").is_err());
|
||||
}
|
||||
|
||||
/// 老类型名并到新写法,参数保留,未知类型原样
|
||||
#[test]
|
||||
fn normalize_type_maps_legacy_names() {
|
||||
use chusql_core_storage::catalog::normalize_type;
|
||||
assert_eq!(normalize_type("integer"), "int");
|
||||
assert_eq!(normalize_type("TEXT"), "str");
|
||||
assert_eq!(normalize_type("BOOLEAN"), "bool");
|
||||
assert_eq!(normalize_type("varchar(20)"), "varchar(20)");
|
||||
assert_eq!(normalize_type("numeric(8,2)"), "decimal(8,2)");
|
||||
assert_eq!(normalize_type("real"), "float");
|
||||
assert_eq!(normalize_type("weird"), "weird");
|
||||
}
|
||||
|
||||
/// 老格式字典(列里没有新字段)能读进来,规范化后写回新写法
|
||||
#[test]
|
||||
fn legacy_catalog_loads_and_normalizes() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("catalog.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"{"tables":{"legacy":{"columns":[{"name":"id","ty":"integer"},{"name":"name","ty":"text"}]}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut c = Catalog::load(&path).unwrap();
|
||||
let s = c.describe("legacy").unwrap();
|
||||
assert_eq!(s.columns.len(), 2);
|
||||
assert!(s.columns[0].nullable, "老列默认可空");
|
||||
assert!(c.normalize_types());
|
||||
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(), "第二次没有可改的");
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use chusql_core_storage::config::{self, Config, DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE, Origin};
|
||||
use chusql_core_storage::log::Level;
|
||||
|
||||
// 配置测试:默认值、文件覆盖、环境变量彻底无效、非法值报错。
|
||||
|
||||
/// set_var 改的是进程环境,用例之间要串行,免得互相污染
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// 拿锁(前面的用例 panic 过也照样拿)
|
||||
fn env_guard() -> std::sync::MutexGuard<'static, ()> {
|
||||
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 无文件时全默认
|
||||
#[test]
|
||||
fn defaults_without_file() {
|
||||
let _guard = env_guard();
|
||||
let loaded = config::resolve(None, None).unwrap();
|
||||
assert_eq!(loaded.config, Config::default());
|
||||
assert!(loaded.config_path.is_none());
|
||||
assert!(loaded.origins.iter().all(|(_, _, o)| *o == Origin::Default));
|
||||
assert_eq!(loaded.config.page_size, DEFAULT_PAGE_SIZE);
|
||||
assert_eq!(loaded.config.btree_order, DEFAULT_BTREE_ORDER);
|
||||
assert_eq!(loaded.config.data_dir, config::default_data_dir());
|
||||
assert_eq!(loaded.config.log_level, Level::Info);
|
||||
}
|
||||
|
||||
/// 只覆盖文件里写了的项
|
||||
#[test]
|
||||
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();
|
||||
|
||||
assert_eq!(loaded.config.page_size, 8192);
|
||||
assert_eq!(loaded.config.btree_order, DEFAULT_BTREE_ORDER);
|
||||
assert_eq!(loaded.config.data_dir, config::default_data_dir());
|
||||
assert_eq!(loaded.config_path, Some(path.clone()));
|
||||
|
||||
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();
|
||||
assert_eq!(order.2, Origin::Default);
|
||||
}
|
||||
|
||||
/// 五项全写都生效
|
||||
#[test]
|
||||
fn file_all_keys() {
|
||||
let _guard = env_guard();
|
||||
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[buffer]\npool_size = 128\n[storage]\ndata_dir = \"mydata\"\n[log]\nlevel = \"debug\"\n";
|
||||
let path = PathBuf::from("t.toml");
|
||||
let loaded = config::resolve(Some(text), Some(path.clone())).unwrap();
|
||||
|
||||
assert_eq!(loaded.config.page_size, 8192);
|
||||
assert_eq!(loaded.config.btree_order, 32);
|
||||
assert_eq!(loaded.config.pool_size, 128);
|
||||
assert_eq!(loaded.config.data_dir, PathBuf::from("mydata"));
|
||||
assert_eq!(loaded.config.log_level, Level::Debug);
|
||||
assert!(
|
||||
loaded
|
||||
.origins
|
||||
.iter()
|
||||
.all(|(_, _, o)| *o == Origin::File(path.clone()))
|
||||
);
|
||||
}
|
||||
|
||||
/// 设了 CHUSQL_* 环境变量也不生效
|
||||
#[test]
|
||||
fn env_vars_are_ignored_entirely() {
|
||||
let _guard = env_guard();
|
||||
unsafe {
|
||||
std::env::set_var("CHUSQL_DATA_DIR", "from-env");
|
||||
std::env::set_var("CHUSQL_PAGE_SIZE", "8192");
|
||||
}
|
||||
|
||||
let text = "[storage]\ndata_dir = \"from-file\"\n";
|
||||
let loaded = config::resolve(Some(text), Some(PathBuf::from("t.toml"))).unwrap();
|
||||
assert_eq!(loaded.config.data_dir, PathBuf::from("from-file"));
|
||||
assert_eq!(loaded.config.page_size, DEFAULT_PAGE_SIZE);
|
||||
let dir = loaded
|
||||
.origins
|
||||
.iter()
|
||||
.find(|(n, _, _)| *n == "storage.data_dir")
|
||||
.unwrap();
|
||||
assert_eq!(dir.2, Origin::File(PathBuf::from("t.toml")));
|
||||
|
||||
let builtin = config::resolve(None, None).unwrap();
|
||||
assert_eq!(builtin.config.data_dir, config::default_data_dir());
|
||||
assert_eq!(builtin.config.page_size, DEFAULT_PAGE_SIZE);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("CHUSQL_DATA_DIR");
|
||||
std::env::remove_var("CHUSQL_PAGE_SIZE");
|
||||
}
|
||||
}
|
||||
|
||||
/// 固定配置路径在系统配置目录的 ChuSQL 下
|
||||
#[test]
|
||||
fn default_config_path_follows_system_convention() {
|
||||
let _guard = env_guard();
|
||||
let path = config::default_config_path();
|
||||
assert_eq!(path.file_name().unwrap(), "chusql.toml");
|
||||
assert_eq!(path.parent().unwrap().file_name().unwrap(), "ChuSQL");
|
||||
assert!(path.ends_with(format!("ChuSQL{}chusql.toml", std::path::MAIN_SEPARATOR)), "got {}", path.display());
|
||||
|
||||
// 本机 APPDATA 存在时必须是 %APPDATA%\ChuSQL\chusql.toml
|
||||
#[cfg(windows)]
|
||||
if let Some(appdata) = std::env::var_os("APPDATA") {
|
||||
assert_eq!(
|
||||
path,
|
||||
PathBuf::from(appdata).join("ChuSQL").join("chusql.toml")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 默认数据目录落地按系统惯例
|
||||
#[test]
|
||||
fn default_data_dir_follows_system_convention() {
|
||||
let _guard = env_guard();
|
||||
let dir = config::default_data_dir();
|
||||
let expected_parent = if cfg!(windows) { "ChuSQL" } else { "chusql" };
|
||||
assert_eq!(dir.file_name().unwrap(), "data");
|
||||
assert_eq!(dir.parent().unwrap().file_name().unwrap(), expected_parent);
|
||||
// 老的内置默认是仓库里的相对路径 ../localdata,现在不能再指回仓库
|
||||
assert_ne!(dir, PathBuf::from("../localdata"));
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(local) = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty()) {
|
||||
assert_eq!(dir, PathBuf::from(local).join("ChuSQL").join("data"));
|
||||
} else if let Some(roaming) = std::env::var_os("APPDATA").filter(|v| !v.is_empty()) {
|
||||
assert_eq!(dir, PathBuf::from(roaming).join("ChuSQL").join("data"));
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
if let Some(xdg) = std::env::var_os("XDG_DATA_HOME").filter(|v| !v.is_empty()) {
|
||||
assert_eq!(dir, PathBuf::from(xdg).join("chusql").join("data"));
|
||||
} else if let Some(home) = std::env::var_os("HOME").filter(|v| !v.is_empty()) {
|
||||
assert_eq!(
|
||||
dir,
|
||||
PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("chusql")
|
||||
.join("data")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 显式给了一个不存在的路径:必须报错,不能静默回退默认
|
||||
#[test]
|
||||
fn explicit_missing_path_is_an_error() {
|
||||
let _guard = env_guard();
|
||||
let missing = std::env::temp_dir().join("chusql-no-such-config-9f21.toml");
|
||||
assert!(!missing.is_file());
|
||||
|
||||
let err = config::load(Some(missing.to_str().unwrap())).unwrap_err();
|
||||
assert!(
|
||||
err.contains("config file not found") || err.contains("cannot read config file"),
|
||||
"got {}",
|
||||
err
|
||||
);
|
||||
assert!(err.contains("chusql-no-such-config-9f21.toml"), "got {}", err);
|
||||
}
|
||||
|
||||
/// 不给 --config 且无配置文件时用内置默认
|
||||
#[test]
|
||||
fn load_without_explicit_path_uses_builtin_defaults() {
|
||||
let _guard = env_guard();
|
||||
let default = config::default_config_path();
|
||||
let loaded = config::load(None).unwrap();
|
||||
if default.is_file() {
|
||||
assert_eq!(loaded.config_path, Some(default));
|
||||
} else {
|
||||
assert_eq!(loaded.config, Config::default());
|
||||
assert_eq!(loaded.config.data_dir, config::default_data_dir());
|
||||
assert!(loaded.origins.iter().all(|(_, _, o)| *o == Origin::Default));
|
||||
assert!(loaded.config_path.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// [server] 分区归别的层管;本层拼写错误要报错
|
||||
#[test]
|
||||
fn server_section_belongs_to_another_layer() {
|
||||
let _guard = env_guard();
|
||||
let db_server = "[server]\nhost = \"0.0.0.0\"\nport = 7778\nmax_rows = 10\n";
|
||||
let loaded = config::resolve(Some(db_server), None).unwrap();
|
||||
assert_eq!(loaded.config.data_dir, config::default_data_dir());
|
||||
|
||||
let typo = "[storage]\ndata_dirs = \"oops\"\n";
|
||||
assert!(config::resolve(Some(typo), None).is_err());
|
||||
}
|
||||
|
||||
/// 日志等级非法要报错
|
||||
#[test]
|
||||
fn rejects_bad_log_level() {
|
||||
let _guard = env_guard();
|
||||
let err = config::resolve(Some("[log]\nlevel = \"loud\"\n"), None).unwrap_err();
|
||||
assert!(err.contains("log.level"), "got {}", err);
|
||||
}
|
||||
|
||||
/// 页大小或 order 非法都要报错
|
||||
#[test]
|
||||
fn rejects_bad_numeric_limits() {
|
||||
let _guard = env_guard();
|
||||
for text in ["[page]\nsize = 3000\n", "[page]\nsize = 128\n"] {
|
||||
let err = config::resolve(Some(text), None).unwrap_err();
|
||||
assert!(err.contains("page.size"), "{}: {}", text, err);
|
||||
}
|
||||
|
||||
let err = config::resolve(Some("[btree]\norder = 2\n"), None).unwrap_err();
|
||||
assert!(err.contains("btree.order"), "got {}", err);
|
||||
|
||||
let err = config::resolve(Some("[btree]\norder = 300\n"), None).unwrap_err();
|
||||
assert!(err.contains("does not fit"), "got {}", err);
|
||||
}
|
||||
|
||||
/// 未知配置键要报错
|
||||
#[test]
|
||||
fn rejects_unknown_key() {
|
||||
let _guard = env_guard();
|
||||
let err = config::resolve(Some("[page]\nsizes = 4096\n"), None).unwrap_err();
|
||||
assert!(err.contains("bad config"), "got {}", err);
|
||||
}
|
||||
|
||||
/// 坏的页面尺寸只能来自文件
|
||||
#[test]
|
||||
fn rejects_bad_page_size_from_file() {
|
||||
let _guard = env_guard();
|
||||
unsafe { std::env::set_var("CHUSQL_PAGE_SIZE", "big") };
|
||||
|
||||
let err = config::resolve(Some("[page]\nsize = 3000\n"), None).unwrap_err();
|
||||
assert!(err.contains("page.size"), "got {}", err);
|
||||
|
||||
unsafe { std::env::remove_var("CHUSQL_PAGE_SIZE") };
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
use chusql_core_storage::config::DEFAULT_PAGE_SIZE;
|
||||
use chusql_core_storage::heap::HeapTable;
|
||||
use chusql_core_storage::page::DEFAULT_POOL_SIZE;
|
||||
use chusql_core_storage::protocol::Row;
|
||||
use serde_json::json;
|
||||
|
||||
// 堆表测试:插查、跨页、大行、重开、索引重建。
|
||||
|
||||
/// 用键值对构造一行
|
||||
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 empty_index_file_is_rebuilt_from_rows() {
|
||||
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();
|
||||
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(), "空索引文件要从堆数据重建");
|
||||
assert!(t.get_by_column_key("id", 9).unwrap().is_none());
|
||||
}
|
||||
|
||||
/// 字符串列能真正建索引:按值点查,NULL 不入索引
|
||||
#[test]
|
||||
fn string_index_looks_up_by_value() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
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();
|
||||
|
||||
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 不入索引");
|
||||
}
|
||||
|
||||
/// 允许重复值建索引:点查把重复的行都取回来
|
||||
#[test]
|
||||
fn duplicate_values_are_indexed_and_all_returned() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
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();
|
||||
|
||||
let mut ids: Vec<i64> = t
|
||||
.get_all_by_string_key("name", "alice")
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|r| r["id"].as_i64())
|
||||
.collect();
|
||||
ids.sort_unstable();
|
||||
assert_eq!(ids, vec![1, 2]);
|
||||
assert!(t.get_all_by_string_key("name", "bob").unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// 500 行跨页顺序不变
|
||||
#[test]
|
||||
fn insert_many_rows_cross_pages() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
|
||||
for i in 0..500 {
|
||||
t.insert_row(&row(&[("id", json!(i))])).unwrap();
|
||||
}
|
||||
|
||||
let rows = t.scan().unwrap();
|
||||
assert_eq!(rows.len(), 500);
|
||||
for (i, r) in rows.iter().enumerate() {
|
||||
assert_eq!(r["id"], json!(i as i64));
|
||||
}
|
||||
}
|
||||
|
||||
/// 2KB 大行放得下
|
||||
#[test]
|
||||
fn large_row_still_fits() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
|
||||
let big = "x".repeat(2000);
|
||||
t.insert_row(&row(&[("data", json!(big.clone()))])).unwrap();
|
||||
|
||||
let rows = t.scan().unwrap();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0]["data"], json!(big));
|
||||
}
|
||||
|
||||
/// 超页的行要报错
|
||||
#[test]
|
||||
fn row_too_large_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
|
||||
let huge = "x".repeat(10_000);
|
||||
let err = t.insert_row(&row(&[("data", json!(huge))]));
|
||||
assert!(err.is_err());
|
||||
}
|
||||
|
||||
/// 重开文件数据还在
|
||||
#[test]
|
||||
fn reopen_and_scan() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
|
||||
{
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
t.insert_row(&row(&[("id", json!(1))])).unwrap();
|
||||
t.insert_row(&row(&[("id", json!(2))])).unwrap();
|
||||
}
|
||||
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||
let rows = t.scan().unwrap();
|
||||
assert_eq!(rows.len(), 2);
|
||||
assert_eq!(rows[0]["id"], json!(1));
|
||||
assert_eq!(rows[1]["id"], json!(2));
|
||||
}
|
||||
|
||||
/// 整表改写后索引重建
|
||||
#[test]
|
||||
fn replace_all_rebuilds_index() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = HeapTable::open_indexed(
|
||||
dir.path().join("t.db"),
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
|
||||
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();
|
||||
assert!(t.get_by_key(1).unwrap().is_some());
|
||||
|
||||
t.replace_all(&[
|
||||
row(&[("id", json!(7)), ("name", json!("Zoe"))]),
|
||||
row(&[("name", json!("NoId"))]),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
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");
|
||||
assert_eq!(got["name"], json!("Zoe"));
|
||||
assert_eq!(t.scan().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
/// 行级删除:行没了、索引条目没了、顺序不乱
|
||||
#[test]
|
||||
fn delete_by_keys_removes_rows() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = HeapTable::open_indexed(
|
||||
dir.path().join("t.db"),
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for i in 1..=5 {
|
||||
t.insert_row(&row(&[("id", json!(i)), ("name", json!(format!("u{}", i)))]))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let deleted = t.delete_by_keys(&[2, 4, 99]).unwrap();
|
||||
assert_eq!(deleted.len(), 2, "only 2 and 4 were actually deleted");
|
||||
|
||||
let ids: Vec<i64> = t
|
||||
.scan()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| r["id"].as_i64().unwrap())
|
||||
.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(4).unwrap().is_none());
|
||||
assert!(t.get_by_key(3).unwrap().is_some());
|
||||
|
||||
t.insert_row(&row(&[("id", json!(2)), ("name", json!("u2b"))]))
|
||||
.unwrap();
|
||||
assert_eq!(t.get_by_key(2).unwrap().unwrap()["name"], json!("u2b"));
|
||||
}
|
||||
|
||||
/// 批量插入
|
||||
#[test]
|
||||
fn insert_rows_batch() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = HeapTable::open_indexed(
|
||||
dir.path().join("t.db"),
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let rows: Vec<Row> = (1..=20)
|
||||
.map(|i| row(&[("id", json!(i)), ("name", json!(format!("u{}", i)))]))
|
||||
.collect();
|
||||
t.insert_rows(&rows).unwrap();
|
||||
|
||||
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))]),
|
||||
];
|
||||
assert!(t.insert_rows(&dup).is_err());
|
||||
}
|
||||
|
||||
/// 第二列索引:建完插入/删除都跟着维护,值可重复
|
||||
#[test]
|
||||
fn secondary_index_is_maintained() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = HeapTable::open_indexed(
|
||||
dir.path().join("t.db"),
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
t.insert_rows(&[
|
||||
row(&[("id", json!(1)), ("code", json!(7001))]),
|
||||
row(&[("id", json!(2)), ("code", json!(7002))]),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(!t.has_index("code"));
|
||||
t.build_index(
|
||||
"code",
|
||||
&dir.path().join("t.code.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(t.has_index("code"));
|
||||
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));
|
||||
|
||||
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();
|
||||
let mut dup_ids: Vec<i64> = t
|
||||
.get_all_by_column_key("code", 7001)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|r| r["id"].as_i64())
|
||||
.collect();
|
||||
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();
|
||||
t2.insert_rows(&[
|
||||
row(&[("id", json!(1)), ("age", json!(30))]),
|
||||
row(&[("id", json!(2)), ("age", json!(30))]),
|
||||
])
|
||||
.unwrap();
|
||||
t2.build_index(
|
||||
"age",
|
||||
&dir.path().join("d.age.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
t2.get_all_by_column_key("age", 30).unwrap().len(),
|
||||
2,
|
||||
"duplicate values are all indexed"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
use chusql_core_storage::config::DEFAULT_PAGE_SIZE;
|
||||
use chusql_core_storage::page::{Page, PageFile};
|
||||
|
||||
// 页文件测试:读写、追加、覆写、页大小校验与页缓存。
|
||||
|
||||
/// 写一页再读回一致
|
||||
#[test]
|
||||
fn write_then_read_page() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("test.db");
|
||||
|
||||
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||
|
||||
let mut p = Page::new(0, DEFAULT_PAGE_SIZE);
|
||||
p.data[0] = 42;
|
||||
p.data[DEFAULT_PAGE_SIZE - 1] = 7;
|
||||
pf.write_page(&p).unwrap();
|
||||
|
||||
let read = pf.read_page(0).unwrap();
|
||||
assert_eq!(read.data[0], 42);
|
||||
assert_eq!(read.data[DEFAULT_PAGE_SIZE - 1], 7);
|
||||
assert_eq!(read.data.len(), DEFAULT_PAGE_SIZE);
|
||||
}
|
||||
|
||||
/// 空文件读第 0 页报错
|
||||
#[test]
|
||||
fn read_out_of_range_returns_error() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("test.db");
|
||||
|
||||
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||
assert!(pf.read_page(0).is_err());
|
||||
}
|
||||
|
||||
/// 追加让页数加一
|
||||
#[test]
|
||||
fn append_grows_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("test.db");
|
||||
|
||||
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||
assert_eq!(pf.num_pages().unwrap(), 0);
|
||||
|
||||
pf.append_page().unwrap();
|
||||
assert_eq!(pf.num_pages().unwrap(), 1);
|
||||
|
||||
pf.append_page().unwrap();
|
||||
pf.append_page().unwrap();
|
||||
assert_eq!(pf.num_pages().unwrap(), 3);
|
||||
}
|
||||
|
||||
/// 覆写同页读到新内容
|
||||
#[test]
|
||||
fn overwrite_page() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("test.db");
|
||||
|
||||
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||
|
||||
let mut a = Page::new(0, DEFAULT_PAGE_SIZE);
|
||||
a.data[0] = 1;
|
||||
pf.write_page(&a).unwrap();
|
||||
|
||||
let mut b = Page::new(0, DEFAULT_PAGE_SIZE);
|
||||
b.data[0] = 99;
|
||||
pf.write_page(&b).unwrap();
|
||||
|
||||
assert_eq!(pf.read_page(0).unwrap().data[0], 99);
|
||||
assert_eq!(pf.num_pages().unwrap(), 1);
|
||||
}
|
||||
|
||||
/// 512 字节页可用
|
||||
#[test]
|
||||
fn custom_page_size() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
let mut pf = PageFile::open(&path, 512).unwrap();
|
||||
|
||||
pf.write_page(&Page::new(0, 512)).unwrap();
|
||||
pf.flush().unwrap();
|
||||
|
||||
assert_eq!(std::fs::metadata(&path).unwrap().len(), 512);
|
||||
}
|
||||
|
||||
/// 长度不是整数倍要报错
|
||||
#[test]
|
||||
fn mismatched_file_length_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("odd.db");
|
||||
std::fs::write(&path, vec![0u8; 100]).unwrap();
|
||||
|
||||
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||
assert!(pf.num_pages().is_err());
|
||||
}
|
||||
|
||||
/// 页大小不符拒绝写入
|
||||
#[test]
|
||||
fn write_wrong_sized_page_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("test.db");
|
||||
|
||||
let mut pf = PageFile::open(&path, 512).unwrap();
|
||||
assert!(pf.write_page(&Page::new(0, 1024)).is_err());
|
||||
}
|
||||
|
||||
/// 第二次读命中缓存
|
||||
#[test]
|
||||
fn cache_hit_avoids_second_read() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
|
||||
{
|
||||
let mut pf = PageFile::open(&path, 4096).unwrap();
|
||||
pf.write_page(&Page::new(0, 4096)).unwrap();
|
||||
pf.flush().unwrap();
|
||||
}
|
||||
|
||||
let mut pf = PageFile::open(&path, 4096).unwrap();
|
||||
let _ = pf.read_page(0).unwrap();
|
||||
let _ = pf.read_page(0).unwrap();
|
||||
let _ = pf.read_page(0).unwrap();
|
||||
|
||||
assert_eq!(pf.hits(), 2);
|
||||
assert_eq!(pf.misses(), 1);
|
||||
assert!(pf.hit_rate() > 0.6);
|
||||
}
|
||||
|
||||
/// 退出时脏页写回
|
||||
#[test]
|
||||
fn dirty_page_flushed_on_drop() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("t.db");
|
||||
|
||||
{
|
||||
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||
let mut p = Page::new(0, DEFAULT_PAGE_SIZE);
|
||||
p.data[0] = 42;
|
||||
pf.write_page(&p).unwrap();
|
||||
}
|
||||
|
||||
let mut pf2 = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||
let p = pf2.read_page(0).unwrap();
|
||||
assert_eq!(p.data[0], 42);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,101 @@
|
||||
use chusql_core_storage::protocol::Row;
|
||||
use chusql_core_storage::wal::{Wal, WalOp};
|
||||
use serde_json::json;
|
||||
|
||||
// WAL 测试:追加、读取、清空、截断尾巴。
|
||||
|
||||
|
||||
/// 用键值对构造一行
|
||||
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(),
|
||||
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());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user