准备可扩展类型系统工作,更合理的JOIN查询优化算法,并行查询
This commit is contained in:
1 parent
9758439b82
commit
274b7fcdcc
75 files changed
+3828
-1789
No files matched your search
@@ -73,14 +73,25 @@ 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(),
|
||||
/// 数值里取整数,整值浮点折成整数
|
||||
pub(crate) fn scalar_int(v: &serde_json::Value) -> Option<i64> {
|
||||
match v {
|
||||
serde_json::Value::Number(n) => n.as_i64().or_else(|| {
|
||||
let f = n.as_f64()?;
|
||||
if f.fract() != 0.0 || f.abs() > 9_007_199_254_740_992.0 {
|
||||
return None;
|
||||
}
|
||||
Some(f as i64)
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 取行里某一列的整数值(不是整数就没有)
|
||||
fn row_int(row: &Row, column: &str) -> Option<i64> {
|
||||
row.get(column).and_then(scalar_int)
|
||||
}
|
||||
|
||||
/// i64 键 → 8 字节大端(保序,负数也在前)
|
||||
fn key_of(value: i64) -> [u8; 8] {
|
||||
(value as u64 ^ (1u64 << 63)).to_be_bytes()
|
||||
@@ -89,7 +100,7 @@ fn key_of(value: i64) -> [u8; 8] {
|
||||
/// JSON 值 → 索引键字节(与写索引时同一套编码)
|
||||
fn value_key(v: &serde_json::Value) -> Option<Vec<u8>> {
|
||||
match v {
|
||||
serde_json::Value::Number(n) => n.as_i64().map(|k| key_of(k).to_vec()),
|
||||
serde_json::Value::Number(_) => scalar_int(v).map(|k| key_of(k).to_vec()),
|
||||
serde_json::Value::String(s) => Some(s.as_bytes().to_vec()),
|
||||
serde_json::Value::Bool(b) => Some(vec![u8::from(*b)]),
|
||||
_ => None,
|
||||
@@ -613,11 +624,41 @@ impl HeapTable {
|
||||
self.scan_projected_with_positions(None)
|
||||
}
|
||||
|
||||
/// 分片扫描,只取 [from, to) 页区间内的行
|
||||
pub fn scan_columns_pages(
|
||||
&mut self,
|
||||
columns: Option<&[String]>,
|
||||
from: PageId,
|
||||
to: PageId,
|
||||
) -> io::Result<Vec<Row>> {
|
||||
Ok(self
|
||||
.scan_pages_with_positions(columns, from, to)?
|
||||
.into_iter()
|
||||
.map(|(_, _, r)| r)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// 当前页数
|
||||
pub fn num_pages(&mut self) -> io::Result<u64> {
|
||||
self.file.num_pages()
|
||||
}
|
||||
|
||||
/// 全表扫描,带位置与列投影
|
||||
fn scan_projected_with_positions(&mut self, columns: Option<&[String]>) -> io::Result<Vec<(PageId, u16, Row)>> {
|
||||
let n = self.file.num_pages()?;
|
||||
self.scan_pages_with_positions(columns, 0, n)
|
||||
}
|
||||
|
||||
/// 扫描 [from, to) 页区间,带位置与列投影
|
||||
fn scan_pages_with_positions(
|
||||
&mut self,
|
||||
columns: Option<&[String]>,
|
||||
from: PageId,
|
||||
to: PageId,
|
||||
) -> io::Result<Vec<(PageId, u16, Row)>> {
|
||||
let n = self.file.num_pages()?;
|
||||
let mut out = Vec::new();
|
||||
for i in 0..n {
|
||||
for i in from..to.min(n) {
|
||||
let tuples = self.file.with_page(i, |p| {
|
||||
let mut v = Vec::new();
|
||||
for slot in HeapPage::iter_slots(p) {
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
use crate::protocol::SchemaColumn;
|
||||
|
||||
// 读取 scripts/init.sql,只认 CREATE TABLE 与 CREATE INDEX。
|
||||
// 系统库的 schema 由这份脚本声明,进程按它建系统表。
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InitColumn {
|
||||
pub name: String,
|
||||
pub ty: String,
|
||||
pub nullable: bool,
|
||||
pub auto_increment: bool,
|
||||
pub primary_key: bool,
|
||||
pub unique: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InitTable {
|
||||
pub name: String,
|
||||
pub columns: Vec<InitColumn>,
|
||||
pub indexes: Vec<String>,
|
||||
}
|
||||
|
||||
impl InitColumn {
|
||||
/// 转成数据字典里的列
|
||||
pub fn to_schema(&self) -> SchemaColumn {
|
||||
SchemaColumn {
|
||||
name: self.name.clone(),
|
||||
ty: self.ty.clone(),
|
||||
nullable: self.nullable,
|
||||
default: None,
|
||||
auto_increment: self.auto_increment,
|
||||
primary_key: self.primary_key,
|
||||
unique: self.unique,
|
||||
check: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析整份脚本;不认识的语句直接报错,避免悄悄漏建表
|
||||
pub fn parse(text: &str) -> Result<Vec<InitTable>, String> {
|
||||
let mut tables: Vec<InitTable> = Vec::new();
|
||||
for statement in statements(text) {
|
||||
let words = statement.split_whitespace().collect::<Vec<_>>();
|
||||
let head = |index: usize| words.get(index).map(|word| word.to_ascii_uppercase());
|
||||
match (head(0).as_deref(), head(1).as_deref()) {
|
||||
(Some("CREATE"), Some("TABLE")) => {
|
||||
let name = clean_name(words.get(2).ok_or("CREATE TABLE needs a name")?);
|
||||
let body = statement.split_once('(').ok_or("CREATE TABLE needs a column list")?.1;
|
||||
let inner = body.rsplit_once(')').map(|(inner, _)| inner).unwrap_or(body);
|
||||
tables.push(InitTable { name, columns: columns(inner)?, indexes: Vec::new() });
|
||||
}
|
||||
(Some("CREATE"), Some("INDEX")) => {
|
||||
let table = clean_name(words.get(2).ok_or("CREATE INDEX needs a table")?);
|
||||
let column = statement
|
||||
.split_once('(')
|
||||
.and_then(|(_, rest)| rest.split_once(')'))
|
||||
.map(|(column, _)| clean_name(column))
|
||||
.ok_or("CREATE INDEX needs a column")?;
|
||||
let entry = tables
|
||||
.iter_mut()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case(&table))
|
||||
.ok_or_else(|| format!("CREATE INDEX on unknown table: {table}"))?;
|
||||
entry.indexes.push(column);
|
||||
}
|
||||
_ => return Err(format!("unsupported statement: {}", statement.trim())),
|
||||
}
|
||||
}
|
||||
Ok(tables)
|
||||
}
|
||||
|
||||
/// 解析表定义里的列清单
|
||||
fn columns(body: &str) -> Result<Vec<InitColumn>, String> {
|
||||
let mut out = Vec::new();
|
||||
for part in split_top_level(body) {
|
||||
let words = part.split_whitespace().collect::<Vec<_>>();
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let keyword = words[0].to_ascii_uppercase();
|
||||
if matches!(keyword.as_str(), "PRIMARY" | "UNIQUE" | "KEY" | "CONSTRAINT" | "INDEX" | "CHECK" | "FOREIGN") {
|
||||
return Err(format!("table-level constraint is not supported: {}", part.trim()));
|
||||
}
|
||||
let name = clean_name(words[0]);
|
||||
let ty = words
|
||||
.get(1)
|
||||
.ok_or_else(|| format!("column {name} needs a type"))?
|
||||
.to_string();
|
||||
let upper = part.to_ascii_uppercase();
|
||||
out.push(InitColumn {
|
||||
name,
|
||||
ty,
|
||||
nullable: !upper.contains("NOT NULL"),
|
||||
auto_increment: upper.contains("AUTO_INCREMENT"),
|
||||
primary_key: upper.contains("PRIMARY KEY"),
|
||||
unique: upper.contains("UNIQUE"),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 按顶层的逗号切开(类型里的括号不算)
|
||||
fn split_top_level(body: &str) -> Vec<String> {
|
||||
let mut parts = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut depth = 0;
|
||||
for ch in body.chars() {
|
||||
match ch {
|
||||
'(' => {
|
||||
depth += 1;
|
||||
current.push(ch);
|
||||
}
|
||||
')' => {
|
||||
depth -= 1;
|
||||
current.push(ch);
|
||||
}
|
||||
',' if depth == 0 => {
|
||||
parts.push(std::mem::take(&mut current));
|
||||
}
|
||||
_ => current.push(ch),
|
||||
}
|
||||
}
|
||||
if !current.trim().is_empty() {
|
||||
parts.push(current);
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
/// 去掉反引号与空白
|
||||
fn clean_name(raw: &str) -> String {
|
||||
raw.trim().trim_matches('`').trim_matches('"').trim().to_string()
|
||||
}
|
||||
|
||||
/// 去掉注释后按分号切开
|
||||
fn statements(text: &str) -> Vec<String> {
|
||||
let mut cleaned = String::new();
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '-' && chars.peek() == Some(&'-') {
|
||||
for next in chars.by_ref() {
|
||||
if next == '\n' {
|
||||
cleaned.push('\n');
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ch == '/' && chars.peek() == Some(&'*') {
|
||||
chars.next();
|
||||
while let Some(next) = chars.next() {
|
||||
if next == '*' && chars.peek() == Some(&'/') {
|
||||
chars.next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cleaned.push(ch);
|
||||
}
|
||||
}
|
||||
cleaned
|
||||
.split(';')
|
||||
.map(|statement| statement.trim().to_string())
|
||||
.filter(|statement| !statement.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 仓库里的系统库脚本必须始终能解析
|
||||
const SYSTEM_SCHEMA: &str = include_str!("../../../scripts/init.sql");
|
||||
|
||||
/// 仓库自带脚本必须能解析出账号表
|
||||
#[test]
|
||||
fn parses_the_repository_init_script() {
|
||||
let tables = parse(SYSTEM_SCHEMA).expect("scripts/init.sql must parse");
|
||||
let users = tables
|
||||
.iter()
|
||||
.find(|table| table.name == "__chusql_users")
|
||||
.expect("init.sql must declare the account table");
|
||||
let names: Vec<&str> = users.columns.iter().map(|column| column.name.as_str()).collect();
|
||||
assert_eq!(names, ["id", "user", "password_hash", "registered_at", "last_login_at", "revision"]);
|
||||
assert_eq!(users.columns[0].ty, "int");
|
||||
assert!(users.columns[0].primary_key && users.columns[0].auto_increment && !users.columns[0].nullable);
|
||||
assert_eq!(users.columns[1].ty, "varchar(64)");
|
||||
assert!(users.columns[1].unique);
|
||||
assert_eq!(users.columns[4].ty, "timestamp");
|
||||
assert!(users.columns[4].nullable);
|
||||
assert_eq!(users.indexes, ["user"]);
|
||||
}
|
||||
|
||||
/// 未知语句与表级约束要报错
|
||||
#[test]
|
||||
fn rejects_unknown_statements_and_table_level_constraints() {
|
||||
assert!(parse("DROP TABLE users;").is_err());
|
||||
assert!(parse("CREATE TABLE t (id int, PRIMARY KEY (id));").is_err());
|
||||
assert!(parse("CREATE INDEX missing (user);").is_err());
|
||||
}
|
||||
|
||||
/// 注释与空语句要被跳过
|
||||
#[test]
|
||||
fn ignores_comments_and_blank_statements() {
|
||||
let tables = parse("-- note\nCREATE TABLE t (a int NOT NULL);\n\n/* done */\n").unwrap();
|
||||
assert_eq!(tables.len(), 1);
|
||||
assert_eq!(tables[0].columns[0].name, "a");
|
||||
assert!(!tables[0].columns[0].nullable);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ pub mod catalog;
|
||||
pub mod config;
|
||||
pub mod ffi;
|
||||
pub mod heap;
|
||||
pub mod initsql;
|
||||
pub mod log;
|
||||
pub mod page;
|
||||
pub mod protocol;
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::collections::HashMap;
|
||||
|
||||
pub type Row = HashMap<String, serde_json::Value>;
|
||||
|
||||
pub const USERS_TABLE: &str = "__chusql_users";
|
||||
pub const USERS_TABLE: &str = "__system_users";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Account {
|
||||
@@ -83,12 +83,28 @@ pub enum Request {
|
||||
at: Option<String>,
|
||||
},
|
||||
AccountDrop { user: String },
|
||||
/// 引导系统目录:建系统表,给了 user 就同时补首个管理员
|
||||
BootstrapSystem {
|
||||
#[serde(default)]
|
||||
user: Option<String>,
|
||||
#[serde(default)]
|
||||
password_hash: Option<String>,
|
||||
},
|
||||
SystemStatus,
|
||||
Ping,
|
||||
Scan {
|
||||
table: String,
|
||||
#[serde(default)]
|
||||
columns: Option<Vec<String>>,
|
||||
},
|
||||
/// 分片扫描:第 shard 片只读总页数按 shards 均分后的那一段
|
||||
ScanShard {
|
||||
table: String,
|
||||
#[serde(default)]
|
||||
columns: Option<Vec<String>>,
|
||||
shard: u32,
|
||||
shards: u32,
|
||||
},
|
||||
Insert {
|
||||
table: String,
|
||||
row: Row,
|
||||
@@ -174,7 +190,8 @@ impl Request {
|
||||
/// 普通表操作的统一访问边界
|
||||
pub fn table(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Scan { table, .. } | Self::Insert { table, .. }
|
||||
Self::Scan { table, .. } | Self::ScanShard { table, .. }
|
||||
| Self::Insert { table, .. }
|
||||
| Self::InsertBatch { table, .. } | Self::DeleteKeys { table, .. }
|
||||
| Self::LookupByIndex { table, .. } | Self::ReplaceAll { table, .. }
|
||||
| Self::RangeByIndex { table, .. }
|
||||
@@ -185,14 +202,14 @@ impl Request {
|
||||
| Self::ReplaceSchema { table, .. } => Some(table),
|
||||
Self::Ping | Self::ListTables | Self::ListCatalog | Self::AccountsList
|
||||
| Self::AccountCreate { .. } | Self::AccountReset { .. } | Self::AccountLogin { .. }
|
||||
| Self::AccountDrop { .. } => None,
|
||||
| Self::AccountDrop { .. } | Self::BootstrapSystem { .. } | Self::SystemStatus => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 是不是保留系统表名
|
||||
pub fn reserved_table(table: &str) -> bool {
|
||||
table.to_ascii_lowercase().starts_with("__chusql_")
|
||||
table.to_ascii_lowercase().starts_with("__system_")
|
||||
}
|
||||
|
||||
/// 没写 column 时按 `id` 算
|
||||
@@ -219,6 +236,8 @@ pub enum Response {
|
||||
},
|
||||
NoIndex,
|
||||
Ok,
|
||||
/// 系统目录状态
|
||||
System { initialized: bool },
|
||||
Error { message: String },
|
||||
Catalog { schemas: Vec<TableSchemaWire> },
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use crate::catalog::Catalog;
|
||||
use crate::config::{self, Config, Loaded};
|
||||
use crate::heap::HeapTable;
|
||||
use crate::initsql::{self, InitColumn, InitTable};
|
||||
use crate::heap::{scalar_int, HeapTable};
|
||||
use crate::log;
|
||||
use crate::protocol::{
|
||||
reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, SchemaColumn, TableSchemaWire, USERS_TABLE,
|
||||
@@ -21,6 +20,7 @@ struct Server {
|
||||
cfg: Config,
|
||||
tables: Mutex<HashMap<String, Arc<Mutex<HeapTable>>>>,
|
||||
write_lock: Mutex<()>,
|
||||
gate: RwLock<()>,
|
||||
catalog: Mutex<Catalog>,
|
||||
wal: Wal,
|
||||
recovery_required: AtomicBool,
|
||||
@@ -38,6 +38,7 @@ impl Server {
|
||||
cfg,
|
||||
tables: Mutex::new(HashMap::new()),
|
||||
write_lock: Mutex::new(()),
|
||||
gate: RwLock::new(()),
|
||||
catalog: Mutex::new(catalog),
|
||||
wal,
|
||||
recovery_required: AtomicBool::new(false),
|
||||
@@ -127,6 +128,56 @@ impl Server {
|
||||
f(&mut guard)
|
||||
}
|
||||
|
||||
/// 请求带列名时逐个查字典
|
||||
fn check_columns(&self, table: &str, columns: Option<&[String]>) -> std::io::Result<()> {
|
||||
let Some(cols) = columns else {
|
||||
return Ok(());
|
||||
};
|
||||
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
|
||||
let schema = c
|
||||
.describe(table)
|
||||
.ok_or_else(|| std::io::Error::other(format!("unknown table: {table}")))?;
|
||||
for col in cols {
|
||||
if !schema.columns.iter().any(|c| &c.name == col) {
|
||||
return Err(std::io::Error::other(format!("unknown column: {col}")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 分片只读扫描:独立句柄读一段连续页,不经表锁
|
||||
fn scan_shard(
|
||||
&self,
|
||||
table: &str,
|
||||
columns: Option<&[String]>,
|
||||
shard: u32,
|
||||
shards: u32,
|
||||
) -> std::io::Result<Vec<Row>> {
|
||||
if shards == 0 || shard >= shards {
|
||||
return Err(std::io::Error::other("invalid shard range"));
|
||||
}
|
||||
if !self.table_exists(table)? {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("unknown table: {}", table),
|
||||
));
|
||||
}
|
||||
let hidden: HashSet<String> = self
|
||||
.catalog
|
||||
.lock()
|
||||
.map_err(|_| std::io::Error::other("catalog lock poisoned"))?
|
||||
.describe(table)
|
||||
.map(|s| s.dropped_columns.iter().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
let mut t = HeapTable::open(self.table_path(table), self.cfg.page_size, self.cfg.pool_size)?;
|
||||
t.set_hidden_columns(hidden);
|
||||
let n = t.num_pages()?;
|
||||
let chunk = n.div_ceil(u64::from(shards));
|
||||
let from = u64::from(shard).saturating_mul(chunk).min(n);
|
||||
let to = u64::from(shard + 1).saturating_mul(chunk).min(n);
|
||||
t.scan_columns_pages(columns, from, to)
|
||||
}
|
||||
|
||||
/// 表不存在就报错
|
||||
fn with_existing_table<R>(
|
||||
&self,
|
||||
@@ -528,6 +579,88 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 账号表是否已登记为系统表
|
||||
fn account_table_ready(&self) -> std::io::Result<bool> {
|
||||
use std::io::{Error, ErrorKind};
|
||||
let registered = {
|
||||
let c = self.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?;
|
||||
c.describe(USERS_TABLE).map(|schema| schema.system)
|
||||
};
|
||||
match registered {
|
||||
Some(true) => Ok(true),
|
||||
Some(false) => Err(Error::new(ErrorKind::AlreadyExists, "reserved account table collision")),
|
||||
None => {
|
||||
if self.table_path(USERS_TABLE).exists() || self.index_path(USERS_TABLE, "id").exists() {
|
||||
Err(Error::new(ErrorKind::AlreadyExists, "reserved account file collision"))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 系统目录里账号表已登记
|
||||
fn system_initialized(&self) -> bool {
|
||||
self.catalog
|
||||
.lock()
|
||||
.map(|c| c.describe(USERS_TABLE).is_some_and(|schema| schema.system))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 读账号表;表没登记或数据文件缺失都算错
|
||||
fn load_accounts(&self) -> std::io::Result<Vec<Account>> {
|
||||
use std::io::Error;
|
||||
if !self.table_path(USERS_TABLE).exists() {
|
||||
return Err(Error::other("missing account data"));
|
||||
}
|
||||
let mut accounts: Vec<Account> = self
|
||||
.with_existing_table(USERS_TABLE, |t| t.scan())?
|
||||
.iter()
|
||||
.map(decode_account_row)
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
for account in accounts.iter_mut() {
|
||||
normalize_account(account);
|
||||
}
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// 引导系统目录:建账号表,空表才补管理员
|
||||
fn bootstrap_system(&self, user: Option<&str>, password_hash: Option<&str>) -> std::io::Result<Vec<Account>> {
|
||||
use std::io::Error;
|
||||
let _guard = self.write_lock.lock().map_err(|_| Error::other("write lock poisoned"))?;
|
||||
if self.recovery_required.load(Ordering::Acquire) {
|
||||
return Err(Error::other("storage recovery required"));
|
||||
}
|
||||
let initialized = self.account_table_ready()?;
|
||||
let mut accounts = if initialized { self.load_accounts()? } else { Vec::new() };
|
||||
let mut changed = !initialized;
|
||||
if let Some(user) = user {
|
||||
let name = account_name(user)?;
|
||||
// 表里已经有行就不再补人:引导程序只在空表上播种,
|
||||
// 免得有人拿 storage 透传绕过账号接口往里塞一个已知口令的账号
|
||||
if accounts.is_empty() && !accounts.iter().any(|a| a.user == name) {
|
||||
let id = accounts.iter().map(|a| a.id).max().unwrap_or(0)
|
||||
.checked_add(1).ok_or_else(|| Error::other("account id exhausted"))?;
|
||||
accounts.push(new_account(id, &name, password_hash.unwrap_or_default().to_string())?);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return Ok(accounts);
|
||||
}
|
||||
for row in account_rows(&accounts)? {
|
||||
HeapTable::encode_row(&row, self.cfg.page_size)?;
|
||||
}
|
||||
let op = WalOp::Accounts { accounts: accounts.clone() };
|
||||
self.recovery_required.store(true, Ordering::Release);
|
||||
self.wal.truncate()?;
|
||||
self.wal.append(&op)?;
|
||||
self.apply_op(&op)?;
|
||||
self.wal.clear()?;
|
||||
self.recovery_required.store(false, Ordering::Release);
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// 账号读写共享串行持久化边界
|
||||
fn accounts_request(&self, request: Request) -> std::io::Result<Vec<Account>> {
|
||||
use std::io::{Error, ErrorKind};
|
||||
@@ -535,31 +668,10 @@ impl Server {
|
||||
if self.recovery_required.load(Ordering::Acquire) {
|
||||
return Err(Error::other("storage recovery required"));
|
||||
}
|
||||
let initialized = {
|
||||
let c = self.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?;
|
||||
match c.describe(USERS_TABLE) {
|
||||
Some(schema) if schema.system => true,
|
||||
Some(_) => return Err(Error::new(ErrorKind::AlreadyExists, "reserved account table collision")),
|
||||
None => false,
|
||||
}
|
||||
};
|
||||
if !initialized && (self.table_path(USERS_TABLE).exists() || self.index_path(USERS_TABLE, "id").exists()) {
|
||||
return Err(Error::new(ErrorKind::AlreadyExists, "reserved account file collision"));
|
||||
if !self.account_table_ready()? {
|
||||
return Err(Error::other("system catalog is not initialized; run csql-bootstrap"));
|
||||
}
|
||||
let mut accounts: Vec<Account> = if initialized {
|
||||
if !self.table_path(USERS_TABLE).exists() {
|
||||
return Err(Error::other("missing account data"));
|
||||
}
|
||||
let mut loaded: Vec<Account> = self
|
||||
.with_existing_table(USERS_TABLE, |t| t.scan())?
|
||||
.iter()
|
||||
.map(decode_account_row)
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
for account in loaded.iter_mut() {
|
||||
normalize_account(account);
|
||||
}
|
||||
loaded
|
||||
} else { Vec::new() };
|
||||
let mut accounts = self.load_accounts()?;
|
||||
match request {
|
||||
Request::AccountsList => return Ok(accounts),
|
||||
Request::AccountCreate { user, password_hash } => {
|
||||
@@ -609,8 +721,27 @@ impl Server {
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// 一条请求翻成一条响应
|
||||
/// 一条请求翻成一条响应:先过读/写门再分发
|
||||
fn handle_request(&self, req: Request) -> Response {
|
||||
if concurrent_read(&req) {
|
||||
match self.gate.read() {
|
||||
Ok(_read) => self.dispatch(req),
|
||||
Err(_) => Response::Error {
|
||||
message: "storage read gate poisoned".into(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
match self.gate.write() {
|
||||
Ok(_write) => self.dispatch(req),
|
||||
Err(_) => Response::Error {
|
||||
message: "storage write gate poisoned".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 持门后分发一条请求
|
||||
fn dispatch(&self, req: Request) -> Response {
|
||||
let _ddl_guard = if matches!(req, Request::CreateTable { .. } | Request::DropTable { .. }
|
||||
| Request::CreateIndex { .. } | Request::DropIndex { .. }) {
|
||||
match self.write_lock.lock() {
|
||||
@@ -641,6 +772,13 @@ impl Server {
|
||||
Err(e) => Response::Error { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
Request::BootstrapSystem { user, password_hash } => {
|
||||
match self.bootstrap_system(user.as_deref(), password_hash.as_deref()) {
|
||||
Ok(_) => Response::System { initialized: true },
|
||||
Err(e) => Response::Error { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
Request::SystemStatus => Response::System { initialized: self.system_initialized() },
|
||||
Request::Ping => {
|
||||
log_debug!(request, "ping");
|
||||
Response::Pong
|
||||
@@ -650,15 +788,7 @@ impl Server {
|
||||
log_debug!(request, "scan table={}", table);
|
||||
|
||||
let result = (|| {
|
||||
if let Some(cols) = &columns {
|
||||
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
|
||||
let schema = c.describe(&table).ok_or_else(|| std::io::Error::other(format!("unknown table: {table}")))?;
|
||||
for col in cols {
|
||||
if !schema.columns.iter().any(|c| &c.name == col) {
|
||||
return Err(std::io::Error::other(format!("unknown column: {col}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.check_columns(&table, columns.as_deref())?;
|
||||
self.with_existing_table(&table, |t| t.scan_columns(columns.as_deref()))
|
||||
})();
|
||||
|
||||
@@ -673,6 +803,30 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
Request::ScanShard {
|
||||
table,
|
||||
columns,
|
||||
shard,
|
||||
shards,
|
||||
} => {
|
||||
log_debug!(request, "scan_shard table={} shard={}/{}", table, shard, shards);
|
||||
|
||||
let result = (|| {
|
||||
self.check_columns(&table, columns.as_deref())?;
|
||||
self.scan_shard(&table, columns.as_deref(), shard, shards)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(rows) => Response::Rows { rows },
|
||||
Err(e) => {
|
||||
log_warn!(request, "scan {} shard {}/{}: {}", table, shard, shards, e);
|
||||
Response::Error {
|
||||
message: format!("scan {}: {}", table, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Request::Insert { table, row } => {
|
||||
log_debug!(request, "insert table={}", table);
|
||||
let tname = table.clone();
|
||||
@@ -733,7 +887,7 @@ impl Server {
|
||||
if !indexed {
|
||||
return Response::NoIndex;
|
||||
}
|
||||
let found = match key.as_i64() {
|
||||
let found = match scalar_int(&key) {
|
||||
Some(k) => self.with_table(&table, |t| t.get_all_by_column_key(&column, k)),
|
||||
None => match key.as_str() {
|
||||
Some(s) => self.with_table(&table, |t| t.get_all_by_string_key(&column, s)),
|
||||
@@ -1025,18 +1179,31 @@ fn account_rows(accounts: &[Account]) -> std::io::Result<Vec<Row>> {
|
||||
]))).collect()
|
||||
}
|
||||
|
||||
/// 按 init.sql 把系统表写进字典
|
||||
/// 系统库的账号表结构:引导模块按这份定义建表
|
||||
fn users_columns() -> Vec<SchemaColumn> {
|
||||
vec![
|
||||
SchemaColumn { name: "id".into(), ty: "int".into(), nullable: false,
|
||||
auto_increment: true, primary_key: true, ..Default::default() },
|
||||
SchemaColumn { name: "user".into(), ty: "varchar(64)".into(), nullable: false,
|
||||
unique: true, ..Default::default() },
|
||||
SchemaColumn { name: "password_hash".into(), ty: "varchar(256)".into(),
|
||||
nullable: false, ..Default::default() },
|
||||
SchemaColumn { name: "registered_at".into(), ty: "timestamp".into(),
|
||||
nullable: false, ..Default::default() },
|
||||
SchemaColumn { name: "last_login_at".into(), ty: "timestamp".into(),
|
||||
..Default::default() },
|
||||
SchemaColumn { name: "revision".into(), ty: "int".into(), nullable: false,
|
||||
..Default::default() },
|
||||
]
|
||||
}
|
||||
|
||||
/// 把系统表写进字典
|
||||
fn declare_system_schema(catalog: &mut Catalog, rows: &[Row]) -> std::io::Result<()> {
|
||||
for table in system_schema() {
|
||||
let stats: &[Row] = if table.name == USERS_TABLE { rows } else { &[] };
|
||||
catalog.rebuild_stats(&table.name, stats);
|
||||
catalog.set_columns(&table.name, table.columns.iter().map(InitColumn::to_schema).collect())?;
|
||||
catalog.rebuild_stats(&table.name, stats);
|
||||
catalog.mark_system(&table.name)?;
|
||||
for index in &table.indexes {
|
||||
catalog.add_index(&table.name, index)?;
|
||||
}
|
||||
}
|
||||
catalog.rebuild_stats(USERS_TABLE, rows);
|
||||
catalog.set_columns(USERS_TABLE, users_columns())?;
|
||||
catalog.rebuild_stats(USERS_TABLE, rows);
|
||||
catalog.mark_system(USERS_TABLE)?;
|
||||
catalog.add_index(USERS_TABLE, "user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1110,9 +1277,9 @@ fn account_name(user: &str) -> std::io::Result<String> {
|
||||
Ok(user.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// 校验口令哈希长度
|
||||
/// 校验口令哈希长度,空串表示免密
|
||||
fn validate_account_hash(hash: &str) -> std::io::Result<()> {
|
||||
if hash.is_empty() || hash.len() > 256 {
|
||||
if hash.len() > 256 {
|
||||
return Err(std::io::Error::other("invalid account credential"));
|
||||
}
|
||||
Ok(())
|
||||
@@ -1172,15 +1339,6 @@ impl Drop for Server {
|
||||
/// 别的库一律要显式建(没有默认工作库)。
|
||||
const SYSTEM_DATABASE: &str = "system";
|
||||
|
||||
/// 系统库的表结构来自这份脚本(编译期嵌进来)
|
||||
const SYSTEM_SCHEMA_SQL: &str = include_str!("../../../scripts/init.sql");
|
||||
|
||||
/// 系统库 schema:脚本缺表或多表都按它来
|
||||
fn system_schema() -> &'static [InitTable] {
|
||||
static TABLES: std::sync::OnceLock<Vec<InitTable>> = std::sync::OnceLock::new();
|
||||
TABLES.get_or_init(|| initsql::parse(SYSTEM_SCHEMA_SQL).unwrap_or_else(|e| panic!("scripts/init.sql: {e}")))
|
||||
}
|
||||
|
||||
/// 保留库:不能建、不能删。
|
||||
fn reserved_database(name: &str) -> bool {
|
||||
name == SYSTEM_DATABASE
|
||||
@@ -1322,7 +1480,8 @@ impl Databases {
|
||||
let request: Request = serde_json::from_value(value).map_err(Error::other)?;
|
||||
let global = matches!(request, Request::AccountsList | Request::AccountCreate { .. }
|
||||
| Request::AccountReset { .. } | Request::AccountLogin { .. }
|
||||
| Request::AccountDrop { .. } | Request::Ping);
|
||||
| Request::AccountDrop { .. } | Request::BootstrapSystem { .. }
|
||||
| Request::SystemStatus | Request::Ping);
|
||||
let server = if global { &self.system }
|
||||
else if selected.is_empty() { return Err(Error::other("no database selected")); }
|
||||
else if selected == SYSTEM_DATABASE { &self.system }
|
||||
@@ -1331,6 +1490,17 @@ impl Databases {
|
||||
}
|
||||
}
|
||||
|
||||
/// 只读表页的请求可与其它读并发,其余独占
|
||||
fn concurrent_read(req: &Request) -> bool {
|
||||
matches!(
|
||||
req,
|
||||
Request::Scan { .. }
|
||||
| Request::ScanShard { .. }
|
||||
| Request::LookupByIndex { .. }
|
||||
| Request::RangeByIndex { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// 校验库名是否合法
|
||||
fn valid_database(name: &str) -> bool {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
|
||||
Reference in new issue
Block a user