diff --git a/AGENTS.md b/AGENTS.md index 42bd2bb..bc0aee8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,5 +28,5 @@ - 改动说明只在交流中记录。 ## 当前重点 -- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权。 +- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名。 - 进度与计数见 `agent_credientials/project-plan.txt`,限制与风险见 `agent_credientials/known-issues.txt`。 diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs index 752c56a..f2e25df 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs @@ -409,6 +409,9 @@ evalRelOpIn db outer (Sort spec op) = do evalRelOpIn db outer (Limit n op) = do rows <- evalRelOpIn db outer op pure (fmap (take n) rows) +evalRelOpIn db outer (Derived mAlias op) = do + rows <- evalRelOpIn db outer op + pure (fmap (prefixRows mAlias) rows) evalRelOpIn db outer (Join kind l r c) = do lres <- evalRelOpIn db outer l case lres of diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs index 47953b5..12cf83a 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs @@ -21,6 +21,7 @@ pass db _ (Aggregate keys aggs x) = Aggregate keys aggs (pass db False x) pass db free (Project cols x) = Project cols (pass db free x) pass db free (Compute items x) = Compute items (pass db free x) pass db free (Filter p x) = Filter p (pass db free x) +pass db free (Derived a x) = Derived a (pass db free x) pass db free (Join kind l r c) | free , Just (leaves, conds) <- innerChain (Join kind l r c) diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Op.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Op.hs index d22cd20..c94da03 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Op.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Op.hs @@ -17,6 +17,7 @@ data RelOp | Sort [(String, SortDir)] RelOp | Limit Int RelOp | Join JoinKind RelOp RelOp Expr + | Derived (Maybe String) RelOp deriving (Show, Eq) -- | 算子会产出哪些列 @@ -39,6 +40,7 @@ relOpCols _ (Project cols _) = cols relOpCols db (Sort _ x) = relOpCols db x relOpCols db (Limit _ x) = relOpCols db x relOpCols db (Join _ l r _) = relOpCols db l ++ relOpCols db r +relOpCols db (Derived mAlias x) = map (qualify mAlias) (relOpCols db x) -- | 连接的显示名 kindLabel :: JoinKind -> String @@ -69,5 +71,6 @@ renderPlan = go 0 Sort s x -> pad ++ "Sort " ++ show s ++ "\n" ++ go (ind + 1) x Limit n x -> pad ++ "Limit " ++ show n ++ "\n" ++ go (ind + 1) x Join k l r c -> pad ++ kindLabel k ++ " on " ++ show c ++ "\n" ++ go (ind + 1) l ++ "\n" ++ go (ind + 1) r + Derived a x -> pad ++ "Derived " ++ show a ++ "\n" ++ go (ind + 1) x where pad = replicate (ind * 2) ' ' diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs index 1ae7fee..cbebc54 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs @@ -79,6 +79,7 @@ planHasDivision (Compute items x) = any (hasDivision . snd) items || planHasDivi planHasDivision (Project _ x) = planHasDivision x planHasDivision (Sort _ x) = planHasDivision x planHasDivision (Limit _ x) = planHasDivision x +planHasDivision (Derived _ x) = planHasDivision x planHasDivision _ = False -- | 是否要全部列 @@ -117,6 +118,7 @@ pushProject db need leaf@(Scan a t _) | needsAll need = leaf | all (`elem` need) (relOpCols db leaf) = leaf | otherwise = Scan a t (Just (nub (map (unqualify a) need))) +pushProject _ _ (Derived a x) = Derived a x pushProject db need leaf@(Lookup _ _ _ _) = pushProjectLeaf db need leaf pushProject db need leaf@(Range _ _ _ _ _) = pushProjectLeaf db need leaf @@ -202,6 +204,7 @@ optimizeRelOp db = rewriteNode db . descend descend (Sort spec x) = Sort spec (optimizeRelOp db x) descend (Limit n x) = Limit n (optimizeRelOp db x) descend (Join kind l r c) = Join kind (optimizeRelOp db l) (optimizeRelOp db r) c + descend (Derived a x) = Derived a (optimizeRelOp db x) descend x = x -- | 反复跑到不动点 diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs index 0777bd3..f707e68 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs @@ -6,18 +6,19 @@ import ChuSQL.Core.Engine.Syntax.AST -- 计划生成:把语句翻译成算子树。 --- | 把 FROM 子句翻成 Scan / Join -fromToRelOp :: FromClause -> RelOp -fromToRelOp FromUnit = Unit -fromToRelOp (FromTable mAlias tbl) = Scan mAlias tbl Nothing -fromToRelOp (FromJoin kind left mAlias tbl cond) = - Join kind (fromToRelOp left) (Scan mAlias tbl Nothing) cond +-- | FROM 子句翻成关系算子 +fromToRelOp :: FromClause -> Either String RelOp +fromToRelOp FromUnit = Right Unit +fromToRelOp (FromTable mAlias tbl) = Right (Scan mAlias tbl Nothing) +fromToRelOp (FromSubquery mAlias stmt) = Derived mAlias <$> translate stmt +fromToRelOp (FromJoin kind left right cond) = + Join kind <$> fromToRelOp left <*> fromToRelOp right <*> pure cond -- | 带上 WHERE 的来源 -source :: FromClause -> Maybe Expr -> RelOp -source fromC mWhere = case mWhere of - Nothing -> fromToRelOp fromC - Just w -> Filter w (fromToRelOp fromC) +source :: FromClause -> Maybe Expr -> Either String RelOp +source fromC mWhere = do + op <- fromToRelOp fromC + Right (maybe op (`Filter` op) mWhere) -- | 给用到的聚合起内部列名,同一个聚合只算一次 nameAggs :: [(String, Expr)] -> [(String, Expr)] @@ -80,9 +81,11 @@ translate q = case q of , selectOrderBy = orderBy , selectLimit = mLimit } -> do - let (plan, _) = grouped groupBy [] (source fromC mWhere) + plan0 <- source fromC mWhere + let (plan, _) = grouped groupBy [] plan0 Right (Project cols (limited orderBy mLimit plan)) SelectExpr items fromC mWhere groupBy orderBy mLimit -> do - let (plan, items') = grouped groupBy items (source fromC mWhere) + plan0 <- source fromC mWhere + let (plan, items') = grouped groupBy items plan0 Right (Compute items' (limited orderBy mLimit plan)) _ -> Left "only SELECT is supported by translate" diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs index 6f6d6ca..88bcbc4 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs @@ -44,13 +44,21 @@ checkFrom _ _ FromUnit = Right [] checkFrom db _ (FromTable mAlias tbl) = do (key, t) <- resolveTable db tbl Right (prefixColumns (Just (deriveQualifier mAlias key)) (tableCols t)) -checkFrom db outer (FromJoin _ left mAlias tbl cond) = do +checkFrom db _ (FromSubquery mAlias stmt) = do + out <- inferSubqueryOutput db (Scope [] []) (Subquery stmt []) + Right (prefixColumns mAlias [(name, plainColumn (derivedType t)) | (name, t) <- out]) +checkFrom db outer (FromJoin _ left right cond) = do lcols <- checkFrom db outer left - (key, t) <- resolveTable db tbl - let env = lcols ++ prefixColumns (Just (deriveQualifier mAlias key)) (tableCols t) + rcols <- checkFrom db outer right + let env = lcols ++ rcols checkBool db "ON" (Scope env outer) cond Right env +-- | 派生表里 NULL 字面量列没有具体类型,按字符串处理 +derivedType :: InferredType -> ColumnType +derivedType (InferType t) = t +derivedType InferNull = CStr + -- | 合并两层列,本层优先且不重复 scopedEnv :: [(String, Column)] -> [(String, Column)] -> [(String, Column)] scopedEnv env outer = env ++ [o | o <- outer, o `notElem` env] @@ -638,7 +646,8 @@ statementColumns stmt = case stmt of -- | 取 FROM 里出现的列 fromColumns FromUnit = [] fromColumns (FromTable _ _) = [] - fromColumns (FromJoin _ l _ _ c) = fromColumns l ++ colsInExpr c + fromColumns (FromSubquery _ _) = [] + fromColumns (FromJoin _ l r c) = fromColumns l ++ fromColumns r ++ colsInExpr c -- | 取语句的 FROM 子句,没有就是 FromUnit statementFrom :: Statement -> FromClause @@ -692,17 +701,18 @@ resolveSelect _ q = Right q -- | 解析投影、条件、排序与连接引用 resolveQuery :: Database -> [(String, Column)] -> Statement -> [(String, Expr)] -> Either String Statement resolveQuery db outer q items = do - env <- checkFrom db outer (selectFrom q) + source <- resolveSource db outer (selectFrom q) + env <- checkFrom db outer source -- | 单表无别名时把裸列名换成物理列名;带别名时表头保留别名限定名 let physical c = case selectFrom q of FromTable Nothing tbl -> unqualify (Just (deriveQualifier Nothing tbl)) c _ -> c -- | 查一列并换算成物理名 name place c = physical . fst <$> scopedColumnAt place env outer c - -- | 换算一个投影项 - output (_, Col c) = do + -- | 换算一个投影项:带别名时保留别名做输出列名 + output (label, Col c) = do k <- name "SELECT" c - pure (k, Col k) + pure (if label == c then (k, Col k) else (label, Col k)) output (label, e) = do e' <- resolveExpr db env outer (name "SELECT") e pure (label, e') @@ -714,24 +724,68 @@ resolveQuery db outer q items = do condition <- mapM (resolveExpr db env outer (name "WHERE")) (selectWhere q) grouping <- mapM (name "GROUP BY") (selectGroupBy q) ordering <- mapM (\(c, d) -> (\k -> (k, d)) <$> name "ORDER BY" c) (selectOrderBy q) - source <- case selectFrom q of - FromTable mAlias tbl -> do - (key, _) <- resolveTable db tbl - Right (FromTable mAlias key) - other -> resolveFrom db outer other pure $ case q of Select{} -> Select (map fst projected) source condition grouping ordering (selectLimit q) _ -> SelectExpr projected source condition grouping ordering (selectLimit q) --- | 连接两侧取表名末段作别名;ON 可引用外层列 +-- | 单表不加限定名,其余来源交给 resolveFrom +resolveSource :: Database -> [(String, Column)] -> FromClause -> Either String FromClause +resolveSource db outer fromC = case fromC of + FromTable mAlias tbl -> do + (key, _) <- resolveTable db tbl + Right (FromTable mAlias key) + other -> resolveFrom db outer other + +-- | 连接两侧与派生表都要先解析,再取环境解析 ON resolveFrom :: Database -> [(String, Column)] -> FromClause -> Either String FromClause resolveFrom _ _ FromUnit = Right FromUnit resolveFrom db _ (FromTable alias tbl) = do (key, _) <- resolveTable db tbl Right (FromTable (Just (deriveQualifier alias key)) key) -resolveFrom db outer fromC@(FromJoin kind left alias tbl cond) = do - env <- checkFrom db outer fromC +resolveFrom db _ (FromSubquery mAlias stmt) = do + inner <- resolveSubqueryStatement db [] stmt + named <- nameDerivedColumns inner + Right (FromSubquery mAlias named) +resolveFrom db outer fromC@(FromJoin kind left right cond) = do l <- resolveFrom db outer left - (key, _) <- resolveTable db tbl + r <- resolveFrom db outer right + env <- checkFrom db outer (FromJoin kind l r cond) c <- resolveExpr db env outer (fmap fst . columnAt "ON" env) cond - pure (FromJoin kind l (Just (deriveQualifier alias key)) key c) + pure (FromJoin kind l r c) + +-- | 派生表的输出列名:列取裸名,表达式保留原标签 +nameDerivedColumns :: Statement -> Either String Statement +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 + let names = map derivedItemName items + checkDerivedNames names + Right q { selectItems = zip names (map snd items) } +nameDerivedColumns _ = Left "FROM: subquery must be a SELECT" + +-- | 派生表里一个投影项的输出名:没写别名的列取裸名 +derivedItemName :: (String, Expr) -> String +derivedItemName (label, Col c) + | label == c = lastTablePart c + | otherwise = label +derivedItemName (label, _) = label + +-- | 派生表列名不能重复 +checkDerivedNames :: [String] -> Either String () +checkDerivedNames = go [] + where + -- | 逐个查有没有见过 + go _ [] = Right () + go seen (n : ns) + | n `elem` seen = Left ("FROM: duplicate column name in subquery: " ++ n) + | otherwise = go (n : seen) ns diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs index 20da5cf..048edeb 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs @@ -73,10 +73,11 @@ data JoinKind | LeftJoin deriving (Show, Eq) --- | 数据来源:单表、带连接的来源,或空 +-- | 数据来源:单表、派生表(子查询,输出列已改成裸列名)、带连接的来源,或空 data FromClause = FromTable (Maybe String) String - | FromJoin JoinKind FromClause (Maybe String) String Expr + | FromSubquery (Maybe String) Statement + | FromJoin JoinKind FromClause FromClause Expr | FromUnit deriving (Show, Eq) diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs index c58a54e..ddf2693 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs @@ -300,21 +300,34 @@ joinKind = <|> try (keyword "inner" *> keyword "join" *> return InnerJoin) <|> (keyword "join" *> return InnerJoin) +-- | FROM 里的一个来源:表或派生表 +fromSource :: Parser FromClause +fromSource = fromSubquerySource <|> (uncurry FromTable <$> tableRef) + +-- | 派生表:括号里的一条 SELECT,必须有别名 +fromSubquerySource :: Parser FromClause +fromSubquerySource = do + stmt <- between (symbol "(") (symbol ")") selectStatement + mAlias <- optional ((keyword "as" *> aliasName) <|> aliasName) + case mAlias of + Nothing -> fail "FROM subquery needs an alias" + Just name -> return (FromSubquery (Just name) stmt) + -- | 一个 JOIN ... ON -joinClause :: Parser (JoinKind, Maybe String, String, Expr) +joinClause :: Parser (JoinKind, FromClause, Expr) joinClause = do kind <- joinKind - (mAlias, tbl) <- tableRef + src <- fromSource keyword "on" cond <- expr - return (kind, mAlias, tbl, cond) + return (kind, src, cond) -- | FROM 子句(可含多个 JOIN) fromClause :: Parser FromClause fromClause = do - (mAlias, tbl) <- tableRef + src <- fromSource joins <- many joinClause - return (foldl (\acc (k, a, t, c) -> FromJoin k acc a t c) (FromTable mAlias tbl) joins) + return (foldl (\acc (k, s, c) -> FromJoin k acc s c) src joins) -- | SELECT 的列清单 selectList :: Parser [(String, Expr)] @@ -324,11 +337,17 @@ selectList = , sepBy1 selectItem (symbol ",") ] --- | 投影标签保留表达式原文 +-- | 投影项:列名或表达式,可带 AS 别名 selectItem :: Parser (String, Expr) selectItem = do (source, e) <- match expr - pure (case e of Col c -> (c, e); _ -> (reverse (dropWhile isSpace (reverse source)), e)) + mAlias <- optional (try (keyword "as" *> aliasName)) + pure (fromMaybe (selectItemLabel source e) mAlias, e) + +-- | 投影项的默认标签:列用列名,表达式用原文 +selectItemLabel :: String -> Expr -> String +selectItemLabel _ (Col c) = c +selectItemLabel source _ = reverse (dropWhile isSpace (reverse source)) -- | 读 CREATE TABLE createTableStatement :: Parser Statement diff --git a/chusql-core/engine/test/Spec.hs b/chusql-core/engine/test/Spec.hs index 8be38fb..275238d 100644 --- a/chusql-core/engine/test/Spec.hs +++ b/chusql-core/engine/test/Spec.hs @@ -324,8 +324,7 @@ main = hspec $ do FromJoin InnerJoin (FromTable Nothing "users") - Nothing - "orders" + (FromTable Nothing "orders") (Eq (Col "id") (Col "user_id")) , selectWhere = Nothing , selectGroupBy = [] @@ -343,8 +342,7 @@ main = hspec $ do FromJoin InnerJoin (FromTable (Just "u") "users") - (Just "o") - "orders" + (FromTable (Just "o") "orders") (Eq (Col "u.id") (Col "o.user_id")) , selectWhere = Nothing , selectGroupBy = [] @@ -362,8 +360,7 @@ main = hspec $ do FromJoin LeftJoin (FromTable (Just "u") "users") - (Just "o") - "orders" + (FromTable (Just "o") "orders") (Eq (Col "u.id") (Col "o.user_id")) , selectWhere = Nothing , selectGroupBy = [] @@ -378,11 +375,90 @@ main = hspec $ do ( FromJoin LeftJoin (FromTable (Just "u") "users") - (Just "o") - "orders" + (FromTable (Just "o") "orders") (Eq (Col "u.id") (Col "o.user_id")) ) + describe "ChuSQL.Core.Engine (派生表)" $ do + -- sql:在默认库上跑 SQL 取结果行 + let sql input = parseStatement input >>= rowsOf . runStatement testDB + -- values:只取结果里的值 + values input = fmap (map (map snd)) (sql input) + it "parses a derived table with an alias" $ do + parseStatement "SELECT d.name FROM (SELECT name FROM users) d" + `shouldBe` Right + ( Select + { selectCols = ["d.name"] + , selectFrom = + FromSubquery + (Just "d") + (Select ["name"] (FromTable Nothing "users") Nothing [] [] Nothing) + , selectWhere = Nothing + , selectGroupBy = [] + , selectOrderBy = [] + , selectLimit = Nothing + } + ) + + it "requires an alias for a derived table" $ do + parseStatement "SELECT name FROM (SELECT name FROM users)" `shouldSatisfy` isLeft + + it "parses a derived table on the right of a JOIN" $ do + fmap selectFrom (parseStatement "SELECT d.name FROM users u JOIN (SELECT name FROM users) d ON u.name = d.name") + `shouldBe` Right + ( FromJoin + InnerJoin + (FromTable (Just "u") "users") + (FromSubquery (Just "d") (Select ["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 + [("total", CountAll)] + (FromTable Nothing "orders") + Nothing + [] + [] + Nothing + ) + + it "runs a derived table as a source" $ do + values "SELECT d.name FROM (SELECT name FROM users) d" + `shouldBe` Right [[VStr "Alice"], [VStr "Bob"], [VStr "Carol"]] + + it "expands the star of a derived table" $ do + sql "SELECT * FROM (SELECT id, name FROM users) d" + `shouldBe` Right + [ [("d.id", VInt 1), ("d.name", VStr "Alice")] + , [("d.id", VInt 2), ("d.name", VStr "Bob")] + , [("d.id", VInt 3), ("d.name", VStr "Carol")] + ] + + it "joins a derived table with a table" $ do + values "SELECT o.id FROM (SELECT id FROM users WHERE age > 20) d JOIN orders o ON o.user_id = d.id ORDER BY o.id" + `shouldBe` Right [[VInt 1], [VInt 3]] + + it "runs an aggregate inside a derived table" $ do + values "SELECT d.total FROM (SELECT user_id, count(*) AS total FROM orders GROUP BY user_id) d ORDER BY d.total" + `shouldBe` Right [[VInt 1], [VInt 2]] + + it "runs a derived table inside a derived table" $ do + values "SELECT inner_d.name FROM (SELECT d.name FROM (SELECT name FROM users) d WHERE d.name = 'Bob') inner_d" + `shouldBe` Right [[VStr "Bob"]] + + it "keeps a projection alias outside a derived table" $ do + sql "SELECT count(*) AS total FROM orders" `shouldBe` Right [[("total", VInt 3)]] + + it "rejects duplicate column names in a derived table" $ do + sql "SELECT d.name FROM (SELECT name, name FROM users) d" `shouldSatisfy` isLeft + + it "does not let a derived table see outer columns" $ do + sql "SELECT d.name FROM users u JOIN (SELECT name FROM users WHERE id = u.id) d ON u.name = d.name" + `shouldSatisfy` isLeft + describe "ChuSQL.Core.Engine (SELECT)" $ do -- sql:在默认库上跑 SQL 取结果行 let sql input = parseStatement input >>= rowsOf . runStatement testDB @@ -991,8 +1067,7 @@ main = hspec $ do FromJoin InnerJoin (FromTable (Just "u") "users") - (Just "x") - "nonexistent" + (FromTable (Just "x") "nonexistent") (Eq (Col "u.id") (Col "x.id")) , selectWhere = Nothing , selectGroupBy = [] diff --git a/chusql-server/src/ChuSQL/Server/Backend.hs b/chusql-server/src/ChuSQL/Server/Backend.hs index a8aa8df..f018fb7 100644 --- a/chusql-server/src/ChuSQL/Server/Backend.hs +++ b/chusql-server/src/ChuSQL/Server/Backend.hs @@ -283,7 +283,8 @@ statementNeedsDatabase stmt = any (not . T.any (== '.')) (statementTables stmt) tableRefsOf :: FromClause -> [Text] tableRefsOf FromUnit = [] tableRefsOf (FromTable _ name) = [T.pack name] -tableRefsOf (FromJoin _ left _ name cond) = tableRefsOf left ++ [T.pack name] ++ exprTables cond +tableRefsOf (FromSubquery _ stmt) = statementTables stmt +tableRefsOf (FromJoin _ left right cond) = tableRefsOf left ++ tableRefsOf right ++ exprTables cond -- | 表达式里出现的表(子查询是唯一的来路) exprTables :: Expr -> [Text] diff --git a/chusql-server/test/Spec.hs b/chusql-server/test/Spec.hs index fddc648..fa69888 100644 --- a/chusql-server/test/Spec.hs +++ b/chusql-server/test/Spec.hs @@ -643,6 +643,15 @@ privilegeSpec = describe "server privileges" $ do Left err -> expectationFailure err Right stmt -> authorize service root testDatabase stmt `shouldReturn` Right () + it "requires the privilege on the table inside a derived table" $ 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) "SELECT d.name FROM (SELECT name FROM users) d" + `shouldReturn` Right () + authorizeSql service (Ordinary testAccount) "SELECT d.name FROM (SELECT product FROM orders) d" + `shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders") + it "keeps the role tables on the real storage" $ withIpcStorage $ do base <- ipcBackend service <- newPrivileges base diff --git a/docs/architecture.md b/docs/architecture.md index a9db245..93b8d2c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -47,12 +47,12 @@ chusql-server 每条连接拥有独立会话状态,例如当前数据库选择不会影响其他连接。不同连接可以同时执行语句:读请求之间可以并行,写请求之间由存储层排队。 -语句里的库前缀和表限定符由引擎在语义层解析:存储层只接受精确的表名键,因此 `FROM sales.orders` 与选定 `sales` 后的 `FROM orders` 指向同一张表。 +语句里的库前缀和表限定符由引擎在语义层解析:存储层只接受精确的表名键,因此 `FROM sales.orders` 与选定 `sales` 后的 `FROM orders` 指向同一张表。FROM 的来源是一棵树:单表、派生表(括号里的子查询,必须带别名)与连接共用同一个 `FromClause`,所以 `JOIN` 两侧都能是派生表。解析派生表时先把内层语句在空作用域里解析成一张“表”——内层不能引用外层列,输出列统一改成裸列名(内层 `u.id` 在外层用 `d.id` 引用),重名直接报错;执行侧对应一个 `Derived` 算子,把内层计划的结果按别名加前缀。派生表不参与连接顺序重排(叶子的行数未知),投影也不下推进去,但内层计划照常做谓词下推与索引改写。投影项的 `AS` 别名与 `SELECT` 的输出列名一起在语义层解析,派生表与最外层查询都按别名暴露列名。 显式事务在会话层实现:`BEGIN` 把当前数据库整库读成内存快照,事务内的语句都在快照上执行;`SAVEPOINT` 把当前快照压进会话里的保存点栈,`ROLLBACK TO` 退回该点并保留它,`RELEASE` 丢掉该点与其后的保存点;`COMMIT` 先把快照、基线与库里现值按行 `id` 逐键比对,检测到写写冲突就报 `serialization_failure` 并保留事务;没有冲突才把差分用一条 `apply_transaction` 请求交给存储层应用;`ROLLBACK` 直接丢弃快照。因此隔离级别是快照读加原子提交:事务里看不到别的事务未提交或后来提交的改动,提交对外一次性可见,冲突由调用方回滚或重试。 WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;每组提交在数据和字典都刷盘之后把 `wal.checkpoint` 推进到这条提交的 LSN,含义是「到此为止的改动都已落盘」,再按这个边界丢掉日志前缀——所以日志里通常只有当组在飞的记录。进程重启时只重放检查点之后的记录,按标记分组取已提交的组、丢弃没有标记的整组,重放前逐条做幂等检查作为第二道保险,结束后再把检查点推到最后一条提交。LSN 从检查点与文件尾里较大的那个续号,跨重启单调,`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。 -系统数据库及部分管理操作仅允许管理员访问。系统表统一用 `__system_` 前缀:账号表 `__system_users` 对语句通道隐藏,角色、授权、成员三张表(`__system_roles` / `__system_grants` / `__system_members`)只在 `system` 库里可见,权限服务自身也通过 SQL 读写它们;存储启动时把旧名 `sys_roles` / `sys_grants` / `sys_members` 连同数据与索引文件一起迁移过来。角色支持互相继承:`GRANT 角色 A TO 角色 B` 记一条 B 属于 A 的成员关系,判定权限时沿成员关系算出向上闭包再取并集;写入时拒绝成环与自继承,删角色会清掉它两个方向的成员关系。带 `WITH GRANT OPTION` 的授权记在第四张表 `__system_grant_options` 里(同样的角色、权限、对象三列,单独的列存不下就单独一张表,省掉系统表的结构迁移):普通身份只要手里那条授权带 grant option 就能把同一项权限转授给别的角色,`REVOKE` 仍然只允许管理员,收回授权时连它的 grant option 一起收掉,但不级联收回别人已经转授出去的授权。授权命令由权限服务在会话层之外直接执行,`GRANT` 命令会先按当前库把对象补全成 `库.表` 再落盘,因此判断转授权资格与实际写入用的是同一个对象名。 +系统数据库及部分管理操作仅允许管理员访问。系统表统一用 `__system_` 前缀:账号表 `__system_users` 对语句通道隐藏,角色、授权、带 option 的授权、成员四张表(`__system_roles` / `__system_grants` / `__system_grant_options` / `__system_members`)只在 `system` 库里可见,权限服务自身也通过 SQL 读写它们;存储启动时把旧名 `sys_roles` / `sys_grants` / `sys_members` 连同数据与索引文件一起迁移过来。角色支持互相继承:`GRANT 角色 A TO 角色 B` 记一条 B 属于 A 的成员关系,判定权限时沿成员关系算出向上闭包再取并集;写入时拒绝成环与自继承,删角色会清掉它两个方向的成员关系。带 `WITH GRANT OPTION` 的授权记在第四张表 `__system_grant_options` 里(同样的角色、权限、对象三列,另开一张表省掉给 `__system_grants` 加列带来的系统表结构迁移):普通身份只要手里那条授权带 grant option 就能把同一项权限转授给别的角色,`REVOKE` 仍然只允许管理员,收回授权时连它的 grant option 一起收掉,但不级联收回别人已经转授出去的授权。授权命令由权限服务在会话层之外直接执行,`GRANT` 命令会先按当前库把对象补全成 `库.表` 再落盘,因此判断转授权资格与实际写入用的是同一个对象名。 服务监听地址、端口和请求限制可在 `chusql.toml` 的 `[server]` 分区中配置。 diff --git a/docs/commands.md b/docs/commands.md index 331fef3..f61da16 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -45,6 +45,13 @@ csql -f json -e "SELECT count(*) FROM orders" - 只有候选唯一时才自动推导前缀;同名的表或列出现在多个候选上会以 `ambiguous table` / `ambiguous column` 报错,需要写全。 - `FROM orders AS o` 之后只能用 `o.id`,不能再写 `orders.id`。 +查询写法: + +- `SELECT` 支持 `WHERE`、`ORDER BY`、`LIMIT`、`JOIN` / `LEFT JOIN`、`GROUP BY` 与聚合,以及标量 / `IN` / `EXISTS` 子查询。 +- 来源可以是派生表:`SELECT d.name FROM (SELECT name FROM users) d`。派生表必须带别名,内层不能引用外层列;输出列取裸列名(内层写 `u.id`,外层用 `d.id` 引用)。 +- 投影列可以起别名:`SELECT count(*) AS total FROM orders`,别名就是结果集的列名。`ORDER BY` 仍按来源列解析,要引用派生表的列得写限定名(如 `ORDER BY d.total`)。 +- 鉴权覆盖整棵来源树:查派生表时内层表也要有权限,只有外层引用的表有权限不够。 + 事务: - `BEGIN`(或 `START TRANSACTION`)开始显式事务,`COMMIT` 提交,`ROLLBACK` 回滚。