From 9b1c835202447612f14292bcb9143beb9b6a7aed Mon Sep 17 00:00:00 2001 From: LIm1tZz7 <36764786+TheBadRoger@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:23:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AF=94=E8=BE=83=E8=BF=90=E7=AE=97=E7=AC=A6?= 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 | 3 + .../src/ChuSQL/Core/Engine/Algebra/Expr.hs | 18 ++++ .../ChuSQL/Core/Engine/Algebra/JoinOrder.hs | 3 + .../ChuSQL/Core/Engine/Algebra/Optimize.hs | 29 ++++++ .../src/ChuSQL/Core/Engine/Algebra/Planner.hs | 3 + .../engine/src/ChuSQL/Core/Engine/Builtin.hs | 12 +++ .../engine/src/ChuSQL/Core/Engine/Semantic.hs | 6 ++ .../src/ChuSQL/Core/Engine/Syntax/AST.hs | 3 + .../src/ChuSQL/Core/Engine/Syntax/Parser.hs | 8 +- chusql-core/engine/test/Spec.hs | 89 +++++++++++++++++++ chusql-server/src/ChuSQL/Server/Backend.hs | 3 + chusql-server/test/Spec.hs | 9 ++ docs/commands.md | 6 ++ 14 files changed, 192 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5967b12..938a6f5 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。 +- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名 → M12 `WITH` / CTE → M13 比较运算符补全。 - 进度与计数见 `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 f2e25df..22a101e 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs @@ -327,6 +327,9 @@ substSubqueries db e env = case e of Gt a b -> bin Gt a b Lt a b -> bin Lt a b Eq a b -> bin Eq a b + GtE a b -> bin GtE a b + LtE a b -> bin LtE a b + NotEq a b -> bin NotEq a b And a b -> bin And a b Or a b -> bin Or a b IsNull a -> un IsNull a 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 ca10b1d..de8e3f0 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs @@ -28,6 +28,9 @@ evalExpr (Neg a) row = do evalExpr (Gt a b) row = liftOperator OpGt a b row evalExpr (Lt a b) row = liftOperator OpLt a b row evalExpr (Eq a b) row = liftOperator OpEq a b row +evalExpr (GtE a b) row = liftOperator OpGtE a b row +evalExpr (LtE a b) row = liftOperator OpLtE a b row +evalExpr (NotEq a b) row = liftOperator OpNe a b row evalExpr (And a b) row = liftOperator OpAnd a b row evalExpr (Or a b) row = liftOperator OpOr a b row evalExpr (IsNull a) row = do @@ -61,6 +64,9 @@ colsInExpr (Neg a) = colsInExpr a colsInExpr (Gt a b) = colsInExpr a ++ colsInExpr b colsInExpr (Lt a b) = colsInExpr a ++ colsInExpr b colsInExpr (Eq a b) = colsInExpr a ++ colsInExpr b +colsInExpr (GtE a b) = colsInExpr a ++ colsInExpr b +colsInExpr (LtE a b) = colsInExpr a ++ colsInExpr b +colsInExpr (NotEq a b) = colsInExpr a ++ colsInExpr b colsInExpr (And a b) = colsInExpr a ++ colsInExpr b colsInExpr (Or a b) = colsInExpr a ++ colsInExpr b colsInExpr (IsNull a) = colsInExpr a @@ -87,6 +93,9 @@ hasDivision e = case e of Gt a b -> both a b Lt a b -> both a b Eq a b -> both a b + GtE a b -> both a b + LtE a b -> both a b + NotEq a b -> both a b And a b -> both a b Or a b -> both a b _ -> False @@ -109,6 +118,9 @@ hasSubquery e = case e of Gt a b -> both a b Lt a b -> both a b Eq a b -> both a b + GtE a b -> both a b + LtE a b -> both a b + NotEq a b -> both a b And a b -> both a b Or a b -> both a b IsNull a -> hasSubquery a @@ -140,6 +152,9 @@ aggregatesIn e = case e of Gt a b -> both a b Lt a b -> both a b Eq a b -> both a b + GtE a b -> both a b + LtE a b -> both a b + NotEq a b -> both a b And a b -> both a b Or a b -> both a b IsNull a -> aggregatesIn a @@ -165,6 +180,9 @@ bareColumns e = case e of Gt a b -> both a b Lt a b -> both a b Eq a b -> both a b + GtE a b -> both a b + LtE a b -> both a b + NotEq a b -> both a b And a b -> both a b Or a b -> both a b IsNull a -> bareColumns a 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 12cf83a..91a865a 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/JoinOrder.hs @@ -62,6 +62,9 @@ scaleRows db p x rows = case p of _ -> quarter Gt _ _ -> quarter Lt _ _ -> quarter + GtE _ _ -> quarter + LtE _ _ -> quarter + NotEq _ _ -> 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 cbebc54..02a063f 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs @@ -144,6 +144,9 @@ foldConstants (Neg a) = foldNode (Neg (foldConstants a)) foldConstants (Gt a b) = foldNode (Gt (foldConstants a) (foldConstants b)) foldConstants (Lt a b) = foldNode (Lt (foldConstants a) (foldConstants b)) 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 (And a b) = foldNode (And (foldConstants a) (foldConstants b)) foldConstants (Or a b) = foldNode (Or (foldConstants a) (foldConstants b)) foldConstants e = e @@ -180,6 +183,32 @@ rewriteNode db (Filter (Lt (Col k) (LitInt v)) (Scan mAlias t Nothing)) | worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, False)) rewriteNode db (Filter (Lt (Col k) (LitStr v)) (Scan mAlias t Nothing)) | worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, False)) +rewriteNode db (Filter (And (GtE (Col k1) (LitInt lo)) (LtE (Col k2) (LitInt hi))) (Scan mAlias t Nothing)) + | k1 == k2 && worthRange db t (unqualify mAlias k1) = + Range mAlias t (unqualify mAlias k1) (Just (VInt lo, True)) (Just (VInt hi, True)) +rewriteNode db (Filter (And (GtE (Col k1) (LitInt lo)) (Lt (Col k2) (LitInt hi))) (Scan mAlias t Nothing)) + | k1 == k2 && worthRange db t (unqualify mAlias k1) = + Range mAlias t (unqualify mAlias k1) (Just (VInt lo, True)) (Just (VInt hi, False)) +rewriteNode db (Filter (And (Gt (Col k1) (LitInt lo)) (LtE (Col k2) (LitInt hi))) (Scan mAlias t Nothing)) + | k1 == k2 && worthRange db t (unqualify mAlias k1) = + Range mAlias t (unqualify mAlias k1) (Just (VInt lo, False)) (Just (VInt hi, True)) +rewriteNode db (Filter (And (GtE (Col k1) (LitStr lo)) (LtE (Col k2) (LitStr hi))) (Scan mAlias t Nothing)) + | k1 == k2 && worthRange db t (unqualify mAlias k1) = + Range mAlias t (unqualify mAlias k1) (Just (VStr lo, True)) (Just (VStr hi, True)) +rewriteNode db (Filter (And (GtE (Col k1) (LitStr lo)) (Lt (Col k2) (LitStr hi))) (Scan mAlias t Nothing)) + | k1 == k2 && worthRange db t (unqualify mAlias k1) = + Range mAlias t (unqualify mAlias k1) (Just (VStr lo, True)) (Just (VStr hi, False)) +rewriteNode db (Filter (And (Gt (Col k1) (LitStr lo)) (LtE (Col k2) (LitStr hi))) (Scan mAlias t Nothing)) + | k1 == k2 && worthRange db t (unqualify mAlias k1) = + Range mAlias t (unqualify mAlias k1) (Just (VStr lo, False)) (Just (VStr hi, True)) +rewriteNode db (Filter (GtE (Col k) (LitInt v)) (Scan mAlias t Nothing)) + | worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VInt v, True)) Nothing +rewriteNode db (Filter (GtE (Col k) (LitStr v)) (Scan mAlias t Nothing)) + | worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VStr v, True)) Nothing +rewriteNode db (Filter (LtE (Col k) (LitInt v)) (Scan mAlias t Nothing)) + | worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, True)) +rewriteNode db (Filter (LtE (Col k) (LitStr v)) (Scan mAlias t Nothing)) + | worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, True)) rewriteNode db (Filter p (Join kind l r c)) | not (hasDivision p || planHasDivision (Join kind l r c)) = case kind of 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 f707e68..510c10f 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs @@ -44,6 +44,9 @@ rewriteAggs aggs e = case [label | (label, agg) <- aggs, agg == e] of Gt a b -> Gt (go a) (go b) Lt a b -> Lt (go a) (go b) Eq a b -> Eq (go a) (go b) + GtE a b -> GtE (go a) (go b) + LtE a b -> LtE (go a) (go b) + NotEq a b -> NotEq (go a) (go b) 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 f7eb90a..cb05a39 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Builtin.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Builtin.hs @@ -137,6 +137,9 @@ data Operator | OpGt | OpLt | OpEq + | OpGtE + | OpLtE + | OpNe | OpAnd | OpOr deriving (Show, Eq, Enum, Bounded) @@ -151,6 +154,9 @@ operatorSymbol OpNeg = "-" operatorSymbol OpGt = ">" operatorSymbol OpLt = "<" operatorSymbol OpEq = "=" +operatorSymbol OpGtE = ">=" +operatorSymbol OpLtE = "<=" +operatorSymbol OpNe = "<>" operatorSymbol OpAnd = "AND" operatorSymbol OpOr = "OR" @@ -164,6 +170,9 @@ operatorOfExpr (Neg _) = Just OpNeg operatorOfExpr (Gt _ _) = Just OpGt operatorOfExpr (Lt _ _) = Just OpLt operatorOfExpr (Eq _ _) = Just OpEq +operatorOfExpr (GtE _ _) = Just OpGtE +operatorOfExpr (LtE _ _) = Just OpLtE +operatorOfExpr (NotEq _ _) = Just OpNe operatorOfExpr (And _ _) = Just OpAnd operatorOfExpr (Or _ _) = Just OpOr operatorOfExpr _ = Nothing @@ -175,6 +184,9 @@ executeOperator op vs = case (op, vs) of (OpGt, [x, y]) -> compareValues (== GT) x y (OpLt, [x, y]) -> compareValues (< EQ) x y (OpEq, [x, y]) -> compareValues (== EQ) x y + (OpGtE, [x, y]) -> compareValues (>= EQ) x y + (OpLtE, [x, y]) -> compareValues (<= EQ) x y + (OpNe, [x, y]) -> compareValues (/= EQ) x y (OpAnd, [x, y]) -> andValues x y (OpOr, [x, y]) -> orValues x y (_, [x, y]) | Just a <- arithOf op -> evalArith a x y diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs index 88bcbc4..b6b2d4b 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs @@ -137,6 +137,9 @@ inferExpr db place env (Neg a) = do inferExpr db place env (Gt a b) = comparison db place env a b inferExpr db place env (Lt a b) = comparison db place env a b inferExpr db place env (Eq a b) = comparison db place env a b +inferExpr db place env (GtE a b) = comparison db place env a b +inferExpr db place env (LtE a b) = comparison db place env a b +inferExpr db place env (NotEq a b) = comparison db place env a b inferExpr db place env (And a b) = boolean db place env a b inferExpr db place env (Or a b) = boolean db place env a b inferExpr db place env (IsNull a) = inferExpr db place env a >> Right (InferType CBool) @@ -597,6 +600,9 @@ resolveExpr db env outer f e = case e of Gt a b -> binary Gt a b Lt a b -> binary Lt a b Eq a b -> binary Eq a b + GtE a b -> binary GtE a b + LtE a b -> binary LtE a b + NotEq a b -> binary NotEq a b And a b -> binary And a b Or a b -> binary Or a b IsNull a -> IsNull <$> resolveExpr db env outer f a 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 048edeb..5b32047 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs @@ -107,6 +107,9 @@ data Expr | Gt Expr Expr | Lt Expr Expr | Eq Expr Expr + | GtE Expr Expr + | LtE Expr Expr + | NotEq Expr Expr | And Expr Expr | Or Expr Expr | IsNull Expr 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 96d67a2..493a7ae 100644 --- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs +++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs @@ -112,7 +112,10 @@ operatorTable = [ [Prefix (Neg <$ symbol "-")] , [InfixL (Mul <$ symbol "*"), InfixL (Div <$ symbol "/")] , [InfixL (Add <$ symbol "+"), InfixL (Sub <$ symbol "-")] - , [ InfixN (Gt <$ symbol ">") + , [ InfixN (NotEq <$ try (symbol "<>" <|> symbol "!=")) + , InfixN (GtE <$ try (symbol ">=")) + , InfixN (LtE <$ try (symbol "<=")) + , InfixN (Gt <$ symbol ">") , InfixN (Lt <$ symbol "<") , InfixN (Eq <$ symbol "=") ] @@ -695,6 +698,9 @@ expandExpr env e = case e of Gt a b -> Gt <$> go a <*> go b Lt a b -> Lt <$> go a <*> go b Eq a b -> Eq <$> go a <*> go b + GtE a b -> GtE <$> go a <*> go b + LtE a b -> LtE <$> go a <*> go b + NotEq a b -> NotEq <$> 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 diff --git a/chusql-core/engine/test/Spec.hs b/chusql-core/engine/test/Spec.hs index 99ef9d9..caa4c0e 100644 --- a/chusql-core/engine/test/Spec.hs +++ b/chusql-core/engine/test/Spec.hs @@ -518,6 +518,90 @@ main = hspec $ do 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 (比较运算符)" $ do + -- sql:在默认库上跑 SQL 取结果行 + let sql input = parseStatement input >>= rowsOf . runStatement testDB + -- values:只取结果里的值 + values input = fmap (map (map snd)) (sql input) + + 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) + + 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) + + it "parses <> and != into the same node" $ do + sql "SELECT name FROM users WHERE age <> 17" + `shouldBe` sql "SELECT name FROM users WHERE age != 17" + values "SELECT name FROM users WHERE age <> 17 ORDER BY name" + `shouldBe` Right [[VStr "Alice"], [VStr "Carol"]] + + it "runs >= and <=" $ do + values "SELECT name FROM users WHERE age >= 25 ORDER BY name" + `shouldBe` Right [[VStr "Alice"], [VStr "Carol"]] + values "SELECT name FROM users WHERE age <= 20" `shouldBe` Right [[VStr "Bob"]] + + it "keeps comparisons with NULL three-valued" $ do + executeOperator OpGtE [VNull, VInt 1] `shouldBe` Right VNull + executeOperator OpLtE [VInt 1, VNull] `shouldBe` Right VNull + executeOperator OpNe [VNull, VInt 1] `shouldBe` Right VNull + executeOperator OpGtE [VInt 1, VFloat 1.0] `shouldBe` Right (VBool True) + executeOperator OpNe [VInt 1, VFloat 1.0] `shouldBe` Right (VBool False) + + it "rewrites >= and <= into an inclusive index range" $ do + let stat rows distinct indexes = TableMeta rows [(c, d, False) | (c, d) <- distinct] indexes + dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m))] + indexed = dbWith (stat 10000 [("id", 10000)] ["id"]) + low = Filter (GtE (Col "id") (LitInt 2)) (Scan Nothing "users" Nothing) + both = Filter (And (GtE (Col "id") (LitInt 1)) (LtE (Col "id") (LitInt 3))) (Scan Nothing "users" Nothing) + halfOpenTop = Filter (And (GtE (Col "id") (LitInt 1)) (Lt (Col "id") (LitInt 3))) (Scan Nothing "users" Nothing) + halfOpenBottom = Filter (And (Gt (Col "id") (LitInt 1)) (LtE (Col "id") (LitInt 3))) (Scan Nothing "users" Nothing) + optimize indexed low `shouldBe` Range Nothing "users" "id" (Just (VInt 2, True)) Nothing + optimize indexed both + `shouldBe` Range Nothing "users" "id" (Just (VInt 1, True)) (Just (VInt 3, True)) + -- 开区间那一端标记取反 + optimize indexed halfOpenTop + `shouldBe` Range Nothing "users" "id" (Just (VInt 1, True)) (Just (VInt 3, False)) + optimize indexed halfOpenBottom + `shouldBe` Range Nothing "users" "id" (Just (VInt 1, False)) (Just (VInt 3, True)) + optimize indexed (Filter (LtE (Col "id") (LitInt 9)) (Scan Nothing "users" Nothing)) + `shouldBe` Range Nothing "users" "id" Nothing (Just (VInt 9, True)) + -- 统计说索引不划算时保持原样,交给全表扫 + optimize (dbWith (stat 10000 [("id", 10000)] [])) low `shouldBe` low + + it "uses an inclusive range on the real storage" $ do + withTestSession $ \srv -> withServerEnv srv $ do + _ <- runIPCStorage (runStatementM (CreateTable "rng_t" [("id", TInt), ("code", TInt)])) + _ <- + runIPCStorage + ( runStatementM + ( Insert + "rng_t" + ["id", "code"] + [ [LitInt 1, LitInt 500] + , [LitInt 2, LitInt 501] + , [LitInt 3, LitInt 502] + , [LitInt 4, LitInt 503] + ] + ) + ) + _ <- runIPCStorage (runStatementM (CreateIndex "rng_t" "code")) + -- ids:按条件取 id,只比较值 + let ids cond = + fmap (map (map snd)) + <$> runIPCStorage (runStatementM (makeSelect ["id"] "rng_t" (Just cond))) + ids (And (GtE (Col "code") (LitInt 501)) (LtE (Col "code") (LitInt 502))) + `shouldReturn` Right [[VInt 2], [VInt 3]] + -- 闭端含边界、开端不含边界 + ids (And (Gt (Col "code") (LitInt 501)) (LtE (Col "code") (LitInt 502))) + `shouldReturn` Right [[VInt 3]] + ids (And (GtE (Col "code") (LitInt 501)) (Lt (Col "code") (LitInt 502))) + `shouldReturn` Right [[VInt 2]] + ids (And (Gt (Col "code") (LitInt 501)) (Lt (Col "code") (LitInt 502))) + `shouldReturn` Right [] + describe "ChuSQL.Core.Engine (SELECT)" $ do -- sql:在默认库上跑 SQL 取结果行 let sql input = parseStatement input >>= rowsOf . runStatement testDB @@ -2660,6 +2744,7 @@ planTables op = case op of Sort _ x -> planTables x Limit _ x -> planTables x Join _ l r _ -> planTables l ++ planTables r + Derived _ x -> planTables x Unit -> [] -- | 计划里每个 Join 节点是不是 LEFT JOIN(先序) @@ -2671,6 +2756,7 @@ leftJoinKinds (Aggregate _ _ x) = leftJoinKinds x leftJoinKinds (Filter _ x) = leftJoinKinds x leftJoinKinds (Sort _ x) = leftJoinKinds x leftJoinKinds (Limit _ x) = leftJoinKinds x +leftJoinKinds (Derived _ x) = leftJoinKinds x leftJoinKinds _ = [] -- | 取计划里第一个过滤条件 @@ -2688,6 +2774,7 @@ firstFilterCond (Join _ l r _) = case firstFilterCond l of firstFilterCond (Scan _ _ _) = Nothing firstFilterCond (Lookup _ _ _ _) = Nothing firstFilterCond (Range _ _ _ _ _) = Nothing +firstFilterCond (Derived _ x) = firstFilterCond x -- | 判断计划里是否还有过滤节点 anyFilter :: RelOp -> Bool @@ -2702,6 +2789,7 @@ anyFilter (Join _ l r _) = anyFilter l || anyFilter r anyFilter (Scan _ _ _) = False anyFilter (Lookup _ _ _ _) = False anyFilter (Range _ _ _ _ _) = False +anyFilter (Derived _ x) = anyFilter x -- | 去掉计划里的投影节点 stripProjects :: RelOp -> RelOp @@ -2716,6 +2804,7 @@ stripProjects (Join k l r c) = Join k (stripProjects l) (stripProjects r) c stripProjects (Scan a t _) = Scan a t Nothing stripProjects (Lookup a t c k) = Lookup a t c k stripProjects (Range a t c lo hi) = Range a t c lo hi +stripProjects (Derived a x) = Derived a (stripProjects x) -- | 解析并做投影下推,返回计划 projectedPlan :: String -> Either String RelOp diff --git a/chusql-server/src/ChuSQL/Server/Backend.hs b/chusql-server/src/ChuSQL/Server/Backend.hs index f018fb7..1cf5a2e 100644 --- a/chusql-server/src/ChuSQL/Server/Backend.hs +++ b/chusql-server/src/ChuSQL/Server/Backend.hs @@ -300,6 +300,9 @@ exprTables expr = case expr of Gt a b -> both a b Lt a b -> both a b Eq a b -> both a b + GtE a b -> both a b + LtE a b -> both a b + NotEq a b -> both a b And a b -> both a b Or a b -> both a b Neg a -> exprTables a diff --git a/chusql-server/test/Spec.hs b/chusql-server/test/Spec.hs index 15e6a99..56ebd32 100644 --- a/chusql-server/test/Spec.hs +++ b/chusql-server/test/Spec.hs @@ -661,6 +661,15 @@ privilegeSpec = describe "server privileges" $ do 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 "requires the privilege on the table inside a comparison 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 >= (SELECT id FROM users)" + `shouldReturn` Right () + authorizeSql service (Ordinary testAccount) "SELECT name FROM users WHERE id >= (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/commands.md b/docs/commands.md index 180a18a..a2ad70d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -52,6 +52,12 @@ csql -f json -e "SELECT count(*) FROM orders" - 投影列可以起别名:`SELECT count(*) AS total FROM orders`,别名就是结果集的列名。`ORDER BY` 仍按来源列解析,要引用派生表的列得写限定名(如 `ORDER BY d.total`)。 - 鉴权覆盖整棵来源树:查派生表时内层表也要有权限,只有外层引用的表有权限不够。 +比较运算符: + +- 支持 `=`、`<>`(也可写 `!=`)、`>`、`>=`、`<`、`<=`;`<>` 与 `!=` 是同一个运算符。 +- `>`、`>=`、`<`、`<=` 在列上有索引时会改写成索引范围扫描;`>=` / `<=` 是闭区间,端点值本身也在结果里。 +- 任一操作数为 `NULL` 时比较结果是 `NULL`,`WHERE` 只保留结果为真的行。 + 公共表表达式: - `WITH 名字 AS (SELECT …), 另一个 AS (SELECT …) SELECT … FROM 名字` 先定义再引用,名字可以带列清单(`WITH t(who) AS (SELECT name FROM users) …`)。