准备可扩展类型系统工作,更合理的JOIN查询优化算法,并行查询

This commit is contained in:
LIm1tZz7 committed 2026-10-03 11:56:13 +08:00
1 parent 9758439b82
commit 274b7fcdcc
75 files changed
+3828 -1789

No files matched your search

-82
View File
@@ -1,82 +0,0 @@
module Main where
import ChuSQL.Core.Engine.Algebra.Op (renderPlan)
import ChuSQL.Core.Engine.Algebra.Optimize (optimize)
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Engine
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser
-- 命令行演示入口:跑几条语句并打印结果与优化前后的计划。
users :: Table
users =
Table
{ tableName = "users"
, tableCols = [("id", TInt), ("name", TStr), ("age", TInt)]
, tableRows =
[ [("id", VInt 1), ("name", VStr "Alice"), ("age", VInt 25)]
, [("id", VInt 2), ("name", VStr "Bob"), ("age", VInt 17)]
, [("id", VInt 3), ("name", VStr "Carol"), ("age", VInt 30)]
]
}
orders :: Table
orders =
Table
{ tableName = "orders"
, tableCols = [("id", TInt), ("user_id", TInt), ("product", TStr)]
, tableRows =
[ [("id", VInt 1), ("user_id", VInt 1), ("product", VStr "Book")]
, [("id", VInt 2), ("user_id", VInt 2), ("product", VStr "Pen")]
, [("id", VInt 3), ("user_id", VInt 1), ("product", VStr "Cup")]
]
}
db :: Database
db = [("users", users), ("orders", orders)]
-- | 跑演示语句,打印结果与优化前后的计划
main :: IO ()
main = do
putStrLn "==================== Engine ====================="
let q1 = makeSelect ["name"] "users" (Just (Gt (Col "age") (LitInt 18)))
print (rowsOf (runStatement db q1))
let q2 = makeSelect ["*"] "users" Nothing
print (rowsOf (runStatement db q2))
let q3 = makeSelect ["name"] "nonexistent" Nothing
print (rowsOf (runStatement db q3))
let q4 =
makeSelect
["name"]
"users"
( Just
( Or
(Lt (Col "age") (LitInt 18))
(Gt (Col "age") (LitInt 30))
)
)
print (rowsOf (runStatement db q4))
let parsed = parseStatement "SELECT name FROM users WHERE age > 18 AND name = 'Alice'"
print (rowsOf (parsed >>= runStatement db))
putStrLn "\n==================== Optimizer ====================="
let planSql =
"SELECT u.name, o.product FROM users u \
\JOIN orders o ON u.id = o.user_id WHERE u.age > 18"
case parseStatement planSql of
Left err -> putStrLn err
Right query -> case translate query of
Left err -> putStrLn err
Right plan -> do
putStrLn "--- plan before optimization ---"
putStrLn (renderPlan plan)
putStrLn "--- plan after optimization ---"
putStrLn (renderPlan (optimize db plan))
putStrLn "--- rows ---"
print (rowsOf (runStatement db query))
+8 -16
View File
@@ -9,13 +9,18 @@ build-type: Simple
library
hs-source-dirs: src
exposed-modules: ChuSQL.Core.Engine.Algebra.Eval
exposed-modules: ChuSQL.Core.Engine.Algebra.Cost
ChuSQL.Core.Engine.Algebra.Eval
ChuSQL.Core.Engine.Algebra.Expr
ChuSQL.Core.Engine.Algebra.JoinOrder
ChuSQL.Core.Engine.Algebra.Op
ChuSQL.Core.Engine.Algebra.Optimize
ChuSQL.Core.Engine.Algebra.Planner
ChuSQL.Core.Engine.Algebra.Sort
ChuSQL.Core.Engine.Builtin
ChuSQL.Core.Engine.Error
ChuSQL.Core.Engine
ChuSQL.Core.Engine.Parallel
ChuSQL.Core.Engine.Semantic
ChuSQL.Core.Engine.Storage
ChuSQL.Core.Engine.Storage.FFI
@@ -42,20 +47,6 @@ library
default-language: Haskell2010
ghc-options: -Wall
executable chusql-core-engine
main-is: Main.hs
hs-source-dirs: app
build-depends: base
, chusql-core-engine
, chusql-core-model
default-language: Haskell2010
ghc-options: -Wall
-- 运行期找 libchusql_core_storage.so:$ORIGIN 让二进制与动态库同目录即可
if os(linux)
ld-options: -Wl,-rpath,$ORIGIN
if os(osx)
ld-options: -Wl,-rpath,@loader_path
test-suite chusql-core-engine-test
type: exitcode-stdio-1.0
main-is: Spec.hs
@@ -65,6 +56,7 @@ test-suite chusql-core-engine-test
, chusql-core-model
, directory
, filepath
, hashable
, hspec
default-language: Haskell2010
ghc-options: -Wall
ghc-options: -Wall -threaded
+43 -28
View File
@@ -11,6 +11,7 @@ import ChuSQL.Core.Engine.Storage.Memory (MemoryStorage (runMemoryStorage))
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseExpression)
import Data.Maybe (mapMaybe)
import qualified Data.HashSet as HS
-- 引擎入口:检查语义、按语句分发执行,另有内存实现与泛型版本。
@@ -86,13 +87,15 @@ runStatementUncheckedM db (Update tbl assigns mWhere) =
rowsResult <- scan tbl
case rowsResult of
Left err -> pure (Left err)
Right rows -> do
newRows <- mapM (updateRowM db table mWhere assigns) rows
case sequence newRows of
Left err -> pure (Left err)
Right rs -> do
result <- replaceAll tbl rs
pure (result >> Right [])
Right rows -> case checkPlan table of
Left err -> pure (Left err)
Right plan -> do
newRows <- mapM (updateRowM db table plan mWhere assigns) rows
case sequence newRows of
Left err -> pure (Left err)
Right rs -> do
result <- replaceAll tbl rs
pure (result >> Right [])
runStatementUncheckedM _ (CreateTable name cols) = do
result <- createTable name cols
pure (result >> Right [])
@@ -156,7 +159,8 @@ buildRows table current cols rows = do
-- | 逐行构造,最后统一查一遍约束
go _ _ [] acc = do
let built = reverse acc
mapM_ (runChecks table) built
plan <- checkPlan table
mapM_ (runChecks plan) built
enforceNotNull (tableCols table) built
enforceUnique (tableCols table) (current ++ built)
Right built
@@ -208,19 +212,29 @@ autoStart (Just (name, _)) rows = case [n | r <- rows, Just (VInt n) <- [lookup
[] -> 1
ns -> maximum ns + 1
-- | 执行 CHECK 约束
runChecks :: Table -> Row -> Either String ()
runChecks table row = mapM_ check (tableCols table)
-- | 一次解析出来的 CHECK 条件
type CheckPlan = [(String, Expr)]
-- | 把表上的 CHECK 文本解析成条件,每语句只解析一次
checkPlan :: Table -> Either String CheckPlan
checkPlan table = mapM parse (mapMaybe checked (tableCols table))
where
-- | 查这一列的 CHECK
check (name, col) = case columnCheck col of
Nothing -> Right ()
Just text -> case parseExpression text of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ firstLine err)
Right e -> case evalCondForRow e row of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ err)
Right True -> Right ()
Right False -> Left ("CHECK on " ++ name ++ " failed")
-- | 只挑出带 CHECK 文本的列
checked (name, col) = (,) name <$> columnCheck col
-- | 解析一列的 CHECK 文本
parse (name, text) = case parseExpression text of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ firstLine err)
Right e -> Right (name, e)
-- | 执行解析好的 CHECK 条件
runChecks :: CheckPlan -> Row -> Either String ()
runChecks plan row = mapM_ check plan
where
-- | 跑一条 CHECK
check (name, e) = case evalCondForRow e row of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ err)
Right True -> Right ()
Right False -> Left ("CHECK on " ++ name ++ " failed")
-- | 只取第一行(报错提示用)
firstLine :: String -> String
@@ -242,18 +256,18 @@ enforceUnique cols rows = mapM_ check targets
where
targets = [(n, c) | (n, c) <- cols, columnUnique c || columnPrimaryKey c]
-- | 查这一列有没有重复值
check (name, _) = go name [] (mapMaybe (lookup name) rows)
check (name, _) = go name HS.empty (mapMaybe (lookup name) rows)
-- | 逐个值比过去
go _ _ [] = Right ()
-- | NULL 不算冲突,跳过
go name seen (VNull : rest) = go name seen rest
go name seen (v : rest)
| v `elem` seen = Left ("UNIQUE: duplicate value in column " ++ name)
| otherwise = go name (v : seen) rest
| HS.member (canonicalValue v) seen = Left ("UNIQUE: duplicate value in column " ++ name)
| otherwise = go name (HS.insert (canonicalValue v) seen) rest
-- | UPDATE 的一行(条件与赋值都可能带子查询)
updateRowM :: (MonadStorage m) => Database -> Table -> Maybe Expr -> [(String, Expr)] -> Row -> m (Either String Row)
updateRowM db table cond asgns row = do
updateRowM :: (MonadStorage m) => Database -> Table -> CheckPlan -> Maybe Expr -> [(String, Expr)] -> Row -> m (Either String Row)
updateRowM db table plan cond asgns row = do
keep <- case cond of
Nothing -> pure (Right True)
Just e -> evalCondForRowM db e row
@@ -262,7 +276,7 @@ updateRowM db table cond asgns row = do
Right False -> pure (Right row)
Right True -> do
row' <- applyUpdatesM db table asgns row
pure (row' >>= \r -> runChecks table r >> Right r)
pure (row' >>= \r -> runChecks plan r >> Right r)
-- | 依次求值、收类型并覆盖列(表达式可以带子查询)
applyUpdatesM :: (MonadStorage m) => Database -> Table -> [(String, Expr)] -> Row -> m (Either String Row)
@@ -316,8 +330,9 @@ alterTable db tbl change = case lookupTable db tbl of
where
-- | 改完再统一查一遍约束
checkAltered (cols, rows) = do
let table = Table tbl cols rows
mapM_ (runChecks table) rows
let table = Table tbl cols rows Nothing
plan <- checkPlan table
mapM_ (runChecks plan) rows
enforceNotNull cols rows
enforceUnique cols rows
Right (cols, rows)
@@ -0,0 +1,31 @@
module ChuSQL.Core.Engine.Algebra.Cost (tableMetaOf, probeCost, pointRows, rangeRows, indexCheaper) where
import ChuSQL.Core.Model
-- 统计与成本估算:行数、列级不同值数与索引探路页数。
-- | 表统计,拿不到就是 Nothing
tableMetaOf :: Database -> String -> Maybe TableMeta
tableMetaOf db t = lookup t db >>= tableMeta
-- | 索引探路的页数:B+Tree 高度
probeCost :: Int -> Int
probeCost rows = max 1 (ceiling (logBase 2 (fromIntegral (max 2 rows) :: Double)))
-- | 等值点查的估算命中行数:行数除以该列不同值数
pointRows :: TableMeta -> String -> Int
pointRows m col = case [d | (c, d, _) <- metaDistinct m, c == col, d > 1] of
(d : _) -> max 1 (rows `div` d)
[] -> rows
where
rows = max 0 (metaRowCount m)
-- | 范围扫描的估算命中行数:没有直方图,按四分之一算
rangeRows :: TableMeta -> Int
rangeRows m = max 0 (metaRowCount m) `div` 4
-- | 有索引且探路加命中比全表扫便宜
indexCheaper :: TableMeta -> String -> Int -> Bool
indexCheaper m col est = col `elem` metaIndexes m && probeCost rows + est < rows
where
rows = max 0 (metaRowCount m)
@@ -1,15 +1,17 @@
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.Optimize (optimize, relOpCols)
import ChuSQL.Core.Engine.Algebra.Optimize (optimize)
import ChuSQL.Core.Engine.Algebra.Op
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Engine.Algebra.Sort (compareValue, sortRows)
import ChuSQL.Core.Engine.Algebra.Sort (sortRows)
import ChuSQL.Core.Engine.Builtin (aggregateArg, builtinOfExpr, invokeBuiltin, unsupportedAggregate)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Storage (IndexResult (..), MonadStorage (..))
import ChuSQL.Core.Engine.Storage.Memory (runMemoryStorage)
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), JoinKind (..), Subquery (..))
import Control.Monad (filterM, foldM)
import Control.Monad (filterM)
import qualified Data.HashMap.Strict as HM
-- 执行:按算子树算出结果行,外层行供相关子查询引用。
@@ -58,17 +60,10 @@ rowsCols :: [Row] -> [String]
rowsCols (r : _) = map fst r
rowsCols [] = []
-- | 参与等值匹配的键:NULL 不参与,整数值的浮点归一
-- | 参与等值匹配的键:NULL 不参与,整值浮点归一
joinKey :: Value -> Maybe Value
joinKey VNull = Nothing
joinKey (VFloat d)
| not (isNaN d)
, not (isInfinite d)
, fromIntegral (rounded :: Integer) == d =
Just (VInt (fromIntegral rounded))
where
rounded = round d :: Integer
joinKey v = Just v
joinKey v = Just (canonicalValue v)
-- | 取一行的连接键
joinKeyOf :: String -> Row -> Maybe Value
@@ -230,57 +225,13 @@ groupRows keys rows = go rows (HM.empty, [])
valuesOf :: Row -> Expr -> [Row] -> Either String [Value]
valuesOf outer e rows = mapM (evalExpr e . (++ outer)) rows
-- | 去掉 NULL
nonNull :: [Value] -> [Value]
nonNull = filter (/= VNull)
-- | 数值相加:整数配整数还是整数
addValue :: Value -> Value -> Either String Value
addValue (VInt a) (VInt b) = Right (VInt (a + b))
addValue (VInt a) (VFloat b) = Right (VFloat (fromIntegral a + b))
addValue (VFloat a) (VInt b) = Right (VFloat (a + fromIntegral b))
addValue (VFloat a) (VFloat b) = Right (VFloat (a + b))
addValue _ _ = Left "type error: expected two numbers"
-- | 能当数用的值
numberOf :: Value -> Either String Double
numberOf (VInt n) = Right (fromIntegral n)
numberOf (VFloat d) = Right d
numberOf _ = Left "type error: expected a number"
-- | 一组值里的极值(NULL 已经先剔掉)
extremeOf :: (Value -> Value -> Value) -> [Value] -> Either String Value
extremeOf _ [] = Right VNull
extremeOf pick (x : xs) = Right (foldl pick x xs)
-- | 一个聚合算子在一组行上的结果
-- | 一个聚合算子在一组行上的结果,统一走内置函数权威
evalAgg :: Row -> Expr -> [Row] -> Either String Value
evalAgg outer agg rows = case agg of
CountAll -> Right (VInt (length rows))
CountOf e -> do
vs <- valuesOf outer e rows
Right (VInt (length (nonNull vs)))
SumOf e -> do
vs <- valuesOf outer e rows
case nonNull vs of
[] -> Right VNull
xs -> foldM addValue (VInt 0) xs
AvgOf e -> do
vs <- valuesOf outer e rows
case nonNull vs of
[] -> Right VNull
xs -> do
total <- foldM addValue (VInt 0) xs
sumOf <- numberOf total
Right (VFloat (sumOf / fromIntegral (length xs)))
MinOf e -> valuesOf outer e rows >>= extremeOf lower . nonNull
MaxOf e -> valuesOf outer e rows >>= extremeOf higher . nonNull
_ -> Left "only COUNT, SUM, AVG, MIN and MAX can be aggregated"
where
-- | 取更小的那个
lower a b = if compareValue b a == LT then b else a
-- | 取更大的那个
higher a b = if compareValue b a == GT then b else a
evalAgg outer agg rows = case builtinOfExpr agg of
Nothing -> Left unsupportedAggregate
Just builtin -> do
vs <- maybe (Right []) (\e -> valuesOf outer e rows) (aggregateArg agg)
invokeBuiltin builtin (length rows) vs
-- | 一组的输出行:分组列 + 各聚合值
oneGroup :: Row -> [String] -> [(String, Expr)] -> ([Value], [Row]) -> Either String Row
@@ -429,8 +380,8 @@ evalRelOpIn db outer (Compute items op) = do
evalRelOpIn db outer (Aggregate keys aggs op) = do
rows <- evalRelOpIn db outer op
pure (rows >>= evalAggregate outer keys aggs)
evalRelOpIn _ _ (Scan mAlias tbl cols) = do
res <- maybe (scan tbl) (scanColumns tbl) cols
evalRelOpIn db _ (Scan mAlias tbl cols) = do
res <- scanTable db tbl cols
pure (fmap (prefixRows mAlias) res)
evalRelOpIn db outer (Lookup mAlias tbl col k) = do
res <- lookupByColumn tbl col k
@@ -487,8 +438,23 @@ scanFiltered ::
String ->
Expr ->
m (Either String [Row])
scanFiltered _ outer mAlias tbl cond = do
res <- scan tbl
scanFiltered db outer mAlias tbl cond = do
res <- scanTable db tbl Nothing
pure $ do
rows <- res
filterM (evalCondForRow cond . (++ outer)) (prefixRows mAlias rows)
-- | 行数到阈值才值得并行分片
parallelRowThreshold :: Int
parallelRowThreshold = 1000
-- | 表够大且存储支持并行时按分片扫,否则顺序扫
scanTable :: (MonadStorage m) => Database -> String -> Maybe [String] -> m (Either String [Row])
scanTable db tbl cols = do
width <- parallelShards
if width > 1 && rows >= parallelRowThreshold
then scanShards width tbl cols
else maybe (scan tbl) (scanColumns tbl) cols
where
-- | 统计里的行数,没有统计算 0
rows = maybe 0 metaRowCount (tableMetaOf db tbl)
@@ -1,8 +1,8 @@
module ChuSQL.Core.Engine.Algebra.Expr (evalExpr, evalCondForRow, colsInExpr, aggregatesIn, hasAggregate, bareColumns, hasSubquery, inValues, threeValuedNot) where
module ChuSQL.Core.Engine.Algebra.Expr (evalExpr, evalCondForRow, colsInExpr, aggregatesIn, hasAggregate, bareColumns, hasSubquery, hasDivision, inValues, threeValuedNot) where
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import Control.Monad (join)
import ChuSQL.Core.Engine.Builtin (Operator (..), executeOperator, inValues, threeValuedNot)
-- 表达式求值:三值逻辑,NULL 参与运算结果仍是 NULL。
@@ -18,22 +18,18 @@ evalExpr (LitBool b) _ = Right (VBool b)
evalExpr (LitDate s) _ = Right (VStr s)
evalExpr (LitTimestamp s) _ = Right (VStr s)
evalExpr (LitBlob s) _ = Right (VStr s)
evalExpr (Add a b) row = liftArith addArith a b row
evalExpr (Sub a b) row = liftArith subArith a b row
evalExpr (Mul a b) row = liftArith mulArith a b row
evalExpr (Div a b) row = liftArith divArith a b row
evalExpr (Add a b) row = liftOperator OpAdd a b row
evalExpr (Sub a b) row = liftOperator OpSub a b row
evalExpr (Mul a b) row = liftOperator OpMul a b row
evalExpr (Div a b) row = liftOperator OpDiv a b row
evalExpr (Neg a) row = do
v <- evalExpr a row
case v of
VNull -> Right VNull
VInt n -> Right (VInt (negate n))
VFloat d -> Right (VFloat (negate d))
_ -> Left "type error: expected a number"
evalExpr (Gt a b) row = liftCompare (> EQ) a b row
evalExpr (Lt a b) row = liftCompare (< EQ) a b row
evalExpr (Eq a b) row = liftCompare (== EQ) a b row
evalExpr (And a b) row = liftBool andValues a b row
evalExpr (Or a b) row = liftBool orValues a b row
executeOperator OpNeg [v]
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 (And a b) row = liftOperator OpAnd a b row
evalExpr (Or a b) row = liftOperator OpOr a b row
evalExpr (IsNull a) row = do
v <- evalExpr a row
Right (VBool (v == VNull))
@@ -54,21 +50,6 @@ evalExpr agg _ = Left ("aggregate functions are not allowed here: " ++ show agg)
subqueryNeedsDatabase :: String
subqueryNeedsDatabase = "subquery needs a database to run"
-- | IN 三值逻辑:命中为真,碰 NULL 得 NULL
inValues :: Value -> [Value] -> Either String Value
inValues VNull _ = Right VNull
inValues v vs = do
results <- mapM (compareValues (== EQ) v) vs
Right $
if VBool True `elem` results
then VBool True
else if VNull `elem` results then VNull else VBool False
-- | NOT 的三值取反
threeValuedNot :: Value -> Value
threeValuedNot (VBool b) = VBool (not b)
threeValuedNot _ = VNull
-- | 表达式用到了哪些列
colsInExpr :: Expr -> [String]
colsInExpr (Col c) = [c]
@@ -95,6 +76,24 @@ colsInExpr (InList a es _) = colsInExpr a ++ concatMap colsInExpr es
colsInExpr (ExistsSub sq _) = subqueryRefs sq
colsInExpr _ = []
-- | 表达式里有没有除法(除法可能报错,不能提前或延后求值)
hasDivision :: Expr -> Bool
hasDivision e = case e of
Div _ _ -> True
Neg a -> hasDivision a
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
Gt a b -> both a b
Lt a b -> both a b
Eq a b -> both a b
And a b -> both a b
Or a b -> both a b
_ -> False
where
-- | 两个子表达式里任意一个有除法
both a b = hasDivision a || hasDivision b
-- | 表达式里有没有子查询
hasSubquery :: Expr -> Bool
hasSubquery e = case e of
@@ -188,107 +187,9 @@ evalCondForRow e row = do
VNull -> Right False
_ -> Left "type error: WHERE condition must be a boolean"
-- | 把二元运算提升到 Either
liftBinOp ::
(Value -> Value -> Either String Value) ->
Expr ->
Expr ->
Row ->
Either String Value
liftBinOp f a b row = join (f <$> evalExpr a row <*> evalExpr b row)
-- | 整数算术与浮点算术成对出现
data Arith = Arith
{ arithInt :: Int -> Int -> Either String Int
, arithFloat :: Double -> Double -> Either String Double
}
-- | 一边是 NULL 就出 NULL,两边都是整数就出整数
liftArith :: Arith -> Expr -> Expr -> Row -> Either String Value
liftArith ops a b row = liftBinOp go a b row
where
-- | NULL 出 NULL,整数配整数走整数算术
go VNull _ = Right VNull
go _ VNull = Right VNull
go (VInt x) (VInt y) = VInt <$> arithInt ops x y
go x y = case (asDouble x, asDouble y) of
(Just p, Just q) -> VFloat <$> arithFloat ops p q
_ -> Left "type error: expected two numbers"
-- | 能当数用的值
asDouble :: Value -> Maybe Double
asDouble (VInt n) = Just (fromIntegral n)
asDouble (VFloat d) = Just d
asDouble _ = Nothing
-- | 加法
addArith :: Arith
addArith = Arith (\x y -> Right (x + y)) (\x y -> Right (x + y))
-- | 减法
subArith :: Arith
subArith = Arith (\x y -> Right (x - y)) (\x y -> Right (x - y))
-- | 乘法
mulArith :: Arith
mulArith = Arith (\x y -> Right (x * y)) (\x y -> Right (x * y))
-- | 整数除法向零截断,溢出返回错误;浮点除法不能除以零
divArith :: Arith
divArith = Arith intDiv floatDiv
where
-- | 整数除法:除零与溢出都报错
intDiv _ 0 = Left "division by zero"
intDiv x y
| x == minBound && y == -1 = Left "integer division overflow"
| otherwise = Right (x `quot` y)
-- | 浮点除法:不能除以零
floatDiv _ 0 = Left "division by zero"
floatDiv x y = Right (x / y)
-- | 比较:任一侧是 NULL 就给 NULL
liftCompare ::
(Ordering -> Bool) ->
Expr ->
Expr ->
Row ->
Either String Value
liftCompare rel a b row = liftBinOp (compareValues rel) a b row
-- | 比较两个值:NULL 给 NULL,其余按数、串、布尔比
compareValues :: (Ordering -> Bool) -> Value -> Value -> Either String Value
compareValues rel x y
| x == VNull || y == VNull = Right VNull
| Just p <- asDouble x, Just q <- asDouble y = Right (VBool (rel (compare p q)))
| (VStr p, VStr q) <- (x, y) = Right (VBool (rel (compare p q)))
| (VBool p, VBool q) <- (x, y) = Right (VBool (rel (compare p q)))
| otherwise = Left "type error: cannot compare these values"
-- | 布尔二元运算
liftBool ::
(Value -> Value -> Either String Value) ->
Expr ->
Expr ->
Row ->
Either String Value
liftBool f = liftBinOp f
-- | 逻辑与的三值真值表
andValues :: Value -> Value -> Either String Value
andValues (VBool False) _ = Right (VBool False)
andValues _ (VBool False) = Right (VBool False)
andValues (VBool True) (VBool True) = Right (VBool True)
andValues VNull (VBool True) = Right VNull
andValues (VBool True) VNull = Right VNull
andValues VNull VNull = Right VNull
andValues _ _ = Left "type error: expected two booleans"
-- | 逻辑或的三值真值表
orValues :: Value -> Value -> Either String Value
orValues (VBool True) _ = Right (VBool True)
orValues _ (VBool True) = Right (VBool True)
orValues (VBool False) (VBool False) = Right (VBool False)
orValues VNull (VBool False) = Right VNull
orValues (VBool False) VNull = Right VNull
orValues VNull VNull = Right VNull
orValues _ _ = Left "type error: expected two booleans"
-- | 二元运算节点统一走运算符权威
liftOperator :: Operator -> Expr -> Expr -> Row -> Either String Value
liftOperator op a b row = do
x <- evalExpr a row
y <- evalExpr b row
executeOperator op [x, y]
@@ -0,0 +1,197 @@
module ChuSQL.Core.Engine.Algebra.JoinOrder (reorderJoins) where
import ChuSQL.Core.Engine.Algebra.Cost (pointRows, rangeRows, tableMetaOf)
import ChuSQL.Core.Engine.Algebra.Expr (colsInExpr, hasDivision)
import ChuSQL.Core.Engine.Algebra.Op (RelOp (..), relOpCols)
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), JoinKind (..))
import ChuSQL.Core.Model
import Data.List (partition)
-- 内连接链顺序优化:按统计估算中间结果,只在行序不可观察处重排。
-- | 重排入口:顶层行序可观察
reorderJoins :: Database -> RelOp -> RelOp
reorderJoins db = pass db False
-- | 往下带“行序是否不可观察”,可观察时不动内连接
pass :: Database -> Bool -> RelOp -> RelOp
pass db _ (Sort spec x) = Sort spec (pass db True x)
pass db _ (Limit n x) = Limit n (pass db False x)
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 (Join kind l r c)
| free
, Just (leaves, conds) <- innerChain (Join kind l r c)
, length leaves > 1
, Just order <- cheaperOrder db leaves conds =
rebuildChain db (map (pass db free) order) conds
| otherwise = Join kind (pass db free l) (pass db free r) c
pass _ _ op = op
-- | 把内连接链拆成叶子与条件,遇到外连接就放弃
innerChain :: RelOp -> Maybe ([RelOp], [Expr])
innerChain (Join InnerJoin l r c) = do
(ls, lcs) <- innerChain l
(rs, rcs) <- innerChain r
pure (ls ++ rs, lcs ++ rcs ++ [c])
innerChain (Join _ _ _ _) = Nothing
innerChain op = Just ([op], [])
-- | 叶子算子的估算行数,拿不到统计就是 Nothing
leafRows :: Database -> RelOp -> Maybe Int
leafRows db op = case op of
Scan _ t _ -> rowCountOf db t
Lookup _ t c _ -> pointRows <$> tableMetaOf db t <*> pure c
Range _ t _ _ _ -> rangeRows <$> tableMetaOf db t
Filter p x -> scaleRows db p x <$> leafRows db x
Project _ x -> leafRows db x
_ -> Nothing
-- | 表统计里的行数
rowCountOf :: Database -> String -> Maybe Int
rowCountOf db t = metaRowCount <$> tableMetaOf db t
-- | 过滤之后的大致行数:等值看不同值数,其他按四分之一
scaleRows :: Database -> Expr -> RelOp -> Int -> Int
scaleRows db p x rows = case p of
Eq (Col k) _ -> case distinctOf db x k of
Just d | d > 1 -> max 1 (rows `div` d)
_ -> quarter
Gt _ _ -> quarter
Lt _ _ -> quarter
And a b -> max 1 (min (scaleRows db a x rows) (scaleRows db b x rows))
_ -> quarter
where
quarter = max 1 (rows `div` 4)
-- | 叶子对应的别名与表名
leafSource :: RelOp -> Maybe (Maybe String, String)
leafSource (Scan a t _) = Just (a, t)
leafSource (Lookup a t _ _) = Just (a, t)
leafSource (Range a t _ _ _) = Just (a, t)
leafSource (Filter _ x) = leafSource x
leafSource (Project _ x) = leafSource x
leafSource _ = Nothing
-- | 某列在叶子统计里的不同值数
distinctOf :: Database -> RelOp -> String -> Maybe Int
distinctOf db leaf col = do
(alias, t) <- leafSource leaf
m <- tableMetaOf db t
num <- lookup (unqualify alias col) [(c, n) | (c, n, _) <- metaDistinct m]
pure num
-- | 若干叶子里某列最大的不同值数
distinctOn :: Database -> [RelOp] -> String -> Maybe Int
distinctOn db leaves col = case [d | leaf <- leaves, Just d <- [distinctOf db leaf col]] of
[] -> Nothing
ds -> Just (maximum ds)
-- | 两侧等值键里较大的不同值数
keyDistinct :: Database -> [RelOp] -> RelOp -> [Expr] -> Maybe Int
keyDistinct db have x conds = case [v | Eq (Col a) (Col b) <- conds, Just v <- [pair a b]] of
(v : _) -> Just v
[] -> Nothing
where
haveCols = concatMap (relOpCols db) have
xCols = relOpCols db x
-- | 这一对列是不是两侧之间的等值键
pair a b
| a `elem` haveCols, b `elem` xCols = bigger (distinctOn db have a) (distinctOf db x b)
| b `elem` haveCols, a `elem` xCols = bigger (distinctOn db have b) (distinctOf db x a)
| otherwise = Nothing
-- | 两个可选统计取大的那个
bigger (Just a) (Just b) = Just (max a b)
bigger (Just a) Nothing = Just a
bigger Nothing (Just b) = Just b
bigger Nothing Nothing = Nothing
-- | 一次连接之后的大致行数
stepRows :: Database -> [RelOp] -> Int -> RelOp -> [Expr] -> Int
stepRows db have acc x conds = case keyDistinct db have x conds of
Just v | v > 1 -> max 1 (acc * rows `div` v)
_ -> acc * rows
where
rows = maybe 1 id (leafRows db x)
-- | 一批叶子按给定顺序连接,累加每一步的中间结果
chainCost :: Database -> [RelOp] -> [Expr] -> Int
chainCost db (l : ls) conds = go [l] (maybe 1 id (leafRows db l)) ls
where
-- | 逐步接上后面的叶子
go _ _ [] = 0
go have acc (x : xs) =
let card = stepRows db have acc x conds
in card + go (have ++ [x]) card xs
chainCost _ [] _ = 0
-- | 贪心扩展一个顺序:每次挑中间结果最小的下一张
growOrder :: Database -> [RelOp] -> [Expr] -> [Int] -> [Int] -> [Int]
growOrder db leaves conds have rest = case rest of
[] -> have
_ ->
let next = pick rest
in growOrder db leaves conds (have ++ [next]) (filter (/= next) rest)
where
-- | 候选里成本最低的那个,同价取序号小的
pick (i : is) = foldl cheaper i is
pick [] = 0
cheaper i j = if costOf j < costOf i then j else i
-- | 这个前缀的估算成本
costOf i = chainCost db [leaves !! k | k <- have ++ [i]] conds
-- | 每个起点贪心一遍,取最便宜的顺序
bestOrder :: Database -> [RelOp] -> [Expr] -> [RelOp]
bestOrder db leaves conds = pick (map start [0 .. n - 1])
where
n = length leaves
-- | 从第 i 张表开始的贪心顺序
start i = [leaves !! k | k <- growOrder db leaves conds [i] (filter (/= i) [0 .. n - 1])]
-- | 成本最低的那个
pick (o : os) = foldl cheaper o os
pick [] = []
cheaper o o' = if cost o' < cost o then o' else o
-- | 一个顺序的估算成本
cost o = chainCost db o conds
-- | 统计齐全、没有除法且确实更省时给出新顺序
cheaperOrder :: Database -> [RelOp] -> [Expr] -> Maybe [RelOp]
cheaperOrder db leaves conds
| any hasDivision conds = Nothing
| any unknown leaves = Nothing
| bestCost < textCost = Just best
| otherwise = Nothing
where
-- | 这个叶子是不是拿不到统计
unknown x = case leafRows db x of
Just _ -> False
Nothing -> True
best = bestOrder db leaves conds
bestCost = chainCost db best conds
textCost = chainCost db leaves conds
-- | 按新顺序重建左深链,条件挂到两侧都齐的那一步
rebuildChain :: Database -> [RelOp] -> [Expr] -> RelOp
rebuildChain db (l : ls) conds = go (relOpCols db l) l ls conds
where
-- | 逐个接上后面的叶子
go _ acc [] rest = applyConds rest acc
go have acc (x : xs) rest =
let have' = have ++ relOpCols db x
(now, later) = partition (\c -> colsInExpr c `subsetOf` have') rest
in go have' (Join InnerJoin acc x (conjOf now)) xs later
-- | 若干条件用 AND 串起来
conjOf [] = LitBool True
conjOf (c : cs) = foldl And c cs
rebuildChain _ [] _ = Unit
-- | 挂不上去的条件套成 Filter
applyConds :: [Expr] -> RelOp -> RelOp
applyConds [] op = op
applyConds (c : cs) op = Filter c (applyConds cs op)
-- | 条件的列是不是都在可用列里
subsetOf :: [String] -> [String] -> Bool
subsetOf need have = all (`elem` have) need
@@ -1,9 +1,9 @@
module ChuSQL.Core.Engine.Algebra.Op (RelOp (..), renderPlan) where
module ChuSQL.Core.Engine.Algebra.Op (RelOp (..), relOpCols, renderPlan) where
import ChuSQL.Core.Model (Value)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST (Expr, JoinKind (..), SortDir)
-- 关系代数算子定义与算子树文本渲染。
-- 关系代数算子定义、输出列推导与算子树文本渲染。
data RelOp
= Scan (Maybe String) String (Maybe [String])
@@ -19,6 +19,27 @@ data RelOp
| Join JoinKind RelOp RelOp Expr
deriving (Show, Eq)
-- | 算子会产出哪些列
relOpCols :: Database -> RelOp -> [String]
relOpCols _ Unit = []
relOpCols _ (Compute items _) = map fst items
relOpCols _ (Aggregate keys aggs _) = keys ++ map fst aggs
relOpCols _ (Scan mAlias _ (Just cols)) = map (qualify mAlias) cols
relOpCols db (Scan mAlias tbl Nothing) =
case lookup tbl db of
Nothing -> []
Just t -> map (prefix ++) (colNames t)
where
prefix = maybe "" (++ ".") mAlias
relOpCols db (Lookup mAlias tbl _ _) = relOpCols db (Scan mAlias tbl Nothing)
relOpCols db (Range mAlias tbl _ _ _) = relOpCols db (Scan mAlias tbl Nothing)
relOpCols db (Filter _ x) = relOpCols db x
relOpCols _ (Project ["*"] _) = ["*"]
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
-- | 连接的显示名
kindLabel :: JoinKind -> String
kindLabel InnerJoin = "Join"
@@ -1,33 +1,14 @@
module ChuSQL.Core.Engine.Algebra.Optimize (optimize, pushProject, relOpCols) where
module ChuSQL.Core.Engine.Algebra.Optimize (optimize, pushProject) where
import ChuSQL.Core.Engine.Algebra.Expr (colsInExpr, evalExpr)
import ChuSQL.Core.Engine.Algebra.Op (RelOp (..))
import ChuSQL.Core.Engine.Algebra.Cost (indexCheaper, pointRows, rangeRows, tableMetaOf)
import ChuSQL.Core.Engine.Algebra.Expr (colsInExpr, evalExpr, hasDivision)
import ChuSQL.Core.Engine.Algebra.JoinOrder (reorderJoins)
import ChuSQL.Core.Engine.Algebra.Op (RelOp (..), relOpCols)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), JoinKind (..))
import Data.List (nub)
-- 查询优化:谓词下推、投影下推与常量折叠。
-- | 算子会产出哪些列
relOpCols :: Database -> RelOp -> [String]
relOpCols _ Unit = []
relOpCols _ (Compute items _) = map fst items
relOpCols _ (Aggregate keys aggs _) = keys ++ map fst aggs
relOpCols _ (Scan mAlias _ (Just cols)) = map (qualify mAlias) cols
relOpCols db (Scan mAlias tbl Nothing) =
case lookup tbl db of
Nothing -> []
Just t -> map (prefix ++) (colNames t)
where
prefix = maybe "" (++ ".") mAlias
relOpCols db (Lookup mAlias tbl _ _) = relOpCols db (Scan mAlias tbl Nothing)
relOpCols db (Range mAlias tbl _ _ _) = relOpCols db (Scan mAlias tbl Nothing)
relOpCols db (Filter _ x) = relOpCols db x
relOpCols _ (Project ["*"] _) = ["*"]
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
-- 查询优化:谓词下推、投影下推、常量折叠、扫描方式与连接顺序选择。
-- | need 是否都在 have 里
isSubsetOf :: [String] -> [String] -> Bool
@@ -90,24 +71,6 @@ pushOne _ (Filter p (Sort spec x))
| not (hasDivision p) = Sort spec (Filter p x)
pushOne _ op = op
-- | 除法可能报错,不能提前或延后求值
hasDivision :: Expr -> Bool
hasDivision e = case e of
Div _ _ -> True
Neg a -> hasDivision a
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
Gt a b -> both a b
Lt a b -> both a b
Eq a b -> both a b
And a b -> both a b
Or a b -> both a b
_ -> False
where
-- | 两个子表达式里任意一个有除法
both a b = hasDivision a || hasDivision b
-- | 检查子计划中可能报错的除法
planHasDivision :: RelOp -> Bool
planHasDivision (Filter p x) = hasDivision p || planHasDivision x
@@ -183,24 +146,38 @@ foldConstants (And a b) = foldNode (And (foldConstants a) (foldConstants b))
foldConstants (Or a b) = foldNode (Or (foldConstants a) (foldConstants b))
foldConstants e = e
-- | 点查值不值得改写;没有统计就按老规矩改
worthLookup :: Database -> String -> String -> Bool
worthLookup db t col = case tableMetaOf db t of
Nothing -> True
Just m -> indexCheaper m col (pointRows m col)
-- | 范围扫描值不值得改写;没有统计就按老规矩改
worthRange :: Database -> String -> String -> Bool
worthRange db t col = case tableMetaOf db t of
Nothing -> True
Just m -> indexCheaper m col (rangeRows m)
-- | 单节点重写
rewriteNode :: Database -> RelOp -> RelOp
rewriteNode _ (Filter (Eq (Col k) (LitInt v)) (Scan mAlias t Nothing)) =
Lookup mAlias t (unqualify mAlias k) (VInt v)
rewriteNode _ (Filter (Eq (Col k) (LitStr v)) (Scan mAlias t Nothing)) =
Lookup mAlias t (unqualify mAlias k) (VStr v)
rewriteNode _ (Filter (And (Gt (Col k1) (LitInt lo)) (Lt (Col k2) (LitInt hi))) (Scan mAlias t Nothing))
| k1 == k2 = Range mAlias t (unqualify mAlias k1) (Just (VInt lo, False)) (Just (VInt hi, False))
rewriteNode _ (Filter (And (Gt (Col k1) (LitStr lo)) (Lt (Col k2) (LitStr hi))) (Scan mAlias t Nothing))
| k1 == k2 = Range mAlias t (unqualify mAlias k1) (Just (VStr lo, False)) (Just (VStr hi, False))
rewriteNode _ (Filter (Gt (Col k) (LitInt v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) (Just (VInt v, False)) Nothing
rewriteNode _ (Filter (Gt (Col k) (LitStr v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) (Just (VStr v, False)) Nothing
rewriteNode _ (Filter (Lt (Col k) (LitInt v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, False))
rewriteNode _ (Filter (Lt (Col k) (LitStr v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, False))
rewriteNode db (Filter (Eq (Col k) (LitInt v)) (Scan mAlias t Nothing))
| worthLookup db t (unqualify mAlias k) = Lookup mAlias t (unqualify mAlias k) (VInt v)
rewriteNode db (Filter (Eq (Col k) (LitStr v)) (Scan mAlias t Nothing))
| worthLookup db t (unqualify mAlias k) = Lookup mAlias t (unqualify mAlias k) (VStr v)
rewriteNode db (Filter (And (Gt (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, False)) (Just (VInt hi, False))
rewriteNode db (Filter (And (Gt (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, False)) (Just (VStr hi, False))
rewriteNode db (Filter (Gt (Col k) (LitInt v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VInt v, False)) Nothing
rewriteNode db (Filter (Gt (Col k) (LitStr v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VStr v, False)) Nothing
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 p (Join kind l r c))
| not (hasDivision p || planHasDivision (Join kind l r c)) =
case kind of
@@ -235,4 +212,4 @@ optimizePredicates db op =
-- | 优化总入口
optimize :: Database -> RelOp -> RelOp
optimize db op = pushProject db ["*"] (optimizePredicates db op)
optimize db op = pushProject db ["*"] (reorderJoins db (optimizePredicates db op))
@@ -12,19 +12,6 @@ flipOrdering LT = GT
flipOrdering GT = LT
flipOrdering EQ = EQ
-- | 比较两个值:NULL 最小,数值跨整数与小数比较
compareValue :: Value -> Value -> Ordering
compareValue VNull VNull = EQ
compareValue VNull _ = LT
compareValue _ VNull = GT
compareValue (VInt a) (VInt b) = compare a b
compareValue (VFloat a) (VFloat b) = compare a b
compareValue (VInt a) (VFloat b) = compare (fromIntegral a) b
compareValue (VFloat a) (VInt b) = compare a (fromIntegral b)
compareValue (VStr a) (VStr b) = compare a b
compareValue (VBool a) (VBool b) = compare a b
compareValue _ _ = EQ
-- | 取出排序键(按 spec 顺序,每行只查一次)
sortKey :: [(String, SortDir)] -> Row -> [Maybe Value]
sortKey spec row = [lookup col row | (col, _) <- spec]
@@ -0,0 +1,282 @@
module ChuSQL.Core.Engine.Builtin
( Builtin (..)
, builtinNames
, resolveBuiltin
, builtinName
, invokeBuiltin
, builtinOfExpr
, builtinNode
, aggregateArg
, unsupportedAggregate
, Operator (..)
, operatorOfExpr
, operatorSymbol
, executeOperator
, threeValuedNot
, inValues
) where
import ChuSQL.Core.Model (Value (..), compareValue)
import ChuSQL.Core.Engine.Syntax.AST (Expr (..))
import Control.Monad (foldM)
import Data.Char (toLower)
-- 内置函数与运算符的唯一权威:名称解析和求值都只在这里实现一次。
-- | 内置聚合函数
data Builtin = BCountAll | BCount | BSum | BAvg | BMin | BMax
deriving (Show, Eq, Enum, Bounded)
-- | 名字到内置函数的对照表,解析只认这张表
builtinNames :: [(String, Builtin)]
builtinNames =
[ ("count", BCount)
, ("sum", BSum)
, ("avg", BAvg)
, ("min", BMin)
, ("max", BMax)
]
-- | 按名字解析内置函数,名字不分大小写
resolveBuiltin :: String -> Maybe Builtin
resolveBuiltin name = lookup (map toLower name) builtinNames
-- | 内置函数在 SQL 里的名字
builtinName :: Builtin -> String
builtinName BCountAll = "count"
builtinName b = case [n | (n, x) <- builtinNames, x == b] of
(n : _) -> n
[] -> "count"
-- | 在一组上调用内置函数:给出行数与各行的参数值
invokeBuiltin :: Builtin -> Int -> [Value] -> Either String Value
invokeBuiltin BCountAll rows _ = Right (VInt rows)
invokeBuiltin BCount _ vs = Right (VInt (length (nonNull vs)))
invokeBuiltin BSum _ vs = case nonNull vs of
[] -> Right VNull
xs -> foldM addValue (VInt 0) xs
invokeBuiltin BAvg _ vs = case nonNull vs of
[] -> Right VNull
xs -> do
total <- foldM addValue (VInt 0) xs
sumValue <- numberOf total
Right (VFloat (sumValue / fromIntegral (length xs)))
invokeBuiltin BMin _ vs = extremeOf lower (nonNull vs)
invokeBuiltin BMax _ vs = extremeOf higher (nonNull vs)
-- | 去掉 NULL
nonNull :: [Value] -> [Value]
nonNull = filter (/= VNull)
-- | 聚合节点对应的内置函数;其余节点给 Nothing
builtinOfExpr :: Expr -> Maybe Builtin
builtinOfExpr CountAll = Just BCountAll
builtinOfExpr (CountOf _) = Just BCount
builtinOfExpr (SumOf _) = Just BSum
builtinOfExpr (AvgOf _) = Just BAvg
builtinOfExpr (MinOf _) = Just BMin
builtinOfExpr (MaxOf _) = Just BMax
builtinOfExpr _ = Nothing
-- | 内置函数对应的 AST 节点
builtinNode :: Builtin -> Expr -> Expr
builtinNode BCountAll _ = CountAll
builtinNode BCount e = CountOf e
builtinNode BSum e = SumOf e
builtinNode BAvg e = AvgOf e
builtinNode BMin e = MinOf e
builtinNode BMax e = MaxOf e
-- | 聚合调用的参数表达式,COUNT(*) 没有参数
aggregateArg :: Expr -> Maybe Expr
aggregateArg (CountOf x) = Just x
aggregateArg (SumOf x) = Just x
aggregateArg (AvgOf x) = Just x
aggregateArg (MinOf x) = Just x
aggregateArg (MaxOf x) = Just x
aggregateArg _ = Nothing
-- | 只支持这几个聚合函数
unsupportedAggregate :: String
unsupportedAggregate = "only COUNT, SUM, AVG, MIN and MAX can be aggregated"
-- | 数值相加:整数配整数还是整数
addValue :: Value -> Value -> Either String Value
addValue (VInt a) (VInt b) = Right (VInt (a + b))
addValue (VInt a) (VFloat b) = Right (VFloat (fromIntegral a + b))
addValue (VFloat a) (VInt b) = Right (VFloat (a + fromIntegral b))
addValue (VFloat a) (VFloat b) = Right (VFloat (a + b))
addValue _ _ = Left "type error: expected two numbers"
-- | 能当数用的值
numberOf :: Value -> Either String Double
numberOf (VInt n) = Right (fromIntegral n)
numberOf (VFloat d) = Right d
numberOf _ = Left "type error: expected a number"
-- | 一组值里的极值,空组给 NULL
extremeOf :: (Value -> Value -> Value) -> [Value] -> Either String Value
extremeOf _ [] = Right VNull
extremeOf pick (x : xs) = Right (foldl pick x xs)
-- | 取更小的那个
lower :: Value -> Value -> Value
lower a b = if compareValue b a == LT then b else a
-- | 取更大的那个
higher :: Value -> Value -> Value
higher a b = if compareValue b a == GT then b else a
-- | 运算符:AST 里的每个运算节点对应一个
data Operator
= OpAdd
| OpSub
| OpMul
| OpDiv
| OpNeg
| OpGt
| OpLt
| OpEq
| OpAnd
| OpOr
deriving (Show, Eq, Enum, Bounded)
-- | 运算符的 SQL 写法
operatorSymbol :: Operator -> String
operatorSymbol OpAdd = "+"
operatorSymbol OpSub = "-"
operatorSymbol OpMul = "*"
operatorSymbol OpDiv = "/"
operatorSymbol OpNeg = "-"
operatorSymbol OpGt = ">"
operatorSymbol OpLt = "<"
operatorSymbol OpEq = "="
operatorSymbol OpAnd = "AND"
operatorSymbol OpOr = "OR"
-- | 运算节点对应的运算符;其余节点给 Nothing
operatorOfExpr :: Expr -> Maybe Operator
operatorOfExpr (Add _ _) = Just OpAdd
operatorOfExpr (Sub _ _) = Just OpSub
operatorOfExpr (Mul _ _) = Just OpMul
operatorOfExpr (Div _ _) = Just OpDiv
operatorOfExpr (Neg _) = Just OpNeg
operatorOfExpr (Gt _ _) = Just OpGt
operatorOfExpr (Lt _ _) = Just OpLt
operatorOfExpr (Eq _ _) = Just OpEq
operatorOfExpr (And _ _) = Just OpAnd
operatorOfExpr (Or _ _) = Just OpOr
operatorOfExpr _ = Nothing
-- | 运算符求值:算错个数是内部错误
executeOperator :: Operator -> [Value] -> Either String Value
executeOperator op vs = case (op, vs) of
(OpNeg, [x]) -> evalNeg x
(OpGt, [x, y]) -> compareValues (== GT) x y
(OpLt, [x, y]) -> compareValues (< EQ) x y
(OpEq, [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
_ -> Left ("internal error: wrong operand count for operator " ++ operatorSymbol op)
-- | 取负
evalNeg :: Value -> Either String Value
evalNeg VNull = Right VNull
evalNeg (VInt n) = Right (VInt (negate n))
evalNeg (VFloat d) = Right (VFloat (negate d))
evalNeg _ = Left "type error: expected a number"
-- | 整数算术与浮点算术成对出现
data Arith = Arith
{ arithInt :: Int -> Int -> Either String Int
, arithFloat :: Double -> Double -> Either String Double
}
-- | 二元算术运算符对应的实现
arithOf :: Operator -> Maybe Arith
arithOf OpAdd = Just (Arith (\x y -> Right (x + y)) (\x y -> Right (x + y)))
arithOf OpSub = Just (Arith (\x y -> Right (x - y)) (\x y -> Right (x - y)))
arithOf OpMul = Just (Arith (\x y -> Right (x * y)) (\x y -> Right (x * y)))
arithOf OpDiv = Just divArith
arithOf _ = Nothing
-- | 一边是 NULL 就出 NULL,两边都是整数就出整数
evalArith :: Arith -> Value -> Value -> Either String Value
evalArith _ VNull _ = Right VNull
evalArith _ _ VNull = Right VNull
evalArith ops (VInt x) (VInt y) = VInt <$> arithInt ops x y
evalArith ops x y = case (asDouble x, asDouble y) of
(Just p, Just q) -> VFloat <$> arithFloat ops p q
_ -> Left "type error: expected two numbers"
-- | 整数除法向零截断,溢出返回错误;浮点除法不能除以零
divArith :: Arith
divArith = Arith intDiv floatDiv
where
-- | 整数除法:除零与溢出都报错
intDiv _ 0 = Left "division by zero"
intDiv x y
| x == minBound && y == -1 = Left "integer division overflow"
| otherwise = Right (x `quot` y)
-- | 浮点除法:不能除以零
floatDiv _ 0 = Left "division by zero"
floatDiv x y = Right (x / y)
-- | 能当数用的值
asDouble :: Value -> Maybe Double
asDouble (VInt n) = Just (fromIntegral n)
asDouble (VFloat d) = Just d
asDouble _ = Nothing
-- | 比较两个值:NULL 给 NULL,其余按全序比
compareValues :: (Ordering -> Bool) -> Value -> Value -> Either String Value
compareValues rel x y
| x == VNull || y == VNull = Right VNull
| comparableValue x y = Right (VBool (rel (compareValue x y)))
| otherwise = Left "type error: cannot compare these values"
-- | 两个值是否落在可以用全序比较的同一族里
comparableValue :: Value -> Value -> Bool
comparableValue (VInt _) (VFloat _) = True
comparableValue (VFloat _) (VInt _) = True
comparableValue (VInt _) (VInt _) = True
comparableValue (VFloat _) (VFloat _) = True
comparableValue (VStr _) (VStr _) = True
comparableValue (VBool _) (VBool _) = True
comparableValue _ _ = False
-- | 逻辑与的三值真值表
andValues :: Value -> Value -> Either String Value
andValues (VBool False) _ = Right (VBool False)
andValues _ (VBool False) = Right (VBool False)
andValues (VBool True) (VBool True) = Right (VBool True)
andValues VNull (VBool True) = Right VNull
andValues (VBool True) VNull = Right VNull
andValues VNull VNull = Right VNull
andValues _ _ = Left "type error: expected two booleans"
-- | 逻辑或的三值真值表
orValues :: Value -> Value -> Either String Value
orValues (VBool True) _ = Right (VBool True)
orValues _ (VBool True) = Right (VBool True)
orValues (VBool False) (VBool False) = Right (VBool False)
orValues VNull (VBool False) = Right VNull
orValues (VBool False) VNull = Right VNull
orValues VNull VNull = Right VNull
orValues _ _ = Left "type error: expected two booleans"
-- | NOT 的三值取反
threeValuedNot :: Value -> Value
threeValuedNot (VBool b) = VBool (not b)
threeValuedNot _ = VNull
-- | IN 三值逻辑:命中为真,碰 NULL 得 NULL
inValues :: Value -> [Value] -> Either String Value
inValues VNull _ = Right VNull
inValues v vs = do
results <- mapM (\x -> executeOperator OpEq [v, x]) vs
Right $
if VBool True `elem` results
then VBool True
else if VNull `elem` results then VNull else VBool False
@@ -0,0 +1,74 @@
module ChuSQL.Core.Engine.Error
( ErrorCategory (..)
, ErrorRule (..)
, errorRules
, errorCategory
, errorCode
, errorLike
, accountErrorCode
) where
import Data.List (isInfixOf)
-- 引擎与存储错误文本的唯一分类表和协议错误码表
-- | 错误大类
data ErrorCategory
= NoDatabaseError
| PermissionError
| CatalogError
| TypeError
| StorageError
| ProtocolError
| UnknownError
deriving (Eq, Show)
-- | 一条识别规则:命中文本片段就算这一类,并给出协议错误码
data ErrorRule = ErrorRule
{ ruleMarker :: String
, ruleCategory :: ErrorCategory
, ruleCode :: String
}
-- | 唯一的错误识别表,顺序即优先级
errorRules :: [ErrorRule]
errorRules =
[ ErrorRule "no database selected" NoDatabaseError "no_database"
, ErrorRule "administrator required" PermissionError "forbidden"
, ErrorRule "unknown table" CatalogError "not_found"
, ErrorRule "unknown column" CatalogError "query_error"
, ErrorRule "unknown database" CatalogError "not_found"
, ErrorRule "unknown role" CatalogError "not_found"
, ErrorRule "unknown account" CatalogError "not_found"
, ErrorRule "unknown index" CatalogError "query_error"
, ErrorRule "unknown user" CatalogError "query_error"
, ErrorRule "permission denied" PermissionError "query_error"
, ErrorRule "already exists" CatalogError "query_error"
, ErrorRule "type error" TypeError "query_error"
, ErrorRule "cannot put this value into" TypeError "query_error"
, ErrorRule "division by zero" TypeError "query_error"
, ErrorRule "overflow" TypeError "query_error"
, ErrorRule "storage" StorageError "query_error"
, ErrorRule "storage library call failed" StorageError "query_error"
, ErrorRule "unsupported protocol" ProtocolError "query_error"
]
-- | 命中的第一条规则
errorLike :: String -> Maybe ErrorRule
errorLike message = case [r | r <- errorRules, ruleMarker r `isInfixOf` message] of
[] -> Nothing
(r : _) -> Just r
-- | 错误文本属于哪一大类
errorCategory :: String -> ErrorCategory
errorCategory message = maybe UnknownError ruleCategory (errorLike message)
-- | 错误文本对应的协议错误码
errorCode :: String -> String
errorCode message = maybe "query_error" ruleCode (errorLike message)
-- | 账号服务对外的错误码
accountErrorCode :: String -> String
accountErrorCode "account already exists" = "conflict"
accountErrorCode "unknown account" = "not_found"
accountErrorCode _ = "storage_error"
@@ -0,0 +1,82 @@
module ChuSQL.Core.Engine.Parallel (
parallelShardLimit,
Pool,
workerPool,
poolRun,
) where
import Control.Concurrent (forkIO)
import Control.Concurrent.MVar (MVar, modifyMVar, modifyMVar_, newEmptyMVar, newMVar, putMVar, takeMVar)
import Control.Exception (SomeException, throwIO, try)
import System.IO.Unsafe (unsafePerformIO)
-- 并行执行:常驻工作线程池,结果按提交顺序收回。
-- | 单次并行扫描最多切几片
parallelShardLimit :: Int
parallelShardLimit = 8
-- | 池:待办队列与唤醒信号
data Pool = Pool
{ poolJobs :: MVar [IO ()]
, poolReady :: MVar ()
}
{-# NOINLINE poolRef #-}
-- | 进程全局的池,按容量复用
poolRef :: MVar (Maybe (Int, Pool))
poolRef = unsafePerformIO (newMVar Nothing)
-- | 按容量取池,不够就新建一个
workerPool :: Int -> IO Pool
workerPool size = modifyMVar poolRef reuse
where
-- | 现有池够大就留着,否则换新的
reuse current = case current of
Just (capacity, pool)
| capacity >= wanted -> pure (current, pool)
_ -> do
pool <- startPool wanted
pure (Just (wanted, pool), pool)
-- | 池的线程数至少一个
wanted = max 1 size
-- | 起 wanted 个常驻线程吃队列
startPool n = do
jobs <- newMVar []
ready <- newMVar ()
sequence_ [forkIO (worker jobs ready) | _ <- [1 .. n]]
pure (Pool jobs ready)
-- | 一个工作线程:被唤醒一次就取一个待办
worker :: MVar [IO ()] -> MVar () -> IO ()
worker jobs ready = do
takeMVar ready
job <- modifyMVar jobs pick
case job of
Nothing -> worker jobs ready
Just act -> act >> worker jobs ready
where
-- | 取队首,空队列就回 Nothing
pick [] = pure ([], Nothing)
pick (j : js) = pure (js, Just j)
-- | 往池里丢一个待办
submit :: Pool -> IO () -> IO ()
submit pool job = do
modifyMVar_ (poolJobs pool) (pure . (++ [job]))
putMVar (poolReady pool) ()
-- | 跑一个动作,结果或异常都填进格子
fillCell :: MVar (Either SomeException a) -> IO a -> IO ()
fillCell cell act = try act >>= putMVar cell
-- | 并发跑一批动作,按输入顺序返回结果与异常
poolRun :: Pool -> [IO a] -> IO [a]
poolRun _ [] = pure []
poolRun pool actions = do
cells <- mapM (const newEmptyMVar) actions
sequence_ [submit pool (fillCell cell act) | (cell, act) <- zip cells actions]
outcomes <- mapM takeMVar cells
mapM (either throwIO pure) outcomes
@@ -243,16 +243,11 @@ comparison db place env a b = do
then Right (InferType CBool)
else Left (place ++ ": both sides of = must have the same type, got " ++ renderType ta ++ " and " ++ renderType tb)
-- | 两类能不能比
-- | 两类能不能比,类型族规则来自 Model
compatible :: InferredType -> InferredType -> Bool
compatible InferNull _ = True
compatible _ InferNull = True
compatible (InferType a) (InferType b)
| numericType a && numericType b = True
| typeClassOf a == typeClassOf b = True
| typeClassOf a == TemporalClass && typeClassOf b == TextClass = True
| typeClassOf b == TemporalClass && typeClassOf a == TextClass = True
| otherwise = False
compatible (InferType a) (InferType b) = comparableTypes a b
-- | 逻辑运算两边的类型检查
boolean :: Database -> String -> Scope -> Expr -> Expr -> Either String InferredType
@@ -35,6 +35,14 @@ class (Monad m) => MonadStorage m where
result <- scan t
pure (fmap (map (\row -> [(c, v) | c <- cols, Just v <- [lookup c row]])) result)
-- \| 本存储能用的并行分片数;不支持并行的存储返回 1
parallelShards :: m Int
parallelShards = pure 1
-- \| 按分片并行扫描并按分片序拼接;默认退回顺序扫描
scanShards :: Int -> String -> Maybe [String] -> m (Either String [Row])
scanShards _ t cols = maybe (scan t) (scanColumns t) cols
-- \| 追加一行
insert :: String -> Row -> m (Either String ())
@@ -28,8 +28,10 @@ module ChuSQL.Core.Engine.Storage.IPC (
import ChuSQL.Core.Model
import ChuSQL.Core.Protocol (Account (..), Request (..), Response (..), SchemaColumn (..), TableInfo (..))
import ChuSQL.Core.Engine.Parallel (parallelShardLimit, poolRun, workerPool)
import ChuSQL.Core.Engine.Storage
import ChuSQL.Core.Engine.Storage.FFI (closeStorage, openStorage, storageRequest)
import Control.Concurrent (getNumCapabilities)
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, readMVar)
import Data.Aeson (eitherDecodeStrict, encode)
import Data.ByteString qualified as BS
@@ -166,6 +168,18 @@ doScan t = ask "scan" (ReqScan t) $ \resp -> case resp of
RespRows rows -> Just rows
_ -> Nothing
-- | 发带投影的 Scan
doScanColumns :: String -> [String] -> IO (Either String [Row])
doScanColumns t cols = ask "scan" (ReqScanColumns t cols) $ \resp -> case resp of
RespRows rows -> Just [[(c, v) | c <- cols, Just v <- [lookup c row]] | row <- rows]
_ -> Nothing
-- | 发分片 Scan:只取第 shard 片
doScanShard :: String -> Maybe [String] -> Int -> Int -> IO (Either String [Row])
doScanShard t cols shard shards = ask "scan_shard" (ReqScanShard t cols shard shards) $ \resp -> case resp of
RespRows rows -> Just rows
_ -> Nothing
-- | 发 Insert(索引由存储层维护)
doInsert :: String -> Row -> IO (Either String ())
doInsert t r = ask "insert" (ReqInsert t r) okOnly
@@ -290,9 +304,20 @@ instance MonadStorage IPCStorage where
-- \| 发 Scan
scan t = IPCStorage (doScan t)
-- \| 只取指定列
scanColumns t cols = IPCStorage $ ask "scan" (ReqScanColumns t cols) $ \resp -> case resp of
RespRows rows -> Just [[(c, v) | c <- cols, Just v <- [lookup c row]] | row <- rows]
_ -> Nothing
scanColumns t cols = IPCStorage (doScanColumns t cols)
-- \| 并行分片数:按 RTS 能力数,封顶在 parallelShardLimit
parallelShards = IPCStorage $ do
caps <- getNumCapabilities
pure (max 1 (min parallelShardLimit caps))
-- \| 分片并行扫,再按分片序拼接
scanShards width t cols
| width <= 1 = IPCStorage (maybe (doScan t) (doScanColumns t) cols)
| otherwise = IPCStorage $ do
pool <- workerPool width
parts <- poolRun pool [doScanShard t cols shard width | shard <- [0 .. width - 1]]
pure (fmap concat (sequence parts))
-- \| 发 Insert(索引由存储层自己维护)
insert t r = IPCStorage (doInsert t r)
@@ -341,7 +366,7 @@ instance MonadStorage IPCStorage where
rows <- doScan (tiTable info)
pure
( tiTable info
, Table (tiTable info) (schemaToColumns (tiColumns info)) (either (const []) id rows)
, Table (tiTable info) (schemaToColumns (tiColumns info)) (either (const []) id rows) (Just (metaOf info))
)
-- \| 只问数据字典要结构,设了当前库只留它的表
@@ -354,6 +379,10 @@ instance MonadStorage IPCStorage where
Left _ -> []
Right xs -> concatMap (visibleTables current) xs
-- | 线上表信息转引擎侧统计
metaOf :: TableInfo -> TableMeta
metaOf info = TableMeta (tiRows info) (tiStats info) (tiIndexes info)
-- | 没设库名就照单全收;设了库名只收这个库的表,键名去掉库名前缀
visibleTables :: Maybe String -> TableInfo -> [(String, Table)]
visibleTables current info
@@ -365,4 +394,4 @@ visibleTables current info
where
named = info {tiTable = unqualify current (tiTable info)}
-- | 组装成不带行的表
bare i = (tiTable i, Table (tiTable i) (schemaToColumns (tiColumns i)) [])
bare i = (tiTable i, Table (tiTable i) (schemaToColumns (tiColumns i)) [] (Just (metaOf i)))
@@ -44,7 +44,7 @@ instance MonadStorage MemoryStorage where
createTable name cols = MemoryStorage $ \db ->
if any ((== name) . fst) db
then Right (Left ("table already exists: " ++ name), db)
else Right (Right (), db ++ [(name, Table name cols [])])
else Right (Right (), db ++ [(name, Table name cols [] Nothing)])
-- \| 删表;不存在报错
dropTable name = MemoryStorage $ \db ->
@@ -3,6 +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.Syntax.AST
import Control.Monad (void)
import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
@@ -172,18 +173,14 @@ inPredicate = do
subquerySelect :: Parser Subquery
subquerySelect = Subquery <$> selectStatement <*> pure []
-- | 聚合调用:COUNT/SUM/AVG/MIN/MAX
-- | 聚合调用:名称与节点都由内置函数表给出
aggregateCall :: Parser Expr
aggregateCall =
choice
[ CountAll <$ try (keyword "count" *> between (symbol "(") (symbol ")") (symbol "*"))
, CountOf <$> aggArg "count"
, SumOf <$> aggArg "sum"
, AvgOf <$> aggArg "avg"
, MinOf <$> aggArg "min"
, MaxOf <$> aggArg "max"
]
aggregateCall = choice (countAll : map named builtinNames)
where
-- | COUNT(*),没有参数
countAll = CountAll <$ try (keyword (builtinName BCountAll) *> between (symbol "(") (symbol ")") (symbol "*"))
-- | 表里每一个带参数的内置函数
named (name, builtin) = builtinNode builtin <$> aggArg name
-- | 读聚合函数的参数
aggArg name = try (keyword name *> between (symbol "(") (symbol ")") expr)
+419 -3
View File
@@ -2,24 +2,32 @@
module Main where
import ChuSQL.Core.Engine.Algebra.Eval (evalRelOp)
import ChuSQL.Core.Engine.Algebra.Eval (evalRelOp, evalRelOpM)
import ChuSQL.Core.Engine.Algebra.Expr (colsInExpr, evalExpr)
import ChuSQL.Core.Engine.Algebra.Op (RelOp (..), renderPlan)
import ChuSQL.Core.Engine.Algebra.Optimize (optimize, pushProject)
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Engine.Algebra.Sort (sortRows)
import ChuSQL.Core.Engine.Builtin
import ChuSQL.Core.Engine.Error
import ChuSQL.Core.Engine
import ChuSQL.Core.Engine.Parallel (poolRun, workerPool)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Semantic (prepare)
import ChuSQL.Core.Engine.Storage (IndexResult (..), MonadStorage (..))
import ChuSQL.Core.Engine.Storage.IPC (IPCStorage (runIPCStorage), closeConnection, doListTables, getDatabaseName, localStorageLink, setDatabaseName, setStorageLink)
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser
import Control.Exception (IOException, finally, try)
import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, readMVar)
import Control.Exception (ErrorCall, IOException, finally, try)
import Data.Hashable (hash)
import Data.List (isInfixOf, sortOn)
import Data.Unique (hashUnique, newUnique)
import System.CPUTime (getCPUTime)
import System.Directory (createDirectoryIfMissing, getTemporaryDirectory, removePathForcibly)
import System.FilePath ((</>))
import System.Timeout (timeout)
import Test.Hspec
-- 引擎层的 hspec 测试:语法、执行、优化器与存储链路行为。
@@ -35,6 +43,7 @@ users =
, [("id", VInt 2), ("name", VStr "Bob"), ("age", VInt 17)]
, [("id", VInt 3), ("name", VStr "Carol"), ("age", VInt 30)]
]
, tableMeta = Nothing
}
-- | 测试表 orders:id/user_id/product
@@ -48,12 +57,51 @@ orders =
, [("id", VInt 2), ("user_id", VInt 2), ("product", VStr "Pen")]
, [("id", VInt 3), ("user_id", VInt 1), ("product", VStr "Cup")]
]
, tableMeta = Nothing
}
-- | 两个测试表组成的默认库
testDB :: Database
testDB = [("users", users), ("orders", orders)]
-- | 连接链测试表 a:6 行,id 各不相同
chainA :: Table
chainA =
Table
{ tableName = "a"
, tableCols = [("id", TInt), ("name", TStr)]
, tableRows = [[("id", VInt n), ("name", VStr ("a" ++ show n))] | n <- [1 .. 6]]
, tableMeta = Just (TableMeta 6 [("id", 6, False)] ["id"])
}
-- | 连接链测试表 b:4 行,a_id 各不相同
chainB :: Table
chainB =
Table
{ tableName = "b"
, tableCols = [("id", TInt), ("a_id", TInt)]
, tableRows = [[("id", VInt n), ("a_id", VInt n)] | n <- [1 .. 4]]
, tableMeta = Just (TableMeta 4 [("id", 4, False), ("a_id", 4, False)] ["id"])
}
-- | 连接链测试表 c:2 行,b_id 各不相同
chainC :: Table
chainC =
Table
{ tableName = "c"
, tableCols = [("id", TInt), ("b_id", TInt)]
, tableRows = [[("id", VInt n), ("b_id", VInt n)] | n <- [1, 2]]
, tableMeta = Just (TableMeta 2 [("id", 2, False), ("b_id", 2, False)] ["id"])
}
-- | 三张表组成的连接链测试库
chainDB :: Database
chainDB = [("a", chainA), ("b", chainB), ("c", chainC)]
-- | 同一个连接链测试库,但没有统计
chainDBWithoutStats :: Database
chainDBWithoutStats = [(n, t { tableMeta = Nothing }) | (n, t) <- chainDB]
-- | 跑引擎层全部 hspec 用例
main :: IO ()
main = hspec $ do
@@ -1161,6 +1209,47 @@ main = hspec $ do
fmap firstFilterCond (optimizedPlan "SELECT name FROM users WHERE 1 > 2")
`shouldBe` Right (Just (LitBool False))
it "picks the index only when the statistics make it cheaper" $ 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))]
unknown = [("users", Table "users" [("id", TInt), ("name", TStr)] [] Nothing)]
point = Filter (Eq (Col "id") (LitInt 1)) (Scan Nothing "users" Nothing)
range = Filter (Gt (Col "id") (LitInt 5)) (Scan Nothing "users" Nothing)
-- 索引在、点查命中一行:改写
optimize (dbWith (stat 10000 [("id", 10000)] ["id"])) point
`shouldBe` Lookup Nothing "users" "id" (VInt 1)
-- 拿不到统计:保持原有改写行为
optimize unknown point `shouldBe` Lookup Nothing "users" "id" (VInt 1)
-- 没有索引:不改写,省掉一次白跑的存储层查询
optimize (dbWith (stat 10000 [("id", 10000)] [])) point `shouldBe` point
-- 有索引但整列一个值:点查等于全表,全表扫更便宜
optimize (dbWith (stat 10000 [("name", 1)] ["name"])) (Filter (Eq (Col "name") (LitStr "a")) (Scan Nothing "users" Nothing))
`shouldBe` Filter (Eq (Col "name") (LitStr "a")) (Scan Nothing "users" Nothing)
-- 范围扫描同理
optimize (dbWith (stat 10000 [("id", 10000)] ["id"])) range
`shouldBe` Range Nothing "users" "id" (Just (VInt 5, False)) Nothing
optimize (dbWith (stat 10000 [("id", 10000)] [])) range `shouldBe` range
it "reorders an inner join chain only when the statistics say it is cheaper" $ do
let sql =
"SELECT a.name FROM a JOIN b ON a.id = b.a_id JOIN c ON b.id = c.b_id ORDER BY a.name"
unordered = "SELECT a.name FROM a JOIN b ON a.id = b.a_id JOIN c ON b.id = c.b_id"
-- 统计说先接最小的 c 更省:文本顺序 a / b / c 换成一个更便宜的顺序
fmap planTables (optimizedPlanIn chainDB sql) `shouldBe` Right ["b", "c", "a"]
-- 拿不到统计:保持文本顺序
fmap planTables (optimizedPlanIn chainDBWithoutStats sql) `shouldBe` Right ["a", "b", "c"]
-- 行序可观察(没有 ORDER BY):不动顺序
fmap planTables (optimizedPlanIn chainDB unordered) `shouldBe` Right ["a", "b", "c"]
it "keeps an inner join chain result identical when it reorders it" $ do
let sql =
"SELECT a.name FROM a JOIN b ON a.id = b.a_id JOIN c ON b.id = c.b_id ORDER BY a.name"
case parseStatement sql >>= prepare chainDB of
Left err -> expectationFailure err
Right q -> case translate q of
Left err -> expectationFailure err
Right relOp -> evalRelOp chainDB (optimize chainDB relOp) `shouldBe` evalRelOp chainDB relOp
it "removes a filter whose predicate folds to true" $ do
fmap anyFilter (optimizedPlan "SELECT name FROM users WHERE 1 = 1")
`shouldBe` Right False
@@ -1600,6 +1689,24 @@ main = hspec $ do
fmap sortRow (indexRow fresh)
`shouldBe` Just (sortRow [("id", VInt 3), ("code", VInt 502)])
it "schema carries the row count, index list and column statistics" $ do
withTestSession $ \srv -> withServerEnv srv $ do
_ <- runIPCStorage (runStatementM (CreateTable "meta_t" [("id", TInt), ("code", TInt)]))
_ <-
runIPCStorage
( runStatementM
(Insert "meta_t" ["id", "code"] [[LitInt 1, LitInt 500], [LitInt 2, LitInt 501]])
)
_ <- runIPCStorage (runStatementM (CreateIndex "meta_t" "code"))
db <- runIPCStorage schema
case lookup "meta_t" db of
Nothing -> expectationFailure "meta_t missing from schema"
Just t -> do
fmap metaRowCount (tableMeta t) `shouldBe` Just 2
fmap metaIndexes (tableMeta t) `shouldBe` Just ["id", "code"]
fmap (sortOn fst . map (\(c, d, _) -> (c, d)) . metaDistinct) (tableMeta t)
`shouldBe` Just [("code", 2), ("id", 2)]
it "CREATE INDEX accepts duplicates and returns every matching row" $ do
withTestSession $ \srv -> withServerEnv srv $ do
_ <- runIPCStorage (runStatementM (CreateTable "dup_ix" [("id", TInt), ("age", TInt)]))
@@ -1662,7 +1769,7 @@ main = hspec $ do
`shouldBe` Right (makeSelect ["*"] "t" (Just (Eq (Col "flag") (LitBool True))))
it "inserts and reads back a boolean column end-to-end" $ do
let db = [("t", Table "t" [("id", TInt), ("flag", TBool)] [])]
let db = [("t", Table "t" [("id", TInt), ("flag", TBool)] [] Nothing)]
case runStatement db (Insert "t" ["id", "flag"] [[LitInt 1, LitBool True]]) of
Left e -> expectationFailure e
Right (db', _) ->
@@ -1877,6 +1984,206 @@ main = hspec $ do
run nulls "ALTER TABLE nl ALTER COLUMN tag DROP NOT NULL" `shouldSatisfy` isRight
run strict "ALTER TABLE nl2 ALTER COLUMN tag DROP NOT NULL" `shouldSatisfy` isRight
describe "ChuSQL.Core.Model (值语义唯一权威)" $ do
it "treats an integral float and an int as the same value" $ do
valuesEqual (VInt 1) (VFloat 1.0) `shouldBe` True
compareValue (VInt 1) (VFloat 1.0) `shouldBe` EQ
hash (VInt 1) `shouldBe` hash (VFloat 1.0)
hashValue (VInt 1) `shouldBe` hashValue (VFloat 1.0)
it "keeps null, text and boolean in their own buckets" $ do
hash VNull `shouldNotBe` hash (VInt 0)
hash (VInt 1) `shouldNotBe` hash (VStr "1")
hash (VStr "a") `shouldNotBe` hash (VBool True)
it "orders null below every value and compares across families" $ do
compareValue VNull (VInt minBound) `shouldBe` LT
compareValue (VInt maxBound) VNull `shouldBe` GT
compareValue VNull VNull `shouldBe` EQ
compareValue (VInt 1) (VStr "a") `shouldBe` LT
compareValue (VStr "a") (VBool True) `shouldBe` LT
compareValue (VBool True) VNull `shouldBe` GT
it "propagates null through comparisons" $ do
evalExpr (Eq (Col "v") LitNull) [("v", VInt 1)] `shouldBe` Right VNull
evalExpr (Gt LitNull (LitInt 1)) [] `shouldBe` Right VNull
it "uses one comparability rule and one canonical form" $ do
comparableTypes CInt CFloat `shouldBe` True
assignable CInt CFloat `shouldBe` True
assignable CStr CInt `shouldBe` False
comparableTypes CInt CStr `shouldBe` False
comparableTypes CStr CDate `shouldBe` True
canonicalValue (VFloat 3.0) `shouldBe` VInt 3
canonicalValue (VFloat 3.5) `shouldBe` VFloat 3.5
describe "比较语义统一(排序 / 聚合 / 哈希连接)" $ do
-- vals:在给定库上跑 SQL 只取值
let vals db input = fmap (map (map snd)) (parseStatement input >>= rowsOf . runStatement db)
-- seed:建一张两列空表
seed name = case parseStatement ("CREATE TABLE " ++ name ++ " (id int, v int)") >>= runStatement emptyDB of
Left e -> error e
Right (db, _) -> db
-- ins:插入一行,失败直接判测试失败
ins db input = case parseStatement input >>= runStatement db of
Left e -> error e
Right (db', _) -> db'
rows3 =
ins
(ins (ins (seed "m") "INSERT INTO m (id, v) VALUES (1, 2)") "INSERT INTO m (id, v) VALUES (2, 1)")
"INSERT INTO m (id, v) VALUES (3, 2)"
withNull =
ins
(ins (ins (seed "m2") "INSERT INTO m2 (id, v) VALUES (1, 2)") "INSERT INTO m2 (id) VALUES (2)")
"INSERT INTO m2 (id, v) VALUES (3, 1)"
it "sorts with the same rule that aggregates use" $ do
map (map snd) (sortRows [("v", Asc)] [[("v", VFloat 1.5)], [("v", VNull)], [("v", VInt 1)]])
`shouldBe` [[VNull], [VInt 1], [VFloat 1.5]]
vals rows3 "SELECT v FROM m ORDER BY v, id" `shouldBe` Right [[VInt 1], [VInt 2], [VInt 2]]
vals rows3 "SELECT v FROM m ORDER BY v DESC, id" `shouldBe` Right [[VInt 2], [VInt 2], [VInt 1]]
vals rows3 "SELECT MIN(v), MAX(v) FROM m" `shouldBe` Right [[VInt 1, VInt 2]]
it "sorts the catalog names that are unknown into one code" $ do
errorCode "unknown database: d" `shouldBe` "not_found"
errorCode "unknown role: r" `shouldBe` "not_found"
errorCode "unknown account" `shouldBe` "not_found"
it "puts null first when sorting and skips it in aggregates" $ do
vals withNull "SELECT v FROM m2 ORDER BY v" `shouldBe` Right [[VNull], [VInt 1], [VInt 2]]
vals withNull "SELECT MIN(v), MAX(v), SUM(v), COUNT(v), COUNT(*) FROM m2"
`shouldBe` Right [[VInt 1, VInt 2, VInt 3, VInt 2, VInt 3]]
it "uses 1 and 1.0 as one key in predicates and unique columns" $ do
let one = ins (seed "f1") "INSERT INTO f1 (id, v) VALUES (1, 1)"
vals one "SELECT id FROM f1 WHERE v = 1.0" `shouldBe` Right [[VInt 1]]
vals one "SELECT id FROM f1 WHERE v = 1" `shouldBe` Right [[VInt 1]]
let uniqueTable = case parseStatement "CREATE TABLE uq (id int, v int UNIQUE)" >>= runStatement emptyDB of
Left e -> error e
Right (db, _) -> db
seeded = ins uniqueTable "INSERT INTO uq (id, v) VALUES (1, 1)"
case parseStatement "INSERT INTO uq (id, v) VALUES (2, 1.0)" >>= runStatement seeded of
Left err -> err `shouldBe` "UNIQUE: duplicate value in column v"
Right _ -> expectationFailure "1.0 must clash with 1 on a unique column"
describe "ChuSQL.Core.Engine.Builtin (解析与调用唯一入口)" $ do
it "resolves aggregate names case-insensitively" $ do
resolveBuiltin "COUNT" `shouldBe` Just BCount
resolveBuiltin "Sum" `shouldBe` Just BSum
resolveBuiltin "median" `shouldBe` Nothing
map fst builtinNames `shouldBe` ["count", "sum", "avg", "min", "max"]
it "invokes every aggregate from one place" $ do
invokeBuiltin BCountAll 3 [] `shouldBe` Right (VInt 3)
invokeBuiltin BCount 3 [VInt 1, VNull, VInt 2] `shouldBe` Right (VInt 2)
invokeBuiltin BSum 0 [] `shouldBe` Right VNull
invokeBuiltin BSum 3 [VInt 1, VNull, VInt 2] `shouldBe` Right (VInt 3)
invokeBuiltin BAvg 2 [VInt 1, VInt 3] `shouldBe` Right (VFloat 2)
invokeBuiltin BMin 2 [VNull, VInt 4] `shouldBe` Right (VInt 4)
invokeBuiltin BMax 2 [VNull, VInt 4] `shouldBe` Right (VInt 4)
it "maps names to syntax nodes in one table" $ do
builtinOfExpr CountAll `shouldBe` Just BCountAll
builtinNode BCount (Col "age") `shouldBe` CountOf (Col "age")
builtinNode BAvg (Col "age") `shouldBe` AvgOf (Col "age")
aggregateArg (SumOf (Col "age")) `shouldBe` Just (Col "age")
aggregateArg CountAll `shouldBe` Nothing
unsupportedAggregate `shouldBe` "only COUNT, SUM, AVG, MIN and MAX can be aggregated"
it "exposes one operator table and one executor" $ do
operatorSymbol <$> operatorOfExpr (Add (LitInt 1) (LitInt 2)) `shouldBe` Just "+"
operatorSymbol <$> operatorOfExpr (And (LitBool True) (LitBool False)) `shouldBe` Just "AND"
executeOperator OpAdd [VInt 1, VInt 2] `shouldBe` Right (VInt 3)
executeOperator OpDiv [VInt 1, VInt 0] `shouldBe` Left "division by zero"
executeOperator OpNeg [VFloat 2] `shouldBe` Right (VFloat (-2))
executeOperator OpEq [VInt 1, VFloat 1.0] `shouldBe` Right (VBool True)
executeOperator OpGt [VNull, VInt 1] `shouldBe` Right VNull
executeOperator OpAnd [VBool True, VNull] `shouldBe` Right VNull
executeOperator OpOr [VBool True, VNull] `shouldBe` Right (VBool True)
executeOperator OpAdd [VInt 1] `shouldSatisfy` isLeft
executeOperator OpNeg [VInt 1, VInt 2] `shouldSatisfy` isLeft
it "keeps null comparisons three-valued" $ do
inValues VNull [VInt 1] `shouldBe` Right VNull
inValues (VInt 1) [VInt 1, VNull] `shouldBe` Right (VBool True)
inValues (VInt 2) [VInt 1, VNull] `shouldBe` Right VNull
threeValuedNot VNull `shouldBe` VNull
threeValuedNot (VBool True) `shouldBe` VBool False
describe "ChuSQL.Core.Engine.Parallel (分片调度与分片选择)" $ do
it "keeps the submission order of finished jobs" $ do
pool <- workerPool 4
results <- poolRun pool [delayed 30000 (1 :: Int), delayed 1000 2, delayed 20000 3]
results `shouldBe` [1, 2, 3]
it "dispatches every job without waiting for the slow one" $ do
pool <- workerPool 4
finished <- timeout 800000 (poolRun pool (replicate 4 (delayed 300000 ())))
finished `shouldBe` Just (replicate 4 ())
it "rethrows job failures in the submission order" $ do
pool <- workerPool 4
let failed = error "first bad job" :: IO ()
later = error "second bad job" :: IO ()
outcome <- try (poolRun pool (pure () : failed : [later])) :: IO (Either ErrorCall [()])
case outcome of
Left err -> show err `shouldContain` "first bad job"
Right _ -> expectationFailure "poolRun returned instead of rethrowing the job failure"
it "scans a small table in one sequential request" $ do
probe <- newProbe users 4
let op = Scan Nothing "users" Nothing
runProbeStorage (evalRelOpM testDB op) probe `shouldReturn` evalRelOp testDB op
recordedCalls probe `shouldReturn` ["scan:users"]
it "splits a large table into the available shards" $ do
probe <- newProbe bigTable 4
let op = Scan Nothing "big" Nothing
runProbeStorage (evalRelOpM bigDB op) probe `shouldReturn` evalRelOp bigDB op
recordedCalls probe `shouldReturn` ["scan_shards:4:big"]
it "projects columns through the shard path" $ do
probe <- newProbe bigTable 3
let op = Scan Nothing "big" (Just ["name"])
runProbeStorage (evalRelOpM bigDB op) probe `shouldReturn` evalRelOp bigDB op
recordedCalls probe `shouldReturn` ["scan_shards:3:big"]
it "reads shards of one table through the wire" $ do
withTestSession $ \srv -> withServerEnv srv $ do
let rows = [[("id", VInt i), ("name", VStr ("n" ++ show i))] | i <- [1 .. 400 :: Int]]
runIPCStorage (insertMany "wide" rows) `shouldReturn` Right ()
whole <- runIPCStorage (scan "wide")
fmap length whole `shouldBe` Right 400
shardRows <- runIPCStorage (scanShards 4 "wide" Nothing)
shardRows `shouldBe` whole
projected <- runIPCStorage (scanShards 4 "wide" (Just ["name"]))
fmap (map (map fst)) projected `shouldBe` Right (map (const ["name"]) rows)
fmap (concatMap (map snd)) projected
`shouldBe` Right (map (VStr . ("n" ++) . show) [1 .. 400 :: Int])
describe "ChuSQL.Core.Engine.Error (错误分类唯一入口)" $ do
it "maps messages to wire codes without changing them" $ do
errorCode "no database selected" `shouldBe` "no_database"
errorCode "administrator required" `shouldBe` "forbidden"
errorCode "unknown table: t" `shouldBe` "not_found"
errorCode "unknown column: c" `shouldBe` "query_error"
errorCode "type error: expected two numbers" `shouldBe` "query_error"
errorCode "something else" `shouldBe` "query_error"
it "keeps the account service codes in the same module" $ do
accountErrorCode "account already exists" `shouldBe` "conflict"
accountErrorCode "unknown account" `shouldBe` "not_found"
accountErrorCode "disk gone" `shouldBe` "storage_error"
it "classifies the same messages into categories" $ do
errorCategory "no database selected" `shouldBe` NoDatabaseError
errorCategory "administrator required" `shouldBe` PermissionError
errorCategory "unknown table: t" `shouldBe` CatalogError
errorCategory "type error: expected two numbers" `shouldBe` TypeError
errorCategory "storage library call failed: x" `shouldBe` StorageError
errorCategory "unsupported protocol version" `shouldBe` ProtocolError
errorCategory "something else" `shouldBe` UnknownError
-- | 每个测试表都清空行后的库
emptyDB :: Database
emptyDB = [(n, t{tableRows = []}) | (n, t) <- testDB]
@@ -1984,6 +2291,94 @@ isLeft :: Either a b -> Bool
isLeft (Left _) = True
isLeft (Right _) = False
-- | 延时后返回一个值
delayed :: Int -> a -> IO a
delayed micros value = threadDelay micros >> pure value
-- | 造一个记录调用序列的探针
newProbe :: Table -> Int -> IO Probe
newProbe table width = do
calls <- newMVar []
pure Probe {probeTable = table, probeCalls = calls, probeWidth = width}
-- | 记一条存储调用
recordCall :: Probe -> String -> IO ()
recordCall probe name = modifyMVar_ (probeCalls probe) (\calls -> pure (calls ++ [name]))
-- | 取回调用序列
recordedCalls :: Probe -> IO [String]
recordedCalls = readMVar . probeCalls
-- | 按列投影(空清单保留行)
projectColumns :: [String] -> [Row] -> [Row]
projectColumns cols = map (\row -> [(c, v) | c <- cols, Just v <- [lookup c row]])
-- | 按行数切成连续分片
rowShards :: Int -> [a] -> [[a]]
rowShards width rows
| width <= 1 = [rows]
| otherwise = go rows
where
-- | 每片行数
chunk = (length rows + width - 1) `div` width
-- | 反复切出前一片
go rest
| null rest = []
| otherwise = let (part, more) = splitAt chunk rest in part : go more
-- | 声明 1000 行的大表
bigTable :: Table
bigTable = users {tableName = "big", tableMeta = Just (TableMeta 1000 [("id", 1000, False)] ["id"])}
-- | 只装大表的库
bigDB :: Database
bigDB = [("big", bigTable)]
-- | 记录存储调用、按行分片的假存储
data Probe = Probe
{ probeTable :: Table
, probeCalls :: MVar [String]
, probeWidth :: Int
}
-- | 探针单子
newtype ProbeStorage a = ProbeStorage {runProbeStorage :: Probe -> IO a}
instance Functor ProbeStorage where
fmap f (ProbeStorage act) = ProbeStorage (fmap f . act)
instance Applicative ProbeStorage where
pure value = ProbeStorage (\_ -> pure value)
ProbeStorage fn <*> ProbeStorage act = ProbeStorage (\probe -> fn probe <*> act probe)
instance Monad ProbeStorage where
ProbeStorage act >>= next =
ProbeStorage (\probe -> act probe >>= \value -> runProbeStorage (next value) probe)
instance MonadStorage ProbeStorage where
scan table = ProbeStorage $ \probe -> do
recordCall probe ("scan:" ++ table)
pure (Right (tableRows (probeTable probe)))
scanColumns table columns = ProbeStorage $ \probe -> do
recordCall probe ("scan_columns:" ++ table)
pure (Right (projectColumns columns (tableRows (probeTable probe))))
parallelShards = ProbeStorage (pure . probeWidth)
scanShards width table columns = ProbeStorage $ \probe -> do
recordCall probe ("scan_shards:" ++ show width ++ ":" ++ table)
let rows = maybe id projectColumns columns (tableRows (probeTable probe))
pure (Right (concat (rowShards width rows)))
insert _ _ = ProbeStorage (\_ -> pure (Left "probe: insert is not supported"))
replaceAll _ _ = ProbeStorage (\_ -> pure (Left "probe: replaceAll is not supported"))
createTable _ _ = ProbeStorage (\_ -> pure (Left "probe: createTable is not supported"))
dropTable _ = ProbeStorage (\_ -> pure (Left "probe: dropTable is not supported"))
dropColumn _ _ = ProbeStorage (\_ -> pure (Left "probe: dropColumn is not supported"))
replaceSchema _ _ _ = ProbeStorage (\_ -> pure (Left "probe: replaceSchema is not supported"))
snapshot = ProbeStorage (\_ -> pure [])
-- | 判断 Either 是 Right
isRight :: Either a b -> Bool
isRight (Right _) = True
@@ -2018,6 +2413,27 @@ optimizedPlan sql = do
q <- parseStatement sql
optimize testDB <$> translate q
-- | 在给定库上解析并优化 SQL,返回计划
optimizedPlanIn :: Database -> String -> Either String RelOp
optimizedPlanIn db sql = do
q <- parseStatement sql
optimize db <$> translate q
-- | 计划里出现的表名(先序)
planTables :: RelOp -> [String]
planTables op = case op of
Scan _ t _ -> [t]
Lookup _ t _ _ -> [t]
Range _ t _ _ _ -> [t]
Filter _ x -> planTables x
Project _ x -> planTables x
Compute _ x -> planTables x
Aggregate _ _ x -> planTables x
Sort _ x -> planTables x
Limit _ x -> planTables x
Join _ l r _ -> planTables l ++ planTables r
Unit -> []
-- | 计划里每个 Join 节点是不是 LEFT JOIN(先序)
leftJoinKinds :: RelOp -> [Bool]
leftJoinKinds (Join kind l r _) = (kind == LeftJoin) : (leftJoinKinds l ++ leftJoinKinds r)