公共表表达式

This commit is contained in:
LIm1tZz7 committed 2026-10-03 20:04:33 +08:00
1 parent 30f1014016
commit df47f7da27
6 files changed
+201 -4

No files matched your search

+1 -1
View File
@@ -28,5 +28,5 @@
- 改动说明只在交流中记录。 - 改动说明只在交流中记录。
## 当前重点 ## 当前重点
- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名。 - 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名 → M12 `WITH` / CTE。
- 进度与计数见 `agent_credientials/project-plan.txt`,限制与风险见 `agent_credientials/known-issues.txt`。 - 进度与计数见 `agent_credientials/project-plan.txt`,限制与风险见 `agent_credientials/known-issues.txt`。
@@ -171,7 +171,7 @@ inPredicate = do
-- | 括号里的子查询;对外层列的引用留给语义检查填 -- | 括号里的子查询;对外层列的引用留给语义检查填
subquerySelect :: Parser Subquery subquerySelect :: Parser Subquery
subquerySelect = Subquery <$> selectStatement <*> pure [] subquerySelect = Subquery <$> queryStatement <*> pure []
-- | 聚合调用:名称与节点都由内置函数表给出 -- | 聚合调用:名称与节点都由内置函数表给出
aggregateCall :: Parser Expr aggregateCall :: Parser Expr
@@ -278,6 +278,7 @@ reservedWords =
, "auto_increment" , "auto_increment"
, "type" , "type"
, "group" , "group"
, "with"
] ]
-- | 别名(不许用保留字) -- | 别名(不许用保留字)
@@ -307,7 +308,7 @@ fromSource = fromSubquerySource <|> (uncurry FromTable <$> tableRef)
-- | 派生表:括号里的一条 SELECT,必须有别名 -- | 派生表:括号里的一条 SELECT,必须有别名
fromSubquerySource :: Parser FromClause fromSubquerySource :: Parser FromClause
fromSubquerySource = do fromSubquerySource = do
stmt <- between (symbol "(") (symbol ")") selectStatement stmt <- between (symbol "(") (symbol ")") queryStatement
mAlias <- optional ((keyword "as" *> aliasName) <|> aliasName) mAlias <- optional ((keyword "as" *> aliasName) <|> aliasName)
case mAlias of case mAlias of
Nothing -> fail "FROM subquery needs an alias" Nothing -> fail "FROM subquery needs an alias"
@@ -598,6 +599,123 @@ selectStatement = do
then Select (map fst cols) fromC mWhere groupBy orderBy mLimit then Select (map fst cols) fromC mWhere groupBy orderBy mLimit
else SelectExpr cols fromC mWhere groupBy orderBy mLimit else SelectExpr cols fromC mWhere groupBy orderBy mLimit
-- | 读 WITH:展开每个 CTE 再读主查询
withStatement :: Parser Statement
withStatement = do
keyword "with"
defs <- sepBy1 cteDef (symbol ",")
main <- selectStatement
either fail return (expandWith defs main)
-- | 一个 CTE:名字、可选列清单与查询
cteDef :: Parser (String, [String], Statement)
cteDef = do
name <- identifier
cols <- fromMaybe [] <$> optional (between (symbol "(") (symbol ")") (sepBy1 identifier (symbol ",")))
keyword "as"
body <- between (symbol "(") (symbol ")") queryStatement
return (name, cols, body)
-- | 查询:允许 WITH 开头
queryStatement :: Parser Statement
queryStatement = withStatement <|> selectStatement
-- | 按顺序把每个 CTE 展开成派生表
expandWith :: [(String, [String], Statement)] -> Statement -> Either String Statement
expandWith defs main = go [] defs
where
-- | 前面的 CTE 已进环境,接着展开本体
go env [] = expandStatement env main
go env ((name, cols, body) : rest) = do
body' <- expandStatement env body
named <- renameColumns name cols body'
go (env ++ [(cteKey name, named)]) rest
-- | CTE 名不分大小写
cteKey :: String -> String
cteKey = map toLower
-- | CTE 带列清单时改本体的输出名
renameColumns :: String -> [String] -> Statement -> Either String Statement
renameColumns _ [] q = Right 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
checkCteArity name cols (map snd items)
Right q { selectItems = zip cols (map snd items) }
renameColumns _ _ _ = Left "WITH: a CTE must be a SELECT"
-- | 列清单个数必须和本体输出一致
checkCteArity :: String -> [String] -> [a] -> Either String ()
checkCteArity name cols out
| length cols == length out = Right ()
| otherwise = Left ("WITH: column list of " ++ name ++ " does not match its query")
-- | 把语句里对 CTE 的引用换成派生表
expandStatement :: [(String, Statement)] -> Statement -> Either String Statement
expandStatement env q = case q of
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
fromC' <- expandClause env fromC
items' <- traverse (\(label, e) -> fmap ((,) label) (expandExpr env e)) items
w <- traverse (expandExpr env) mWhere
Right q { selectFrom = fromC', selectItems = items', selectWhere = w }
_ -> Right q
-- | 来源树里的 CTE 引用换成派生表
expandClause :: [(String, Statement)] -> FromClause -> Either String FromClause
expandClause env fromC = case fromC of
FromTable mAlias tbl
| Just body <- lookup (cteKey tbl) env -> Right (FromSubquery (Just (fromMaybe tbl mAlias)) body)
| otherwise -> Right fromC
FromSubquery mAlias stmt -> FromSubquery mAlias <$> expandStatement env stmt
FromJoin kind left right cond ->
FromJoin kind <$> expandClause env left <*> expandClause env right <*> expandExpr env cond
FromUnit -> Right FromUnit
-- | 表达式里的子查询也要展开
expandExpr :: [(String, Statement)] -> Expr -> Either String Expr
expandExpr env e = case e of
Add a b -> Add <$> go a <*> go b
Sub a b -> Sub <$> go a <*> go b
Mul a b -> Mul <$> go a <*> go b
Div a b -> Div <$> go a <*> go b
Gt a b -> Gt <$> go a <*> go b
Lt a b -> Lt <$> go a <*> go b
Eq a b -> Eq <$> go a <*> go b
And a b -> And <$> go a <*> go b
Or a b -> Or <$> go a <*> go b
Neg a -> Neg <$> go a
IsNull a -> IsNull <$> go a
IsNotNull a -> IsNotNull <$> go a
CountOf a -> CountOf <$> go a
SumOf a -> SumOf <$> go a
AvgOf a -> AvgOf <$> go a
MinOf a -> MinOf <$> go a
MaxOf a -> MaxOf <$> go a
ScalarSub sq -> ScalarSub <$> goSub sq
InSub x sq neg -> (\x' sq' -> InSub x' sq' neg) <$> go x <*> goSub sq
InList x es neg -> (\x' es' -> InList x' es' neg) <$> go x <*> traverse go es
ExistsSub sq neg -> (\sq' -> ExistsSub sq' neg) <$> goSub sq
_ -> Right e
where
-- | 继续看子表达式
go = expandExpr env
-- | 继续看子查询里的语句
goSub sq = (\stmt -> sq { subqueryStatement = stmt }) <$> expandStatement env (subqueryStatement sq)
-- | 读 INSERT,可一次插多行 -- | 读 INSERT,可一次插多行
insertStatement :: Parser Statement insertStatement :: Parser Statement
insertStatement = do insertStatement = do
@@ -660,7 +778,8 @@ parseStatement input =
where where
-- | 按关键字分派到各语句解析器 -- | 按关键字分派到各语句解析器
statementP = statementP =
transactionStatement withStatement
<|> transactionStatement
<|> selectStatement <|> selectStatement
<|> try (CreateDatabase <$> (keyword "create" *> keyword "database" *> identifier)) <|> try (CreateDatabase <$> (keyword "create" *> keyword "database" *> identifier))
<|> try (DropDatabase <$> (keyword "drop" *> keyword "database" *> identifier)) <|> try (DropDatabase <$> (keyword "drop" *> keyword "database" *> identifier))
+59
View File
@@ -459,6 +459,65 @@ main = hspec $ do
sql "SELECT d.name FROM users u JOIN (SELECT name FROM users WHERE id = u.id) d ON u.name = d.name" sql "SELECT d.name FROM users u JOIN (SELECT name FROM users WHERE id = u.id) d ON u.name = d.name"
`shouldSatisfy` isLeft `shouldSatisfy` isLeft
describe "ChuSQL.Core.Engine (WITH)" $ do
-- sql:在默认库上跑 SQL 取结果行
let sql input = parseStatement input >>= rowsOf . runStatement testDB
-- values:只取结果里的值
values input = fmap (map (map snd)) (sql input)
it "parses a CTE into a derived table" $ do
parseStatement "WITH t AS (SELECT name FROM users) SELECT t.name FROM t"
`shouldBe` Right
( Select
{ selectCols = ["t.name"]
, selectFrom =
FromSubquery
(Just "t")
(Select ["name"] (FromTable Nothing "users") Nothing [] [] Nothing)
, selectWhere = Nothing
, selectGroupBy = []
, selectOrderBy = []
, selectLimit = Nothing
}
)
it "runs a CTE as a source" $ do
values "WITH t AS (SELECT name FROM users) SELECT t.name FROM t"
`shouldBe` Right [[VStr "Alice"], [VStr "Bob"], [VStr "Carol"]]
it "refers to an earlier CTE inside the next one" $ do
values "WITH young AS (SELECT name FROM users WHERE age < 20), picked AS (SELECT y.name FROM young y) SELECT p.name FROM picked p"
`shouldBe` Right [[VStr "Bob"]]
it "renames the output columns of a CTE" $ 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 "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"]]
it "runs a CTE inside an IN subquery" $ do
values "WITH grown AS (SELECT id FROM users WHERE age > 20) SELECT name FROM users WHERE id IN (SELECT id FROM grown) ORDER BY name"
`shouldBe` Right [[VStr "Alice"], [VStr "Carol"]]
it "joins a CTE with itself" $ do
values "WITH t AS (SELECT id, name FROM users) SELECT a.name FROM t a JOIN t b ON a.id = b.id ORDER BY a.name"
`shouldBe` Right [[VStr "Alice"], [VStr "Bob"], [VStr "Carol"]]
it "keeps a CTE inside a derived table" $ do
values "SELECT d.name FROM (WITH t AS (SELECT name FROM users) SELECT t.name FROM t) d"
`shouldBe` Right [[VStr "Alice"], [VStr "Bob"], [VStr "Carol"]]
it "rejects a column list that does not match" $ do
sql "WITH t(a, b) AS (SELECT name FROM users) SELECT t.a FROM t" `shouldSatisfy` isLeft
it "reports an unknown table for a forward reference" $ do
sql "WITH a AS (SELECT b.name FROM b), b AS (SELECT name FROM users) SELECT a.name FROM a"
`shouldSatisfy` isLeft
it "reports an unknown table for a name that is not a CTE" $ do
sql "WITH t AS (SELECT name FROM users) SELECT x.name FROM x" `shouldSatisfy` isLeft
describe "ChuSQL.Core.Engine (SELECT)" $ do describe "ChuSQL.Core.Engine (SELECT)" $ do
-- sql:在默认库上跑 SQL 取结果行 -- sql:在默认库上跑 SQL 取结果行
let sql input = parseStatement input >>= rowsOf . runStatement testDB let sql input = parseStatement input >>= rowsOf . runStatement testDB
+9
View File
@@ -652,6 +652,15 @@ privilegeSpec = describe "server privileges" $ do
authorizeSql service (Ordinary testAccount) "SELECT d.name FROM (SELECT product FROM orders) d" authorizeSql service (Ordinary testAccount) "SELECT d.name FROM (SELECT product FROM orders) d"
`shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders") `shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders")
it "requires the privilege on the table inside a CTE" $ withPrivileges $ \service root -> do
_ <- runPrivilegeCommand service root testDatabase (CreateRoleCommand "reader")
_ <- runPrivilegeCommand service root testDatabase (GrantPrivilegesCommand ["select"] "users" "reader" False)
_ <- runPrivilegeCommand service root testDatabase (GrantRoleCommand "reader" ["alice"])
authorizeSql service (Ordinary testAccount) "WITH d AS (SELECT name FROM users) SELECT d.name FROM d"
`shouldReturn` Right ()
authorizeSql service (Ordinary testAccount) "WITH d AS (SELECT product FROM orders) SELECT d.product FROM d"
`shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders")
it "keeps the role tables on the real storage" $ withIpcStorage $ do it "keeps the role tables on the real storage" $ withIpcStorage $ do
base <- ipcBackend base <- ipcBackend
service <- newPrivileges base service <- newPrivileges base
+2
View File
@@ -49,6 +49,8 @@ chusql-server
语句里的库前缀和表限定符由引擎在语义层解析:存储层只接受精确的表名键,因此 `FROM sales.orders` 与选定 `sales` 后的 `FROM orders` 指向同一张表。FROM 的来源是一棵树:单表、派生表(括号里的子查询,必须带别名)与连接共用同一个 `FromClause`,所以 `JOIN` 两侧都能是派生表。解析派生表时先把内层语句在空作用域里解析成一张“表”——内层不能引用外层列,输出列统一改成裸列名(内层 `u.id` 在外层用 `d.id` 引用),重名直接报错;执行侧对应一个 `Derived` 算子,把内层计划的结果按别名加前缀。派生表不参与连接顺序重排(叶子的行数未知),投影也不下推进去,但内层计划照常做谓词下推与索引改写。投影项的 `AS` 别名与 `SELECT` 的输出列名一起在语义层解析,派生表与最外层查询都按别名暴露列名。 语句里的库前缀和表限定符由引擎在语义层解析:存储层只接受精确的表名键,因此 `FROM sales.orders` 与选定 `sales` 后的 `FROM orders` 指向同一张表。FROM 的来源是一棵树:单表、派生表(括号里的子查询,必须带别名)与连接共用同一个 `FromClause`,所以 `JOIN` 两侧都能是派生表。解析派生表时先把内层语句在空作用域里解析成一张“表”——内层不能引用外层列,输出列统一改成裸列名(内层 `u.id` 在外层用 `d.id` 引用),重名直接报错;执行侧对应一个 `Derived` 算子,把内层计划的结果按别名加前缀。派生表不参与连接顺序重排(叶子的行数未知),投影也不下推进去,但内层计划照常做谓词下推与索引改写。投影项的 `AS` 别名与 `SELECT` 的输出列名一起在语义层解析,派生表与最外层查询都按别名暴露列名。
`WITH` 不新增语法树节点:解析器在读到一个 `WITH` 时就把每个 CTE 的名字与语句记进一张环境表,遇到 `FROM` 里出现的 CTE 名(不分大小写,只认前面已定义的)就换成 M11 的派生表节点,主语句与 CTE 内部都能再写 `WITH`。这样展开出来的语句对语义层、执行器与鉴权完全等价于手写派生表:CTE 用列清单改名走派生表的投影别名,来源树与权限覆盖不必另做一套。代价是 CTE 不做物化(引用两次就执行两次)、不支持递归,且前向引用会当成普通表名去查。
显式事务在会话层实现:`BEGIN` 把当前数据库整库读成内存快照,事务内的语句都在快照上执行;`SAVEPOINT` 把当前快照压进会话里的保存点栈,`ROLLBACK TO` 退回该点并保留它,`RELEASE` 丢掉该点与其后的保存点;`COMMIT` 先把快照、基线与库里现值按行 `id` 逐键比对,检测到写写冲突就报 `serialization_failure` 并保留事务;没有冲突才把差分用一条 `apply_transaction` 请求交给存储层应用;`ROLLBACK` 直接丢弃快照。因此隔离级别是快照读加原子提交:事务里看不到别的事务未提交或后来提交的改动,提交对外一次性可见,冲突由调用方回滚或重试。 显式事务在会话层实现:`BEGIN` 把当前数据库整库读成内存快照,事务内的语句都在快照上执行;`SAVEPOINT` 把当前快照压进会话里的保存点栈,`ROLLBACK TO` 退回该点并保留它,`RELEASE` 丢掉该点与其后的保存点;`COMMIT` 先把快照、基线与库里现值按行 `id` 逐键比对,检测到写写冲突就报 `serialization_failure` 并保留事务;没有冲突才把差分用一条 `apply_transaction` 请求交给存储层应用;`ROLLBACK` 直接丢弃快照。因此隔离级别是快照读加原子提交:事务里看不到别的事务未提交或后来提交的改动,提交对外一次性可见,冲突由调用方回滚或重试。
WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;每组提交在数据和字典都刷盘之后把 `wal.checkpoint` 推进到这条提交的 LSN,含义是「到此为止的改动都已落盘」,再按这个边界丢掉日志前缀——所以日志里通常只有当组在飞的记录。进程重启时只重放检查点之后的记录,按标记分组取已提交的组、丢弃没有标记的整组,重放前逐条做幂等检查作为第二道保险,结束后再把检查点推到最后一条提交。LSN 从检查点与文件尾里较大的那个续号,跨重启单调,`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。 WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;每组提交在数据和字典都刷盘之后把 `wal.checkpoint` 推进到这条提交的 LSN,含义是「到此为止的改动都已落盘」,再按这个边界丢掉日志前缀——所以日志里通常只有当组在飞的记录。进程重启时只重放检查点之后的记录,按标记分组取已提交的组、丢弃没有标记的整组,重放前逐条做幂等检查作为第二道保险,结束后再把检查点推到最后一条提交。LSN 从检查点与文件尾里较大的那个续号,跨重启单调,`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。
+8
View File
@@ -52,6 +52,14 @@ csql -f json -e "SELECT count(*) FROM orders"
- 投影列可以起别名:`SELECT count(*) AS total FROM orders`,别名就是结果集的列名。`ORDER BY` 仍按来源列解析,要引用派生表的列得写限定名(如 `ORDER BY d.total`)。 - 投影列可以起别名:`SELECT count(*) AS total FROM orders`,别名就是结果集的列名。`ORDER BY` 仍按来源列解析,要引用派生表的列得写限定名(如 `ORDER BY d.total`)。
- 鉴权覆盖整棵来源树:查派生表时内层表也要有权限,只有外层引用的表有权限不够。 - 鉴权覆盖整棵来源树:查派生表时内层表也要有权限,只有外层引用的表有权限不够。
公共表表达式:
- `WITH 名字 AS (SELECT …), 另一个 AS (SELECT …) SELECT … FROM 名字` 先定义再引用,名字可以带列清单(`WITH t(who) AS (SELECT name FROM users) …`)。
- `WITH` 在解析期就展开成派生表,所以引用处只能当普通表用:没有别名时用 CTE 名当限定名(`WITH t AS (…) SELECT t.name FROM t`)。
- 名字不分大小写;只能引用前面已经定义的 CTE,向前引用会当成普通表名去查(有同名表就用表,没有就报 `unknown table`)。
- 不支持递归 CTE,主语句与每个 CTE 都只能是 `SELECT`;同一个 CTE 被引用两次会各执行一次(不做物化)。
- 鉴权同样覆盖 CTE 里的表:`WITH d AS (SELECT name FROM orders) …` 要求对 `orders` 有权限。
事务: 事务:
- `BEGIN`(或 `START TRANSACTION`)开始显式事务,`COMMIT` 提交,`ROLLBACK` 回滚。 - `BEGIN`(或 `START TRANSACTION`)开始显式事务,`COMMIT` 提交,`ROLLBACK` 回滚。