大改,整理项目结构,完成rust ffi调用,写好tcp接口

This commit is contained in:
LIm1tZz7 committed 2026-10-02 15:07:38 +08:00
1 parent df6d348995
commit 9758439b82
118 files changed
+8103 -6321

No files matched your search

+294
View File
@@ -0,0 +1,294 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "chusql-core-storage"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"tempfile",
"toml",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "chusql-core-storage"
version = "0.1.0"
edition = "2024"
[lib]
name = "chusql_core_storage"
crate-type = ["cdylib", "rlib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
[dev-dependencies]
tempfile = "3"
@@ -0,0 +1,38 @@
# ChuSQL 存储服务配置示例
#
# 用法:安装时把本文件放到固定的用户配置目录(Windows 是 %APPDATA%\ChuSQL\chusql.toml,
# 其它平台是 ${XDG_CONFIG_HOME:-$HOME/.config}/ChuSQL/chusql.toml);
# 临时换一份就用命令行参数 --config <path>(没有环境变量通道了)。
#
# 每一项都可以删掉:没写的项回退到内置默认值(下面注释里的值就是默认值)。
# 优先级:本文件 > 内置默认值(没有配置键走环境变量了)。
# 启动时 server 会把"读了哪个文件、每个选项从哪来"打进日志([INFO] 开头那几行)。
[page]
# 页大小(字节):512 ~ 65536 之间的 2 的幂。
# 注意:改它会让已有数据文件(.db)读不出来(文件长度对不上页大小会直接报错)。
size = 4096
[btree]
# B+ 树一个节点最多几个孩子(>= 3,且必须能放进一页:12 + (order-1)*8 + order*8 <= size)。
# 注意:改它会让已有索引文件(.idx)对不上文件头里记的值,打开时报错。
order = 4
[buffer]
# 缓冲池里最多放几页(>= 1)。
pool_size = 1024
[storage]
# 数据目录:一张表一个 <表名>.db,一个索引一个 <表名>.idx。
# 不写就跟随平台惯例(Windows %LOCALAPPDATA%\ChuSQL\data,
# Unix ${XDG_DATA_HOME:-$HOME/.local/share}/chusql/data,跟安装脚本装的位置一致);
# 要固定就写绝对路径,例如:
# data_dir = "/srv/chusql/data"
[log]
# 日志等级:off / error / warn / info / debug。debug 会多打每个请求和连接。
level = "info"
# 颜色没有配置项也没有环境变量:要写的那条流是终端就着色,重定向到文件就不着色。
# 行格式:[等级 本地日期时间] [来源] 消息;来源 core=服务自身、request=由存储请求触发
# ERROR 加粗红、WARN 加粗橙、DEBUG 加粗不变色、INFO 不加样式
+507
View File
@@ -0,0 +1,507 @@
use std::io;
use std::path::Path;
use crate::page::{Page, PageFile, PageId};
// 磁盘 B+ 树:键是变长字节串,允许重复键,叶子链支持范围扫描。
const META_PAGE: PageId = 0;
const META_MAGIC: &[u8; 4] = b"CBK2";
const OFF_PAGE_SIZE: usize = 4;
const OFF_MAX_KEY: usize = 8;
const OFF_ROOT: usize = 10;
const OFF_ORDER: usize = 18;
const NODE_LEAF: u8 = 0;
const NODE_INTERNAL: u8 = 1;
const OFF_KIND: usize = 0;
const OFF_COUNT: usize = 1;
const OFF_NEXT: usize = 3;
const HEADER: usize = 11;
const SLOT: usize = 2;
const CELL_FIXED: usize = 2 + 8;
/// 键长上限:节点里的格子必须放得进一页
pub const MAX_KEY_BYTES: usize = 64;
/// 一个节点最多几个键
fn max_keys(order: usize) -> usize {
order - 1
}
/// 一个格子最多占多少字节
fn max_cell_bytes() -> usize {
CELL_FIXED + MAX_KEY_BYTES
}
/// 这个 order 放得进一页吗
pub fn fits_in_page(page_size: usize, order: usize) -> bool {
order >= 2 && HEADER + max_keys(order) * (SLOT + max_cell_bytes()) <= page_size
}
/// 一页最多几个孩子
pub fn max_order(page_size: usize) -> usize {
page_size.saturating_sub(HEADER) / (SLOT + max_cell_bytes()) + 1
}
/// 读一个 u8
fn read_u8(p: &Page, off: usize) -> u8 {
p.data[off]
}
/// 写一个 u8
fn write_u8(p: &mut Page, off: usize, v: u8) {
p.data[off] = v;
}
/// 读一个小端 u16
fn read_u16(p: &Page, off: usize) -> u16 {
u16::from_le_bytes([p.data[off], p.data[off + 1]])
}
/// 写一个小端 u16
fn write_u16(p: &mut Page, off: usize, v: u16) {
p.data[off..off + 2].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u32
fn read_u32(p: &Page, off: usize) -> u32 {
u32::from_le_bytes([p.data[off], p.data[off + 1], p.data[off + 2], p.data[off + 3]])
}
/// 写一个小端 u32
fn write_u32(p: &mut Page, off: usize, v: u32) {
p.data[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u64
fn read_u64(p: &Page, off: usize) -> u64 {
let mut b = [0u8; 8];
b.copy_from_slice(&p.data[off..off + 8]);
u64::from_le_bytes(b)
}
/// 写一个小端 u64
fn write_u64(p: &mut Page, off: usize, v: u64) {
p.data[off..off + 8].copy_from_slice(&v.to_le_bytes());
}
/// 写文件头
fn write_meta(page: &mut Page, page_size: usize, order: usize, root: PageId) {
page.zero();
page.data[0..4].copy_from_slice(META_MAGIC);
write_u32(page, OFF_PAGE_SIZE, page_size as u32);
write_u16(page, OFF_MAX_KEY, MAX_KEY_BYTES as u16);
write_u64(page, OFF_ROOT, root);
write_u16(page, OFF_ORDER, order as u16);
}
/// 校验文件头与配置
fn check_meta(page: &Page, page_size: usize, order: usize) -> io::Result<()> {
if page.data[0..4] != *META_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"not a chusql index file (bad magic)",
));
}
let stored_page = read_u32(page, OFF_PAGE_SIZE) as usize;
let stored_order = read_u16(page, OFF_ORDER) as usize;
if stored_page != page_size || stored_order != order {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"index file was built with page.size={} btree.order={}, config says page.size={} btree.order={}",
stored_page, stored_order, page_size, order
),
));
}
Ok(())
}
/// 一个节点:叶子存行位置,内部节点存孩子页号
struct Node {
kind: u8,
next: PageId,
keys: Vec<Vec<u8>>,
values: Vec<u64>,
}
/// 一个格子占多少字节
fn cell_bytes(key: &[u8]) -> usize {
CELL_FIXED + key.len()
}
/// 节点占多少字节
fn node_bytes(node: &Node) -> usize {
HEADER + node.keys.len() * SLOT + node.keys.iter().map(|k| cell_bytes(k)).sum::<usize>()
}
/// 把节点写回一页(格子从页尾往前摆,槽位从头部往后长)
fn put_node(p: &mut Page, node: &Node) -> io::Result<()> {
if node.keys.len() > u16::MAX as usize || node_bytes(node) > p.data.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"index node does not fit in a page",
));
}
p.zero();
write_u8(p, OFF_KIND, node.kind);
write_u16(p, OFF_COUNT, node.keys.len() as u16);
// 内部节点比键多一个孩子,最后一个放在 next 槽位里
let trailer = if node.kind == NODE_INTERNAL {
node.values.last().copied().unwrap_or(0)
} else {
node.next
};
write_u64(p, OFF_NEXT, trailer);
let mut cursor = p.data.len();
for (i, key) in node.keys.iter().enumerate() {
let size = cell_bytes(key);
cursor -= size;
write_u16(p, cursor, key.len() as u16);
p.data[cursor + 2..cursor + 2 + key.len()].copy_from_slice(key);
write_u64(p, cursor + 2 + key.len(), node.values[i]);
write_u16(p, HEADER + i * SLOT, cursor as u16);
}
Ok(())
}
/// 从一页读出节点
fn get_node(p: &Page) -> Node {
let kind = read_u8(p, OFF_KIND);
let count = read_u16(p, OFF_COUNT) as usize;
let trailer = read_u64(p, OFF_NEXT);
let mut keys = Vec::with_capacity(count);
let mut values = Vec::with_capacity(count + 1);
for i in 0..count {
let off = read_u16(p, HEADER + i * SLOT) as usize;
let klen = read_u16(p, off) as usize;
keys.push(p.data[off + 2..off + 2 + klen].to_vec());
values.push(read_u64(p, off + 2 + klen));
}
if kind == NODE_INTERNAL {
values.push(trailer);
Node { kind, next: 0, keys, values }
} else {
Node { kind, next: trailer, keys, values }
}
}
/// 叶子里的插入位置:同键按 value 排,保证顺序稳定
fn leaf_slot(keys: &[Vec<u8>], values: &[u64], key: &[u8], value: u64) -> usize {
let mut i = keys.partition_point(|k| k.as_slice() < key);
while i < keys.len() && keys[i].as_slice() == key && values[i] < value {
i += 1;
}
i
}
/// 内部节点里选哪个孩子
fn child_slot(keys: &[Vec<u8>], key: &[u8]) -> usize {
keys.partition_point(|k| k.as_slice() <= key)
}
/// 切开一个节点:返回上提的键与右半节点
fn split_node(node: &mut Node) -> (Vec<u8>, Node) {
let mid = node.keys.len() / 2;
if node.kind == NODE_LEAF {
let sep = node.keys[mid].clone();
let right = Node {
kind: NODE_LEAF,
next: node.next,
keys: node.keys.split_off(mid),
values: node.values.split_off(mid),
};
(sep, right)
} else {
let sep = node.keys[mid].clone();
let right_keys = node.keys.split_off(mid + 1);
let right_values = node.values.split_off(mid + 1);
node.keys.pop();
(
sep,
Node { kind: NODE_INTERNAL, next: 0, keys: right_keys, values: right_values },
)
}
}
/// 磁盘 B+ 树
pub struct DiskBTree {
file: PageFile,
order: usize,
}
impl DiskBTree {
/// 打开或新建索引文件
pub fn open<P: AsRef<Path>>(
path: P,
page_size: usize,
order: usize,
pool_size: usize,
) -> io::Result<Self> {
if !fits_in_page(page_size, order) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"btree order {} does not fit in a {}-byte page (max {})",
order,
page_size,
max_order(page_size)
),
));
}
let mut file = PageFile::with_options(path, page_size, pool_size)?;
if file.num_pages()? == 0 {
let mut meta = Page::new(META_PAGE, page_size);
write_meta(&mut meta, page_size, order, 0);
file.write_page(&meta)?;
} else {
let meta = file.read_page(META_PAGE)?;
check_meta(&meta, page_size, order)?;
}
Ok(DiskBTree { file, order })
}
/// 键太长直接报错,避免写坏节点
fn check_key(&self, key: &[u8]) -> io::Result<()> {
if key.len() > MAX_KEY_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"index key is {} bytes, at most {} are supported",
key.len(),
MAX_KEY_BYTES
),
));
}
Ok(())
}
/// 读根页页号;空树是 None
fn root(&mut self) -> io::Result<Option<PageId>> {
let r = self.file.with_page(META_PAGE, |p| read_u64(p, OFF_ROOT))?;
Ok(if r == 0 { None } else { Some(r) })
}
/// 写根页页号
fn set_root(&mut self, r: Option<PageId>) -> io::Result<()> {
self.file
.update_page(META_PAGE, |p| write_u64(p, OFF_ROOT, r.unwrap_or(0)))
}
/// 追加一个新节点页
fn alloc(&mut self, node: &Node) -> io::Result<PageId> {
let page = self.file.append_page()?;
let id = page.id;
self.file.update_page(id, |p| put_node(p, node))??;
Ok(id)
}
/// 按页号读节点
fn load(&mut self, id: PageId) -> io::Result<Node> {
self.file.with_page(id, get_node)
}
/// 按页号写节点
fn store(&mut self, id: PageId, node: &Node) -> io::Result<()> {
self.file.update_page(id, |p| put_node(p, node))?
}
/// 清空整棵树
pub fn clear(&mut self) -> io::Result<()> {
let page_size = self.file.page_size();
self.file.truncate()?;
let mut meta = Page::new(META_PAGE, page_size);
write_meta(&mut meta, page_size, self.order, 0);
self.file.write_page(&meta)?;
Ok(())
}
/// 树是空的吗(只看最左叶子,不用整树遍历)
pub fn is_empty(&mut self) -> io::Result<bool> {
let Some(mut id) = self.root()? else {
return Ok(true);
};
loop {
let node = self.load(id)?;
if node.kind == NODE_LEAF {
return Ok(node.keys.is_empty());
}
id = node.values[0];
}
}
/// 按 key 取第一个位置
pub fn get(&mut self, key: &[u8]) -> io::Result<Option<u64>> {
Ok(self.get_all(key)?.into_iter().next())
}
/// 按 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)];
}
}
/// 插入一个键值对(键可重复)
pub fn insert(&mut self, key: &[u8], value: u64) -> io::Result<()> {
self.check_key(key)?;
match self.root()? {
None => {
let node = Node {
kind: NODE_LEAF,
next: 0,
keys: vec![key.to_vec()],
values: vec![value],
};
let id = self.alloc(&node)?;
self.set_root(Some(id))
}
Some(root) => match self.insert_rec(root, key, value)? {
None => Ok(()),
Some((sep, right_id)) => {
let node = Node {
kind: NODE_INTERNAL,
next: 0,
keys: vec![sep],
values: vec![root, right_id],
};
let id = self.alloc(&node)?;
self.set_root(Some(id))
}
},
}
}
/// 递归插入,溢出就把右半节点与上提键交给上层
fn insert_rec(
&mut self,
id: PageId,
key: &[u8],
value: u64,
) -> io::Result<Option<(Vec<u8>, PageId)>> {
let mut node = self.load(id)?;
if node.kind == NODE_LEAF {
let at = leaf_slot(&node.keys, &node.values, key, value);
node.keys.insert(at, key.to_vec());
node.values.insert(at, value);
} else {
let child = node.values[child_slot(&node.keys, key)];
if let Some((sep, right_id)) = self.insert_rec(child, key, value)? {
let at = node.keys.partition_point(|k| k.as_slice() <= sep.as_slice());
node.keys.insert(at, sep);
node.values.insert(at + 1, right_id);
}
}
if node.keys.len() <= max_keys(self.order) {
self.store(id, &node)?;
return Ok(None);
}
let (sep, mut 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)?;
Ok(Some((sep, right_id)))
}
/// 删掉一个 (key, value)
pub fn delete(&mut self, key: &[u8], value: u64) -> io::Result<bool> {
let Some(root) = self.root()? else {
return Ok(false);
};
self.delete_rec(root, key, value)
}
/// 递归删一个 (key, value)
fn delete_rec(&mut self, id: PageId, key: &[u8], value: u64) -> io::Result<bool> {
let mut node = self.load(id)?;
if node.kind == NODE_LEAF {
let at = leaf_slot(&node.keys, &node.values, key, value);
if at < node.keys.len() && node.keys[at].as_slice() == key && node.values[at] == value {
node.keys.remove(at);
node.values.remove(at);
self.store(id, &node)?;
return Ok(true);
}
return Ok(false);
}
let child = node.values[child_slot(&node.keys, key)];
self.delete_rec(child, key, value)
}
/// 叶子链上按顺序取全部 (键, 值)
pub fn iter_all(&mut self) -> io::Result<Vec<(Vec<u8>, u64)>> {
self.scan(None, None)
}
/// 范围扫描:lo / hi 都是闭区间,None 表示不限
pub fn scan(
&mut self,
lo: Option<&[u8]>,
hi: Option<&[u8]>,
) -> io::Result<Vec<(Vec<u8>, u64)>> {
let Some(mut id) = self.root()? else {
return Ok(Vec::new());
};
loop {
let node = self.load(id)?;
if node.kind == NODE_LEAF {
id = match lo {
None => id,
Some(start) => {
let at = node.keys.partition_point(|k| k.as_slice() < start);
if at < node.keys.len() { id } else { node.next }
}
};
break;
}
id = match lo {
None => node.values[0],
Some(start) => node.values[child_slot(&node.keys, start)],
};
}
let mut out = Vec::new();
while id != 0 {
let node = self.load(id)?;
for i in 0..node.keys.len() {
if lo.is_some_and(|start| node.keys[i].as_slice() < start) {
continue;
}
if hi.is_some_and(|end| node.keys[i].as_slice() > end) {
return Ok(out);
}
out.push((node.keys[i].clone(), node.values[i]));
}
id = node.next;
}
Ok(out)
}
/// 脏页写回
pub fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
/// 当前缓存页数
pub fn cached_pages(&self) -> usize {
self.file.cached_pages()
}
}
+382
View File
@@ -0,0 +1,382 @@
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::hash::{Hash, Hasher};
use std::io;
use std::io::Write;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::protocol::{Row, SchemaColumn};
// 数据字典:记每张表有哪些列、什么类型、多少行、有哪些索引,以及列级统计。
const STATS_DISTINCT_CAP: u64 = 4096;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexSchema {
pub column: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ColumnStat {
pub distinct: u64,
#[serde(default)]
pub capped: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TableSchema {
#[serde(default)]
pub system: bool,
pub columns: Vec<SchemaColumn>,
#[serde(default)]
pub dropped_columns: BTreeSet<String>,
#[serde(default)]
pub compacted: bool,
#[serde(default)]
pub row_count: u64,
#[serde(default)]
pub indexes: Vec<IndexSchema>,
#[serde(default)]
pub stats: BTreeMap<String, ColumnStat>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Catalog {
tables: BTreeMap<String, TableSchema>,
#[serde(skip)]
seen: BTreeMap<String, HashSet<u64>>,
}
impl Catalog {
/// 从文件读;不存在给空字典
pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
if !path.as_ref().exists() {
return Ok(Catalog::default());
}
let bytes = std::fs::read(path)?;
if bytes.is_empty() {
return Ok(Catalog::default());
}
serde_json::from_slice(&bytes).map_err(io::Error::other)
}
/// 返回所有表的 (表名, schema)。
pub fn all_tables(&self) -> Vec<(&str, &TableSchema)> {
self.tables.iter().map(|(k, v)| (k.as_str(), v)).collect()
}
/// 写回文件
pub fn save<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let bytes = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
let pending = path.as_ref().with_extension("json.pending");
let mut file = std::fs::File::create(&pending)?;
file.write_all(&bytes)?;
file.sync_all()?;
drop(file);
std::fs::rename(pending, &path)?;
std::fs::OpenOptions::new().read(true).write(true).open(path)?.sync_all()
}
/// 一张表的 schema
pub fn describe(&self, table: &str) -> Option<&TableSchema> {
self.tables.get(table)
}
/// 记录过的所有表名
pub fn table_names(&self) -> Vec<String> {
self.tables.keys().cloned().collect()
}
/// 按一行数据补列
pub fn ensure_columns(&mut self, table: &str, row: &Row) {
let entry = self.tables.entry(table.to_string()).or_default();
add_missing_columns(entry, row);
}
/// 建表;已存在报错
pub fn create_table(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> {
if self.tables.contains_key(table) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("table already exists: {}", table),
));
}
self.tables.insert(
table.to_string(),
TableSchema {
system: false,
columns,
dropped_columns: BTreeSet::new(),
compacted: false,
row_count: 0,
indexes: Vec::new(),
stats: BTreeMap::new(),
},
);
Ok(())
}
/// 标记内部账号表
pub fn mark_system(&mut self, table: &str) -> io::Result<()> {
let entry = self.tables.get_mut(table)
.ok_or_else(|| io::Error::other("missing system table"))?;
entry.system = true;
Ok(())
}
/// 记一次插入,返回列有没有变化
pub fn record_insert(&mut self, table: &str, row: &Row) -> bool {
let changed = {
let entry = self.tables.entry(table.to_string()).or_default();
let changed = add_missing_columns(entry, row);
entry.row_count += 1;
changed
};
self.count_values(table, row);
changed
}
/// 记一次删除:行数减少 + 更新列统计
pub fn record_delete(&mut self, table: &str, rows: &[Row]) {
let entry = self.tables.entry(table.to_string()).or_default();
entry.row_count = entry.row_count.saturating_sub(rows.len() as u64);
let seen = self.seen.entry(table.to_string()).or_default();
for r in rows {
for (name, v) in r {
let stat = entry.stats.entry(name.clone()).or_default();
if stat.capped {
continue;
}
if seen.remove(&fold(name, value_hash(v))) {
stat.distinct = stat.distinct.saturating_sub(1);
}
}
}
}
/// 重数一遍统计与行数
pub fn rebuild_stats(&mut self, table: &str, rows: &[Row]) -> bool {
self.seen.remove(table);
{
let entry = self.tables.entry(table.to_string()).or_default();
entry.stats.clear();
entry.row_count = 0;
}
let mut changed = false;
for row in rows {
changed = self.record_insert(table, row) || changed;
}
changed
}
/// 这一列第一次见到这个值就加一
fn count_values(&mut self, table: &str, row: &Row) {
let entry = self.tables.entry(table.to_string()).or_default();
let seen = self.seen.entry(table.to_string()).or_default();
for (name, v) in row {
let stat = entry.stats.entry(name.clone()).or_default();
if stat.capped {
continue;
}
if seen.insert(fold(name, value_hash(v))) {
stat.distinct += 1;
if stat.distinct >= STATS_DISTINCT_CAP {
stat.capped = true;
seen.clear();
}
}
}
}
/// 删表
pub fn drop_table(&mut self, table: &str) -> io::Result<()> {
if self.tables.remove(table).is_none() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("unknown table: {}", table),
));
}
self.seen.remove(table);
Ok(())
}
/// ALTER:整列定义换成新的,并清掉二级索引与统计
pub fn set_columns(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
entry.columns = columns;
entry.dropped_columns.clear();
entry.indexes.clear();
entry.stats.clear();
self.seen.remove(table);
Ok(())
}
/// 老数据的列类型名规范化;返回有没有改动
pub fn normalize_types(&mut self) -> bool {
let mut changed = false;
for entry in self.tables.values_mut() {
for column in entry.columns.iter_mut() {
let fixed = normalize_type(&column.ty);
if fixed != column.ty {
column.ty = fixed;
changed = true;
}
}
}
changed
}
/// 删一列;之前没有也返回成功(重放幂等)
pub fn remove_column(&mut self, table: &str, column: &str) -> io::Result<bool> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
let had_column = {
let before = entry.columns.len();
entry.columns.retain(|c| c.name != column);
entry.columns.len() != before
};
entry.stats.remove(column);
entry.dropped_columns.insert(column.to_string());
entry.compacted = false;
entry.indexes.retain(|i| i.column != column);
self.seen.remove(table);
Ok(had_column)
}
/// 记住这一版已整理过(隐藏列名要留住)
pub fn mark_compacted(&mut self, table: &str) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
entry.compacted = true;
self.seen.remove(table);
Ok(())
}
/// 还有隐藏列字节待整理的表
pub fn dropped_tables(&self) -> Vec<String> {
self.tables
.iter()
.filter(|(_, e)| !e.dropped_columns.is_empty() && !e.compacted)
.map(|(t, _)| t.clone())
.collect()
}
/// 记下一条索引定义
pub fn add_index(&mut self, table: &str, column: &str) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
if entry.indexes.iter().any(|i| i.column == column) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("index on column \"{}\" already exists", column),
));
}
entry.indexes.push(IndexSchema {
column: column.to_string(),
});
Ok(())
}
/// 去掉一条索引定义
pub fn remove_index(&mut self, table: &str, column: &str) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
match entry.indexes.iter().position(|i| i.column == column) {
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no index on column \"{}\"", column),
)),
Some(i) => {
entry.indexes.remove(i);
Ok(())
}
}
}
}
/// 把行里"字典还不知道的列"补进来;返回有没有变化
fn add_missing_columns(entry: &mut TableSchema, row: &Row) -> bool {
let mut changed = false;
for (k, v) in row {
if entry.dropped_columns.contains(k) || entry.columns.iter().any(|c| &c.name == k) {
continue;
}
if let Some(ty) = infer_type(v) {
entry.columns.push(SchemaColumn {
name: k.clone(),
ty,
..Default::default()
});
changed = true;
}
}
changed
}
/// 由 JSON 值猜列类型
fn infer_type(v: &serde_json::Value) -> Option<String> {
match v {
serde_json::Value::Number(_) => Some("int".to_string()),
serde_json::Value::String(_) => Some("str".to_string()),
serde_json::Value::Bool(_) => Some("bool".to_string()),
_ => None,
}
}
/// 类型别名统一成规范名,参数原样保留
pub fn normalize_type(ty: &str) -> String {
let trimmed = ty.trim();
let (base, args) = match trimmed.find('(') {
Some(i) => (&trimmed[..i], &trimmed[i..]),
None => (trimmed, ""),
};
match base.trim().to_ascii_lowercase().as_str() {
"integer" => format!("int{args}"),
"int" => format!("int{args}"),
"bigint" => format!("bigint{args}"),
"smallint" => format!("smallint{args}"),
"str" => format!("str{args}"),
"text" => format!("str{args}"),
"varchar" => format!("varchar{args}"),
"char" => format!("char{args}"),
"bool" => format!("bool{args}"),
"boolean" => format!("bool{args}"),
"float" => format!("float{args}"),
"real" => format!("float{args}"),
"double" => format!("double{args}"),
"decimal" => format!("decimal{args}"),
"numeric" => format!("decimal{args}"),
"date" => format!("date{args}"),
"timestamp" => format!("timestamp{args}"),
"blob" => format!("blob{args}"),
_ => trimmed.to_string(),
}
}
/// 一个值的哈希,统计不同值个数用
fn value_hash(v: &serde_json::Value) -> u64 {
let mut h = DefaultHasher::new();
v.to_string().hash(&mut h);
h.finish()
}
/// 列名与值哈希合成一个哈希
fn fold(column: &str, value_hash: u64) -> u64 {
let mut h = DefaultHasher::new();
column.hash(&mut h);
value_hash.hash(&mut h);
h.finish()
}
+340
View File
@@ -0,0 +1,340 @@
use std::path::PathBuf;
use serde::Deserialize;
use crate::log::Level;
// 全局配置:只从 TOML 文件读,缺项回退内置默认值;
// 存储相关分区由本模块解析校验,其它层的分区一概不碰。
/// 配置文件名
pub const CONFIG_FILE_NAME: &str = "chusql.toml";
/// 配置目录名(随系统惯例放在用户配置目录下)
pub const APP_DIR_NAME: &str = "ChuSQL";
/// Unix 下的数据目录名(XDG 惯例小写,跟安装脚本的默认安装目录一致)
pub const UNIX_APP_DIR_NAME: &str = "chusql";
/// 数据子目录名
pub const DATA_DIR_NAME: &str = "data";
pub const DEFAULT_POOL_SIZE: usize = 1024;
pub const DEFAULT_PAGE_SIZE: usize = 4096;
pub const DEFAULT_BTREE_ORDER: usize = 4;
pub const DEFAULT_LOG_LEVEL: &str = "info";
const MIN_PAGE_SIZE: usize = 512;
const MAX_PAGE_SIZE: usize = 65536;
const MIN_BTREE_ORDER: usize = 3;
/// 默认配置文件路径(按平台惯例)
pub fn default_config_path() -> PathBuf {
#[cfg(windows)]
{
let appdata = std::env::var_os("APPDATA").filter(|v| !v.is_empty());
if let Some(appdata) = appdata {
return PathBuf::from(appdata).join(APP_DIR_NAME).join(CONFIG_FILE_NAME);
}
}
#[cfg(not(windows))]
{
let xdg = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty());
if let Some(xdg) = xdg {
return PathBuf::from(xdg).join(APP_DIR_NAME).join(CONFIG_FILE_NAME);
}
let home = std::env::var_os("HOME").filter(|v| !v.is_empty());
if let Some(home) = home {
return PathBuf::from(home)
.join(".config")
.join(APP_DIR_NAME)
.join(CONFIG_FILE_NAME);
}
}
PathBuf::from(APP_DIR_NAME).join(CONFIG_FILE_NAME)
}
/// Windows 默认数据目录
#[cfg(windows)]
pub fn default_data_dir() -> PathBuf {
let local = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty());
if let Some(local) = local {
return PathBuf::from(local).join(APP_DIR_NAME).join(DATA_DIR_NAME);
}
let roaming = std::env::var_os("APPDATA").filter(|v| !v.is_empty());
if let Some(roaming) = roaming {
return PathBuf::from(roaming).join(APP_DIR_NAME).join(DATA_DIR_NAME);
}
PathBuf::from(APP_DIR_NAME).join(DATA_DIR_NAME)
}
/// Unix 默认数据目录(XDG 惯例)
#[cfg(not(windows))]
pub fn default_data_dir() -> PathBuf {
let xdg = std::env::var_os("XDG_DATA_HOME").filter(|v| !v.is_empty());
if let Some(xdg) = xdg {
return PathBuf::from(xdg)
.join(UNIX_APP_DIR_NAME)
.join(DATA_DIR_NAME);
}
let home = std::env::var_os("HOME").filter(|v| !v.is_empty());
if let Some(home) = home {
return PathBuf::from(home)
.join(".local")
.join("share")
.join(UNIX_APP_DIR_NAME)
.join(DATA_DIR_NAME);
}
PathBuf::from(UNIX_APP_DIR_NAME).join(DATA_DIR_NAME)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub page_size: usize,
pub btree_order: usize,
pub pool_size: usize,
pub data_dir: PathBuf,
pub log_level: Level,
}
impl Default for Config {
/// 内置默认配置
fn default() -> Self {
Config {
page_size: DEFAULT_PAGE_SIZE,
btree_order: DEFAULT_BTREE_ORDER,
pool_size: DEFAULT_POOL_SIZE,
data_dir: default_data_dir(),
log_level: Level::Info,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Origin {
Default,
File(PathBuf),
}
impl Origin {
/// 来源的文案
pub fn describe(&self) -> String {
match self {
Origin::Default => "default".to_string(),
Origin::File(p) => format!("file {}", p.display()),
}
}
}
#[derive(Debug, Clone)]
pub struct Loaded {
pub config: Config,
pub config_path: Option<PathBuf>,
pub origins: Vec<(&'static str, String, Origin)>,
}
#[derive(Debug, Default, Deserialize)]
struct FileConfig {
page: Option<FilePage>,
btree: Option<FileBtree>,
buffer: Option<FileBuffer>,
storage: Option<FileStorage>,
log: Option<FileLog>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileBuffer {
pool_size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FilePage {
size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileBtree {
order: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileStorage {
data_dir: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileLog {
level: Option<String>,
}
/// 加载配置:显式路径优先,其次默认位置
pub fn load(explicit: Option<&str>) -> Result<Loaded, String> {
let explicit = explicit.map(str::trim).filter(|p| !p.is_empty());
let path = match explicit {
Some(p) => {
let p = PathBuf::from(p);
if !p.is_file() {
return Err(format!("config file not found: {}", p.display()));
}
Some(p)
}
None => {
let p = default_config_path();
if p.is_file() { Some(p) } else { None }
}
};
let text = match &path {
Some(p) => Some(
std::fs::read_to_string(p)
.map_err(|e| format!("cannot read config file {}: {}", p.display(), e))?,
),
None => None,
};
resolve(text.as_deref(), path)
}
/// 解析文本:文件 -> 默认
pub fn resolve(text: Option<&str>, path: Option<PathBuf>) -> Result<Loaded, String> {
let file: FileConfig = match text {
None => FileConfig::default(),
Some(t) => toml::from_str(t).map_err(|e| match &path {
Some(p) => format!("bad config file {}: {}", p.display(), e),
None => format!("bad config: {}", e),
})?,
};
let file_origin = Origin::File(
path.clone().unwrap_or_else(default_config_path),
);
let (page_size, page_origin) = pick(
file.page.and_then(|p| p.size),
DEFAULT_PAGE_SIZE,
&file_origin,
);
let (btree_order, order_origin) = pick(
file.btree.and_then(|b| b.order),
DEFAULT_BTREE_ORDER,
&file_origin,
);
let (pool_size, pool_origin) = pick(
file.buffer.and_then(|b| b.pool_size),
DEFAULT_POOL_SIZE,
&file_origin,
);
let (data_dir, dir_origin) = pick(
file.storage.as_ref().and_then(|s| s.data_dir.clone()),
default_data_dir().to_string_lossy().into_owned(),
&file_origin,
);
// 管名已经没有了:存储不再是独立进程,管道/套接字那一层随 FFI 一起删掉
let (log_level, log_origin) = pick(
file.log.and_then(|l| l.level),
DEFAULT_LOG_LEVEL.to_string(),
&file_origin,
);
let data_dir = data_dir.trim().to_string();
let log_level = log_level.trim().to_string();
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() {
return Err("storage.data_dir must not be empty".to_string());
}
validate_layout(page_size, btree_order)?;
if pool_size == 0 {
return Err("buffer.pool_size must be at least 1".to_string());
}
let origins = vec![
("page.size", page_size.to_string(), page_origin),
("btree.order", btree_order.to_string(), order_origin),
("buffer.pool_size", pool_size.to_string(), pool_origin),
("storage.data_dir", data_dir.clone(), dir_origin),
("log.level", log_level.name().to_string(), log_origin),
];
Ok(Loaded {
config: Config {
page_size,
btree_order,
pool_size,
data_dir: PathBuf::from(data_dir),
log_level,
},
config_path: if text.is_some() { path } else { None },
origins,
})
}
/// 取一项:文件 -> 默认
fn pick<T>(file_value: Option<T>, default: T, file_origin: &Origin) -> (T, Origin) {
if let Some(value) = file_value {
return (value, file_origin.clone());
}
(default, Origin::Default)
}
/// 检查选项是否自洽
fn validate_layout(page_size: usize, btree_order: usize) -> Result<(), String> {
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&page_size) || !page_size.is_power_of_two() {
return Err(format!(
"page.size must be a power of two between {} and {}, got {}",
MIN_PAGE_SIZE, MAX_PAGE_SIZE, page_size
));
}
if btree_order < MIN_BTREE_ORDER {
return Err(format!(
"btree.order must be at least {}, got {}",
MIN_BTREE_ORDER, btree_order
));
}
if !crate::btree::fits_in_page(page_size, btree_order) {
return Err(format!(
"btree.order {} does not fit in a {}-byte page (max {})",
btree_order,
page_size,
crate::btree::max_order(page_size)
));
}
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]
host = "127.0.0.1"
port = 7777
max_message = 1048576
[web]
port = 7778
user = "root"
"#;
let loaded = resolve(Some(text), Some(PathBuf::from("chusql.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());
}
}
+154
View File
@@ -0,0 +1,154 @@
use std::cell::RefCell;
use std::ffi::{c_char, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;
use std::slice;
use crate::runtime::Storage;
// C ABI:Haskell 侧唯一存储入口,请求与响应都是 UTF-8 JSON。
// 不 panic 穿过边界,失败走线程局部 last_error。
const OK: i32 = 0;
const ERR: i32 = -1;
thread_local! {
static LAST_ERROR: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// 记下本线程最近的失败信息
fn set_error(message: impl AsRef<str>) {
let text = message.as_ref().as_bytes().to_vec();
LAST_ERROR.with(|slot| *slot.borrow_mut() = text);
}
/// 版本号字符串(静态常量,调用方不要释放)。
#[unsafe(no_mangle)]
pub extern "C" fn chusql_storage_version() -> *const c_char {
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
VERSION.as_ptr() as *const c_char
}
/// 打开数据目录;失败返回 NULL
///
/// # Safety
/// `config_path` 要么是 NULL,要么指向以 NUL 结尾的有效字符串。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_open(config_path: *const c_char) -> *mut Storage {
let opened = catch_unwind(AssertUnwindSafe(|| {
let path = if config_path.is_null() {
None
} else {
let text = unsafe { CStr::from_ptr(config_path) }
.to_str()
.map_err(|_| "config path is not valid UTF-8".to_string())?;
if text.is_empty() { None } else { Some(text.to_string()) }
};
Storage::open(path.as_deref()).map(Box::new)
}));
match opened {
Ok(Ok(storage)) => Box::into_raw(storage),
Ok(Err(e)) => {
set_error(e);
ptr::null_mut()
}
Err(_) => {
set_error("panic in chusql_storage_open");
ptr::null_mut()
}
}
}
/// 关闭句柄(NULL 安全)
///
/// # Safety
/// `handle` 必须是 chusql_storage_open 返回且未被关闭过的句柄。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_close(handle: *mut Storage) {
if handle.is_null() {
return;
}
let _ = catch_unwind(AssertUnwindSafe(|| unsafe { drop(Box::from_raw(handle)) }));
}
/// 处理一条请求,响应缓冲须归还
///
/// # Safety
/// `handle` 必须是有效句柄;`request` 指向 `request_len` 个字节;`out`/`out_len` 可写。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_request(
handle: *const Storage,
request: *const u8,
request_len: usize,
out: *mut *mut u8,
out_len: *mut usize,
) -> i32 {
if !out.is_null() {
unsafe { *out = ptr::null_mut() };
}
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
if handle.is_null() || request.is_null() || out.is_null() || out_len.is_null() {
set_error("invalid argument");
return ERR;
}
let storage = unsafe { &*handle };
let bytes = unsafe { slice::from_raw_parts(request, request_len) };
let text = match std::str::from_utf8(bytes) {
Ok(text) => text,
Err(e) => {
set_error(format!("request is not valid UTF-8: {}", e));
return ERR;
}
};
let response = match catch_unwind(AssertUnwindSafe(|| storage.request_line(text))) {
Ok(response) => response,
Err(_) => {
set_error("panic in chusql_storage_request");
return ERR;
}
};
let mut buffer = response.into_bytes().into_boxed_slice();
let ptr = buffer.as_mut_ptr();
let len = buffer.len();
std::mem::forget(buffer);
unsafe {
*out = ptr;
*out_len = len;
}
OK
}
/// 归还响应缓冲
///
/// # Safety
/// `data`/`len` 必须是 chusql_storage_request 原样返回且未被释放过的一对值。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_free(data: *mut u8, len: usize) {
if data.is_null() {
return;
}
let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
let slice = ptr::slice_from_raw_parts_mut(data, len);
let _owned: Box<[u8]> = Box::from_raw(slice);
}));
}
/// 取本线程最近的失败信息
///
/// # Safety
/// `out_len` 可写,或为 NULL。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_last_error(out_len: *mut usize) -> *const u8 {
LAST_ERROR.with(|slot| {
let bytes = slot.borrow();
if !out_len.is_null() {
unsafe { *out_len = bytes.len() };
}
if bytes.is_empty() { ptr::null() } else { bytes.as_ptr() }
})
}
+672
View File
@@ -0,0 +1,672 @@
use std::collections::HashSet;
use std::io;
use std::path::Path;
use serde::de::{IgnoredAny, MapAccess, Visitor};
use serde::Deserializer;
use crate::btree::DiskBTree;
use crate::page::PageId;
use crate::page::{Page, PageFile};
use crate::protocol::Row;
// 堆表:行按插入顺序进页,支持全表扫描、按索引取行、行级删除。
const HEADER_SIZE: usize = 2;
const SLOT_SIZE: usize = 8;
const SLOT_DIR_START: usize = HEADER_SIZE;
/// 未选字段只校验并跳过,不分配值。
fn decode_row(bytes: &[u8], columns: Option<&[String]>, hidden: &HashSet<String>) -> io::Result<Row> {
struct RowVisitor<'a>(Option<&'a [String]>, &'a HashSet<String>);
impl<'de> Visitor<'de> for RowVisitor<'_> {
type Value = Row;
/// 说明期望的输入形态
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a row object")
}
/// 逐字段解出一行
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Row, M::Error> {
let mut row = Row::new();
while let Some(key) = map.next_key::<String>()? {
if !self.1.contains(&key) && self.0.is_none_or(|cols| cols.contains(&key)) {
row.insert(key, map.next_value()?);
} else {
map.next_value::<IgnoredAny>()?;
}
}
Ok(row)
}
}
let mut decoder = serde_json::Deserializer::from_slice(bytes);
let row = decoder.deserialize_map(RowVisitor(columns, hidden)).map_err(io::Error::other)?;
decoder.end().map_err(io::Error::other)?;
Ok(row)
}
/// 读一个小端 u16
fn read_u16(buf: &[u8], off: usize) -> u16 {
u16::from_le_bytes([buf[off], buf[off + 1]])
}
/// 写一个小端 u16
fn write_u16(buf: &mut [u8], off: usize, v: u16) {
buf[off..off + 2].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u32
fn read_u32(buf: &[u8], off: usize) -> u32 {
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
}
/// 写一个小端 u32
fn write_u32(buf: &mut [u8], off: usize, v: u32) {
buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
/// (页号, 槽位号) 打包成 u64
fn pack_position(page_id: PageId, slot: u16) -> u64 {
(page_id << 16) | (slot as u64)
}
/// 从 u64 解回 (页号, 槽位号)
fn unpack_position(v: u64) -> (PageId, u16) {
(v >> 16, (v & 0xFFFF) as u16)
}
/// 取行里某一列的整数值(不是整数就没有)
fn row_int(row: &Row, column: &str) -> Option<i64> {
match row.get(column) {
Some(serde_json::Value::Number(n)) => n.as_i64(),
_ => None,
}
}
/// i64 键 → 8 字节大端(保序,负数也在前)
fn key_of(value: i64) -> [u8; 8] {
(value as u64 ^ (1u64 << 63)).to_be_bytes()
}
/// JSON 值 → 索引键字节(与写索引时同一套编码)
fn value_key(v: &serde_json::Value) -> Option<Vec<u8>> {
match v {
serde_json::Value::Number(n) => n.as_i64().map(|k| key_of(k).to_vec()),
serde_json::Value::String(s) => Some(s.as_bytes().to_vec()),
serde_json::Value::Bool(b) => Some(vec![u8::from(*b)]),
_ => None,
}
}
/// 取行里某一列的索引键;NULL 与缺列不入索引
fn row_key(row: &Row, column: &str) -> Option<Vec<u8>> {
row.get(column).and_then(value_key)
}
pub struct HeapPage;
impl HeapPage {
/// 这一页有几个槽位
pub fn slot_count(page: &Page) -> u16 {
read_u16(&page.data, 0)
}
/// 改写槽位数量
fn set_slot_count(page: &mut Page, v: u16) {
write_u16(&mut page.data, 0, v);
}
/// 空闲空间下界
fn free_start(page: &Page) -> usize {
SLOT_DIR_START + (Self::slot_count(page) as usize) * SLOT_SIZE
}
/// 空闲空间上界
fn free_end(page: &Page) -> usize {
let n = Self::slot_count(page);
let mut min_off = page.data.len() as u32;
for i in 0..n as usize {
let slot_off = SLOT_DIR_START + i * SLOT_SIZE;
let off = read_u32(&page.data, slot_off);
let len = read_u32(&page.data, slot_off + 4);
if len > 0 && off < min_off {
min_off = off;
}
}
min_off as usize
}
/// 试插一行,放不下返回 None
pub fn insert_tuple(page: &mut Page, tuple: &[u8]) -> Option<u16> {
let n = Self::slot_count(page);
let free_start = Self::free_start(page);
let free_end = Self::free_end(page);
if free_start + SLOT_SIZE + tuple.len() > free_end {
return None;
}
let tuple_off = free_end - tuple.len();
page.data[tuple_off..tuple_off + tuple.len()].copy_from_slice(tuple);
let slot_off = SLOT_DIR_START + (n as usize) * SLOT_SIZE;
write_u32(&mut page.data, slot_off, tuple_off as u32);
write_u32(&mut page.data, slot_off + 4, tuple.len() as u32);
Self::set_slot_count(page, n + 1);
Some(n)
}
/// 删掉一个槽位,只把长度置 0
pub fn delete_tuple(page: &mut Page, slot: u16) -> bool {
let n = Self::slot_count(page);
if slot >= n {
return false;
}
let slot_off = SLOT_DIR_START + (slot as usize) * SLOT_SIZE;
if read_u32(&page.data, slot_off + 4) == 0 {
return false;
}
write_u32(&mut page.data, slot_off + 4, 0);
true
}
/// 读槽位里的行字节
pub fn get_tuple(page: &Page, slot: u16) -> Option<Vec<u8>> {
let n = Self::slot_count(page);
if slot >= n {
return None;
}
let slot_off = SLOT_DIR_START + (slot as usize) * SLOT_SIZE;
let off = read_u32(&page.data, slot_off) as usize;
let len = read_u32(&page.data, slot_off + 4) as usize;
if len == 0 {
return None;
}
Some(page.data[off..off + len].to_vec())
}
/// 所有槽位号
pub fn iter_slots(page: &Page) -> Vec<u16> {
(0..Self::slot_count(page)).collect()
}
}
struct NamedIndex {
column: String,
tree: DiskBTree,
}
pub struct HeapTable {
hidden_columns: HashSet<String>,
file: PageFile,
indexes: Vec<NamedIndex>,
}
impl HeapTable {
/// 打开表(不带索引)
pub fn open<P: AsRef<Path>>(path: P, page_size: usize, pool_size: usize) -> io::Result<Self> {
Ok(HeapTable {
hidden_columns: HashSet::new(),
file: PageFile::with_options(path, page_size, pool_size)?,
indexes: Vec::new(),
})
}
/// 打开表并挂上 id 列索引
pub fn open_indexed<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
index_path: Q,
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<Self> {
Self::open_with_indexes(
path,
&[("id".to_string(), index_path.as_ref().to_path_buf())],
page_size,
btree_order,
pool_size,
)
}
/// 打开表并挂上多组索引
pub fn open_with_indexes<P: AsRef<Path>>(
path: P,
indexes: &[(String, std::path::PathBuf)],
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<Self> {
let mut t = HeapTable {
hidden_columns: HashSet::new(),
file: PageFile::with_options(path, page_size, pool_size)?,
indexes: Vec::new(),
};
let mut filled = false;
for (col, p) in indexes {
match t.attach_index(col, p, page_size, btree_order, pool_size) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::InvalidData => {
// 格式不兼容(例如上一版落的索引文件):删掉重开
let _ = std::fs::remove_file(p);
t.attach_index(col, p, page_size, btree_order, pool_size)?;
}
Err(err) => return Err(err),
}
// 空索引(新文件、被截断、强杀留下)都按堆数据补齐
if t.index_empty(col)? {
t.fill_index(col)?;
filled = true;
}
}
if filled {
t.flush()?;
}
Ok(t)
}
/// 挂上一棵索引树,不填数据
fn attach_index<P: AsRef<Path>>(
&mut self,
column: &str,
path: P,
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<()> {
let tree = DiskBTree::open(path, page_size, btree_order, pool_size)?;
self.indexes.push(NamedIndex {
column: column.to_string(),
tree,
});
Ok(())
}
/// 这一列有索引吗
pub fn has_index(&self, column: &str) -> bool {
self.indexes.iter().any(|i| i.column == column)
}
/// 建索引:id 列必须唯一,其余列可重复
pub fn build_index(
&mut self,
column: &str,
path: &Path,
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<()> {
if self.has_index(column) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("index on column \"{}\" already exists", column),
));
}
let mut seen: HashSet<Vec<u8>> = HashSet::new();
let mut entries: Vec<(Vec<u8>, u64)> = Vec::new();
for (page_id, slot, r) in self.scan_with_positions()? {
let Some(k) = row_key(&r, column) else {
continue;
};
// 行身份索引仍然唯一;其余列允许重复值
if column == "id" && !seen.insert(k.clone()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("column \"{}\" has duplicate values", column),
));
}
entries.push((k, pack_position(page_id, slot)));
}
self.attach_index(column, path, page_size, btree_order, pool_size)?;
let i = self.indexes.len() - 1;
self.indexes[i].tree.clear()?;
for (k, pos) in entries {
self.indexes[i].tree.insert(&k, pos)?;
}
Ok(())
}
/// 这一列的索引是空的吗
fn index_empty(&mut self, column: &str) -> io::Result<bool> {
match self.indexes.iter_mut().find(|i| i.column == column) {
Some(idx) => idx.tree.is_empty(),
None => Ok(false),
}
}
/// 按堆里的行把某一列索引补齐
fn fill_index(&mut self, column: &str) -> io::Result<()> {
let rows = self.scan_with_positions()?;
let Some(i) = self.indexes.iter().position(|idx| idx.column == column) else {
return Ok(());
};
for (page_id, slot, row) in rows {
if let Some(k) = row_key(&row, column) {
self.indexes[i]
.tree
.insert(&k, pack_position(page_id, slot))?;
}
}
Ok(())
}
/// 摘掉一个索引(不删文件)
pub fn detach_index(&mut self, column: &str) -> bool {
match self.indexes.iter().position(|i| i.column == column) {
None => false,
Some(i) => {
self.indexes.remove(i);
true
}
}
}
/// 设置隐藏列集合
pub fn set_hidden_columns(&mut self, columns: HashSet<String>) {
self.hidden_columns = columns;
}
/// 隐藏一列并摘掉它的索引
pub fn hide_column(&mut self, column: &str) {
self.detach_index(column);
self.hidden_columns.insert(column.to_string());
}
/// 插入一行,并顺手写各索引
pub fn insert_row(&mut self, row: &Row) -> io::Result<()> {
self.insert_row_returning_position(row).map(|_| ())
}
/// 插入一行并返回它的位置
pub fn insert_row_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
let mut pending: Vec<(usize, Vec<u8>)> = Vec::new();
for (i, idx) in self.indexes.iter_mut().enumerate() {
let Some(k) = row_key(row, &idx.column) else {
continue;
};
if idx.column == "id" && idx.tree.get(&k)?.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"duplicate value on the id index".to_string(),
));
}
pending.push((i, k));
}
let (page_id, slot) = self.insert_returning_position(row)?;
let pos = pack_position(page_id, slot);
for (i, k) in pending {
self.indexes[i].tree.insert(&k, pos)?;
}
Ok((page_id, slot))
}
/// 批量插入,落盘由调用方负责
pub fn insert_rows(&mut self, rows: &[Row]) -> io::Result<()> {
for r in rows {
self.insert_row(r)?;
}
Ok(())
}
/// 编码并验证一行能放进页
pub fn encode_row(row: &Row, page_size: usize) -> io::Result<Vec<u8>> {
let tuple = serde_json::to_vec(row).map_err(io::Error::other)?;
if HEADER_SIZE + SLOT_SIZE + tuple.len() > page_size {
return Err(io::Error::other(format!(
"tuple too large: {} bytes",
tuple.len()
)));
}
Ok(tuple)
}
/// 插入一行,只进堆表、不碰索引
fn insert_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
let tuple = Self::encode_row(row, self.file.page_size())?;
let n = self.file.num_pages()?;
if n > 0 {
let last_id = n - 1;
let mut slot = None;
self.file.update_page(last_id, |p| {
slot = HeapPage::insert_tuple(p, &tuple);
})?;
if let Some(s) = slot {
return Ok((last_id, s));
}
}
let mut p = self.file.append_page()?;
let page_id = p.id;
let slot = HeapPage::insert_tuple(&mut p, &tuple).expect("pre-checked fits");
self.file.write_page(&p)?;
Ok((page_id, slot))
}
/// 按 `id` 取一行
pub fn get_by_key(&mut self, key: i64) -> io::Result<Option<Row>> {
self.get_by_column_key("id", key)
}
/// 按某一列的整数索引取一行
pub fn get_by_column_key(&mut self, column: &str, key: i64) -> io::Result<Option<Row>> {
self.get_by_bytes(column, &key_of(key))
}
/// 按某一列的字符串索引取一行
pub fn get_by_string_key(&mut self, column: &str, key: &str) -> io::Result<Option<Row>> {
self.get_by_bytes(column, key.as_bytes())
}
/// 按某一列的整数索引取全部行
pub fn get_all_by_column_key(&mut self, column: &str, key: i64) -> io::Result<Vec<Row>> {
self.get_all_by_bytes(column, &key_of(key))
}
/// 按某一列的字符串索引取全部行
pub fn get_all_by_string_key(&mut self, column: &str, key: &str) -> io::Result<Vec<Row>> {
self.get_all_by_bytes(column, key.as_bytes())
}
/// 按索引键的字节取第一行
fn get_by_bytes(&mut self, column: &str, key: &[u8]) -> io::Result<Option<Row>> {
Ok(self.get_all_by_bytes(column, key)?.into_iter().next())
}
/// 按索引键的字节取全部行(重复值都给出来)
fn get_all_by_bytes(&mut self, column: &str, key: &[u8]) -> io::Result<Vec<Row>> {
let Some(idx) = self.indexes.iter_mut().find(|i| i.column == column) else {
return Ok(Vec::new());
};
let positions = idx.tree.get_all(key)?;
let mut rows = Vec::new();
for pos in positions {
let (page_id, slot) = unpack_position(pos);
if let Some(row) = self.read_at(page_id, slot)? {
rows.push(row);
}
}
Ok(rows)
}
/// 按索引做范围扫描;没索引返回 None
pub fn scan_range(
&mut self,
column: &str,
lo: Option<(&serde_json::Value, bool)>,
hi: Option<(&serde_json::Value, bool)>,
) -> io::Result<Option<Vec<Row>>> {
let Some(i) = self.indexes.iter().position(|idx| idx.column == column) else {
return Ok(None);
};
let lo_bound = lo.and_then(|(v, inclusive)| value_key(v).map(|k| (k, inclusive)));
let hi_bound = hi.and_then(|(v, inclusive)| value_key(v).map(|k| (k, inclusive)));
// 树按闭区间取,排他端在取回来之后再筛
let pairs = self.indexes[i].tree.scan(
lo_bound.as_ref().map(|(k, _)| k.as_slice()),
hi_bound.as_ref().map(|(k, _)| k.as_slice()),
)?;
let mut rows = Vec::new();
for (key, pos) in pairs {
if let Some((bound, inclusive)) = &lo_bound {
let outside = if *inclusive { &key < bound } else { &key <= bound };
if outside {
continue;
}
}
if let Some((bound, inclusive)) = &hi_bound {
let outside = if *inclusive { &key > bound } else { &key >= bound };
if outside {
continue;
}
}
let (page_id, slot) = unpack_position(pos);
if let Some(row) = self.read_at(page_id, slot)? {
rows.push(row);
}
}
Ok(Some(rows))
}
/// 按位置读一行
pub fn read_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
let bytes = self.file.with_page(page_id, |p| HeapPage::get_tuple(p, slot))?;
match bytes {
None => Ok(None),
Some(bytes) => {
let row = decode_row(&bytes, None, &self.hidden_columns)?;
Ok(Some(row))
}
}
}
/// 按位置删一行并返回它
pub fn delete_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
let row = self.read_at(page_id, slot)?;
let existed = self.file.update_page(page_id, |p| HeapPage::delete_tuple(p, slot))?;
if !existed {
return Ok(None);
}
if let Some(r) = &row {
let pos = pack_position(page_id, slot);
for idx in &mut self.indexes {
if let Some(k) = row_key(r, &idx.column) {
idx.tree.delete(&k, pos)?;
}
}
}
Ok(row)
}
/// 按 id 批量删行,返回删掉的行
pub fn delete_by_keys(&mut self, keys: &[i64]) -> io::Result<Vec<Row>> {
let mut positions: Vec<(PageId, u16)> = Vec::new();
if self.has_index("id") {
for &k in keys {
let pos = self
.indexes
.iter_mut()
.find(|i| i.column == "id")
.and_then(|i| i.tree.get(&key_of(k)).ok().flatten());
if let Some(p) = pos {
positions.push(unpack_position(p));
}
}
} else {
let mut wanted: Vec<i64> = keys.to_vec();
wanted.sort_unstable();
for (page_id, slot, r) in self.scan_with_positions()? {
if row_int(&r, "id").is_some_and(|k| wanted.binary_search(&k).is_ok()) {
positions.push((page_id, slot));
}
}
}
let mut deleted: Vec<Row> = Vec::new();
for (page_id, slot) in positions {
if let Some(r) = self.delete_at(page_id, slot)? {
deleted.push(r);
}
}
Ok(deleted)
}
/// 全表扫描
pub fn scan(&mut self) -> io::Result<Vec<Row>> {
self.scan_columns(None)
}
/// 全表扫描,只解码指定列
pub fn scan_columns(&mut self, columns: Option<&[String]>) -> io::Result<Vec<Row>> {
Ok(self
.scan_projected_with_positions(columns)?
.into_iter()
.map(|(_, _, r)| r)
.collect())
}
/// 全表扫描,同时给出每行的位置(建索引用)
pub fn scan_with_positions(&mut self) -> io::Result<Vec<(PageId, u16, Row)>> {
self.scan_projected_with_positions(None)
}
/// 全表扫描,带位置与列投影
fn scan_projected_with_positions(&mut self, columns: Option<&[String]>) -> io::Result<Vec<(PageId, u16, Row)>> {
let n = self.file.num_pages()?;
let mut out = Vec::new();
for i in 0..n {
let tuples = self.file.with_page(i, |p| {
let mut v = Vec::new();
for slot in HeapPage::iter_slots(p) {
if let Some(bytes) = HeapPage::get_tuple(p, slot) {
v.push((slot, bytes));
}
}
v
})?;
for (slot, bytes) in tuples {
let row = decode_row(&bytes, columns, &self.hidden_columns)?;
out.push((i, slot, row));
}
}
Ok(out)
}
/// 整表改写并重建所有索引
pub fn replace_all(&mut self, rows: &[Row]) -> io::Result<()> {
self.file.truncate()?;
for idx in &mut self.indexes {
idx.tree.clear()?;
}
for r in rows {
let (page_id, slot) = self.insert_returning_position(r)?;
let pos = pack_position(page_id, slot);
for idx in &mut self.indexes {
if let Some(k) = row_key(r, &idx.column) {
idx.tree.insert(&k, pos)?;
}
}
}
Ok(())
}
/// 把脏页写回(索引也一起)
pub fn flush(&mut self) -> io::Result<()> {
for idx in &mut self.indexes {
idx.tree.flush()?;
}
self.file.flush()
}
/// 当前缓存页数
pub fn cached_pages(&self) -> usize { self.file.cached_pages() }
/// 命中次数
pub fn hits(&self) -> u64 { self.file.hits() }
/// 未命中次数
pub fn misses(&self) -> u64 { self.file.misses() }
/// 命中率
pub fn hit_rate(&self) -> f64 { self.file.hit_rate() }
}
+206
View File
@@ -0,0 +1,206 @@
use crate::protocol::SchemaColumn;
// 读取 scripts/init.sql,只认 CREATE TABLE 与 CREATE INDEX。
// 系统库的 schema 由这份脚本声明,进程按它建系统表。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitColumn {
pub name: String,
pub ty: String,
pub nullable: bool,
pub auto_increment: bool,
pub primary_key: bool,
pub unique: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitTable {
pub name: String,
pub columns: Vec<InitColumn>,
pub indexes: Vec<String>,
}
impl InitColumn {
/// 转成数据字典里的列
pub fn to_schema(&self) -> SchemaColumn {
SchemaColumn {
name: self.name.clone(),
ty: self.ty.clone(),
nullable: self.nullable,
default: None,
auto_increment: self.auto_increment,
primary_key: self.primary_key,
unique: self.unique,
check: None,
}
}
}
/// 解析整份脚本;不认识的语句直接报错,避免悄悄漏建表
pub fn parse(text: &str) -> Result<Vec<InitTable>, String> {
let mut tables: Vec<InitTable> = Vec::new();
for statement in statements(text) {
let words = statement.split_whitespace().collect::<Vec<_>>();
let head = |index: usize| words.get(index).map(|word| word.to_ascii_uppercase());
match (head(0).as_deref(), head(1).as_deref()) {
(Some("CREATE"), Some("TABLE")) => {
let name = clean_name(words.get(2).ok_or("CREATE TABLE needs a name")?);
let body = statement.split_once('(').ok_or("CREATE TABLE needs a column list")?.1;
let inner = body.rsplit_once(')').map(|(inner, _)| inner).unwrap_or(body);
tables.push(InitTable { name, columns: columns(inner)?, indexes: Vec::new() });
}
(Some("CREATE"), Some("INDEX")) => {
let table = clean_name(words.get(2).ok_or("CREATE INDEX needs a table")?);
let column = statement
.split_once('(')
.and_then(|(_, rest)| rest.split_once(')'))
.map(|(column, _)| clean_name(column))
.ok_or("CREATE INDEX needs a column")?;
let entry = tables
.iter_mut()
.find(|entry| entry.name.eq_ignore_ascii_case(&table))
.ok_or_else(|| format!("CREATE INDEX on unknown table: {table}"))?;
entry.indexes.push(column);
}
_ => return Err(format!("unsupported statement: {}", statement.trim())),
}
}
Ok(tables)
}
/// 解析表定义里的列清单
fn columns(body: &str) -> Result<Vec<InitColumn>, String> {
let mut out = Vec::new();
for part in split_top_level(body) {
let words = part.split_whitespace().collect::<Vec<_>>();
if words.is_empty() {
continue;
}
let keyword = words[0].to_ascii_uppercase();
if matches!(keyword.as_str(), "PRIMARY" | "UNIQUE" | "KEY" | "CONSTRAINT" | "INDEX" | "CHECK" | "FOREIGN") {
return Err(format!("table-level constraint is not supported: {}", part.trim()));
}
let name = clean_name(words[0]);
let ty = words
.get(1)
.ok_or_else(|| format!("column {name} needs a type"))?
.to_string();
let upper = part.to_ascii_uppercase();
out.push(InitColumn {
name,
ty,
nullable: !upper.contains("NOT NULL"),
auto_increment: upper.contains("AUTO_INCREMENT"),
primary_key: upper.contains("PRIMARY KEY"),
unique: upper.contains("UNIQUE"),
});
}
Ok(out)
}
/// 按顶层的逗号切开(类型里的括号不算)
fn split_top_level(body: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut depth = 0;
for ch in body.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
parts.push(std::mem::take(&mut current));
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
parts.push(current);
}
parts
}
/// 去掉反引号与空白
fn clean_name(raw: &str) -> String {
raw.trim().trim_matches('`').trim_matches('"').trim().to_string()
}
/// 去掉注释后按分号切开
fn statements(text: &str) -> Vec<String> {
let mut cleaned = String::new();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '-' && chars.peek() == Some(&'-') {
for next in chars.by_ref() {
if next == '\n' {
cleaned.push('\n');
break;
}
}
} else if ch == '/' && chars.peek() == Some(&'*') {
chars.next();
while let Some(next) = chars.next() {
if next == '*' && chars.peek() == Some(&'/') {
chars.next();
break;
}
}
} else {
cleaned.push(ch);
}
}
cleaned
.split(';')
.map(|statement| statement.trim().to_string())
.filter(|statement| !statement.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// 仓库里的系统库脚本必须始终能解析
const SYSTEM_SCHEMA: &str = include_str!("../../../scripts/init.sql");
/// 仓库自带脚本必须能解析出账号表
#[test]
fn parses_the_repository_init_script() {
let tables = parse(SYSTEM_SCHEMA).expect("scripts/init.sql must parse");
let users = tables
.iter()
.find(|table| table.name == "__chusql_users")
.expect("init.sql must declare the account table");
let names: Vec<&str> = users.columns.iter().map(|column| column.name.as_str()).collect();
assert_eq!(names, ["id", "user", "password_hash", "registered_at", "last_login_at", "revision"]);
assert_eq!(users.columns[0].ty, "int");
assert!(users.columns[0].primary_key && users.columns[0].auto_increment && !users.columns[0].nullable);
assert_eq!(users.columns[1].ty, "varchar(64)");
assert!(users.columns[1].unique);
assert_eq!(users.columns[4].ty, "timestamp");
assert!(users.columns[4].nullable);
assert_eq!(users.indexes, ["user"]);
}
/// 未知语句与表级约束要报错
#[test]
fn rejects_unknown_statements_and_table_level_constraints() {
assert!(parse("DROP TABLE users;").is_err());
assert!(parse("CREATE TABLE t (id int, PRIMARY KEY (id));").is_err());
assert!(parse("CREATE INDEX missing (user);").is_err());
}
/// 注释与空语句要被跳过
#[test]
fn ignores_comments_and_blank_statements() {
let tables = parse("-- note\nCREATE TABLE t (a int NOT NULL);\n\n/* done */\n").unwrap();
assert_eq!(tables.len(), 1);
assert_eq!(tables[0].columns[0].name, "a");
assert!(!tables[0].columns[0].nullable);
}
}
+13
View File
@@ -0,0 +1,13 @@
// 存储层库入口:对外暴露下面这些模块,服务与集成测试都从这里引。
pub mod btree;
pub mod catalog;
pub mod config;
pub mod ffi;
pub mod heap;
pub mod initsql;
pub mod log;
pub mod page;
pub mod protocol;
pub mod runtime;
pub mod wal;
+368
View File
@@ -0,0 +1,368 @@
use std::fmt;
use std::io::{IsTerminal, Write};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::OnceLock;
// 极简日志:等级用原子变量,关掉的等级只花一次原子读。
const RESET: &str = "\u{1b}[0m";
const BOLD_RED: &str = "\u{1b}[1m\u{1b}[31m";
const BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m";
const BOLD: &str = "\u{1b}[1m";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Off,
Error,
Warn,
Info,
Debug,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Channel {
Core,
Request,
}
impl Channel {
/// 来源标签
fn tag(self) -> &'static str {
match self {
Channel::Core => "core",
Channel::Request => "request",
}
}
}
impl Level {
/// 解析等级名
pub fn parse(s: &str) -> Option<Level> {
match s.trim().to_ascii_lowercase().as_str() {
"off" => Some(Level::Off),
"error" => Some(Level::Error),
"warn" | "warning" => Some(Level::Warn),
"info" => Some(Level::Info),
"debug" => Some(Level::Debug),
_ => None,
}
}
/// 等级名
pub fn name(self) -> &'static str {
match self {
Level::Off => "off",
Level::Error => "error",
Level::Warn => "warn",
Level::Info => "info",
Level::Debug => "debug",
}
}
/// 行里的等级标签
fn tag(self) -> &'static str {
match self {
Level::Error => "ERROR",
Level::Warn => "WARN ",
Level::Info => "INFO ",
_ => "DEBUG",
}
}
/// 等级的颜色
fn color(self) -> &'static str {
match self {
Level::Error => BOLD_RED,
Level::Warn => BOLD_ORANGE,
Level::Debug => BOLD,
_ => "",
}
}
}
static LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
static COLOR_STDERR: OnceLock<bool> = OnceLock::new();
static COLOR_STDOUT: OnceLock<bool> = OnceLock::new();
/// 设置全局等级
pub fn set_level(level: Level) {
LEVEL.store(level as u8, Ordering::Relaxed);
}
#[inline]
/// 这个等级要不要输出
pub fn enabled(level: Level) -> bool {
level as u8 <= LEVEL.load(Ordering::Relaxed)
}
/// 启动时调一次
pub fn init() {
enable_vt();
}
/// 写出一行日志,按等级选流向
pub fn write(channel: Channel, level: Level, args: fmt::Arguments) {
let msg = args.to_string();
let to_stderr = streams_to_stderr(level);
let line = render(level, channel, color_enabled(to_stderr), &now(), &msg);
if to_stderr {
let mut out = std::io::stderr().lock();
let _ = writeln!(out, "{}", line);
} else {
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "{}", line);
}
}
/// 这个等级是否写 stderr
fn streams_to_stderr(level: Level) -> bool {
matches!(level, Level::Error | Level::Warn)
}
/// 按目标流是否终端决定是否着色
fn color_enabled(to_stderr: bool) -> bool {
let cached = if to_stderr { &COLOR_STDERR } else { &COLOR_STDOUT };
*cached.get_or_init(|| if to_stderr { std::io::stderr().is_terminal() } else { std::io::stdout().is_terminal() })
}
/// 拼一行(纯函数,可测)
fn render(level: Level, channel: Channel, color: bool, stamp: &Stamp, msg: &str) -> String {
let tag = level.tag();
let paint = level.color();
let level_text = if color && !paint.is_empty() {
format!("{}{}{}", paint, tag, RESET)
} else {
tag.to_string()
};
format!("[{} {}] [{}] {}", level_text, stamp, channel.tag(), msg)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stamp {
pub year: u16,
pub month: u16,
pub day: u16,
pub hour: u16,
pub minute: u16,
pub second: u16,
pub milli: u16,
}
impl fmt::Display for Stamp {
/// 格式化成时间戳文本
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",
self.year, self.month, self.day, self.hour, self.minute, self.second, self.milli
)
}
}
/// 当前本地时间(Windows)
#[cfg(windows)]
fn now() -> Stamp {
#[repr(C)]
struct SystemTime {
year: u16,
month: u16,
day_of_week: u16,
day: u16,
hour: u16,
minute: u16,
second: u16,
milliseconds: u16,
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetLocalTime(out: *mut SystemTime);
}
let mut t = SystemTime {
year: 0,
month: 0,
day_of_week: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
milliseconds: 0,
};
unsafe { GetLocalTime(&mut t) };
Stamp {
year: t.year,
month: t.month,
day: t.day,
hour: t.hour,
minute: t.minute,
second: t.second,
milli: t.milliseconds,
}
}
/// 当前时间(其他平台用 UTC)
#[cfg(not(windows))]
fn now() -> Stamp {
let d = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = d.as_secs();
let (year, month, day) = civil_from_days((secs / 86_400) as i64);
Stamp {
year: year as u16,
month,
day,
hour: ((secs % 86_400) / 3600) as u16,
minute: ((secs % 3_600) / 60) as u16,
second: (secs % 60) as u16,
milli: d.subsec_millis() as u16,
}
}
/// 天数换算成日期
pub fn civil_from_days(days: i64) -> (i64, u16, u16) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = (doy - (153 * mp + 2) / 5 + 1) as u16;
let month = if mp < 10 { mp + 3 } else { mp - 9 } as u16;
(y + i64::from(month <= 2), month, day)
}
/// 打开 Windows 的 VT 处理
#[cfg(windows)]
fn enable_vt() {
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetStdHandle(which: u32) -> *mut core::ffi::c_void;
fn GetConsoleMode(handle: *mut core::ffi::c_void, mode: *mut u32) -> i32;
fn SetConsoleMode(handle: *mut core::ffi::c_void, mode: u32) -> i32;
}
const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
unsafe {
let handle = GetStdHandle(STD_ERROR_HANDLE);
let mut mode = 0u32;
if GetConsoleMode(handle, &mut mode) != 0 {
let _ = SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
/// 其他平台什么都不用做
#[cfg(not(windows))]
fn enable_vt() {}
#[macro_export]
macro_rules! log_debug {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Debug, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Debug, $($arg)*)
};
}
#[macro_export]
macro_rules! log_info {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Info, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Info, $($arg)*)
};
}
#[macro_export]
macro_rules! log_warn {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Warn, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Warn, $($arg)*)
};
}
#[macro_export]
macro_rules! log_error {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Error, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Error, $($arg)*)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! log_line {
($chan:expr, $lvl:expr, $($arg:tt)*) => {
if $crate::log::enabled($lvl) {
$crate::log::write($chan, $lvl, format_args!($($arg)*))
}
};
}
#[cfg(test)]
mod tests {
use super::*;
/// 已知日期换算要正确
#[test]
fn civil_from_days_known_dates() {
assert_eq!(civil_from_days(0), (1970, 1, 1));
assert_eq!(civil_from_days(10_957), (2000, 1, 1));
assert_eq!(civil_from_days(11_016), (2000, 2, 29));
assert_eq!(civil_from_days(19_723), (2024, 1, 1));
assert_eq!(civil_from_days(19_782), (2024, 2, 29));
}
/// 行里要有等级、时间与来源
#[test]
fn render_has_time_channel_and_colors() {
let stamp = Stamp {
year: 2026,
month: 9,
day: 24,
hour: 13,
minute: 45,
second: 12,
milli: 130,
};
assert_eq!(
render(Level::Error, Channel::Request, true, &stamp, "boom"),
"[\u{1b}[1m\u{1b}[31mERROR\u{1b}[0m 2026-09-24 13:45:12.130] [request] boom"
);
assert_eq!(
render(Level::Warn, Channel::Core, true, &stamp, "careful"),
"[\u{1b}[1m\u{1b}[38;5;208mWARN \u{1b}[0m 2026-09-24 13:45:12.130] [core] careful"
);
assert_eq!(
render(Level::Debug, Channel::Request, true, &stamp, "detail"),
"[\u{1b}[1mDEBUG\u{1b}[0m 2026-09-24 13:45:12.130] [request] detail"
);
assert_eq!(
render(Level::Info, Channel::Core, true, &stamp, "hello"),
"[INFO 2026-09-24 13:45:12.130] [core] hello"
);
}
/// warn 与 error 走 stderr
#[test]
fn warnings_and_errors_go_to_stderr() {
assert!(streams_to_stderr(Level::Error));
assert!(streams_to_stderr(Level::Warn));
assert!(!streams_to_stderr(Level::Info));
assert!(!streams_to_stderr(Level::Debug));
}
}
+289
View File
@@ -0,0 +1,289 @@
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
// 页管理:定长页的读写,外加一层页缓冲池。
pub type PageId = u64;
pub const DEFAULT_POOL_SIZE: usize = 64;
#[derive(Debug, Clone)]
pub struct Page {
pub id: PageId,
pub data: Vec<u8>,
}
impl Page {
/// 新建一页,全 0
pub fn new(id: PageId, size: usize) -> Self {
Page {
id,
data: vec![0u8; size],
}
}
/// 清零
pub fn zero(&mut self) {
self.data.fill(0);
}
}
#[derive(Debug)]
pub struct PageFile {
file: File,
page_size: usize,
capacity: usize,
cache: HashMap<PageId, Page>,
last_used: HashMap<PageId, u64>,
clock: u64,
dirty: HashSet<PageId>,
hits: u64,
misses: u64,
}
impl PageFile {
/// 打开页文件(默认池容量)
pub fn open<P: AsRef<Path>>(path: P, page_size: usize) -> io::Result<Self> {
Self::with_options(path, page_size, DEFAULT_POOL_SIZE)
}
/// 打开页文件并指定池容量
pub fn with_options<P: AsRef<Path>>(
path: P,
page_size: usize,
capacity: usize,
) -> io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
Ok(PageFile {
file,
page_size,
capacity,
cache: HashMap::new(),
last_used: HashMap::new(),
clock: 0,
dirty: HashSet::new(),
hits: 0,
misses: 0,
})
}
/// 页大小
pub fn page_size(&self) -> usize {
self.page_size
}
/// 池容量(页数)
pub fn capacity(&self) -> usize {
self.capacity
}
/// 当前缓存了多少页
pub fn cached_pages(&self) -> usize {
self.cache.len()
}
/// 命中次数
pub fn hits(&self) -> u64 {
self.hits
}
/// 未命中次数
pub fn misses(&self) -> u64 {
self.misses
}
/// 命中率
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
/// 标记为最近使用
fn touch(&mut self, id: PageId) {
self.clock += 1;
self.last_used.insert(id, self.clock);
}
/// 淘汰一个最冷的页
fn evict_one(&mut self) -> io::Result<bool> {
let victim = self
.cache
.keys()
.min_by_key(|id| self.last_used.get(id).copied().unwrap_or(0))
.copied();
let Some(victim) = victim else {
return Ok(false);
};
if self.dirty.contains(&victim) {
let p = self.cache[&victim].clone();
self.write_raw(&p)?;
self.dirty.remove(&victim);
}
self.cache.remove(&victim);
self.last_used.remove(&victim);
Ok(true)
}
/// 直接写文件
fn write_raw(&mut self, page: &Page) -> io::Result<()> {
if page.data.len() != self.page_size {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"page has {} bytes, this file uses {}",
page.data.len(),
self.page_size
),
));
}
self.file
.seek(SeekFrom::Start(page.id * self.page_size as u64))?;
self.file.write_all(&page.data)
}
/// 直接从文件读
fn read_raw(&mut self, id: PageId) -> io::Result<Page> {
let mut data = vec![0u8; self.page_size];
self.file.seek(SeekFrom::Start(id * self.page_size as u64))?;
self.file.read_exact(&mut data)?;
Ok(Page { id, data })
}
/// 读一页(走缓存)
pub fn read_page(&mut self, id: PageId) -> io::Result<Page> {
self.note_access(id)?;
Ok(self.cache[&id].clone())
}
/// 只读地用一页(走缓存,不整页拷贝)
pub fn with_page<R>(&mut self, id: PageId, f: impl FnOnce(&Page) -> R) -> io::Result<R> {
self.note_access(id)?;
Ok(f(&self.cache[&id]))
}
/// 就地改一页(走缓存,不整页拷贝)
pub fn update_page<R>(&mut self, id: PageId, f: impl FnOnce(&mut Page) -> R) -> io::Result<R> {
self.note_access(id)?;
let r = f(self.cache.get_mut(&id).expect("cached above"));
self.dirty.insert(id);
Ok(r)
}
/// 记一次访问:命中 / 未命中都记数
fn note_access(&mut self, id: PageId) -> io::Result<()> {
if self.cache.contains_key(&id) {
self.hits += 1;
} else {
self.misses += 1;
let p = self.read_raw(id)?;
if self.cache.len() >= self.capacity {
self.evict_one()?;
}
self.cache.insert(id, p);
}
self.touch(id);
Ok(())
}
/// 写一页(走缓存)
pub fn write_page(&mut self, page: &Page) -> io::Result<()> {
if page.data.len() != self.page_size {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"page has {} bytes, this file uses {}",
page.data.len(),
self.page_size
),
));
}
if self.cache.len() >= self.capacity && !self.cache.contains_key(&page.id) {
self.evict_one()?;
}
self.cache.insert(page.id, page.clone());
self.dirty.insert(page.id);
self.touch(page.id);
Ok(())
}
/// 末尾追加一页
pub fn append_page(&mut self) -> io::Result<Page> {
let id = self.num_pages()?;
let page = Page::new(id, self.page_size);
self.write_page(&page)?;
Ok(page)
}
/// 文件里有多少页
pub fn num_pages(&mut self) -> io::Result<PageId> {
let len = self.file.metadata()?.len();
let size = self.page_size as u64;
if len % size != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"file length {} is not a multiple of page size {} (did page.size change?)",
len, size
),
));
}
let on_disk = len / size;
let cached_max = self
.cache
.keys()
.copied()
.max()
.map(|m| m + 1)
.unwrap_or(0);
Ok(on_disk.max(cached_max))
}
/// 把脏页写回磁盘
pub fn flush(&mut self) -> io::Result<()> {
let dirty: Vec<PageId> = self.dirty.iter().copied().collect();
for id in dirty {
if let Some(p) = self.cache.get(&id) {
let p = p.clone();
self.write_raw(&p)?;
}
}
self.file.sync_all()?;
self.dirty.clear();
Ok(())
}
/// flush 之后 fsync
pub fn sync(&mut self) -> io::Result<()> {
self.flush()
}
/// 清空文件
pub fn truncate(&mut self) -> io::Result<()> {
self.cache.clear();
self.last_used.clear();
self.dirty.clear();
self.file.set_len(0)?;
self.file.seek(SeekFrom::Start(0))?;
Ok(())
}
}
impl Drop for PageFile {
/// 析构时把脏页写回
fn drop(&mut self) {
let _ = self.flush();
}
}
+236
View File
@@ -0,0 +1,236 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// 协议:Haskell 与 Rust 之间的请求 / 响应类型。
pub type Row = HashMap<String, serde_json::Value>;
pub const USERS_TABLE: &str = "__chusql_users";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Account {
pub id: i64,
pub user: String,
pub password_hash: String,
pub revision: u64,
#[serde(default)]
pub registered_at: String,
#[serde(default)]
pub last_login_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchemaColumn {
pub name: String,
pub ty: String,
#[serde(default = "default_true")]
pub nullable: bool,
#[serde(default)]
pub default: Option<serde_json::Value>,
#[serde(default)]
pub auto_increment: bool,
#[serde(default)]
pub primary_key: bool,
#[serde(default)]
pub unique: bool,
#[serde(default)]
pub check: Option<String>,
}
impl Default for SchemaColumn {
/// 列定义的默认值
fn default() -> Self {
SchemaColumn {
name: String::new(),
ty: String::new(),
nullable: true,
default: None,
auto_increment: false,
primary_key: false,
unique: false,
check: None,
}
}
}
/// serde 里缺省为真
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ColumnStatWire {
pub name: String,
pub distinct: u64,
#[serde(default)]
pub capped: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IndexWire {
pub column: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum Request {
AccountsList,
AccountCreate { user: String, password_hash: String },
AccountReset { user: String, password_hash: String },
AccountLogin {
user: String,
#[serde(default)]
at: Option<String>,
},
AccountDrop { user: String },
Ping,
Scan {
table: String,
#[serde(default)]
columns: Option<Vec<String>>,
},
Insert {
table: String,
row: Row,
},
InsertBatch {
table: String,
rows: Vec<Row>,
},
DeleteKeys {
table: String,
keys: Vec<i64>,
},
LookupByIndex {
table: String,
#[serde(default = "default_id_column")]
column: String,
key: serde_json::Value,
},
RangeByIndex {
table: String,
column: String,
#[serde(default)]
lo: Option<serde_json::Value>,
#[serde(default = "default_true")]
lo_inclusive: bool,
#[serde(default)]
hi: Option<serde_json::Value>,
#[serde(default = "default_true")]
hi_inclusive: bool,
},
ReplaceAll {
table: String,
rows: Vec<Row>,
},
ListTables,
DescribeTable {
table: String,
},
CreateTable {
table: String,
columns: Vec<SchemaColumn>,
},
DropTable {
table: String,
},
ListCatalog,
CreateIndex {
table: String,
column: String,
},
DropIndex {
table: String,
column: String,
},
DropColumn {
table: String,
column: String,
},
Compact {
table: String,
},
ReplaceSchema {
table: String,
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
},
}
impl Request {
/// 拒绝可能影响文件路径的列名
pub fn valid_columns(&self) -> bool {
let valid = |name: &str| !name.is_empty()
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
match self {
Self::CreateIndex { column, .. } | Self::DropIndex { column, .. }
| Self::DropColumn { column, .. } | Self::LookupByIndex { column, .. }
| Self::RangeByIndex { column, .. } => valid(column),
Self::CreateTable { columns, .. } => columns.iter().all(|c| valid(&c.name)),
Self::ReplaceSchema { columns, .. } => columns.iter().all(|c| valid(&c.name)),
_ => true,
}
}
/// 普通表操作的统一访问边界
pub fn table(&self) -> Option<&str> {
match self {
Self::Scan { table, .. } | Self::Insert { table, .. }
| Self::InsertBatch { table, .. } | Self::DeleteKeys { table, .. }
| Self::LookupByIndex { table, .. } | Self::ReplaceAll { table, .. }
| Self::RangeByIndex { table, .. }
| Self::DescribeTable { table } | Self::CreateTable { table, .. }
| Self::DropTable { table } | Self::CreateIndex { table, .. }
| Self::DropIndex { table, .. } | Self::DropColumn { table, .. }
| Self::Compact { table }
| Self::ReplaceSchema { table, .. } => Some(table),
Self::Ping | Self::ListTables | Self::ListCatalog | Self::AccountsList
| Self::AccountCreate { .. } | Self::AccountReset { .. } | Self::AccountLogin { .. }
| Self::AccountDrop { .. } => None,
}
}
}
/// 是不是保留系统表名
pub fn reserved_table(table: &str) -> bool {
table.to_ascii_lowercase().starts_with("__chusql_")
}
/// 没写 column 时按 `id` 算
fn default_id_column() -> String {
"id".to_string()
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum Response {
Accounts { accounts: Vec<Account> },
Pong,
Rows { rows: Vec<Row> },
Tables { tables: Vec<String> },
Schema {
table: String,
columns: Vec<SchemaColumn>,
#[serde(default)]
row_count: u64,
#[serde(default)]
indexes: Vec<IndexWire>,
#[serde(default)]
stats: Vec<ColumnStatWire>,
},
NoIndex,
Ok,
Error { message: String },
Catalog { schemas: Vec<TableSchemaWire> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchemaWire {
pub table: String,
pub columns: Vec<SchemaColumn>,
#[serde(default)]
pub row_count: u64,
#[serde(default)]
pub indexes: Vec<IndexWire>,
#[serde(default)]
pub stats: Vec<ColumnStatWire>,
}
File diff suppressed because it is too large. Load diff
+296
View File
@@ -0,0 +1,296 @@
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::protocol::{Account, Row, SchemaColumn, USERS_TABLE};
// 预写日志:先记操作并 fsync,再改数据;重启重放残留。
const OP_INSERT: u8 = 1;
const OP_REPLACE_ALL: u8 = 2;
const OP_INSERT_BATCH: u8 = 3;
const OP_DELETE_KEYS: u8 = 4;
const OP_DROP_COLUMN: u8 = 5;
const OP_ACCOUNTS: u8 = 6;
const OP_REPLACE_SCHEMA: u8 = 7;
const OP_HIDE_COLUMN: u8 = 8;
const OP_COMPACT: u8 = 9;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WalOp {
HideColumn { table: String, column: String },
Compact { table: String, rows: Vec<Row> },
Accounts { accounts: Vec<Account> },
Insert {
table: String,
row: Row,
},
InsertBatch {
table: String,
rows: Vec<Row>,
},
DeleteKeys {
table: String,
keys: Vec<i64>,
},
ReplaceAll {
table: String,
rows: Vec<Row>,
},
DropColumn {
table: String,
column: String,
rows: Vec<Row>,
},
ReplaceSchema {
table: String,
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
},
}
#[derive(Debug, Serialize, Deserialize)]
struct DropColumnPayload {
column: String,
rows: Vec<Row>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ReplaceSchemaPayload {
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
}
pub struct Wal {
path: PathBuf,
handle: Mutex<Option<File>>,
}
impl Wal {
/// 绑定 WAL 文件路径
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Wal {
path: path.as_ref().to_path_buf(),
handle: Mutex::new(None),
}
}
/// 日志文件路径
pub fn path(&self) -> &Path {
&self.path
}
/// 拿常驻句柄(没有就开一个)
fn with_handle<R>(&self, f: impl FnOnce(&mut File) -> io::Result<R>) -> io::Result<R> {
let mut slot = self.handle.lock().unwrap();
if slot.is_none() {
*slot = Some(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&self.path)?,
);
}
f(slot.as_mut().expect("just opened"))
}
/// 截断到空并落盘
pub fn clear(&self) -> io::Result<()> {
self.with_handle(|f| {
truncate(f)?;
f.sync_all()
})
}
/// 只截断,落盘交给紧随的 append
pub fn truncate(&self) -> io::Result<()> {
self.with_handle(truncate)
}
/// 追加一条并 fsync
pub fn append(&self, op: &WalOp) -> io::Result<()> {
let bytes = encode(op)?;
self.with_handle(|f| {
f.write_all(&bytes)?;
f.sync_data()
})
}
/// 读当前 WAL;空则 None
pub fn read(&self) -> io::Result<Option<WalOp>> {
if !self.path.exists() {
return Ok(None);
}
let mut f = File::open(&self.path)?;
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
if buf.is_empty() {
return Ok(None);
}
match decode(&buf) {
Ok(opt) => Ok(opt),
Err(e) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("corrupt WAL: {}", e),
)),
}
}
}
/// 清空文件并把读写位置归零
fn truncate(f: &mut File) -> io::Result<()> {
f.set_len(0)?;
f.seek(SeekFrom::Start(0))?;
Ok(())
}
/// 把操作编成字节
fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
let (op_type, table, payload) = match op {
WalOp::HideColumn { table, column } => (OP_HIDE_COLUMN, table.clone(),
serde_json::to_vec(column).map_err(io::Error::other)?),
WalOp::Compact { table, rows } => {
let p = serde_json::to_vec(rows).map_err(io::Error::other)?;
(OP_COMPACT, table.clone(), p)
}
WalOp::Accounts { accounts } => (OP_ACCOUNTS, USERS_TABLE.to_string(),
serde_json::to_vec(accounts).map_err(io::Error::other)?),
WalOp::Insert { table, row } => {
let p = serde_json::to_vec(row).map_err(io::Error::other)?;
(OP_INSERT, table.clone(), p)
}
WalOp::InsertBatch { table, rows } => {
let p = serde_json::to_vec(rows).map_err(io::Error::other)?;
(OP_INSERT_BATCH, table.clone(), p)
}
WalOp::DeleteKeys { table, keys } => {
let p = serde_json::to_vec(keys).map_err(io::Error::other)?;
(OP_DELETE_KEYS, table.clone(), p)
}
WalOp::ReplaceAll { table, rows } => {
let p = serde_json::to_vec(rows).map_err(io::Error::other)?;
(OP_REPLACE_ALL, table.clone(), p)
}
WalOp::DropColumn { table, column, rows } => {
let p = serde_json::to_vec(&DropColumnPayload {
column: column.clone(),
rows: rows.clone(),
})
.map_err(io::Error::other)?;
(OP_DROP_COLUMN, table.clone(), p)
}
WalOp::ReplaceSchema { table, columns, rows } => {
let p = serde_json::to_vec(&ReplaceSchemaPayload {
columns: columns.clone(),
rows: rows.clone(),
})
.map_err(io::Error::other)?;
(OP_REPLACE_SCHEMA, table.clone(), p)
}
};
let table_bytes = table.as_bytes();
let lsn: u64 = 1;
let mut body = Vec::with_capacity(8 + 1 + 2 + table_bytes.len() + payload.len());
body.extend_from_slice(&lsn.to_le_bytes());
body.push(op_type);
body.extend_from_slice(&(table_bytes.len() as u16).to_le_bytes());
body.extend_from_slice(table_bytes);
body.extend_from_slice(&payload);
let mut out = Vec::with_capacity(4 + body.len());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
Ok(out)
}
/// 从字节解回操作
fn decode(buf: &[u8]) -> Result<Option<WalOp>, String> {
if buf.len() < 4 {
return Ok(None);
}
let body_len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
if buf.len() < 4 + body_len {
return Ok(None);
}
let body = &buf[4..4 + body_len];
if body.len() < 8 + 1 + 2 {
return Err("body too short".into());
}
let _lsn = u64::from_le_bytes([
body[0], body[1], body[2], body[3], body[4], body[5], body[6], body[7],
]);
let op_type = body[8];
let table_len = u16::from_le_bytes([body[9], body[10]]) as usize;
if body.len() < 11 + table_len {
return Err("table name truncated".into());
}
let table = String::from_utf8(body[11..11 + table_len].to_vec())
.map_err(|e| format!("bad utf8 table name: {}", e))?;
let payload = &body[11 + table_len..];
let op = match op_type {
OP_HIDE_COLUMN => {
let column = serde_json::from_slice(payload)
.map_err(|e| format!("bad hide_column payload: {e}"))?;
WalOp::HideColumn { table, column }
}
OP_COMPACT => {
let rows: Vec<Row> = serde_json::from_slice(payload)
.map_err(|e| format!("bad compact payload: {e}"))?;
WalOp::Compact { table, rows }
}
OP_ACCOUNTS => {
if table != USERS_TABLE { return Err("invalid account WAL table".into()); }
let accounts = serde_json::from_slice(payload)
.map_err(|e| format!("bad account payload: {e}"))?;
WalOp::Accounts { accounts }
}
OP_INSERT => {
let row: Row =
serde_json::from_slice(payload).map_err(|e| format!("bad insert payload: {}", e))?;
WalOp::Insert { table, row }
}
OP_INSERT_BATCH => {
let rows: Vec<Row> = serde_json::from_slice(payload)
.map_err(|e| format!("bad insert_batch payload: {}", e))?;
WalOp::InsertBatch { table, rows }
}
OP_DELETE_KEYS => {
let keys: Vec<i64> = serde_json::from_slice(payload)
.map_err(|e| format!("bad delete_keys payload: {}", e))?;
WalOp::DeleteKeys { table, keys }
}
OP_REPLACE_ALL => {
let rows: Vec<Row> = serde_json::from_slice(payload)
.map_err(|e| format!("bad replace_all payload: {}", e))?;
WalOp::ReplaceAll { table, rows }
}
OP_DROP_COLUMN => {
let p: DropColumnPayload = serde_json::from_slice(payload)
.map_err(|e| format!("bad drop_column payload: {}", e))?;
WalOp::DropColumn {
table,
column: p.column,
rows: p.rows,
}
}
OP_REPLACE_SCHEMA => {
let p: ReplaceSchemaPayload = serde_json::from_slice(payload)
.map_err(|e| format!("bad replace_schema payload: {}", e))?;
WalOp::ReplaceSchema {
table,
columns: p.columns,
rows: p.rows,
}
}
other => return Err(format!("unknown op type: {}", other)),
};
Ok(Some(op))
}
+257
View File
@@ -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));
}
+118
View File
@@ -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(), "第二次没有可改的");
}
+239
View File
@@ -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") };
}
+306
View File
@@ -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"
);
}
+144
View File
@@ -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
+101
View File
@@ -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());
}