优化和收尾

This commit is contained in:
LIm1tZz7 committed 2026-09-25 14:21:55 +08:00
1 parent 7f5bcc289e
commit 9e8c5f1ef0
33 files changed
+3332 -598

No files matched your search

+324 -75
View File
@@ -9,26 +9,25 @@ 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, SchemaColumn, TableSchemaWire};
use chusql_storage::protocol::{
ColumnStatWire, IndexWire, Request, Response, SchemaColumn, TableSchemaWire,
};
use chusql_storage::wal::{Wal, WalOp};
use chusql_storage::{log_debug, log_error, log_info, log_warn};
use interprocess::local_socket::{
prelude::*,
GenericNamespaced,
ListenerOptions,
};
use interprocess::local_socket::{prelude::*, GenericNamespaced, ListenerOptions};
use interprocess::TryClone;
// 命名管道服务:按行读写 JSON,Windows 上映射成 \\.\pipe\<name>。
// 启动时读配置并把每项来源打进日志;表句柄按名字缓存复用。
// 服务器
/// 服务器状态:配置 + 表句柄缓存 + 写锁
/// 服务器状态:配置 + 表句柄缓存 + 写锁 + 数据字典
struct Server {
cfg: Config,
tables: Mutex<HashMap<String, Arc<Mutex<HeapTable>>>>,
write_lock: Mutex<()>,
catalog: Mutex<Catalog>,
wal: Wal,
}
impl Server {
@@ -36,23 +35,34 @@ impl Server {
fn new(cfg: Config) -> std::io::Result<Self> {
std::fs::create_dir_all(&cfg.data_dir)?;
let catalog = Catalog::load(cfg.data_dir.join("catalog.json"))?;
let wal = Wal::new(cfg.data_dir.join("wal.log"));
Ok(Server {
cfg,
tables: Mutex::new(HashMap::new()),
write_lock: Mutex::new(()),
catalog: Mutex::new(catalog),
wal,
})
}
// 路径
// 路径
/// 表名 -> 数据文件
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))
/// (表名, 列名) -> 索引文件。
///
/// `id` 是内建索引,沿用老文件名 `表名.idx`(老数据目录不用迁移);
/// 其余列是 `表名.列名.idx`。
fn index_path(&self, table: &str, column: &str) -> PathBuf {
if column == "id" {
self.cfg.data_dir.join(format!("{}.idx", table))
} else {
self.cfg
.data_dir
.join(format!("{}.{}.idx", table, column))
}
}
/// 数据字典文件
@@ -60,13 +70,7 @@ impl Server {
self.cfg.data_dir.join("catalog.json")
}
/// WAL 文件
fn wal_path(&self) -> PathBuf {
self.cfg.data_dir.join("wal.log")
}
// 表句柄
// 表句柄
/// 表文件存在且非空
fn table_exists(&self, table: &str) -> bool {
if self.tables.lock().unwrap().contains_key(table) {
@@ -82,16 +86,40 @@ impl Server {
.unwrap_or(false)
}
/// 一张表该开哪些索引:内建的 `id`,加上数据字典里记的那些
fn index_columns(&self, table: &str) -> Vec<String> {
let mut cols = vec!["id".to_string()];
let c = self.catalog.lock().unwrap();
if let Some(s) = c.describe(table) {
for i in &s.indexes {
if !cols.contains(&i.column) {
cols.push(i.column.clone());
}
}
}
cols
}
/// 拿表句柄(缓存复用)
fn table_handle(&self, table: &str) -> std::io::Result<Arc<Mutex<HeapTable>>> {
if let Some(h) = self.tables.lock().unwrap().get(table) {
return Ok(h.clone());
}
// 先把索引清单取出来,再进 tables 锁(别套着锁去要 catalog)
let columns = self.index_columns(table);
let paths: Vec<(String, PathBuf)> = columns
.iter()
.map(|c| (c.clone(), self.index_path(table, c)))
.collect();
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(
let t = HeapTable::open_with_indexes(
self.table_path(table),
self.index_path(table),
&paths,
self.cfg.page_size,
self.cfg.btree_order,
self.cfg.pool_size,
@@ -112,15 +140,6 @@ impl Server {
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,
@@ -138,32 +157,45 @@ impl Server {
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 row: chusql_storage::protocol::Row = row.clone().into_iter().collect();
WalOp::Insert { table, row } => {
self.with_table(table, |t| t.insert_row(row))?;
let mut c = self.catalog.lock().unwrap();
if c.record_insert(table, &row) {
if 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 rows: Vec<chusql_storage::protocol::Row> =
rows.iter().map(|r| r.clone().into_iter().collect()).collect();
WalOp::InsertBatch { table, rows } => {
self.with_table(table, |t| t.insert_rows(rows))?;
let mut c = self.catalog.lock().unwrap();
if c.record_replace_all(table, &rows) {
let mut changed = false;
for r in rows {
if c.record_insert(table, r) {
changed = true;
}
}
if changed {
c.save(self.catalog_path())?;
}
Ok(())
}
WalOp::DeleteKeys { table, keys } => {
let deleted = self.with_table(table, |t| t.delete_by_keys(keys))?;
let mut c = self.catalog.lock().unwrap();
c.record_delete(table, &deleted);
Ok(())
}
WalOp::ReplaceAll { table, rows } => {
self.with_table(table, |t| t.replace_all(rows))?;
// 整表重写 = 统计与行数从头再数一遍
let mut c = self.catalog.lock().unwrap();
c.rebuild_stats(table, rows);
c.save(self.catalog_path())
}
}
}
@@ -171,9 +203,11 @@ impl Server {
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());
let wal = &self.wal;
wal.clear()?;
// 只截断不落盘:上一条写操作最后已经把 WAL 刷成空的了,
// 这里的截断只是保证下面追加的是唯一一条,跟着的 append 会顺手把它落下去。
wal.truncate()?;
wal.append(op)?;
@@ -185,30 +219,26 @@ impl Server {
}
}
// 这一步必须落盘:WAL 空 = 这条操作已经提交
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())
}
}
let table = match op {
WalOp::Insert { table, .. }
| WalOp::InsertBatch { table, .. }
| WalOp::DeleteKeys { table, .. }
| WalOp::ReplaceAll { table, .. } => table,
};
self.with_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());
let wal = &self.wal;
match wal.read()? {
None => Ok(()),
Some(op) => {
@@ -221,8 +251,103 @@ impl Server {
}
}
/// 启动时把列统计重数一遍。
///
/// 统计是"每列见过哪些值"精确数出来的,只在内存里维护(随 catalog 落盘),
/// 所以重启之后要扫一遍表把它重建出来;否则统计会随重启慢慢失真。
fn rebuild_stats(&self) -> std::io::Result<()> {
let names: Vec<String> = self.catalog.lock().unwrap().table_names();
let mut touched = false;
for table in names {
if !self.table_path(&table).exists() {
continue;
}
let rows = self.with_table(&table, |t| t.scan())?;
let mut c = self.catalog.lock().unwrap();
c.rebuild_stats(&table, &rows);
touched = true;
}
if touched {
let c = self.catalog.lock().unwrap();
c.save(self.catalog_path())?;
}
Ok(())
}
// 请求分发
// 索引维护
/// 给一列建索引
fn create_index(&self, table: &str, column: &str) -> std::io::Result<()> {
if column == "id" {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"id 是内建索引,不需要再建",
));
}
{
let c = self.catalog.lock().unwrap();
let Some(schema) = c.describe(table) else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("unknown table: {}", table),
));
};
if !schema.columns.iter().any(|c| c.name == column) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("unknown column: {}", column),
));
}
if schema.indexes.iter().any(|i| i.column == column) {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("index on column \"{}\" already exists", column),
));
}
}
let path = self.index_path(table, column);
let (page_size, order, pool) = (self.cfg.page_size, self.cfg.btree_order, self.cfg.pool_size);
self.with_table(table, |t| {
t.build_index(column, &path, page_size, order, pool)
})?;
let mut c = self.catalog.lock().unwrap();
c.add_index(table, column)?;
c.save(self.catalog_path())
}
/// 去掉一列的索引(连文件一起删)
fn drop_index(&self, table: &str, column: &str) -> std::io::Result<()> {
if column == "id" {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"id 是内建索引,不能删",
));
}
{
let c = self.catalog.lock().unwrap();
if c.describe(table).is_none() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("unknown table: {}", table),
));
}
}
self.with_table(table, |t| {
t.detach_index(column);
Ok(())
})?;
let mut c = self.catalog.lock().unwrap();
c.remove_index(table, column)?;
c.save(self.catalog_path())?;
drop(c);
let _ = std::fs::remove_file(self.index_path(table, column));
Ok(())
}
// 请求分发
/// 一条请求翻成一条响应
fn handle_request(&self, req: Request) -> Response {
match req {
@@ -247,10 +372,10 @@ impl Server {
}
}
Request::Insert { table, row, key } => {
log_debug!(pipe, "insert table={} key={:?}", table, key);
Request::Insert { table, row } => {
log_debug!(pipe, "insert table={}", table);
let tname = table.clone();
let op = WalOp::Insert { table, row, key };
let op = WalOp::Insert { table, row };
match self.write_via_wal(&op) {
Ok(()) => Response::Ok,
Err(e) => {
@@ -262,9 +387,54 @@ impl Server {
}
}
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)) {
Request::InsertBatch { table, rows } => {
log_debug!(pipe, "insert_batch table={} rows={}", table, rows.len());
let tname = table.clone();
let op = WalOp::InsertBatch { table, rows };
match self.write_via_wal(&op) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "insert_batch {}: {}", tname, e);
Response::Error {
message: format!("insert_batch {}: {}", tname, e),
}
}
}
}
Request::DeleteKeys { table, keys } => {
log_debug!(pipe, "delete_keys table={} keys={}", table, keys.len());
let tname = table.clone();
let op = WalOp::DeleteKeys { table, keys };
match self.write_via_wal(&op) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "delete_keys {}: {}", tname, e);
Response::Error {
message: format!("delete_keys {}: {}", tname, e),
}
}
}
}
Request::LookupByIndex { table, column, key } => {
log_debug!(pipe, "lookup_by_index table={} column={} key={}", table, column, key);
let indexed =
match self.with_existing_table(&table, |t| Ok(t.has_index(&column))) {
Ok(v) => v,
Err(e) => {
log_warn!(pipe, "lookup_by_index {}: {}", table, e);
return Response::Error {
message: format!("lookup_by_index {}: {}", table, e),
};
}
};
if !indexed {
// 没有这个索引:告诉查询层"这条我帮不上忙",让它退回全表扫描。
// 回 NoIndex 而不是空结果,是因为"没有索引"和"没查到"必须分开。
return Response::NoIndex;
}
match self.with_table(&table, |t| t.get_by_column_key(&column, key)) {
Ok(Some(row)) => Response::Rows { rows: vec![row] },
Ok(None) => Response::Rows { rows: vec![] },
Err(e) => {
@@ -314,6 +484,8 @@ impl Server {
table: name.to_string(),
columns: ts.columns.clone(),
row_count: ts.row_count,
indexes: index_wires(ts),
stats: stat_wires(ts),
})
.collect();
Response::Catalog { schemas }
@@ -322,9 +494,12 @@ impl Server {
Request::DescribeTable { table } => {
log_debug!(pipe, "describe_table table={}", table);
match self.describe_table(&table) {
Ok((cols, count)) => Response::Schema {
Ok((cols, count, indexes, stats)) => Response::Schema {
table,
columns: cols,
row_count: count,
indexes,
stats,
},
Err(e) => {
log_warn!(pipe, "describe_table {}: {}", table, e);
@@ -370,8 +545,12 @@ impl Server {
Request::DropTable { table } => {
log_debug!(pipe, "drop_table table={}", table);
{
let dropped_indexes: Vec<String> = {
let mut c = self.catalog.lock().unwrap();
let idx = c
.describe(&table)
.map(|s| s.indexes.iter().map(|i| i.column.clone()).collect())
.unwrap_or_default();
if let Err(e) = c.drop_table(&table) {
return Response::Error {
message: format!("drop_table {}: {}", table, e),
@@ -382,18 +561,48 @@ impl Server {
message: format!("drop_table {}: catalog save: {}", table, e),
};
}
}
idx
};
self.tables.lock().unwrap().remove(&table);
let _ = std::fs::remove_file(self.table_path(&table));
let _ = std::fs::remove_file(self.index_path(&table));
let _ = std::fs::remove_file(self.index_path(&table, "id"));
for col in dropped_indexes {
let _ = std::fs::remove_file(self.index_path(&table, &col));
}
Response::Ok
}
Request::CreateIndex { table, column } => {
log_debug!(pipe, "create_index table={} column={}", table, column);
match self.create_index(&table, &column) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "create_index {}({}): {}", table, column, e);
Response::Error {
message: format!("create_index {}({}): {}", table, column, e),
}
}
}
}
Request::DropIndex { table, column } => {
log_debug!(pipe, "drop_index table={} column={}", table, column);
match self.drop_index(&table, &column) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(pipe, "drop_index {}({}): {}", table, column, e);
Response::Error {
message: format!("drop_index {}({}): {}", table, column, e),
}
}
}
}
}
}
// 信息查询
// 信息查询
/// 表名:catalog 与目录取并集
fn list_tables(&self) -> std::io::Result<Vec<String>> {
let mut names = BTreeSet::new();
@@ -416,15 +625,23 @@ impl Server {
Ok(names.into_iter().collect())
}
/// 查一张表的 schema
fn describe_table(&self, table: &str) -> std::io::Result<(Vec<SchemaColumn>, u64)> {
/// 查一张表的 schema(列 + 行数 + 索引 + 统计)
fn describe_table(
&self,
table: &str,
) -> std::io::Result<(Vec<SchemaColumn>, u64, Vec<IndexWire>, Vec<ColumnStatWire>)> {
let c = self.catalog.lock().unwrap();
if let Some(schema) = c.describe(table) {
return Ok((schema.columns.clone(), schema.row_count));
return Ok((
schema.columns.clone(),
schema.row_count,
index_wires(schema),
stat_wires(schema),
));
}
drop(c);
if std::fs::metadata(self.table_path(table)).is_ok() {
return Ok((Vec::new(), 0));
return Ok((Vec::new(), 0, Vec::new(), Vec::new()));
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
@@ -433,6 +650,35 @@ impl Server {
}
}
/// 一张表的索引定义 -> 线上格式。
/// `id` 是内建索引(表一开出来就有),数据字典里不记它,但对外要报出来。
fn index_wires(schema: &chusql_storage::catalog::TableSchema) -> Vec<IndexWire> {
let mut out = vec![IndexWire {
column: "id".to_string(),
}];
for i in &schema.indexes {
if i.column != "id" {
out.push(IndexWire {
column: i.column.clone(),
});
}
}
out
}
/// 一张表的列统计 -> 线上格式
fn stat_wires(schema: &chusql_storage::catalog::TableSchema) -> Vec<ColumnStatWire> {
schema
.stats
.iter()
.map(|(name, s)| ColumnStatWire {
name: name.clone(),
distinct: s.distinct,
capped: s.capped,
})
.collect()
}
impl Drop for Server {
fn drop(&mut self) {
if let Ok(c) = self.catalog.lock() {
@@ -441,7 +687,6 @@ impl Drop for Server {
}
}
// 连接
/// 处理一条连接
fn handle(server: &Server, conn: LocalSocketStream) -> std::io::Result<()> {
@@ -471,7 +716,6 @@ fn handle(server: &Server, conn: LocalSocketStream) -> std::io::Result<()> {
Ok(())
}
// 入口
/// 把配置来源打进日志
fn log_config(loaded: &Loaded) {
@@ -506,6 +750,11 @@ fn main() -> std::io::Result<()> {
return Err(e);
}
if let Err(e) = server.rebuild_stats() {
// 统计重建失败不影响服务:报一声,继续跑(统计会停留在落盘时的值)
log_warn!(core, "stats rebuild skipped: {}", e);
}
let ns_name = server
.cfg
.pipe_name
+35
View File
@@ -430,6 +430,41 @@ impl DiskBTree {
}
}
/// 删掉一个键;键不存在返回 false。
///
/// 这是**惰性删除**:只把叶子里那一对 (key, value) 拿掉,不做合并、也不向兄弟借位。
/// 于是树可能变稀、甚至留下空叶子,但查找依然正确——内部节点的分隔键仍然满足
/// "小于等于它的往左、大于它的往右",空叶子二分找不到东西自然返回 None。
/// 代价是空间不回收(节点删空了也不还页),整表重写(`clear`)时会一起重建。
pub fn delete(&mut self, key: i64) -> io::Result<bool> {
match self.root()? {
None => Ok(false),
Some(r) => self.delete_rec(r, key),
}
}
/// 递归删除:一路走到叶子,删掉那一对
fn delete_rec(&mut self, page_id: PageId, key: i64) -> io::Result<bool> {
let order = self.order;
match self.file.with_page(page_id, |p| read_node(p, order))? {
NodeData::Leaf { mut keys, mut values, next } => match keys.binary_search(&key) {
Err(_) => Ok(false),
Ok(i) => {
keys.remove(i);
values.remove(i);
self.file.update_page(page_id, |p| {
write_node(p, &NodeData::Leaf { keys, values, next }, order)
})?;
Ok(true)
}
},
NodeData::Internal { keys, children } => {
let i = child_index(&keys, key);
self.delete_rec(children[i], key)
}
}
}
/// 按键升序返回全部
pub fn iter_all(&mut self) -> io::Result<Vec<(i64, u64)>> {
let mut out = Vec::new();
+160 -48
View File
@@ -1,5 +1,7 @@
use std::collections::BTreeMap;
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, HashSet};
use std::hash::{Hash, Hasher};
use std::io;
use std::path::Path;
@@ -7,24 +9,52 @@ use serde::{Deserialize, Serialize};
use crate::protocol::{ColumnType, Row, SchemaColumn};
// 数据字典:记每张表有哪些列、什么类型、多少行。
// 数据字典:记每张表有哪些列、什么类型、多少行、有哪些索引,以及列级统计。
//
// 统计只在内存里精确维护(每列见过哪些值的哈希,有上限),随 catalog.json 一起落盘;
// 服务启动时会扫一遍表把统计重建出来,所以重启之后数字仍然是对的。
/// 一列的不同值个数最多精确统计到这里;超过就只报"下界"
const STATS_DISTINCT_CAP: u64 = 4096;
// 表结构
#[derive(Debug, Clone, Serialize, Deserialize)]
/// 一条索引定义:只记"盯的是哪一列"(索引按列命名,一列最多一个)
pub struct IndexSchema {
pub column: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
/// 一张表的 schema
/// 一列的统计
pub struct ColumnStat {
/// 不同值个数(`capped` 为真时是下界)
pub distinct: u64,
/// 不同值太多,已经不再精确统计
#[serde(default)]
pub capped: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
/// 一张表的 schema + 统计
pub struct TableSchema {
pub columns: Vec<SchemaColumn>,
#[serde(default)]
pub row_count: u64,
#[serde(default)]
pub indexes: Vec<IndexSchema>,
#[serde(default)]
pub stats: BTreeMap<String, ColumnStat>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
/// 数据字典:表名到 schema
pub struct Catalog {
tables: BTreeMap<String, TableSchema>,
/// 每列"见过的值"的哈希集合(只在内存里,不落盘;有上限)
#[serde(skip)]
seen: BTreeMap<String, HashSet<u64>>,
}
impl Catalog {
// 读取与保存
/// 从文件读;不存在给空字典
@@ -65,17 +95,7 @@ impl Catalog {
/// 按一行数据补列
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 {
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,
});
}
}
add_missing_columns(entry, row);
}
/// 建表;已存在报错
@@ -91,67 +111,144 @@ impl Catalog {
TableSchema {
columns,
row_count: 0,
indexes: Vec::new(),
stats: BTreeMap::new(),
},
);
Ok(())
}
/// 按多行数据补列
pub fn ensure_columns_from_rows(&mut self, table: &str, rows: &[Row]) {
for r in rows {
self.ensure_columns(table, r);
}
}
/// 记一次插入:补列 + 行数 +1
/// 记一次插入:补列 + 行数 +1 + 更新列统计;返回"列有没有变化"(决定要不要立刻写盘)
pub fn record_insert(&mut self, table: &str, row: &Row) -> bool {
let entry = self.tables.entry(table.to_string()).or_default();
let mut changed = false;
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 });
changed = true;
}
}
entry.row_count += 1;
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_replace_all(&mut self, table: &str, rows: &[Row]) -> bool {
/// 记一次删除:行数减少 + 更新列统计
pub fn record_delete(&mut self, table: &str, rows: &[Row]) {
let entry = self.tables.entry(table.to_string()).or_default();
let mut changed = false;
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 (k, v) in r {
if entry.columns.iter().any(|c| &c.name == k) {
for (name, v) in r {
let stat = entry.stats.entry(name.clone()).or_default();
if stat.capped {
continue;
}
if let Some(ty) = infer_type(v) {
entry.columns.push(SchemaColumn { name: k.clone(), ty });
changed = true;
// 这个值彻底没了才减:集合里删得掉说明没有别的行还用它
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;
}
// 逐行"插入"地数:行数、列、统计一次到位
rows.iter()
.fold(false, |changed, r| self.record_insert(table, r) || changed)
}
/// 列统计:这一列第一次见到这个值就 distinct +1(到上限之后只报下界)
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(); // 到上限就不再精确统计,把内存还回去
}
}
}
entry.row_count = rows.len() as u64;
changed
}
/// 删表
pub fn drop_table(&mut self, table: &str) -> io::Result<()> {
if self.tables.remove(table).is_none() {
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(())
}
// 索引
/// 记下一条索引定义
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.columns.iter().any(|c| &c.name == k) {
continue;
}
if let Some(ty) = infer_type(v) {
entry.columns.push(SchemaColumn {
name: k.clone(),
ty,
});
changed = true;
}
}
changed
}
/// 由 JSON 值猜列类型
fn infer_type(v: &serde_json::Value) -> Option<ColumnType> {
match v {
@@ -161,3 +258,18 @@ fn infer_type(v: &serde_json::Value) -> Option<ColumnType> {
_ => None,
}
}
/// 一个值的哈希(按它的 JSON 文本算;统计"不同值个数"用)
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()
}
+287 -67
View File
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::io;
use std::path::Path;
@@ -6,7 +7,10 @@ use crate::page::PageId;
use crate::page::{Page, PageFile};
use crate::protocol::Row;
// 堆表:行按插入顺序进页,支持全表扫描与按索引取行。
// 堆表:行按插入顺序进页,支持全表扫描、按索引取行、行级删除。
//
// 索引由**表自己维护**:有哪些索引记在数据字典里,插入时按索引的列从行里取值写进去,
// 调用方不需要知道有哪些索引,也不需要自己指定 key。
// 页内布局
/// 页头:slot_count 占 2 字节
@@ -47,6 +51,14 @@ 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,
}
}
// 页内操作
/// 只管一页里的字节
pub struct HeapPage;
@@ -103,6 +115,20 @@ impl HeapPage {
Some(n)
}
/// 删掉一个槽位:只把长度置 0,行字节原地留着(读的时候按"长度 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);
@@ -124,27 +150,30 @@ impl HeapPage {
}
}
// 索引
/// 表上的一个索引:盯着一列,键是这一列的整数值
struct NamedIndex {
column: String,
tree: DiskBTree,
}
// 堆表
/// 一个文件 = 若干页
/// 一个数据文件 = 若干页;外加若干索引文件
pub struct HeapTable {
file: PageFile,
index: Option<DiskBTree>,
indexes: Vec<NamedIndex>,
}
impl HeapTable {
/// 打开表(不带索引)
pub fn open<P: AsRef<Path>>(
path: P,
page_size: usize,
pool_size: usize,
) -> io::Result<Self> {
Ok(HeapTable {
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,
indexes: Vec::new(),
})
}
/// 打开表 + 索引
/// 打开表 + `id` 列索引(`id` 沿用老文件名 `表名.idx`)
pub fn open_indexed<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
index_path: Q,
@@ -152,19 +181,155 @@ impl HeapTable {
btree_order: usize,
pool_size: usize,
) -> io::Result<Self> {
Ok(HeapTable {
let mut t = HeapTable {
file: PageFile::with_options(path, page_size, pool_size)?,
index: Some(DiskBTree::open(index_path, page_size, btree_order, pool_size)?),
})
indexes: Vec::new(),
};
t.attach_index("id", index_path, page_size, btree_order, pool_size)?;
Ok(t)
}
/// 插入一行,不更新索引
pub fn insert(&mut self, row: &Row) -> io::Result<()> {
self.insert_returning_position(row).map(|_| ())
/// 打开表 + 任意组索引:(列名, 索引文件路径)
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 {
file: PageFile::with_options(path, page_size, pool_size)?,
indexes: Vec::new(),
};
for (col, p) in indexes {
t.attach_index(col, p, page_size, btree_order, pool_size)?;
}
Ok(t)
}
/// 插入并返回行位置
pub fn insert_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
/// 挂上一棵索引树(只打开/创建文件,不填数据)
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)
}
/// 建索引:先扫一遍查重,再建树填数据。
///
/// **要求这一列的整数取值唯一**,不唯一就报错、一个索引文件都不留。
/// 这样"走索引取一行"和"全表扫一遍再筛"必然得到同一个答案;
/// 非唯一索引要等 B+ 树支持重复键(记在项目计划的 P2-② 里)。
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),
));
}
// 第 1 步:扫全表,查重并攒下 (键, 位置)
let mut seen: HashSet<i64> = HashSet::new();
let mut entries: Vec<(i64, u64)> = Vec::new();
for (page_id, slot, r) in self.scan_with_positions()? {
let Some(k) = row_int(&r, column) else {
continue; // 这一列不是整数的行不进索引
};
if !seen.insert(k) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"column \"{}\" has duplicate value {} — 暂不支持非唯一索引",
column, k
),
));
}
entries.push((k, pack_position(page_id, slot)));
}
// 第 2 步:建树
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(())
}
/// 摘掉一个索引(不删文件)
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 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, i64)> = Vec::new();
for (i, idx) in self.indexes.iter_mut().enumerate() {
let Some(k) = row_int(row, &idx.column) else {
continue; // 这一列不是整数(或没这一列):这行不进这个索引
};
if idx.tree.get(k)?.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("duplicate key {} on indexed column \"{}\"", k, idx.column),
));
}
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))
}
/// 批量插入(只写缓冲池,落盘由调用方 `flush` 一次完成)
pub fn insert_rows(&mut self, rows: &[Row]) -> io::Result<()> {
for r in rows {
self.insert_row(r)?;
}
Ok(())
}
/// 插入一行,只进堆表、不碰索引
fn insert_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
let tuple = serde_json::to_vec(row).map_err(io::Error::other)?;
if HEADER_SIZE + SLOT_SIZE + tuple.len() > self.file.page_size() {
@@ -193,23 +358,18 @@ impl HeapTable {
Ok((page_id, slot))
}
/// 插入一行并写索引
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 {
idx.insert(key, pack_position(page_id, slot))?;
}
Ok(())
/// 按 `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_key(&mut self, key: i64) -> io::Result<Option<Row>> {
let pos = match &mut self.index {
None => return Ok(None),
Some(idx) => match idx.get(key)? {
None => return Ok(None),
Some(p) => p,
},
/// 按某一列的索引取一行
pub fn get_by_column_key(&mut self, column: &str, key: i64) -> io::Result<Option<Row>> {
let Some(idx) = self.indexes.iter_mut().find(|i| i.column == column) else {
return Ok(None);
};
let Some(pos) = idx.tree.get(key)? else {
return Ok(None);
};
let (page_id, slot) = unpack_position(pos);
self.read_at(page_id, slot)
@@ -227,47 +387,115 @@ impl HeapTable {
}
}
/// 全表扫描
pub fn scan(&mut self) -> io::Result<Vec<Row>> {
let n = self.file.num_pages()?;
let mut rows = Vec::new();
for i in 0..n {
let tuples = self.file.with_page(i, |p| {
let mut out = Vec::new();
for slot in HeapPage::iter_slots(p) {
if let Some(bytes) = HeapPage::get_tuple(p, slot) {
out.push(bytes);
}
/// 按位置删一行:槽位长度置 0,从所有索引里摘掉,并把这一行交出来
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 {
for idx in &mut self.indexes {
if let Some(k) = row_int(r, &idx.column) {
idx.tree.delete(k)?;
}
out
})?;
for bytes in tuples {
let row: Row = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
rows.push(row);
}
}
Ok(rows)
Ok(row)
}
/// 整表改写并重建索引
/// 按 `id` 批量删行,返回被删掉的那些行(数据字典要用它更新统计)。
///
/// 定位靠 `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(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 let Some(k) = row_int(&r, "id") {
if 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>> {
Ok(self
.scan_with_positions()?
.into_iter()
.map(|(_, _, r)| r)
.collect())
}
/// 全表扫描,同时给出每行的位置(建索引用)
pub fn scan_with_positions(&mut self) -> 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: Row = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
out.push((i, slot, row));
}
}
Ok(out)
}
/// 整表改写并重建所有索引
pub fn replace_all(&mut self, rows: &[Row]) -> io::Result<()> {
self.file.truncate()?;
if let Some(idx) = &mut self.index {
idx.clear()?;
for idx in &mut self.indexes {
idx.tree.clear()?;
}
for r in rows {
let (page_id, slot) = self.insert_returning_position(r)?;
if let (Some(idx), Some(key)) = (&mut self.index, row_key(r)) {
idx.insert(key, pack_position(page_id, slot))?;
let pos = pack_position(page_id, slot);
for idx in &mut self.indexes {
if let Some(k) = row_int(r, &idx.column) {
idx.tree.insert(k, pos)?;
}
}
}
Ok(())
}
/// 把脏页写回
/// 把脏页写回(索引也一起)
pub fn flush(&mut self) -> io::Result<()> {
if let Some(idx) = &mut self.index {
idx.flush()?;
for idx in &mut self.indexes {
idx.tree.flush()?;
}
self.file.flush()
}
@@ -282,11 +510,3 @@ impl HeapTable {
/// 命中率
pub fn hit_rate(&self) -> f64 { self.file.hit_rate() }
}
/// 取行里的 id 作索引键
fn row_key(row: &Row) -> Option<i64> {
match row.get("id") {
Some(serde_json::Value::Number(n)) => n.as_i64(),
_ => None,
}
}
+75 -9
View File
@@ -22,42 +22,104 @@ pub struct SchemaColumn {
pub ty: ColumnType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
/// 一列的统计(随数据字典一起走)
pub struct ColumnStatWire {
pub name: String,
/// 不同值个数;`capped` 为真时它只是下界
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")]
/// 请求:method 字段决定类型
pub enum Request {
Ping,
Scan { table: String },
Scan {
table: String,
},
/// 插一行;要不要写索引由表上的索引决定(调用方不用管)
Insert {
table: String,
row: Row,
#[serde(default)]
key: Option<i64>,
},
LookupByIndex { table: String, key: i64 },
ReplaceAll { table: String, rows: Vec<Row> },
/// 一批行一起插:一次 WAL、一次 fsync
InsertBatch {
table: String,
rows: Vec<Row>,
},
/// 按 id 批量删行
DeleteKeys {
table: String,
keys: Vec<i64>,
},
/// 按某一列的索引取一行(列名默认 "id",兼容老客户端)
LookupByIndex {
table: String,
#[serde(default = "default_id_column")]
column: String,
key: i64,
},
ReplaceAll {
table: String,
rows: Vec<Row>,
},
ListTables,
DescribeTable { table: String },
DescribeTable {
table: String,
},
CreateTable {
table: String,
columns: Vec<SchemaColumn>,
},
DropTable { table: String },
ListCatalog
DropTable {
table: String,
},
ListCatalog,
/// 给某一列建索引(要求这一列整数取值唯一)
CreateIndex {
table: String,
column: String,
},
/// 去掉某一列的索引
DropIndex {
table: String,
column: String,
},
}
/// 没写 column 时按 `id` 算
fn default_id_column() -> String {
"id".to_string()
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "lowercase")]
#[serde(tag = "status", rename_all = "snake_case")]
/// 响应:status 字段决定类型
pub enum Response {
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> },
@@ -69,4 +131,8 @@ pub struct TableSchemaWire {
pub columns: Vec<SchemaColumn>,
#[serde(default)]
pub row_count: u64,
#[serde(default)]
pub indexes: Vec<IndexWire>,
#[serde(default)]
pub stats: Vec<ColumnStatWire>,
}
+81 -21
View File
@@ -1,7 +1,8 @@
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
@@ -11,6 +12,8 @@ use crate::protocol::Row;
const OP_INSERT: u8 = 1;
const OP_REPLACE_ALL: u8 = 2;
const OP_INSERT_BATCH: u8 = 3;
const OP_DELETE_KEYS: u8 = 4;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
/// 一条待写入的操作
@@ -18,7 +21,16 @@ pub enum WalOp {
Insert {
table: String,
row: Row,
key: Option<i64>,
},
/// 一批行:一次日志、一次 fsync 就能全部落盘
InsertBatch {
table: String,
rows: Vec<Row>,
},
/// 按 id 批量删行(重放是幂等的:再删一次等于什么都没删)
DeleteKeys {
table: String,
keys: Vec<i64>,
},
ReplaceAll {
table: String,
@@ -27,8 +39,13 @@ pub enum WalOp {
}
/// WAL 文件
///
/// 句柄只开一次并一直留着:写入路径是每条操作一次
/// `truncate -> append -> truncate`,每次都重新 open/close 文件要多花
/// 一倍的时间(见 benchmark/chusql-storage)。
pub struct Wal {
path: PathBuf,
handle: Mutex<Option<File>>,
}
impl Wal {
@@ -37,6 +54,7 @@ impl Wal {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Wal {
path: path.as_ref().to_path_buf(),
handle: Mutex::new(None),
}
}
@@ -45,26 +63,43 @@ impl Wal {
&self.path
}
/// 清空 WAL
pub fn clear(&self) -> io::Result<()> {
if self.path.exists() {
let f = OpenOptions::new().write(true).open(&self.path)?;
f.set_len(0)?;
f.sync_all()?;
/// 拿常驻句柄(没有就开一个)
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)?,
);
}
Ok(())
f(slot.as_mut().expect("just opened"))
}
/// 截断到空并落盘
pub fn clear(&self) -> io::Result<()> {
self.with_handle(|f| {
truncate(f)?;
f.sync_all()
})
}
/// 只截断、不落盘:紧跟其后的 `append` 会 fsync,
/// 那一次会把截断一起落下去,所以这里不必单独再刷一遍。
pub fn truncate(&self) -> io::Result<()> {
self.with_handle(truncate)
}
/// 追加一条并 fsync
pub fn append(&self, op: &WalOp) -> io::Result<()> {
let bytes = encode(op)?;
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
f.write_all(&bytes)?;
f.sync_data()?;
Ok(())
self.with_handle(|f| {
f.write_all(&bytes)?;
f.sync_data()
})
}
/// 读当前 WAL;空则 None
@@ -88,14 +123,29 @@ impl Wal {
}
}
/// 清空文件并回到起点(句柄不是 append 模式,位置要自己摆正)
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::Insert { table, row, key } => {
let p = serde_json::to_vec(&(row, key)).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)
@@ -145,9 +195,19 @@ fn decode(buf: &[u8]) -> Result<Option<WalOp>, String> {
let op = match op_type {
OP_INSERT => {
let (row, key): (Row, Option<i64>) = serde_json::from_slice(payload)
.map_err(|e| format!("bad insert payload: {}", e))?;
WalOp::Insert { table, row, key }
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)
+60
View File
@@ -153,3 +153,63 @@ 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());
}
/// 删掉键之后查不到,别人不受影响
#[test]
fn delete_removes_only_that_key() {
let dir = tempfile::tempdir().unwrap();
let mut t = DiskBTree::open(
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in 0..200 {
t.insert(i, i as u64 + 1).unwrap();
}
assert!(t.delete(50).unwrap(), "删一个存在的键应该返回 true");
assert_eq!(t.get(50).unwrap(), None);
assert_eq!(t.get(49).unwrap(), Some(50));
assert_eq!(t.get(51).unwrap(), Some(52));
// 再删一次:它已经不在了
assert!(!t.delete(50).unwrap());
// 遍历里也不能再有它
let all = t.iter_all().unwrap();
assert_eq!(all.len(), 199);
assert!(all.iter().all(|(k, _)| *k != 50));
}
/// 删过之后树还能继续插(惰性删除会留下稀疏节点,但不能影响正确性)
#[test]
fn insert_after_delete_still_works() {
let dir = tempfile::tempdir().unwrap();
let mut t = DiskBTree::open(
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in 0..300 {
t.insert(i, i as u64).unwrap();
}
// 把偶数键全删掉
for i in (0..300).step_by(2) {
assert!(t.delete(i).unwrap());
}
// 再插一批新的
for i in 1000..1100 {
t.insert(i, i as u64).unwrap();
}
for i in 0..300 {
let want = if i % 2 == 0 { None } else { Some(i as u64) };
assert_eq!(t.get(i).unwrap(), want, "key {}", i);
}
for i in 1000..1100 {
assert_eq!(t.get(i).unwrap(), Some(i as u64));
}
}
+157 -10
View File
@@ -21,7 +21,7 @@ fn insert_and_scan_one_row() {
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
t.insert_row(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
.unwrap();
let rows = t.scan().unwrap();
@@ -38,7 +38,7 @@ fn insert_many_rows_cross_pages() {
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();
t.insert_row(&row(&[("id", json!(i))])).unwrap();
}
let rows = t.scan().unwrap();
@@ -56,7 +56,7 @@ fn large_row_still_fits() {
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();
t.insert_row(&row(&[("data", json!(big.clone()))])).unwrap();
let rows = t.scan().unwrap();
assert_eq!(rows.len(), 1);
@@ -71,7 +71,7 @@ fn row_too_large_errors() {
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))]));
let err = t.insert_row(&row(&[("data", json!(huge))]));
assert!(err.is_err());
}
@@ -83,8 +83,8 @@ fn reopen_and_scan() {
{
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();
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();
@@ -102,12 +102,12 @@ fn custom_page_size() {
let mut t = HeapTable::open(&path, 512, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
t.insert(&row(&[("id", json!(i))])).unwrap();
t.insert_row(&row(&[("id", json!(i))])).unwrap();
}
assert_eq!(t.scan().unwrap().len(), 50);
let big = "x".repeat(600);
assert!(t.insert(&row(&[("data", json!(big))])).is_err());
assert!(t.insert_row(&row(&[("data", json!(big))])).is_err());
}
/// 整表改写后索引重建
@@ -123,9 +123,9 @@ fn replace_all_rebuilds_index() {
)
.unwrap();
t.insert_keyed(&row(&[("id", json!(1)), ("name", json!("Alice"))]), 1)
t.insert_row(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
.unwrap();
t.insert_keyed(&row(&[("id", json!(2)), ("name", json!("Bob"))]), 2)
t.insert_row(&row(&[("id", json!(2)), ("name", json!("Bob"))]))
.unwrap();
assert!(t.get_by_key(1).unwrap().is_some());
@@ -141,3 +141,150 @@ fn replace_all_rebuilds_index() {
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_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, "只有 2、4 真的删掉了");
let ids: Vec<i64> = t
.scan()
.unwrap()
.iter()
.map(|r| r["id"].as_i64().unwrap())
.collect();
assert_eq!(ids, vec![1, 3, 5], "剩下的行要保持原顺序");
assert!(t.get_by_key(2).unwrap().is_none(), "索引条目要跟着删");
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_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_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_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());
// code 已经有索引,重复的 code 插不进去(索引列是唯一的)
assert!(t.insert_row(&row(&[("id", json!(4)), ("code", json!(7001))])).is_err());
let mut t2 = HeapTable::open(&dir.path().join("d.db"), DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t2.insert_rows(&[
row(&[("id", json!(1)), ("age", json!(30))]),
row(&[("id", json!(2)), ("age", json!(30))]),
])
.unwrap();
let err = t2.build_index(
"age",
&dir.path().join("d.age.idx"),
DEFAULT_PAGE_SIZE,
chusql_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
);
assert!(err.is_err(), "重复值不给建索引");
assert!(!t2.has_index("age"), "失败之后不留半成品");
}
/// 同一个 id 插两次要报错
#[test]
fn duplicate_key_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let mut t = HeapTable::open_indexed(
dir.path().join("t.db"),
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
chusql_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
t.insert_row(&row(&[("id", json!(1))])).unwrap();
let r = t.insert_row(&row(&[("id", json!(1))]));
assert!(r.is_err());
assert_eq!(t.scan().unwrap().len(), 1, "失败的那一行不应该进堆表");
}
+163 -9
View File
@@ -152,17 +152,17 @@ fn replace_all_replaces_rows() {
assert!(!r.contains("Alice"), "old row should be gone: {}", r);
}
/// 带 key 插入后能按下标查
/// 按 id 查(列名走默认值 `id`,老客户端不用改)
#[test]
fn insert_with_key_then_lookup() {
fn insert_then_lookup_by_index() {
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":{}}}"#,
id, name, id
r#"{{"method":"insert","table":"idx_users","row":{{"id":{},"name":"{}"}}}}"#,
id, name
);
let r = send(&mut c, &req);
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
@@ -175,16 +175,170 @@ fn insert_with_key_then_lookup() {
assert!(r.contains(r#""rows":[]"#), "miss: {}", r);
}
/// 不带 key 不建索引
/// 插入时索引由表自己维护:没给 key,id 也会进索引
#[test]
fn insert_without_key_does_not_index() {
fn insert_indexes_the_id_column_automatically() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
send(&mut c, r#"{"method":"insert","table":"nokey","row":{"id":1}}"#);
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"nokey","key":1}"#);
assert!(r.contains(r#""rows":[]"#), "got: {}", r);
send(&mut c, r#"{"method":"insert","table":"auto_idx","row":{"id":7,"name":"Zoe"}}"#);
let r = send(
&mut c,
r#"{"method":"lookup_by_index","table":"auto_idx","column":"id","key":7}"#,
);
assert!(r.contains("Zoe"), "got: {}", r);
}
/// 同一个 id 插两次要报错(索引列是唯一的)
#[test]
fn duplicate_id_is_rejected() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let req = r#"{"method":"insert","table":"dup_id","row":{"id":1,"name":"A"}}"#;
assert!(send(&mut c, req).contains(r#""status":"ok""#));
let r = send(&mut c, r#"{"method":"insert","table":"dup_id","row":{"id":1,"name":"B"}}"#);
assert!(r.contains(r#""status":"error""#), "got: {}", r);
assert!(r.contains("duplicate key"), "got: {}", r);
}
/// 批量插入一次全进
#[test]
fn insert_batch_writes_all_rows() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let r = send(
&mut c,
r#"{"method":"insert_batch","table":"batch_t","rows":[{"id":1,"name":"A"},{"id":2,"name":"B"},{"id":3,"name":"C"}]}"#,
);
assert!(r.contains(r#""status":"ok""#), "batch: {}", r);
let r = send(&mut c, r#"{"method":"scan","table":"batch_t"}"#);
assert!(r.contains("A") && r.contains("B") && r.contains("C"), "scan: {}", r);
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"batch_t","key":2}"#);
assert!(r.contains("B"), "index after batch: {}", r);
}
/// 行级删除:行没了、索引条目也没了、别人不受影响
#[test]
fn delete_keys_removes_row_and_index_entry() {
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":"del_t","row":{{"id":{},"name":"{}"}}}}"#,
id, name
);
send(&mut c, &req);
}
let r = send(&mut c, r#"{"method":"delete_keys","table":"del_t","keys":[2]}"#);
assert!(r.contains(r#""status":"ok""#), "delete: {}", r);
let r = send(&mut c, r#"{"method":"scan","table":"del_t"}"#);
assert!(!r.contains("Bob"), "row should be gone: {}", r);
assert!(r.contains("Alice") && r.contains("Carol"), "others stay: {}", r);
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"del_t","key":2}"#);
assert!(r.contains(r#""rows":[]"#), "index entry should be gone: {}", r);
let r = send(&mut c, r#"{"method":"insert","table":"del_t","row":{"id":2,"name":"Bob2"}}"#);
assert!(r.contains(r#""status":"ok""#), "id 2 应该能再插进来: {}", r);
}
/// 给第二列建索引后能按那一列查
#[test]
fn create_index_on_secondary_column() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
for (id, code) in [(1, 5001), (2, 5002), (3, 5003)] {
let req = format!(
r#"{{"method":"insert","table":"sec_t","row":{{"id":{},"code":{}}}}}"#,
id, code
);
send(&mut c, &req);
}
// 没建之前:明确回 no_index,而不是回空结果
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5002}"#);
assert!(r.contains(r#""status":"no_index""#), "got: {}", r);
let r = send(&mut c, r#"{"method":"create_index","table":"sec_t","column":"code"}"#);
assert!(r.contains(r#""status":"ok""#), "create_index: {}", r);
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5002}"#);
assert!(r.contains(r#""id":2"#), "lookup by code: {}", r);
// 建完索引之后新插入的行也要进索引
send(&mut c, r#"{"method":"insert","table":"sec_t","row":{"id":4,"code":5004}}"#);
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5004}"#);
assert!(r.contains(r#""id":4"#), "lookup new row: {}", r);
// schema 里能看到这条索引
let r = send(&mut c, r#"{"method":"describe_table","table":"sec_t"}"#);
assert!(r.contains(r#""column":"code""#), "describe: {}", r);
// 删掉索引后回到 no_index
let r = send(&mut c, r#"{"method":"drop_index","table":"sec_t","column":"code"}"#);
assert!(r.contains(r#""status":"ok""#), "drop_index: {}", r);
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"sec_t","column":"code","key":5002}"#);
assert!(r.contains(r#""status":"no_index""#), "after drop: {}", r);
}
/// 有重复值的列不给建索引(不然"走索引"和"全表扫"会给出不同答案)
#[test]
fn create_index_rejects_duplicate_values() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
send(&mut c, r#"{"method":"insert","table":"dup_t","row":{"id":1,"age":30}}"#);
send(&mut c, r#"{"method":"insert","table":"dup_t","row":{"id":2,"age":30}}"#);
let r = send(&mut c, r#"{"method":"create_index","table":"dup_t","column":"age"}"#);
assert!(r.contains(r#""status":"error""#), "got: {}", r);
assert!(r.contains("duplicate value"), "got: {}", r);
// 失败之后不留半成品:查这一列仍然是 no_index
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"dup_t","column":"age","key":30}"#);
assert!(r.contains(r#""status":"no_index""#), "got: {}", r);
}
/// 数据字典里的统计:不同值个数
#[test]
fn stats_report_distinct_values() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
for (id, age) in [(1, 30), (2, 30), (3, 41)] {
let req = format!(
r#"{{"method":"insert","table":"stat_t","row":{{"id":{},"age":{}}}}}"#,
id, age
);
send(&mut c, &req);
}
let r = send(&mut c, r#"{"method":"describe_table","table":"stat_t"}"#);
assert!(r.contains(r#""row_count":3"#), "row_count: {}", r);
assert!(r.contains(r#""name":"age","distinct":2"#), "age 有 2 个不同值: {}", r);
assert!(r.contains(r#""name":"id","distinct":3"#), "id 有 3 个不同值: {}", r);
// 删掉一行,统计跟着掉
send(&mut c, r#"{"method":"delete_keys","table":"stat_t","keys":[1]}"#);
let r = send(&mut c, r#"{"method":"describe_table","table":"stat_t"}"#);
assert!(r.contains(r#""row_count":2"#), "after delete: {}", r);
assert!(r.contains(r#""name":"age","distinct":1"#), "after delete: {}", r);
}
/// 重开服务后索引仍在
+1 -4
View File
@@ -22,7 +22,6 @@ fn append_and_read_insert() {
let op = WalOp::Insert {
table: "users".into(),
row: row(&[("id", json!(1)), ("name", json!("Alice"))]),
key: Some(1),
};
wal.append(&op).unwrap();
@@ -77,7 +76,6 @@ fn clear_truncates() {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(1))]),
key: None,
})
.unwrap();
assert!(wal.read().unwrap().is_some());
@@ -97,7 +95,6 @@ fn truncated_tail_is_ignored() {
wal.append(&WalOp::Insert {
table: "users".into(),
row: row(&[("id", json!(1)), ("name", json!("Alice"))]),
key: Some(1),
})
.unwrap();
@@ -116,9 +113,9 @@ fn overwrite_on_second_append_is_visible() {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(1))]),
key: None,
})
.unwrap();
let first = wal.read().unwrap().unwrap();
assert!(matches!(first, WalOp::Insert { .. }));
}