v26.10.2 修复bug,简化项目
This commit is contained in:
1 parent
ff3dbddd13
commit
fc9bbc20e4
26 files changed
+557
-299
No files matched your search
@@ -167,23 +167,47 @@ fn put_node(p: &mut Page, node: &Node) -> io::Result<()> {
|
||||
}
|
||||
|
||||
/// 从一页读出节点
|
||||
fn get_node(p: &Page) -> Node {
|
||||
fn get_node(p: &Page) -> io::Result<Node> {
|
||||
if p.data.len() < HEADER {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "truncated index node header"));
|
||||
}
|
||||
let kind = read_u8(p, OFF_KIND);
|
||||
let count = read_u16(p, OFF_COUNT) as usize;
|
||||
let slots_end = HEADER + count * SLOT;
|
||||
if !matches!(kind, NODE_LEAF | NODE_INTERNAL) || slots_end > p.data.len() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid index node kind or slot count"));
|
||||
}
|
||||
let trailer = read_u64(p, OFF_NEXT);
|
||||
let mut keys = Vec::with_capacity(count);
|
||||
let mut values = Vec::with_capacity(count + 1);
|
||||
let mut cell_end = p.data.len();
|
||||
for i in 0..count {
|
||||
let off = read_u16(p, HEADER + i * SLOT) as usize;
|
||||
if off < slots_end || off + CELL_FIXED > p.data.len() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid index cell offset"));
|
||||
}
|
||||
let klen = read_u16(p, off) as usize;
|
||||
if klen > MAX_KEY_BYTES || off + CELL_FIXED + klen > p.data.len() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid index key length"));
|
||||
}
|
||||
if off + CELL_FIXED + klen > cell_end {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "overlapping index cells"));
|
||||
}
|
||||
cell_end = off;
|
||||
keys.push(p.data[off + 2..off + 2 + klen].to_vec());
|
||||
values.push(read_u64(p, off + 2 + klen));
|
||||
}
|
||||
if keys.windows(2).any(|pair| pair[0] > pair[1]) {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "index keys are not sorted"));
|
||||
}
|
||||
if kind == NODE_INTERNAL {
|
||||
values.push(trailer);
|
||||
Node { kind, next: 0, keys, values }
|
||||
if values.contains(&META_PAGE) {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid index child page"));
|
||||
}
|
||||
Ok(Node { kind, next: 0, keys, values })
|
||||
} else {
|
||||
Node { kind, next: trailer, keys, values }
|
||||
Ok(Node { kind, next: trailer, keys, values })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +323,7 @@ impl DiskBTree {
|
||||
|
||||
/// 按页号读节点
|
||||
fn load(&mut self, id: PageId) -> io::Result<Node> {
|
||||
self.file.with_page(id, get_node)
|
||||
self.file.with_page(id, get_node)?
|
||||
}
|
||||
|
||||
/// 按页号写节点
|
||||
@@ -338,24 +362,9 @@ impl DiskBTree {
|
||||
|
||||
/// 按 key 取全部位置(重复键按值升序)
|
||||
pub fn get_all(&mut self, key: &[u8]) -> io::Result<Vec<u64>> {
|
||||
let Some(mut id) = self.root()? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
loop {
|
||||
let node = self.load(id)?;
|
||||
if node.kind == NODE_LEAF {
|
||||
let start = node.keys.partition_point(|k| k.as_slice() < key);
|
||||
let mut out = Vec::new();
|
||||
for i in start..node.keys.len() {
|
||||
if node.keys[i].as_slice() != key {
|
||||
break;
|
||||
}
|
||||
out.push(node.values[i]);
|
||||
}
|
||||
return Ok(out);
|
||||
}
|
||||
id = node.values[child_slot(&node.keys, key)];
|
||||
}
|
||||
let mut values: Vec<_> = self.scan(Some(key), Some(key))?.into_iter().map(|(_, value)| value).collect();
|
||||
values.sort_unstable();
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// 插入一个键值对(键可重复)
|
||||
@@ -412,10 +421,9 @@ impl DiskBTree {
|
||||
self.store(id, &node)?;
|
||||
return Ok(None);
|
||||
}
|
||||
let (sep, mut right) = split_node(&mut node);
|
||||
let (sep, right) = split_node(&mut node);
|
||||
let right_id = self.alloc(&right)?;
|
||||
if node.kind == NODE_LEAF {
|
||||
right.next = node.next;
|
||||
node.next = right_id;
|
||||
}
|
||||
self.store(id, &node)?;
|
||||
@@ -443,8 +451,14 @@ impl DiskBTree {
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
let child = node.values[child_slot(&node.keys, key)];
|
||||
self.delete_rec(child, key, value)
|
||||
let first = node.keys.partition_point(|k| k.as_slice() < key);
|
||||
let last = child_slot(&node.keys, key);
|
||||
for &child in &node.values[first..=last] {
|
||||
if self.delete_rec(child, key, value)? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// 叶子链上按顺序取全部 (键, 值)
|
||||
@@ -475,7 +489,7 @@ impl DiskBTree {
|
||||
}
|
||||
id = match lo {
|
||||
None => node.values[0],
|
||||
Some(start) => node.values[child_slot(&node.keys, start)],
|
||||
Some(start) => node.values[node.keys.partition_point(|k| k.as_slice() < start)],
|
||||
};
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -229,17 +229,17 @@ pub fn resolve(text: Option<&str>, path: Option<PathBuf>) -> Result<Loaded, Stri
|
||||
DEFAULT_POOL_SIZE,
|
||||
&file_origin,
|
||||
);
|
||||
let storage = file.storage.unwrap_or_default();
|
||||
let (data_dir, dir_origin) = pick(
|
||||
file.storage.as_ref().and_then(|s| s.data_dir.clone()),
|
||||
storage.data_dir,
|
||||
default_data_dir().to_string_lossy().into_owned(),
|
||||
&file_origin,
|
||||
);
|
||||
let (log_files, log_files_origin) = pick(
|
||||
file.storage.as_ref().and_then(|s| s.log_files.clone()),
|
||||
storage.log_files,
|
||||
DEFAULT_LOG_FILES.to_string(),
|
||||
&file_origin,
|
||||
);
|
||||
// 管名已经没有了:存储不再是独立进程,管道/套接字那一层随 FFI 一起删掉
|
||||
let (log_level, log_origin) = pick(
|
||||
file.log.and_then(|l| l.level),
|
||||
DEFAULT_LOG_LEVEL.to_string(),
|
||||
@@ -248,9 +248,9 @@ pub fn resolve(text: Option<&str>, path: Option<PathBuf>) -> Result<Loaded, Stri
|
||||
|
||||
let data_dir = data_dir.trim().to_string();
|
||||
let log_files = log_files.trim().to_string();
|
||||
let log_level = log_level.trim().to_string();
|
||||
let log_level = log_level.trim();
|
||||
|
||||
let log_level = Level::parse(&log_level)
|
||||
let log_level = Level::parse(log_level)
|
||||
.ok_or_else(|| format!("log.level is not one of off/error/warn/info/debug: {}", log_level))?;
|
||||
|
||||
if data_dir.is_empty() {
|
||||
@@ -318,38 +318,3 @@ fn validate_layout(page_size: usize, btree_order: usize) -> Result<(), String> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 别的层的分区不能被当成错误
|
||||
#[test]
|
||||
fn ignores_sections_owned_by_other_layers() {
|
||||
let text = r#"
|
||||
[page]
|
||||
size = 8192
|
||||
|
||||
[storage]
|
||||
data_dir = "../data"
|
||||
|
||||
[server]
|
||||
listen_host = "127.0.0.1"
|
||||
port = 7777
|
||||
max_message = 1048576
|
||||
|
||||
[web]
|
||||
port = 7778
|
||||
"#;
|
||||
let loaded = resolve(Some(text), Some(PathBuf::from("settings.toml"))).unwrap();
|
||||
assert_eq!(loaded.config.page_size, 8192);
|
||||
assert_eq!(loaded.config.data_dir, PathBuf::from("../data"));
|
||||
}
|
||||
|
||||
/// 自己分区里的拼写错误仍然要报错
|
||||
#[test]
|
||||
fn still_rejects_typos_inside_its_own_sections() {
|
||||
let text = "[page]\npage_size = 8192\n";
|
||||
assert!(resolve(Some(text), None).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1021,17 +1021,12 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// 系统目录里账号表已登记
|
||||
fn system_initialized(&self) -> bool {
|
||||
self.catalog
|
||||
.lock()
|
||||
.map(|c| c.describe(USERS_TABLE).is_some_and(|schema| schema.system))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 最近一次落盘的检查点 LSN
|
||||
fn last_checkpoint(&self) -> u64 {
|
||||
self.wal.read_checkpoint().ok().flatten().unwrap_or(0)
|
||||
/// 读取系统初始化状态与检查点 LSN
|
||||
fn system_status(&self) -> std::io::Result<Response> {
|
||||
let initialized = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?
|
||||
.describe(USERS_TABLE).is_some_and(|schema| schema.system);
|
||||
let last_lsn = self.wal.read_checkpoint()?.unwrap_or(0);
|
||||
Ok(Response::System { initialized, last_lsn })
|
||||
}
|
||||
|
||||
/// 读账号表;表没登记或数据文件缺失都算错
|
||||
@@ -1346,12 +1341,12 @@ impl Server {
|
||||
}
|
||||
}
|
||||
Request::BootstrapSystem { user, password_hash } => {
|
||||
match self.bootstrap_system(user.as_deref(), password_hash.as_deref()) {
|
||||
Ok(_) => Response::System { initialized: true, last_lsn: self.last_checkpoint() },
|
||||
match self.bootstrap_system(user.as_deref(), password_hash.as_deref()).and_then(|_| self.system_status()) {
|
||||
Ok(response) => response,
|
||||
Err(e) => Response::Error { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
Request::SystemStatus => Response::System { initialized: self.system_initialized(), last_lsn: self.last_checkpoint() },
|
||||
Request::SystemStatus => self.system_status().unwrap_or_else(|e| Response::Error { message: e.to_string() }),
|
||||
Request::Ping => {
|
||||
log_debug!(request, "ping");
|
||||
Response::Pong
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
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;
|
||||
use std::io::{ErrorKind, Seek, SeekFrom, Write};
|
||||
|
||||
// B+ 树测试:字节键、重复键、范围扫描、重开与配置校验。
|
||||
|
||||
/// 损坏节点返回数据错误且重复读取不 panic
|
||||
#[test]
|
||||
fn malformed_node_fields_return_invalid_data() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("corrupt.idx");
|
||||
{
|
||||
let mut tree = open(dir.path(), "corrupt.idx");
|
||||
tree.insert(b"x", 1).unwrap();
|
||||
tree.insert(b"y", 2).unwrap();
|
||||
}
|
||||
let original = std::fs::read(&path).unwrap();
|
||||
let cell = u16::from_le_bytes([original[DEFAULT_PAGE_SIZE + 11], original[DEFAULT_PAGE_SIZE + 12]]) as usize;
|
||||
for (name, offset, bytes) in [
|
||||
("slot count", 1, vec![255, 255]),
|
||||
("node kind", 0, vec![2]),
|
||||
("cell overlaps slots", 11, vec![0, 0]),
|
||||
("cell beyond page", 11, vec![255, 15]),
|
||||
("overlapping cells", 13, (cell as u16).to_le_bytes().to_vec()),
|
||||
("key length", cell, vec![255, 255]),
|
||||
("key order", cell + 2, vec![b'z']),
|
||||
("zero child", 0, vec![1]),
|
||||
] {
|
||||
std::fs::write(&path, &original).unwrap();
|
||||
let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
|
||||
file.seek(SeekFrom::Start((DEFAULT_PAGE_SIZE + offset) as u64)).unwrap();
|
||||
file.write_all(&bytes).unwrap();
|
||||
drop(file);
|
||||
let mut tree = open(dir.path(), "corrupt.idx");
|
||||
for _ in 0..2 {
|
||||
let error = tree.get(b"x").unwrap_err();
|
||||
assert_eq!(error.kind(), ErrorKind::InvalidData, "{name}: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 按默认参数打开指定索引文件
|
||||
fn open(dir: &std::path::Path, name: &str) -> DiskBTree {
|
||||
DiskBTree::open(dir.join(name), DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap()
|
||||
@@ -14,6 +50,29 @@ fn key(n: i64) -> [u8; 8] {
|
||||
(n as u64 ^ (1u64 << 63)).to_be_bytes()
|
||||
}
|
||||
|
||||
/// 重复键跨页后仍能完整查找和删除
|
||||
#[test]
|
||||
fn duplicate_keys_across_splits() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("split-duplicates.idx");
|
||||
let mut tree = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
|
||||
for value in (0..100).rev() {
|
||||
tree.insert(b"same", value).unwrap();
|
||||
}
|
||||
drop(tree);
|
||||
let mut tree = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
|
||||
assert_eq!(tree.get(b"same").unwrap(), Some(0));
|
||||
assert_eq!(tree.get_all(b"same").unwrap(), (0..100).collect::<Vec<_>>());
|
||||
assert_eq!(tree.scan(Some(b"same"), Some(b"same")).unwrap().len(), 100);
|
||||
for value in 0..100 {
|
||||
assert!(tree.delete(b"same", value).unwrap());
|
||||
}
|
||||
assert!(!tree.delete(b"same", 0).unwrap());
|
||||
drop(tree);
|
||||
let mut tree = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
|
||||
assert!(tree.get_all(b"same").unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// 空树查不到,遍历为空
|
||||
#[test]
|
||||
fn empty_tree_get() {
|
||||
|
||||
@@ -187,13 +187,14 @@ fn load_without_explicit_path_uses_builtin_defaults() {
|
||||
}
|
||||
}
|
||||
|
||||
/// [server] 分区归别的层管;本层拼写错误要报错
|
||||
/// 忽略其他层分区并拒绝存储配置拼写错误
|
||||
#[test]
|
||||
fn server_section_belongs_to_another_layer() {
|
||||
let _guard = env_guard();
|
||||
let db_server = "[server]\nlisten_host = \"0.0.0.0\"\nport = 7778\nmax_rows = 10\n";
|
||||
let db_server = "[page]\nsize = 8192\n[storage]\ndata_dir = \"../data\"\n[server]\nlisten_host = \"0.0.0.0\"\nport = 7778\nmax_rows = 10\n[web]\nport = 7778\n";
|
||||
let loaded = config::resolve(Some(db_server), None).unwrap();
|
||||
assert_eq!(loaded.config.data_dir, config::default_data_dir());
|
||||
assert_eq!(loaded.config.page_size, 8192);
|
||||
assert_eq!(loaded.config.data_dir, PathBuf::from("../data"));
|
||||
|
||||
let typo = "[storage]\ndata_dirs = \"oops\"\n";
|
||||
assert!(config::resolve(Some(typo), None).is_err());
|
||||
|
||||
@@ -5,6 +5,38 @@ use chusql_core_storage::runtime::Storage;
|
||||
|
||||
// 进程内存储集成测试:协议请求、索引、账号与配置。
|
||||
|
||||
/// 检查点损坏不冒充零 LSN且可恢复读取
|
||||
#[test]
|
||||
fn system_status_reports_checkpoint_read_errors() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_server, data) = start_server();
|
||||
let mut connection = connect()?;
|
||||
let checkpoint = data.path().join("system/wal.checkpoint");
|
||||
let initial: serde_json::Value = serde_json::from_str(&send_raw(&mut connection, r#"{"method":"system_status"}"#))?;
|
||||
assert_eq!(initial["last_lsn"], 0);
|
||||
request_ok(&mut connection, serde_json::json!({"method":"object_acl_replace","expected":null,"payload":"{}"}))?;
|
||||
let original = std::fs::read(&checkpoint)?;
|
||||
let before: serde_json::Value = serde_json::from_str(&send_raw(&mut connection, r#"{"method":"system_status"}"#))?;
|
||||
assert_eq!(before["status"], "system");
|
||||
assert!(before["last_lsn"].as_u64().unwrap() > 0);
|
||||
std::fs::write(&checkpoint, b"bad")?;
|
||||
let broken: serde_json::Value = serde_json::from_str(&send_raw(&mut connection, r#"{"method":"system_status"}"#))?;
|
||||
assert_eq!(broken["status"], "error");
|
||||
assert!(broken["message"].as_str().unwrap().contains("invalid WAL checkpoint length"));
|
||||
assert!(broken.get("last_lsn").is_none());
|
||||
assert!(send_raw(&mut connection, r#"{"method":"ping"}"#).contains("pong"));
|
||||
std::fs::write(&checkpoint, &original)?;
|
||||
let restored: serde_json::Value = serde_json::from_str(&send_raw(&mut connection, r#"{"method":"system_status"}"#))?;
|
||||
assert_eq!(restored, before);
|
||||
std::fs::remove_file(&checkpoint)?;
|
||||
std::fs::create_dir(&checkpoint)?;
|
||||
let unreadable: serde_json::Value = serde_json::from_str(&send_raw(&mut connection, r#"{"method":"system_status"}"#))?;
|
||||
assert_eq!(unreadable["status"], "error");
|
||||
assert!(unreadable.get("last_lsn").is_none());
|
||||
std::fs::remove_dir(&checkpoint)?;
|
||||
std::fs::write(&checkpoint, original)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 权限快照支持跨页文本并拒绝旧版本覆盖
|
||||
#[test]
|
||||
fn object_acl_snapshots_are_private_and_compare_versions() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
Reference in new issue
Block a user