准备可扩展类型系统工作,更合理的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)
|
||||
|
||||
@@ -9,6 +9,7 @@ module ChuSQL.Core.Model
|
||||
, TypeClass (..)
|
||||
, Value (..)
|
||||
, Row
|
||||
, TableMeta (..)
|
||||
, Table (..)
|
||||
, Database
|
||||
, colNames
|
||||
@@ -24,10 +25,15 @@ module ChuSQL.Core.Model
|
||||
, parseColumnType
|
||||
, typeClassOf
|
||||
, assignable
|
||||
, comparableTypes
|
||||
, integerType
|
||||
, numericType
|
||||
, valueFits
|
||||
, coerceValue
|
||||
, canonicalValue
|
||||
, compareValue
|
||||
, valuesEqual
|
||||
, hashValue
|
||||
) where
|
||||
|
||||
import Data.Char (isDigit, isSpace, toLower)
|
||||
@@ -230,6 +236,19 @@ integerType CBigInt = True
|
||||
integerType CSmallInt = True
|
||||
integerType _ = False
|
||||
|
||||
-- | 比较用的类型族:同大类可比,日期时间与文本互比
|
||||
comparableTypes :: ColumnType -> ColumnType -> Bool
|
||||
comparableTypes a b = sameClass || crossTemporal
|
||||
where
|
||||
-- | 两个类型同属一个大类
|
||||
sameClass = typeClassOf a == typeClassOf b
|
||||
-- | 时间与文本放在一起比
|
||||
crossTemporal = (temporal a && textual b) || (textual a && temporal b)
|
||||
-- | 是不是时间类型
|
||||
temporal t = typeClassOf t == TemporalClass
|
||||
-- | 是不是文本类型
|
||||
textual t = typeClassOf t == TextClass
|
||||
|
||||
-- | 是不是数值类型
|
||||
numericType :: ColumnType -> Bool
|
||||
numericType t = typeClassOf t == NumericClass
|
||||
@@ -363,22 +382,76 @@ data Value
|
||||
| VBool Bool
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 值的比较关键字:整值浮点折成整数
|
||||
canonicalValue :: Value -> Value
|
||||
canonicalValue (VFloat d)
|
||||
| wholeDouble d = VInt (truncate d)
|
||||
canonicalValue v = v
|
||||
|
||||
-- | 浮点是不是能精确折成整数的整值
|
||||
wholeDouble :: Double -> Bool
|
||||
wholeDouble d =
|
||||
not (isNaN d)
|
||||
&& not (isInfinite d)
|
||||
&& abs d <= 9007199254740992
|
||||
&& d == fromIntegral (truncate d :: Int)
|
||||
|
||||
-- | 值的全序:NULL 最小,数值跨类型比大小,异族按族的先后
|
||||
compareValue :: Value -> Value -> Ordering
|
||||
compareValue a b = case (canonicalValue a, canonicalValue b) of
|
||||
(VNull, VNull) -> EQ
|
||||
(VNull, _) -> LT
|
||||
(_, VNull) -> GT
|
||||
(VInt x, VInt y) -> compare x y
|
||||
(VInt x, VFloat y) -> compare (fromIntegral x) y
|
||||
(VFloat x, VInt y) -> compare x (fromIntegral y)
|
||||
(VFloat x, VFloat y) -> compare x y
|
||||
(VStr x, VStr y) -> compare x y
|
||||
(VBool x, VBool y) -> compare x y
|
||||
(x, y) -> compare (valueRank x) (valueRank y)
|
||||
|
||||
-- | 族的先后,只用来给异族值定序
|
||||
valueRank :: Value -> Int
|
||||
valueRank VNull = 0
|
||||
valueRank (VInt _) = 1
|
||||
valueRank (VFloat _) = 1
|
||||
valueRank (VStr _) = 2
|
||||
valueRank (VBool _) = 3
|
||||
|
||||
-- | 相等语义:比较关键字相同即相等,与 hashValue 配套
|
||||
valuesEqual :: Value -> Value -> Bool
|
||||
valuesEqual a b = canonicalValue a == canonicalValue b
|
||||
|
||||
-- | 值的关键字哈希,与 valuesEqual 用同一套关键字
|
||||
hashValue :: Value -> Int
|
||||
hashValue v = case canonicalValue v of
|
||||
VNull -> hashWithSalt 0 (0 :: Int)
|
||||
VInt n -> hashWithSalt 1 n
|
||||
VFloat d -> hashWithSalt 1 d
|
||||
VStr t -> hashWithSalt 2 t
|
||||
VBool b -> hashWithSalt 3 b
|
||||
|
||||
-- | 值按类型打散搅拌
|
||||
instance Hashable Value where
|
||||
hashWithSalt s VNull = hashWithSalt (hashWithSalt s (0 :: Int)) ()
|
||||
hashWithSalt s (VInt n) = hashWithSalt (hashWithSalt s (1 :: Int)) n
|
||||
hashWithSalt s (VFloat d) = hashWithSalt (hashWithSalt s (2 :: Int)) d
|
||||
hashWithSalt s (VStr t) = hashWithSalt (hashWithSalt s (3 :: Int)) t
|
||||
hashWithSalt s (VBool b) = hashWithSalt (hashWithSalt s (4 :: Int)) b
|
||||
hashWithSalt s = hashWithSalt s . hashValue
|
||||
|
||||
-- | 一行数据
|
||||
type Row = [(String, Value)]
|
||||
|
||||
-- | 一张表:名字、列定义与全部行
|
||||
-- | 一张表的统计:行数、各列不同值数(capped 表示只数到上限)、有索引的列
|
||||
data TableMeta = TableMeta
|
||||
{ metaRowCount :: Int
|
||||
, metaDistinct :: [(String, Int, Bool)]
|
||||
, metaIndexes :: [String]
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- | 一张表:名字、列定义与全部行;meta 为存储层统计,拿不到是 Nothing
|
||||
data Table = Table
|
||||
{ tableName :: String
|
||||
, tableCols :: [(String, Column)]
|
||||
, tableRows :: [Row]
|
||||
, tableMeta :: Maybe TableMeta
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ data Request
|
||||
| ReqAccountDrop T.Text
|
||||
| ReqScan String
|
||||
| ReqScanColumns String [String]
|
||||
| ReqScanShard String (Maybe [String]) Int Int
|
||||
| ReqInsert String Row
|
||||
| ReqInsertBatch String [Row]
|
||||
| ReqDeleteKeys String [Int]
|
||||
@@ -111,6 +112,14 @@ instance ToJSON Request where
|
||||
toJSON ReqPing = object ["method" .= ("ping" :: T.Text)]
|
||||
toJSON (ReqScan t) = object ["method" .= ("scan" :: T.Text), "table" .= t]
|
||||
toJSON (ReqScanColumns t cols) = object ["method" .= ("scan" :: T.Text), "table" .= t, "columns" .= cols]
|
||||
toJSON (ReqScanShard t cols shard shards) =
|
||||
object
|
||||
[ "method" .= ("scan_shard" :: T.Text)
|
||||
, "table" .= t
|
||||
, "columns" .= cols
|
||||
, "shard" .= shard
|
||||
, "shards" .= shards
|
||||
]
|
||||
toJSON (ReqInsert t r) =
|
||||
object
|
||||
[ "method" .= ("insert" :: T.Text)
|
||||
|
||||
@@ -73,14 +73,25 @@ fn unpack_position(v: u64) -> (PageId, u16) {
|
||||
(v >> 16, (v & 0xFFFF) as u16)
|
||||
}
|
||||
|
||||
/// 取行里某一列的整数值(不是整数就没有)
|
||||
fn row_int(row: &Row, column: &str) -> Option<i64> {
|
||||
match row.get(column) {
|
||||
Some(serde_json::Value::Number(n)) => n.as_i64(),
|
||||
/// 数值里取整数,整值浮点折成整数
|
||||
pub(crate) fn scalar_int(v: &serde_json::Value) -> Option<i64> {
|
||||
match v {
|
||||
serde_json::Value::Number(n) => n.as_i64().or_else(|| {
|
||||
let f = n.as_f64()?;
|
||||
if f.fract() != 0.0 || f.abs() > 9_007_199_254_740_992.0 {
|
||||
return None;
|
||||
}
|
||||
Some(f as i64)
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 取行里某一列的整数值(不是整数就没有)
|
||||
fn row_int(row: &Row, column: &str) -> Option<i64> {
|
||||
row.get(column).and_then(scalar_int)
|
||||
}
|
||||
|
||||
/// i64 键 → 8 字节大端(保序,负数也在前)
|
||||
fn key_of(value: i64) -> [u8; 8] {
|
||||
(value as u64 ^ (1u64 << 63)).to_be_bytes()
|
||||
@@ -89,7 +100,7 @@ fn key_of(value: i64) -> [u8; 8] {
|
||||
/// JSON 值 → 索引键字节(与写索引时同一套编码)
|
||||
fn value_key(v: &serde_json::Value) -> Option<Vec<u8>> {
|
||||
match v {
|
||||
serde_json::Value::Number(n) => n.as_i64().map(|k| key_of(k).to_vec()),
|
||||
serde_json::Value::Number(_) => scalar_int(v).map(|k| key_of(k).to_vec()),
|
||||
serde_json::Value::String(s) => Some(s.as_bytes().to_vec()),
|
||||
serde_json::Value::Bool(b) => Some(vec![u8::from(*b)]),
|
||||
_ => None,
|
||||
@@ -613,11 +624,41 @@ impl HeapTable {
|
||||
self.scan_projected_with_positions(None)
|
||||
}
|
||||
|
||||
/// 分片扫描,只取 [from, to) 页区间内的行
|
||||
pub fn scan_columns_pages(
|
||||
&mut self,
|
||||
columns: Option<&[String]>,
|
||||
from: PageId,
|
||||
to: PageId,
|
||||
) -> io::Result<Vec<Row>> {
|
||||
Ok(self
|
||||
.scan_pages_with_positions(columns, from, to)?
|
||||
.into_iter()
|
||||
.map(|(_, _, r)| r)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// 当前页数
|
||||
pub fn num_pages(&mut self) -> io::Result<u64> {
|
||||
self.file.num_pages()
|
||||
}
|
||||
|
||||
/// 全表扫描,带位置与列投影
|
||||
fn scan_projected_with_positions(&mut self, columns: Option<&[String]>) -> io::Result<Vec<(PageId, u16, Row)>> {
|
||||
let n = self.file.num_pages()?;
|
||||
self.scan_pages_with_positions(columns, 0, n)
|
||||
}
|
||||
|
||||
/// 扫描 [from, to) 页区间,带位置与列投影
|
||||
fn scan_pages_with_positions(
|
||||
&mut self,
|
||||
columns: Option<&[String]>,
|
||||
from: PageId,
|
||||
to: PageId,
|
||||
) -> io::Result<Vec<(PageId, u16, Row)>> {
|
||||
let n = self.file.num_pages()?;
|
||||
let mut out = Vec::new();
|
||||
for i in 0..n {
|
||||
for i in from..to.min(n) {
|
||||
let tuples = self.file.with_page(i, |p| {
|
||||
let mut v = Vec::new();
|
||||
for slot in HeapPage::iter_slots(p) {
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
use crate::protocol::SchemaColumn;
|
||||
|
||||
// 读取 scripts/init.sql,只认 CREATE TABLE 与 CREATE INDEX。
|
||||
// 系统库的 schema 由这份脚本声明,进程按它建系统表。
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InitColumn {
|
||||
pub name: String,
|
||||
pub ty: String,
|
||||
pub nullable: bool,
|
||||
pub auto_increment: bool,
|
||||
pub primary_key: bool,
|
||||
pub unique: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InitTable {
|
||||
pub name: String,
|
||||
pub columns: Vec<InitColumn>,
|
||||
pub indexes: Vec<String>,
|
||||
}
|
||||
|
||||
impl InitColumn {
|
||||
/// 转成数据字典里的列
|
||||
pub fn to_schema(&self) -> SchemaColumn {
|
||||
SchemaColumn {
|
||||
name: self.name.clone(),
|
||||
ty: self.ty.clone(),
|
||||
nullable: self.nullable,
|
||||
default: None,
|
||||
auto_increment: self.auto_increment,
|
||||
primary_key: self.primary_key,
|
||||
unique: self.unique,
|
||||
check: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析整份脚本;不认识的语句直接报错,避免悄悄漏建表
|
||||
pub fn parse(text: &str) -> Result<Vec<InitTable>, String> {
|
||||
let mut tables: Vec<InitTable> = Vec::new();
|
||||
for statement in statements(text) {
|
||||
let words = statement.split_whitespace().collect::<Vec<_>>();
|
||||
let head = |index: usize| words.get(index).map(|word| word.to_ascii_uppercase());
|
||||
match (head(0).as_deref(), head(1).as_deref()) {
|
||||
(Some("CREATE"), Some("TABLE")) => {
|
||||
let name = clean_name(words.get(2).ok_or("CREATE TABLE needs a name")?);
|
||||
let body = statement.split_once('(').ok_or("CREATE TABLE needs a column list")?.1;
|
||||
let inner = body.rsplit_once(')').map(|(inner, _)| inner).unwrap_or(body);
|
||||
tables.push(InitTable { name, columns: columns(inner)?, indexes: Vec::new() });
|
||||
}
|
||||
(Some("CREATE"), Some("INDEX")) => {
|
||||
let table = clean_name(words.get(2).ok_or("CREATE INDEX needs a table")?);
|
||||
let column = statement
|
||||
.split_once('(')
|
||||
.and_then(|(_, rest)| rest.split_once(')'))
|
||||
.map(|(column, _)| clean_name(column))
|
||||
.ok_or("CREATE INDEX needs a column")?;
|
||||
let entry = tables
|
||||
.iter_mut()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case(&table))
|
||||
.ok_or_else(|| format!("CREATE INDEX on unknown table: {table}"))?;
|
||||
entry.indexes.push(column);
|
||||
}
|
||||
_ => return Err(format!("unsupported statement: {}", statement.trim())),
|
||||
}
|
||||
}
|
||||
Ok(tables)
|
||||
}
|
||||
|
||||
/// 解析表定义里的列清单
|
||||
fn columns(body: &str) -> Result<Vec<InitColumn>, String> {
|
||||
let mut out = Vec::new();
|
||||
for part in split_top_level(body) {
|
||||
let words = part.split_whitespace().collect::<Vec<_>>();
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let keyword = words[0].to_ascii_uppercase();
|
||||
if matches!(keyword.as_str(), "PRIMARY" | "UNIQUE" | "KEY" | "CONSTRAINT" | "INDEX" | "CHECK" | "FOREIGN") {
|
||||
return Err(format!("table-level constraint is not supported: {}", part.trim()));
|
||||
}
|
||||
let name = clean_name(words[0]);
|
||||
let ty = words
|
||||
.get(1)
|
||||
.ok_or_else(|| format!("column {name} needs a type"))?
|
||||
.to_string();
|
||||
let upper = part.to_ascii_uppercase();
|
||||
out.push(InitColumn {
|
||||
name,
|
||||
ty,
|
||||
nullable: !upper.contains("NOT NULL"),
|
||||
auto_increment: upper.contains("AUTO_INCREMENT"),
|
||||
primary_key: upper.contains("PRIMARY KEY"),
|
||||
unique: upper.contains("UNIQUE"),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 按顶层的逗号切开(类型里的括号不算)
|
||||
fn split_top_level(body: &str) -> Vec<String> {
|
||||
let mut parts = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut depth = 0;
|
||||
for ch in body.chars() {
|
||||
match ch {
|
||||
'(' => {
|
||||
depth += 1;
|
||||
current.push(ch);
|
||||
}
|
||||
')' => {
|
||||
depth -= 1;
|
||||
current.push(ch);
|
||||
}
|
||||
',' if depth == 0 => {
|
||||
parts.push(std::mem::take(&mut current));
|
||||
}
|
||||
_ => current.push(ch),
|
||||
}
|
||||
}
|
||||
if !current.trim().is_empty() {
|
||||
parts.push(current);
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
/// 去掉反引号与空白
|
||||
fn clean_name(raw: &str) -> String {
|
||||
raw.trim().trim_matches('`').trim_matches('"').trim().to_string()
|
||||
}
|
||||
|
||||
/// 去掉注释后按分号切开
|
||||
fn statements(text: &str) -> Vec<String> {
|
||||
let mut cleaned = String::new();
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '-' && chars.peek() == Some(&'-') {
|
||||
for next in chars.by_ref() {
|
||||
if next == '\n' {
|
||||
cleaned.push('\n');
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ch == '/' && chars.peek() == Some(&'*') {
|
||||
chars.next();
|
||||
while let Some(next) = chars.next() {
|
||||
if next == '*' && chars.peek() == Some(&'/') {
|
||||
chars.next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cleaned.push(ch);
|
||||
}
|
||||
}
|
||||
cleaned
|
||||
.split(';')
|
||||
.map(|statement| statement.trim().to_string())
|
||||
.filter(|statement| !statement.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 仓库里的系统库脚本必须始终能解析
|
||||
const SYSTEM_SCHEMA: &str = include_str!("../../../scripts/init.sql");
|
||||
|
||||
/// 仓库自带脚本必须能解析出账号表
|
||||
#[test]
|
||||
fn parses_the_repository_init_script() {
|
||||
let tables = parse(SYSTEM_SCHEMA).expect("scripts/init.sql must parse");
|
||||
let users = tables
|
||||
.iter()
|
||||
.find(|table| table.name == "__chusql_users")
|
||||
.expect("init.sql must declare the account table");
|
||||
let names: Vec<&str> = users.columns.iter().map(|column| column.name.as_str()).collect();
|
||||
assert_eq!(names, ["id", "user", "password_hash", "registered_at", "last_login_at", "revision"]);
|
||||
assert_eq!(users.columns[0].ty, "int");
|
||||
assert!(users.columns[0].primary_key && users.columns[0].auto_increment && !users.columns[0].nullable);
|
||||
assert_eq!(users.columns[1].ty, "varchar(64)");
|
||||
assert!(users.columns[1].unique);
|
||||
assert_eq!(users.columns[4].ty, "timestamp");
|
||||
assert!(users.columns[4].nullable);
|
||||
assert_eq!(users.indexes, ["user"]);
|
||||
}
|
||||
|
||||
/// 未知语句与表级约束要报错
|
||||
#[test]
|
||||
fn rejects_unknown_statements_and_table_level_constraints() {
|
||||
assert!(parse("DROP TABLE users;").is_err());
|
||||
assert!(parse("CREATE TABLE t (id int, PRIMARY KEY (id));").is_err());
|
||||
assert!(parse("CREATE INDEX missing (user);").is_err());
|
||||
}
|
||||
|
||||
/// 注释与空语句要被跳过
|
||||
#[test]
|
||||
fn ignores_comments_and_blank_statements() {
|
||||
let tables = parse("-- note\nCREATE TABLE t (a int NOT NULL);\n\n/* done */\n").unwrap();
|
||||
assert_eq!(tables.len(), 1);
|
||||
assert_eq!(tables[0].columns[0].name, "a");
|
||||
assert!(!tables[0].columns[0].nullable);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ pub mod catalog;
|
||||
pub mod config;
|
||||
pub mod ffi;
|
||||
pub mod heap;
|
||||
pub mod initsql;
|
||||
pub mod log;
|
||||
pub mod page;
|
||||
pub mod protocol;
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::collections::HashMap;
|
||||
|
||||
pub type Row = HashMap<String, serde_json::Value>;
|
||||
|
||||
pub const USERS_TABLE: &str = "__chusql_users";
|
||||
pub const USERS_TABLE: &str = "__system_users";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Account {
|
||||
@@ -83,12 +83,28 @@ pub enum Request {
|
||||
at: Option<String>,
|
||||
},
|
||||
AccountDrop { user: String },
|
||||
/// 引导系统目录:建系统表,给了 user 就同时补首个管理员
|
||||
BootstrapSystem {
|
||||
#[serde(default)]
|
||||
user: Option<String>,
|
||||
#[serde(default)]
|
||||
password_hash: Option<String>,
|
||||
},
|
||||
SystemStatus,
|
||||
Ping,
|
||||
Scan {
|
||||
table: String,
|
||||
#[serde(default)]
|
||||
columns: Option<Vec<String>>,
|
||||
},
|
||||
/// 分片扫描:第 shard 片只读总页数按 shards 均分后的那一段
|
||||
ScanShard {
|
||||
table: String,
|
||||
#[serde(default)]
|
||||
columns: Option<Vec<String>>,
|
||||
shard: u32,
|
||||
shards: u32,
|
||||
},
|
||||
Insert {
|
||||
table: String,
|
||||
row: Row,
|
||||
@@ -174,7 +190,8 @@ impl Request {
|
||||
/// 普通表操作的统一访问边界
|
||||
pub fn table(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Scan { table, .. } | Self::Insert { table, .. }
|
||||
Self::Scan { table, .. } | Self::ScanShard { table, .. }
|
||||
| Self::Insert { table, .. }
|
||||
| Self::InsertBatch { table, .. } | Self::DeleteKeys { table, .. }
|
||||
| Self::LookupByIndex { table, .. } | Self::ReplaceAll { table, .. }
|
||||
| Self::RangeByIndex { table, .. }
|
||||
@@ -185,14 +202,14 @@ impl Request {
|
||||
| Self::ReplaceSchema { table, .. } => Some(table),
|
||||
Self::Ping | Self::ListTables | Self::ListCatalog | Self::AccountsList
|
||||
| Self::AccountCreate { .. } | Self::AccountReset { .. } | Self::AccountLogin { .. }
|
||||
| Self::AccountDrop { .. } => None,
|
||||
| Self::AccountDrop { .. } | Self::BootstrapSystem { .. } | Self::SystemStatus => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 是不是保留系统表名
|
||||
pub fn reserved_table(table: &str) -> bool {
|
||||
table.to_ascii_lowercase().starts_with("__chusql_")
|
||||
table.to_ascii_lowercase().starts_with("__system_")
|
||||
}
|
||||
|
||||
/// 没写 column 时按 `id` 算
|
||||
@@ -219,6 +236,8 @@ pub enum Response {
|
||||
},
|
||||
NoIndex,
|
||||
Ok,
|
||||
/// 系统目录状态
|
||||
System { initialized: bool },
|
||||
Error { message: String },
|
||||
Catalog { schemas: Vec<TableSchemaWire> },
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use crate::catalog::Catalog;
|
||||
use crate::config::{self, Config, Loaded};
|
||||
use crate::heap::HeapTable;
|
||||
use crate::initsql::{self, InitColumn, InitTable};
|
||||
use crate::heap::{scalar_int, HeapTable};
|
||||
use crate::log;
|
||||
use crate::protocol::{
|
||||
reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, SchemaColumn, TableSchemaWire, USERS_TABLE,
|
||||
@@ -21,6 +20,7 @@ struct Server {
|
||||
cfg: Config,
|
||||
tables: Mutex<HashMap<String, Arc<Mutex<HeapTable>>>>,
|
||||
write_lock: Mutex<()>,
|
||||
gate: RwLock<()>,
|
||||
catalog: Mutex<Catalog>,
|
||||
wal: Wal,
|
||||
recovery_required: AtomicBool,
|
||||
@@ -38,6 +38,7 @@ impl Server {
|
||||
cfg,
|
||||
tables: Mutex::new(HashMap::new()),
|
||||
write_lock: Mutex::new(()),
|
||||
gate: RwLock::new(()),
|
||||
catalog: Mutex::new(catalog),
|
||||
wal,
|
||||
recovery_required: AtomicBool::new(false),
|
||||
@@ -127,6 +128,56 @@ impl Server {
|
||||
f(&mut guard)
|
||||
}
|
||||
|
||||
/// 请求带列名时逐个查字典
|
||||
fn check_columns(&self, table: &str, columns: Option<&[String]>) -> std::io::Result<()> {
|
||||
let Some(cols) = columns else {
|
||||
return Ok(());
|
||||
};
|
||||
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
|
||||
let schema = c
|
||||
.describe(table)
|
||||
.ok_or_else(|| std::io::Error::other(format!("unknown table: {table}")))?;
|
||||
for col in cols {
|
||||
if !schema.columns.iter().any(|c| &c.name == col) {
|
||||
return Err(std::io::Error::other(format!("unknown column: {col}")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 分片只读扫描:独立句柄读一段连续页,不经表锁
|
||||
fn scan_shard(
|
||||
&self,
|
||||
table: &str,
|
||||
columns: Option<&[String]>,
|
||||
shard: u32,
|
||||
shards: u32,
|
||||
) -> std::io::Result<Vec<Row>> {
|
||||
if shards == 0 || shard >= shards {
|
||||
return Err(std::io::Error::other("invalid shard range"));
|
||||
}
|
||||
if !self.table_exists(table)? {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("unknown table: {}", table),
|
||||
));
|
||||
}
|
||||
let hidden: HashSet<String> = self
|
||||
.catalog
|
||||
.lock()
|
||||
.map_err(|_| std::io::Error::other("catalog lock poisoned"))?
|
||||
.describe(table)
|
||||
.map(|s| s.dropped_columns.iter().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
let mut t = HeapTable::open(self.table_path(table), self.cfg.page_size, self.cfg.pool_size)?;
|
||||
t.set_hidden_columns(hidden);
|
||||
let n = t.num_pages()?;
|
||||
let chunk = n.div_ceil(u64::from(shards));
|
||||
let from = u64::from(shard).saturating_mul(chunk).min(n);
|
||||
let to = u64::from(shard + 1).saturating_mul(chunk).min(n);
|
||||
t.scan_columns_pages(columns, from, to)
|
||||
}
|
||||
|
||||
/// 表不存在就报错
|
||||
fn with_existing_table<R>(
|
||||
&self,
|
||||
@@ -528,6 +579,88 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 账号表是否已登记为系统表
|
||||
fn account_table_ready(&self) -> std::io::Result<bool> {
|
||||
use std::io::{Error, ErrorKind};
|
||||
let registered = {
|
||||
let c = self.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?;
|
||||
c.describe(USERS_TABLE).map(|schema| schema.system)
|
||||
};
|
||||
match registered {
|
||||
Some(true) => Ok(true),
|
||||
Some(false) => Err(Error::new(ErrorKind::AlreadyExists, "reserved account table collision")),
|
||||
None => {
|
||||
if self.table_path(USERS_TABLE).exists() || self.index_path(USERS_TABLE, "id").exists() {
|
||||
Err(Error::new(ErrorKind::AlreadyExists, "reserved account file collision"))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 系统目录里账号表已登记
|
||||
fn system_initialized(&self) -> bool {
|
||||
self.catalog
|
||||
.lock()
|
||||
.map(|c| c.describe(USERS_TABLE).is_some_and(|schema| schema.system))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 读账号表;表没登记或数据文件缺失都算错
|
||||
fn load_accounts(&self) -> std::io::Result<Vec<Account>> {
|
||||
use std::io::Error;
|
||||
if !self.table_path(USERS_TABLE).exists() {
|
||||
return Err(Error::other("missing account data"));
|
||||
}
|
||||
let mut accounts: Vec<Account> = self
|
||||
.with_existing_table(USERS_TABLE, |t| t.scan())?
|
||||
.iter()
|
||||
.map(decode_account_row)
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
for account in accounts.iter_mut() {
|
||||
normalize_account(account);
|
||||
}
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// 引导系统目录:建账号表,空表才补管理员
|
||||
fn bootstrap_system(&self, user: Option<&str>, password_hash: Option<&str>) -> std::io::Result<Vec<Account>> {
|
||||
use std::io::Error;
|
||||
let _guard = self.write_lock.lock().map_err(|_| Error::other("write lock poisoned"))?;
|
||||
if self.recovery_required.load(Ordering::Acquire) {
|
||||
return Err(Error::other("storage recovery required"));
|
||||
}
|
||||
let initialized = self.account_table_ready()?;
|
||||
let mut accounts = if initialized { self.load_accounts()? } else { Vec::new() };
|
||||
let mut changed = !initialized;
|
||||
if let Some(user) = user {
|
||||
let name = account_name(user)?;
|
||||
// 表里已经有行就不再补人:引导程序只在空表上播种,
|
||||
// 免得有人拿 storage 透传绕过账号接口往里塞一个已知口令的账号
|
||||
if accounts.is_empty() && !accounts.iter().any(|a| a.user == name) {
|
||||
let id = accounts.iter().map(|a| a.id).max().unwrap_or(0)
|
||||
.checked_add(1).ok_or_else(|| Error::other("account id exhausted"))?;
|
||||
accounts.push(new_account(id, &name, password_hash.unwrap_or_default().to_string())?);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return Ok(accounts);
|
||||
}
|
||||
for row in account_rows(&accounts)? {
|
||||
HeapTable::encode_row(&row, self.cfg.page_size)?;
|
||||
}
|
||||
let op = WalOp::Accounts { accounts: accounts.clone() };
|
||||
self.recovery_required.store(true, Ordering::Release);
|
||||
self.wal.truncate()?;
|
||||
self.wal.append(&op)?;
|
||||
self.apply_op(&op)?;
|
||||
self.wal.clear()?;
|
||||
self.recovery_required.store(false, Ordering::Release);
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// 账号读写共享串行持久化边界
|
||||
fn accounts_request(&self, request: Request) -> std::io::Result<Vec<Account>> {
|
||||
use std::io::{Error, ErrorKind};
|
||||
@@ -535,31 +668,10 @@ impl Server {
|
||||
if self.recovery_required.load(Ordering::Acquire) {
|
||||
return Err(Error::other("storage recovery required"));
|
||||
}
|
||||
let initialized = {
|
||||
let c = self.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?;
|
||||
match c.describe(USERS_TABLE) {
|
||||
Some(schema) if schema.system => true,
|
||||
Some(_) => return Err(Error::new(ErrorKind::AlreadyExists, "reserved account table collision")),
|
||||
None => false,
|
||||
}
|
||||
};
|
||||
if !initialized && (self.table_path(USERS_TABLE).exists() || self.index_path(USERS_TABLE, "id").exists()) {
|
||||
return Err(Error::new(ErrorKind::AlreadyExists, "reserved account file collision"));
|
||||
if !self.account_table_ready()? {
|
||||
return Err(Error::other("system catalog is not initialized; run csql-bootstrap"));
|
||||
}
|
||||
let mut accounts: Vec<Account> = if initialized {
|
||||
if !self.table_path(USERS_TABLE).exists() {
|
||||
return Err(Error::other("missing account data"));
|
||||
}
|
||||
let mut loaded: Vec<Account> = self
|
||||
.with_existing_table(USERS_TABLE, |t| t.scan())?
|
||||
.iter()
|
||||
.map(decode_account_row)
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
for account in loaded.iter_mut() {
|
||||
normalize_account(account);
|
||||
}
|
||||
loaded
|
||||
} else { Vec::new() };
|
||||
let mut accounts = self.load_accounts()?;
|
||||
match request {
|
||||
Request::AccountsList => return Ok(accounts),
|
||||
Request::AccountCreate { user, password_hash } => {
|
||||
@@ -609,8 +721,27 @@ impl Server {
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// 一条请求翻成一条响应
|
||||
/// 一条请求翻成一条响应:先过读/写门再分发
|
||||
fn handle_request(&self, req: Request) -> Response {
|
||||
if concurrent_read(&req) {
|
||||
match self.gate.read() {
|
||||
Ok(_read) => self.dispatch(req),
|
||||
Err(_) => Response::Error {
|
||||
message: "storage read gate poisoned".into(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
match self.gate.write() {
|
||||
Ok(_write) => self.dispatch(req),
|
||||
Err(_) => Response::Error {
|
||||
message: "storage write gate poisoned".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 持门后分发一条请求
|
||||
fn dispatch(&self, req: Request) -> Response {
|
||||
let _ddl_guard = if matches!(req, Request::CreateTable { .. } | Request::DropTable { .. }
|
||||
| Request::CreateIndex { .. } | Request::DropIndex { .. }) {
|
||||
match self.write_lock.lock() {
|
||||
@@ -641,6 +772,13 @@ impl Server {
|
||||
Err(e) => Response::Error { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
Request::BootstrapSystem { user, password_hash } => {
|
||||
match self.bootstrap_system(user.as_deref(), password_hash.as_deref()) {
|
||||
Ok(_) => Response::System { initialized: true },
|
||||
Err(e) => Response::Error { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
Request::SystemStatus => Response::System { initialized: self.system_initialized() },
|
||||
Request::Ping => {
|
||||
log_debug!(request, "ping");
|
||||
Response::Pong
|
||||
@@ -650,15 +788,7 @@ impl Server {
|
||||
log_debug!(request, "scan table={}", table);
|
||||
|
||||
let result = (|| {
|
||||
if let Some(cols) = &columns {
|
||||
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
|
||||
let schema = c.describe(&table).ok_or_else(|| std::io::Error::other(format!("unknown table: {table}")))?;
|
||||
for col in cols {
|
||||
if !schema.columns.iter().any(|c| &c.name == col) {
|
||||
return Err(std::io::Error::other(format!("unknown column: {col}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.check_columns(&table, columns.as_deref())?;
|
||||
self.with_existing_table(&table, |t| t.scan_columns(columns.as_deref()))
|
||||
})();
|
||||
|
||||
@@ -673,6 +803,30 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
Request::ScanShard {
|
||||
table,
|
||||
columns,
|
||||
shard,
|
||||
shards,
|
||||
} => {
|
||||
log_debug!(request, "scan_shard table={} shard={}/{}", table, shard, shards);
|
||||
|
||||
let result = (|| {
|
||||
self.check_columns(&table, columns.as_deref())?;
|
||||
self.scan_shard(&table, columns.as_deref(), shard, shards)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(rows) => Response::Rows { rows },
|
||||
Err(e) => {
|
||||
log_warn!(request, "scan {} shard {}/{}: {}", table, shard, shards, e);
|
||||
Response::Error {
|
||||
message: format!("scan {}: {}", table, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Request::Insert { table, row } => {
|
||||
log_debug!(request, "insert table={}", table);
|
||||
let tname = table.clone();
|
||||
@@ -733,7 +887,7 @@ impl Server {
|
||||
if !indexed {
|
||||
return Response::NoIndex;
|
||||
}
|
||||
let found = match key.as_i64() {
|
||||
let found = match scalar_int(&key) {
|
||||
Some(k) => self.with_table(&table, |t| t.get_all_by_column_key(&column, k)),
|
||||
None => match key.as_str() {
|
||||
Some(s) => self.with_table(&table, |t| t.get_all_by_string_key(&column, s)),
|
||||
@@ -1025,18 +1179,31 @@ fn account_rows(accounts: &[Account]) -> std::io::Result<Vec<Row>> {
|
||||
]))).collect()
|
||||
}
|
||||
|
||||
/// 按 init.sql 把系统表写进字典
|
||||
/// 系统库的账号表结构:引导模块按这份定义建表
|
||||
fn users_columns() -> Vec<SchemaColumn> {
|
||||
vec![
|
||||
SchemaColumn { name: "id".into(), ty: "int".into(), nullable: false,
|
||||
auto_increment: true, primary_key: true, ..Default::default() },
|
||||
SchemaColumn { name: "user".into(), ty: "varchar(64)".into(), nullable: false,
|
||||
unique: true, ..Default::default() },
|
||||
SchemaColumn { name: "password_hash".into(), ty: "varchar(256)".into(),
|
||||
nullable: false, ..Default::default() },
|
||||
SchemaColumn { name: "registered_at".into(), ty: "timestamp".into(),
|
||||
nullable: false, ..Default::default() },
|
||||
SchemaColumn { name: "last_login_at".into(), ty: "timestamp".into(),
|
||||
..Default::default() },
|
||||
SchemaColumn { name: "revision".into(), ty: "int".into(), nullable: false,
|
||||
..Default::default() },
|
||||
]
|
||||
}
|
||||
|
||||
/// 把系统表写进字典
|
||||
fn declare_system_schema(catalog: &mut Catalog, rows: &[Row]) -> std::io::Result<()> {
|
||||
for table in system_schema() {
|
||||
let stats: &[Row] = if table.name == USERS_TABLE { rows } else { &[] };
|
||||
catalog.rebuild_stats(&table.name, stats);
|
||||
catalog.set_columns(&table.name, table.columns.iter().map(InitColumn::to_schema).collect())?;
|
||||
catalog.rebuild_stats(&table.name, stats);
|
||||
catalog.mark_system(&table.name)?;
|
||||
for index in &table.indexes {
|
||||
catalog.add_index(&table.name, index)?;
|
||||
}
|
||||
}
|
||||
catalog.rebuild_stats(USERS_TABLE, rows);
|
||||
catalog.set_columns(USERS_TABLE, users_columns())?;
|
||||
catalog.rebuild_stats(USERS_TABLE, rows);
|
||||
catalog.mark_system(USERS_TABLE)?;
|
||||
catalog.add_index(USERS_TABLE, "user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1110,9 +1277,9 @@ fn account_name(user: &str) -> std::io::Result<String> {
|
||||
Ok(user.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// 校验口令哈希长度
|
||||
/// 校验口令哈希长度,空串表示免密
|
||||
fn validate_account_hash(hash: &str) -> std::io::Result<()> {
|
||||
if hash.is_empty() || hash.len() > 256 {
|
||||
if hash.len() > 256 {
|
||||
return Err(std::io::Error::other("invalid account credential"));
|
||||
}
|
||||
Ok(())
|
||||
@@ -1172,15 +1339,6 @@ impl Drop for Server {
|
||||
/// 别的库一律要显式建(没有默认工作库)。
|
||||
const SYSTEM_DATABASE: &str = "system";
|
||||
|
||||
/// 系统库的表结构来自这份脚本(编译期嵌进来)
|
||||
const SYSTEM_SCHEMA_SQL: &str = include_str!("../../../scripts/init.sql");
|
||||
|
||||
/// 系统库 schema:脚本缺表或多表都按它来
|
||||
fn system_schema() -> &'static [InitTable] {
|
||||
static TABLES: std::sync::OnceLock<Vec<InitTable>> = std::sync::OnceLock::new();
|
||||
TABLES.get_or_init(|| initsql::parse(SYSTEM_SCHEMA_SQL).unwrap_or_else(|e| panic!("scripts/init.sql: {e}")))
|
||||
}
|
||||
|
||||
/// 保留库:不能建、不能删。
|
||||
fn reserved_database(name: &str) -> bool {
|
||||
name == SYSTEM_DATABASE
|
||||
@@ -1322,7 +1480,8 @@ impl Databases {
|
||||
let request: Request = serde_json::from_value(value).map_err(Error::other)?;
|
||||
let global = matches!(request, Request::AccountsList | Request::AccountCreate { .. }
|
||||
| Request::AccountReset { .. } | Request::AccountLogin { .. }
|
||||
| Request::AccountDrop { .. } | Request::Ping);
|
||||
| Request::AccountDrop { .. } | Request::BootstrapSystem { .. }
|
||||
| Request::SystemStatus | Request::Ping);
|
||||
let server = if global { &self.system }
|
||||
else if selected.is_empty() { return Err(Error::other("no database selected")); }
|
||||
else if selected == SYSTEM_DATABASE { &self.system }
|
||||
@@ -1331,6 +1490,17 @@ impl Databases {
|
||||
}
|
||||
}
|
||||
|
||||
/// 只读表页的请求可与其它读并发,其余独占
|
||||
fn concurrent_read(req: &Request) -> bool {
|
||||
matches!(
|
||||
req,
|
||||
Request::Scan { .. }
|
||||
| Request::ScanShard { .. }
|
||||
| Request::LookupByIndex { .. }
|
||||
| Request::RangeByIndex { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// 校验库名是否合法
|
||||
fn valid_database(name: &str) -> bool {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
|
||||
@@ -32,6 +32,190 @@ fn scan_projects_columns_and_preserves_empty_rows() -> Result<(), Box<dyn std::e
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 分片扫描按页序拼接后与整表扫描逐行一致
|
||||
#[test]
|
||||
fn scan_shard_concatenates_to_the_whole_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, data) = start_server();
|
||||
let mut c = connect()?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"wide","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?;
|
||||
let rows: Vec<serde_json::Value> = (1..=1200)
|
||||
.map(|i| serde_json::json!({"id": i, "name": format!("row-{i:04}")}))
|
||||
.collect();
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert_batch","table":"wide","rows":rows}))?;
|
||||
let heap_bytes = std::fs::metadata(data.path().join("databases/main/wide.db"))?.len();
|
||||
assert!(heap_bytes > 4 * 4096, "分片要有意义得多页,实际只有 {heap_bytes} 字节");
|
||||
let whole: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"wide"}"#))?;
|
||||
let all = whole["rows"].as_array().expect("rows").clone();
|
||||
assert_eq!(all.len(), 1200);
|
||||
for shards in [1, 3, 4, 7, 16] {
|
||||
let mut merged = Vec::new();
|
||||
for shard in 0..shards {
|
||||
let request = serde_json::json!({"method":"scan_shard","table":"wide","shard":shard,"shards":shards});
|
||||
let part: serde_json::Value = serde_json::from_str(&send(&mut c, &request.to_string()))?;
|
||||
assert_eq!(part["status"], "rows", "shards={shards} shard={shard}: {part}");
|
||||
merged.extend(part["rows"].as_array().expect("rows").iter().cloned());
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::Value::Array(merged),
|
||||
serde_json::Value::Array(all.clone()),
|
||||
"{shards} 个分片拼接后必须与整表同序"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 分片扫描支持投影与空列,越界分片和未知名都明确报错
|
||||
#[test]
|
||||
fn scan_shard_projects_columns_and_rejects_bad_ranges() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, _data) = start_server();
|
||||
let mut c = connect()?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"items","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert_batch","table":"items","rows":[{"id":1,"name":"one"},{"id":2,"name":null}]}))?;
|
||||
for (columns, expected) in [
|
||||
(serde_json::json!(["name"]), serde_json::json!([{"name":"one"},{"name":null}])),
|
||||
(serde_json::json!([]), serde_json::json!([{},{}])),
|
||||
] {
|
||||
let mut merged = Vec::new();
|
||||
for shard in 0..3 {
|
||||
let request = serde_json::json!({"method":"scan_shard","table":"items","columns":columns,"shard":shard,"shards":3});
|
||||
let part: serde_json::Value = serde_json::from_str(&send(&mut c, &request.to_string()))?;
|
||||
assert_eq!(part["status"], "rows", "{part}");
|
||||
merged.extend(part["rows"].as_array().expect("rows").iter().cloned());
|
||||
}
|
||||
assert_eq!(serde_json::Value::Array(merged), expected);
|
||||
}
|
||||
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"blank","columns":[{"name":"id","ty":"int"}]}))?;
|
||||
for (line, why) in [
|
||||
(r#"{"method":"scan_shard","table":"blank","shard":0,"shards":4}"#, "空表"),
|
||||
(r#"{"method":"scan_shard","table":"items","shard":3,"shards":3}"#, "分片号越界"),
|
||||
(r#"{"method":"scan_shard","table":"items","shard":0,"shards":0}"#, "分片数为零"),
|
||||
(r#"{"method":"scan_shard","table":"missing","shard":0,"shards":2}"#, "未知表"),
|
||||
(r#"{"method":"scan_shard","table":"items","columns":["missing"],"shard":0,"shards":2}"#, "未知列"),
|
||||
] {
|
||||
let result: serde_json::Value = serde_json::from_str(&send(&mut c, line))?;
|
||||
if why == "空表" {
|
||||
assert_eq!(result["rows"], serde_json::json!([]), "{line}");
|
||||
} else {
|
||||
assert_eq!(result["status"], "error", "{why}: {line} -> {result}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 多线程并发分片读同一实例,各分片结果与整表一致
|
||||
#[test]
|
||||
fn concurrent_shard_reads_share_one_instance() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, _data) = start_server();
|
||||
let mut c = connect()?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"wide","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?;
|
||||
let rows: Vec<serde_json::Value> = (1..=600)
|
||||
.map(|i| serde_json::json!({"id": i, "name": format!("row-{i:04}")}))
|
||||
.collect();
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert_batch","table":"wide","rows":rows}))?;
|
||||
|
||||
let storage = CURRENT.with(|slot| slot.borrow().as_ref().cloned().expect("storage opened"));
|
||||
let shards = 4;
|
||||
let parts = std::thread::scope(|scope| {
|
||||
let handles: Vec<_> = (0..shards)
|
||||
.map(|shard| {
|
||||
let storage = Arc::clone(&storage);
|
||||
scope.spawn(move || {
|
||||
let request = format!(
|
||||
r#"{{"method":"scan_shard","database":"{FIXTURE_DATABASE}","table":"wide","shard":{shard},"shards":{shards}}}"#
|
||||
);
|
||||
let mut rows = Vec::new();
|
||||
for _ in 0..4 {
|
||||
let reply = storage.request_line(&request);
|
||||
let value: serde_json::Value = serde_json::from_str(&reply).expect("json");
|
||||
assert_eq!(value["status"], "rows", "{reply}");
|
||||
rows = value["rows"].as_array().expect("rows").clone();
|
||||
}
|
||||
rows
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
handles
|
||||
.into_iter()
|
||||
.map(|h| h.join().expect("worker thread"))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let whole: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"wide"}"#))?;
|
||||
let merged: Vec<serde_json::Value> = parts.into_iter().flatten().collect();
|
||||
assert_eq!(merged.len(), 600, "并发分片不能丢行");
|
||||
assert_eq!(serde_json::Value::Array(merged), whole["rows"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 分片读与同表写并发时读者只看到一致快照
|
||||
#[test]
|
||||
fn concurrent_shard_reads_exclude_writers() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, _data) = start_server();
|
||||
let mut c = connect()?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"wide","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?;
|
||||
let rows: Vec<serde_json::Value> = (1..=400)
|
||||
.map(|i| serde_json::json!({"id": i, "name": format!("row-{i:04}")}))
|
||||
.collect();
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert_batch","table":"wide","rows":rows}))?;
|
||||
|
||||
let storage = CURRENT.with(|slot| slot.borrow().as_ref().cloned().expect("storage opened"));
|
||||
let shards = 3;
|
||||
let inserted = 20;
|
||||
std::thread::scope(|scope| {
|
||||
let writer = {
|
||||
let storage = Arc::clone(&storage);
|
||||
scope.spawn(move || {
|
||||
for i in 401..=400 + inserted {
|
||||
let request = format!(
|
||||
r#"{{"method":"insert","database":"{FIXTURE_DATABASE}","table":"wide","row":{{"id":{i},"name":"row-{i:04}"}}}}"#
|
||||
);
|
||||
let reply = storage.request_line(&request);
|
||||
let value: serde_json::Value = serde_json::from_str(&reply).expect("json");
|
||||
assert_eq!(value["status"], "ok", "{reply}");
|
||||
}
|
||||
})
|
||||
};
|
||||
let readers: Vec<_> = (0..3)
|
||||
.map(|_| {
|
||||
let storage = Arc::clone(&storage);
|
||||
scope.spawn(move || {
|
||||
let mut seen = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let mut total = 0usize;
|
||||
for shard in 0..shards {
|
||||
let request = format!(
|
||||
r#"{{"method":"scan_shard","database":"{FIXTURE_DATABASE}","table":"wide","shard":{shard},"shards":{shards}}}"#
|
||||
);
|
||||
let reply = storage.request_line(&request);
|
||||
let value: serde_json::Value = serde_json::from_str(&reply).expect("json");
|
||||
assert_eq!(value["status"], "rows", "{reply}");
|
||||
total += value["rows"].as_array().expect("rows").len();
|
||||
}
|
||||
seen.push(total);
|
||||
}
|
||||
seen
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for reader in readers {
|
||||
let seen: Vec<usize> = reader.join().expect("reader thread");
|
||||
assert!(
|
||||
seen.iter().all(|n| (400..=400 + inserted).contains(n)),
|
||||
"分片读看到越界行数: {seen:?}"
|
||||
);
|
||||
assert!(
|
||||
seen.windows(2).all(|pair| pair[0] <= pair[1]),
|
||||
"并发写时快照行数倒退: {seen:?}"
|
||||
);
|
||||
}
|
||||
writer.join().expect("writer thread");
|
||||
});
|
||||
|
||||
let whole: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"wide"}"#))?;
|
||||
assert_eq!(whole["rows"].as_array().expect("rows").len(), 400 + inserted);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删列不改堆字节,重启后不再暴露该列
|
||||
#[test]
|
||||
fn drop_column_keeps_heap_bytes_and_hides_values_after_restart() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -240,7 +424,7 @@ fn startup_compaction_reclaims_dropped_columns() -> Result<(), Box<dyn std::erro
|
||||
fn databases_reject_unsafe_names_and_keep_only_system_reserved() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, data) = start_server();
|
||||
let mut c = connect()?;
|
||||
for database in ["", "../escape", "a/b", "a.b", "CON", "__chusql_users"] {
|
||||
for database in ["", "../escape", "a/b", "a.b", "CON", "__system_users"] {
|
||||
let response: serde_json::Value = serde_json::from_str(&send(&mut c, &serde_json::json!({"method":"create_database","database":database}).to_string()))?;
|
||||
assert_eq!(response["status"], "error", "{database}: {response}");
|
||||
}
|
||||
@@ -270,7 +454,7 @@ fn system_database_owns_accounts_and_nothing_is_default() -> Result<(), Box<dyn
|
||||
assert_eq!(listed["tables"], serde_json::json!(["main", "system"]));
|
||||
let created: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"account_create","user":"alice","password_hash":"hash-1"}"#))?;
|
||||
assert_eq!(created["status"], "accounts", "{created}");
|
||||
assert!(data.path().join("system/__chusql_users.db").is_file());
|
||||
assert!(data.path().join("system/__system_users.db").is_file());
|
||||
// 账号是全局的:请求里带的库名对账号操作不起作用
|
||||
let accounts: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"accounts_list","database":"sales"}"#))?;
|
||||
assert_eq!(accounts["status"], "accounts");
|
||||
@@ -323,9 +507,10 @@ fn start_server() -> (ServerProc, tempfile::TempDir) {
|
||||
(server, data)
|
||||
}
|
||||
|
||||
/// 在指定数据目录起存储并建测试库
|
||||
/// 在指定数据目录起存储、引导系统目录并建测试库
|
||||
fn start_server_in(data: &std::path::Path) -> ServerProc {
|
||||
open_storage(&write_config(data));
|
||||
bootstrap_system();
|
||||
create_fixture_database();
|
||||
ServerProc
|
||||
}
|
||||
@@ -371,7 +556,7 @@ fn named_database_survives_restart_and_accounts_stay_global() -> Result<(), Box<
|
||||
assert_eq!(rows["rows"], serde_json::json!([{"id":7}]));
|
||||
let accounts: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"accounts_list","database":"sales"}"#))?;
|
||||
assert_eq!(accounts["status"], "accounts");
|
||||
let hidden: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"sales.__chusql_users"}"#))?;
|
||||
let hidden: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"sales.__system_users"}"#))?;
|
||||
assert_eq!(hidden["status"], "error");
|
||||
Ok(())
|
||||
}
|
||||
@@ -404,6 +589,15 @@ fn create_fixture_database() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 引导系统目录:建账号表,不塞账号
|
||||
fn bootstrap_system() {
|
||||
let reply = CURRENT.with(|slot| {
|
||||
let storage = slot.borrow().as_ref().cloned().expect("storage opened");
|
||||
storage.request_line(r#"{"method":"bootstrap_system"}"#)
|
||||
});
|
||||
assert!(reply.contains(r#""status":"system""#), "{reply}");
|
||||
}
|
||||
|
||||
/// 发一行请求读一行响应(没写库名就补测试库)
|
||||
fn send(c: &mut Conn, line: &str) -> String {
|
||||
send_raw(c, &with_default_database(line))
|
||||
@@ -437,7 +631,7 @@ fn reserved_tables_reject_every_generic_operation() -> std::io::Result<()> {
|
||||
for method in ["scan", "insert", "insert_batch", "delete_keys", "lookup_by_index",
|
||||
"replace_all", "describe_table", "create_table", "drop_table", "create_index",
|
||||
"drop_index", "drop_column"] {
|
||||
let request = serde_json::json!({"method": method, "table": "__chusql_users",
|
||||
let request = serde_json::json!({"method": method, "table": "__system_users",
|
||||
"column": "id", "key": 1, "keys": [], "row": {"id": 1}, "rows": [], "columns": []});
|
||||
let result = send(&mut c, &request.to_string());
|
||||
assert!(result.contains("reserved system table"), "{method}: {result}");
|
||||
@@ -450,11 +644,11 @@ fn reserved_tables_reject_every_generic_operation() -> std::io::Result<()> {
|
||||
fn generic_ipc_rejects_path_aliases() -> std::io::Result<()> {
|
||||
let (_srv, _data) = start_server();
|
||||
let mut c = connect()?;
|
||||
for table in ["../__chusql_users", "__CHUSQL_USERS", "__chusql_users."] {
|
||||
for table in ["../__system_users", "__SYSTEM_USERS", "__system_users."] {
|
||||
let result = send(&mut c, &serde_json::json!({"method":"scan", "table":table}).to_string());
|
||||
assert!(result.contains("error"), "{result}");
|
||||
}
|
||||
let result = send(&mut c, r#"{"method":"create_index","table":"business","column":"../__chusql_users"}"#);
|
||||
let result = send(&mut c, r#"{"method":"create_index","table":"business","column":"../__system_users"}"#);
|
||||
assert!(result.contains("invalid column name"), "{result}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -484,7 +678,7 @@ fn account_create_reset_drop_and_hidden_from_ordinary_api() -> Result<(), Box<dy
|
||||
assert_eq!(empty["accounts"], serde_json::json!([]));
|
||||
for method in ["list_tables", "list_catalog"] {
|
||||
let result = send(&mut c, &serde_json::json!({"method":method}).to_string());
|
||||
assert!(!result.contains("__chusql_users"), "{result}");
|
||||
assert!(!result.contains("__system_users"), "{result}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -492,6 +686,7 @@ fn account_create_reset_drop_and_hidden_from_ordinary_api() -> Result<(), Box<dy
|
||||
/// 用同一份数据目录重开一次存储
|
||||
fn restart_at(data: &std::path::Path) -> std::io::Result<ServerProc> {
|
||||
open_storage(&write_config(data));
|
||||
bootstrap_system();
|
||||
create_fixture_database();
|
||||
Ok(ServerProc)
|
||||
}
|
||||
@@ -514,8 +709,8 @@ fn account_snapshot_replays_twice_without_duplicating_accounts() -> Result<(), B
|
||||
wal.truncate()?;
|
||||
wal.append(&WalOp::Accounts { accounts: accounts.clone() })?;
|
||||
drop(wal);
|
||||
std::fs::write(data.path().join("system/__chusql_users.idx"), b"torn index")?;
|
||||
std::fs::write(data.path().join("system/__chusql_users.db"), b"torn heap")?;
|
||||
std::fs::write(data.path().join("system/__system_users.idx"), b"torn index")?;
|
||||
std::fs::write(data.path().join("system/__system_users.db"), b"torn heap")?;
|
||||
let restarted = restart_at(data.path())?;
|
||||
let mut connection = connect()?;
|
||||
let actual: serde_json::Value = serde_json::from_str(&send(&mut connection, r#"{"method":"accounts_list"}"#))?;
|
||||
@@ -555,12 +750,13 @@ fn account_failure_preserves_wal_and_blocks_writes() -> Result<(), Box<dyn std::
|
||||
/// 账号文件已存在时拒绝建账号
|
||||
#[test]
|
||||
fn account_create_rejects_reserved_file_collision() -> std::io::Result<()> {
|
||||
let (_server, data) = start_server();
|
||||
std::fs::write(data.path().join("system/__chusql_users.db"), b"existing")?;
|
||||
let data = tempfile::tempdir()?;
|
||||
open_storage(&write_config(data.path()));
|
||||
std::fs::write(data.path().join("system/__system_users.db"), b"existing")?;
|
||||
let mut c = connect()?;
|
||||
let result = send(&mut c, r#"{"method":"account_create","user":"alice","password_hash":"hash-one"}"#);
|
||||
assert!(result.contains("collision"), "{result}");
|
||||
assert_eq!(std::fs::read(data.path().join("system/__chusql_users.db"))?, b"existing");
|
||||
assert_eq!(std::fs::read(data.path().join("system/__system_users.db"))?, b"existing");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -578,21 +774,98 @@ fn oversized_account_hash_is_rejected_before_wal() -> Result<(), Box<dyn std::er
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 空哈希表示免密,允许建立
|
||||
#[test]
|
||||
fn account_create_accepts_empty_hash() -> std::io::Result<()> {
|
||||
let (_server, _data) = start_server();
|
||||
let mut c = connect()?;
|
||||
let created: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"account_create","user":"root","password_hash":""}"#))?;
|
||||
assert_eq!(created["accounts"][0]["password_hash"], serde_json::json!(""));
|
||||
let listed = send(&mut c, r#"{"method":"accounts_list"}"#);
|
||||
assert!(listed.contains("root"), "{listed}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 字典里已有同名普通表时拒绝建账号
|
||||
#[test]
|
||||
fn account_create_rejects_existing_ordinary_catalog_entry() -> std::io::Result<()> {
|
||||
let data = tempfile::tempdir()?;
|
||||
let mut catalog = chusql_core_storage::catalog::Catalog::default();
|
||||
catalog.create_table("__chusql_users", Vec::new())?;
|
||||
catalog.create_table("__system_users", Vec::new())?;
|
||||
std::fs::create_dir(data.path().join("system"))?;
|
||||
catalog.save(data.path().join("system/catalog.json"))?;
|
||||
let _server = restart_at(data.path())?;
|
||||
open_storage(&write_config(data.path()));
|
||||
let mut c = connect()?;
|
||||
let result = send(&mut c, r#"{"method":"account_create","user":"alice","password_hash":"hash-one"}"#);
|
||||
assert!(result.contains("collision"), "{result}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 没引导过就不能动账号
|
||||
#[test]
|
||||
fn account_requests_require_bootstrap() -> std::io::Result<()> {
|
||||
let data = tempfile::tempdir()?;
|
||||
open_storage(&write_config(data.path()));
|
||||
let mut c = connect()?;
|
||||
let status = send(&mut c, r#"{"method":"system_status"}"#);
|
||||
assert!(status.contains(r#""initialized":false"#), "{status}");
|
||||
for request in [r#"{"method":"accounts_list"}"#,
|
||||
r#"{"method":"account_create","user":"alice","password_hash":"hash-one"}"#] {
|
||||
let result = send(&mut c, request);
|
||||
assert!(result.contains("csql-bootstrap"), "{request}: {result}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 引导幂等:再跑一次不改已有管理员的口令
|
||||
#[test]
|
||||
fn bootstrap_is_idempotent_and_keeps_the_password() -> std::io::Result<()> {
|
||||
let data = tempfile::tempdir()?;
|
||||
open_storage(&write_config(data.path()));
|
||||
let mut c = connect()?;
|
||||
let created = send(&mut c, r#"{"method":"bootstrap_system","user":"root","password_hash":"hash-one"}"#);
|
||||
assert!(created.contains(r#""initialized":true"#), "{created}");
|
||||
let again = send(&mut c, r#"{"method":"bootstrap_system","user":"root","password_hash":"hash-two"}"#);
|
||||
assert!(again.contains(r#""initialized":true"#), "{again}");
|
||||
let listed = send(&mut c, r#"{"method":"accounts_list"}"#);
|
||||
assert!(listed.contains("hash-one"), "{listed}");
|
||||
assert!(!listed.contains("hash-two"), "{listed}");
|
||||
assert_eq!(std::fs::metadata(data.path().join("system/wal.log"))?.len(), 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 引导程序不给已经播过种的表塞新账号
|
||||
#[test]
|
||||
fn bootstrap_does_not_add_an_account_to_a_seeded_table() -> std::io::Result<()> {
|
||||
let data = tempfile::tempdir()?;
|
||||
open_storage(&write_config(data.path()));
|
||||
let mut c = connect()?;
|
||||
send(&mut c, r#"{"method":"bootstrap_system","user":"root","password_hash":"hash-one"}"#);
|
||||
let sneaky = send(&mut c, r#"{"method":"bootstrap_system","user":"eve","password_hash":"raw-hash"}"#);
|
||||
assert!(sneaky.contains(r#""initialized":true"#), "{sneaky}");
|
||||
let listed = send(&mut c, r#"{"method":"accounts_list"}"#);
|
||||
assert!(!listed.contains("eve"), "{listed}");
|
||||
assert!(listed.contains("hash-one"), "{listed}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 空表还能被重新播种:先只建表,再补管理员
|
||||
#[test]
|
||||
fn bootstrap_seeds_an_empty_account_table() -> std::io::Result<()> {
|
||||
let data = tempfile::tempdir()?;
|
||||
open_storage(&write_config(data.path()));
|
||||
let mut c = connect()?;
|
||||
let bare = send(&mut c, r#"{"method":"bootstrap_system"}"#);
|
||||
assert!(bare.contains(r#""initialized":true"#), "{bare}");
|
||||
let empty = send(&mut c, r#"{"method":"accounts_list"}"#);
|
||||
assert!(empty.contains(r#""accounts":[]"#), "{empty}");
|
||||
let seeded = send(&mut c, r#"{"method":"bootstrap_system","user":"root","password_hash":"hash-again"}"#);
|
||||
assert!(seeded.contains(r#""initialized":true"#), "{seeded}");
|
||||
let listed = send(&mut c, r#"{"method":"accounts_list"}"#);
|
||||
assert!(listed.contains("hash-again"), "{listed}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 列出已建的表
|
||||
#[test]
|
||||
fn list_tables_returns_created_tables() {
|
||||
@@ -663,6 +936,38 @@ fn duplicate_id_is_rejected() {
|
||||
assert!(r.contains("duplicate value on the id index"), "got: {}", r);
|
||||
}
|
||||
|
||||
/// 整值浮点与整数是同一个索引键
|
||||
#[test]
|
||||
fn integral_float_uses_the_same_index_key() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_srv, _data) = start_server();
|
||||
let mut c = connect()?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"create_table","table":"float_id","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?;
|
||||
request_ok(&mut c, serde_json::json!({"method":"insert","table":"float_id","row":{"id":7,"name":"A"}}))?;
|
||||
|
||||
let found: serde_json::Value = serde_json::from_str(&send(
|
||||
&mut c,
|
||||
r#"{"method":"lookup_by_index","table":"float_id","key":7.0}"#,
|
||||
))?;
|
||||
assert_eq!(
|
||||
found["rows"],
|
||||
serde_json::json!([{"id":7,"name":"A"}]),
|
||||
"lookup by 7.0: {}",
|
||||
found
|
||||
);
|
||||
|
||||
let dup: serde_json::Value = serde_json::from_str(&send(
|
||||
&mut c,
|
||||
r#"{"method":"insert","table":"float_id","row":{"id":7.0,"name":"B"}}"#,
|
||||
))?;
|
||||
assert_eq!(dup["status"], "error", "7.0 must clash with 7: {}", dup);
|
||||
assert!(
|
||||
dup["message"].as_str().unwrap_or("").contains("duplicate value on the id index"),
|
||||
"got: {}",
|
||||
dup
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 批量插入一次全进
|
||||
#[test]
|
||||
fn insert_batch_writes_all_rows() {
|
||||
@@ -878,6 +1183,7 @@ fn config_file_is_used() {
|
||||
std::fs::write(&config_path, text).unwrap();
|
||||
|
||||
open_storage(&config_path);
|
||||
bootstrap_system();
|
||||
create_fixture_database();
|
||||
|
||||
let mut c = connect().unwrap();
|
||||
@@ -1016,7 +1322,7 @@ fn account_table_uses_typed_columns_and_stamps_login() -> Result<(), Box<dyn std
|
||||
assert!(account["last_login_at"].is_null(), "还没登录过: {}", account);
|
||||
|
||||
let catalog: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(data.path().join("system/catalog.json"))?)?;
|
||||
let entry = &catalog["tables"]["__chusql_users"];
|
||||
let entry = &catalog["tables"]["__system_users"];
|
||||
let columns = entry["columns"].as_array().ok_or("missing columns")?;
|
||||
let names: Vec<&str> = columns.iter().map(|col| col["name"].as_str().unwrap_or("")).collect();
|
||||
assert_eq!(names, vec!["id", "user", "password_hash", "registered_at", "last_login_at", "revision"]);
|
||||
|
||||
Reference in new issue
Block a user