大更新:
1、完善SQL语法 2、移除node前端,只保留静态资源 3、写好安装脚本 4、做好linux适配
This commit is contained in:
1 parent
687c012841
commit
df6d348995
166 files changed
+16317
-10763
No files matched your search
+147
-102
@@ -1,21 +1,24 @@
|
||||
use chusql_storage::btree::DiskBTree;
|
||||
use chusql_storage::btree::{fits_in_page, DiskBTree, MAX_KEY_BYTES};
|
||||
use chusql_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
|
||||
use chusql_storage::page::DEFAULT_POOL_SIZE;
|
||||
|
||||
// B+ 树测试:插查、重开、分裂、页大小与 order 校验。
|
||||
// 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 = DiskBTree::open(
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(t.get(1).unwrap(), None);
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
assert_eq!(t.get(&key(1)).unwrap(), None);
|
||||
assert_eq!(t.iter_all().unwrap(), vec![]);
|
||||
}
|
||||
|
||||
@@ -23,18 +26,12 @@ fn empty_tree_get() {
|
||||
#[test]
|
||||
fn insert_then_get() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = DiskBTree::open(
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in 0..100 {
|
||||
t.insert(i, i as u64 * 10).unwrap();
|
||||
t.insert(&key(i), i as u64 * 10).unwrap();
|
||||
}
|
||||
for i in 0..100 {
|
||||
assert_eq!(t.get(i).unwrap(), Some(i as u64 * 10));
|
||||
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64 * 10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,81 +43,42 @@ fn reopen_persists() {
|
||||
{
|
||||
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
|
||||
for i in 0..50 {
|
||||
t.insert(i, i as u64).unwrap();
|
||||
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(i).unwrap(), Some(i as u64));
|
||||
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64));
|
||||
}
|
||||
}
|
||||
|
||||
/// 同一个键后写的值生效
|
||||
/// 同一个键可以挂多个位置,按值升序返回
|
||||
#[test]
|
||||
fn update_existing() {
|
||||
fn duplicate_keys_keep_every_value() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = DiskBTree::open(
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
t.insert(5, 100).unwrap();
|
||||
t.insert(5, 200).unwrap();
|
||||
assert_eq!(t.get(5).unwrap(), Some(200));
|
||||
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 = DiskBTree::open(
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in (0..50).rev() {
|
||||
t.insert(i, i as u64).unwrap();
|
||||
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, i as i64);
|
||||
}
|
||||
}
|
||||
|
||||
/// 倒序插 500 个键都能查到
|
||||
#[test]
|
||||
fn reverse_order_insert() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = DiskBTree::open(
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
for i in (0..500).rev() {
|
||||
t.insert(i, i as u64).unwrap();
|
||||
}
|
||||
for i in 0..500 {
|
||||
assert_eq!(t.get(i).unwrap(), Some(i as u64));
|
||||
}
|
||||
}
|
||||
|
||||
/// 512 字节页 + 8 个孩子可用
|
||||
#[test]
|
||||
fn custom_page_size_and_order() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut t = DiskBTree::open(dir.path().join("t.idx"), 512, 8, DEFAULT_POOL_SIZE).unwrap();
|
||||
for i in 0..200 {
|
||||
t.insert(i, i as u64 + 1).unwrap();
|
||||
}
|
||||
for i in 0..200 {
|
||||
assert_eq!(t.get(i).unwrap(), Some(i as u64 + 1));
|
||||
assert_eq!(k.as_slice(), key(i as i64).as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +89,7 @@ fn config_mismatch_errors() {
|
||||
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(1, 1).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());
|
||||
@@ -152,60 +110,147 @@ fn not_an_index_file_errors() {
|
||||
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 = DiskBTree::open(
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in 0..200 {
|
||||
t.insert(i, i as u64 + 1).unwrap();
|
||||
t.insert(&key(i), i as u64 + 1).unwrap();
|
||||
}
|
||||
|
||||
assert!(t.delete(50).unwrap(), "deleting an existing key should return true");
|
||||
assert_eq!(t.get(50).unwrap(), None);
|
||||
assert_eq!(t.get(49).unwrap(), Some(50));
|
||||
assert_eq!(t.get(51).unwrap(), Some(52));
|
||||
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(50).unwrap());
|
||||
assert!(!t.delete(&key(50), 51).unwrap());
|
||||
|
||||
let all = t.iter_all().unwrap();
|
||||
assert_eq!(all.len(), 199);
|
||||
assert!(all.iter().all(|(k, _)| *k != 50));
|
||||
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 = DiskBTree::open(
|
||||
dir.path().join("t.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
let mut t = open(dir.path(), "t.idx");
|
||||
for i in 0..300 {
|
||||
t.insert(i, i as u64).unwrap();
|
||||
t.insert(&key(i), i as u64).unwrap();
|
||||
}
|
||||
for i in (0..300).step_by(2) {
|
||||
assert!(t.delete(i).unwrap());
|
||||
assert!(t.delete(&key(i), i as u64).unwrap());
|
||||
}
|
||||
for i in 1000..1100 {
|
||||
t.insert(i, i as u64).unwrap();
|
||||
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(i).unwrap(), want, "key {}", i);
|
||||
assert_eq!(t.get(&key(i)).unwrap(), want, "key {}", i);
|
||||
}
|
||||
for i in 1000..1100 {
|
||||
assert_eq!(t.get(i).unwrap(), Some(i as u64));
|
||||
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));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use chusql_storage::catalog::Catalog;
|
||||
use chusql_storage::protocol::{ColumnType, Row};
|
||||
use chusql_storage::protocol::Row;
|
||||
use serde_json::json;
|
||||
|
||||
// 数据字典测试:补列、去重、未知表。
|
||||
@@ -14,13 +14,13 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
||||
m
|
||||
}
|
||||
|
||||
/// 空文件得到空字典
|
||||
/// 空文件得到空字典,未知表返回 None
|
||||
#[test]
|
||||
fn empty_file_gives_empty_catalog() {
|
||||
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);
|
||||
assert!(c.describe("nope").is_none());
|
||||
}
|
||||
|
||||
/// 补列之后能查到 schema
|
||||
@@ -40,9 +40,9 @@ fn ensure_then_describe() {
|
||||
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, ColumnType::Int);
|
||||
assert_eq!(id_col.ty, "int");
|
||||
let name_col = s.columns.iter().find(|c| c.name == "name").unwrap();
|
||||
assert_eq!(name_col.ty, ColumnType::Str);
|
||||
assert_eq!(name_col.ty, "str");
|
||||
}
|
||||
|
||||
/// 再插入不会重复列
|
||||
@@ -57,15 +57,6 @@ fn second_insert_does_not_duplicate_columns() {
|
||||
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
|
||||
}
|
||||
|
||||
/// 未知表返回 None
|
||||
#[test]
|
||||
fn describe_unknown_returns_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("catalog.json");
|
||||
let c = Catalog::load(&path).unwrap();
|
||||
assert!(c.describe("nope").is_none());
|
||||
}
|
||||
|
||||
/// 删列同时清掉统计与索引
|
||||
#[test]
|
||||
fn remove_column_drops_definition_stats_and_index() {
|
||||
@@ -83,10 +74,44 @@ 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.get("code").is_none(), "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);
|
||||
|
||||
assert!(c.remove_column("nope", "code").is_err());
|
||||
}
|
||||
|
||||
/// 老类型名并到新写法,参数保留,未知类型原样
|
||||
#[test]
|
||||
fn normalize_type_maps_legacy_names() {
|
||||
use chusql_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(), "第二次没有可改的");
|
||||
}
|
||||
@@ -1,23 +1,32 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use chusql_storage::config::{
|
||||
self, Config, DEFAULT_BTREE_ORDER, DEFAULT_DATA_DIR, DEFAULT_PAGE_SIZE, DEFAULT_PIPE_NAME,
|
||||
Origin,
|
||||
self, Config, DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE, DEFAULT_PIPE_NAME, Origin,
|
||||
};
|
||||
use chusql_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 loaded = config::resolve(None, None, &|_| None).unwrap();
|
||||
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, PathBuf::from(DEFAULT_DATA_DIR));
|
||||
assert_eq!(loaded.config.data_dir, config::default_data_dir());
|
||||
assert_eq!(loaded.config.pipe_name, DEFAULT_PIPE_NAME);
|
||||
assert_eq!(loaded.config.log_level, Level::Info);
|
||||
}
|
||||
@@ -25,13 +34,14 @@ fn defaults_without_file() {
|
||||
/// 只覆盖文件里写了的项
|
||||
#[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()), &|_| None)
|
||||
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, PathBuf::from(DEFAULT_DATA_DIR));
|
||||
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();
|
||||
@@ -43,9 +53,10 @@ fn file_overrides_only_written_keys() {
|
||||
/// 六项全写都生效
|
||||
#[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[server]\npipe_name = \"mypipe\"\n[log]\nlevel = \"debug\"\n";
|
||||
let path = PathBuf::from("t.toml");
|
||||
let loaded = config::resolve(Some(text), Some(path.clone()), &|_| None).unwrap();
|
||||
let loaded = config::resolve(Some(text), Some(path.clone())).unwrap();
|
||||
|
||||
assert_eq!(loaded.config.page_size, 8192);
|
||||
assert_eq!(loaded.config.btree_order, 32);
|
||||
@@ -61,71 +72,161 @@ fn file_all_keys() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 环境变量优先于文件
|
||||
/// 环境变量完全无效:这条通道已经删掉,设了 CHUSQL_* 也只认文件/内置默认
|
||||
#[test]
|
||||
fn env_beats_file() {
|
||||
let text = "[storage]\ndata_dir = \"from-file\"\n";
|
||||
let env = |k: &str| (k == "CHUSQL_DATA_DIR").then(|| "from-env".to_string());
|
||||
let loaded = config::resolve(Some(text), Some(PathBuf::from("t.toml")), &env).unwrap();
|
||||
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");
|
||||
}
|
||||
|
||||
assert_eq!(loaded.config.data_dir, PathBuf::from("from-env"));
|
||||
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::Env("CHUSQL_DATA_DIR"));
|
||||
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/chusql.toml
|
||||
#[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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 默认数据目录也按系统惯例:Windows 落 %LOCALAPPDATA%\ChuSQL\data,Unix 落 XDG 数据目录
|
||||
#[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_eq!(loaded.config.pipe_name, DEFAULT_PIPE_NAME);
|
||||
assert!(loaded.origins.iter().all(|(_, _, o)| *o == Origin::Default));
|
||||
assert!(loaded.config_path.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志等级非法要报错
|
||||
#[test]
|
||||
fn rejects_bad_log_level() {
|
||||
let err = config::resolve(Some("[log]\nlevel = \"loud\"\n"), None, &|_| None).unwrap_err();
|
||||
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_page_size() {
|
||||
let err = config::resolve(Some("[page]\nsize = 3000\n"), None, &|_| None).unwrap_err();
|
||||
assert!(err.contains("page.size"), "got {}", err);
|
||||
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("[page]\nsize = 128\n"), None, &|_| None).unwrap_err();
|
||||
assert!(err.contains("page.size"), "got {}", err);
|
||||
}
|
||||
|
||||
/// order 非法要报错
|
||||
#[test]
|
||||
fn rejects_bad_btree_order() {
|
||||
let err = config::resolve(Some("[btree]\norder = 2\n"), None, &|_| None).unwrap_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, &|_| None).unwrap_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 err = config::resolve(Some("[page]\nsizes = 4096\n"), None, &|_| None).unwrap_err();
|
||||
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_env_value() {
|
||||
let env = |k: &str| (k == "CHUSQL_PAGE_SIZE").then(|| "big".to_string());
|
||||
let err = config::resolve(None, None, &env).unwrap_err();
|
||||
assert!(err.contains("CHUSQL_PAGE_SIZE"), "got {}", err);
|
||||
}
|
||||
fn rejects_bad_page_size_from_file() {
|
||||
let _guard = env_guard();
|
||||
unsafe { std::env::set_var("CHUSQL_PAGE_SIZE", "big") };
|
||||
|
||||
/// 来源文案正确
|
||||
#[test]
|
||||
fn origin_description() {
|
||||
assert_eq!(Origin::Default.describe(), "default");
|
||||
assert_eq!(Origin::Env("CHUSQL_PIPE").describe(), "env CHUSQL_PIPE");
|
||||
assert_eq!(
|
||||
Origin::File(PathBuf::from("a.toml")).describe(),
|
||||
format!("file {}", PathBuf::from("a.toml").display())
|
||||
);
|
||||
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") };
|
||||
}
|
||||
@@ -14,20 +14,62 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
||||
m
|
||||
}
|
||||
|
||||
/// 插一行再扫描读回
|
||||
/// 索引文件被清空(强杀或截断)后打开,会按堆数据补齐
|
||||
#[test]
|
||||
fn insert_and_scan_one_row() {
|
||||
fn empty_index_file_is_rebuilt_from_rows() {
|
||||
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 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());
|
||||
}
|
||||
|
||||
t.insert_row(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
|
||||
.unwrap();
|
||||
/// 字符串列能真正建索引:按值点查,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();
|
||||
|
||||
let rows = t.scan().unwrap();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0]["id"], json!(1));
|
||||
assert_eq!(rows[0]["name"], json!("Alice"));
|
||||
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 行跨页顺序不变
|
||||
@@ -94,22 +136,6 @@ fn reopen_and_scan() {
|
||||
assert_eq!(rows[1]["id"], json!(2));
|
||||
}
|
||||
|
||||
/// 512 字节页可用
|
||||
#[test]
|
||||
fn custom_page_size() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("small.db");
|
||||
let mut t = HeapTable::open(&path, 512, DEFAULT_POOL_SIZE).unwrap();
|
||||
|
||||
for i in 0..50 {
|
||||
t.insert_row(&row(&[("id", json!(i))])).unwrap();
|
||||
}
|
||||
assert_eq!(t.scan().unwrap().len(), 50);
|
||||
|
||||
let big = "x".repeat(600);
|
||||
assert!(t.insert_row(&row(&[("data", json!(big))])).is_err());
|
||||
}
|
||||
|
||||
/// 整表改写后索引重建
|
||||
#[test]
|
||||
fn replace_all_rebuilds_index() {
|
||||
@@ -246,40 +272,34 @@ fn secondary_index_is_maintained() {
|
||||
t.delete_by_keys(&[2]).unwrap();
|
||||
assert!(t.get_by_column_key("code", 7002).unwrap().is_none());
|
||||
|
||||
assert!(t.insert_row(&row(&[("id", json!(4)), ("code", json!(7001))])).is_err());
|
||||
let mut t2 = HeapTable::open(&dir.path().join("d.db"), DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).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()
|
||||
.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();
|
||||
let err = t2.build_index(
|
||||
t2.build_index(
|
||||
"age",
|
||||
&dir.path().join("d.age.idx"),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
chusql_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
);
|
||||
assert!(err.is_err(), "duplicate values must not be indexed");
|
||||
assert!(!t2.has_index("age"), "no half-built index after failure");
|
||||
}
|
||||
|
||||
/// 同一个 id 插两次要报错
|
||||
#[test]
|
||||
fn duplicate_key_is_rejected() {
|
||||
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_storage::config::DEFAULT_BTREE_ORDER,
|
||||
DEFAULT_POOL_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
t.insert_row(&row(&[("id", json!(1))])).unwrap();
|
||||
let r = t.insert_row(&row(&[("id", json!(1))]));
|
||||
assert!(r.is_err());
|
||||
assert_eq!(t.scan().unwrap().len(), 1, "the failed row should not reach the heap");
|
||||
assert_eq!(
|
||||
t2.get_all_by_column_key("age", 30).unwrap().len(),
|
||||
2,
|
||||
"duplicate values are all indexed"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,25 +49,6 @@ fn append_grows_file() {
|
||||
assert_eq!(pf.num_pages().unwrap(), 3);
|
||||
}
|
||||
|
||||
/// 两页内容互不影响
|
||||
#[test]
|
||||
fn two_pages_independent() {
|
||||
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;
|
||||
let mut b = Page::new(1, DEFAULT_PAGE_SIZE);
|
||||
b.data[0] = 2;
|
||||
pf.write_page(&a).unwrap();
|
||||
pf.write_page(&b).unwrap();
|
||||
|
||||
assert_eq!(pf.read_page(0).unwrap().data[0], 1);
|
||||
assert_eq!(pf.read_page(1).unwrap().data[0], 2);
|
||||
}
|
||||
|
||||
/// 覆写同页读到新内容
|
||||
#[test]
|
||||
fn overwrite_page() {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -13,77 +13,51 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
||||
m
|
||||
}
|
||||
|
||||
/// 写读 Insert 操作
|
||||
/// 三种操作写完都能原样读回
|
||||
#[test]
|
||||
fn append_and_read_insert() {
|
||||
fn append_and_read_round_trips() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
|
||||
let op = WalOp::Insert {
|
||||
table: "users".into(),
|
||||
row: row(&[("id", json!(1)), ("name", json!("Alice"))]),
|
||||
};
|
||||
wal.append(&op).unwrap();
|
||||
|
||||
let back = wal.read().unwrap().unwrap();
|
||||
assert_eq!(back, op);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 写读 ReplaceAll 操作
|
||||
/// 文件不存在或为空都返回 None
|
||||
#[test]
|
||||
fn append_and_read_replace_all() {
|
||||
fn read_without_content_returns_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
let missing = Wal::new(dir.path().join("missing.log"));
|
||||
assert!(missing.read().unwrap().is_none());
|
||||
|
||||
let op = WalOp::ReplaceAll {
|
||||
table: "users".into(),
|
||||
rows: vec![
|
||||
row(&[("id", json!(1)), ("name", json!("Alice"))]),
|
||||
row(&[("id", json!(2)), ("name", json!("Bob"))]),
|
||||
],
|
||||
};
|
||||
wal.append(&op).unwrap();
|
||||
|
||||
let back = wal.read().unwrap().unwrap();
|
||||
assert_eq!(back, op);
|
||||
}
|
||||
|
||||
/// 写读 DropColumn 操作,行随日志落盘
|
||||
#[test]
|
||||
fn append_and_read_drop_column() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
|
||||
let op = WalOp::DropColumn {
|
||||
table: "users".into(),
|
||||
column: "age".into(),
|
||||
rows: vec![
|
||||
row(&[("id", json!(1)), ("name", json!("Alice"))]),
|
||||
row(&[("id", json!(2)), ("name", json!("Bob"))]),
|
||||
],
|
||||
};
|
||||
wal.append(&op).unwrap();
|
||||
|
||||
let back = wal.read().unwrap().unwrap();
|
||||
assert_eq!(back, op);
|
||||
}
|
||||
|
||||
/// 文件不存在返回 None
|
||||
#[test]
|
||||
fn missing_file_returns_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
assert!(wal.read().unwrap().is_none());
|
||||
}
|
||||
|
||||
/// 空文件返回 None
|
||||
#[test]
|
||||
fn empty_file_returns_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("wal.log");
|
||||
std::fs::write(&path, b"").unwrap();
|
||||
let wal = Wal::new(&path);
|
||||
assert!(wal.read().unwrap().is_none());
|
||||
let empty = Wal::new(&path);
|
||||
assert!(empty.read().unwrap().is_none());
|
||||
}
|
||||
|
||||
/// 清空后读不到
|
||||
@@ -124,18 +98,3 @@ fn truncated_tail_is_ignored() {
|
||||
assert!(wal.read().unwrap().is_none());
|
||||
}
|
||||
|
||||
/// 第二次追加会覆盖
|
||||
#[test]
|
||||
fn overwrite_on_second_append_is_visible() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(dir.path().join("wal.log"));
|
||||
|
||||
wal.append(&WalOp::Insert {
|
||||
table: "t".into(),
|
||||
row: row(&[("id", json!(1))]),
|
||||
})
|
||||
.unwrap();
|
||||
let first = wal.read().unwrap().unwrap();
|
||||
assert!(matches!(first, WalOp::Insert { .. }));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user