系统表统一命名与迁移

This commit is contained in:
LIm1tZz7 committed 2026-10-03 17:16:26 +08:00
1 parent 4c030e6181
commit 29529444a1
7 files changed
+178 -15

No files matched your search

+17
View File
@@ -202,6 +202,23 @@ impl Catalog {
Ok(()) Ok(())
} }
/// 表改名到指定名字
pub fn rename_table(&mut self, from: &str, to: &str) -> io::Result<bool> {
let Some(entry) = self.tables.remove(from) else {
return Ok(false);
};
if self.tables.contains_key(to) {
self.tables.insert(from.to_string(), entry);
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("table already exists: {}", to),
));
}
self.seen.remove(from);
self.tables.insert(to.to_string(), entry);
Ok(true)
}
/// ALTER:整列定义换成新的,并清掉二级索引与统计 /// ALTER:整列定义换成新的,并清掉二级索引与统计
pub fn set_columns(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> { pub fn set_columns(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> {
let entry = self let entry = self
+13
View File
@@ -229,6 +229,19 @@ pub fn reserved_table(table: &str) -> bool {
table.to_ascii_lowercase().starts_with("__system_") table.to_ascii_lowercase().starts_with("__system_")
} }
/// 服务自己的角色与授权表名
pub const PRIVILEGE_TABLES: [&str; 3] = ["__system_roles", "__system_grants", "__system_members"];
/// 是不是服务自己经请求通道读写的系统表
pub fn internal_table(table: &str) -> bool {
PRIVILEGE_TABLES.contains(&table.to_ascii_lowercase().as_str())
}
/// 是不是请求通道要拒绝的系统表
pub fn blocked_table(table: &str) -> bool {
reserved_table(table) && !internal_table(table)
}
/// 没写 column 时按 `id` 算 /// 没写 column 时按 `id` 算
fn default_id_column() -> String { fn default_id_column() -> String {
"id".to_string() "id".to_string()
+60 -7
View File
@@ -8,8 +8,8 @@ use crate::config::{self, Config, Loaded};
use crate::heap::{scalar_int, HeapTable}; use crate::heap::{scalar_int, HeapTable};
use crate::log; use crate::log;
use crate::protocol::{ use crate::protocol::{
reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, SchemaColumn, StorageOp, blocked_table, reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row,
TableSchemaWire, USERS_TABLE, SchemaColumn, StorageOp, TableSchemaWire, USERS_TABLE,
}; };
use crate::wal::{committed_ops, Wal, WalOp}; use crate::wal::{committed_ops, Wal, WalOp};
use crate::{log_debug, log_error, log_info, log_warn}; use crate::{log_debug, log_error, log_info, log_warn};
@@ -386,7 +386,7 @@ impl Server {
| StorageOp::Delete { table, .. } | StorageOp::Delete { table, .. }
| StorageOp::Replace { table, .. } => table, | StorageOp::Replace { table, .. } => table,
}; };
if reserved_table(table) { if blocked_table(table) {
return Err(std::io::Error::other("reserved system table")); return Err(std::io::Error::other("reserved system table"));
} }
if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
@@ -604,6 +604,7 @@ impl Server {
/// 老数据迁移:类型名规范、缺列补 null /// 老数据迁移:类型名规范、缺列补 null
fn migrate_data(&self) -> std::io::Result<()> { fn migrate_data(&self) -> std::io::Result<()> {
self.migrate_table_names()?;
let names: Vec<String> = self let names: Vec<String> = self
.catalog .catalog
.lock() .lock()
@@ -649,6 +650,51 @@ impl Server {
Ok(()) Ok(())
} }
/// 老目录里的角色与授权表改到内部前缀
fn migrate_table_names(&self) -> std::io::Result<()> {
for (from, to) in LEGACY_TABLE_RENAMES {
let indexes = {
let c = self
.catalog
.lock()
.map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
match c.describe(from) {
Some(schema) if c.describe(to).is_none() => {
schema.indexes.iter().map(|i| i.column.clone()).collect::<Vec<String>>()
}
_ => continue,
}
};
self.rename_table_files(from, to, &indexes);
{
let mut c = self
.catalog
.lock()
.map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
if !c.rename_table(from, to)? {
continue;
}
c.save(self.catalog_path())?;
}
if let Ok(mut tables) = self.tables.lock() {
tables.remove(from);
}
log_info!(core, "renamed system table {} to {}", from, to);
}
Ok(())
}
/// 把一张表的数据文件与索引文件改名
fn rename_table_files(&self, from: &str, to: &str, indexes: &[String]) {
if !self.table_path(from).exists() {
return;
}
let _ = std::fs::rename(self.table_path(from), self.table_path(to));
for column in std::iter::once("id").chain(indexes.iter().map(String::as_str)) {
let _ = std::fs::rename(self.index_path(from, column), self.index_path(to, column));
}
}
/// 建索引前的检查:内置列、未知表列、重复索引 /// 建索引前的检查:内置列、未知表列、重复索引
fn check_create_index(&self, table: &str, column: &str) -> std::io::Result<()> { fn check_create_index(&self, table: &str, column: &str) -> std::io::Result<()> {
if column == "id" { if column == "id" {
@@ -963,7 +1009,7 @@ impl Server {
return Response::Error { message: "invalid column name".into() }; return Response::Error { message: "invalid column name".into() };
} }
if let Some(table) = req.table() { if let Some(table) = req.table() {
if reserved_table(table) { if blocked_table(table) {
return Response::Error { message: "reserved system table".into() }; return Response::Error { message: "reserved system table".into() };
} }
if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
@@ -1192,7 +1238,7 @@ impl Server {
let schemas: Vec<TableSchemaWire> = c let schemas: Vec<TableSchemaWire> = c
.all_tables() .all_tables()
.into_iter() .into_iter()
.filter(|(name, _)| !reserved_table(name)) .filter(|(name, _)| !blocked_table(name))
.map(|(name, ts)| TableSchemaWire { .map(|(name, ts)| TableSchemaWire {
table: name.to_string(), table: name.to_string(),
columns: ts.columns.clone(), columns: ts.columns.clone(),
@@ -1364,10 +1410,10 @@ impl Server {
} }
} }
/// 只列出数据字典中的业务表 /// 列出业务表与授权服务用的三张系统表
fn list_tables(&self) -> std::io::Result<Vec<String>> { fn list_tables(&self) -> std::io::Result<Vec<String>> {
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?; let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
Ok(c.table_names().into_iter().filter(|t| !reserved_table(t)).collect()) Ok(c.table_names().into_iter().filter(|t| !blocked_table(t)).collect())
} }
/// 查一张表的 schema /// 查一张表的 schema
@@ -1567,6 +1613,13 @@ impl Drop for Server {
/// 别的库一律要显式建(没有默认工作库)。 /// 别的库一律要显式建(没有默认工作库)。
const SYSTEM_DATABASE: &str = "system"; const SYSTEM_DATABASE: &str = "system";
/// 老目录里角色与授权表的旧名对照
const LEGACY_TABLE_RENAMES: [(&str, &str); 3] = [
("sys_roles", "__system_roles"),
("sys_grants", "__system_grants"),
("sys_members", "__system_members"),
];
/// 保留库:不能建、不能删。 /// 保留库:不能建、不能删。
fn reserved_database(name: &str) -> bool { fn reserved_database(name: &str) -> bool {
name == SYSTEM_DATABASE name == SYSTEM_DATABASE
+53
View File
@@ -1686,6 +1686,59 @@ fn legacy_catalog_and_partial_rows_are_migrated() -> Result<(), Box<dyn std::err
Ok(()) Ok(())
} }
/// 老系统库的角色表启动时改到内部前缀
#[test]
fn legacy_privilege_tables_are_renamed_on_start() -> Result<(), Box<dyn std::error::Error>> {
let (server, data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"create_table","database":"system","table":"sys_roles","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?;
request_ok(&mut c, serde_json::json!({"method":"insert","database":"system","table":"sys_roles","row":{"id":1,"name":"admin"}}))?;
drop(c);
drop(server);
let system_dir = data.path().join("system");
assert!(system_dir.join("sys_roles.db").exists(), "老数据文件要先生成");
let _restarted = start_server_in(data.path());
let mut c = connect()?;
let described: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"describe_table","database":"system","table":"__system_roles"}"#))?;
assert_eq!(described["status"], "schema", "{described}");
let rows: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","database":"system","table":"__system_roles"}"#))?;
assert_eq!(rows["rows"], serde_json::json!([{"id":1,"name":"admin"}]), "行要跟着改名走");
let old: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"describe_table","database":"system","table":"sys_roles"}"#))?;
assert_eq!(old["status"], "error", "旧名不该还在:{old}");
assert!(system_dir.join("__system_roles.db").exists(), "数据文件要改名");
assert!(!system_dir.join("sys_roles.db").exists(), "老数据文件要改掉");
Ok(())
}
/// 角色与授权表能被请求通道读到,账号表不能
#[test]
fn privilege_tables_are_reachable_but_account_table_is_not() -> Result<(), Box<dyn std::error::Error>> {
let (_server, _data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"create_table","database":"system","table":"__system_roles","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?;
let scanned: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","database":"system","table":"__system_roles"}"#))?;
assert_eq!(scanned["status"], "rows", "{scanned}");
let denied: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","database":"system","table":"__system_users"}"#))?;
assert_eq!(denied["status"], "error", "{denied}");
assert!(
denied["message"].as_str().unwrap_or("").contains("reserved system table"),
"{denied}"
);
let listed: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"list_tables","database":"system"}"#))?;
let tables: Vec<&str> = listed["tables"]
.as_array()
.ok_or("missing tables")?
.iter()
.map(|one| one.as_str().unwrap_or(""))
.collect();
assert!(tables.contains(&"__system_roles"), "授权表要在字典里: {listed}");
assert!(!tables.contains(&"__system_users"), "账号表不能露面: {listed}");
Ok(())
}
/// 账号表落盘类型与索引,登录盖时间戳 /// 账号表落盘类型与索引,登录盖时间戳
#[test] #[test]
fn account_table_uses_typed_columns_and_stamps_login() -> Result<(), Box<dyn std::error::Error>> { fn account_table_uses_typed_columns_and_stamps_login() -> Result<(), Box<dyn std::error::Error>> {
+15 -7
View File
@@ -188,9 +188,9 @@ memoryBackendWith trusted name ref =
} }
where where
-- | 按视角过滤库里的表 -- | 按视角过滤库里的表
scoped = if trusted then filter (not . reserved . fst) else visible scoped = if trusted then filter (not . blocked . fst) else visible
-- | 该视角下要隐藏的表 -- | 该视角下要隐藏的表
hidden = if trusted then reserved else internalTable hidden = if trusted then blocked else internalTable
-- | 取结果列名,空结果回落到解析 -- | 取结果列名,空结果回落到解析
colsOf db stmt rows = case rows of colsOf db stmt rows = case rows of
[] -> columnsFromStatement db stmt [] -> columnsFromStatement db stmt
@@ -352,15 +352,15 @@ systemDatabaseName = "system"
usersTable :: String usersTable :: String
usersTable = "__system_users" usersTable = "__system_users"
-- | 角色与授权表的名字,不带内部前缀 -- | 角色与授权表的名字,在系统库里但服务自己要读写
rolesTable :: String rolesTable :: String
rolesTable = "sys_roles" rolesTable = "__system_roles"
grantsTable :: String grantsTable :: String
grantsTable = "sys_grants" grantsTable = "__system_grants"
membersTable :: String membersTable :: String
membersTable = "sys_members" membersTable = "__system_members"
internalNames :: [String] internalNames :: [String]
internalNames = [rolesTable, grantsTable, membersTable] internalNames = [rolesTable, grantsTable, membersTable]
@@ -369,9 +369,17 @@ internalNames = [rolesTable, grantsTable, membersTable]
reserved :: String -> Bool reserved :: String -> Bool
reserved = T.isPrefixOf "__system_" . T.toLower . T.pack reserved = T.isPrefixOf "__system_" . T.toLower . T.pack
-- | 是不是服务自己要经请求通道读写的表
privilegeTable :: String -> Bool
privilegeTable name = T.unpack (T.toLower (T.pack name)) `elem` internalNames
-- | 是不是服务自己的内部表 -- | 是不是服务自己的内部表
internalTable :: String -> Bool internalTable :: String -> Bool
internalTable name = reserved name || T.unpack (T.toLower (T.pack name)) `elem` internalNames internalTable name = reserved name || privilegeTable name
-- | 外部语句能不能碰这张表
blocked :: String -> Bool
blocked name = reserved name && not (privilegeTable name)
-- | 去掉内部表 -- | 去掉内部表
visible :: Database -> Database visible :: Database -> Database
+19
View File
@@ -553,6 +553,25 @@ privilegeSpec = describe "server privileges" $ do
Left err -> expectationFailure err Left err -> expectationFailure err
Right stmt -> authorize service root testDatabase stmt `shouldReturn` Right () Right stmt -> authorize service root testDatabase stmt `shouldReturn` Right ()
it "keeps the role tables on the real storage" $ withIpcStorage $ do
base <- ipcBackend
service <- newPrivileges base
created <- runPrivilegeCommand service (Root testRootName) testDatabase (CreateRoleCommand "reader")
created `shouldBe` Right ()
views <- listRoleViews service
fmap (map roleName) views `shouldBe` Right ["reader"]
-- 改名后真存储上仍要读得到角色表
let system = beWithDatabase base "system"
stored <- beStatement system "SELECT * FROM __system_roles"
case stored of
Left err -> expectationFailure ("select __system_roles: " ++ err)
Right result -> concatMap (map snd) (srRows result) `shouldContain` [VStr "reader"]
-- 账号表不给语句通道看到
denied <- beStatement system "SELECT * FROM __system_users"
case denied of
Left _ -> pure ()
Right result -> expectationFailure ("account table must stay hidden: " ++ show (srRows result))
-- 真存储夹具:一个用例一份数据目录,跑完关链路删干净 -- 真存储夹具:一个用例一份数据目录,跑完关链路删干净
-- | 本次用例的数据目录(用例串行跑,固定名够用) -- | 本次用例的数据目录(用例串行跑,固定名够用)
+1 -1
View File
@@ -53,6 +53,6 @@ chusql-server
WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;进程重启时按标记分组重放已提交的记录、丢弃没有标记的整组,重放前逐条做幂等检查,恢复结束把最大 LSN 写进 `wal.checkpoint` 并清空 WAL。`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。 WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;进程重启时按标记分组重放已提交的记录、丢弃没有标记的整组,重放前逐条做幂等检查,恢复结束把最大 LSN 写进 `wal.checkpoint` 并清空 WAL。`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。
系统数据库及部分管理操作仅允许管理员访问。 系统数据库及部分管理操作仅允许管理员访问。系统表统一用 `__system_` 前缀:账号表 `__system_users` 对语句通道隐藏,角色、授权、成员三张表(`__system_roles` / `__system_grants` / `__system_members`)只在 `system` 库里可见,权限服务自身也通过 SQL 读写它们;存储启动时把旧名 `sys_roles` / `sys_grants` / `sys_members` 连同数据与索引文件一起迁移过来。
服务监听地址、端口和请求限制可在 `chusql.toml` 的 `[server]` 分区中配置。 服务监听地址、端口和请求限制可在 `chusql.toml` 的 `[server]` 分区中配置。