From 7d96933e32925f8ba5e6086bdbe66a08a808e5fc Mon Sep 17 00:00:00 2001 From: LIm1tZz7 <36764786+TheBadRoger@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:52:02 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8F=E8=AF=8D=E6=AF=94=E8=BE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- .../src/ChuSQL/Core/Engine/Algebra/Eval.hs | 10 +++- .../src/ChuSQL/Core/Engine/Algebra/Expr.hs | 16 +++++- .../ChuSQL/Core/Engine/Algebra/JoinOrder.hs | 1 + .../ChuSQL/Core/Engine/Algebra/Optimize.hs | 1 + .../src/ChuSQL/Core/Engine/Algebra/Planner.hs | 1 + .../engine/src/ChuSQL/Core/Engine/Builtin.hs | 22 ++++++++- .../engine/src/ChuSQL/Core/Engine/Semantic.hs | 14 ++++++ .../src/ChuSQL/Core/Engine/Syntax/AST.hs | 19 ++++++- .../src/ChuSQL/Core/Engine/Syntax/Parser.hs | 45 ++++++++++++++--- chusql-core/engine/test/Spec.hs | 49 +++++++++++++++++++ chusql-server/src/ChuSQL/Server/Backend.hs | 1 + chusql-server/test/Spec.hs | 9 ++++ docs/architecture.md | 2 + docs/commands.md | 8 ++- 15 files changed, 185 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 938a6f5..b81c686 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,5 +28,5 @@ - 改动说明只在交流中记录。 ## 当前重点 -- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名 → M12 `WITH` / CTE → M13 比较运算符补全。 +- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名 → M12 `WITH` / CTE → M13 比较运算符补全 → M14 ANY / ALL 量词比较。 - 进度与计数见 `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 22a101e..94a7dae 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs @@ -1,7 +1,7 @@ module ChuSQL.Core.Engine.Algebra.Eval (evalRelOp, evalRelOpM, evalExprM, evalCondForRowM) where import ChuSQL.Core.Engine.Algebra.Cost (tableMetaOf) -import ChuSQL.Core.Engine.Algebra.Expr (evalCondForRow, evalExpr) +import ChuSQL.Core.Engine.Algebra.Expr (evalCondForRow, evalExpr, quantifiedCompare) import ChuSQL.Core.Engine.Algebra.Optimize (optimize) import ChuSQL.Core.Engine.Algebra.Op import ChuSQL.Core.Engine.Algebra.Planner (translate) @@ -312,6 +312,14 @@ substSubqueries db e env = case e of ExistsSub sq negated -> do rows <- runSubqueryRows db sq env pure (fmap (\rs -> LitBool (if negated then null rs else not (null rs))) rows) + QuantCmp op a sq q -> do + lhs <- substSubqueries db a env + rows <- runSubqueryRows db sq env + pure (do + a' <- lhs + rs <- rows + vs <- mapM rowValue rs + Right (quantifiedCompare op a' (map valueLiteral vs) q)) InList a es negated -> do lhs <- substSubqueries db a env es' <- mapM (\x -> substSubqueries db x env) es diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs index de8e3f0..17c5979 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs @@ -1,8 +1,8 @@ -module ChuSQL.Core.Engine.Algebra.Expr (evalExpr, evalCondForRow, colsInExpr, aggregatesIn, hasAggregate, bareColumns, hasSubquery, hasDivision, inValues, threeValuedNot) where +module ChuSQL.Core.Engine.Algebra.Expr (evalExpr, evalCondForRow, colsInExpr, aggregatesIn, hasAggregate, bareColumns, hasSubquery, hasDivision, inValues, threeValuedNot, quantifiedCompare) where import ChuSQL.Core.Model import ChuSQL.Core.Engine.Syntax.AST -import ChuSQL.Core.Engine.Builtin (Operator (..), executeOperator, inValues, threeValuedNot) +import ChuSQL.Core.Engine.Builtin (Operator (..), compareNode, executeOperator, inValues, threeValuedNot) -- 表达式求值:三值逻辑,NULL 参与运算结果仍是 NULL。 @@ -47,12 +47,19 @@ evalExpr (InList a es negated) row = do evalExpr (ScalarSub _) _ = Left subqueryNeedsDatabase evalExpr (InSub _ _ _) _ = Left subqueryNeedsDatabase evalExpr (ExistsSub _ _) _ = Left subqueryNeedsDatabase +evalExpr (QuantCmp _ _ _ _) _ = Left subqueryNeedsDatabase evalExpr agg _ = Left ("aggregate functions are not allowed here: " ++ show agg) -- | 子查询要连上存储才能跑 subqueryNeedsDatabase :: String subqueryNeedsDatabase = "subquery needs a database to run" +-- | 量词比较摊成 AND / OR 链 +quantifiedCompare :: CompareOp -> Expr -> [Expr] -> Quantifier -> Expr +quantifiedCompare op lhs vs q = case vs of + [] -> LitBool (q == AllQ) + _ -> foldr1 (if q == AllQ then And else Or) (map (compareNode op lhs) vs) + -- | 表达式用到了哪些列 colsInExpr :: Expr -> [String] colsInExpr (Col c) = [c] @@ -80,6 +87,7 @@ colsInExpr (ScalarSub sq) = subqueryRefs sq colsInExpr (InSub a sq _) = colsInExpr a ++ subqueryRefs sq colsInExpr (InList a es _) = colsInExpr a ++ concatMap colsInExpr es colsInExpr (ExistsSub sq _) = subqueryRefs sq +colsInExpr (QuantCmp _ a sq _) = colsInExpr a ++ subqueryRefs sq colsInExpr _ = [] -- | 表达式里有没有除法(除法可能报错,不能提前或延后求值) @@ -98,6 +106,7 @@ hasDivision e = case e of NotEq a b -> both a b And a b -> both a b Or a b -> both a b + QuantCmp _ a _ _ -> hasDivision a _ -> False where -- | 两个子表达式里任意一个有除法 @@ -110,6 +119,7 @@ hasSubquery e = case e of InSub _ _ _ -> True InList a es _ -> any hasSubquery (a : es) ExistsSub _ _ -> True + QuantCmp _ _ _ _ -> True Add a b -> both a b Sub a b -> both a b Mul a b -> both a b @@ -144,6 +154,7 @@ aggregatesIn e = case e of a@(AvgOf _) -> [a] a@(MinOf _) -> [a] a@(MaxOf _) -> [a] + QuantCmp _ a _ _ -> aggregatesIn a Add a b -> both a b Sub a b -> both a b Mul a b -> both a b @@ -191,6 +202,7 @@ bareColumns e = case e of InSub a sq _ -> bareColumns a ++ subqueryRefs sq InList a es _ -> bareColumns a ++ concatMap bareColumns es ExistsSub sq _ -> subqueryRefs sq + QuantCmp _ a sq _ -> bareColumns a ++ subqueryRefs sq _ -> [] where -- | 两个子表达式的裸列拼起来 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 91a865a..3c060d3 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs @@ -65,6 +65,7 @@ scaleRows db p x rows = case p of GtE _ _ -> quarter LtE _ _ -> quarter NotEq _ _ -> quarter + QuantCmp _ _ _ _ -> quarter And a b -> max 1 (min (scaleRows db a x rows) (scaleRows db b x rows)) _ -> quarter where 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 02a063f..f66f19e 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs @@ -147,6 +147,7 @@ foldConstants (Eq a b) = foldNode (Eq (foldConstants a) (foldConstants b)) foldConstants (GtE a b) = foldNode (GtE (foldConstants a) (foldConstants b)) foldConstants (LtE a b) = foldNode (LtE (foldConstants a) (foldConstants b)) foldConstants (NotEq a b) = foldNode (NotEq (foldConstants a) (foldConstants b)) +foldConstants (QuantCmp op a sq q) = QuantCmp op (foldConstants a) sq q foldConstants (And a b) = foldNode (And (foldConstants a) (foldConstants b)) foldConstants (Or a b) = foldNode (Or (foldConstants a) (foldConstants b)) foldConstants e = e 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 510c10f..000b88d 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs @@ -47,6 +47,7 @@ rewriteAggs aggs e = case [label | (label, agg) <- aggs, agg == e] of GtE a b -> GtE (go a) (go b) LtE a b -> LtE (go a) (go b) NotEq a b -> NotEq (go a) (go b) + QuantCmp op a sq q -> QuantCmp op (go a) sq q And a b -> And (go a) (go b) Or a b -> Or (go a) (go b) IsNull a -> IsNull (go a) diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Builtin.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Builtin.hs index cb05a39..9ef55c4 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Builtin.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Builtin.hs @@ -11,13 +11,15 @@ module ChuSQL.Core.Engine.Builtin , Operator (..) , operatorOfExpr , operatorSymbol + , operatorOfCompare + , compareNode , executeOperator , threeValuedNot , inValues ) where import ChuSQL.Core.Model (Value (..), compareValue) -import ChuSQL.Core.Engine.Syntax.AST (Expr (..)) +import ChuSQL.Core.Engine.Syntax.AST (CompareOp (..), Expr (..)) import Control.Monad (foldM) import Data.Char (toLower) @@ -177,6 +179,24 @@ operatorOfExpr (And _ _) = Just OpAnd operatorOfExpr (Or _ _) = Just OpOr operatorOfExpr _ = Nothing +-- | 量词比较里的比较运算符 +operatorOfCompare :: CompareOp -> Operator +operatorOfCompare CmpEq = OpEq +operatorOfCompare CmpNe = OpNe +operatorOfCompare CmpGt = OpGt +operatorOfCompare CmpGtE = OpGtE +operatorOfCompare CmpLt = OpLt +operatorOfCompare CmpLtE = OpLtE + +-- | 量词比较在字面量上的节点 +compareNode :: CompareOp -> Expr -> Expr -> Expr +compareNode CmpEq = Eq +compareNode CmpNe = NotEq +compareNode CmpGt = Gt +compareNode CmpGtE = GtE +compareNode CmpLt = Lt +compareNode CmpLtE = LtE + -- | 运算符求值:算错个数是内部错误 executeOperator :: Operator -> [Value] -> Either String Value executeOperator op vs = case (op, vs) of diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs index b6b2d4b..37edbfb 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs @@ -177,6 +177,19 @@ inferExpr db place env (InList a es _) = do mapM_ (itemType place ta) ts Right (InferType CBool) inferExpr _ _ _ (ExistsSub _ _) = Right (InferType CBool) +inferExpr db place env (QuantCmp _ a sq q) = do + ta <- inferExpr db place env a + cols <- inferSubqueryOutput db env sq + case cols of + [(_, t)] + | compatible ta t -> Right (InferType CBool) + | otherwise -> Left (place ++ ": " ++ quantLabel q ++ " subquery column has type " ++ renderType t ++ ", expected " ++ renderType ta) + _ -> Left (place ++ ": " ++ quantLabel q ++ " subquery must return exactly one column") + +-- | 量词在错误文本里的名字 +quantLabel :: Quantifier -> String +quantLabel AnyQ = "ANY" +quantLabel AllQ = "ALL" -- | IN 列表里的每一项都要跟左边的类型对得上 itemType :: String -> InferredType -> InferredType -> Either String () @@ -617,6 +630,7 @@ resolveExpr db env outer f e = case e of InSub a sq negated -> InSub <$> resolveExpr db env outer f a <*> resolveSubquery db (env ++ outer) sq <*> pure negated InList a es negated -> InList <$> resolveExpr db env outer f a <*> mapM (resolveExpr db env outer f) es <*> pure negated ExistsSub sq negated -> ExistsSub <$> resolveSubquery db (env ++ outer) sq <*> pure negated + QuantCmp op a sq q -> QuantCmp op <$> resolveExpr db env outer f a <*> resolveSubquery db (env ++ outer) sq <*> pure q _ -> Right e where -- | 递归处理两个子表达式 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 5b32047..649f1a8 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs @@ -1,4 +1,4 @@ -module ChuSQL.Core.Engine.Syntax.AST (Statement (..), FromClause (..), JoinKind (..), Expr (..), Subquery (..), SortDir (..), makeSelect) where +module ChuSQL.Core.Engine.Syntax.AST (Statement (..), FromClause (..), JoinKind (..), Expr (..), Subquery (..), Quantifier (..), CompareOp (..), SortDir (..), makeSelect) where import ChuSQL.Core.Model (Column (..), ColumnType, Value (..)) @@ -88,6 +88,22 @@ data Subquery = Subquery } deriving (Show, Eq) +-- | 量词:ANY 至少一行成立,ALL 每一行都成立 +data Quantifier + = AnyQ + | AllQ + deriving (Show, Eq) + +-- | 量词比较里的比较运算符 +data CompareOp + = CmpEq + | CmpNe + | CmpGt + | CmpGtE + | CmpLt + | CmpLtE + deriving (Show, Eq) + -- | 条件表达式:列、字面量、算术、比较、逻辑、聚合与子查询 data Expr = Col String @@ -124,6 +140,7 @@ data Expr | InSub Expr Subquery Bool | InList Expr [Expr] Bool | ExistsSub Subquery Bool + | QuantCmp CompareOp Expr Subquery Quantifier 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 493a7ae..d1b6f6f 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs @@ -3,7 +3,7 @@ module ChuSQL.Core.Engine.Syntax.Parser (parseStatement, parseExpression) where import ChuSQL.Core.Model (Column (..), ColumnType (..), Value (..), plainColumn) -import ChuSQL.Core.Engine.Builtin (Builtin (..), builtinName, builtinNames, builtinNode) +import ChuSQL.Core.Engine.Builtin (Builtin (..), builtinName, builtinNames, builtinNode, compareNode) import ChuSQL.Core.Engine.Syntax.AST import Control.Monad (void) import Control.Monad.Combinators.Expr (Operator (..), makeExprParser) @@ -112,14 +112,9 @@ operatorTable = [ [Prefix (Neg <$ symbol "-")] , [InfixL (Mul <$ symbol "*"), InfixL (Div <$ symbol "/")] , [InfixL (Add <$ symbol "+"), InfixL (Sub <$ symbol "-")] - , [ InfixN (NotEq <$ try (symbol "<>" <|> symbol "!=")) - , InfixN (GtE <$ try (symbol ">=")) - , InfixN (LtE <$ try (symbol "<=")) - , InfixN (Gt <$ symbol ">") - , InfixN (Lt <$ symbol "<") - , InfixN (Eq <$ symbol "=") - ] + , [ InfixN comparisonOp ] , [ Postfix inPredicate + , Postfix (try quantifiedCmp) , Postfix (IsNotNull <$ try (keyword "is" *> keyword "not" *> keyword "null")) , Postfix (IsNull <$ (keyword "is" *> keyword "null")) ] @@ -127,6 +122,37 @@ operatorTable = , [InfixL (Or <$ keyword "OR")] ] +-- | 普通比较:读最长的运算符,后面不能是 ANY / ALL +comparisonOp :: Parser (Expr -> Expr -> Expr) +comparisonOp = try $ do + op <- cmpOp + notFollowedBy quantifierKeyword + return (compareNode op) + +-- | 量词:ANY 或 ALL +quantifierKeyword :: Parser Quantifier +quantifierKeyword = (AnyQ <$ keyword "any") <|> (AllQ <$ keyword "all") + +-- | 比较运算符符号 +cmpOp :: Parser CompareOp +cmpOp = + choice + [ CmpNe <$ try (symbol "<>" <|> symbol "!=") + , CmpGtE <$ try (symbol ">=") + , CmpLtE <$ try (symbol "<=") + , CmpGt <$ symbol ">" + , CmpLt <$ symbol "<" + , CmpEq <$ symbol "=" + ] + +-- | 量词比较 ANY / ALL +quantifiedCmp :: Parser (Expr -> Expr) +quantifiedCmp = do + op <- cmpOp + q <- quantifierKeyword + sq <- between (symbol "(") (symbol ")") subquerySelect + return (\e -> QuantCmp op e sq q) + -- | 最小的表达式单位 atom :: Parser Expr atom = @@ -282,6 +308,8 @@ reservedWords = , "type" , "group" , "with" + , "any" + , "all" ] -- | 别名(不许用保留字) @@ -715,6 +743,7 @@ expandExpr env e = case e of 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 + QuantCmp op x sq q -> (\x' sq' -> QuantCmp op x' sq' q) <$> go x <*> goSub sq _ -> Right e where -- | 继续看子表达式 diff --git a/chusql-core/engine/test/Spec.hs b/chusql-core/engine/test/Spec.hs index caa4c0e..ef1cf9a 100644 --- a/chusql-core/engine/test/Spec.hs +++ b/chusql-core/engine/test/Spec.hs @@ -602,6 +602,55 @@ main = hspec $ do ids (And (Gt (Col "code") (LitInt 501)) (Lt (Col "code") (LitInt 502))) `shouldReturn` Right [] + describe "ChuSQL.Core.Engine (ANY / ALL)" $ do + -- sql:在默认库上跑 SQL 取结果行 + let sql input = parseStatement input >>= rowsOf . runStatement testDB + -- values:只取结果里的值 + values input = fmap (map (map snd)) (sql input) + + 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) + + 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) + + it "still reads plain comparisons" $ do + values "SELECT name FROM users WHERE age > 20 ORDER BY name" + `shouldBe` Right [[VStr "Alice"], [VStr "Carol"]] + + it "satisfies ANY when at least one row matches" $ do + values "SELECT name FROM users WHERE age > ANY (SELECT age FROM users WHERE name = 'Bob')" + `shouldBe` Right [[VStr "Alice"], [VStr "Carol"]] + values "SELECT name FROM users WHERE age = ANY (SELECT age FROM users WHERE name = 'Bob')" + `shouldBe` Right [[VStr "Bob"]] + + it "satisfies ALL only when every row matches" $ do + values "SELECT name FROM users WHERE age >= ALL (SELECT age FROM users)" + `shouldBe` Right [[VStr "Carol"]] + values "SELECT name FROM users WHERE age > ALL (SELECT age FROM users)" `shouldBe` Right [] + + it "treats an empty subquery as true for ALL and false for ANY" $ do + values "SELECT name FROM users WHERE age > ALL (SELECT age FROM users WHERE name = 'Nobody')" + `shouldBe` Right [[VStr "Alice"], [VStr "Bob"], [VStr "Carol"]] + values "SELECT name FROM users WHERE age > ANY (SELECT age FROM users WHERE name = 'Nobody')" + `shouldBe` Right [] + + it "keeps NULL in the subquery three-valued" $ do + values "SELECT name FROM users WHERE age > ANY (SELECT NULL)" `shouldBe` Right [] + values "SELECT name FROM users WHERE age > ALL (SELECT NULL)" `shouldBe` Right [] + + it "agrees with IN on the equality form" $ do + sql "SELECT name FROM users WHERE id = ANY (SELECT id FROM orders)" + `shouldBe` sql "SELECT name FROM users WHERE id IN (SELECT id FROM orders)" + + it "rejects a multi-column or mistyped quantifier subquery" $ do + isLeft (sql "SELECT name FROM users WHERE age > ANY (SELECT id, name FROM users)") + `shouldBe` True + isLeft (sql "SELECT name FROM users WHERE name > ANY (SELECT id FROM users)") + `shouldBe` True + describe "ChuSQL.Core.Engine (SELECT)" $ do -- sql:在默认库上跑 SQL 取结果行 let sql input = parseStatement input >>= rowsOf . runStatement testDB diff --git a/chusql-server/src/ChuSQL/Server/Backend.hs b/chusql-server/src/ChuSQL/Server/Backend.hs index 1cf5a2e..5f6009f 100644 --- a/chusql-server/src/ChuSQL/Server/Backend.hs +++ b/chusql-server/src/ChuSQL/Server/Backend.hs @@ -292,6 +292,7 @@ exprTables expr = case expr of ScalarSub sub -> statementTables (subqueryStatement sub) ExistsSub sub _ -> statementTables (subqueryStatement sub) InSub value sub _ -> exprTables value ++ statementTables (subqueryStatement sub) + QuantCmp _ value sub _ -> exprTables value ++ statementTables (subqueryStatement sub) InList value items _ -> concatMap exprTables (value : items) Add a b -> both a b Sub a b -> both a b diff --git a/chusql-server/test/Spec.hs b/chusql-server/test/Spec.hs index 56ebd32..3914d18 100644 --- a/chusql-server/test/Spec.hs +++ b/chusql-server/test/Spec.hs @@ -670,6 +670,15 @@ privilegeSpec = describe "server privileges" $ do authorizeSql service (Ordinary testAccount) "SELECT name FROM users WHERE id >= (SELECT id FROM orders)" `shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders") + it "requires the privilege on the table inside a quantifier subquery" $ 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 name FROM users WHERE id > ANY (SELECT id FROM users)" + `shouldReturn` Right () + authorizeSql service (Ordinary testAccount) "SELECT name FROM users WHERE id > ANY (SELECT id FROM orders)" + `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 eb2d008..0cb52b9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -51,6 +51,8 @@ chusql-server `WITH` 不新增语法树节点:解析器在读到一个 `WITH` 时就把每个 CTE 的名字与语句记进一张环境表,遇到 `FROM` 里出现的 CTE 名(不分大小写,只认前面已定义的)就换成 M11 的派生表节点,主语句与 CTE 内部都能再写 `WITH`。这样展开出来的语句对语义层、执行器与鉴权完全等价于手写派生表:CTE 用列清单改名走派生表的投影别名,来源树与权限覆盖不必另做一套。代价是 CTE 不做物化(引用两次就执行两次)、不支持递归,且前向引用会当成普通表名去查。 +量词比较走另一个真节点 `QuantCmp`:解析时普通比较运算符先读最长符号,发现后面是 `ANY` / `ALL` 就让给量词解析,因此 `age > ANY (SELECT …)` 与 `age > 25` 共用同一层优先级。求值时先把子查询的行取成字面量,再摊成 `AND`(`ALL`)/ `OR`(`ANY`)链交给既有三值逻辑,于是空子查询 `ANY` 为假、`ALL` 为真,子查询里的 `NULL` 按三值规则传播。节点不带取反位(本方言没有前缀 `NOT`),取反写相反的运算符,如 `<> ALL` 与 `NOT IN` 同义。 + 显式事务在会话层实现:`BEGIN` 把当前数据库整库读成内存快照,事务内的语句都在快照上执行;`SAVEPOINT` 把当前快照压进会话里的保存点栈,`ROLLBACK TO` 退回该点并保留它,`RELEASE` 丢掉该点与其后的保存点;`COMMIT` 先把快照、基线与库里现值按行 `id` 逐键比对,检测到写写冲突就报 `serialization_failure` 并保留事务;没有冲突才把差分用一条 `apply_transaction` 请求交给存储层应用;`ROLLBACK` 直接丢弃快照。因此隔离级别是快照读加原子提交:事务里看不到别的事务未提交或后来提交的改动,提交对外一次性可见,冲突由调用方回滚或重试。 WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;每组提交在数据和字典都刷盘之后把 `wal.checkpoint` 推进到这条提交的 LSN,含义是「到此为止的改动都已落盘」,再按这个边界丢掉日志前缀——所以日志里通常只有当组在飞的记录。进程重启时只重放检查点之后的记录,按标记分组取已提交的组、丢弃没有标记的整组,重放前逐条做幂等检查作为第二道保险,结束后再把检查点推到最后一条提交。LSN 从检查点与文件尾里较大的那个续号,跨重启单调,`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。 diff --git a/docs/commands.md b/docs/commands.md index a2ad70d..7b1dcaf 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -47,7 +47,7 @@ csql -f json -e "SELECT count(*) FROM orders" 查询写法: -- `SELECT` 支持 `WHERE`、`ORDER BY`、`LIMIT`、`JOIN` / `LEFT JOIN`、`GROUP BY` 与聚合,以及标量 / `IN` / `EXISTS` 子查询。 +- `SELECT` 支持 `WHERE`、`ORDER BY`、`LIMIT`、`JOIN` / `LEFT JOIN`、`GROUP BY` 与聚合,以及标量 / `IN` / `EXISTS` 子查询和量词比较(`op ANY` / `op ALL`)。 - 来源可以是派生表:`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`)。 - 鉴权覆盖整棵来源树:查派生表时内层表也要有权限,只有外层引用的表有权限不够。 @@ -58,6 +58,12 @@ csql -f json -e "SELECT count(*) FROM orders" - `>`、`>=`、`<`、`<=` 在列上有索引时会改写成索引范围扫描;`>=` / `<=` 是闭区间,端点值本身也在结果里。 - 任一操作数为 `NULL` 时比较结果是 `NULL`,`WHERE` 只保留结果为真的行。 +量词比较: + +- `值 op ANY (SELECT 单列 …)` 表示子查询里至少有一行让比较成立;`值 op ALL (…)` 表示每一行都成立。 +- `ANY` 的空子查询为假、`ALL` 的空子查询为真;子查询里出现 `NULL` 时按 `AND` / `OR` 的三值规则,条件可能为 `NULL`(`WHERE` 里等同不通过)。 +- 取反写相反的运算符即可:`<> ALL` 与 `NOT IN` 同义,`<> ANY` 表示不存在相等的行。 + 公共表表达式: - `WITH 名字 AS (SELECT …), 另一个 AS (SELECT …) SELECT … FROM 名字` 先定义再引用,名字可以带列清单(`WITH t(who) AS (SELECT name FROM users) …`)。