系统表统一命名与迁移
This commit is contained in:
1 parent
4c030e6181
commit
29529444a1
7 files changed
+178
-15
No files matched your search
@@ -202,6 +202,23 @@ impl Catalog {
|
||||
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:整列定义换成新的,并清掉二级索引与统计
|
||||
pub fn set_columns(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> {
|
||||
let entry = self
|
||||
|
||||
@@ -229,6 +229,19 @@ pub fn reserved_table(table: &str) -> bool {
|
||||
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` 算
|
||||
fn default_id_column() -> String {
|
||||
"id".to_string()
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::config::{self, Config, Loaded};
|
||||
use crate::heap::{scalar_int, HeapTable};
|
||||
use crate::log;
|
||||
use crate::protocol::{
|
||||
reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, SchemaColumn, StorageOp,
|
||||
TableSchemaWire, USERS_TABLE,
|
||||
blocked_table, reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row,
|
||||
SchemaColumn, StorageOp, TableSchemaWire, USERS_TABLE,
|
||||
};
|
||||
use crate::wal::{committed_ops, Wal, WalOp};
|
||||
use crate::{log_debug, log_error, log_info, log_warn};
|
||||
@@ -386,7 +386,7 @@ impl Server {
|
||||
| StorageOp::Delete { table, .. }
|
||||
| StorageOp::Replace { table, .. } => table,
|
||||
};
|
||||
if reserved_table(table) {
|
||||
if blocked_table(table) {
|
||||
return Err(std::io::Error::other("reserved system table"));
|
||||
}
|
||||
if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
||||
@@ -604,6 +604,7 @@ impl Server {
|
||||
|
||||
/// 老数据迁移:类型名规范、缺列补 null
|
||||
fn migrate_data(&self) -> std::io::Result<()> {
|
||||
self.migrate_table_names()?;
|
||||
let names: Vec<String> = self
|
||||
.catalog
|
||||
.lock()
|
||||
@@ -649,6 +650,51 @@ impl Server {
|
||||
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<()> {
|
||||
if column == "id" {
|
||||
@@ -963,7 +1009,7 @@ impl Server {
|
||||
return Response::Error { message: "invalid column name".into() };
|
||||
}
|
||||
if let Some(table) = req.table() {
|
||||
if reserved_table(table) {
|
||||
if blocked_table(table) {
|
||||
return Response::Error { message: "reserved system table".into() };
|
||||
}
|
||||
if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
||||
@@ -1192,7 +1238,7 @@ impl Server {
|
||||
let schemas: Vec<TableSchemaWire> = c
|
||||
.all_tables()
|
||||
.into_iter()
|
||||
.filter(|(name, _)| !reserved_table(name))
|
||||
.filter(|(name, _)| !blocked_table(name))
|
||||
.map(|(name, ts)| TableSchemaWire {
|
||||
table: name.to_string(),
|
||||
columns: ts.columns.clone(),
|
||||
@@ -1364,10 +1410,10 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// 只列出数据字典中的业务表
|
||||
/// 列出业务表与授权服务用的三张系统表
|
||||
fn list_tables(&self) -> std::io::Result<Vec<String>> {
|
||||
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
|
||||
@@ -1567,6 +1613,13 @@ impl Drop for Server {
|
||||
/// 别的库一律要显式建(没有默认工作库)。
|
||||
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 {
|
||||
name == SYSTEM_DATABASE
|
||||
|
||||
@@ -1686,6 +1686,59 @@ fn legacy_catalog_and_partial_rows_are_migrated() -> Result<(), Box<dyn std::err
|
||||
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]
|
||||
fn account_table_uses_typed_columns_and_stamps_login() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
@@ -188,9 +188,9 @@ memoryBackendWith trusted name ref =
|
||||
}
|
||||
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
|
||||
[] -> columnsFromStatement db stmt
|
||||
@@ -352,15 +352,15 @@ systemDatabaseName = "system"
|
||||
usersTable :: String
|
||||
usersTable = "__system_users"
|
||||
|
||||
-- | 角色与授权表的名字,不带内部前缀
|
||||
-- | 角色与授权表的名字,在系统库里但服务自己要读写
|
||||
rolesTable :: String
|
||||
rolesTable = "sys_roles"
|
||||
rolesTable = "__system_roles"
|
||||
|
||||
grantsTable :: String
|
||||
grantsTable = "sys_grants"
|
||||
grantsTable = "__system_grants"
|
||||
|
||||
membersTable :: String
|
||||
membersTable = "sys_members"
|
||||
membersTable = "__system_members"
|
||||
|
||||
internalNames :: [String]
|
||||
internalNames = [rolesTable, grantsTable, membersTable]
|
||||
@@ -369,9 +369,17 @@ internalNames = [rolesTable, grantsTable, membersTable]
|
||||
reserved :: String -> Bool
|
||||
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 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
|
||||
|
||||
@@ -553,6 +553,25 @@ privilegeSpec = describe "server privileges" $ do
|
||||
Left err -> expectationFailure err
|
||||
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))
|
||||
|
||||
-- 真存储夹具:一个用例一份数据目录,跑完关链路删干净
|
||||
|
||||
-- | 本次用例的数据目录(用例串行跑,固定名够用)
|
||||
|
||||
@@ -53,6 +53,6 @@ chusql-server
|
||||
|
||||
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]` 分区中配置。
|
||||
Reference in new issue
Block a user