大更新:

1、完善SQL语法
2、移除node前端,只保留静态资源
3、写好安装脚本
4、做好linux适配
This commit is contained in:
LIm1tZz7 committed 2026-10-01 20:48:38 +08:00
1 parent 687c012841
commit df6d348995
166 files changed
+16317 -10763

No files matched your search

+147 -102
View File
@@ -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));
}
+41 -16
View File
@@ -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(), "第二次没有可改的");
}
+146 -45
View File
@@ -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") };
}
+69 -49
View File
@@ -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"
);
}
-19
View File
@@ -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
+34 -75
View File
@@ -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 { .. }));
}