做完rust全链路

This commit is contained in:
LIm1tZz7 committed 2026-09-24 21:28:49 +08:00
1 parent e428265357
commit 2a4b446252
34 files changed
+1756 -1031

No files matched your search

+407 -303
View File
@@ -1,17 +1,15 @@
//! 命名管道服务:按行读写 JSON。Windows 上管道名映射为 \\.\pipe\<name>。
//! 启动时读配置(环境变量 → TOML → 默认值)并把每项来源打进日志。
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::thread;
use chusql_storage::catalog::Catalog;
use chusql_storage::config::{self, Config, Loaded};
use chusql_storage::heap::HeapTable;
use chusql_storage::log;
use chusql_storage::protocol::{Request, Response};
use chusql_storage::protocol::{Request, Response, SchemaColumn};
use chusql_storage::wal::{Wal, WalOp};
use chusql_storage::{log_debug, log_error, log_info, log_warn};
use interprocess::local_socket::{
@@ -21,308 +19,413 @@ use interprocess::local_socket::{
};
use interprocess::TryClone;
// * 路径
// 命名管道服务:按行读写 JSON,Windows 上映射成 \\.\pipe\<name>。
// 启动时读配置并把每项来源打进日志;表句柄按名字缓存复用。
/// 表名 -> 数据文件路径。
fn table_path(cfg: &Config, table: &str) -> PathBuf {
cfg.data_dir.join(format!("{}.db", table))
// 服务器
/// 服务器状态:配置 + 表句柄缓存 + 写锁
struct Server {
cfg: Config,
tables: Mutex<HashMap<String, Arc<Mutex<HeapTable>>>>,
write_lock: Mutex<()>,
}
/// 表名 -> 索引文件路径。
fn index_path(cfg: &Config, table: &str) -> PathBuf {
cfg.data_dir.join(format!("{}.idx", table))
}
impl Server {
/// 新建服务器状态
fn new(cfg: Config) -> std::io::Result<Self> {
std::fs::create_dir_all(&cfg.data_dir)?;
Ok(Server {
cfg,
tables: Mutex::new(HashMap::new()),
write_lock: Mutex::new(()),
})
}
/// 数据字典文件路径。
fn catalog_path(cfg: &Config) -> PathBuf {
cfg.data_dir.join("catalog.json")
}
/// WAL 文件路径。
fn wal_path(cfg: &Config) -> PathBuf {
cfg.data_dir.join("wal.log")
}
// 路径
/// 表名 -> 数据文件
fn table_path(&self, table: &str) -> PathBuf {
self.cfg.data_dir.join(format!("{}.db", table))
}
// * 表操作
/// 表名 -> 索引文件
fn index_path(&self, table: &str) -> PathBuf {
self.cfg.data_dir.join(format!("{}.idx", table))
}
/// 打开一张表,执行闭包;表不存在就创建。
fn with_table<R>(
cfg: &Config,
table: &str,
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
) -> std::io::Result<R> {
std::fs::create_dir_all(&cfg.data_dir)?;
let mut t = HeapTable::open(table_path(cfg, table), cfg.page_size)?;
f(&mut t)
}
/// 数据字典文件
fn catalog_path(&self) -> PathBuf {
self.cfg.data_dir.join("catalog.json")
}
/// 打开带索引的表,执行闭包。
fn with_indexed_table<R>(
cfg: &Config,
table: &str,
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
) -> std::io::Result<R> {
std::fs::create_dir_all(&cfg.data_dir)?;
let mut t = HeapTable::open_indexed(
table_path(cfg, table),
index_path(cfg, table),
cfg.page_size,
cfg.btree_order,
)?;
f(&mut t)
}
/// WAL 文件
fn wal_path(&self) -> PathBuf {
self.cfg.data_dir.join("wal.log")
}
/// 表文件是否存在且非空。
fn table_exists(cfg: &Config, table: &str) -> bool {
let p = table_path(cfg, table);
std::fs::metadata(&p).map(|m| m.len() > 0).unwrap_or(false)
}
/// 打开已存在的表;不存在返回 NotFound。
fn with_existing_table<R>(
cfg: &Config,
table: &str,
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
) -> std::io::Result<R> {
if !table_exists(cfg, table) {
return Err(std::io::Error::new(
// 表句柄
/// 表文件存在且非空
fn table_exists(&self, table: &str) -> bool {
if self.tables.lock().unwrap().contains_key(table) {
return true;
}
if let Ok(c) = Catalog::load(self.catalog_path()) {
if c.describe(table).is_some() {
return true;
}
}
std::fs::metadata(self.table_path(table))
.map(|m| m.len() > 0)
.unwrap_or(false)
}
/// 拿表句柄(缓存复用)
fn table_handle(&self, table: &str) -> std::io::Result<Arc<Mutex<HeapTable>>> {
let mut map = self.tables.lock().unwrap();
if let Some(h) = map.get(table) {
return Ok(h.clone());
}
std::fs::create_dir_all(&self.cfg.data_dir)?;
let t = HeapTable::open_indexed(
self.table_path(table),
self.index_path(table),
self.cfg.page_size,
self.cfg.btree_order,
self.cfg.pool_size,
)?;
let h = Arc::new(Mutex::new(t));
map.insert(table.to_string(), h.clone());
Ok(h)
}
/// 在表上跑闭包
fn with_table<R>(
&self,
table: &str,
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
) -> std::io::Result<R> {
let h = self.table_handle(table)?;
let mut guard = h.lock().unwrap();
f(&mut guard)
}
/// 同上,但一定带索引
fn with_indexed_table<R>(
&self,
table: &str,
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
) -> std::io::Result<R> {
self.with_table(table, f)
}
/// 表不存在就报错
fn with_existing_table<R>(
&self,
table: &str,
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
) -> std::io::Result<R> {
if !self.table_exists(table) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("unknown table: {}", table),
));
}
self.with_table(table, f)
}
// 写入路径
/// 把一条 WAL 操作落到磁盘
fn apply_op(&self, op: &WalOp) -> std::io::Result<()> {
match op {
WalOp::Insert { table, row, key } => {
match key {
None => self.with_table(table, |t| t.insert(row))?,
Some(k) => self.with_indexed_table(table, |t| t.insert_keyed(row, *k))?,
}
let mut c = Catalog::load(self.catalog_path())?;
let row: chusql_storage::protocol::Row = row.clone().into_iter().collect();
c.record_insert(table, &row);
c.save(self.catalog_path())?;
Ok(())
}
WalOp::ReplaceAll { table, rows } => {
self.with_indexed_table(table, |t| t.replace_all(rows))?;
let mut c = Catalog::load(self.catalog_path())?;
let rows: Vec<chusql_storage::protocol::Row> =
rows.iter().map(|r| r.clone().into_iter().collect()).collect();
c.record_replace_all(table, &rows);
c.save(self.catalog_path())?;
Ok(())
}
}
}
/// 先写 WAL 再改数据
fn write_via_wal(&self, op: &WalOp) -> std::io::Result<()> {
let _guard = self.write_lock.lock().unwrap();
std::fs::create_dir_all(&self.cfg.data_dir)?;
let wal = Wal::new(self.wal_path());
wal.clear()?;
wal.append(op)?;
let result = self.apply_op(op);
if result.is_ok() {
if let Err(e) = self.flush_op_tables(op) {
return Err(e);
}
}
wal.clear()?;
result
}
/// 把涉及的表写回磁盘
fn flush_op_tables(&self, op: &WalOp) -> std::io::Result<()> {
match op {
WalOp::Insert { table, key, .. } => {
if key.is_some() {
self.with_indexed_table(table, |t| t.flush())
} else {
self.with_table(table, |t| t.flush())
}
}
WalOp::ReplaceAll { table, .. } => {
self.with_indexed_table(table, |t| t.flush())
}
}
}
/// 启动时重放残留 WAL
fn recover_wal(&self) -> std::io::Result<()> {
std::fs::create_dir_all(&self.cfg.data_dir)?;
let wal = Wal::new(self.wal_path());
match wal.read()? {
None => Ok(()),
Some(op) => {
log_warn!(core, "replaying incomplete WAL entry");
self.apply_op(&op)?;
wal.clear()?;
log_info!(core, "WAL replay complete");
Ok(())
}
}
}
// 请求分发
/// 一条请求翻成一条响应
fn handle_request(&self, req: Request) -> Response {
match req {
Request::Ping => {
log_debug!(pipe, "ping");
Response::Pong
}
Request::Scan { table } => {
log_debug!(pipe, "scan table={}", table);
let result = self.with_existing_table(&table, |t| t.scan());
match result {
Ok(rows) => Response::Rows { rows },
Err(e) => {
log_warn!(pipe, "scan {}: {}", table, e);
Response::Error {
message: format!("scan {}: {}", table, e),
}
}
}
}
Request::Insert { table, row, key } => {
log_debug!(pipe, "insert table={} key={:?}", table, key);
let tname = table.clone();
let op = WalOp::Insert { table, row, key };
match self.write_via_wal(&op) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "insert {}: {}", tname, e);
Response::Error {
message: format!("insert {}: {}", tname, e),
}
}
}
}
Request::LookupByIndex { table, key } => {
log_debug!(pipe, "lookup_by_index table={} key={}", table, key);
match self.with_indexed_table(&table, |t| t.get_by_key(key)) {
Ok(Some(row)) => Response::Rows { rows: vec![row] },
Ok(None) => Response::Rows { rows: vec![] },
Err(e) => {
log_warn!(pipe, "lookup_by_index {}: {}", table, e);
Response::Error {
message: format!("lookup_by_index {}: {}", table, e),
}
}
}
}
Request::ReplaceAll { table, rows } => {
log_debug!(pipe, "replace_all table={} rows={}", table, rows.len());
let tname = table.clone();
let op = WalOp::ReplaceAll { table, rows };
match self.write_via_wal(&op) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "replace_all {}: {}", tname, e);
Response::Error {
message: format!("replace_all {}: {}", tname, e),
}
}
}
}
Request::ListTables => {
log_debug!(pipe, "list_tables");
match self.list_tables() {
Ok(tables) => Response::Tables { tables },
Err(e) => {
log_warn!(pipe, "list_tables: {}", e);
Response::Error {
message: format!("list_tables: {}", e),
}
}
}
}
Request::DescribeTable { table } => {
log_debug!(pipe, "describe_table table={}", table);
match self.describe_table(&table) {
Ok((cols, count)) => Response::Schema {
columns: cols,
row_count: count,
},
Err(e) => {
log_warn!(pipe, "describe_table {}: {}", table, e);
Response::Error {
message: format!("describe_table {}: {}", table, e),
}
}
}
}
Request::CreateTable { table, columns } => {
log_debug!(pipe, "create_table table={} cols={}", table, columns.len());
let mut c = match Catalog::load(self.catalog_path()) {
Ok(c) => c,
Err(e) => {
return Response::Error {
message: format!("create_table {}: catalog: {}", table, e),
}
}
};
if c.describe(&table).is_some() {
return Response::Error {
message: format!("create_table {}: table already exists", table),
};
}
if let Err(e) = self.with_table(&table, |_t| Ok(())) {
return Response::Error {
message: format!("create_table {}: {}", table, e),
};
}
if let Err(e) = c.create_table(&table, columns) {
return Response::Error {
message: format!("create_table {}: {}", table, e),
};
}
if let Err(e) = c.save(self.catalog_path()) {
return Response::Error {
message: format!("create_table {}: catalog save: {}", table, e),
};
}
Response::Ok
}
Request::DropTable { table } => {
log_debug!(pipe, "drop_table table={}", table);
let mut c = match Catalog::load(self.catalog_path()) {
Ok(c) => c,
Err(e) => return Response::Error {
message: format!("drop_table {}: catalog: {}", table, e),
},
};
if let Err(e) = c.drop_table(&table) {
return Response::Error {
message: format!("drop_table {}: {}", table, e),
};
}
if let Err(e) = c.save(self.catalog_path()) {
return Response::Error {
message: format!("drop_table {}: catalog save: {}", table, e),
};
}
self.tables.lock().unwrap().remove(&table);
let _ = std::fs::remove_file(self.table_path(&table));
let _ = std::fs::remove_file(self.index_path(&table));
Response::Ok
}
}
}
// 信息查询
/// 表名:catalog 与目录取并集
fn list_tables(&self) -> std::io::Result<Vec<String>> {
let mut names = BTreeSet::new();
let c = Catalog::load(self.catalog_path())?;
for t in c.table_names() {
names.insert(t);
}
let dir = &self.cfg.data_dir;
if dir.exists() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if let Some(stem) = name.strip_suffix(".db") {
names.insert(stem.to_string());
}
}
}
Ok(names.into_iter().collect())
}
/// 查一张表的 schema
fn describe_table(&self, table: &str) -> std::io::Result<(Vec<SchemaColumn>, u64)> {
let c = Catalog::load(self.catalog_path())?;
if let Some(schema) = c.describe(table) {
return Ok((schema.columns.clone(), schema.row_count));
}
if self.tables.lock().unwrap().contains_key(table)
|| std::fs::metadata(self.table_path(table)).is_ok()
{
return Ok((Vec::new(), 0));
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("unknown table: {}", table),
));
}
let mut t = HeapTable::open(table_path(cfg, table), cfg.page_size)?;
f(&mut t)
}
// * 数据字典操作
/// 读 catalog,按一行数据补列,再写回。
fn catalog_add_row(
cfg: &Config,
table: &str,
row: &serde_json::Map<String, serde_json::Value>,
) -> std::io::Result<()> {
let mut c = Catalog::load(catalog_path(cfg))?;
let row: chusql_storage::protocol::Row = row.clone().into_iter().collect();
c.ensure_columns(table, &row);
c.save(catalog_path(cfg))
}
/// 读 catalog,按多行数据补列,再写回。
fn catalog_add_rows(
cfg: &Config,
table: &str,
rows: &[serde_json::Map<String, serde_json::Value>],
) -> std::io::Result<()> {
let mut c = Catalog::load(catalog_path(cfg))?;
for r in rows {
let row: chusql_storage::protocol::Row = r.clone().into_iter().collect();
c.ensure_columns(table, &row);
}
c.save(catalog_path(cfg))
}
// * WAL 写路径与恢复
/// 把一条 WAL 操作真正落到磁盘(堆表 + 索引 + catalog)。
fn apply_op(cfg: &Config, op: &WalOp) -> std::io::Result<()> {
match op {
WalOp::Insert { table, row, key } => {
match key {
None => with_table(cfg, table, |t| t.insert(row))?,
Some(k) => with_indexed_table(cfg, table, |t| t.insert_keyed(row, *k))?,
}
let map: serde_json::Map<_, _> = row.clone().into_iter().collect();
catalog_add_row(cfg, table, &map)?;
Ok(())
}
WalOp::ReplaceAll { table, rows } => {
with_indexed_table(cfg, table, |t| t.replace_all(rows))?;
let maps: Vec<_> = rows.iter().map(|r| r.clone().into_iter().collect()).collect();
catalog_add_rows(cfg, table, &maps)?;
Ok(())
}
))
}
}
/// 写前日志 + 写数据 + 清空 WAL。
///
/// 顺序:clear(清掉上次已完成的) → append+fsync → apply → clear。
/// 若中途进程被杀,重启时 WAL 里能读到那条操作并重放。
fn write_via_wal(cfg: &Config, op: &WalOp) -> std::io::Result<()> {
let wal = Wal::new(wal_path(cfg));
// 清掉上一次遗留(正常情况下已经是空的)
wal.clear()?;
// 先写 WAL 并 fsync
wal.append(op)?;
// 再写数据
let result = apply_op(cfg, op);
// 无论 apply 成功还是返回 Err,都清空 WAL:
// 失败的操作不应被重放;若中途被杀,这里跑不到,WAL 会留到重启时重放。
wal.clear()?;
result
}
/// 启动时重放 WAL 里的残留操作。
fn recover_wal(cfg: &Config) -> std::io::Result<()> {
let wal = Wal::new(wal_path(cfg));
match wal.read()? {
None => Ok(()),
Some(op) => {
log_warn!(core, "replaying incomplete WAL entry");
apply_op(cfg, &op)?;
wal.clear()?;
log_info!(core, "WAL replay complete");
Ok(())
}
}
}
// * 请求分发
/// 把一条请求翻译成一条响应。
fn handle_request(cfg: &Config, req: Request) -> Response {
match req {
Request::Ping => {
log_debug!(pipe, "ping");
Response::Pong
}
Request::Scan { table } => {
log_debug!(pipe, "scan table={}", table);
match with_existing_table(cfg, &table, |t| t.scan()) {
Ok(rows) => Response::Rows { rows },
Err(e) => {
log_warn!(pipe, "scan {}: {}", table, e);
Response::Error {
message: format!("scan {}: {}", table, e),
}
}
}
}
Request::Insert { table, row, key } => {
log_debug!(pipe, "insert table={} key={:?}", table, key);
let tname = table.clone();
let op = WalOp::Insert { table, row, key };
match write_via_wal(cfg, &op) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "insert {}: {}", tname, e);
Response::Error {
message: format!("insert {}: {}", tname, e),
}
}
}
}
Request::LookupByIndex { table, key } => {
log_debug!(pipe, "lookup_by_index table={} key={}", table, key);
match with_indexed_table(cfg, &table, |t| t.get_by_key(key)) {
Ok(Some(row)) => Response::Rows { rows: vec![row] },
Ok(None) => Response::Rows { rows: vec![] },
Err(e) => {
log_warn!(pipe, "lookup_by_index {}: {}", table, e);
Response::Error {
message: format!("lookup_by_index {}: {}", table, e),
}
}
}
}
Request::ReplaceAll { table, rows } => {
log_debug!(pipe, "replace_all table={} rows={}", table, rows.len());
let tname = table.clone();
let op = WalOp::ReplaceAll { table, rows };
match write_via_wal(cfg, &op) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "replace_all {}: {}", tname, e);
Response::Error {
message: format!("replace_all {}: {}", tname, e),
}
}
}
}
Request::ListTables => {
log_debug!(pipe, "list_tables");
match list_tables(cfg) {
Ok(tables) => Response::Tables { tables },
Err(e) => {
log_warn!(pipe, "list_tables: {}", e);
Response::Error {
message: format!("list_tables: {}", e),
}
}
}
}
Request::DescribeTable { table } => {
log_debug!(pipe, "describe_table table={}", table);
match describe_table(cfg, &table) {
Ok(cols) => Response::Schema { columns: cols },
Err(e) => {
log_warn!(pipe, "describe_table {}: {}", table, e);
Response::Error {
message: format!("describe_table {}: {}", table, e),
}
}
}
}
}
}
/// 列出表名:catalog 记录 + data 目录下的 .db 文件,取并集。
fn list_tables(cfg: &Config) -> std::io::Result<Vec<String>> {
let mut names = BTreeSet::new();
let c = Catalog::load(catalog_path(cfg))?;
for t in c.table_names() {
names.insert(t);
}
let dir = &cfg.data_dir;
if dir.exists() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if let Some(stem) = name.strip_suffix(".db") {
names.insert(stem.to_string());
}
}
}
Ok(names.into_iter().collect())
}
/// 查一张表的 schema。表在 catalog 里没有,但文件存在,返回空列。
fn describe_table(
cfg: &Config,
table: &str,
) -> std::io::Result<Vec<chusql_storage::protocol::SchemaColumn>> {
let c = Catalog::load(catalog_path(cfg))?;
if let Some(schema) = c.describe(table) {
return Ok(schema.columns.clone());
}
if table_exists(cfg, table) {
return Ok(Vec::new());
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("unknown table: {}", table),
))
}
// * 连接处理
/// 处理一条连接:逐行读 JSON 请求,逐行回 JSON 响应。
fn handle(cfg: &Config, conn: LocalSocketStream) -> std::io::Result<()> {
// 连接
/// 处理一条连接
fn handle(server: &Server, conn: LocalSocketStream) -> std::io::Result<()> {
let mut writer = conn.try_clone()?;
let reader = BufReader::new(conn);
@@ -333,7 +436,7 @@ fn handle(cfg: &Config, conn: LocalSocketStream) -> std::io::Result<()> {
}
let resp = match serde_json::from_str::<Request>(&line) {
Ok(req) => handle_request(cfg, req),
Ok(req) => server.handle_request(req),
Err(e) => {
log_warn!(pipe, "bad request line: {}", e);
Response::Error {
@@ -349,19 +452,20 @@ fn handle(cfg: &Config, conn: LocalSocketStream) -> std::io::Result<()> {
Ok(())
}
// * 主入口
/// 把"配置从哪来"打进日志。
// 入口
/// 把配置来源打进日志
fn log_config(loaded: &Loaded) {
match &loaded.config_path {
Some(p) => log_info!(core, "config file: {}", p.display()),
None => log_info!(core, "config file: none"),
}
for (name, value, origin) in &loaded.origins {
log_info!(core, " {:<17} {} [{}]", name, value, origin.describe());
log_info!(core, " {:<18} {} [{}]", name, value, origin.describe());
}
}
/// 进程入口
fn main() -> std::io::Result<()> {
let loaded = match config::load() {
Ok(l) => l,
@@ -376,15 +480,15 @@ fn main() -> std::io::Result<()> {
log_info!(core, "chusql-storage {} starting", env!("CARGO_PKG_VERSION"));
log_config(&loaded);
let cfg = Arc::new(loaded.config);
let server = Arc::new(Server::new(loaded.config)?);
// 启动时先重放 WAL,再做其他事
if let Err(e) = recover_wal(&cfg) {
if let Err(e) = server.recover_wal() {
log_error!(core, "WAL recovery failed: {}", e);
return Err(e);
}
let ns_name = cfg
let ns_name = server
.cfg
.pipe_name
.as_str()
.to_ns_name::<GenericNamespaced>()
@@ -394,16 +498,16 @@ fn main() -> std::io::Result<()> {
log_info!(
core,
"listening on pipe {} (\\\\.\\pipe\\{})",
cfg.pipe_name,
cfg.pipe_name
server.cfg.pipe_name,
server.cfg.pipe_name
);
for conn in listener.incoming() {
let conn = conn?;
let cfg = Arc::clone(&cfg);
let server = Arc::clone(&server);
thread::spawn(move || {
log_debug!(pipe, "connection accepted");
if let Err(e) = handle(&cfg, conn) {
if let Err(e) = handle(&server, conn) {
log_error!(pipe, "connection error: {}", e);
}
});
+57 -49
View File
@@ -3,100 +3,98 @@ use std::path::Path;
use crate::page::{Page, PageFile, PageId};
// 磁盘版 B+ 树:每个节点占一页,page 0 存文件头(魔数 + 页大小 + order + 根页号)。
// 节点页布局(order = 一个节点最多几个孩子,来自配置):
// [0] 节点类型:0=叶子,1=内部
// [1..3] 键数量 u16
// [3..11] 叶子的 next 页号 u64(内部保留)
// [12..12+K*8] 键:K = order-1 个 i64
// [后面] 叶子:K 个 u64 值;内部:order 个 u64 孩子页号
// 磁盘 B+ 树:节点占一页,文件头记页大小与 order。
/// 文件头放在这一页。
// 布局常量
/// 文件头放在第 0 页
const META_PAGE: PageId = 0;
/// 文件头魔数。
/// 魔数,用来认出索引文件
const META_MAGIC: &[u8; 4] = b"CBTR";
/// 文件头里各字段的偏移。
/// 文件头各字段的偏移
const OFF_META_PAGE_SIZE: usize = 4;
const OFF_META_ORDER: usize = 8;
const OFF_META_ROOT: usize = 10;
/// 节点类型标记。
/// 节点类型标记
const NODE_LEAF: u8 = 0;
const NODE_INTERNAL: u8 = 1;
/// 布局偏移。
/// 节点内布局的偏移
const OFF_TYPE: usize = 0;
const OFF_COUNT: usize = 1;
const OFF_NEXT: usize = 3;
const OFF_KEYS: usize = 12;
/// 一个 order 的节点占多少字节(内部节点最坏情况:order-1 个键 + order 个孩子)。
// 容量计算
/// 一个节点占多少字节
fn node_bytes(order: usize) -> usize {
OFF_KEYS + max_keys(order) * 8 + order * 8
}
/// 一个节点最多几个键。
/// 最多几个键
fn max_keys(order: usize) -> usize {
order - 1
}
/// 键后面那片区域的起点。
/// 值区域的起点
fn off_payload(order: usize) -> usize {
OFF_KEYS + max_keys(order) * 8
}
/// 这个 order 放得进 size 字节的页吗。
/// 这个 order 放得进一页吗
pub fn fits_in_page(page_size: usize, order: usize) -> bool {
order >= 2 && node_bytes(order) <= page_size
}
/// 给定页大小,order 最大能取多少(解 12 + (order-1)*8 + order*8 <= page_size)。
/// 一页最多几个孩子
pub fn max_order(page_size: usize) -> usize {
page_size.saturating_sub(4) / 16
}
/// 读一个 u8。
// 小端读写
/// 读一个 u8
fn read_u8(p: &Page, off: usize) -> u8 {
p.data[off]
}
/// 写一个 u8。
/// 写一个 u8
fn write_u8(p: &mut Page, off: usize, v: u8) {
p.data[off] = v;
}
/// 读一个小端 u16。
/// 读一个小端 u16
fn read_u16(p: &Page, off: usize) -> u16 {
u16::from_le_bytes([p.data[off], p.data[off + 1]])
}
/// 写一个小端 u16。
/// 写一个小端 u16
fn write_u16(p: &mut Page, off: usize, v: u16) {
p.data[off..off + 2].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u32。
/// 读一个小端 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。
/// 写一个小端 u32
fn write_u32(p: &mut Page, off: usize, v: u32) {
p.data[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u64。
/// 读一个小端 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。
/// 写一个小端 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);
@@ -105,7 +103,7 @@ fn write_meta(page: &mut Page, page_size: usize, order: usize, root: PageId) {
write_u64(page, OFF_META_ROOT, root);
}
/// 检查文件头和当前配置是否一致(页大小 / order 改了就会对不上)。
/// 校验文件头与配置
fn check_meta(page: &Page, page_size: usize, order: usize) -> io::Result<()> {
if page.data[0..4] != *META_MAGIC {
return Err(io::Error::new(
@@ -127,7 +125,8 @@ fn check_meta(page: &Page, page_size: usize, order: usize) -> io::Result<()> {
Ok(())
}
/// 一个节点在内存里的样子。
// 节点编解码
/// 节点在内存里的样子
enum NodeData {
Leaf {
keys: Vec<i64>,
@@ -140,7 +139,7 @@ enum NodeData {
},
}
/// 从一页里读出节点。
/// 从一页读出节点
fn read_node(p: &Page, order: usize) -> NodeData {
let ty = read_u8(p, OFF_TYPE);
let n = read_u16(p, OFF_COUNT) as usize;
@@ -167,7 +166,7 @@ fn read_node(p: &Page, order: usize) -> NodeData {
}
}
/// 把节点写回一页(先清零,避免残留)。
/// 把节点写回一页
fn write_node(p: &mut Page, node: &NodeData, order: usize) {
p.zero();
let payload = off_payload(order);
@@ -196,7 +195,7 @@ fn write_node(p: &mut Page, node: &NodeData, order: usize) {
}
}
/// 二分找 key 应该下探到的孩子下标。
/// 二分选孩子
fn child_index(keys: &[i64], key: i64) -> usize {
let mut lo = 0;
let mut hi = keys.len();
@@ -211,15 +210,21 @@ fn child_index(keys: &[i64], key: i64) -> usize {
lo
}
/// 磁盘版 B+ 树。
// B+ 树
/// 磁盘 B+ 树
pub struct DiskBTree {
file: PageFile,
order: usize,
}
impl DiskBTree {
/// 打开或新建索引文件;页大小 / order 和文件头里记的不一致就报错。
pub fn open<P: AsRef<Path>>(path: P, page_size: usize, order: usize) -> io::Result<Self> {
/// 打开或新建索引文件
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,
@@ -231,7 +236,7 @@ impl DiskBTree {
),
));
}
let mut file = PageFile::open(path, 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);
@@ -243,26 +248,26 @@ impl DiskBTree {
Ok(DiskBTree { file, order })
}
/// 读根页号;0 表示空树。
/// 读根页号
fn root(&mut self) -> io::Result<Option<PageId>> {
let meta = self.file.read_page(META_PAGE)?;
let r = read_u64(&meta, OFF_META_ROOT);
Ok(if r == 0 { None } else { Some(r) })
}
/// 写根页号。
/// 写根页号
fn set_root(&mut self, r: Option<PageId>) -> io::Result<()> {
let mut meta = self.file.read_page(META_PAGE)?;
write_u64(&mut meta, OFF_META_ROOT, r.unwrap_or(0));
self.file.write_page(&meta)
}
/// 分配一个新页。
/// 分配新页
fn alloc(&mut self) -> io::Result<PageId> {
Ok(self.file.append_page()?.id)
}
/// 清空整棵树:截断文件后重写文件头(根页号归零)。
/// 清空整棵树
pub fn clear(&mut self) -> io::Result<()> {
let page_size = self.file.page_size();
self.file.truncate()?;
@@ -272,12 +277,12 @@ impl DiskBTree {
Ok(())
}
/// 空页,用来拼一个新节点。
/// 空页,用来拼节点
fn new_page(&self, id: PageId) -> Page {
Page::new(id, self.file.page_size())
}
/// 按 key 找 value。
/// 按 key 找 value
pub fn get(&mut self, key: i64) -> io::Result<Option<u64>> {
match self.root()? {
None => Ok(None),
@@ -285,7 +290,7 @@ impl DiskBTree {
}
}
/// 递归查找。
/// 递归查找
fn get_rec(&mut self, page_id: PageId, key: i64) -> io::Result<Option<u64>> {
let p = self.file.read_page(page_id)?;
match read_node(&p, self.order) {
@@ -299,7 +304,7 @@ impl DiskBTree {
}
}
/// 插入 / 覆盖一个键值对。
/// 插入或覆盖
pub fn insert(&mut self, key: i64, value: u64) -> io::Result<()> {
match self.root()? {
None => {
@@ -317,7 +322,6 @@ impl DiskBTree {
Some(r) => {
let (_, split) = self.insert_rec(r, key, value)?;
if let Some((sep, right_id)) = split {
// 根分裂:长高一层。
let root_id = self.alloc()?;
let node = NodeData::Internal {
keys: vec![sep],
@@ -334,7 +338,7 @@ impl DiskBTree {
}
}
/// 递归插入;返回可能上提的 (分隔键, 新右页号)。
/// 递归插入,可能上提
fn insert_rec(
&mut self,
page_id: PageId,
@@ -358,7 +362,6 @@ impl DiskBTree {
self.file.write_page(&p2)?;
Ok((page_id, None))
} else {
// 叶子分裂:右叶最小键复制上提。
let mid = keys.len() / 2;
let rkeys = keys.split_off(mid);
let rvalues = values.split_off(mid);
@@ -409,7 +412,6 @@ impl DiskBTree {
self.file.write_page(&p2)?;
Ok((page_id, None))
} else {
// 内部节点分裂:中间键上提,不再保留原件。
let mid = keys.len() / 2;
let up = keys[mid];
let rkeys = keys.split_off(mid + 1);
@@ -441,7 +443,7 @@ impl DiskBTree {
}
}
/// 按 key 升序返回所有键值对。
/// 按键升序返回全部
pub fn iter_all(&mut self) -> io::Result<Vec<(i64, u64)>> {
let mut out = Vec::new();
if let Some(r) = self.root()? {
@@ -450,7 +452,7 @@ impl DiskBTree {
Ok(out)
}
/// 中序遍历一棵子树。
/// 中序遍历
fn walk(&mut self, page_id: PageId, out: &mut Vec<(i64, u64)>) -> io::Result<()> {
let p = self.file.read_page(page_id)?;
match read_node(&p, self.order) {
@@ -467,4 +469,10 @@ impl DiskBTree {
}
Ok(())
}
/// 把脏页写回
pub fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
}
+77 -11
View File
@@ -1,5 +1,3 @@
//! 数据字典:记录每张表有哪些列、什么类型。
//! 存在 data/catalog.json。insert 时自动补列,describe 时读回。
use std::collections::BTreeMap;
use std::io;
@@ -9,20 +7,27 @@ use serde::{Deserialize, Serialize};
use crate::protocol::{ColumnType, Row, SchemaColumn};
/// 一张表的 schema。
// 数据字典:记每张表有哪些列、什么类型、多少行。
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
/// 一张表的 schema
pub struct TableSchema {
pub columns: Vec<SchemaColumn>,
#[serde(default)]
pub row_count: u64,
}
/// 整个数据字典。
#[derive(Debug, Default, Serialize, Deserialize)]
/// 数据字典:表名到 schema
pub struct Catalog {
tables: BTreeMap<String, TableSchema>,
}
impl Catalog {
/// 从文件读;文件不存在返回空字典。
// 读取与保存
/// 从文件读;不存在给空字典
pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
if !path.as_ref().exists() {
return Ok(Catalog::default());
@@ -34,23 +39,25 @@ impl Catalog {
serde_json::from_slice(&bytes).map_err(io::Error::other)
}
/// 写回文件。
/// 写回文件
pub fn save<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let bytes = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
std::fs::write(path, bytes)
}
/// 一张表的 schema;没有就返回 None。
// 查询
/// 一张表的 schema
pub fn describe(&self, table: &str) -> Option<&TableSchema> {
self.tables.get(table)
}
/// catalog 里记录的所有表名。
/// 记录过的所有表名
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();
for (k, v) in row {
@@ -66,15 +73,74 @@ impl Catalog {
}
}
/// 按多行数据补列;replace_all 用。
/// 建表;已存在报错
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 {
columns,
row_count: 0,
},
);
Ok(())
}
/// 按多行数据补列
pub fn ensure_columns_from_rows(&mut self, table: &str, rows: &[Row]) {
for r in rows {
self.ensure_columns(table, r);
}
}
/// 记一次插入:补列 + 行数 +1
pub fn record_insert(&mut self, table: &str, row: &Row) {
let entry = self.tables.entry(table.to_string()).or_default();
for (k, v) in row {
if entry.columns.iter().any(|c| &c.name == k) {
continue;
}
if let Some(ty) = infer_type(v) {
entry.columns.push(SchemaColumn { name: k.clone(), ty });
}
}
entry.row_count += 1;
}
/// 记一次整表替换:列照补,行数重置
pub fn record_replace_all(&mut self, table: &str, rows: &[Row]) {
let entry = self.tables.entry(table.to_string()).or_default();
for r in rows {
for (k, v) in r {
if entry.columns.iter().any(|c| &c.name == k) {
continue;
}
if let Some(ty) = infer_type(v) {
entry.columns.push(SchemaColumn { name: k.clone(), ty });
}
}
}
entry.row_count = rows.len() as u64;
}
/// 删表
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),
));
}
Ok(())
}
}
/// 从 JSON 值推断列类型;不支持的类型返回 None。
// 推断
/// 由 JSON 值猜列类型
fn infer_type(v: &serde_json::Value) -> Option<ColumnType> {
match v {
serde_json::Value::Number(_) => Some(ColumnType::Int),
+58 -21
View File
@@ -4,45 +4,50 @@ use serde::Deserialize;
use crate::log::Level;
// 配置:优先环境变量,其次 TOML 文件,最后内置默认值(下面写的就是默认值)。
// 每个选项的来源都记在 Loaded::origins 里,server 启动时打进日志——读了哪个文件、
// 哪些选项走了默认回退,一眼能看出来。
// 配置:环境变量优先于 TOML,最后回退内置默认值。
/// 默认配置文件名(相对当前工作目录);可用 CHUSQL_CONFIG 指到别处。
// 默认值
/// 默认配置文件路径
pub const DEFAULT_CONFIG_PATH: &str = "chusql-storage.toml";
/// 默认页大小(字节)。
/// 默认池容量(页数)
pub const DEFAULT_POOL_SIZE: usize = 64;
/// 默认页大小
pub const DEFAULT_PAGE_SIZE: usize = 4096;
/// 默认 B+ 树节点最大孩子数。
/// 默认 B+ 树 order
pub const DEFAULT_BTREE_ORDER: usize = 4;
/// 默认数据目录。
/// 默认数据目录
pub const DEFAULT_DATA_DIR: &str = "data";
/// 默认管道名。
/// 默认管道名
pub const DEFAULT_PIPE_NAME: &str = "chusql-storage";
/// 默认日志等级。
/// 默认日志等级
pub const DEFAULT_LOG_LEVEL: &str = "info";
/// 页大小允许的范围。
/// 页大小允许范围
const MIN_PAGE_SIZE: usize = 512;
const MAX_PAGE_SIZE: usize = 65536;
/// B+ 树节点至少要放得下 2 个键,分裂规则才成立。
/// order 最小值
const MIN_BTREE_ORDER: usize = 3;
/// 生效的配置。
#[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 pipe_name: String,
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: PathBuf::from(DEFAULT_DATA_DIR),
pipe_name: DEFAULT_PIPE_NAME.to_string(),
log_level: Level::Info,
@@ -50,8 +55,8 @@ impl Default for Config {
}
}
/// 一个选项的来源。
#[derive(Debug, Clone, PartialEq, Eq)]
/// 一个选项的来源
pub enum Origin {
Default,
File(PathBuf),
@@ -59,7 +64,7 @@ pub enum Origin {
}
impl Origin {
/// 日志里那一列怎么写。
/// 来源的文案
pub fn describe(&self) -> String {
match self {
Origin::Default => "default".to_string(),
@@ -69,20 +74,22 @@ impl Origin {
}
}
/// 生效配置 + 每个选项的来源。
#[derive(Debug, Clone)]
/// 配置 + 每项来源
pub struct Loaded {
pub config: Config,
pub config_path: Option<PathBuf>,
pub origins: Vec<(&'static str, String, Origin)>,
}
/// TOML 文件里的形状:字段全部可选,没写的就当"文件没提这一项"。
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
// 文件形状
/// TOML 的形状(字段都可选)
struct FileConfig {
page: Option<FilePage>,
btree: Option<FileBtree>,
buffer: Option<FileBuffer>,
storage: Option<FileStorage>,
server: Option<FileServer>,
log: Option<FileLog>,
@@ -90,35 +97,48 @@ struct FileConfig {
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
/// [buffer] 段
struct FileBuffer {
pool_size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
/// [page] 段
struct FilePage {
size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
/// [btree] 段
struct FileBtree {
order: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
/// [storage] 段
struct FileStorage {
data_dir: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
/// [server] 段
struct FileServer {
pipe_name: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
/// [log] 段
struct FileLog {
level: Option<String>,
}
/// 从真实环境加载:CHUSQL_CONFIG(或当前目录的默认文件)+ 环境变量覆盖。
// 加载
/// 按真实环境加载
pub fn load() -> Result<Loaded, String> {
let path = match std::env::var("CHUSQL_CONFIG") {
Ok(p) if !p.trim().is_empty() => Some(PathBuf::from(p)),
@@ -137,7 +157,7 @@ pub fn load() -> Result<Loaded, String> {
resolve(text.as_deref(), path, &|k| std::env::var(k).ok())
}
/// 解析 TOML 文本 + 套环境变量覆盖;测试直接调这个,不碰真实环境。
/// 解析文本并套环境变量
pub fn resolve(
text: Option<&str>,
path: Option<PathBuf>,
@@ -180,6 +200,18 @@ pub fn resolve(
.map_err(|_| format!("not a number: {}", s))
},
)?;
let (pool_size, pool_origin) = pick(
"CHUSQL_POOL_SIZE",
file.buffer.and_then(|b| b.pool_size),
DEFAULT_POOL_SIZE,
&file_origin,
env,
|s| {
s.trim()
.parse::<usize>()
.map_err(|_| format!("not a number: {}", s))
},
)?;
let (data_dir, dir_origin) = pick(
"CHUSQL_DATA_DIR",
file.storage.and_then(|s| s.data_dir),
@@ -214,10 +246,13 @@ pub fn resolve(
return Err("server.pipe_name 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),
("server.pipe_name", pipe_name.clone(), pipe_origin),
("log.level", log_level.name().to_string(), log_origin),
@@ -227,6 +262,7 @@ pub fn resolve(
config: Config {
page_size,
btree_order,
pool_size,
data_dir: PathBuf::from(data_dir),
pipe_name,
log_level,
@@ -236,7 +272,7 @@ pub fn resolve(
})
}
/// 取一个选项:环境变量 → 配置文件 → 默认值,并记下它从哪来。
/// 取一项:env -> 文件 -> 默认
fn pick<T: Clone>(
env_key: &'static str,
file_value: Option<T>,
@@ -255,7 +291,8 @@ fn pick<T: Clone>(
Ok((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!(
+61 -43
View File
@@ -6,71 +6,68 @@ use crate::page::PageId;
use crate::page::{Page, PageFile};
use crate::protocol::Row;
// 堆表:把行按插入顺序塞进页里,支持全表扫描。页大小由配置给(默认 4096)。
// 页内布局(slotted page):
// [0..2] slot_count: u16(小端)
// [2..] 槽位目录,每槽 8 字节:offset u32 + len u32
// [中间] 空闲空间
// [页尾] 行数据,从后往前长
// 删除:把槽位的 len 置 0,数据不动(vacuum 以后再说)。
// 堆表:行按插入顺序进页,支持全表扫描与按索引取行。
/// 页头:slot_count 占 2 字节。
// 页内布局
/// 页头:slot_count 占 2 字节
const HEADER_SIZE: usize = 2;
/// 一个槽位占 8 字节:offset(u32) + len(u32)。
/// 一个槽位占 8 字节
const SLOT_SIZE: usize = 8;
/// 槽位目录起点。
/// 槽位目录起点
const SLOT_DIR_START: usize = HEADER_SIZE;
/// 从字节里读一个小端 u16。
/// 读一个小端 u16
fn read_u16(buf: &[u8], off: usize) -> u16 {
u16::from_le_bytes([buf[off], buf[off + 1]])
}
/// 往字节里写一个小端 u16。
/// 写一个小端 u16
fn write_u16(buf: &mut [u8], off: usize, v: u16) {
buf[off..off + 2].copy_from_slice(&v.to_le_bytes());
}
/// 从字节里读一个小端 u32。
/// 读一个小端 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。
/// 写一个小端 u32
fn write_u32(buf: &mut [u8], off: usize, v: u32) {
buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
/// 一个 (页号, 槽位号) 打包成一个 u64。页号高 48 位,槽位号低 16 位。
// 索引位置
/// (页号, 槽位号) 打包成 u64
fn pack_position(page_id: PageId, slot: u16) -> u64 {
(page_id << 16) | (slot as u64)
}
/// 从 u64 解回 (页号, 槽位号)。
/// 从 u64 解回 (页号, 槽位号)
fn unpack_position(v: u64) -> (PageId, u16) {
(v >> 16, (v & 0xFFFF) as u16)
}
/// 页内操作:不管文件,只管一页里的字节。
// 页内操作
/// 只管一页里的字节
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;
@@ -85,7 +82,7 @@ impl HeapPage {
min_off as usize
}
/// 试插一行:成功返回槽位号,放不下返回 None。
/// 试插一行,放不下返回 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);
@@ -95,11 +92,9 @@ impl HeapPage {
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);
@@ -108,7 +103,7 @@ impl HeapPage {
Some(n)
}
/// 读槽位里的行字节;槽位不存在或已删除返回 None。
/// 读槽位里的行字节
pub fn get_tuple(page: &Page, slot: u16) -> Option<Vec<u8>> {
let n = Self::slot_count(page);
if slot >= n {
@@ -123,46 +118,52 @@ impl HeapPage {
Some(page.data[off..off + len].to_vec())
}
/// 所有槽位号(含已删除的)。
/// 所有槽位号
pub fn iter_slots(page: &Page) -> Vec<u16> {
(0..Self::slot_count(page)).collect()
}
}
/// 堆表:一个文件 = 若干页,顺序追加行。
// 堆表
/// 一个文件 = 若干页
pub struct HeapTable {
file: PageFile,
index: Option<DiskBTree>,
}
impl HeapTable {
/// 打开(或创建)一张堆表。
pub fn open<P: AsRef<Path>>(path: P, page_size: usize) -> io::Result<Self> {
Ok(HeapTable {
file: PageFile::open(path, page_size)?,
/// 打开表(不带索引)
pub fn open<P: AsRef<Path>>(
path: P,
page_size: usize,
pool_size: usize,
) -> io::Result<Self> {
Ok(HeapTable {
file: PageFile::with_options(path, page_size, pool_size)?,
index: None,
})
}
/// 打开带 B+ 树索引的表;索引文件不存在会自动建。
/// 打开表 + 索引
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> {
Ok(HeapTable {
file: PageFile::open(path, page_size)?,
index: Some(DiskBTree::open(index_path, page_size, btree_order)?),
file: PageFile::with_options(path, page_size, pool_size)?,
index: Some(DiskBTree::open(index_path, page_size, btree_order, pool_size)?),
})
}
/// 插入一行;不更新索引。
/// 插入一行,不更新索引
pub fn insert(&mut self, row: &Row) -> io::Result<()> {
self.insert_returning_position(row).map(|_| ())
}
/// 插入一行并返回它的 (页号, 槽位号)。
/// 插入并返回行位置
pub fn insert_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
let tuple = serde_json::to_vec(row).map_err(io::Error::other)?;
@@ -190,7 +191,7 @@ impl HeapTable {
Ok((page_id, slot))
}
/// 插入一行,并把 (key, 位置) 写进索引。
/// 插入一行并写索引
pub fn insert_keyed(&mut self, row: &Row, key: i64) -> io::Result<()> {
let (page_id, slot) = self.insert_returning_position(row)?;
if let Some(idx) = &mut self.index {
@@ -199,7 +200,7 @@ impl HeapTable {
Ok(())
}
/// 按索引键取一行;没索引或键不存在返回 None。
/// 按索引键取一行
pub fn get_by_key(&mut self, key: i64) -> io::Result<Option<Row>> {
let pos = match &mut self.index {
None => return Ok(None),
@@ -212,7 +213,7 @@ impl HeapTable {
self.read_at(page_id, slot)
}
/// 按 (页号, 槽位号) 读一行。
/// 按位置读一行
pub fn read_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
let p = self.file.read_page(page_id)?;
match HeapPage::get_tuple(&p, slot) {
@@ -224,7 +225,7 @@ impl HeapTable {
}
}
/// 全表扫描:按页顺序、按槽位顺序返回所有行。
/// 全表扫描
pub fn scan(&mut self) -> io::Result<Vec<Row>> {
let n = self.file.num_pages()?;
let mut rows = Vec::new();
@@ -241,8 +242,7 @@ impl HeapTable {
Ok(rows)
}
/// 整表改写:清空堆表和索引,再把新行逐行插回去。
/// 行里有整数 id 就顺手重建索引 —— 堆表位置变了,旧索引必须作废,否则会按老位置读出别的行。
/// 整表改写并重建索引
pub fn replace_all(&mut self, rows: &[Row]) -> io::Result<()> {
self.file.truncate()?;
if let Some(idx) = &mut self.index {
@@ -256,9 +256,27 @@ impl HeapTable {
}
Ok(())
}
/// 把脏页写回
pub fn flush(&mut self) -> io::Result<()> {
if let Some(idx) = &mut self.index {
idx.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() }
}
/// 一行里的索引键:id 列是整数就用它(和 Haskell 侧"主键列叫 id"的约定一致)。
/// 取行里的 id 作索引键
fn row_key(row: &Row) -> Option<i64> {
match row.get("id") {
Some(serde_json::Value::Number(n)) => n.as_i64(),
+1 -1
View File
@@ -1,4 +1,4 @@
// 存储层库入口:对外暴露 page / protocol / heap / btree / config / log(server 与集成测试都从这里引)。
// 存储层库入口:对外暴露下面这些模块,服务与集成测试都从这里引。
pub mod btree;
pub mod config;
+36 -40
View File
@@ -3,23 +3,21 @@ use std::io::{IsTerminal, Write};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::OnceLock;
// 极简日志:等级存在一个原子变量里,被关掉的等级只花一次原子读——不求值参数、不做格式化。
// 行格式:[等级 本地日期时间] [来源] 消息
// - ERROR 加粗红、WARN 加粗橙、DEBUG 加粗不变色(只在 stderr 是终端时着色,重定向到文件时是纯文本)
// - 来源区分 core(服务自身:启动、配置)和 pipe(由管道请求触发)
// 等级由配置文件的 log.level 或环境变量 CHUSQL_LOG 决定。
// 极简日志:等级用原子变量,关掉的等级只花一次原子读。
/// 转义序列收尾。
// 颜色
/// 转义序列收尾
const RESET: &str = "\u{1b}[0m";
/// 加粗红(ERROR)。
/// 加粗红(ERROR)
const BOLD_RED: &str = "\u{1b}[1m\u{1b}[31m";
/// 加粗橙(WARN):256 色里的 208 号。
/// 加粗橙(WARN)
const BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m";
/// 只加粗(DEBUG)。
/// 只加粗(DEBUG)
const BOLD: &str = "\u{1b}[1m";
/// 日志等级;Off 表示全关。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
// 等级与来源
/// 日志等级
pub enum Level {
Off,
Error,
@@ -28,15 +26,15 @@ pub enum Level {
Debug,
}
/// 日志来源:core = 服务自身,pipe = 由管道请求触发。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// 来源:服务自身或管道请求
pub enum Channel {
Core,
Pipe,
}
impl Channel {
/// 日志行里的来源标签。
/// 来源标签
fn tag(self) -> &'static str {
match self {
Channel::Core => "core",
@@ -46,7 +44,7 @@ impl Channel {
}
impl Level {
/// 解析等级名(大小写不敏感)。
/// 解析等级名
pub fn parse(s: &str) -> Option<Level> {
match s.trim().to_ascii_lowercase().as_str() {
"off" => Some(Level::Off),
@@ -58,7 +56,7 @@ impl Level {
}
}
/// 等级名(写日志和写配置说明用)。
/// 等级名
pub fn name(self) -> &'static str {
match self {
Level::Off => "off",
@@ -69,7 +67,7 @@ impl Level {
}
}
/// 日志行里的标签,固定 5 字符宽,方便对齐。
/// 行里的等级标签
fn tag(self) -> &'static str {
match self {
Level::Error => "ERROR",
@@ -79,7 +77,7 @@ impl Level {
}
}
/// 这个等级要不要着色(INFO 不着色)。
/// 等级的颜色
fn color(self) -> &'static str {
match self {
Level::Error => BOLD_RED,
@@ -90,29 +88,28 @@ impl Level {
}
}
/// 当前等级,默认 info。
static LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
/// stderr 是不是终端(决定要不要着色),只问一次。
static COLOR: 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)
}
/// 启动时调一次:Windows 上给 stderr 打开 VT 处理,让 ANSI 颜色能被认识。
/// 启动时调一次
pub fn init() {
enable_vt();
}
/// 写出一行;只在 enabled 为真时调用。
/// 写出一行
pub fn write(channel: Channel, level: Level, args: fmt::Arguments) {
let msg = args.to_string();
let line = render(level, channel, color_enabled(), &now(), &msg);
@@ -121,7 +118,7 @@ pub fn write(channel: Channel, level: Level, args: fmt::Arguments) {
let _ = writeln!(out, "{}", line);
}
/// stderr 是终端才着色;CHUSQL_LOG_COLOR=always / never 可以强制开关。
/// 是否着色(终端或强制)
fn color_enabled() -> bool {
*COLOR.get_or_init(|| match std::env::var("CHUSQL_LOG_COLOR").as_deref().map(str::trim) {
Ok("always") | Ok("yes") | Ok("1") => true,
@@ -130,7 +127,7 @@ fn color_enabled() -> bool {
})
}
/// 拼一行:[等级 时间] [来源] 消息。纯函数,方便测试。
/// 拼一行(纯函数,可测)
fn render(level: Level, channel: Channel, color: bool, stamp: &Stamp, msg: &str) -> String {
let tag = level.tag();
let paint = level.color();
@@ -142,8 +139,9 @@ fn render(level: Level, channel: Channel, color: bool, stamp: &Stamp, msg: &str)
format!("[{} {}] [{}] {}", level_text, stamp, channel.tag(), msg)
}
/// 一个本地日期时间。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
// 时间
/// 本地日期时间
pub struct Stamp {
pub year: u16,
pub month: u16,
@@ -154,6 +152,7 @@ pub struct Stamp {
pub milli: u16,
}
/// 格式:年-月-日 时:分:秒.毫秒
impl fmt::Display for Stamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
@@ -164,7 +163,7 @@ impl fmt::Display for Stamp {
}
}
/// 当前本地时间:Windows 上走 kernel32::GetLocalTime。
/// 当前本地时间(Windows)
#[cfg(windows)]
fn now() -> Stamp {
#[repr(C)]
@@ -206,7 +205,7 @@ fn now() -> Stamp {
}
}
/// 当前时间:其他平台没有取本地时区的零依赖办法,退回 UTC。
/// 当前时间(其他平台用 UTC)
#[cfg(not(windows))]
fn now() -> Stamp {
let d = std::time::SystemTime::now()
@@ -225,7 +224,7 @@ fn now() -> Stamp {
}
}
/// 把"距 1970-01-01 的天数"换算成 (年, 月, 日):非 Windows 平台算时间用,单元测试也走这里。
/// 天数换算成日期
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;
@@ -239,7 +238,8 @@ pub fn civil_from_days(days: i64) -> (i64, u16, u16) {
(y + i64::from(month <= 2), month, day)
}
/// Windows:打开 stderr 的 VT 处理。
// 终端
/// 打开 Windows 的 VT 处理
#[cfg(windows)]
fn enable_vt() {
#[link(name = "kernel32")]
@@ -249,9 +249,7 @@ fn enable_vt() {
fn SetConsoleMode(handle: *mut core::ffi::c_void, mode: u32) -> i32;
}
/// STD_ERROR_HANDLE:-12 的无符号写法。
const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
/// 让控制台认识 ANSI 转义序列。
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
unsafe {
@@ -263,11 +261,12 @@ fn enable_vt() {
}
}
/// 非 Windows:不用做任何事。
/// 其他平台什么都不用做
#[cfg(not(windows))]
fn enable_vt() {}
/// 打一条 debug 日志(关掉时只花一次原子读)。
// 宏
/// debug 日志
#[macro_export]
macro_rules! log_debug {
(core, $($arg:tt)*) => {
@@ -278,7 +277,7 @@ macro_rules! log_debug {
};
}
/// 打一条 info 日志。
/// info 日志
#[macro_export]
macro_rules! log_info {
(core, $($arg:tt)*) => {
@@ -289,7 +288,7 @@ macro_rules! log_info {
};
}
/// 打一条 warn 日志。
/// warn 日志
#[macro_export]
macro_rules! log_warn {
(core, $($arg:tt)*) => {
@@ -300,7 +299,7 @@ macro_rules! log_warn {
};
}
/// 打一条 error 日志。
/// error 日志
#[macro_export]
macro_rules! log_error {
(core, $($arg:tt)*) => {
@@ -311,7 +310,7 @@ macro_rules! log_error {
};
}
/// 上面四个宏的公共部分:先看等级,再看要不要真的写。内部用,别直接调。
/// 四个宏的公共部分
#[doc(hidden)]
#[macro_export]
macro_rules! log_line {
@@ -326,7 +325,6 @@ macro_rules! log_line {
mod tests {
use super::*;
/// 天数换算日期:几个已知点 + 两次闰年的 2 月 29 日。
#[test]
fn civil_from_days_known_dates() {
assert_eq!(civil_from_days(0), (1970, 1, 1));
@@ -336,7 +334,6 @@ mod tests {
assert_eq!(civil_from_days(19_782), (2024, 2, 29));
}
/// 行格式:等级 + 本地日期时间 + 来源 + 消息,等级该粗的粗、该色的色。
#[test]
fn render_has_time_channel_and_colors() {
let stamp = Stamp {
@@ -367,7 +364,6 @@ mod tests {
);
}
/// 关掉颜色时整行必须是纯文本(重定向到文件时用)。
#[test]
fn render_without_color_is_plain() {
let stamp = Stamp {
+180 -27
View File
@@ -1,23 +1,26 @@
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
// 页管理:数据库文件按固定大小的页组织,页大小由配置给(默认 4096,见 config.rs)。
// 只负责"按页号读 / 写",不理解页里的内容 —— 内容怎么解释由上层(堆表 / B+ 树 / WAL)决定。
// 页号从 0 开始,第 n 页在文件中的偏移是 n * page_size。
// 页管理:定长页的读写,外加一层页缓冲池。
/// 页号:从 0 开始。u64 足够大,不用担心中途用完。
// 页
/// 页号:从 0 开始
pub type PageId = u64;
/// 一页:固定 page_size 字节,外加自己的页号。
/// 缓冲池默认容量(页数)
pub const DEFAULT_POOL_SIZE: usize = 64;
#[derive(Debug, Clone)]
/// 一页:定长字节 + 页号
pub struct Page {
pub id: PageId,
pub data: Vec<u8>,
}
impl Page {
/// 新建一页,内容全 0。
/// 新建一页,全 0
pub fn new(id: PageId, size: usize) -> Self {
Page {
id,
@@ -25,46 +28,118 @@ impl Page {
}
}
/// 清零。复用时先清空,避免残留旧数据。
/// 清零
pub fn zero(&mut self) {
self.data.fill(0);
}
}
/// 页文件:负责把 Page 落到磁盘上(不做缓存,每次读写都直接 syscall;缓冲池是后面的阶段)。
#[derive(Debug)]
/// 页文件:定长页读写 + 页缓存
pub struct PageFile {
file: File,
page_size: usize,
capacity: usize,
cache: HashMap<PageId, Page>,
order: VecDeque<PageId>,
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 })
Ok(PageFile {
file,
page_size,
capacity,
cache: HashMap::new(),
order: VecDeque::new(),
dirty: HashSet::new(),
hits: 0,
misses: 0,
})
}
/// 这个文件的页大小。
/// 页大小
pub fn page_size(&self) -> usize {
self.page_size
}
/// 读取指定页号的页;页号超出文件末尾时返回 UnexpectedEof。
pub fn read_page(&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 capacity(&self) -> usize {
self.capacity
}
/// 写入一页。若页号超出当前文件末尾,文件会自动扩展。
pub fn write_page(&mut self, page: &Page) -> io::Result<()> {
/// 当前缓存了多少页
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) {
if let Some(pos) = self.order.iter().position(|&x| x == id) {
self.order.remove(pos);
}
self.order.push_back(id);
}
/// 淘汰一个最冷的页
fn evict_one(&mut self) -> io::Result<bool> {
while let Some(victim) = self.order.pop_front() {
if let Some(p) = self.cache.get(&victim) {
if self.dirty.contains(&victim) {
let p = p.clone();
self.write_raw(&p)?;
self.dirty.remove(&victim);
}
self.cache.remove(&victim);
return Ok(true);
}
}
Ok(false)
}
/// 直接写文件
fn write_raw(&mut self, page: &Page) -> io::Result<()> {
if page.data.len() != self.page_size {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
@@ -77,12 +152,58 @@ impl PageFile {
}
self.file
.seek(SeekFrom::Start(page.id * self.page_size as u64))?;
self.file.write_all(&page.data)?;
self.file.flush()?;
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> {
if let Some(p) = self.cache.get(&id) {
let p = p.clone();
self.hits += 1;
self.touch(id);
return Ok(p);
}
self.misses += 1;
let p = self.read_raw(id)?;
if self.cache.len() >= self.capacity {
self.evict_one()?;
}
self.cache.insert(id, p.clone());
self.order.push_back(id);
Ok(p)
}
/// 写一页(走缓存)
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);
@@ -90,7 +211,7 @@ impl PageFile {
Ok(page)
}
/// 当前文件里有多少页;长度不是页大小整数倍,说明页大小和文件对不上。
/// 文件里有多少页
pub fn num_pages(&mut self) -> io::Result<PageId> {
let len = self.file.metadata()?.len();
let size = self.page_size as u64;
@@ -103,18 +224,50 @@ impl PageFile {
),
));
}
Ok(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.file.sync_all()
self.flush()
}
/// 清空文件,游标归零。
/// 清空文件
pub fn truncate(&mut self) -> io::Result<()> {
self.cache.clear();
self.order.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();
}
}
+16 -7
View File
@@ -1,30 +1,30 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// 协议定义:Haskell ↔ Rust 之间那条"行分隔 JSON"消息的类型(请求 / 响应)。
// 协议:Haskell 与 Rust 之间的请求 / 响应类型。
/// 一行:列名到 JSON 值的映射。
/// 一行:列名到 JSON 值
pub type Row = HashMap<String, serde_json::Value>;
/// 列类型;跟 Haskell 侧 TInt / TStr / TBool 对应。
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
/// 列类型(对应 Haskell 的 TInt / TStr / TBool)
pub enum ColumnType {
Int,
Str,
Bool,
}
/// 一列的 schema。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
/// 一列的 schema
pub struct SchemaColumn {
pub name: String,
pub ty: ColumnType,
}
/// 请求;method 字段做 tag。
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "snake_case")]
/// 请求:method 字段决定类型
pub enum Request {
Ping,
Scan { table: String },
@@ -38,16 +38,25 @@ pub enum Request {
ReplaceAll { table: String, rows: Vec<Row> },
ListTables,
DescribeTable { table: String },
CreateTable {
table: String,
columns: Vec<SchemaColumn>,
},
DropTable { table: String },
}
/// 响应;status 字段做 tag。
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "lowercase")]
/// 响应:status 字段决定类型
pub enum Response {
Pong,
Rows { rows: Vec<Row> },
Tables { tables: Vec<String> },
Schema { columns: Vec<SchemaColumn> },
Schema {
columns: Vec<SchemaColumn>,
#[serde(default)]
row_count: u64,
},
Ok,
Error { message: String },
}
+13 -32
View File
@@ -1,15 +1,3 @@
//! 简化版 WAL:写前日志 + 崩溃恢复。
//!
//! 条目格式:
//! [u32 body_len][body]
//! body = [u64 LSN][u8 操作类型][u16 表名长度][表名字节][JSON 载荷]
//!
//! 简化版约束:
//! - 最多只有一条未完成的操作。写成功就清空 WAL。
//! - 不做 checkpoint / redo / undo / 事务。
//!
//! 崩溃恢复:
//! - 启动时若 WAL 非空,重放那一条,然后清空。
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
@@ -19,12 +7,13 @@ use serde::{Deserialize, Serialize};
use crate::protocol::Row;
/// 操作类型标记。
// 预写日志:先记操作并 fsync,再改数据;重启重放残留。
const OP_INSERT: u8 = 1;
const OP_REPLACE_ALL: u8 = 2;
/// 一条 WAL 操作。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
/// 一条待写入的操作
pub enum WalOp {
Insert {
table: String,
@@ -37,25 +26,26 @@ pub enum WalOp {
},
}
/// WAL 文件句柄。
/// WAL 文件
pub struct Wal {
path: PathBuf,
}
impl Wal {
/// 绑定到 WAL 文件路径;首次 append 时自动创建文件。
// 读写
/// 绑定 WAL 文件路径
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Wal {
path: path.as_ref().to_path_buf(),
}
}
/// 日志文件路径。
/// 日志文件路径
pub fn path(&self) -> &Path {
&self.path
}
/// 清空 WAL;文件不存在则什么都不做。
/// 清空 WAL
pub fn clear(&self) -> io::Result<()> {
if self.path.exists() {
let f = OpenOptions::new().write(true).open(&self.path)?;
@@ -65,7 +55,7 @@ impl Wal {
Ok(())
}
/// 追加一条操作并 fsync。
/// 追加一条并 fsync
pub fn append(&self, op: &WalOp) -> io::Result<()> {
let bytes = encode(op)?;
let mut f = OpenOptions::new()
@@ -77,7 +67,7 @@ impl Wal {
Ok(())
}
/// 读取当前 WAL;空文件 / 不存在返回 None。
/// 读当前 WAL;空则 None
pub fn read(&self) -> io::Result<Option<WalOp>> {
if !self.path.exists() {
return Ok(None);
@@ -98,9 +88,9 @@ impl Wal {
}
}
/// 把一条操作编成字节。
// 编解码
/// 把操作编成字节
fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
// 先编出 (类型, 表名, 载荷)
let (op_type, table, payload) = match op {
WalOp::Insert { table, row, key } => {
let p = serde_json::to_vec(&(row, key)).map_err(io::Error::other)?;
@@ -114,8 +104,6 @@ fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
let table_bytes = table.as_bytes();
// body = [u64 LSN][u8 类型][u16 表名长度][表名][载荷]
// 简化版 LSN 固定为 1;完整版会改成真正递增。
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());
@@ -124,32 +112,25 @@ fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
body.extend_from_slice(table_bytes);
body.extend_from_slice(&payload);
// 整条 = [u32 body_len][body]
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)
}
/// 解析一条 WAL 条目。
///
/// - `Ok(Some(op))`:成功解析出操作
/// - `Ok(None)`:尾部被截断(写入一半就断电),按"没有这条"处理
/// - `Err(_)`:字节流本身损坏,抛错
/// 从字节解回操作
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());
}
// LSN 简化版忽略
let _lsn = u64::from_le_bytes([
body[0], body[1], body[2], body[3], body[4], body[5], body[6], body[7],
]);
+26 -21
View File
@@ -1,10 +1,10 @@
use chusql_storage::btree::DiskBTree;
use chusql_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
use chusql_storage::page::DEFAULT_POOL_SIZE;
// B+ 树测试:空树查询、插入后查找、重开文件数据仍在、覆盖同一个键、升序遍历、
// 倒序插入 500 个键、页大小 / order 可以配置、配置和文件头对不上时报错。
// B+ 树测试:插查、重开、分裂、页大小与 order 校验。
/// 空树的 get 返回 None,遍历返回空。
/// 空树查不到,遍历为空
#[test]
fn empty_tree_get() {
let dir = tempfile::tempdir().unwrap();
@@ -12,13 +12,14 @@ fn empty_tree_get() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
assert_eq!(t.get(1).unwrap(), None);
assert_eq!(t.iter_all().unwrap(), vec![]);
}
/// 插 100 个键,每个都能按 key 查回来。
/// 插 100 个键都能查回
#[test]
fn insert_then_get() {
let dir = tempfile::tempdir().unwrap();
@@ -26,6 +27,7 @@ fn insert_then_get() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in 0..100 {
@@ -36,24 +38,24 @@ fn insert_then_get() {
}
}
/// 关掉再打开同一个索引文件,数据还在。
/// 重开文件数据还在
#[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).unwrap();
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
t.insert(i, i as u64).unwrap();
}
}
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).unwrap();
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
assert_eq!(t.get(i).unwrap(), Some(i as u64));
}
}
/// 同一个键插两次,后写的值生效。
/// 同一个键后写的值生效
#[test]
fn update_existing() {
let dir = tempfile::tempdir().unwrap();
@@ -61,6 +63,7 @@ fn update_existing() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
t.insert(5, 100).unwrap();
@@ -68,7 +71,7 @@ fn update_existing() {
assert_eq!(t.get(5).unwrap(), Some(200));
}
/// 倒序插入后遍历,结果仍按 key 升序。
/// 倒序插入后遍历仍升序
#[test]
fn iter_all_sorted() {
let dir = tempfile::tempdir().unwrap();
@@ -76,6 +79,7 @@ fn iter_all_sorted() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in (0..50).rev() {
@@ -88,7 +92,7 @@ fn iter_all_sorted() {
}
}
/// 倒序插入 500 个键(必然多次分裂),每个键都能查到。
/// 倒序插 500 个键都能查到
#[test]
fn reverse_order_insert() {
let dir = tempfile::tempdir().unwrap();
@@ -96,6 +100,7 @@ fn reverse_order_insert() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in (0..500).rev() {
@@ -106,11 +111,11 @@ fn reverse_order_insert() {
}
}
/// 页大小和 order 都可以配置:512 字节的页 + 8 个孩子照样能插能查。
/// 512 字节页 + 8 个孩子可用
#[test]
fn custom_page_size_and_order() {
let dir = tempfile::tempdir().unwrap();
let mut t = DiskBTree::open(dir.path().join("t.idx"), 512, 8).unwrap();
let mut t = DiskBTree::open(dir.path().join("t.idx"), 512, 8, DEFAULT_POOL_SIZE).unwrap();
for i in 0..200 {
t.insert(i, i as u64 + 1).unwrap();
}
@@ -119,32 +124,32 @@ fn custom_page_size_and_order() {
}
}
/// 文件头里记着页大小和 order:换配置再打开要报错,而不是读出乱七八糟的东西。
/// 页大小或 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).unwrap();
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
t.insert(1, 1).unwrap();
}
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 8).is_err());
assert!(DiskBTree::open(&path, 8192, DEFAULT_BTREE_ORDER).is_err());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).is_ok());
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).is_err());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
}
/// order 放不进一页时直接拒绝。
/// order 放不进一页要报错
#[test]
fn order_too_large_for_page_errors() {
let dir = tempfile::tempdir().unwrap();
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300).is_err());
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300, DEFAULT_POOL_SIZE).is_err());
}
+8
View File
@@ -2,6 +2,10 @@ use chusql_storage::catalog::Catalog;
use chusql_storage::protocol::{ColumnType, Row};
use serde_json::json;
// 数据字典测试:补列、去重、未知表。
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
@@ -10,6 +14,7 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
m
}
/// 空文件得到空字典
#[test]
fn empty_file_gives_empty_catalog() {
let dir = tempfile::tempdir().unwrap();
@@ -18,6 +23,7 @@ fn empty_file_gives_empty_catalog() {
assert_eq!(c.table_names().len(), 0);
}
/// 补列之后能查到 schema
#[test]
fn ensure_then_describe() {
let dir = tempfile::tempdir().unwrap();
@@ -39,6 +45,7 @@ fn ensure_then_describe() {
assert_eq!(name_col.ty, ColumnType::Str);
}
/// 再插入不会重复列
#[test]
fn second_insert_does_not_duplicate_columns() {
let dir = tempfile::tempdir().unwrap();
@@ -50,6 +57,7 @@ fn second_insert_does_not_duplicate_columns() {
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
}
/// 未知表返回 None
#[test]
fn describe_unknown_returns_none() {
let dir = tempfile::tempdir().unwrap();
+13 -13
View File
@@ -6,10 +6,9 @@ use chusql_storage::config::{
};
use chusql_storage::log::Level;
// 配置测试:没有文件时全默认、文件里写了才覆盖、环境变量优先于文件、
// 非法值 / 未知键要报错、每个选项的来源都记得下来。
// 配置测试:默认值、文件覆盖、环境变量优先、非法值报错。
/// 没有配置文件、没有环境变量:全部走内置默认值。
/// 无文件时全默认
#[test]
fn defaults_without_file() {
let loaded = config::resolve(None, None, &|_| None).unwrap();
@@ -23,7 +22,7 @@ fn defaults_without_file() {
assert_eq!(loaded.config.log_level, Level::Info);
}
/// 文件里只写了一项:这一项生效,其余回退默认(并且来源记得清清楚楚)。
/// 只覆盖文件里写了的项
#[test]
fn file_overrides_only_written_keys() {
let path = PathBuf::from("t.toml");
@@ -41,15 +40,16 @@ fn file_overrides_only_written_keys() {
assert_eq!(order.2, Origin::Default);
}
/// 五项全写:值都生效,来源都是文件。
/// 六项全写都生效
#[test]
fn file_all_keys() {
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[storage]\ndata_dir = \"mydata\"\n[server]\npipe_name = \"mypipe\"\n[log]\nlevel = \"debug\"\n";
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[buffer]\npool_size = 128\n[storage]\ndata_dir = \"mydata\"\n[server]\npipe_name = \"mypipe\"\n[log]\nlevel = \"debug\"\n";
let path = PathBuf::from("t.toml");
let loaded = config::resolve(Some(text), Some(path.clone()), &|_| None).unwrap();
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.pipe_name, "mypipe");
assert_eq!(loaded.config.log_level, Level::Debug);
@@ -61,7 +61,7 @@ fn file_all_keys() {
);
}
/// 环境变量优先于文件。
/// 环境变量优先于文件
#[test]
fn env_beats_file() {
let text = "[storage]\ndata_dir = \"from-file\"\n";
@@ -77,14 +77,14 @@ fn env_beats_file() {
assert_eq!(dir.2, Origin::Env("CHUSQL_DATA_DIR"));
}
/// 日志等级写错要报错,而不是悄悄按默认跑。
/// 日志等级非法要报错
#[test]
fn rejects_bad_log_level() {
let err = config::resolve(Some("[log]\nlevel = \"loud\"\n"), None, &|_| None).unwrap_err();
assert!(err.contains("log.level"), "got {}", err);
}
/// 页大小必须是 512..65536 之间的 2 的幂。
/// 页大小非法要报错
#[test]
fn rejects_bad_page_size() {
let err = config::resolve(Some("[page]\nsize = 3000\n"), None, &|_| None).unwrap_err();
@@ -94,7 +94,7 @@ fn rejects_bad_page_size() {
assert!(err.contains("page.size"), "got {}", err);
}
/// order 太小、或者放不进一页,都要报错。
/// order 非法要报错
#[test]
fn rejects_bad_btree_order() {
let err = config::resolve(Some("[btree]\norder = 2\n"), None, &|_| None).unwrap_err();
@@ -104,14 +104,14 @@ fn rejects_bad_btree_order() {
assert!(err.contains("does not fit"), "got {}", err);
}
/// 配置里出现认不出来的键要报错(拼错了不能当没看见)。
/// 未知配置键要报错
#[test]
fn rejects_unknown_key() {
let err = config::resolve(Some("[page]\nsizes = 4096\n"), None, &|_| None).unwrap_err();
assert!(err.contains("bad config"), "got {}", err);
}
/// 环境变量给了非法值也要报错。
/// 环境变量非法要报错
#[test]
fn rejects_bad_env_value() {
let env = |k: &str| (k == "CHUSQL_PAGE_SIZE").then(|| "big".to_string());
@@ -119,7 +119,7 @@ fn rejects_bad_env_value() {
assert!(err.contains("CHUSQL_PAGE_SIZE"), "got {}", err);
}
/// 来源文案要能说清"哪来的"。
/// 来源文案正确
#[test]
fn origin_description() {
assert_eq!(Origin::Default.describe(), "default");
+17 -17
View File
@@ -1,11 +1,11 @@
use chusql_storage::config::DEFAULT_PAGE_SIZE;
use chusql_storage::heap::HeapTable;
use chusql_storage::page::DEFAULT_POOL_SIZE;
use chusql_storage::protocol::Row;
use serde_json::json;
// 堆表测试:插入 + 全表扫描、跨页多行、大行、超大行报错、重开文件后数据还在、自定义页大小。
// 堆表测试:插查、跨页、大行、重开、索引重建。
/// 造一行测试数据(列名 → JSON 值)。
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
@@ -14,12 +14,12 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
m
}
/// 插一行再扫描,能原样读回来。
/// 插一行再扫描读回
#[test]
fn insert_and_scan_one_row() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
.unwrap();
@@ -30,12 +30,12 @@ fn insert_and_scan_one_row() {
assert_eq!(rows[0]["name"], json!("Alice"));
}
/// 插 500 行(必然跨多页),扫描回来的行数和顺序都要对。
/// 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).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
for i in 0..500 {
t.insert(&row(&[("id", json!(i))])).unwrap();
@@ -43,18 +43,17 @@ fn insert_many_rows_cross_pages() {
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 的大行也要能放进一页。
/// 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).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let big = "x".repeat(2000);
t.insert(&row(&[("data", json!(big.clone()))])).unwrap();
@@ -64,43 +63,43 @@ fn large_row_still_fits() {
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).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let huge = "x".repeat(10_000);
let err = t.insert(&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).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert(&row(&[("id", json!(1))])).unwrap();
t.insert(&row(&[("id", json!(2))])).unwrap();
}
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).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));
}
/// 页大小可以配置:512 字节的页装得下小行、放不下大行,行数不变。
/// 512 字节页可用
#[test]
fn custom_page_size() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("small.db");
let mut t = HeapTable::open(&path, 512).unwrap();
let mut t = HeapTable::open(&path, 512, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
t.insert(&row(&[("id", json!(i))])).unwrap();
@@ -111,7 +110,7 @@ fn custom_page_size() {
assert!(t.insert(&row(&[("data", json!(big))])).is_err());
}
/// 整表改写之后索引必须跟着重建:旧键查不到,新行按 id 能查到。
/// 整表改写后索引重建
#[test]
fn replace_all_rebuilds_index() {
let dir = tempfile::tempdir().unwrap();
@@ -120,6 +119,7 @@ fn replace_all_rebuilds_index() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
chusql_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
+52 -18
View File
@@ -1,10 +1,9 @@
use chusql_storage::config::DEFAULT_PAGE_SIZE;
use chusql_storage::page::{Page, PageFile};
// 页文件测试:一页的读写、页号越界报错、追加页让文件变长、多页互不干扰、覆写同一页、
// 自定义页大小、页大小和文件对不上时报错。
// 页文件测试:读写、追加、覆写、页大小校验与页缓存。
/// 写入一页再读回来,内容和长度都应该一致。
/// 写一页再读回一致
#[test]
fn write_then_read_page() {
let dir = tempfile::tempdir().unwrap();
@@ -23,7 +22,7 @@ fn write_then_read_page() {
assert_eq!(read.data.len(), DEFAULT_PAGE_SIZE);
}
/// 空文件读第 0 页应该报错。
/// 空文件读第 0 页报错
#[test]
fn read_out_of_range_returns_error() {
let dir = tempfile::tempdir().unwrap();
@@ -33,7 +32,7 @@ fn read_out_of_range_returns_error() {
assert!(pf.read_page(0).is_err());
}
/// 每 append 一页,num_pages 就加一。
/// 追加让页数加一
#[test]
fn append_grows_file() {
let dir = tempfile::tempdir().unwrap();
@@ -50,7 +49,7 @@ fn append_grows_file() {
assert_eq!(pf.num_pages().unwrap(), 3);
}
/// 页 0 和页 1 的内容互不影响。
/// 两页内容互不影响
#[test]
fn two_pages_independent() {
let dir = tempfile::tempdir().unwrap();
@@ -69,7 +68,7 @@ fn two_pages_independent() {
assert_eq!(pf.read_page(1).unwrap().data[0], 2);
}
/// 覆写同一页:读到的是新内容,页数不变。
/// 覆写同页读到新内容
#[test]
fn overwrite_page() {
let dir = tempfile::tempdir().unwrap();
@@ -89,25 +88,20 @@ fn overwrite_page() {
assert_eq!(pf.num_pages().unwrap(), 1);
}
/// 页大小可以配置:512 字节的页照样读写,文件按 512 字节对齐。
/// 512 字节页可用
#[test]
fn custom_page_size() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("small.db");
let path = dir.path().join("t.db");
let mut pf = PageFile::open(&path, 512).unwrap();
assert_eq!(pf.page_size(), 512);
let mut p = Page::new(0, 512);
p.data[511] = 9;
pf.write_page(&p).unwrap();
pf.write_page(&Page::new(0, 512)).unwrap();
pf.flush().unwrap();
assert_eq!(pf.read_page(0).unwrap().data[511], 9);
assert_eq!(pf.num_pages().unwrap(), 1);
assert_eq!(std::fs::metadata(&path).unwrap().len(), 512);
}
/// 文件长度不是页大小整数倍(比如页大小被改过)要报错,而不是算出错误的页数。
/// 长度不是整数倍要报错
#[test]
fn mismatched_file_length_errors() {
let dir = tempfile::tempdir().unwrap();
@@ -118,7 +112,7 @@ fn mismatched_file_length_errors() {
assert!(pf.num_pages().is_err());
}
/// 页大小对不上的页不许写进去。
/// 页大小不符拒绝写入
#[test]
fn write_wrong_sized_page_errors() {
let dir = tempfile::tempdir().unwrap();
@@ -127,3 +121,43 @@ fn write_wrong_sized_page_errors() {
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);
}
+71 -19
View File
@@ -9,9 +9,9 @@ use interprocess::local_socket::{
};
use interprocess::TryClone;
// 命名管道集成测试:起 server 子进程,用唯一管道名,测试之间互不干扰。
// 命名管道集成测试:起真 server,跑协议、索引与配置。
/// 生成唯一管道名,避免测试之间抢占。
/// 唯一管道名,避免测试互抢
fn unique_pipe_name() -> String {
let id = std::process::id();
let ns = std::time::SystemTime::now()
@@ -21,18 +21,18 @@ fn unique_pipe_name() -> String {
format!("chusql-test-{}-{}", id, ns)
}
/// 持有 server 子进程,Drop 时杀掉。
/// 持有 server 子进程,退出时杀掉
struct ServerProc(Child);
impl Drop for ServerProc {
/// 测试结束时杀掉子进程并回收,避免留下孤儿进程。
/// 测试结束杀掉子进程
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
/// 启动 server 子进程,并等管道可连(最多 5 秒)。
/// 起 server,等管道可连
fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
let data = tempfile::tempdir().unwrap();
@@ -57,7 +57,7 @@ fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
panic!("server did not start within 5s");
}
/// 连到指定管道。
/// 连到指定管道
fn connect(pipe: &str) -> std::io::Result<LocalSocketStream> {
let name = pipe
.to_ns_name::<GenericNamespaced>()
@@ -65,7 +65,7 @@ fn connect(pipe: &str) -> std::io::Result<LocalSocketStream> {
LocalSocketStream::connect(name)
}
/// 发一行请求,读一行响应。
/// 发一行请求读一行响应
fn send(stream: &mut LocalSocketStream, line: &str) -> String {
stream.write_all(line.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
@@ -77,7 +77,7 @@ fn send(stream: &mut LocalSocketStream, line: &str) -> String {
buf.trim().to_string()
}
/// ping 应该回 pong。
/// ping 回 pong
#[test]
fn ping_pong() {
let pipe = unique_pipe_name();
@@ -87,7 +87,7 @@ fn ping_pong() {
assert_eq!(send(&mut c, r#"{"method":"ping"}"#), r#"{"status":"pong"}"#);
}
/// 插一行再 scan,能扫到刚插进去的数据。
/// 插入后能扫到
#[test]
fn insert_then_scan() {
let pipe = unique_pipe_name();
@@ -104,6 +104,7 @@ fn insert_then_scan() {
assert!(r2.contains("Alice"), "scan response: {}", r2);
}
/// 不存在的表报错
#[test]
fn scan_unknown_table_errors() {
let pipe = unique_pipe_name();
@@ -115,6 +116,7 @@ fn scan_unknown_table_errors() {
assert!(r.contains("unknown table"), "got {}", r);
}
/// 列出已建的表
#[test]
fn list_tables_returns_created_tables() {
let pipe = unique_pipe_name();
@@ -129,6 +131,7 @@ fn list_tables_returns_created_tables() {
assert!(r.contains("orders"), "got {}", r);
}
/// 整表替换只剩新行
#[test]
fn replace_all_replaces_rows() {
let pipe = unique_pipe_name();
@@ -149,13 +152,13 @@ fn replace_all_replaces_rows() {
assert!(!r.contains("Alice"), "old row should be gone: {}", r);
}
/// 带 key 插入后能按下标查
#[test]
fn insert_with_key_then_lookup() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
// 带索引键插三行
for (id, name) in [(1, "Alice"), (2, "Bob"), (3, "Carol")] {
let req = format!(
r#"{{"method":"insert","table":"idx_users","row":{{"id":{},"name":"{}"}},"key":{}}}"#,
@@ -165,15 +168,14 @@ fn insert_with_key_then_lookup() {
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
}
// 命中
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":2}"#);
assert!(r.contains("Bob"), "lookup: {}", r);
// 未命中
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":99}"#);
assert!(r.contains(r#""rows":[]"#), "miss: {}", r);
}
/// 不带 key 不建索引
#[test]
fn insert_without_key_does_not_index() {
let pipe = unique_pipe_name();
@@ -185,12 +187,12 @@ fn insert_without_key_does_not_index() {
assert!(r.contains(r#""rows":[]"#), "got: {}", r);
}
/// 重开服务后索引仍在
#[test]
fn lookup_survives_reopen() {
let pipe = unique_pipe_name();
let data = tempfile::tempdir().unwrap();
// 第一次:写 3 行带 key
{
let exe = env!("CARGO_BIN_EXE_server");
let child = Command::new(exe)
@@ -213,13 +215,11 @@ fn lookup_survives_reopen() {
);
send(&mut c, &req);
}
drop(srv); // 杀 server
drop(srv);
}
// 等一下,让文件句柄真正释放
thread::sleep(Duration::from_millis(200));
// 第二次:重开 server,从索引查到
let (_srv, _data) = {
let child = Command::new(env!("CARGO_BIN_EXE_server"))
.env("CHUSQL_PIPE", &pipe)
@@ -241,7 +241,7 @@ fn lookup_survives_reopen() {
assert!(r.contains("Bob"), "persist lookup: {}", r);
}
/// 配置文件里的管道名和数据目录要真的生效(这里不设 CHUSQL_PIPE / CHUSQL_DATA_DIR)。
/// 配置文件真的生效
#[test]
fn config_file_is_used() {
let dir = tempfile::tempdir().unwrap();
@@ -249,7 +249,6 @@ fn config_file_is_used() {
let data_dir = dir.path().join("data");
let config_path = dir.path().join("chusql-storage.toml");
// TOML 里写正斜杠,省得转义 Windows 路径
let text = format!(
"[server]\npipe_name = \"{}\"\n[storage]\ndata_dir = \"{}\"\n[log]\nlevel = \"debug\"\n",
pipe,
@@ -276,9 +275,62 @@ fn config_file_is_used() {
let r = send(&mut c, r#"{"method":"insert","table":"cfg","row":{"id":1}}"#);
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
// 数据真的落在配置文件指的那个目录里
assert!(
data_dir.join("cfg.db").exists(),
"data dir from config file was not used"
);
}
/// 建表后能查 schema
#[test]
fn create_table_then_describe() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let r = send(
&mut c,
r#"{"method":"create_table","table":"ct_users","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}"#,
);
assert!(r.contains(r#""status":"ok""#), "create: {}", r);
let r = send(&mut c, r#"{"method":"describe_table","table":"ct_users"}"#);
assert!(r.contains("id"), "describe: {}", r);
assert!(r.contains("name"), "describe: {}", r);
assert!(r.contains(r#""ty":"int""#), "describe: {}", r);
assert!(r.contains(r#""ty":"str""#), "describe: {}", r);
}
/// 重复建表报错
#[test]
fn create_table_twice_errors() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let req = r#"{"method":"create_table","table":"dup","columns":[{"name":"id","ty":"int"}]}"#;
let r1 = send(&mut c, req);
assert!(r1.contains(r#""status":"ok""#), "first: {}", r1);
let r2 = send(&mut c, req);
assert!(r2.contains(r#""status":"error""#), "second: {}", r2);
assert!(r2.contains("already exists"), "second: {}", r2);
}
/// 新建空表能扫描
#[test]
fn scan_empty_table_after_create() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let r = send(
&mut c,
r#"{"method":"create_table","table":"empty_t","columns":[{"name":"id","ty":"int"}]}"#,
);
assert!(r.contains(r#""status":"ok""#), "create: {}", r);
let r = send(&mut c, r#"{"method":"scan","table":"empty_t"}"#);
assert!(r.contains(r#""status":"rows""#), "scan: {}", r);
assert!(r.contains(r#""rows":[]"#), "scan: {}", r);
}
+10 -5
View File
@@ -2,6 +2,9 @@ use chusql_storage::protocol::Row;
use chusql_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 {
@@ -10,6 +13,7 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
m
}
/// 写读 Insert 操作
#[test]
fn append_and_read_insert() {
let dir = tempfile::tempdir().unwrap();
@@ -26,6 +30,7 @@ fn append_and_read_insert() {
assert_eq!(back, op);
}
/// 写读 ReplaceAll 操作
#[test]
fn append_and_read_replace_all() {
let dir = tempfile::tempdir().unwrap();
@@ -44,6 +49,7 @@ fn append_and_read_replace_all() {
assert_eq!(back, op);
}
/// 文件不存在返回 None
#[test]
fn missing_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
@@ -51,6 +57,7 @@ fn missing_file_returns_none() {
assert!(wal.read().unwrap().is_none());
}
/// 空文件返回 None
#[test]
fn empty_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
@@ -60,6 +67,7 @@ fn empty_file_returns_none() {
assert!(wal.read().unwrap().is_none());
}
/// 清空后读不到
#[test]
fn clear_truncates() {
let dir = tempfile::tempdir().unwrap();
@@ -79,6 +87,7 @@ fn clear_truncates() {
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
/// 尾巴截断被忽略
#[test]
fn truncated_tail_is_ignored() {
let dir = tempfile::tempdir().unwrap();
@@ -92,19 +101,15 @@ fn truncated_tail_is_ignored() {
})
.unwrap();
// 砍掉最后 5 个字节,模拟"写一半断电"
let full = std::fs::read(&path).unwrap();
std::fs::write(&path, &full[..full.len() - 5]).unwrap();
// 读回来应该当作"没有这条"
assert!(wal.read().unwrap().is_none());
}
/// 第二次追加会覆盖
#[test]
fn overwrite_on_second_append_is_visible() {
// 简化版一次只允许一条,append 两次会叠在文件里——
// 但正常路径下 clear 先跑,所以实际不会叠加。
// 这个测试只验证 read 能解析最前面那一条。
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));