整理代码
This commit is contained in:
1 parent
e87c4ee6a8
commit
a45e33f94b
39 files changed
+263
-916
No files matched your search
@@ -2,7 +2,7 @@
|
||||
进度见 progress.txt,限制见 known-issues.txt,用户操作见 docs/。
|
||||
|
||||
## 架构与接口
|
||||
- 后续设计按领域记录于 docs/normalization.md、docs/types.md、docs/architecture.md 和 docs/release-signing.md;只读范式入口已提供;后续实施 bound IR / DOMAIN,再推进物化、递归、MVCC、compact 和并行写。各文档的设计章节定义拟议能力,现有运行语义不变。
|
||||
- 用户文档按主题记录于 docs/commands.md、docs/types.md、docs/architecture.md 和 docs/install.md;只读范式入口已提供。后续实施 bound IR / DOMAIN,再推进物化、递归、MVCC、compact 和并行写;规划不代表现有能力。
|
||||
- 范式迁移首版保留来源,拒绝 NULL、重复行与依赖不保持;发布目标须验证约束、完整重建并使用独立原子结构 WAL 组,不能复用禁止 DDL 的会话事务拼接执行。
|
||||
- 发布真实性采用 TUF 元数据链与 Ed25519,初始 root 必须可信取得;离线验证不证明最新版本,操作系统原生代码签名另行接入。
|
||||
- Web / CLI → server TCP → Haskell 引擎 → FFI → Rust 存储;server 独占数据目录,Haskell 库静态链接,存储为 cdylib。
|
||||
|
||||
@@ -39,7 +39,7 @@ ChuSQL 能力与进展摘要
|
||||
- SQL 复合列声明、LIST / MAYBE / TUPLE、嵌套构造器、INSERT / UPDATE / CHECK / 子查询接入;CLI / Web 显示及 SQL 生成。
|
||||
- 兼容边界:旧 ColumnType 和 RelOp 保留,日期 / 时间戳 / Blob 明确使用 CompatibilityType;完整 capability / operator / cast / 用户 ADT / Extension 不在本阶段。
|
||||
- 本批已验证:Engine 466 / Server 141 / Web 75 / CLI 17 全部通过;前端运行时值 9 条断言及事务 10 项通过,改动文件注释检查无问题,git diff --check 通过。
|
||||
- 使用说明:docs/runtime-types.md;本批日志:logs/runtime-engine.log、logs/runtime-web.log、logs/runtime-web-final.log、logs/runtime-cli.log。Server 最终回归亦通过;Web 最终日志由单独重跑生成。
|
||||
- 使用说明:docs/types.md;本批日志:logs/runtime-engine.log、logs/runtime-web.log、logs/runtime-web-final.log、logs/runtime-cli.log。Server 最终回归亦通过;Web 最终日志由单独重跑生成。
|
||||
|
||||
## 既有模块验证
|
||||
|
||||
|
||||
@@ -45,4 +45,4 @@ DECIMAL 是有限浮点表示,采用 ties-to-even 舍入与舍入后溢出检
|
||||
|
||||
## References
|
||||
|
||||
按需读 `docs/runtime-types.md`;`docs/types.md` 含未来设计,必须与实现区分。以上源码模块是契约核对入口。
|
||||
按需读 `docs/types.md`,区分当前类型用法与未来约束设计。以上源码模块是契约核对入口。
|
||||
@@ -44,7 +44,6 @@ csql-web
|
||||
|
||||
## 更多参考文档
|
||||
|
||||
- [数据库原理学习与答辩文档](learnning/README.md)
|
||||
- [安装与卸载](docs/install.md)
|
||||
- [配置选项](docs/config.md)
|
||||
- [命令与选项](docs/commands.md)
|
||||
|
||||
@@ -47,10 +47,6 @@ runStatementUncheckedM db q@Select{} =
|
||||
case bindQuery db q of
|
||||
Left e -> pure (Left e)
|
||||
Right bound -> executeQueryM bound
|
||||
runStatementUncheckedM db q@SelectExpr{} =
|
||||
case bindQuery db q of
|
||||
Left e -> pure (Left e)
|
||||
Right bound -> executeQueryM bound
|
||||
runStatementUncheckedM db (Insert tbl cols rows) =
|
||||
case lookupTable db tbl of
|
||||
Left e -> pure (Left e)
|
||||
|
||||
@@ -86,26 +86,16 @@ translate q = case q of
|
||||
ordering <- distinctOrdering inner
|
||||
plan <- case inner of
|
||||
s@Select{} -> translate s { selectOrderBy = [], selectLimit = Nothing }
|
||||
s@SelectExpr{} -> translate s { selectOrderBy = [], selectLimit = Nothing }
|
||||
_ -> Left "DISTINCT requires SELECT"
|
||||
Right (limited ordering (selectLimit inner) (Distinct plan))
|
||||
Select
|
||||
{ selectCols = cols
|
||||
, selectFrom = fromC
|
||||
, selectWhere = mWhere
|
||||
, selectGroupBy = groupBy
|
||||
, selectOrderBy = orderBy
|
||||
, selectLimit = mLimit
|
||||
} -> do
|
||||
plan0 <- source fromC mWhere
|
||||
let (plan, _) = grouped groupBy [] plan0
|
||||
Right (Project cols (limited orderBy mLimit plan))
|
||||
SelectExpr items fromC mWhere groupBy orderBy mLimit -> do
|
||||
Select items fromC mWhere groupBy orderBy mLimit -> do
|
||||
plan0 <- source fromC mWhere
|
||||
let (plan, items') = grouped groupBy items plan0
|
||||
Right $ if any (\(c, _) -> c `elem` map fst items') orderBy
|
||||
then orderedProjection items' orderBy mLimit plan
|
||||
else Compute items' (limited orderBy mLimit plan)
|
||||
Right $ if all (\(label, expression) -> expression == Col label) items'
|
||||
then Project (map fst items') (limited orderBy mLimit plan)
|
||||
else if any (\(c, _) -> c `elem` map fst items') orderBy
|
||||
then orderedProjection items' orderBy mLimit plan
|
||||
else Compute items' (limited orderBy mLimit plan)
|
||||
_ -> Left "only SELECT is supported by translate"
|
||||
|
||||
-- | 在排序前计算别名并保留输出顺序
|
||||
|
||||
@@ -10,8 +10,7 @@ import Data.List (nub)
|
||||
queryTables :: Statement -> [String]
|
||||
queryTables statement = nub (case statement of
|
||||
SelectDistinct child -> queryTables child
|
||||
Select _ source condition _ _ _ -> sourceTables source ++ concatMap expressionTables condition
|
||||
SelectExpr items source condition _ _ _ -> sourceTables source
|
||||
Select items source condition _ _ _ -> sourceTables source
|
||||
++ concatMap (expressionTables . snd) items ++ concatMap expressionTables condition
|
||||
_ -> [])
|
||||
|
||||
|
||||
@@ -238,21 +238,13 @@ inferSubqueryOutput db outerScope sq = case subqueryStatement sq of
|
||||
mapM_ (requireOrdering "DISTINCT" . snd) columns
|
||||
_ <- distinctOrdering q
|
||||
Right columns
|
||||
q@Select{selectCols = cols} -> do
|
||||
env <- checkFrom db enclosing (selectFrom q)
|
||||
checkResolvedWith db enclosing q
|
||||
mapM (columnTypeOf env) cols
|
||||
q@SelectExpr{selectItems = items} -> do
|
||||
q@Select{selectItems = items} -> do
|
||||
env <- checkFrom db enclosing (selectFrom q)
|
||||
checkResolvedWith db enclosing q
|
||||
mapM (\(l, e) -> (\t -> (l, t)) <$> inferExpr db "SELECT" (Scope env enclosing) e) items
|
||||
_ -> Left "subquery must be a SELECT"
|
||||
where
|
||||
enclosing = scopeFlat outerScope
|
||||
-- \| 查这一列的推导类型
|
||||
columnTypeOf env c = do
|
||||
(_, col) <- scopeAt "SELECT" (Scope env enclosing) c
|
||||
Right (c, inferredColumnType (columnType col))
|
||||
|
||||
-- | 聚合的参数:列要在,且不能嵌套聚合
|
||||
aggregateArg :: Database -> String -> Scope -> Expr -> Either String InferredType
|
||||
@@ -450,27 +442,13 @@ checkResolved db = checkResolvedWith db []
|
||||
checkResolvedWith :: Database -> [(String, Column)] -> Statement -> Either String ()
|
||||
checkResolvedWith db outer q = case q of
|
||||
SelectDistinct inner -> () <$ inferSubqueryOutput db (Scope [] outer) (Subquery (SelectDistinct inner) [])
|
||||
Select
|
||||
{ selectCols = cols
|
||||
, selectFrom = fromC
|
||||
, selectWhere = mWhere
|
||||
, selectGroupBy = groupBy
|
||||
, selectOrderBy = orderBy
|
||||
} -> do
|
||||
env <- checkFrom db outer fromC
|
||||
let full = Scope env outer
|
||||
checkColumns "SELECT" full cols
|
||||
checkGrouping full groupBy (map (\c -> (c, Col c)) cols)
|
||||
checkColumns "ORDER BY" full (map fst orderBy)
|
||||
mapM_ (checkRuntimeColumn "ORDER BY" full . fst) orderBy
|
||||
mapM_ (checkNoAggregate "WHERE") mWhere
|
||||
mapM_ (checkBool db "WHERE" full) mWhere
|
||||
SelectExpr items fromC mWhere groupBy orderBy _ -> do
|
||||
Select items fromC mWhere groupBy orderBy _ -> do
|
||||
env <- checkFrom db outer fromC
|
||||
let full = Scope env outer
|
||||
direct = all (\(label, expression) -> expression == Col label) items
|
||||
mapM_ (\item -> () <$ inferExpr db "SELECT" full (snd item)) items
|
||||
checkGrouping full groupBy (items ++ [(c, Col c) | (c, _) <- orderBy, c `notElem` map fst items])
|
||||
mapM_ (checkOrder db full items . fst) orderBy
|
||||
checkGrouping full groupBy (items ++ [(c, Col c) | (c, _) <- orderBy, not direct, c `notElem` map fst items])
|
||||
mapM_ ((if direct then checkRuntimeColumn "ORDER BY" full else checkOrder db full items) . fst) orderBy
|
||||
mapM_ (checkNoAggregate "WHERE") mWhere
|
||||
mapM_ (checkBool db "WHERE" full) mWhere
|
||||
Insert tbl cols rows -> do
|
||||
@@ -850,9 +828,7 @@ outerRefsOf db stmt = nub [c | c <- statementColumns stmt, not (inInner c)]
|
||||
statementColumns :: Statement -> [String]
|
||||
statementColumns stmt = case stmt of
|
||||
SelectDistinct q -> statementColumns q
|
||||
Select cols fromC mWhere groupBy orderBy _ ->
|
||||
cols ++ fromColumns fromC ++ concatMap colsInExpr mWhere ++ groupBy ++ map fst orderBy
|
||||
SelectExpr items fromC mWhere groupBy orderBy _ ->
|
||||
Select items fromC mWhere groupBy orderBy _ ->
|
||||
concatMap (colsInExpr . snd) items ++ fromColumns fromC ++ concatMap colsInExpr mWhere ++ groupBy ++ map fst orderBy
|
||||
_ -> []
|
||||
where
|
||||
@@ -866,14 +842,12 @@ statementColumns stmt = case stmt of
|
||||
statementFrom :: Statement -> FromClause
|
||||
statementFrom (SelectDistinct q) = statementFrom q
|
||||
statementFrom Select{selectFrom = fromC} = fromC
|
||||
statementFrom SelectExpr{selectFrom = fromC} = fromC
|
||||
statementFrom _ = FromUnit
|
||||
|
||||
-- | 解析子查询里的 SELECT
|
||||
resolveSubqueryStatement :: Database -> [(String, Column)] -> Statement -> Either String Statement
|
||||
resolveSubqueryStatement db outer (SelectDistinct q) = SelectDistinct <$> resolveSubqueryStatement db outer q
|
||||
resolveSubqueryStatement db outer q@Select{} = resolveQuery db outer q (map (\c -> (c, Col c)) (selectCols q))
|
||||
resolveSubqueryStatement db outer q@SelectExpr{} = resolveQuery db outer q (selectItems q)
|
||||
resolveSubqueryStatement db outer q@Select{} = resolveQuery db outer q (selectItems q)
|
||||
resolveSubqueryStatement _ _ _ = Left "subquery must be a SELECT"
|
||||
|
||||
-- | 列名解析入口:查询走连接解析,改数据的语句按本表列解析
|
||||
@@ -881,7 +855,6 @@ resolveStatement :: Database -> Statement -> Either String Statement
|
||||
resolveStatement db q = case q of
|
||||
SelectDistinct inner -> SelectDistinct <$> resolveStatement db inner
|
||||
Select{} -> resolveSelect db q
|
||||
SelectExpr{} -> resolveSelect db q
|
||||
Update tbl assigns mWhere -> do
|
||||
(key, t) <- resolveTable db tbl
|
||||
let env = tableCols t
|
||||
@@ -910,8 +883,7 @@ scopedColumnAt place inner outer c = scopeAt place (Scope inner outer) c
|
||||
|
||||
-- | 单表保留原表头,连接使用唯一前缀
|
||||
resolveSelect :: Database -> Statement -> Either String Statement
|
||||
resolveSelect db q@Select{} = resolveQuery db [] q (map (\c -> (c, Col c)) (selectCols q))
|
||||
resolveSelect db q@SelectExpr{} = resolveQuery db [] q (selectItems q)
|
||||
resolveSelect db q@Select{} = resolveQuery db [] q (selectItems q)
|
||||
resolveSelect _ q = Right q
|
||||
|
||||
-- | 检查排序输出别名或来源列
|
||||
@@ -978,11 +950,9 @@ resolveQuery db outer q items = do
|
||||
[] -> name "ORDER BY" c
|
||||
[label] -> Right label
|
||||
_ -> Left ("ORDER BY: ambiguous projection alias: " ++ c)
|
||||
qIsExpression = case q of SelectExpr{} -> True; _ -> False
|
||||
qIsExpression = any (\(label, expression) -> expression /= Col label) items
|
||||
ordering <- mapM (\(c, d) -> (\k -> (k, d)) <$> orderName c) (selectOrderBy q)
|
||||
pure $ case q of
|
||||
Select{} -> Select (map fst projected) source condition grouping ordering (selectLimit q)
|
||||
_ -> SelectExpr projected source condition grouping ordering (selectLimit q)
|
||||
pure (Select projected source condition grouping ordering (selectLimit q))
|
||||
|
||||
-- | 收集没有别名遮蔽的来源表名
|
||||
sourceTableNames :: FromClause -> [String]
|
||||
@@ -1028,19 +998,7 @@ resolveFrom db outer (FromJoin kind left right cond) = do
|
||||
-- | 派生表的输出列名:列取裸名,表达式保留原标签
|
||||
nameDerivedColumns :: Statement -> Either String Statement
|
||||
nameDerivedColumns (SelectDistinct q) = SelectDistinct <$> nameDerivedColumns q
|
||||
nameDerivedColumns q@Select{selectCols = cols, selectFrom = fromC} = do
|
||||
let names = map lastTablePart cols
|
||||
checkDerivedNames names
|
||||
Right
|
||||
( SelectExpr
|
||||
[(n, Col c) | (n, c) <- zip names cols]
|
||||
fromC
|
||||
(selectWhere q)
|
||||
(selectGroupBy q)
|
||||
(selectOrderBy q)
|
||||
(selectLimit q)
|
||||
)
|
||||
nameDerivedColumns q@SelectExpr{selectItems = items} = do
|
||||
nameDerivedColumns q@Select{selectItems = items} = do
|
||||
let names = map derivedItemName items
|
||||
checkDerivedNames names
|
||||
Right q{selectItems = zip names (map snd items)}
|
||||
|
||||
@@ -8,14 +8,7 @@ import ChuSQL.Core.Engine.Runtime.Functions (FunctionId)
|
||||
|
||||
data Statement
|
||||
= Select
|
||||
{ selectCols :: [String]
|
||||
, selectFrom :: FromClause
|
||||
, selectWhere :: Maybe Expr
|
||||
, selectGroupBy :: [String]
|
||||
, selectOrderBy :: [(String, SortDir)]
|
||||
, selectLimit :: Maybe Int
|
||||
}
|
||||
| SelectExpr
|
||||
-- | 输出名称与投影表达式,普通列用 Col
|
||||
{ selectItems :: [(String, Expr)]
|
||||
, selectFrom :: FromClause
|
||||
, selectWhere :: Maybe Expr
|
||||
@@ -170,7 +163,7 @@ data SortDir
|
||||
makeSelect :: [String] -> String -> Maybe Expr -> Statement
|
||||
makeSelect cols tbl w =
|
||||
Select
|
||||
{ selectCols = cols
|
||||
{ selectItems = [(c, Col c) | c <- cols]
|
||||
, selectFrom = FromTable Nothing tbl
|
||||
, selectWhere = w
|
||||
, selectGroupBy = []
|
||||
@@ -181,7 +174,6 @@ makeSelect cols tbl w =
|
||||
-- | 将去重排序映射到输出列
|
||||
distinctOrdering :: Statement -> Either String [(String, SortDir)]
|
||||
distinctOrdering q@Select{} = resolveDistinctOrdering q
|
||||
distinctOrdering q@SelectExpr{} = resolveDistinctOrdering q
|
||||
distinctOrdering _ = Left "DISTINCT requires SELECT"
|
||||
|
||||
-- | 解析去重输出的排序列
|
||||
@@ -189,8 +181,7 @@ resolveDistinctOrdering :: Statement -> Either String [(String, SortDir)]
|
||||
resolveDistinctOrdering q = mapM resolve (selectOrderBy q)
|
||||
where
|
||||
items = case q of
|
||||
Select { selectCols = cols } -> [(c, Col c) | c <- cols]
|
||||
SelectExpr { selectItems = projected } -> projected
|
||||
Select { selectItems = projected } -> projected
|
||||
_ -> []
|
||||
-- | 排序键必须出现在去重结果中
|
||||
resolve (column, direction) = case [label | (label, e) <- items, label == column || e == Col column] of
|
||||
|
||||
@@ -697,9 +697,7 @@ selectStatement = do
|
||||
groupBy <- fromMaybe [] <$> optional groupByClause
|
||||
orderBy <- fromMaybe [] <$> optional orderByClause
|
||||
mLimit <- optional limitClause
|
||||
pure $ (if distinct then SelectDistinct else id) $ if all (\(name, e) -> e == Col name) cols
|
||||
then Select (map fst cols) fromC mWhere groupBy orderBy mLimit
|
||||
else SelectExpr cols fromC mWhere groupBy orderBy mLimit
|
||||
pure $ (if distinct then SelectDistinct else id) (Select cols fromC mWhere groupBy orderBy mLimit)
|
||||
|
||||
-- | 读 WITH:展开每个 CTE 再读主查询
|
||||
withStatement :: Parser Statement
|
||||
@@ -741,20 +739,11 @@ cteKey = map toLower
|
||||
renameColumns :: String -> [String] -> Statement -> Either String Statement
|
||||
renameColumns _ [] q = Right q
|
||||
renameColumns name cols (SelectDistinct q) = SelectDistinct <$> renameColumns name cols q
|
||||
renameColumns name cols q@Select { selectCols = names } = do
|
||||
checkCteArity name cols names
|
||||
Right
|
||||
( SelectExpr
|
||||
[(c, Col n) | (c, n) <- zip cols names]
|
||||
(selectFrom q)
|
||||
(selectWhere q)
|
||||
(selectGroupBy q)
|
||||
(selectOrderBy q)
|
||||
(selectLimit q)
|
||||
)
|
||||
renameColumns name cols q@SelectExpr { selectItems = items } = do
|
||||
renameColumns name cols q@Select { selectItems = items } = do
|
||||
checkCteArity name cols (map snd items)
|
||||
ordering <- mapM renameOrder (selectOrderBy q)
|
||||
ordering <- if all (\(label, expression) -> expression == Col label) items
|
||||
then Right (selectOrderBy q)
|
||||
else mapM renameOrder (selectOrderBy q)
|
||||
Right q { selectItems = zip cols (map snd items), selectOrderBy = ordering }
|
||||
where
|
||||
-- | 列清单同步改写内层排序输出别名
|
||||
@@ -774,11 +763,7 @@ checkCteArity name cols out
|
||||
expandStatement :: [(String, Statement)] -> Statement -> Either String Statement
|
||||
expandStatement env stmt = case stmt of
|
||||
SelectDistinct q -> SelectDistinct <$> expandStatement env q
|
||||
q@Select { selectFrom = fromC, selectWhere = mWhere } -> do
|
||||
fromC' <- expandClause env fromC
|
||||
w <- traverse (expandExpr env) mWhere
|
||||
Right q { selectFrom = fromC', selectWhere = w }
|
||||
q@SelectExpr { selectFrom = fromC, selectItems = items, selectWhere = mWhere } -> do
|
||||
q@Select { selectFrom = fromC, selectItems = items, selectWhere = mWhere } -> do
|
||||
fromC' <- expandClause env fromC
|
||||
items' <- traverse (\(name, e) -> fmap ((,) name) (expandExpr env e)) items
|
||||
w <- traverse (expandExpr env) mWhere
|
||||
|
||||
@@ -420,7 +420,7 @@ spec = do
|
||||
it "binds calls before execution" $ do
|
||||
let bound = parseStatement "SELECT length('abc')" >>= prepare []
|
||||
case bound of
|
||||
Right SelectExpr {selectItems = [(_, BoundFunction (FunctionId 1) _ [_])]} -> pure ()
|
||||
Right Select {selectItems = [(_, BoundFunction (FunctionId 1) _ [_])]} -> pure ()
|
||||
_ -> expectationFailure (show bound)
|
||||
it "rejects unknown SQL functions before scanning" $
|
||||
sql "SELECT missing('abc')" `shouldSatisfy` isFailure
|
||||
|
||||
@@ -564,7 +564,7 @@ main = hspec $ do
|
||||
parseStatement "SELECT name FROM users JOIN orders ON id = user_id"
|
||||
`shouldBe` Right
|
||||
( Select
|
||||
{ selectCols = ["name"]
|
||||
{ selectItems = [("name", Col "name")]
|
||||
, selectFrom =
|
||||
FromJoin
|
||||
InnerJoin
|
||||
@@ -582,7 +582,7 @@ main = hspec $ do
|
||||
parseStatement "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id"
|
||||
`shouldBe` Right
|
||||
( Select
|
||||
{ selectCols = ["u.name"]
|
||||
{ selectItems = [("u.name", Col "u.name")]
|
||||
, selectFrom =
|
||||
FromJoin
|
||||
InnerJoin
|
||||
@@ -600,7 +600,7 @@ main = hspec $ do
|
||||
parseStatement "SELECT u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id"
|
||||
`shouldBe` Right
|
||||
( Select
|
||||
{ selectCols = ["u.name"]
|
||||
{ selectItems = [("u.name", Col "u.name")]
|
||||
, selectFrom =
|
||||
FromJoin
|
||||
LeftJoin
|
||||
@@ -633,11 +633,11 @@ main = hspec $ do
|
||||
parseStatement "SELECT d.name FROM (SELECT name FROM users) d"
|
||||
`shouldBe` Right
|
||||
( Select
|
||||
{ selectCols = ["d.name"]
|
||||
{ selectItems = [("d.name", Col "d.name")]
|
||||
, selectFrom =
|
||||
FromSubquery
|
||||
(Just "d")
|
||||
(Select ["name"] (FromTable Nothing "users") Nothing [] [] Nothing)
|
||||
(Select [("name", Col "name")] (FromTable Nothing "users") Nothing [] [] Nothing)
|
||||
, selectWhere = Nothing
|
||||
, selectGroupBy = []
|
||||
, selectOrderBy = []
|
||||
@@ -654,14 +654,14 @@ main = hspec $ do
|
||||
( FromJoin
|
||||
InnerJoin
|
||||
(FromTable (Just "u") "users")
|
||||
(FromSubquery (Just "d") (Select ["name"] (FromTable Nothing "users") Nothing [] [] Nothing))
|
||||
(FromSubquery (Just "d") (Select [("name", Col "name")] (FromTable Nothing "users") Nothing [] [] Nothing))
|
||||
(Eq (Col "u.name") (Col "d.name"))
|
||||
)
|
||||
|
||||
it "parses a projection alias" $ do
|
||||
parseStatement "SELECT count(*) AS total FROM orders"
|
||||
`shouldBe` Right
|
||||
( SelectExpr
|
||||
( Select
|
||||
[("total", CountAll)]
|
||||
(FromTable Nothing "orders")
|
||||
Nothing
|
||||
@@ -713,11 +713,11 @@ main = hspec $ do
|
||||
parseStatement "WITH t AS (SELECT name FROM users) SELECT t.name FROM t"
|
||||
`shouldBe` Right
|
||||
( Select
|
||||
{ selectCols = ["t.name"]
|
||||
{ selectItems = [("t.name", Col "t.name")]
|
||||
, selectFrom =
|
||||
FromSubquery
|
||||
(Just "t")
|
||||
(Select ["name"] (FromTable Nothing "users") Nothing [] [] Nothing)
|
||||
(Select [("name", Col "name")] (FromTable Nothing "users") Nothing [] [] Nothing)
|
||||
, selectWhere = Nothing
|
||||
, selectGroupBy = []
|
||||
, selectOrderBy = []
|
||||
@@ -737,6 +737,14 @@ main = hspec $ do
|
||||
values "WITH t(who) AS (SELECT name FROM users) SELECT t.who FROM t ORDER BY t.who"
|
||||
`shouldBe` Right [[VStr "Alice"], [VStr "Bob"], [VStr "Carol"]]
|
||||
|
||||
it "keeps source ordering when renaming repeated CTE columns" $ do
|
||||
values "WITH t(first, second) AS (SELECT name, name FROM users ORDER BY name DESC LIMIT 1) SELECT first, second FROM t"
|
||||
`shouldBe` Right [[VStr "Carol", VStr "Carol"]]
|
||||
|
||||
it "renames expression ordering aliases with CTE columns" $ do
|
||||
values "WITH t(next_age) AS (SELECT age + 1 AS years FROM users ORDER BY years DESC LIMIT 1) SELECT next_age FROM t"
|
||||
`shouldBe` Right [[VInt 31]]
|
||||
|
||||
it "takes a bare column from a CTE" $ do
|
||||
values "WITH t(who) AS (SELECT name FROM users) SELECT who FROM t ORDER BY who"
|
||||
`shouldBe` Right [[VStr "Alice"], [VStr "Bob"], [VStr "Carol"]]
|
||||
@@ -771,11 +779,11 @@ main = hspec $ do
|
||||
|
||||
it "parses >= into GtE" $ do
|
||||
parseStatement "SELECT name FROM users WHERE age >= 18"
|
||||
`shouldBe` Right (Select ["name"] (FromTable Nothing "users") (Just (GtE (Col "age") (LitInt 18))) [] [] Nothing)
|
||||
`shouldBe` Right (Select [("name", Col "name")] (FromTable Nothing "users") (Just (GtE (Col "age") (LitInt 18))) [] [] Nothing)
|
||||
|
||||
it "parses <= into LtE" $ do
|
||||
parseStatement "SELECT name FROM users WHERE age <= 30"
|
||||
`shouldBe` Right (Select ["name"] (FromTable Nothing "users") (Just (LtE (Col "age") (LitInt 30))) [] [] Nothing)
|
||||
`shouldBe` Right (Select [("name", Col "name")] (FromTable Nothing "users") (Just (LtE (Col "age") (LitInt 30))) [] [] Nothing)
|
||||
|
||||
it "parses <> and != into the same node" $ do
|
||||
sql "SELECT name FROM users WHERE age <> 17"
|
||||
@@ -855,11 +863,11 @@ main = hspec $ do
|
||||
|
||||
it "parses > ANY into a quantified comparison" $ do
|
||||
parseStatement "SELECT name FROM users WHERE age > ANY (SELECT age FROM orders)"
|
||||
`shouldBe` Right (Select ["name"] (FromTable Nothing "users") (Just (QuantCmp CmpGt (Col "age") (Subquery (Select ["age"] (FromTable Nothing "orders") Nothing [] [] Nothing) []) AnyQ)) [] [] Nothing)
|
||||
`shouldBe` Right (Select [("name", Col "name")] (FromTable Nothing "users") (Just (QuantCmp CmpGt (Col "age") (Subquery (Select [("age", Col "age")] (FromTable Nothing "orders") Nothing [] [] Nothing) []) AnyQ)) [] [] Nothing)
|
||||
|
||||
it "parses <> ALL into a quantified comparison" $ do
|
||||
parseStatement "SELECT name FROM users WHERE age <> ALL (SELECT age FROM orders)"
|
||||
`shouldBe` Right (Select ["name"] (FromTable Nothing "users") (Just (QuantCmp CmpNe (Col "age") (Subquery (Select ["age"] (FromTable Nothing "orders") Nothing [] [] Nothing) []) AllQ)) [] [] Nothing)
|
||||
`shouldBe` Right (Select [("name", Col "name")] (FromTable Nothing "users") (Just (QuantCmp CmpNe (Col "age") (Subquery (Select [("age", Col "age")] (FromTable Nothing "orders") Nothing [] [] Nothing) []) AllQ)) [] [] Nothing)
|
||||
|
||||
it "still reads plain comparisons" $ do
|
||||
values "SELECT name FROM users WHERE age > 20 ORDER BY name"
|
||||
@@ -1269,7 +1277,7 @@ main = hspec $ do
|
||||
|
||||
it "user is reserved as an alias but still usable inside a longer name" $ do
|
||||
parseStatement "SELECT * FROM users user" `shouldSatisfy` isLeft
|
||||
parseStatement "SELECT user1 FROM users" `shouldBe` Right (Select ["user1"] (FromTable Nothing "users") Nothing [] [] Nothing)
|
||||
parseStatement "SELECT user1 FROM users" `shouldBe` Right (Select [("user1", Col "user1")] (FromTable Nothing "users") Nothing [] [] Nothing)
|
||||
|
||||
describe "ChuSQL.Core.Engine (DELETE)" $ do
|
||||
it "parses DELETE with WHERE" $ do
|
||||
@@ -1407,7 +1415,7 @@ main = hspec $ do
|
||||
it "executes ORDER BY age ASC" $ do
|
||||
let q =
|
||||
Select
|
||||
{ selectCols = ["name"]
|
||||
{ selectItems = [("name", Col "name")]
|
||||
, selectFrom = FromTable Nothing "users"
|
||||
, selectWhere = Nothing
|
||||
, selectGroupBy = []
|
||||
@@ -1424,7 +1432,7 @@ main = hspec $ do
|
||||
it "executes ORDER BY age DESC" $ do
|
||||
let q =
|
||||
Select
|
||||
{ selectCols = ["name"]
|
||||
{ selectItems = [("name", Col "name")]
|
||||
, selectFrom = FromTable Nothing "users"
|
||||
, selectWhere = Nothing
|
||||
, selectGroupBy = []
|
||||
@@ -1454,7 +1462,7 @@ main = hspec $ do
|
||||
it "executes LIMIT without ORDER BY" $ do
|
||||
let q =
|
||||
Select
|
||||
{ selectCols = ["name"]
|
||||
{ selectItems = [("name", Col "name")]
|
||||
, selectFrom = FromTable Nothing "users"
|
||||
, selectWhere = Nothing
|
||||
, selectGroupBy = []
|
||||
@@ -1470,7 +1478,7 @@ main = hspec $ do
|
||||
it "executes LIMIT with ORDER BY" $ do
|
||||
let q =
|
||||
Select
|
||||
{ selectCols = ["name"]
|
||||
{ selectItems = [("name", Col "name")]
|
||||
, selectFrom = FromTable Nothing "users"
|
||||
, selectWhere = Nothing
|
||||
, selectGroupBy = []
|
||||
@@ -1486,7 +1494,7 @@ main = hspec $ do
|
||||
it "executes LIMIT 0" $ do
|
||||
let q =
|
||||
Select
|
||||
{ selectCols = ["name"]
|
||||
{ selectItems = [("name", Col "name")]
|
||||
, selectFrom = FromTable Nothing "users"
|
||||
, selectWhere = Nothing
|
||||
, selectGroupBy = []
|
||||
@@ -1535,7 +1543,7 @@ main = hspec $ do
|
||||
runStatement
|
||||
testDB
|
||||
( Select
|
||||
{ selectCols = ["u.name"]
|
||||
{ selectItems = [("u.name", Col "u.name")]
|
||||
, selectFrom =
|
||||
FromJoin
|
||||
InnerJoin
|
||||
@@ -2512,7 +2520,7 @@ main = hspec $ do
|
||||
runIPCStorage
|
||||
( runStatementM
|
||||
( Select
|
||||
{ selectCols = ["name"]
|
||||
{ selectItems = [("name", Col "name")]
|
||||
, selectFrom = FromTable Nothing "ipc_idx"
|
||||
, selectWhere = Just (Eq (Col "id") (LitInt 42))
|
||||
, selectGroupBy = []
|
||||
|
||||
@@ -49,7 +49,6 @@ data WebConfig = WebConfig
|
||||
, wcMaxPageSize :: Int
|
||||
, wcMaxRows :: Int
|
||||
, wcMaxSqlLength :: Int
|
||||
, wcSeedDemo :: Bool
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
@@ -73,7 +72,6 @@ defaultWebConfig =
|
||||
, wcMaxPageSize = 500
|
||||
, wcMaxRows = 1000
|
||||
, wcMaxSqlLength = 20000
|
||||
, wcSeedDemo = False
|
||||
}
|
||||
|
||||
-- | 把设置键统一成连字符写法,重名时连字符优先
|
||||
@@ -148,7 +146,6 @@ webConfigFromSection saved =
|
||||
, wcMaxPageSize = sectionInt "max-page-size" saved (wcMaxPageSize defaultWebConfig)
|
||||
, wcMaxRows = sectionInt "max-rows" saved (wcMaxRows defaultWebConfig)
|
||||
, wcMaxSqlLength = sectionInt "max-sql-length" saved (wcMaxSqlLength defaultWebConfig)
|
||||
, wcSeedDemo = sectionBool "seed" saved (wcSeedDemo defaultWebConfig)
|
||||
}
|
||||
|
||||
-- | 分区里一个键的非空字符串值
|
||||
|
||||
@@ -61,7 +61,6 @@ settingCatalogue =
|
||||
, owned "storage-btree-order" "Storage B+tree order" "storage" "int" "4" "btree" "order"
|
||||
, owned "storage-buffer-pool" "Storage buffer pool pages" "storage" "int" "1024" "buffer" "pool_size"
|
||||
, owned "storage-log" "Storage log level" "storage" "text" "info" "log" "level"
|
||||
, item "seed" "Seed demo data on start" "storage" "bool" "1" True True
|
||||
, owned "tcp-listen-host" "TCP listen address" "network" "text" "127.0.0.1" "server" "listen_host"
|
||||
, owned "tcp-port" "TCP port" "network" "int" "7777" "server" "port"
|
||||
, owned "tcp-max-message" "TCP max request line (bytes)" "network" "int" "1048576" "server" "max_message"
|
||||
@@ -231,7 +230,6 @@ effectiveSettings cfg =
|
||||
, ("max-page-size", tshow (wcMaxPageSize cfg))
|
||||
, ("max-rows", tshow (wcMaxRows cfg))
|
||||
, ("max-sql-length", tshow (wcMaxSqlLength cfg))
|
||||
, ("seed", boolText (wcSeedDemo cfg))
|
||||
, ("data-dir", maybe "" T.pack (wcDataDir cfg))
|
||||
, ("log-files", T.pack (wcLogFiles cfg))
|
||||
]
|
||||
|
||||
@@ -76,8 +76,7 @@ data Backend = Backend
|
||||
columnsFromStatement :: Database -> Statement -> [String]
|
||||
columnsFromStatement db stmt = case prepare db stmt of
|
||||
Right (SelectDistinct q) -> columnsFromStatement db q
|
||||
Right (Select cols _ _ _ _ _) -> cols
|
||||
Right (SelectExpr items _ _ _ _ _) -> map fst items
|
||||
Right (Select items _ _ _ _ _) -> map fst items
|
||||
Right ShowDomains -> ["domain", "base_type"]
|
||||
_ -> []
|
||||
|
||||
@@ -331,9 +330,6 @@ statementTables :: Statement -> [Text]
|
||||
statementTables stmt = nub (case stmt of
|
||||
SelectDistinct q -> statementTables q
|
||||
Select{} ->
|
||||
tableRefsOf (selectFrom stmt)
|
||||
++ concatMap exprTables (maybe [] (: []) (selectWhere stmt) ++ map Col (selectCols stmt ++ selectGroupBy stmt ++ map fst (selectOrderBy stmt)))
|
||||
SelectExpr{} ->
|
||||
tableRefsOf (selectFrom stmt)
|
||||
++ concatMap exprTables (maybe [] (: []) (selectWhere stmt) ++ map snd (selectItems stmt) ++ map Col (selectGroupBy stmt ++ map fst (selectOrderBy stmt)))
|
||||
Insert table _ rows -> T.pack table : concatMap exprTables (concat rows)
|
||||
|
||||
@@ -199,8 +199,7 @@ reconcileObjects service = withTables service (catalog (OA.objectReconcile (pvBa
|
||||
requiredActions :: Statement -> Maybe [(Text, Text)]
|
||||
requiredActions statement = case statement of
|
||||
SelectDistinct q -> requiredActions q
|
||||
Select{} -> Just (selectNeeds (selectFrom statement) (maybe [] (: []) (selectWhere statement)))
|
||||
SelectExpr{} -> Just (selectNeeds (selectFrom statement) (maybe [] (: []) (selectWhere statement) ++ map snd (selectItems statement)))
|
||||
Select{} -> Just (selectNeeds (selectFrom statement) (maybe [] (: []) (selectWhere statement) ++ map snd (selectItems statement)))
|
||||
Insert table _ rows -> Just ((T.pack table, "insert") : selectsOf (concat rows))
|
||||
Update table assignments condition -> Just ((T.pack table, "update") : selectsOf (map snd assignments ++ maybe [] (: []) condition))
|
||||
Delete table condition -> Just ((T.pack table, "delete") : selectsOf (maybe [] (: []) condition))
|
||||
|
||||
@@ -328,7 +328,6 @@ transactionData :: Statement -> Bool
|
||||
transactionData statement = case statement of
|
||||
SelectDistinct q -> transactionData q
|
||||
Select{} -> True
|
||||
SelectExpr{} -> True
|
||||
Insert{} -> True
|
||||
Update{} -> True
|
||||
Delete{} -> True
|
||||
|
||||
@@ -17,10 +17,8 @@ import ChuSQL.Interface.Config (
|
||||
)
|
||||
import ChuSQL.Interface.Link (clientPing, closeClient, connectClient)
|
||||
import ChuSQL.Interface.RateLimit (newRateLimiter)
|
||||
import ChuSQL.Interface.Session (authenticateSession, newSession)
|
||||
import ChuSQL.Interface.Settings (effectiveSettings)
|
||||
import ChuSQL.Interface.TOML (resolveConfigPath)
|
||||
import ChuSQL.Web.Demo (SeedReport (..), seedDemo)
|
||||
import Control.Exception (IOException, try)
|
||||
import Data.Char (toLower)
|
||||
import Data.String (fromString)
|
||||
@@ -52,7 +50,6 @@ data Options = Options
|
||||
, optMaxPageSize :: Maybe Int
|
||||
, optMaxRows :: Maybe Int
|
||||
, optMaxSqlLength :: Maybe Int
|
||||
, optSeedDemo :: Maybe Bool
|
||||
, optHelp :: Bool
|
||||
}
|
||||
|
||||
@@ -74,7 +71,6 @@ emptyOptions =
|
||||
, optMaxPageSize = Nothing
|
||||
, optMaxRows = Nothing
|
||||
, optMaxSqlLength = Nothing
|
||||
, optSeedDemo = Nothing
|
||||
, optHelp = False
|
||||
}
|
||||
|
||||
@@ -99,7 +95,6 @@ parseArgs = go emptyOptions
|
||||
go opts ("--max-page-size" : v : rest) = withInt "--max-page-size" v (\n -> go opts{optMaxPageSize = Just n} rest)
|
||||
go opts ("--max-rows" : v : rest) = withInt "--max-rows" v (\n -> go opts{optMaxRows = Just n} rest)
|
||||
go opts ("--max-sql-length" : v : rest) = withInt "--max-sql-length" v (\n -> go opts{optMaxSqlLength = Just n} rest)
|
||||
go opts ("--seed" : v : rest) = go opts{optSeedDemo = Just (isTrue v)} rest
|
||||
go opts ("--cookie-secure" : v : rest) = go opts{optCookieSecure = Just (isTrue v)} rest
|
||||
go _ (a : _) = Left ("unrecognized argument or missing value: " ++ a)
|
||||
|
||||
@@ -136,7 +131,6 @@ usage = do
|
||||
putStrLn " --max-page-size N rows per page ceiling ([web] max_page_size, default 500)"
|
||||
putStrLn " --max-rows N rows returned per query ([web] max_rows, default 1000)"
|
||||
putStrLn " --max-sql-length N max SQL characters ([web] max_sql_length, default 20000)"
|
||||
putStrLn " --seed BOOL load demo data when the db is empty ([web] seed, default false)"
|
||||
putStrLn ""
|
||||
putStrLn "administrator password: lives in the __system_users system table, never in settings.toml"
|
||||
putStrLn " the installer always sets one; an account whose hash is still empty signs in with an empty password"
|
||||
@@ -164,7 +158,6 @@ applyOptions cfg opts =
|
||||
, wcMaxPageSize = orElse (optMaxPageSize opts) (wcMaxPageSize cfg)
|
||||
, wcMaxRows = orElse (optMaxRows opts) (wcMaxRows cfg)
|
||||
, wcMaxSqlLength = orElse (optMaxSqlLength opts) (wcMaxSqlLength cfg)
|
||||
, wcSeedDemo = orElse (optSeedDemo opts) (wcSeedDemo cfg)
|
||||
}
|
||||
where
|
||||
-- | 有就用新的,没有留旧值
|
||||
@@ -220,7 +213,6 @@ run opts = do
|
||||
printBanner cfg configPath staticDir
|
||||
reportCredential cfg legacy
|
||||
checkServer (aeServerHost env) (aeServerPort env)
|
||||
seedIfWanted cfg (aeServerHost env) (aeServerPort env)
|
||||
let settings =
|
||||
Warp.setPort (wcPort cfg) (Warp.setHost (fromString (wcHost cfg)) Warp.defaultSettings)
|
||||
serve settings app
|
||||
@@ -262,32 +254,6 @@ reportCredential cfg legacy = do
|
||||
putStrLn " !! remove it: the administrator password lives in the __system_users system table"
|
||||
else pure ()
|
||||
|
||||
-- | 配了 seed 就补演示数据(管理员已设口令时跳过)
|
||||
seedIfWanted :: WebConfig -> Text -> Int -> IO ()
|
||||
seedIfWanted cfg host port
|
||||
| not (wcSeedDemo cfg) = pure ()
|
||||
| otherwise = do
|
||||
opened <- connectClient host port
|
||||
case opened of
|
||||
Left message -> TIO.putStrLn ("demo data skipped: " <> message)
|
||||
Right client -> do
|
||||
session <- newSession client
|
||||
signed <- authenticateSession session (rootUserName cfg) ""
|
||||
case signed of
|
||||
Left message -> TIO.putStrLn ("demo data skipped: " <> message <> " (set the administrator password after seeding)")
|
||||
Right () -> do
|
||||
result <- seedDemo session
|
||||
case result of
|
||||
Left message -> TIO.putStrLn ("demo data skipped: " <> message)
|
||||
Right report ->
|
||||
TIO.putStrLn
|
||||
( "demo data: "
|
||||
<> (if null (srCreated report) then "already present" else "created " <> T.pack (unwords (srCreated report)))
|
||||
<> (if null (srSkipped report) then "" else ", kept " <> T.pack (unwords (srSkipped report)))
|
||||
<> (if null (srIndexes report) then "" else ", index on " <> T.pack (unwords (srIndexes report)))
|
||||
)
|
||||
closeClient client
|
||||
|
||||
-- | ping 一次 server,报告它通不通
|
||||
checkServer :: Text -> Int -> IO ()
|
||||
checkServer host port = do
|
||||
|
||||
@@ -12,7 +12,6 @@ build-type: Simple
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules: ChuSQL.Web.API
|
||||
ChuSQL.Web.Demo
|
||||
ChuSQL.Web.Secure
|
||||
ChuSQL.Web.Static
|
||||
ChuSQL.Web.UISettings
|
||||
|
||||
@@ -101,7 +101,6 @@ import ChuSQL.Interface.Settings (
|
||||
writeSettingsFile,
|
||||
)
|
||||
import ChuSQL.Interface.TOML (defaultConfigFile)
|
||||
import ChuSQL.Web.Demo (SeedReport (..), seedDemo)
|
||||
import ChuSQL.Web.Secure (bodyLimitDynamic, sameOriginOnly, securityHeaders, contentSecurityPolicy)
|
||||
import ChuSQL.Web.Static (contentTypeOf, readStatic, safeRelative)
|
||||
import ChuSQL.Web.UISettings (
|
||||
@@ -331,7 +330,6 @@ routes env = do
|
||||
post "/api/tables/:t/indexes" (withDatabase env createIndexH)
|
||||
delete "/api/tables/:t/indexes/:col" (withDatabase env dropIndexH)
|
||||
delete "/api/tables/:t/columns/:col" (withDatabase env dropColumnH)
|
||||
post "/api/demo-data" (withDatabase env demoDataH)
|
||||
get "/api/roles" (withDatabase env rolesH)
|
||||
post "/api/roles" (withDatabase env createRoleH)
|
||||
delete "/api/roles/:name" (withDatabase env dropRoleH)
|
||||
@@ -1241,24 +1239,6 @@ dropColumnH env = do
|
||||
Left err -> reply400 "bad_request" (T.pack err)
|
||||
Right sql -> runWrite ws sql
|
||||
|
||||
-- | 一键灌演示数据(缺什么补什么,可以反复点)
|
||||
demoDataH :: AppEnv -> ActionM ()
|
||||
demoDataH env = do
|
||||
ws <- requireAdminSession env
|
||||
_ <- requireDatabaseName ws
|
||||
result <- liftIO (seedDemo (wsSession ws))
|
||||
case result of
|
||||
Left message -> serverError message
|
||||
Right report ->
|
||||
json
|
||||
( object
|
||||
[ "ok" .= True
|
||||
, "created" .= srCreated report
|
||||
, "skipped" .= srSkipped report
|
||||
, "indexes" .= srIndexes report
|
||||
]
|
||||
)
|
||||
|
||||
-- | 角色总览:只有管理员能看
|
||||
rolesH :: AppEnv -> ActionM ()
|
||||
rolesH env = do
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChuSQL.Web.Demo (
|
||||
SeedReport (..),
|
||||
demoTableSpecs,
|
||||
demoRows,
|
||||
demoIndexes,
|
||||
demoNames,
|
||||
demoPlan,
|
||||
seedDemo,
|
||||
) where
|
||||
|
||||
import ChuSQL.Core.Model (Value (..))
|
||||
import ChuSQL.Core.Protocol (TableInfo (..))
|
||||
import ChuSQL.Interface.Actions (
|
||||
ColumnSpec (..),
|
||||
CreateTableSpec (..),
|
||||
createIndexSql,
|
||||
createTableSql,
|
||||
insertRowsSql,
|
||||
)
|
||||
import ChuSQL.Interface.Session (Session, catalog, runStatement)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
|
||||
-- 演示数据:三张表加一个二级索引,语句与界面走同一套拼 SQL 的代码。
|
||||
|
||||
-- | 演示表(建表顺序即依赖顺序;没有外键约束,纯展示)
|
||||
demoTableSpecs :: [CreateTableSpec]
|
||||
demoTableSpecs =
|
||||
[ CreateTableSpec "users" [ColumnSpec "id" "int", ColumnSpec "name" "str", ColumnSpec "age" "int"]
|
||||
, CreateTableSpec "orders" [ColumnSpec "id" "int", ColumnSpec "user_id" "int", ColumnSpec "product" "str", ColumnSpec "amount" "int"]
|
||||
, CreateTableSpec "products" [ColumnSpec "id" "int", ColumnSpec "name" "str", ColumnSpec "sku" "int", ColumnSpec "price" "int"]
|
||||
]
|
||||
|
||||
-- | 每张表的演示行
|
||||
demoRows :: [(T.Text, [[(String, Value)]])]
|
||||
demoRows =
|
||||
[ ( "users"
|
||||
, [ [("id", VInt i), ("name", VStr name), ("age", VInt age)]
|
||||
| (i, (name, age)) <- zip [1 :: Int ..] userRecords
|
||||
]
|
||||
)
|
||||
, ( "orders"
|
||||
, [ [("id", VInt i), ("user_id", VInt uid), ("product", VStr p), ("amount", VInt a)]
|
||||
| (i, (uid, p, a)) <- zip [1 :: Int ..] orderRecords
|
||||
]
|
||||
)
|
||||
, ( "products"
|
||||
, [ [("id", VInt i), ("name", VStr n), ("sku", VInt sku), ("price", VInt price)]
|
||||
| (i, (n, sku, price)) <- zip [1 :: Int ..] productRecords
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
-- | 演示索引:products.sku 取值唯一
|
||||
demoIndexes :: [(T.Text, T.Text)]
|
||||
demoIndexes = [("products", "sku")]
|
||||
|
||||
-- | 演示数据涉及的表名
|
||||
demoNames :: [T.Text]
|
||||
demoNames = map ctsTable demoTableSpecs
|
||||
|
||||
-- | 完整的一套语句(建表 + 插行 + 建索引),按顺序执行
|
||||
demoPlan :: Either String [String]
|
||||
demoPlan = do
|
||||
creates <- mapM createTableSql demoTableSpecs
|
||||
inserts <- mapM (uncurry insertRowsSql) demoRows
|
||||
indexes <- mapM (uncurry createIndexSql) demoIndexes
|
||||
pure (creates ++ inserts ++ indexes)
|
||||
|
||||
data SeedReport = SeedReport
|
||||
{ srCreated :: [String]
|
||||
, srSkipped :: [String]
|
||||
, srIndexes :: [String]
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 按需灌演示数据:缺表补表、缺索引补索引
|
||||
seedDemo :: Session -> IO (Either Text SeedReport)
|
||||
seedDemo session = do
|
||||
catalogResult <- catalog session
|
||||
case catalogResult of
|
||||
Left message -> pure (Left message)
|
||||
Right infos -> do
|
||||
let existingTables = map tiTable infos
|
||||
missing = [spec | spec <- demoTableSpecs, T.unpack (ctsTable spec) `notElem` existingTables]
|
||||
skipped = [T.unpack (ctsTable spec) | spec <- demoTableSpecs, T.unpack (ctsTable spec) `elem` existingTables]
|
||||
missingNames = map ctsTable missing
|
||||
wantedIndexes =
|
||||
[ (t, c)
|
||||
| (t, c) <- demoIndexes
|
||||
, t `elem` missingNames || indexMissing infos t c
|
||||
]
|
||||
created <- runAll (map createTableSql missing)
|
||||
case created of
|
||||
Left message -> pure (Left message)
|
||||
Right () -> do
|
||||
rowsDone <- runAll [insertRowsSql t rs | (t, rs) <- demoRows, t `elem` missingNames]
|
||||
case rowsDone of
|
||||
Left message -> pure (Left message)
|
||||
Right () -> do
|
||||
indexes <- runAll [createIndexSql t c | (t, c) <- wantedIndexes]
|
||||
pure $ fmap (const (SeedReport (map T.unpack missingNames) skipped (map (T.unpack . snd) wantedIndexes))) indexes
|
||||
where
|
||||
-- | 顺序跑完一串语句,首错即停
|
||||
runAll :: [Either String String] -> IO (Either Text ())
|
||||
runAll [] = pure (Right ())
|
||||
runAll (sql : rest) = case sql of
|
||||
Left e -> pure (Left (T.pack e))
|
||||
Right statement -> do
|
||||
result <- runStatement session (T.pack statement)
|
||||
case result of
|
||||
Left message -> pure (Left message)
|
||||
Right _ -> runAll rest
|
||||
|
||||
-- | 目录里这张表缺不缺这个索引
|
||||
indexMissing :: [TableInfo] -> Text -> Text -> Bool
|
||||
indexMissing infos t c = case lookup (T.unpack t) [(tiTable i, tiIndexes i) | i <- infos] of
|
||||
Nothing -> True
|
||||
Just cols -> T.unpack c `notElem` cols
|
||||
|
||||
-- | 演示用户数据
|
||||
userRecords :: [(String, Int)]
|
||||
userRecords =
|
||||
[ ("Alice", 24)
|
||||
, ("Bob", 31)
|
||||
, ("Carol", 45)
|
||||
, ("Dave", 19)
|
||||
, ("Erin", 38)
|
||||
, ("Frank", 52)
|
||||
, ("Grace", 27)
|
||||
, ("Heidi", 33)
|
||||
, ("Ivan", 41)
|
||||
, ("Judy", 29)
|
||||
]
|
||||
|
||||
-- | 演示订单数据
|
||||
orderRecords :: [(Int, String, Int)]
|
||||
orderRecords =
|
||||
[ (1, "Laptop", 1299)
|
||||
, (1, "Mouse", 25)
|
||||
, (2, "Keyboard", 79)
|
||||
, (3, "Monitor", 349)
|
||||
, (3, "Webcam", 89)
|
||||
, (4, "Headset", 119)
|
||||
, (5, "SSD", 139)
|
||||
, (5, "RAM", 99)
|
||||
, (6, "Dock", 199)
|
||||
, (7, "Cable", 12)
|
||||
, (8, "Stand", 45)
|
||||
, (9, "Lamp", 39)
|
||||
]
|
||||
|
||||
-- | 演示产品数据
|
||||
productRecords :: [(String, Int, Int)]
|
||||
productRecords =
|
||||
[ ("Laptop", 1001, 1299)
|
||||
, ("Mouse", 1002, 25)
|
||||
, ("Keyboard", 1003, 79)
|
||||
, ("Monitor", 1004, 349)
|
||||
, ("Webcam", 1005, 89)
|
||||
, ("Headset", 1006, 119)
|
||||
, ("SSD", 1007, 139)
|
||||
, ("RAM", 1008, 99)
|
||||
]
|
||||
@@ -297,11 +297,6 @@ export async function query(sql, database, tabId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 灌演示数据。
|
||||
export async function seedDemoData(database) {
|
||||
return request('/api/demo-data', jsonInit('POST', {}), database);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 设置
|
||||
|
||||
// 读取服务器设置。
|
||||
|
||||
@@ -147,7 +147,6 @@ function renderShell() {
|
||||
<div class="brand"><span class="brand-mark">ChuSQL</span><span class="brand-name">数据库终端</span></div>
|
||||
<nav class="titlebar-actions">
|
||||
<button data-action="new-query" title="新建查询">新建查询</button>
|
||||
<button data-action="demo-data" title="灌一份演示数据">演示数据</button>
|
||||
<button data-action="refresh" title="重新加载当前库">刷新</button>
|
||||
<button data-action="settings" title="IDE 与服务器设置">设置</button>
|
||||
</nav>
|
||||
@@ -1400,7 +1399,7 @@ function startSplitterDrag(kind, event) {
|
||||
|
||||
// 这些动作都要求已经选中一个库(服务没有默认库)
|
||||
const DATABASE_ACTIONS = new Set([
|
||||
'open-normalization', 'analyze-normalization', 'open-table', 'demo-data', 'open-table-dialog', 'create-table',
|
||||
'open-normalization', 'analyze-normalization', 'open-table', 'open-table-dialog', 'create-table',
|
||||
'open-index-dialog', 'create-index', 'drop-table', 'drop-column', 'drop-index',
|
||||
'drop-database', 'commit', 'rollback', 'copy-sql',
|
||||
]);
|
||||
@@ -1496,18 +1495,6 @@ function onClick(event) {
|
||||
case 'refresh':
|
||||
void loadWorkspace().then(reloadOpenTables);
|
||||
return;
|
||||
case 'demo-data':
|
||||
void (async () => {
|
||||
try {
|
||||
const report = await api.seedDemoData(state.database);
|
||||
log('info', `演示数据:新建 ${(report.created ?? []).length} 张,保留 ${(report.skipped ?? []).length} 张。`);
|
||||
await loadWorkspace();
|
||||
await reloadOpenTables();
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
case 'open-db-dialog':
|
||||
region('dlg-db-name').value = '';
|
||||
region('dlg-db-error').textContent = '';
|
||||
|
||||
+34
-116
@@ -1,16 +1,22 @@
|
||||
# 架构与组件
|
||||
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [组件与访问路径](#组件与访问路径) · [数据目录](#数据目录) · [会话与并发](#会话与并发) · [SQL 查询处理](#sql-查询处理) · [快照事务](#快照事务) · [WAL 与崩溃恢复](#wal-与崩溃恢复) · [页面与空间回收](#页面与空间回收) · [身份、授权与系统目录](#身份授权与系统目录) · [权限变更与会话失效](#权限变更与会话失效) · [扩展方向](#扩展方向)
|
||||
|
||||
ChuSQL 采用客户端 / 服务端架构。正常运行时,只有数据库服务直接访问数据文件;离线初始化与恢复由 bootstrap 完成。
|
||||
|
||||
## 组件与访问路径
|
||||
|
||||
~~~text
|
||||
Web / CLI / 其他客户端
|
||||
↓ TCP
|
||||
chusql-server
|
||||
↓
|
||||
SQL 引擎 → Rust 存储层 → 磁盘
|
||||
~~~
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CLI[CLI / 其他客户端] -->|TCP| Server[chusql-server]
|
||||
Browser[浏览器] -->|HTTP| Web[chusql-web]
|
||||
Web -->|TCP| Server
|
||||
Server --> Engine[Haskell SQL 引擎]
|
||||
Engine --> Storage[Rust 存储层]
|
||||
Storage --> Disk[(磁盘)]
|
||||
```
|
||||
|
||||
| 组件 | 职责 |
|
||||
| --- | --- |
|
||||
@@ -21,11 +27,13 @@ Web / CLI / 其他客户端
|
||||
| chusql-core/storage | catalog、页面、B+ 树、缓存、WAL 与持久化 |
|
||||
| csql-bootstrap | 安装引导及离线系统目录修复、恢复、重置 |
|
||||
|
||||
查询 AST 的 `Select` 使用 `selectItems :: [(String, Expr)]` 保存输出名称与投影表达式;普通列使用 `Col`,别名、计算和聚合使用相同结构。便捷函数 `makeSelect` 接受列名列表并构造这些投影项。
|
||||
|
||||
服务默认监听 127.0.0.1:7777,Web 默认监听 127.0.0.1:7778。配置位置和平台数据目录见 [config.md](config.md)。
|
||||
|
||||
## 数据目录
|
||||
|
||||
[storage] data_dir 是持久化数据的根目录,诊断日志由 log_files 单独指定。
|
||||
`[storage] data_dir` 保存持久化数据,`log_files` 单独指定诊断日志目录。
|
||||
|
||||
| 位置 | 内容 |
|
||||
| --- | --- |
|
||||
@@ -47,6 +55,8 @@ Web / CLI / 其他客户端
|
||||
|
||||
查询依次经过解析、基于 schema 的语义检查、优化和执行。库名、表名、列名及别名在语义层解析;存储层使用精确的表名键。
|
||||
|
||||
绑定查询保存依赖对象编号、schema 版本、列定义和传递 DOMAIN 依赖。执行前复查,真实存储的每次读取还在相关库读锁内验证全部依赖;结构改变或对象替换明确报错。守卫不提供整条多请求查询的数据快照;旧目录的未知身份/版本保留兼容校验,旧存储未确认守卫则拒绝执行。
|
||||
|
||||
| 能力 | 实现与边界 |
|
||||
| --- | --- |
|
||||
| 派生表 | 内层独立解析,必须有别名,不能引用外层列;输出列按别名暴露 |
|
||||
@@ -115,120 +125,28 @@ Web / CLI / 其他客户端
|
||||
|
||||
TCP 连接在下一次请求时返回 unauthorized 并关闭;空闲连接不被强行中断。权限逐条查询,避免继续使用旧授权。Web 会话还通过账号版本校验,口令或身份变更使旧凭据失效。
|
||||
|
||||
## 查询引擎扩展设计(尚未实现)
|
||||
## 扩展方向
|
||||
|
||||
按 bound IR → 子查询物化 / 递归 CTE 推进;DOMAIN 约束见[类型文档](types.md#domain-约束设计尚未实现)。以下设计尚未实施,当前能力见前文;实施状态以 .agents/context/progress.txt 为准。
|
||||
> [!NOTE]
|
||||
> 以下是规划,不能作为当前 SQL、事务或恢复能力使用。进度与验收见 `.agents/context/progress.txt`。
|
||||
|
||||
### bound IR
|
||||
|
||||
#### 列与作用域绑定
|
||||
已有列槽位、类型化函数与 `BoundQuery`;后续补齐多级词法作用域、来源/列身份,以及写语句与约束的统一绑定。迁移保留同名遮蔽、NULL / DOMAIN、输出布局和错误时序,结构改写应在绑定前完成或重新绑定。
|
||||
|
||||
保留语法 AST 用于解析和诊断;语义检查产出独立 `BoundStatement`,规划、优化及执行逐步只接收它。列引用绑定为作用域编号、来源编号、列编号和类型,保留原始位置用于诊断;相关列明确标记所属外层作用域,不再通过字符串补限定名辨认身份。
|
||||
### 查询扩展
|
||||
|
||||
#### 表达式类型与错误语义
|
||||
| 方向 | 前置条件与边界 |
|
||||
| --- | --- |
|
||||
| 通用子查询物化 | 缓存绑定对象/schema、外层值及快照,保留惰性求值和首次错误;限制内存、执行预算及取消 |
|
||||
| 递归 CTE | 先实现类型一致的迭代关系,明确去重、循环终止、轮次与行数预算;不隐含支持写入型 CTE |
|
||||
|
||||
表达式节点携带结果类型、nullable、已解析运算符/函数标识和显式转换。没有证据证明安全时 `may_error` 为 true;优化不得删除或提前求值可能报错的表达式。未定型 NULL 只参与推导,最后绑定到上下文类型;不允许流入存储 schema。DOMAIN 身份保留在类型节点中,比较可以用底层语义,赋值需经过领域校验。
|
||||
### 存储扩展
|
||||
|
||||
#### schema 与对象版本
|
||||
| 方向 | 前置条件与边界 |
|
||||
| --- | --- |
|
||||
| 行版本 MVCC | 稳定且不复用的 `row_id`、版本链与快照序号;提交复查写冲突和唯一键,旧索引候选随活跃快照保留 |
|
||||
| 跨页 compact | MVCC 与稳定定位映射,影子文件经 WAL 原子切换;保留旧读引用和逻辑扫描顺序 |
|
||||
| 并行写 | 固定对象/行/唯一键锁序及取消机制,WAL/CSN 发布保持连续已应用前缀;DDL、权限和目录变更继续排序 |
|
||||
|
||||
绑定仅使用 schema,不能扫描数据或打开索引。catalog 解析对象后提供 schema 与版本;执行前再次验证稳定对象编号和 schema 版本。统计独立附着,不纳入绑定身份。内存存储使用与磁盘相同的绑定规则。
|
||||
|
||||
#### 实施与验收
|
||||
|
||||
迁移顺序为查询表达式 → RelOp 输入 → 写语句和约束;过渡适配器单向转换且仅位于边界,不维护两套优化规则。验收用现有查询全集并补同名遮蔽、相关列、别名、NULL/DOMAIN 和错误时序回归;既有可观察顺序保持。
|
||||
|
||||
### 通用子查询物化
|
||||
|
||||
#### 惰性执行与缓存身份
|
||||
|
||||
引入查询内 `LazyMaterialize` 算子:首次实际需要时执行,保留结果行序与重复;非相关查询每语句一次,相关查询按 bound IR 外层列绑定值缓存。缓存键沿用规范值相等、NULL 与领域底层语义,并绑定子计划和执行视图;不能跨语句、会话或权限版本复用。
|
||||
|
||||
#### 求值语义
|
||||
|
||||
首版只物化已证明确定且无副作用的子计划;可能报错的表达式保持现有首次使用和短路路径,无法证明错误时序相同的改写禁用。空输入不触发子查询,标量多行仍在实际消费时报错。ANY/ALL、IN、EXISTS 各自保留三值及空集规则,不能用集合去重替代结果。
|
||||
|
||||
#### 预算与回退
|
||||
|
||||
预算同时限制单缓存、查询总缓存和相关键数量;超过预算时在执行该次子计划前选择不缓存的原路径,不能重跑已执行一半的计划。磁盘溢写作为后续阶段,需明确临时目录、类型编码、权限及取消清理。EXPLAIN 展示物化范围与估算,运行统计不泄露缓存行值。
|
||||
|
||||
#### 验收条件
|
||||
|
||||
验收比较启用/禁用的结果多重集、行序、首次错误、空输入、NULL、重复外层绑定、多次 CTE 引用、预算及取消;不确定函数不可复用。
|
||||
|
||||
### 递归 CTE
|
||||
|
||||
#### 首版语法范围
|
||||
|
||||
保留现有非递归 WITH 行为;新增 WITH RECURSIVE 走绑定作用域,不在解析期无限展开。首版仅单一自引用、锚点 UNION ALL 递归项;自引用只能出现在递归项 FROM,锚点不可引用自身。互递归、多个自引用、递归项聚合/排序/LIMIT、外连接及非单调子查询明确拒绝。UNION 去重递归留后续设计,不能冒用 UNION ALL。
|
||||
|
||||
#### 类型绑定与迭代
|
||||
|
||||
锚点和递归项输出列数相同;显式列名唯一,先由锚点确定类型,再校验递归项可赋值转换,不按运行数据扩宽。无法给锚点 NULL 定型时要求显式类型。执行用 worktable,递归项仅读取上一轮 delta,按轮次和每轮既有行序追加输出,保留 UNION ALL 重复。
|
||||
|
||||
#### 资源预算与终止
|
||||
|
||||
拟定默认深度 1000、累计行数 100000、内存 64 MiB,并受服务端上限限制;达到任一预算返回 resource_limit,不返回截断结果。深度定义为递归项执行轮数;delta 为空正常终止。外层 LIMIT 首版不作为终止证明,先完整执行递归再消费,避免隐藏循环/错误。取消释放所有 worktable。
|
||||
|
||||
#### 验收条件
|
||||
|
||||
验收覆盖零轮、多轮、重复路径、循环超限、NULL/类型冲突、空锚点、名称遮蔽、非法自引用和查询取消。EXPLAIN 明确递归与预算。
|
||||
|
||||
## 存储与事务扩展设计(尚未实现)
|
||||
|
||||
先引入稳定行编号和 MVCC,再实施跨页 compact 与并行写。前置条件是新页格式、版本化 WAL、离线升级工具及恢复验收。
|
||||
|
||||
新格式必须标版本,旧二进制拒绝打开。旧数据在保留备份的停服副本上升级、校验后发布,不混用新旧写入路径。每阶段先运行改动模块的规定测试入口,全量门禁由所有者运行;性能数据不能代替语义、恢复或权限验收。
|
||||
|
||||
### 行版本 MVCC
|
||||
|
||||
#### 身份与可见性
|
||||
|
||||
引入库内单调 row_id,独立于用户 `id`、主键及物理槽号;删除不复用。每版本包含 row_id、版本编号、创建/结束提交序号与前版本定位,未提交版本只在事务私有工作集内。读视图固定 snapshot_csn,选取创建序号不大于快照且结束序号大于快照的版本,再合并自身修改。提交序号与 WAL LSN 分离,映射随提交记录持久化。
|
||||
|
||||
每连接仍只允许一个事务,保持快照隔离和保存点语义;不声称可串行化,写偏斜仍可能发生。写集记录读取的基线版本,提交验证该版本仍为最新;同 row_id 更新/删除冲突返回 serialization_failure。唯一键冲突在当前已提交状态和本次写集联合检查,不能仅用开始快照验证。无业务 id 的表也按 row_id 提交,不再整表替换。
|
||||
|
||||
索引键定位 row_id,索引查询再按快照检查版本;更新键保留旧键候选直到所有活跃快照不再需要,不能只索引最新值。扫描先保持旧格式导入的行序,更新不改 row_id 的扫描位置,新插入按持久化序号排序;不得借 MVCC 无意改变现有无 ORDER BY 输出。
|
||||
|
||||
#### 提交、恢复与回收
|
||||
|
||||
首版仍串行提交:验证 → 分配 CSN → WAL 记录版本变化及提交 → fsync → 应用 → 统一发布可见 CSN。读者仅获取已完整应用的可见序号;持久化提交后应用失败继续采用 recovery_required 封闭请求,不能把已提交组当作失败撤销。不引入未提交脏页,因此首版保持 redo-only;若将来允许提前刷未提交页,必须另行设计 undo/状态恢复。
|
||||
|
||||
保存点记录私有写集边界与不可变根,回退不撤销其他事务版本。活跃快照登记器包括流式扫描和物化消费者;最老快照决定旧版本回收界线。回收同时处理 heap 和索引,读定位有 pin,取消/断线可靠注销。长事务受现有寿命及新增保留空间预算限制,不能为了回收静默推进其快照。
|
||||
|
||||
升级在停服副本中为旧行分配 row_id、初始版本和顺序,重建索引,验证行数/值/约束后切换;不得把重复业务 id 合并。新 WAL 与页格式显式版本化,截断仍遵循已刷盘检查点,不顺带改成页 LSN 检查点。
|
||||
|
||||
#### 验收条件
|
||||
|
||||
验收包含重复/缺失业务 id、旧快照与键更新、删除重插、保存点、唯一并发冲突、写偏斜边界、长读回收、断线及提交每个持久化边界的崩溃恢复;复测 BEGIN/COMMIT 的传输和比较成本。
|
||||
|
||||
### 跨页 compact
|
||||
|
||||
#### 定位与影子页
|
||||
|
||||
使用 row_id 到版本位置的存储内部定位映射,索引不保存可失效的页/槽定位。首版在表维护独占锁下进行,等待该表读 pin 释放;其他表仍可运行。以快照回收界线保留所有可能可见版本,按既有逻辑扫描顺序打包到影子页和定位映射,随后 WAL 记录并原子切换文件代次。
|
||||
|
||||
#### 恢复与旧版本保护
|
||||
|
||||
旧代次直到无读引用且新代次检查点完成才删除;提交前崩溃丢弃影子文件,提交后重放切换。映射缺失、重复 row_id/版本和校验失败明确报错,不返回缺行扫描。隐藏列一并按当前 schema 改写,旧快照需要的结构不得提前删除。跨页整理不改变对象编号、行编号、值或输出顺序。
|
||||
|
||||
#### 验收条件
|
||||
|
||||
验收验证前后全量扫描和索引点/范围查一致、快照旧版本、隐藏列、页损坏、空间不足、取消与文件切换崩溃;记录回收页数及 IO,不能只比较耗时。
|
||||
|
||||
### 并行写
|
||||
|
||||
#### 实施顺序与锁范围
|
||||
|
||||
先允许事务私有写集准备并行,继续保留短串行 WAL/CSN 发布;第二阶段按稳定表编号与 row_id 排序取得写锁、唯一键锁。DDL/维护使用库结构锁,跨表事务必须一次取得完整锁集,禁止边执行边任意升级。权限读门在执行和最终复查期间保持现有语义;目录及 ACL 修改仍单独排序。
|
||||
|
||||
#### 唯一键锁与取消
|
||||
|
||||
唯一键锁使用与值相等一致的规范编码,缺主键也使用 row_id 锁。等待有取消与超时,超时返回明确错误且保持/回退事务遵循现有错误分类;固定锁序避免内部锁环,不承诺支持任意用户锁或死锁自动重试。
|
||||
|
||||
#### 提交发布顺序
|
||||
|
||||
允许不同表/行的验证和页面准备并行,但同一页短 latch、catalog 高水位、索引节点和 WAL 写入仍保护。发布 CSN 必须按提交顺序形成连续已应用前缀,后提交不能跨过未完成组而对读者可见。group commit 为后续性能阶段,不能改变每组原子性和持久化失败语义。
|
||||
|
||||
#### 验收条件
|
||||
|
||||
验收覆盖同表不同行、同一行、唯一键冲突、跨表相反顺序、DDL/compact/撤权竞争、超时取消和 WAL 故障;用确定同步点断言并发结果,再单独测吞吐与等待时间。
|
||||
新页格式、版本化 WAL、离线升级和恢复验证是前提。升级在停服副本上保留备份并校验后发布;旧二进制不得打开不支持的格式。任何持久化应用失败须保持 `recovery_required`,不将已提交 WAL 组当作失败撤销。性能收益不能代替语义、权限和崩溃恢复验证。
|
||||
+8
-1
@@ -1,6 +1,13 @@
|
||||
# Bootstrap 安装引导与离线修复
|
||||
|
||||
`csql-bootstrap` 初始化系统目录,也能在服务无法启动时离线修复。所有维护命令必须先停止使用同一数据目录的 server、Web、CLI 和其他进程。新版本使用数据目录锁拒绝并发维护;旧版本进程不具备该锁,仍须手动停止。
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [安装引导](#安装引导) · [三种维护策略](#三种维护策略) · [副本、备份与恢复边界](#副本备份与恢复边界)
|
||||
|
||||
`csql-bootstrap` 初始化系统目录,也能在服务无法启动时离线修复。
|
||||
|
||||
> [!WARNING]
|
||||
> 维护前必须停止使用同一数据目录的所有进程。数据目录锁拒绝并发维护,但不能依赖旧版进程具备该锁。
|
||||
|
||||
## 安装引导
|
||||
|
||||
|
||||
+39
-12
@@ -1,22 +1,18 @@
|
||||
# 命令与选项
|
||||
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [组件命令](#组件命令) · [csql](#csql) · [chusql-web](#chusql-web) · [chusql-server](#chusql-server) · [范式分析](#范式分析)
|
||||
|
||||
ChuSQL 的主要用户命令如下:
|
||||
|
||||
类型说明见 [自定义类型与预定义类型](types.md),身份属性说明见 [用户、角色与 root 属性](users-and-roles.md)。
|
||||
类型见 [类型与函数](types.md),身份与授权见 [用户与角色](users-and-roles.md)。
|
||||
|
||||
SQL 的查询、事务、类型、用户与授权语法见 [syntax.md](syntax.md)。
|
||||
|
||||
## 组件命令
|
||||
|
||||
安装脚本在启动服务前运行 `csql-bootstrap`:创建系统目录、初始化默认 `root`(LOGIN / SUPERUSER / ENABLED),再登记内置类型。管理员名称可通过安装选项修改。初始化失败时安装报错,停止启动服务。
|
||||
|
||||
预装目录 `system.__system_types` 登记 18 个名称与别名:INT、INTEGER、BIGINT、SMALLINT、VARCHAR、CHAR、TEXT、STR、BOOLEAN、BOOL、FLOAT、REAL、DOUBLE、DECIMAL、NUMERIC、DATE、TIMESTAMP、BLOB;记录基础表示及是否接受类型参数。该目录为受保护的系统元数据,不创建预定义 DOMAIN;DOMAIN 仍按工作库保存。
|
||||
|
||||
重复引导保留已有身份、密码及一致的类型记录,补齐缺失类型;同名定义冲突明确报错。已有引导身份失去管理员属性时拒绝继续,不自动提升权限。
|
||||
|
||||
修复或补齐安装引导时,可停止数据库服务后运行 `csql-bootstrap --config FILE --user root --password-stdin`,通过标准输入提供口令。
|
||||
|
||||
系统目录故障时,先停止所有数据库进程,再依次考虑 `csql-bootstrap repair --config FILE`、`recover` 或清除系统身份和授权的 `reset`。`repair` 仅修复结构,涉及数据修改时中止;完整说明见 [bootstrap.md](bootstrap.md)。
|
||||
安装器先运行 `csql-bootstrap` 初始化系统身份和类型目录,再启动服务;初始化失败即停止。重复引导与离线维护见 [系统引导与恢复](bootstrap.md)。
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
@@ -81,6 +77,7 @@ SQL 语句以分号结束,可跨多行输入。
|
||||
| `\c <库>` | 切换数据库 |
|
||||
| `\f <格式>` | 修改输出格式 |
|
||||
| `\dr` | 查看角色与授权信息(管理员) |
|
||||
| `\normalize TABLE --declaration FILE --format text\|json` | 只读分析表的范式 |
|
||||
|
||||
## chusql-web
|
||||
|
||||
@@ -112,8 +109,6 @@ chusql-web [options]
|
||||
|
||||
SQL 控制台使用另一条服务端连接;SQL 的 BEGIN / COMMIT / ROLLBACK 绑定发起它的页面与查询标签。事务尚未结束时,其他控制台和普通 REST 写操作不能占用该连接;关闭所属查询标签或页面时请求回滚。系统身份修改不支持数据事务,账号表只读,使用 SQL 用户与角色命令管理。
|
||||
|
||||
Web 模块测试入口为 `stack test chusql-web --fast`,其中同时运行 `node --test test/transactions.mjs`;测试环境需要 Node.js。
|
||||
|
||||
## chusql-server
|
||||
|
||||
基本用法:
|
||||
@@ -135,3 +130,35 @@ chusql-server [options]
|
||||
| `-h, --help` | 显示帮助 |
|
||||
|
||||
`chusql-server` 是数据目录的唯一直接访问者。Web、CLI 及其他客户端均通过数据库服务访问数据。
|
||||
|
||||
## 范式分析
|
||||
|
||||
分析只读取 schema 和业务依赖声明,要求当前数据库的 `CONNECT` 与目标表的 `SELECT` 权限;系统对象拒绝分析。它不检查样本数据,也不执行数据迁移。
|
||||
|
||||
声明文件使用 UTF-8 JSON,字段名须与表结构一致:
|
||||
|
||||
```json
|
||||
{
|
||||
"declared_keys": [["student", "course"]],
|
||||
"dependencies": [{"lhs": ["student"], "rhs": ["name"]}],
|
||||
"dependencies_complete": true,
|
||||
"atomic_attributes": true
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
csql -d school --normalize enrollment --declaration dependencies.json --format json
|
||||
```
|
||||
|
||||
| 入口 | 用法 |
|
||||
| --- | --- |
|
||||
| CLI 批处理 | `--normalize` 与 `--execute` 互斥,需提供声明文件 |
|
||||
| CLI 交互 | `\normalize enrollment --declaration dependencies.json --format text` |
|
||||
| Web | 在资源管理器展开表,进入结构区的“范式分析”,填写声明 JSON |
|
||||
|
||||
> [!NOTE]
|
||||
> 结论以用户声明为条件。完整性默认 `false`、原子性默认 `null`,信息不足时不会认证高阶范式;SQL 约束和样本数据不会自动证明业务依赖。
|
||||
|
||||
报告包含候选键、1NF / 2NF / 3NF / BCNF 结论、违反证据及一次 BCNF 分解建议。子关系可能仍需分析,分解不保证依赖保持,也不证明现有 SQL 数据满足声明。Web 使用发起页面或控制台的事务视图;对象或 schema 变化须重新分析。
|
||||
|
||||
最多分析 12 列,依赖和键声明各最多 256 项,运行预算 5 秒;超限、权限或输入错误明确返回失败,不截断为完整报告。CLI 成功获得报告时退出码为 `0`,即使结论违反或信息不足;请求失败时退出非零。
|
||||
+6
-2
@@ -1,5 +1,9 @@
|
||||
# 配置选项
|
||||
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [存储配置](#存储配置) · [Web 配置](#web-配置) · [Server 配置](#server-配置) · [账号与安全](#账号与安全) · [修改配置](#修改配置)
|
||||
|
||||
ChuSQL 使用统一配置文件 `settings.toml`。默认位置:
|
||||
|
||||
| 平台 | 路径 |
|
||||
@@ -38,7 +42,6 @@ ChuSQL 使用统一配置文件 `settings.toml`。默认位置:
|
||||
| `max_page_size` | `500` | 最大分页大小 |
|
||||
| `max_rows` | `1000` | 单次查询最大返回行数 |
|
||||
| `max_sql_length` | `20000` | SQL 最大字符数 |
|
||||
| `seed` | `false` | 空库时是否写入演示数据 |
|
||||
| `password_min_length` | `12` | 普通账号最短口令长度 |
|
||||
| `password_classes` | `2` | 口令至少包含的字符类别数 |
|
||||
|
||||
@@ -55,7 +58,8 @@ ChuSQL 使用统一配置文件 `settings.toml`。默认位置:
|
||||
|
||||
本机 sudo 认证额外使用 `[server] sudo_auth_user`,默认空字符串,表示关闭。设置为一个已启用 `ALLOW_SUDO_AUTH` 的数据库身份后,必须以 Unix root 或 Windows 提升后的管理员运行 `chusql-server`。这是明确的本机身份映射,与启动参数 `--user` 和最高权限属性分别设置。配置变更后重启服务生效,Web 设置页面不提供此项。操作步骤见 [用户与角色](users-and-roles.md#本机-sudo-认证)。
|
||||
|
||||
修改 Server 相关配置后需要重启 `chusql-server`。
|
||||
> [!NOTE]
|
||||
> 修改 Server 配置后须重启 `chusql-server`。sudo 映射默认关闭,启用时还须满足身份属性和操作系统提升要求。
|
||||
|
||||
## 账号与安全
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# ChuSQL 技术总结与数据库课程答辩
|
||||
|
||||
完整学习与答辩材料已集中到仓库根目录的 `learnning/`:
|
||||
|
||||
- [分章节学习目录](../learnning/README.md)
|
||||
- [答辩总览](../learnning/00-defense-overview.md)
|
||||
- [关键函数导航](../learnning/17-function-atlas.md)
|
||||
- [现场演示与排练](../learnning/14-demonstration.md)
|
||||
+10
-13
@@ -1,5 +1,9 @@
|
||||
# 安装与卸载
|
||||
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [在线安装](#在线安装) · [安装方式](#安装方式) · [包内安装](#包内安装) · [常用安装选项](#常用安装选项) · [从源码安装](#从源码安装) · [安装完成后](#安装完成后) · [更新](#更新) · [卸载](#卸载) · [发布校验](#发布校验)
|
||||
|
||||
发行包已经包含编译后的程序,普通用户安装时无需安装 GHC 或 Rust 工具链。
|
||||
|
||||
## 在线安装
|
||||
@@ -33,9 +37,11 @@ ChuSQL 支持三种安装方式:
|
||||
|
||||
完成系统目录初始化后,安装程序默认会启动 `chusql-server`;如不希望自动启动,可使用 `--no-start`(Windows 为 `-NoStart`)。
|
||||
|
||||
### Linux 用户服务
|
||||
|
||||
Linux 默认注册 `chusql-server.service` 为当前操作系统用户的 systemd 服务,并启用 linger 和开机启动,无需登录即可运行,异常退出后自动重启。需要可用的 systemd 用户会话;若启用 linger 时权限不足,按安装提示执行 `sudo loginctl enable-linger "$(id -un)"` 后重试。数据库管理员 `--user` 与运行服务的操作系统用户是两个独立身份。
|
||||
|
||||
`--no-start` 仅跳过本次启动,仍启用开机启动。非 systemd 环境可使用 `--no-service` 跳过服务注册,此时保留原有后台启动方式。
|
||||
`--no-start` 仅跳过本次启动,仍启用开机启动。非 systemd 环境可使用 `--no-service` 跳过服务注册,直接在后台启动进程。
|
||||
|
||||
```sh
|
||||
systemctl --user status chusql-server.service
|
||||
@@ -117,16 +123,6 @@ Linux 更新会先停止该安装目录中的 Web 和数据库进程,保留已
|
||||
|
||||
Linux 安装目录中保留 `uninstall.sh`。卸载时指定安装目录,数据目录默认读取现有配置。
|
||||
|
||||
已构建的 Linux 包可用隔离验证脚本测试,不修改正常安装的配置或数据:
|
||||
|
||||
```sh
|
||||
python3 scripts/test-linux-lifecycle.py releases/chusql-linux-x86_64.tar.gz
|
||||
```
|
||||
|
||||
该验证需要 Python 3,使用真实数据库程序,覆盖安装、运行中更新、配置与 SQL 数据保留、保留数据卸载、重装和完全卸载;基础验证通过 `--no-service` 隔离 systemd 服务。
|
||||
|
||||
添加 `--systemd` 可验证真实用户服务的注册、启动、更新及卸载。此选项要求不存在已有的 `chusql-server.service`,并且当前用户能够启用 linger(或已由管理员启用);临时单元链接会在测试后清理。本机 HTTP 发行站夹具用于验证在线安装流程,不依赖 GitHub 上已有 Release。
|
||||
|
||||
## 卸载
|
||||
|
||||
Linux / macOS:
|
||||
@@ -145,6 +141,7 @@ Windows:
|
||||
|
||||
Linux 卸载时会停止服务、取消开机启动并移除服务文件。用户的 linger 设置保留,其他用户服务仍可使用它。
|
||||
|
||||
## 发布真实性与后续签名方案
|
||||
## 发布校验
|
||||
|
||||
当前 SHA256 校验用于检测产物损坏,发布签名尚未实现。密钥管理、可信根、轮换、在线与离线验证的设计见[发布签名与验证](release-signing.md);设计中的验证器和签名安装流程目前不可用。
|
||||
> [!IMPORTANT]
|
||||
> SHA256 校验只检测产物损坏,不证明发布来源。发布签名、可信根和轮换验证尚未实现。
|
||||
@@ -1,168 +0,0 @@
|
||||
# 数据库范式分析与迁移
|
||||
|
||||
引擎模块 `ChuSQL.Core.Engine.Normalization` 提供只读范式分析。输入只有 schema 与用户声明,不读取样本数据,不访问或修改存储。
|
||||
|
||||
## 接口与示例
|
||||
|
||||
```haskell
|
||||
analyzeNormalization
|
||||
:: [(String, Column)]
|
||||
-> Declaration
|
||||
-> Either String Analysis
|
||||
```
|
||||
|
||||
调用示例:
|
||||
|
||||
```haskell
|
||||
analyzeNormalization [("student", TInt), ("course", TInt), ("name", TStr)]
|
||||
(Declaration
|
||||
{ declaredKeys = [["student", "course"]]
|
||||
, dependencies = [Dependency ["student"] ["name"]]
|
||||
, dependenciesComplete = True
|
||||
, atomicAttributes = Just True
|
||||
})
|
||||
```
|
||||
|
||||
这个声明违反 2NF、3NF、BCNF:非主属性 `name` 依赖复合候选键的真子集 `student`。
|
||||
|
||||
报告的 `witnesses` 给出相关字段,`condition` 给出范式条件。建议分为 `(student, name)` 与 `(course, student)`,无损连接并保持声明依赖。
|
||||
|
||||
## 声明与输入校验
|
||||
|
||||
`declaredKeys` 中每个键作为决定全部字段的依赖加入公理推导;检查器验证其最小性,并枚举所有最小超键。空键允许表达只有一行的关系约束。
|
||||
|
||||
字段名称使用 schema 中的确切名称,不自行处理 SQL 标识符解析。以下输入返回明确错误:
|
||||
|
||||
- 未知字段或重复字段。
|
||||
- 空依赖右部。
|
||||
- 非最小候选键。
|
||||
- 空 schema。
|
||||
|
||||
## 公理推导与范式条件
|
||||
|
||||
属性闭包以输入属性实现自反性,逐轮加入已满足左部的依赖右部,得到阿姆斯特朗增广和传递规则蕴含的闭包。检查器枚举字段子集及其闭包,检查完整的依赖投影。
|
||||
|
||||
| 范式 | 检查条件 |
|
||||
| --- | --- |
|
||||
| 1NF | 根据用户声明判断领域值原子性。 |
|
||||
| 2NF | 检查所有候选键的部分依赖。 |
|
||||
| 3NF | 检查非超键左部与非主属性右部。 |
|
||||
| BCNF | 检查非超键左部。 |
|
||||
|
||||
理论定义可参考[数据库课程讲义](https://documents.uow.edu.au/~jrg/235/slides/03databasenormalization/03databasenormalization.html)。
|
||||
|
||||
## 信息完整性与判定
|
||||
|
||||
### 领域值原子性
|
||||
|
||||
`atomicAttributes` 是用户关于领域值原子性的声明:
|
||||
|
||||
| 声明值 | 1NF 判定 |
|
||||
| --- | --- |
|
||||
| `Just True` | 满足。 |
|
||||
| `Just False` | 违反。 |
|
||||
| `Nothing` | 信息不足。 |
|
||||
|
||||
schema 类型不证明业务意义上的原子性。不满足 1NF 时更高范式也判违反;原子性未知时更高范式信息不足。
|
||||
|
||||
### 依赖完整性
|
||||
|
||||
只有 `dependenciesComplete = True` 才对更高范式给出满足或违反结论。这表示用户承诺给出了完整业务依赖的一个覆盖,而非必须列出所有推导依赖。完整的空依赖集有效。
|
||||
|
||||
未承诺完整时,更高范式返回 `Insufficient`,不输出违反证据或分解建议。此时 `candidateKeys` 仅是相对已知声明求得的键,不能视为完整业务候选键清单。
|
||||
|
||||
分析始终以声明为条件,不能证明数据库实际数据满足声明。
|
||||
|
||||
## 分解建议
|
||||
|
||||
分解建议仅针对第一条 BCNF 违反依赖 `X -> Y`,输出两个子关系:
|
||||
|
||||
- `X ∪ Y`
|
||||
- `R − (Y − X)`
|
||||
|
||||
该二元分解由依赖保证无损;依赖保持通过两个子关系的完整依赖投影之并是否蕴含原依赖集判定。
|
||||
|
||||
建议只是一轮分解,子关系可能仍需继续分析,不承诺一次达到 BCNF。依赖不保持时需跨关系检查原约束。
|
||||
|
||||
## 当前限制
|
||||
|
||||
- 最多接受 12 列,超过返回错误,避免指数枚举无界增长。
|
||||
- 提供 Haskell 只读 API、TCP 专用请求、CLI 和 Web 结构入口;不执行数据迁移。
|
||||
- 输入与输出有预算,超过预算返回明确错误,不截断为完整报告。
|
||||
|
||||
## TCP、CLI 与 Web 入口
|
||||
|
||||
以下只读入口已实现。分析只读取 schema,不能据此认定 SQL 数据满足声明或直接执行分解。实施状态以 .agents/context/progress.txt 为准。
|
||||
|
||||
### 协议与鉴权
|
||||
|
||||
`ChuSQL.Interface.Protocol` 的专用客户端请求 `normalization_analyze` 由 server 调用 engine API;不经最高权限专用的原始 `ReqStorage` 入口。请求包含 `api_version=1`、数据库与表标识、声明,以及可选的预期对象编号和 schema 指纹。server 在现有权限读门内以 catalog 解析现存对象,检查 CONNECT 与 SELECT;系统对象拒绝访问。事务内取该连接的 schema 快照,返回视图来源和对象编号,不能混入其他连接的快照。
|
||||
|
||||
声明字段固定为 `declared_keys`、`dependencies`(`lhs` / `rhs`)、`dependencies_complete`、`atomic_attributes`。完整性缺省 false,原子性缺省 null;不把 PRIMARY KEY、UNIQUE 或样本自动当作完整业务依赖。前端可以展示实际 schema 约束供用户选择,确认后才成为声明。候选键的 nullable 与 SQL UNIQUE 语义单独提示,不自动证明经典关系模型的键。
|
||||
|
||||
响应包含对象编号、schema 指纹、规范化声明、候选键、四项 findings 和一次分解建议。稳定标签使用 `1nf/2nf/3nf/bcnf`、`satisfied/violated/insufficient`;条件说明与机器字段分开。所有结论标明“以声明为条件”,无损连接标签不代表现有 SQL 数据或袋语义已经验证。
|
||||
|
||||
字段名称必须与返回 schema 的确切列名一致;对象名称使用现有标识符规则,数据库限定名必须与请求数据库一致。`schema_fingerprint` 使用 SHA256,编码为 `[库/表编号字符串数组, 列结构数组]`;每列按名称、类型、nullable、default、auto_increment、primary_key、unique、check 排列。它排除行数、索引统计与直方图,并绑定稳定对象编号,避免同名重建复用报告。对象编号在报告中使用 UInt64 范围内的正整数十进制字符串,避免浏览器数值精度损失。
|
||||
|
||||
拒绝未知协议版本、未知字段、重复字段、空右部、非最小声明键、空 schema 和超过 12 列。请求 JSON 上限 64 KiB、嵌套深度上限 64;依赖与键声明分别最多 256 项。服务端最多接纳 8 个请求,最多 2 个同时执行;排队、权限门等待与计算合计最多 5 秒。完整报告超过 64000 字节即拒绝,预留协议外层开销;客户端请求也为外层协议预留空间。超限返回 too_large,队列满返回 too_busy,超时返回 timeout,不截断成完整报告。取消执行或超时均释放执行槽和权限读门;断线请求最多运行至预算到期。
|
||||
|
||||
请求外层形状:
|
||||
|
||||
```json
|
||||
{"method":"normalization_analyze","request":{"api_version":1,"database":"sales","table":"orders","declaration":{"declared_keys":[["id"]],"dependencies":[],"dependencies_complete":false,"atomic_attributes":null}}}
|
||||
```
|
||||
|
||||
成功响应的 `status` 为 `normalization`,`analysis` 保存完整报告;可选 `expected_object` 与 `expected_schema_fingerprint` 放在 `request` 中。数据库必须与该连接的工作库一致;表可使用裸名或同库限定名。没有登录返回 unauthorized,系统对象拒绝分析;不会通过另一个连接代取事务视图。
|
||||
|
||||
### CLI 与 Web
|
||||
|
||||
CLI 元命令为 `\normalize TABLE --declaration FILE --format text|json`,通过当前登录连接和工作库请求。声明文件为 UTF-8 JSON,错误写 stderr;成功分析退出码 0,即使结论违反或信息不足;协议、权限、输入与运行错误退出非零。批处理使用独立 `--normalize` 模式,与 `--execute` 互斥,不把元命令发给 SQL 解析器。
|
||||
|
||||
Web 使用 `POST /api/normalization/analyze`,复用登录、CSRF 和 Cookie 串行规则。入口位于资源管理器展开表后的结构区:展示 schema、编辑声明、提交分析、显示证据和分解字段列表。完整性与原子性不预先勾选;没有自动执行分解按钮。编辑页分析必须携带既有页面事务标识,不能自动开启另一事务;SQL 控制台有事务时,分析请求携带该控制台身份并使用其快照。对象或 schema 改变返回冲突并要求重新分析。
|
||||
|
||||
声明文件示例(不自动声明完整性或原子性):
|
||||
|
||||
```json
|
||||
{
|
||||
"declared_keys": [["student", "course"]],
|
||||
"dependencies": [{"lhs": ["student"], "rhs": ["name"]}],
|
||||
"dependencies_complete": true,
|
||||
"atomic_attributes": true
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
csql -d school --normalize enrollment --declaration dependencies.json --format json
|
||||
```
|
||||
|
||||
交互模式:`\normalize enrollment --declaration "My Data/dependencies.json" --format text`。成功报告即使违反范式或信息不足也退出 0;输入、权限、协议或预算错误退出非零。文本输出保留与 JSON 输出相同的报告字段。Web 在资源管理器展开表后点击“范式分析”,输入声明 JSON;初始声明为空,完整性 false、原子性 null。报告给出候选键、违反证据和一次分解建议,只有分析按钮。
|
||||
|
||||
### 入口验收
|
||||
|
||||
验收覆盖 engine 报告的协议往返、CLI 文本/JSON 同值、Web 与 TCP 越权、跨库同名、删表重建、事务快照、超限及取消。沿用现有 14 项算法回归,不复制算法到客户端。
|
||||
|
||||
## 数据迁移设计(尚未实现)
|
||||
|
||||
只读入口已具备;在 [DOMAIN 约束](types.md#domain-约束设计尚未实现)、依赖目录和原子结构发布具备后实施迁移。
|
||||
|
||||
### 规划条件
|
||||
|
||||
迁移首版限定单业务表、二元分解、显式目标名,保留原表,不自动改名或删除。请求必须引用报告对象编号、schema 指纹及声明摘要;报告本身不是执行授权。要求来源 SELECT、数据库 CREATE;切换旧表需要来源所有权。每个目标取得新编号和执行者所有权,原 ACL 不自动复制。
|
||||
|
||||
经典 FD 与无损分解适用于关系集合,当前 SQL 保留重复行且有 NULL。首版只接受所有参与字段 NOT NULL、全行无重复且声明确实成立的数据;否则明确拒绝,不能通过 DISTINCT 悄悄丢行。候选键也必须验证唯一和非空,包括空键最多一行。FD 验证按引擎规范值相等规则分组,禁止使用文本序列化代替值比较。
|
||||
|
||||
目标数据为各字段投影的集合。验证每条声明 FD、全部候选键、目标约束及两目标连接与来源的完整多重集相等;无损性理论证明和实际重建检查均须通过。若投影依赖不能保持所有原依赖,首版拒绝迁移;后续有跨表约束执行器后再开放,不只弹提示继续。
|
||||
|
||||
### 结构依赖与发布
|
||||
|
||||
规划器枚举索引、默认值、CHECK、DOMAIN 引用、外键及其他已登记依赖,逐项生成保留或拒绝理由。单目标可表达的约束绑定后迁移;跨目标 CHECK、无法保存的索引/唯一约束及任何无法枚举的受支持依赖阻止发布。不得根据 SQL 文本替换列名。客户端保存的任意 SQL 无法枚举,保留来源表可避免静默改坏;首版不提供透明替换。
|
||||
|
||||
执行流程为 prepare → validate → publish:prepare 创建不对业务 catalog 可见的暂存对象;validate 固定来源视图并生成目标、验证重建;publish 在短结构锁内复查身份、权限、对象编号、schema 与数据版本,原子登记全部目标。来源变化返回 serialization_failure,不自动重试。暂存对象需要空间预算、作业编号和取消清理。
|
||||
|
||||
现有事务禁止 DDL,不能拼接 CREATE/INSERT 伪装成原子迁移。实施前必须加入包含 catalog、页文件、索引和所有权声明的迁移 WAL 发布组;提交前暂存文件可清理,提交后故障进入 recovery_required 并重放到完整目标组。对象编号可以跳号但不得复用。恢复仅依据作业状态与 WAL 清理暂存对象,不能按名称猜测。
|
||||
|
||||
后续替换来源需独立设计:稳定对象引用、授权重绑定、完整依赖图与原子交换。保留原对象及其编号直到该方案验收;当前阶段不提供多轮自动 BCNF 化。
|
||||
|
||||
### 迁移验收
|
||||
|
||||
验收包含重复/NULL/不成立 FD 的拒绝、依赖不保持、跨目标约束、并发修改、撤权、名称占用、取消、空间不足,以及每个 WAL/文件发布边界的崩溃恢复;失败不得留下部分可见目标。
|
||||
+28
-47
@@ -1,5 +1,9 @@
|
||||
# 性能基准
|
||||
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [测试方法与复现](#测试方法与复现) · [实测环境](#实测环境) · [性能测试结果](#性能测试结果) · [事务基准](#事务基准) · [瓶颈与使用范围](#瓶颈与使用范围)
|
||||
|
||||
## 测试方法与复现
|
||||
|
||||
`benchmark/` 中的应用名为 `benchmark4csql`。它启动真实 `chusql-server`,通过本机 TCP 使用共享客户端协议,服务端调用 Rust 存储;不以纯内存引擎测量代替端到端性能。每次运行创建独立临时数据库,结束后停止服务并清理数据。任一 SQL、结果校验或并发请求失败会使运行失败。
|
||||
@@ -7,15 +11,18 @@
|
||||
在 `benchmark/` 中构建优化版本,并在 PATH 中放置 release 存储动态库:
|
||||
|
||||
```powershell
|
||||
$env:TMP = 'D:\Workspace\ChuSQL\logs\cli-test-tmp'
|
||||
$env:TEMP = $env:TMP
|
||||
$env:PATH = 'D:\Workspace\ChuSQL\chusql-core\storage\target\release;' + $env:PATH
|
||||
$repoRoot = (Resolve-Path '..').Path
|
||||
$benchmarkTemp = Join-Path $repoRoot 'logs/benchmark-tmp'
|
||||
New-Item -ItemType Directory -Force -Path $benchmarkTemp | Out-Null
|
||||
$env:TEMP = $benchmarkTemp
|
||||
$env:TMP = $benchmarkTemp
|
||||
$env:PATH = (Join-Path $repoRoot 'chusql-core/storage/target/release') + ';' + $env:PATH
|
||||
$env:GHC_CHARENC = 'UTF-8'
|
||||
stack build chusql-core-model chusql-core-engine chusql-interface chusql-server chusql-bootstrap benchmark4csql --force-dirty --ghc-options=-O2
|
||||
stack exec -- benchmark4csql 10000 50000
|
||||
```
|
||||
|
||||
首次运行前从 `chusql-core/storage/` 执行 `cargo build --release`,并创建上面的临时目录。Linux 使用 `LD_LIBRARY_PATH` 指向 release 存储目录。两个参数分别为用户数、订单数,默认 10,000 / 50,000,均需至少 100。
|
||||
首次运行前从 `chusql-core/storage/` 执行 `cargo build --release`。Linux 使用 `LD_LIBRARY_PATH` 指向 release 存储目录。两个参数分别为用户数、订单数,默认 10,000 / 50,000,均需至少 100。
|
||||
|
||||
读取场景先预热一次,再采样七次,记录均值、p50、p95、最大延迟与请求数/秒。p95 使用最近秩;七次采样的 p95 等于最大值,只适合比较本次场景,不能作为稳定的生产尾延迟。并发场景使用独立的已认证 TCP 会话,1、4、8 个客户端各执行 100 次点查,同步开始;吞吐以整个并发阶段的墙上耗时计算,连接与登录不计入该阶段。
|
||||
|
||||
@@ -33,7 +40,7 @@ stack exec -- benchmark4csql 10000 50000
|
||||
|
||||
## 性能测试结果
|
||||
|
||||
完整运行返回退出码 0,输出 `PRESSURE_OK all result checks passed` 和 `BENCHMARK_OK all scenarios completed`。原始日志保存于本机 `logs/benchmark-performance.log`。下面保留该次成功运行的数据;首次失败运行不参与统计。
|
||||
下表为 2026-10-05 的成功基准样本,不代表当前提交的重新测量。完整运行须通过结果校验并输出 `BENCHMARK_OK`;原始日志为本机 `logs/benchmark-performance.log`。
|
||||
|
||||
启动、引导、认证及创建基准库合计 0.62 秒。纯 TCP Ping 200 次平均 0.100 ms;schema catalog 200 次平均 0.267 ms。主键点查另外连续执行 100 次,平均 0.300 ms,100/100 命中。
|
||||
|
||||
@@ -80,7 +87,6 @@ stack exec -- benchmark4csql 10000 50000
|
||||
| 订单加载,500 行/提交 | 50,000 | 3,915.697 | 12,769.12 |
|
||||
| 512 字节宽行加载,500 行/提交 | 5,000 | 1,084.740 | 4,609.40 |
|
||||
| writes 逐条持久化 INSERT | 200 | 1,119.0 | 178.73 |
|
||||
| writes 事务中 200 条 INSERT,含 BEGIN / COMMIT | 200 | 67,549.539 | 2.96 |
|
||||
| batch_t 加载,100 行/提交 | 10,000 | 1,309.4 | 7,637.09 |
|
||||
| items 加载,100 行/提交 | 50,000 | 6,336.8 | 7,890.42 |
|
||||
|
||||
@@ -101,45 +107,9 @@ stack exec -- benchmark4csql 10000 50000
|
||||
|
||||
回滚后校验 `writes.id = 1` 的 age 仍为 2,事务中写入的 0 没有落入正式数据。删除后 writes 行数为 200。加载、连接及并发点查的结果校验全部通过。
|
||||
|
||||
## 性能瓶颈分析
|
||||
## 事务基准
|
||||
|
||||
本节描述首次基准时的实现;事务差分优化后的结果见文末复测。
|
||||
|
||||
### 1. 事务提交的整库差分是首要排查点
|
||||
|
||||
本次 200 条事务 INSERT 比 200 次独立持久化 INSERT 慢约 60 倍。事务阶段开始时,数据库已有 10,000 用户、50,000 订单及 5,000 宽行,事务只修改 writes 表。事务内一次 400 行 UPDATE 仅耗时 0.5 ms,说明不能将整个事务阶段的 67.55 秒简单归因于 SQL 更新计算或 fsync。
|
||||
|
||||
代码依据:服务端 [Session.hs](../chusql-server/src/ChuSQL/Server/Session.hs) 的 BEGIN 和 COMMIT 都获取整库快照。`transactionDiffs` 对具有整数 id 的表先走 keyedDiff,即使 staged 与 base 完全相同,也会建立关联列表并对每行执行线性 `lookup` / `any`;50,000 行未修改表仍可能进行平方级比较。COMMIT 分别调用冲突检查和操作生成,可能重复计算差分。这是与实测异常最吻合的瓶颈候选。
|
||||
|
||||
当前没有 BEGIN、逐条语句、COMMIT 的独立计时或 CPU profile,不能给各部分分摊比例。下一步应先拆分计时,再对未修改表快速跳过、以 Map / HashMap 做按 id 差分、复用一次差分计算结果,并考虑记录实际修改表与键。整库快照及内存统计重算也需要测量。不能据当前实现假设把 Web 编辑放进一个大事务就会更快。
|
||||
|
||||
### 2. 不同绑定的相关子查询仍重复扫描
|
||||
|
||||
20 个 EXISTS 绑定平均 2.06 秒,而单次 50,000 行订单扫描约 114 ms;两者量级相符。缓存只能合并相同绑定,这里的 20 个用户 id 都不同,因此仍需多次读取订单。执行路径 [Eval.hs](../chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs) 将子查询逐绑定执行,没有半连接,EXISTS 也先得到子查询结果再判断是否为空。
|
||||
|
||||
优先验证 `orders.user_id` 二级索引及基于统计的点查选择,再评估半连接、EXISTS 找到首行即可停止、或单次物化内层匹配键。这个推断有查询结构和耗时量级支持,但本次没有对索引版相关子查询做对照,不把预期收益当作已测结果。
|
||||
|
||||
### 3. 扫描、结果传输与全量排序限制大结果查询
|
||||
|
||||
50,000 行扫描比 10,000 行扫描慢约 4.8 倍。无索引 name 点查只返回一行,耗时仍接近用户全表扫描;索引后均值下降约 64 倍,说明扫描工作量具有决定性影响。宽行加载吞吐降至约 4,609 行/秒,低于短行的约 1.3 万行/秒。
|
||||
|
||||
`sortRows` 使用完整 `sortBy` 后再 LIMIT,返回 100 行并不代表只处理 100 行。当前全表结果会经过存储解码、引擎求值及 TCP JSON 编码。下一步可测 Top-K 排序、分页或流式返回,以及存储、引擎、编码各阶段的时间和分配量。本次没有冷缓存、磁盘裸吞吐、分阶段 CPU 或内存峰值测量,无法把扫描耗时全部归到磁盘。
|
||||
|
||||
### 4. 批量提交有效,读并发扩展低于线性
|
||||
|
||||
批量加载达到约 1.3 万行/秒,200 次独立提交只有约 179 行/秒。数据和已打开表数量不同,这不是严格的单变量对照;但与 WAL 提交、表刷盘和检查点固定成本被摊薄的执行路径一致。Rust 的 `group_via_wal` 还会刷所有已打开表,应单独测量并评估只刷本组实际修改表的收益及恢复正确性。
|
||||
|
||||
8 客户端点查总吞吐约为 1 客户端的 4.8 倍,单请求均值从 0.290 ms 上升至 0.481 ms。表明只读请求有并发收益,也存在共享资源、调度或协议开销。本次没有混合读写或并发写压测,不据此推断写扩展能力;当前每库写独占的限制仍在。
|
||||
|
||||
## 使用范围
|
||||
|
||||
这是单机回环、主要为热缓存、管理员身份的单次基准,不是生产容量承诺。未覆盖远程网络、鉴权角色开销、崩溃恢复耗时、并发写冲突或大型宽表。七次读采样和单次写测量不足以建立统计置信区间,后续优化应使用相同参数、多次独立运行和分阶段 profile 比较。
|
||||
|
||||
## 2026-10-05 事务差分优化复测
|
||||
|
||||
使用相同机器、优化构建及 10,000 用户、50,000 订单规模运行完整基准,所有场景及结果校验通过(`BENCHMARK_OK`)。日志为 `logs/repair-txn-performance.log`。
|
||||
|
||||
COMMIT 跳过未修改表,以 IntMap / IntSet 查询整数键差分,冲突检查与操作生成共享差分;保持原行顺序及重复 id 的首行查找语义。
|
||||
同日、相同机器和 10,000 用户 / 50,000 订单规模的独立测量,结果校验通过。日志为本机 `logs/repair-txn-performance.log`;以下数据与上面的场景不是同一次采样。
|
||||
|
||||
| 指标 | 耗时(ms) |
|
||||
| --- | ---: |
|
||||
@@ -149,8 +119,19 @@ COMMIT 跳过未修改表,以 IntMap / IntSet 查询整数键差分,冲突
|
||||
| 事务内 200 条 INSERT | 41.129 |
|
||||
| COMMIT | 376.345 |
|
||||
|
||||
事务总耗时从首次的 67,549.539 ms 降至 687.355 ms,约快 98 倍,吞吐从 2.96 升至 290.97 行/秒;本次事务写入约比逐条持久化快 1.65 倍。两次独立运行未建立统计置信区间,倍数仅描述这两次测量。
|
||||
事务写入约 290.97 行/秒。BEGIN 与 COMMIT 合计约占总耗时的 94%;COMMIT 还包含差分、冲突检查和持久化,不能全部归因于快照。提交跳过未修改表,整数键差分使用 `IntMap` / `IntSet`,冲突检查与操作生成复用差分。
|
||||
|
||||
BEGIN 与 COMMIT 仍获取整库快照,合计约占事务总耗时的 94%;COMMIT 计时也包含差分、冲突检查和持久化,不能全部归因于快照。整库快照传输、修改表比较及刷盘仍需进一步分阶段测量。
|
||||
## 瓶颈与使用范围
|
||||
|
||||
20 个不同绑定的相关 EXISTS 平均 2,135.218 ms(首次 2,061.770 ms),该瓶颈仍存在,本轮没有改变子查询执行方式。
|
||||
| 观察 | 当前边界与测量方向 |
|
||||
| --- | --- |
|
||||
| 整库事务快照成本高 | BEGIN / COMMIT 仍读取整库,需分别测传输、修改表比较与刷盘 |
|
||||
| 不同外层绑定重复扫描 | 20 个不同绑定的相关 EXISTS 在事务基准运行中均值为 2,135.218 ms;相同绑定缓存不等于半连接或通用物化 |
|
||||
| 大结果与排序有全量成本 | 扫描包含解码、执行、TCP JSON;完整排序后 LIMIT,可分别评估 Top-K、分页和流式返回 |
|
||||
| 批量提交摊薄固定开销 | 场景的数据库大小和已打开表数量不同,不能当作严格单变量对照;刷盘优化仍须验证恢复 |
|
||||
| 读并发低于线性扩展 | 8 客户端吞吐约为 1 客户端的 4.8 倍;未测混合读写或并发写 |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 这是单机回环、主要为热缓存、管理员身份的基准样本,不是生产容量承诺。未测远程网络、权限目录规模、冷缓存、内存峰值、崩溃恢复耗时或并发写冲突;七次读采样和单次写测量不足以建立统计置信区间。
|
||||
|
||||
后续比较应保持机器、数据分布和构建参数一致,多次独立运行,并记录各阶段时间与分配量。
|
||||
@@ -1,31 +0,0 @@
|
||||
# 发布签名与验证设计
|
||||
|
||||
本文定义尚未实现的发行安全方案。当前操作见[安装文档](install.md),实施状态以 .agents/context/progress.txt 为准。先实现离线验证,再接入在线安装;该工作可独立于查询和存储扩展推进。
|
||||
|
||||
## 选择与信任入口
|
||||
|
||||
采用标准 TUF root/targets/snapshot/timestamp 元数据和 Ed25519 签名,使用经过维护的实现与标准测试向量,不自行定义签名 JSON 规范。Ed25519 算法依据 [RFC 8032](https://datatracker.ietf.org/doc/rfc8032/),角色、轮换、版本及过期验证依据 [TUF 规范](https://theupdateframework.github.io/specification/v1.0.36/)。SHA256 保留为产物散列,签名覆盖其可信元数据来源。
|
||||
|
||||
可信初始 root 随独立取得的安装器/验证器发布,记录版本与公钥;不能从同一个未认证下载响应接受任意公钥。首次下载安装器仍依赖用户可信取得该安装器这一入口,不能宣称脚本内公钥解决首次下载脚本被替换。升级客户端保存可信元数据的版本与过期状态,拒绝回退、混配和过期;状态缺失不等同于已完成反回退验证。
|
||||
|
||||
targets 绑定确切文件名、长度、SHA256、平台/架构、版本、提交号、组件清单与最低数据格式要求。snapshot 固定元数据版本/散列,timestamp 指向 snapshot。禁止通配匹配产物、跨平台替代和同版本静默覆盖;签名通过后才允许解包或执行 bootstrap。
|
||||
|
||||
## 密钥与发布操作
|
||||
|
||||
root 使用离线 2-of-3 阈值密钥,分开保管及加密备份;CI 不持有 root 私钥。targets 发布密钥限定于受保护发布环境,仅受信任标签与人工发布流程可调用;PR 构建、普通日志、构建缓存和发行包均不能读取私钥。snapshot/timestamp 使用独立在线密钥,权限和有效期按角色设置;拟定 targets/snapshot 30 天、timestamp 7 天、root 1 年,并通过到期监控续签。
|
||||
|
||||
轮换 root 按连续版本发布,同时满足旧 root 与新 root 阈值;离线备份定期做恢复演练。在线密钥泄露时停止发布,用 root 撤销并更换受影响角色密钥,重新生成元数据;所有 root 信任丢失时需要经独立可信渠道重新引导,不能靠损坏的 root 自证恢复。
|
||||
|
||||
发布顺序为构建与模块/所有者门禁 → 装配确切产物 → 计算散列 → 签 targets/snapshot → 上传产物和版本化元数据 → 最后发布 timestamp。任何产物字节变化均需重新签署。签名不证明构建内容正确,平台真机验收仍单列。
|
||||
|
||||
## 安装与兼容性
|
||||
|
||||
新增小型跨平台验证器,打包随安装器提供;验证器不可作为首个未验证的临时下载再立即执行。在线安装先验证元数据链、版本、有效期与目标平台,再下载并验长度/散列,之后才解包。签名缺失、密钥未知、元数据过期、系统时间无法可信校验等均明确失败,不降级成仅 SHA256。
|
||||
|
||||
本地离线安装使用随包 root 链、targets 和 snapshot 验证真实性与完整性;缺少在线 timestamp 时明确说明不能证明最新版本。离线模式必须显式选择且不联网;已有可信版本仍禁止回退,元数据过期仍失败,需要重新取得有效离线元数据。历史无签名包不纳入新安装器的可信安装路径。
|
||||
|
||||
系统原生代码签名分别设计接入 Windows Authenticode 和 macOS Developer ID/notarization;TUF 不能替代操作系统签名,第一阶段也不声称消除 SmartScreen/Gatekeeper 提示。Linux $ORIGIN、在线下载与 macOS 发行继续按现有实机待办验收。
|
||||
|
||||
## 验收条件
|
||||
|
||||
验收覆盖合法包、字节篡改、错误平台、截断、签名替换、重复键/非法元数据、未知密钥、过期、回退、元数据混配、连续 root 轮换/阈值不足、撤销、断网离线、下载中断与私钥日志检查;三平台从干净安装目录验证拒绝路径不会执行包内程序。
|
||||
@@ -1,15 +0,0 @@
|
||||
# ChuSQL v26.10.1
|
||||
|
||||
ChuSQL 是一个以 SQL 为主要接口的关系数据库,由 Haskell 查询引擎和 Rust 存储层组成。服务端管理数据与用户会话,用户可通过命令行客户端或浏览器中的 Web 界面访问。
|
||||
|
||||
这是 ChuSQL 的第一个正式发布版,提供建表、数据增删改查、连接、聚合、子查询和 CTE,以及索引、查询优化、事务、保存点、WAL 崩溃恢复和用户/角色授权。
|
||||
|
||||
本版还提供运行时类型与函数基础:支持 String、List、Maybe、Tuple 等类型,以及 length、not 函数;复合值可通过 SQL 构造、查询和持久化,CLI 与 Web 均可显示。
|
||||
|
||||
发行包支持 Windows x86_64 和 Linux x86_64,包含数据库服务端、初始化工具、CLI、Web 界面和安装/卸载脚本。下载下方压缩包,使用包内 install.ps1(Windows)或 install.sh(Linux)安装,可选择 CLI 或 Web 组件,无需自行安装 Haskell 或 Rust 工具链。每个包附 SHA256 校验文件。
|
||||
|
||||
安装与操作说明:[安装指南](https://github.com/TheBadRoger/ChuSQL/blob/v26.10.1/docs/install.md)、[SQL 语法](https://github.com/TheBadRoger/ChuSQL/blob/v26.10.1/docs/syntax.md)、[运行时类型](https://github.com/TheBadRoger/ChuSQL/blob/v26.10.1/docs/runtime-types.md)。
|
||||
|
||||
首版适合学习、开发和小规模内部应用试点。事务采用整库内存快照,同库写入串行;DECIMAL 使用浮点表示;尚无运行时类型的完整比较/索引能力。关键业务上线前应完成真实负载测试及备份恢复演练。升级已有数据前请备份,并避免用旧二进制打开新增类型的数据。
|
||||
|
||||
本版已通过所有者全量门禁;发布流程要求 Linux 测试与两个平台的发行包安装验证全部成功后上传正式下载包。
|
||||
@@ -1,88 +0,0 @@
|
||||
# 运行时类型与函数基础
|
||||
|
||||
阶段 A 提供固定身份的内建类型、函数注册表、schema 驱动的绑定和类型化查询入口。
|
||||
完整 capability、运算符注册、转换、用户 ADT 与 Extension 留在后续阶段。
|
||||
|
||||
## SQL 使用
|
||||
|
||||
```sql
|
||||
CREATE TABLE typed_values (
|
||||
id Int,
|
||||
name String,
|
||||
numbers [Int],
|
||||
label Maybe String,
|
||||
pair (Int, String)
|
||||
);
|
||||
|
||||
INSERT INTO typed_values (id, name, numbers, label, pair)
|
||||
VALUES (1, 'abc', LIST<Int>(1, 2), MAYBE<String>(), TUPLE<Int, String>(3, 'x'));
|
||||
|
||||
SELECT length(name), numbers, label, pair FROM typed_values;
|
||||
|
||||
UPDATE typed_values SET numbers = LIST<Int>(9), label = MAYBE<String>('ready') WHERE id = 1;
|
||||
|
||||
SELECT LIST<Maybe Int>(MAYBE<Int>(), MAYBE<Int>(42));
|
||||
SELECT not(TRUE), length('你好');
|
||||
```
|
||||
|
||||
类型语法支持 `Int`、`Double`、`Bool`、`String`、`[a]` / `List a`、`Maybe a` 和 `(a, b, ...)`。
|
||||
构造器中的类型参数是完整逻辑类型;空列表和 Nothing 也有确定类型。
|
||||
`MAYBE<a>()` 表示 Nothing,`MAYBE<a>(value)` 表示 Just;不能用 SQL NULL 代替复合值内部的 Nothing。
|
||||
SQL 可空列仍接受 NULL,和 Maybe 的 Nothing 保持区别。
|
||||
构造器按完整类型校验,不隐式转换 List 的元素类型。
|
||||
|
||||
内建签名为 `length :: String -> Int` 和 `not :: Bool -> Bool`,名称不区分大小写。
|
||||
SQL 参数为 NULL 时结果为 NULL;具体非空参数仍校验类型。
|
||||
尚无运行时 Eq / Ord / Hash 字典,复合类型的比较、排序、分组、极值和索引不会自动取得能力。
|
||||
Web 显示复合值及 SQL 构造器预览,复合字段通过 SQL 控制台修改。
|
||||
|
||||
## 身份与编码
|
||||
|
||||
`ChuSQL.Core.Runtime` 是共享模型中的权威定义;Engine 模块导出同一组身份。
|
||||
内建构造器编号固定为 Int=1、Double=2、Bool=3、String=4、List=5、Maybe=6、Tuple=7。
|
||||
TypeId 包含已校验的完整构造器应用,比较和恢复不依赖注册顺序或随机哈希。
|
||||
FunctionId 固定为 length=1、not=2;内建元数据可直接重建注册表。
|
||||
|
||||
逻辑 TypeExpr 与 PhysicalType 分开;List、Maybe、Tuple 使用递归 sequence、optional、product 表示。
|
||||
schema 保存版本化的 TypeId,协议值保存版本、类型身份和 payload;包括 String 在内的运行时值共用编码。
|
||||
Rust 的通用行、目录和 WAL 路径保存该表示,读取时由共享模型验证版本、类型、元素和字段个数。
|
||||
未知版本、身份溢出、类型不符、非有限浮点数及损坏 payload 明确返回错误。
|
||||
内建类型不可删除,复合类型的依赖完整包含在 TypeExpr 中。
|
||||
旧标量 schema 和协议继续读取;包含运行时类型的数据不能交给不支持该编码的旧客户端。
|
||||
|
||||
## 绑定与执行
|
||||
|
||||
`Runtime.Binding` 提供独立的 TypedExpression 和 TypedPlan,绑定只接受 catalog schema。
|
||||
表达式和计划构造器隐藏,调用者必须经过 Binder;执行使用已解析的 TypeId / FunctionId,并校验存储行。
|
||||
该 API 覆盖单行、表扫描、投影和过滤,可构造嵌套复合值及函数调用。
|
||||
列引用按当前输入 schema 的槽位绑定,列名保留用于诊断和位置校验。独立调用 `evaluateExpression`
|
||||
必须按绑定时的 schema 顺序提供行;位置不符或类型不符明确报错。`executePlan` 在扫描边界
|
||||
校验完整行并按 schema 排序,投影后的表达式重新绑定到子计划输出槽位。
|
||||
|
||||
SQL SELECT 通过 `Runtime.Query.bindQuery` 生成不可直接构造的 BoundQuery,再由 `executeQueryM` 执行。
|
||||
绑定计划保存输出 schema 与类型身份;现有 RelOp 作为关系执行和优化的兼容桥,输出按位置校验,保留重复别名。
|
||||
类型化函数和构造器节点也进入 INSERT、UPDATE、CHECK、聚合参数和子查询。
|
||||
日期、时间戳、Blob 等阶段 A 外的类型保留明确的 CompatibilityType。
|
||||
`queryTypes` 用 `DomainType 名称 基础类型` 保留每一层 DOMAIN 引用,包括嵌套领域和兼容类型。
|
||||
直接列引用及派生表别名保留该身份;函数结果使用函数声明的返回类型,不继承参数的 DOMAIN。
|
||||
领域名称来自当前库 schema 的解析结果;定义版本与约束绑定仍待完整 bound IR 实现。
|
||||
SQL 计划在优化完成后由 `Runtime.Relation` 将投影、过滤、计算、聚合参数和连接条件中的列引用
|
||||
绑定为 `ColumnSlot`。扫描、点查和范围查询在存储边界按算子布局排列返回行;相关子查询
|
||||
按自己的局部布局和外层行布局绑定槽位。局部引用使用 ColumnSlot,外层引用使用 OuterColumnSlot,
|
||||
保存局部布局宽度与外层相对位置;重新绑定时外层引用只在外层布局中查找,防止同名局部列捕获。
|
||||
执行槽位不接受语义层再次推导,语义输入必须使用未绑定列。哈希/索引连接仍使用列名识别等值键。
|
||||
投影单向转换为已有 Compute 算子,使用绑定列槽位取值;保留输出顺序、重复列及空投影的行数。
|
||||
排序转换为 BoundSort,分组转换为 BoundAggregate;键使用绑定槽位,列名保留为输出标签和诊断。
|
||||
排序每行计算一次键并保留同键行序,分组保留首次出现顺序及空输入全局聚合行为。
|
||||
该步骤仍复用 Expr/RelOp 和扁平化外层行;多级词法作用域/来源编号及 schema 版本尚未引入。
|
||||
绑定后的计划用于执行;结构改写必须在绑定前完成,或重新按新布局绑定。
|
||||
执行前重新取得存储 schema,比较查询依赖表的完整列定义;依赖收集覆盖来源树、连接条件、
|
||||
投影和过滤表达式里的嵌套查询,并递归追踪来源列使用的 DOMAIN 定义。缺表、列定义变化或 schema
|
||||
读取失败明确返回错误,行数据、统计及无关表、无关 DOMAIN 的结构变化不使绑定失效。
|
||||
Table 与 TableInfo 保留 catalog 的库编号和对象编号;执行前比较身份,可识别同名同结构的对象替换。
|
||||
内存 Server 目录和存储快照同样保留身份。TableInfo 输出十进制字符串编号,读取兼容数字编号,
|
||||
校验无符号 64 位范围;旧响应缺少编号或原始目录对象编号为零时,以列定义校验作为兼容路径。
|
||||
身份与列定义校验不能替代服务端读写门或未来的原子 schema 版本校验。
|
||||
旧 ColumnType 和运算符实现尚未整体移除;统一 capability、cast 和 rewrite 将在后续阶段收拢。
|
||||
|
||||
测试覆盖稳定编码、kind / 参数校验、共享协议、SQL 调用和复合列、实际存储重载、HTTP/TCP 与客户端显示。
|
||||
+5
-3
@@ -1,5 +1,9 @@
|
||||
# SQL 语法
|
||||
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [查询、名称与事务](#查询名称与事务) · [自定义类型 DOMAIN](#自定义类型-domain) · [账号与权限](#账号与权限)
|
||||
|
||||
客户端选项与元命令见 [commands.md](commands.md),类型与身份字段分别见 [types.md](types.md) 和 [users-and-roles.md](users-and-roles.md)。
|
||||
|
||||
## 查询、名称与事务
|
||||
@@ -7,7 +11,6 @@
|
||||
### 表名与列名
|
||||
|
||||
- 表级 DDL 可使用当前库限定名,例如选定 `sales` 后执行 `CREATE TABLE sales.orders (id INT)`;必须先选库,跨库 DDL 会被拒绝。
|
||||
|
||||
- 选定数据库后语句里直接写表名即可,例如 `SELECT name FROM orders WHERE orders.id = 1`。
|
||||
- 表名可以带库前缀(`sales.orders`),列名可以带表限定符(`orders.id`)或库限定符(`sales.orders.id`),这几种写法指向同一张表和同一个列。
|
||||
- 只有候选唯一时才自动推导前缀;同名的表或列出现在多个候选上会以 `ambiguous table` / `ambiguous column` 报错,需要写全。
|
||||
@@ -132,7 +135,7 @@ SHOW ROLES;
|
||||
|
||||
`SUPERUSER` 不依赖启动参数里的管理员名称(`--user`),也不会通过角色成员关系继承。禁用登录身份会拒绝登录;禁用权限角色会停止贡献继承权限。属性修改会使受影响的既有连接失效,下一次请求返回 `unauthorized` 后关闭。最后一个启用且可以登录的最高权限身份不能删除、禁用、取消登录或取消最高权限。
|
||||
|
||||
服务启动时迁移旧账号及角色,保留账号编号、口令、时间和原有授权关系。旧用户名与角色名重名时明确拒绝迁移,不合并身份。删除身份会同时清理其授权和两向成员关系;重建同名身份会分配新编号。
|
||||
删除身份同时清理授权和双向成员关系;重建同名身份分配新编号。升级迁移的兼容边界见 [用户与角色](users-and-roles.md#升级兼容)。
|
||||
|
||||
常用角色和授权命令:
|
||||
|
||||
@@ -169,7 +172,6 @@ REVOKE SELECT ON sales.orders FROM reviewer;
|
||||
|
||||
系统目录访问还受目录管理属性控制,具体能力见 [用户与角色](users-and-roles.md)。
|
||||
|
||||
|
||||
目录管理属性示例(仅 SUPERUSER 可设置):
|
||||
|
||||
```sql
|
||||
|
||||
+50
-19
@@ -1,6 +1,10 @@
|
||||
# 自定义类型与预定义类型
|
||||
# 类型、函数与类型目录
|
||||
|
||||
本文描述 ChuSQL 当前实现。预定义类型是引擎已经支持的基础类型;自定义类型使用 `DOMAIN` 给基础类型命名,便于多个表列复用同一类型定义。
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [自定义类型 DOMAIN](#自定义类型-domain) · [预定义类型的含义](#预定义类型的含义) · [系统类型目录字段](#系统类型目录字段) · [运行时类型与函数](#运行时类型与函数) · [DOMAIN 约束设计(尚未实现)](#domain-约束设计尚未实现)
|
||||
|
||||
ChuSQL 支持基础类型、运行时复合类型和按数据库保存的 DOMAIN。DOMAIN 为基础类型命名,便于多个表列复用同一定义。
|
||||
|
||||
## 自定义类型 DOMAIN
|
||||
|
||||
@@ -47,22 +51,49 @@ DOMAIN 按数据库保存,保留名称并复用基础类型的校验与运算
|
||||
|
||||
首次安装完成管理员初始化后登记类型。重复引导保留一致的记录并补齐缺失项;同名定义冲突会报错,停止安装启动流程,不覆盖现有定义。
|
||||
|
||||
## 运行时类型与函数
|
||||
|
||||
```sql
|
||||
CREATE TABLE typed_values (
|
||||
id Int,
|
||||
name String,
|
||||
numbers [Int],
|
||||
label Maybe String,
|
||||
pair (Int, String)
|
||||
);
|
||||
|
||||
INSERT INTO typed_values (id, name, numbers, label, pair)
|
||||
VALUES (1, 'abc', LIST<Int>(1, 2), MAYBE<String>(), TUPLE<Int, String>(3, 'x'));
|
||||
|
||||
SELECT length(name), numbers, label, pair FROM typed_values;
|
||||
|
||||
UPDATE typed_values SET numbers = LIST<Int>(9), label = MAYBE<String>('ready') WHERE id = 1;
|
||||
|
||||
SELECT LIST<Maybe Int>(MAYBE<Int>(), MAYBE<Int>(42));
|
||||
SELECT not(TRUE), length('你好');
|
||||
```
|
||||
|
||||
| 类型或函数 | 写法与语义 |
|
||||
| --- | --- |
|
||||
| 标量 | `Int`、`Double`、`Bool`、`String` |
|
||||
| 列表 | `[a]` / `List a`;`LIST<a>(...)` 按完整类型校验,不隐式转换元素类型 |
|
||||
| 可选值 | `Maybe a`;`MAYBE<a>()` 为 Nothing,`MAYBE<a>(value)` 为 Just |
|
||||
| 元组 | `(a, b, ...)`;`TUPLE<a, b>(...)` 校验各字段类型 |
|
||||
| `length` | `String -> Int` |
|
||||
| `not` | `Bool -> Bool` |
|
||||
|
||||
空列表和 Nothing 也有确定类型。SQL 可空列的 `NULL` 与 Maybe 内部的 Nothing 不同,不能互相替代。内建函数名称不区分大小写,NULL 参数传播为 NULL,非空参数仍校验类型。
|
||||
|
||||
> [!NOTE]
|
||||
> 复合类型尚无 SQL Eq / Ord / Hash 能力,不能自动用于比较、排序、分组、极值或索引。Web 可以显示复合值及构造器预览,修改须通过 SQL 控制台。
|
||||
|
||||
### 类型身份与持久化
|
||||
|
||||
内建构造器编号固定为 `Int=1`、`Double=2`、`Bool=3`、`String=4`、`List=5`、`Maybe=6`、`Tuple=7`;函数编号固定为 `length=1`、`not=2`。完整 `TypeId` 与逻辑类型、物理表示分开,schema 和值使用版本化编码。未知版本、类型或载荷不匹配、非有限浮点数明确报错。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 旧标量编码可继续读取;包含运行时复合类型的数据不能交给不支持该编码的旧客户端。
|
||||
|
||||
## DOMAIN 约束设计(尚未实现)
|
||||
|
||||
以下约束与 ALTER 能力是后续设计,当前 DOMAIN 仍只提供类型命名。实施依赖 [bound IR](architecture.md#bound-ir),先固化类型、列身份与表达式求值规则。
|
||||
|
||||
### 约束与默认值语义
|
||||
|
||||
定义扩展为基础类型引用、可选 DEFAULT、NOT NULL、命名 CHECK 及定义版本。CHECK 只接受绑定的单值 `VALUE` 表达式和确定性内置函数,禁止子查询、表引用和自定义副作用函数;CHECK 的 false 拒绝,unknown 允许,NOT NULL 独立拒绝 NULL。嵌套领域从底层向外校验,约束取交集;显式列默认优先,否则使用最近外层有定义的领域默认,只在省略列值时使用,不替换显式 NULL。
|
||||
|
||||
### 引擎与存储校验
|
||||
|
||||
server/engine 负责绑定;Rust 存储根据版本化约束 IR 在全部写入口再次验证,包含批写、原始请求和迁移。两层必须共享运算符标识、类型及 NULL 规则并用相同测试向量验证;不能让 Rust 重新解析 SQL 或只依赖客户端验证。解析诊断不得吞错。
|
||||
|
||||
### 结构变更与持久化
|
||||
|
||||
首版仅支持 CREATE 带约束与 ALTER 添加经全量验证的约束、修改默认;不支持 NOT VALID 或 CASCADE。ALTER 在稳定 schema/数据视图中校验所有直接和间接使用列,短锁发布前复查版本;失败不改变定义。类型定义、依赖版本和受影响 schema 同一 WAL 组提交。现有 DOMAIN 空目录表升级为明确版本元数据,旧编码读入并转换,拒绝无法解析的定义。
|
||||
|
||||
### 验收条件
|
||||
|
||||
验收覆盖嵌套默认优先级、三值 CHECK、NULL、批写全回退、领域改变并发写、循环/间接依赖、内存事务提交与 WAL 重放。新约束加入既有事务后,提交必须按最新定义重新验证或报 schema 冲突。
|
||||
当前 DOMAIN 只提供类型命名,不支持自身 `CHECK`、`NOT NULL`、`DEFAULT`、`ALTER DOMAIN` 或 `CASCADE`。后续约束绑定依赖统一 [bound IR](architecture.md#bound-ir)、对象依赖管理和原子结构事务;不能仅放开语法而绕过写入校验、旧数据验证及 WAL 恢复。
|
||||
+20
-2
@@ -1,5 +1,9 @@
|
||||
# 用户、角色与 root 属性
|
||||
|
||||
[← 返回 README](../README.md)
|
||||
|
||||
**本页导航:** [root 的含义](#root-的含义) · [默认属性与修改方法](#默认属性与修改方法) · [全部身份及凭据字段](#全部身份及凭据字段) · [本机 sudo 认证](#本机-sudo-认证) · [对象授权](#对象授权) · [属性、授权与会话的关系](#属性授权与会话的关系) · [升级兼容](#升级兼容)
|
||||
|
||||
ChuSQL 的用户和角色共用一套身份目录及名称空间。用户是允许登录的角色;`CREATE USER` 与 `CREATE ROLE` 的主要区别是默认登录能力。同名用户和角色不能同时存在。
|
||||
|
||||
## root 的含义
|
||||
@@ -34,6 +38,9 @@ ChuSQL 的用户和角色共用一套身份目录及名称空间。用户是允
|
||||
|
||||
SUPERUSER 可设置目录管理属性。该属性默认为 false,不经成员关系继承;只有自身启用且允许登录的身份能使用它。目录管理者可以进入 system 库、查看身份目录、创建普通身份、修改普通身份属性与口令及删除普通用户身份。它不能管理 SUPERUSER、其他目录管理者或启用 sudo 认证的身份,不能授予这三个特权属性,不能执行 `DROP ROLE`,也不会因此获得创建业务数据库、直接修改系统表或读取业务数据的权限。受限身份变更在存储写锁内再次检查目标权限。
|
||||
|
||||
<details>
|
||||
<summary><strong>身份与凭据的内部字段</strong></summary>
|
||||
|
||||
## 全部身份及凭据字段
|
||||
|
||||
身份元数据保存于隐藏的 `system.__system_identities`;凭据单独保存于 `system.__system_users`。以下字段解释实际持久化内容,不表示都能通过 SQL 修改或查询。
|
||||
@@ -59,6 +66,8 @@ SUPERUSER 可设置目录管理属性。该属性默认为 false,不经成员
|
||||
|
||||
内部账号协议可将身份字段和 `password_hash` 合并为账号对象,但哈希不属于公开的角色展示内容。`SHOW ROLES` 展示全部身份的编号、名称、登录能力、最高权限、启用状态、目录管理属性、sudo 认证开关和创建时间,不返回口令哈希,也不展示全部内部字段。系统表由身份管理接口维护,不应直接修改。
|
||||
|
||||
</details>
|
||||
|
||||
## 本机 sudo 认证
|
||||
|
||||
此入口适用于 Linux、macOS 和 Windows,默认关闭。Unix 有效 UID 0(通常通过 sudo 获得)映射到 `[server] sudo_auth_user`;Windows 提升令牌中的 Administrators 成员映射到同一配置身份。不会使用客户端上报的操作系统用户名、`SUDO_USER`、数据库用户名或角色属性作为操作系统身份凭据。
|
||||
@@ -86,12 +95,17 @@ sudo csql --config /absolute/path/settings.toml --sudo -e "SELECT 1"
|
||||
|
||||
Windows 在提升的终端中运行 `csql --config C:\absolute\path\settings.toml --sudo -e "SELECT 1"`。`--sudo` 不读取数据库口令,省略 `--user` 时使用配置映射。显式指定其他账号也不能绕过服务端映射。普通 TCP/CLI 口令登录与 Web 登录保持原有口令认证;远程连接无法请求此入口,CLI 也校验实际连接对端为回环地址。
|
||||
|
||||
<details>
|
||||
<summary><strong>凭据保护、认证协议与异常清理</strong></summary>
|
||||
|
||||
服务启动时产生独立随机凭据。Unix 放在 `/var/run/chusql-sudo-<实际端口>/credential`,目录 root 所有且为 0700,文件为 0600;Windows 放在系统目录的 `config\ChuSQL-Sudo-<实际端口>\credential`,使用不继承的 Administrators / SYSTEM 专属 DACL。读取入口检查当前进程提升身份;Unix 还验证文件所有权、权限和类型,拒绝链接,Windows 拒绝重解析路径。凭据不会传输到网络,CLI 使用 HMAC-SHA256 回应绑定连接、账号和服务实例的一次性随机挑战;挑战 60 秒后过期,每次认证尝试都会消费挑战,失败按账号限流。
|
||||
|
||||
服务端在 stderr 记录启用映射和认证结果,不记录凭据或证明。成功登录更新身份目录的 `last_login_at`。`ALTER USER local_operator NOALLOW_SUDO_AUTH`、`DISABLED`、`NOLOGIN`、修改口令或删除身份都会使既有连接在下一次请求时失效。映射配置清空并重启服务后,本机入口关闭。
|
||||
|
||||
正常停止服务时清理凭据。异常退出可能留下端口专属目录;再次启用会明确报错,不读取或覆盖旧凭据。确认对应服务已停止后,由提升身份清理该目录再启动;不要把凭据复制到普通账号可访问的目录。
|
||||
|
||||
</details>
|
||||
|
||||
## 对象授权
|
||||
|
||||
业务授权绑定数据库编号与表编号,身份与所有者绑定身份编号。删除后重建同名数据库、表或身份会获得新编号,旧授权不会随名称恢复。对象存在性以 catalog 为准;不同数据库的同名表互不共享授权。
|
||||
@@ -116,8 +130,6 @@ PUBLIC 是所有身份共享的虚拟角色,不能新建同名用户或角色
|
||||
|
||||
每次授予记录授予者。普通身份须持有有效 grant option 或对象所有权,可撤销自己的授予;所有者和 SUPERUSER 可撤销对象上的其他来源。撤销、删除身份或成员变更会清理失去合法来源的转授权链;其他独立来源保留,循环转授不能维持失去根来源的权限。禁用身份暂停权限贡献,保留所有权及管理员直接授权;失去启用授予者的下游转授权会被清理。
|
||||
|
||||
旧名称授权在首次启动时一次性绑定现存对象和身份编号。旧记录不含授予者,迁移为管理来源;旧全局 `*` 仅转换成当时现存表的明确授权,不扩展到未来对象。已经不存在的对象和身份记录被清理。迁移后不再读取旧名称授权作为鉴权依据。
|
||||
|
||||
权限快照位于受保护系统目录,经 WAL 原子保存;普通存储请求不能访问该目录。SQL、TCP 和 Web 使用同一服务端检查;TCP 原始存储入口仅允许 SUPERUSER。查询与授权变更通过共享读写锁排序,建表前持久化创建者声明供重启恢复所有权。事务提交重新检查权限和稳定对象编号,权限撤销或对象替换后不能提交旧事务。
|
||||
|
||||
## 属性、授权与会话的关系
|
||||
@@ -129,3 +141,9 @@ PUBLIC 是所有身份共享的虚拟角色,不能新建同名用户或角色
|
||||
修改身份属性、口令或相关权限会使受影响的既有会话失效;TCP 连接在下一次请求时收到 `unauthorized` 并关闭。删除身份会清理它的授权及双向成员关系;重新创建同名身份不会恢复原有授权。
|
||||
|
||||
当前尚未提供 PostgreSQL 的全部角色选项,例如独立的 `CREATEDB`、`CREATEROLE`、连接数限制或口令到期时间。应以本文列出的属性及当前支持的授权命令为准。
|
||||
|
||||
## 升级兼容
|
||||
|
||||
旧名称授权在首次启动时一次性绑定现存对象和身份编号。旧记录不含授予者,迁移为管理来源;旧全局 `*` 仅转换成当时现存表的明确授权,不扩展到未来对象。已经不存在的对象和身份记录被清理。迁移后不再读取旧名称授权作为鉴权依据。
|
||||
|
||||
旧账号与角色迁移保留编号、口令、时间和原有授权关系;同名冲突拒绝迁移,不合并身份。已有同名 `public` 身份在删除前按真实身份解析。迁移后的身份目录和 DOMAIN 编码不能由旧二进制安全读取,不应降级使用已升级的数据目录。
|
||||
@@ -68,8 +68,6 @@ max_page_size = 500
|
||||
max_rows = 1000
|
||||
# Maximum SQL input length in characters accepted by the web interface.
|
||||
max_sql_length = 20000
|
||||
# Create demonstration data on startup; keep disabled for normal installations.
|
||||
seed = false
|
||||
# Minimum password length for ordinary accounts, in characters.
|
||||
password_min_length = 12
|
||||
# Minimum character classes required: lowercase, uppercase, digits and symbols.
|
||||
|
||||
@@ -68,8 +68,6 @@ max_page_size = 500
|
||||
max_rows = 1000
|
||||
# Maximum SQL input length in characters accepted by the web interface.
|
||||
max_sql_length = 20000
|
||||
# Create demonstration data on startup; keep disabled for normal installations.
|
||||
seed = false
|
||||
# Minimum password length for ordinary accounts, in characters.
|
||||
password_min_length = 12
|
||||
# Minimum character classes required: lowercase, uppercase, digits and symbols.
|
||||
|
||||
Reference in new issue
Block a user