准备可扩展类型系统工作,更合理的JOIN查询优化算法,并行查询
This commit is contained in:
1 parent
9758439b82
commit
274b7fcdcc
75 files changed
+3828
-1789
No files matched your search
@@ -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))
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user