做完rust全链路
This commit is contained in:
1 parent
e428265357
commit
2a4b446252
34 files changed
+1608
-883
No files matched your search
@@ -4,13 +4,16 @@ import ChuSQL.Algebra.Eval (evalRelOp)
|
|||||||
import ChuSQL.Algebra.Optimize (optimize)
|
import ChuSQL.Algebra.Optimize (optimize)
|
||||||
import ChuSQL.Algebra.Planner (translate)
|
import ChuSQL.Algebra.Planner (translate)
|
||||||
import ChuSQL.Model
|
import ChuSQL.Model
|
||||||
import ChuSQL.Syntax.Parser (parseQuery)
|
import ChuSQL.Syntax.Parser (parseStatement)
|
||||||
import Control.Exception (evaluate)
|
import Control.Exception (evaluate)
|
||||||
import Data.IORef (IORef, newIORef, readIORef)
|
import Data.IORef (IORef, newIORef, readIORef)
|
||||||
import System.CPUTime (getCPUTime)
|
import System.CPUTime (getCPUTime)
|
||||||
import System.Environment (getArgs)
|
import System.Environment (getArgs)
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
|
|
||||||
|
-- 性能基准:同一批语句,比较优化前后的耗时。
|
||||||
|
|
||||||
|
-- | 造一张 users 表
|
||||||
mkUsers :: Int -> Table
|
mkUsers :: Int -> Table
|
||||||
mkUsers n =
|
mkUsers n =
|
||||||
Table
|
Table
|
||||||
@@ -22,6 +25,7 @@ mkUsers n =
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- | 造一张 orders 表
|
||||||
mkOrders :: Int -> Int -> Table
|
mkOrders :: Int -> Int -> Table
|
||||||
mkOrders n m =
|
mkOrders n m =
|
||||||
Table
|
Table
|
||||||
@@ -33,37 +37,41 @@ mkOrders n m =
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- | 结果校验和(防被优化掉)
|
||||||
checksum :: Either String [Row] -> Int
|
checksum :: Either String [Row] -> Int
|
||||||
checksum (Left _) = -1
|
checksum (Left _) = -1
|
||||||
checksum (Right rows) = go 0 rows
|
checksum (Right rows) = go 0 rows
|
||||||
where
|
where
|
||||||
|
-- \| 累加行长
|
||||||
go acc [] = acc
|
go acc [] = acc
|
||||||
go acc (r : rs) = go (acc + length r) rs
|
go acc (r : rs) = go (acc + length r) rs
|
||||||
|
|
||||||
|
-- | 结果行数
|
||||||
rowCount :: Either String [Row] -> Int
|
rowCount :: Either String [Row] -> Int
|
||||||
rowCount (Left _) = -1
|
rowCount (Left _) = -1
|
||||||
rowCount (Right rows) = length rows
|
rowCount (Right rows) = length rows
|
||||||
|
|
||||||
-- salt 是从 IORef 里读出来的:有了这个"IO 里来的"依赖,GHC 就没法把整段纯计算
|
-- | 跑一次,返回校验和
|
||||||
-- 提到循环外只算一次 —— 否则批量计时会把 1 次的开销除以 k,测出 0.000 ms 这种假数据
|
|
||||||
runOnce :: IORef String -> Database -> String -> Bool -> IO Int
|
runOnce :: IORef String -> Database -> String -> Bool -> IO Int
|
||||||
runOnce saltRef db sql useOpt = do
|
runOnce saltRef db sql useOpt = do
|
||||||
salt <- readIORef saltRef
|
salt <- readIORef saltRef
|
||||||
let result = case parseQuery (sql ++ salt) >>= translate of
|
let result = case parseStatement (sql ++ salt) >>= translate of
|
||||||
Left e -> Left e
|
Left e -> Left e
|
||||||
Right plan -> evalRelOp db (if useOpt then optimize db plan else plan)
|
Right plan -> evalRelOp db (if useOpt then optimize db plan else plan)
|
||||||
forced <- evaluate (checksum result)
|
forced <- evaluate (checksum result)
|
||||||
return forced
|
return forced
|
||||||
|
|
||||||
|
-- | 跑一次,返回行数
|
||||||
resultRows :: IORef String -> Database -> String -> Bool -> IO Int
|
resultRows :: IORef String -> Database -> String -> Bool -> IO Int
|
||||||
resultRows saltRef db sql useOpt = do
|
resultRows saltRef db sql useOpt = do
|
||||||
salt <- readIORef saltRef
|
salt <- readIORef saltRef
|
||||||
let result = case parseQuery (sql ++ salt) >>= translate of
|
let result = case parseStatement (sql ++ salt) >>= translate of
|
||||||
Left e -> Left e
|
Left e -> Left e
|
||||||
Right plan -> evalRelOp db (if useOpt then optimize db plan else plan)
|
Right plan -> evalRelOp db (if useOpt then optimize db plan else plan)
|
||||||
_ <- evaluate (checksum result)
|
_ <- evaluate (checksum result)
|
||||||
return (rowCount result)
|
return (rowCount result)
|
||||||
|
|
||||||
|
-- | 量一段 IO 的 CPU 时间
|
||||||
timed :: IO a -> IO Double
|
timed :: IO a -> IO Double
|
||||||
timed act = do
|
timed act = do
|
||||||
t0 <- getCPUTime
|
t0 <- getCPUTime
|
||||||
@@ -71,13 +79,16 @@ timed act = do
|
|||||||
t1 <- getCPUTime
|
t1 <- getCPUTime
|
||||||
return (fromIntegral (t1 - t0) / 1e12)
|
return (fromIntegral (t1 - t0) / 1e12)
|
||||||
|
|
||||||
|
-- | 加倍批量次数再折算单次
|
||||||
benchOp :: Double -> IO Int -> IO (Double, Int)
|
benchOp :: Double -> IO Int -> IO (Double, Int)
|
||||||
benchOp target act = loop 1
|
benchOp target act = loop 1
|
||||||
where
|
where
|
||||||
|
-- \| 批次翻倍直到超过目标
|
||||||
loop k = do
|
loop k = do
|
||||||
t <- timed (mapM_ (const act) [1 .. k])
|
t <- timed (mapM_ (const act) [1 .. k])
|
||||||
if t < target then loop (k * 2) else return (t / fromIntegral k, k)
|
if t < target then loop (k * 2) else return (t / fromIntegral k, k)
|
||||||
|
|
||||||
|
-- | 打印一个用例的对比
|
||||||
report :: IORef String -> Database -> Double -> String -> IO ()
|
report :: IORef String -> Database -> Double -> String -> IO ()
|
||||||
report saltRef db target sql = do
|
report saltRef db target sql = do
|
||||||
nUn <- resultRows saltRef db sql False
|
nUn <- resultRows saltRef db sql False
|
||||||
@@ -107,6 +118,7 @@ queries =
|
|||||||
warmupSql :: String
|
warmupSql :: String
|
||||||
warmupSql = "SELECT name FROM users"
|
warmupSql = "SELECT name FROM users"
|
||||||
|
|
||||||
|
-- | 基准入口
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
args <- getArgs
|
args <- getArgs
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import ChuSQL.Model
|
|||||||
import ChuSQL.Syntax.AST
|
import ChuSQL.Syntax.AST
|
||||||
import ChuSQL.Syntax.Parser
|
import ChuSQL.Syntax.Parser
|
||||||
|
|
||||||
|
-- 命令行演示:跑几条语句,打印结果与优化前后的计划。
|
||||||
|
|
||||||
users :: Table
|
users :: Table
|
||||||
users =
|
users =
|
||||||
Table
|
Table
|
||||||
@@ -35,17 +37,18 @@ orders =
|
|||||||
db :: Database
|
db :: Database
|
||||||
db = [("users", users), ("orders", orders)]
|
db = [("users", users), ("orders", orders)]
|
||||||
|
|
||||||
|
-- | 演示入口
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
putStrLn "==================== Engine ====================="
|
putStrLn "==================== Engine ====================="
|
||||||
let q1 = makeSelect ["name"] "users" (Just (Gt (Col "age") (LitInt 18)))
|
let q1 = makeSelect ["name"] "users" (Just (Gt (Col "age") (LitInt 18)))
|
||||||
print (rowsOf (runQuery db q1))
|
print (rowsOf (runStatement db q1))
|
||||||
|
|
||||||
let q2 = makeSelect ["*"] "users" Nothing
|
let q2 = makeSelect ["*"] "users" Nothing
|
||||||
print (rowsOf (runQuery db q2))
|
print (rowsOf (runStatement db q2))
|
||||||
|
|
||||||
let q3 = makeSelect ["name"] "nonexistent" Nothing
|
let q3 = makeSelect ["name"] "nonexistent" Nothing
|
||||||
print (rowsOf (runQuery db q3))
|
print (rowsOf (runStatement db q3))
|
||||||
|
|
||||||
let q4 =
|
let q4 =
|
||||||
makeSelect
|
makeSelect
|
||||||
@@ -57,17 +60,16 @@ main = do
|
|||||||
(Gt (Col "age") (LitInt 30))
|
(Gt (Col "age") (LitInt 30))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print (rowsOf (runQuery db q4))
|
print (rowsOf (runStatement db q4))
|
||||||
|
|
||||||
-- 解析 SQL 文本,再交给执行引擎
|
let parsed = parseStatement "SELECT name FROM users WHERE age > 18 AND name = 'Alice'"
|
||||||
let parsed = parseQuery "SELECT name FROM users WHERE age > 18 AND name = 'Alice'"
|
print (rowsOf (parsed >>= runStatement db))
|
||||||
print (rowsOf (parsed >>= runQuery db))
|
|
||||||
|
|
||||||
putStrLn "\n==================== Optimizer ====================="
|
putStrLn "\n==================== Optimizer ====================="
|
||||||
let planSql =
|
let planSql =
|
||||||
"SELECT u.name, o.product FROM users u \
|
"SELECT u.name, o.product FROM users u \
|
||||||
\JOIN orders o ON u.id = o.user_id WHERE u.age > 18"
|
\JOIN orders o ON u.id = o.user_id WHERE u.age > 18"
|
||||||
case parseQuery planSql of
|
case parseStatement planSql of
|
||||||
Left err -> putStrLn err
|
Left err -> putStrLn err
|
||||||
Right query -> case translate query of
|
Right query -> case translate query of
|
||||||
Left err -> putStrLn err
|
Left err -> putStrLn err
|
||||||
@@ -77,4 +79,4 @@ main = do
|
|||||||
putStrLn "--- plan after optimization ---"
|
putStrLn "--- plan after optimization ---"
|
||||||
putStrLn (renderPlan (optimize db plan))
|
putStrLn (renderPlan (optimize db plan))
|
||||||
putStrLn "--- rows ---"
|
putStrLn "--- rows ---"
|
||||||
print (rowsOf (runQuery db query))
|
print (rowsOf (runStatement db query))
|
||||||
@@ -8,29 +8,31 @@ import ChuSQL.Storage (MonadStorage (..))
|
|||||||
import ChuSQL.Syntax.AST (Expr (..))
|
import ChuSQL.Syntax.AST (Expr (..))
|
||||||
import Control.Monad (filterM)
|
import Control.Monad (filterM)
|
||||||
|
|
||||||
-- 执行:按关系算子树真正算出结果行(Scan 阶段会给列名加上别名前缀)。
|
-- 执行:按算子树算出结果行(Scan 阶段会加别名前缀)。
|
||||||
|
|
||||||
-- 投影:只保留清单里的列;哨兵 "*" 表示全部列。
|
-- * 工具
|
||||||
|
-- | 只保留清单里的列
|
||||||
project :: [String] -> Row -> Row
|
project :: [String] -> Row -> Row
|
||||||
project ["*"] row = row
|
project ["*"] row = row
|
||||||
project cs row = [(c, v) | (c, v) <- row, c `elem` cs]
|
project cs row = [(c, v) | (c, v) <- row, c `elem` cs]
|
||||||
|
|
||||||
-- 给一行里的每个列名加上别名前缀(如 "u.")。
|
-- | 给列名加别名前缀
|
||||||
addPrefix :: String -> Row -> Row
|
addPrefix :: String -> Row -> Row
|
||||||
addPrefix prefix = map (\(k, v) -> (prefix ++ k, v))
|
addPrefix prefix = map (\(k, v) -> (prefix ++ k, v))
|
||||||
|
|
||||||
-- 索引查找的约定:主键列名是 "id"(优化器只在这种情况下才把过滤改写成 Lookup)。
|
-- | 主键匹配条件(id = k)
|
||||||
keyCondition :: Int -> Expr
|
keyCondition :: Int -> Expr
|
||||||
keyCondition k = Eq (Col "id") (LitInt k)
|
keyCondition k = Eq (Col "id") (LitInt k)
|
||||||
|
|
||||||
-- 扫描一张表;有别名就给每个列名加上前缀,JOIN 两边同名列靠它区分。
|
-- | 扫描一张表
|
||||||
evalScan :: Database -> Maybe String -> String -> Either String [Row]
|
evalScan :: Database -> Maybe String -> String -> Either String [Row]
|
||||||
evalScan db mAlias tbl = do
|
evalScan db mAlias tbl = do
|
||||||
table <- lookupTable db tbl
|
table <- lookupTable db tbl
|
||||||
let prefix = maybe "" (++ ".") mAlias
|
let prefix = maybe "" (++ ".") mAlias
|
||||||
Right (map (addPrefix prefix) (tableRows table))
|
Right (map (addPrefix prefix) (tableRows table))
|
||||||
|
|
||||||
-- 执行关系代数运算:Scan / Lookup / Filter / Project / Sort / Limit / Join 各一支。
|
-- * 求值
|
||||||
|
-- | 纯求值(不需要存储)
|
||||||
evalRelOp :: Database -> RelOp -> Either String [Row]
|
evalRelOp :: Database -> RelOp -> Either String [Row]
|
||||||
evalRelOp db (Scan mAlias tbl) = evalScan db mAlias tbl
|
evalRelOp db (Scan mAlias tbl) = evalScan db mAlias tbl
|
||||||
evalRelOp db (Lookup tbl k) = do
|
evalRelOp db (Lookup tbl k) = do
|
||||||
@@ -51,18 +53,18 @@ evalRelOp db (Limit n op) = do
|
|||||||
evalRelOp db (Join left right cond) = do
|
evalRelOp db (Join left right cond) = do
|
||||||
lrows <- evalRelOp db left
|
lrows <- evalRelOp db left
|
||||||
rrows <- evalRelOp db right
|
rrows <- evalRelOp db right
|
||||||
let cross = [l ++ r | l <- lrows, r <- rrows] -- 笛卡尔积
|
let cross = [l ++ r | l <- lrows, r <- rrows]
|
||||||
filterM (evalCondForRow cond) cross
|
filterM (evalCondForRow cond) cross
|
||||||
|
|
||||||
-- | Monadic 版本:Lookup 直接问存储要一行(走索引),其余算子照原样递归。
|
-- | 单子求值:Lookup 问存储
|
||||||
evalRelOpM :: (MonadStorage m) => Database -> RelOp -> m (Either String [Row])
|
evalRelOpM :: (MonadStorage m) => Database -> RelOp -> m (Either String [Row])
|
||||||
|
evalRelOpM db (Scan a t) = pure (evalScan db a t)
|
||||||
evalRelOpM _ (Lookup t k) = do
|
evalRelOpM _ (Lookup t k) = do
|
||||||
result <- lookupByKey t k
|
result <- lookupByKey t k
|
||||||
pure $ case result of
|
pure $ case result of
|
||||||
Left e -> Left e
|
Left e -> Left e
|
||||||
Right Nothing -> Right []
|
Right Nothing -> Right []
|
||||||
Right (Just r) -> Right [r]
|
Right (Just r) -> Right [r]
|
||||||
evalRelOpM db (Scan a t) = pure (evalScan db a t)
|
|
||||||
evalRelOpM db (Filter p x) = do
|
evalRelOpM db (Filter p x) = do
|
||||||
result <- evalRelOpM db x
|
result <- evalRelOpM db x
|
||||||
pure $ do
|
pure $ do
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import ChuSQL.Model
|
|||||||
import ChuSQL.Syntax.AST
|
import ChuSQL.Syntax.AST
|
||||||
import Control.Monad (join)
|
import Control.Monad (join)
|
||||||
|
|
||||||
-- 表达式求值:在一行(列名 → 值)上算出单个值;另外提供"静态收集表达式里用到哪些列"的工具。
|
-- 表达式求值:拿一行数据算出一个值。
|
||||||
|
|
||||||
-- 表达式求值
|
-- * 求值
|
||||||
|
-- | 在一行上求出表达式的值
|
||||||
evalExpr :: Expr -> Row -> Either String Value
|
evalExpr :: Expr -> Row -> Either String Value
|
||||||
evalExpr (Col name) row =
|
evalExpr (Col name) row =
|
||||||
maybe (Left ("unknown column: " ++ name)) Right (lookup name row)
|
maybe (Left ("unknown column: " ++ name)) Right (lookup name row)
|
||||||
@@ -19,7 +20,8 @@ evalExpr (Eq a b) row = liftBinOp eqOp a b row
|
|||||||
evalExpr (And a b) row = liftBinOp (boolOp (&&)) a b row
|
evalExpr (And a b) row = liftBinOp (boolOp (&&)) a b row
|
||||||
evalExpr (Or a b) row = liftBinOp (boolOp (||)) a b row
|
evalExpr (Or a b) row = liftBinOp (boolOp (||)) a b row
|
||||||
|
|
||||||
-- 收集表达式里用到的列名
|
-- * 列引用
|
||||||
|
-- | 表达式用到了哪些列
|
||||||
colsInExpr :: Expr -> [String]
|
colsInExpr :: Expr -> [String]
|
||||||
colsInExpr (Col c) = [c]
|
colsInExpr (Col c) = [c]
|
||||||
colsInExpr (Gt a b) = colsInExpr a ++ colsInExpr b
|
colsInExpr (Gt a b) = colsInExpr a ++ colsInExpr b
|
||||||
@@ -29,7 +31,7 @@ colsInExpr (And a b) = colsInExpr a ++ colsInExpr b
|
|||||||
colsInExpr (Or a b) = colsInExpr a ++ colsInExpr b
|
colsInExpr (Or a b) = colsInExpr a ++ colsInExpr b
|
||||||
colsInExpr _ = []
|
colsInExpr _ = []
|
||||||
|
|
||||||
-- 对行计算条件语句
|
-- | 判断条件真假
|
||||||
evalCondForRow :: Expr -> Row -> Either String Bool
|
evalCondForRow :: Expr -> Row -> Either String Bool
|
||||||
evalCondForRow e row = do
|
evalCondForRow e row = do
|
||||||
v <- evalExpr e row
|
v <- evalExpr e row
|
||||||
@@ -37,7 +39,8 @@ evalCondForRow e row = do
|
|||||||
VBool b -> Right b
|
VBool b -> Right b
|
||||||
_ -> Left "type error: WHERE condition must be a boolean"
|
_ -> Left "type error: WHERE condition must be a boolean"
|
||||||
|
|
||||||
-- 固定模式:对两个子表达式求值,并返还给二元操作符
|
-- * 内部
|
||||||
|
-- | 把二元运算提升到 Either
|
||||||
liftBinOp ::
|
liftBinOp ::
|
||||||
(Value -> Value -> Either String Value) ->
|
(Value -> Value -> Either String Value) ->
|
||||||
Expr ->
|
Expr ->
|
||||||
@@ -46,16 +49,16 @@ liftBinOp ::
|
|||||||
Either String Value
|
Either String Value
|
||||||
liftBinOp f a b row = join (f <$> evalExpr a row <*> evalExpr b row)
|
liftBinOp f a b row = join (f <$> evalExpr a row <*> evalExpr b row)
|
||||||
|
|
||||||
-- 整数比较
|
-- | 整数二元运算
|
||||||
intOp :: (Int -> Int -> Bool) -> Value -> Value -> Either String Value
|
intOp :: (Int -> Int -> Bool) -> Value -> Value -> Either String Value
|
||||||
intOp f (VInt x) (VInt y) = Right (VBool (f x y))
|
intOp f (VInt x) (VInt y) = Right (VBool (f x y))
|
||||||
intOp _ _ _ = Left "type error: expected two integers"
|
intOp _ _ _ = Left "type error: expected two integers"
|
||||||
|
|
||||||
-- 布尔运算
|
-- | 布尔二元运算
|
||||||
boolOp :: (Bool -> Bool -> Bool) -> Value -> Value -> Either String Value
|
boolOp :: (Bool -> Bool -> Bool) -> Value -> Value -> Either String Value
|
||||||
boolOp f (VBool x) (VBool y) = Right (VBool (f x y))
|
boolOp f (VBool x) (VBool y) = Right (VBool (f x y))
|
||||||
boolOp _ _ _ = Left "type error: expected two booleans"
|
boolOp _ _ _ = Left "type error: expected two booleans"
|
||||||
|
|
||||||
-- 相等比较
|
-- | 相等比较
|
||||||
eqOp :: Value -> Value -> Either String Value
|
eqOp :: Value -> Value -> Either String Value
|
||||||
eqOp x y = Right (VBool (x == y))
|
eqOp x y = Right (VBool (x == y))
|
||||||
@@ -2,9 +2,9 @@ module ChuSQL.Algebra.Op (RelOp (..), renderPlan) where
|
|||||||
|
|
||||||
import ChuSQL.Syntax.AST (Expr, SortDir)
|
import ChuSQL.Syntax.AST (Expr, SortDir)
|
||||||
|
|
||||||
-- 关系代数算子:执行计划用 RelOp 表示;另外提供一个把计划打印成缩进文本的辅助函数。
|
-- 关系代数算子,以及把算子树渲染成文本。
|
||||||
|
|
||||||
-- 关系代数算子
|
-- | 算子树的一个节点
|
||||||
data RelOp
|
data RelOp
|
||||||
= Scan (Maybe String) String
|
= Scan (Maybe String) String
|
||||||
| Lookup String Int
|
| Lookup String Int
|
||||||
@@ -15,13 +15,13 @@ data RelOp
|
|||||||
| Join RelOp RelOp Expr
|
| Join RelOp RelOp Expr
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- 把算子树画成缩进文本(演示和调试用)
|
-- | 渲染成缩进文本
|
||||||
renderPlan :: RelOp -> String
|
renderPlan :: RelOp -> String
|
||||||
renderPlan = go 0
|
renderPlan = go 0
|
||||||
where
|
where
|
||||||
go ind op = case op of
|
go ind op = case op of
|
||||||
Lookup t k -> "Lookup " ++ show t ++ " " ++ show k
|
|
||||||
Scan a t -> pad ++ "Scan " ++ show a ++ " " ++ show t
|
Scan a t -> pad ++ "Scan " ++ show a ++ " " ++ show t
|
||||||
|
Lookup t k -> pad ++ "Lookup " ++ show t ++ " " ++ show k
|
||||||
Filter e x -> pad ++ "Filter " ++ show e ++ "\n" ++ go (ind + 1) x
|
Filter e x -> pad ++ "Filter " ++ show e ++ "\n" ++ go (ind + 1) x
|
||||||
Project c x -> pad ++ "Project " ++ show c ++ "\n" ++ go (ind + 1) x
|
Project c x -> pad ++ "Project " ++ show c ++ "\n" ++ go (ind + 1) x
|
||||||
Sort s x -> pad ++ "Sort " ++ show s ++ "\n" ++ go (ind + 1) x
|
Sort s x -> pad ++ "Sort " ++ show s ++ "\n" ++ go (ind + 1) x
|
||||||
|
|||||||
@@ -5,42 +5,41 @@ import ChuSQL.Algebra.Op (RelOp (..))
|
|||||||
import ChuSQL.Model
|
import ChuSQL.Model
|
||||||
import ChuSQL.Syntax.AST (Expr (..))
|
import ChuSQL.Syntax.AST (Expr (..))
|
||||||
|
|
||||||
-- 查询优化:谓词下推 + 投影下推 + 常量折叠,反复应用规则直到算子树形状不再变化。
|
-- 查询优化:谓词下推 + 投影下推 + 常量折叠,跑到不动点。
|
||||||
|
|
||||||
-- * 谓词下推
|
-- * 谓词下推
|
||||||
|
-- | 算子会产出哪些列
|
||||||
-- | 一个算子会产出哪些列;返回 ["*"] 表示"全部列,不用挑"
|
|
||||||
relOpCols :: Database -> RelOp -> [String]
|
relOpCols :: Database -> RelOp -> [String]
|
||||||
relOpCols db (Filter _ x) = relOpCols db x
|
|
||||||
relOpCols db (Sort _ x) = relOpCols db x
|
|
||||||
relOpCols db (Limit _ x) = relOpCols db x
|
|
||||||
relOpCols _ (Project ["*"] _) = ["*"]
|
|
||||||
relOpCols _ (Project cols _) = cols
|
|
||||||
relOpCols db (Join l r _) = relOpCols db l ++ relOpCols db r
|
|
||||||
relOpCols db (Lookup tbl _) = relOpCols db (Scan Nothing tbl)
|
|
||||||
relOpCols db (Scan mAlias tbl) =
|
relOpCols db (Scan mAlias tbl) =
|
||||||
case lookup tbl db of
|
case lookup tbl db of
|
||||||
Nothing -> []
|
Nothing -> []
|
||||||
Just t -> map (prefix ++) (colNames t)
|
Just t -> map (prefix ++) (colNames t)
|
||||||
where
|
where
|
||||||
prefix = maybe "" (++ ".") mAlias
|
prefix = maybe "" (++ ".") mAlias
|
||||||
|
relOpCols db (Lookup tbl _) = relOpCols db (Scan Nothing tbl)
|
||||||
|
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 里找到;have 为 ["*"] 时一律为真
|
-- | need 是否都在 have 里
|
||||||
isSubsetOf :: [String] -> [String] -> Bool
|
isSubsetOf :: [String] -> [String] -> Bool
|
||||||
isSubsetOf _ ["*"] = True
|
isSubsetOf _ ["*"] = True
|
||||||
isSubsetOf need have = all (`elem` have) need
|
isSubsetOf need have = all (`elem` have) need
|
||||||
|
|
||||||
-- | 把一个用 AND 串起来的条件拆成清单:a AND b AND c 变成 [a, b, c]
|
-- | 把 AND 链拆成清单
|
||||||
splitConj :: Expr -> [Expr]
|
splitConj :: Expr -> [Expr]
|
||||||
splitConj (And a b) = splitConj a ++ splitConj b
|
splitConj (And a b) = splitConj a ++ splitConj b
|
||||||
splitConj e = [e]
|
splitConj e = [e]
|
||||||
|
|
||||||
-- | 反过来:把条件清单用 AND 重新串成一个;空清单表示"没有条件",返回 Nothing
|
-- | 把条件清单用 AND 串起来
|
||||||
combineConj :: [Expr] -> Maybe Expr
|
combineConj :: [Expr] -> Maybe Expr
|
||||||
combineConj [] = Nothing
|
combineConj [] = Nothing
|
||||||
combineConj (e : es) = Just (foldl And e es)
|
combineConj (e : es) = Just (foldl And e es)
|
||||||
|
|
||||||
-- | 把条件按"只用到哪一边的列"分成两堆:能下推过去的 / 必须留在原地的
|
-- | 条件按列归属分两边
|
||||||
partitionPred :: [String] -> [Expr] -> ([Expr], [Expr])
|
partitionPred :: [String] -> [Expr] -> ([Expr], [Expr])
|
||||||
partitionPred have = foldr pick ([], [])
|
partitionPred have = foldr pick ([], [])
|
||||||
where
|
where
|
||||||
@@ -48,12 +47,12 @@ partitionPred have = foldr pick ([], [])
|
|||||||
| colsInExpr e `isSubsetOf` have = (e : yes, no)
|
| colsInExpr e `isSubsetOf` have = (e : yes, no)
|
||||||
| otherwise = (yes, e : no)
|
| otherwise = (yes, e : no)
|
||||||
|
|
||||||
-- | 把一组条件依次套到算子上,一个条件一层 Filter
|
-- | 依次套上若干 Filter
|
||||||
applyFilters :: [Expr] -> RelOp -> RelOp
|
applyFilters :: [Expr] -> RelOp -> RelOp
|
||||||
applyFilters [] op = op
|
applyFilters [] op = op
|
||||||
applyFilters (e : es) op = applyFilters es (Filter e op)
|
applyFilters (e : es) op = applyFilters es (Filter e op)
|
||||||
|
|
||||||
-- | Filter 压在 Join 上时的下推规则:条件按列归属拆到左右两侧,跨两边的才留在 Join 上方
|
-- | Filter 压在 Join 上时下推
|
||||||
pushJoin :: Database -> Expr -> RelOp -> RelOp -> Expr -> RelOp
|
pushJoin :: Database -> Expr -> RelOp -> RelOp -> Expr -> RelOp
|
||||||
pushJoin db p l r c =
|
pushJoin db p l r c =
|
||||||
let preds = splitConj p
|
let preds = splitConj p
|
||||||
@@ -68,26 +67,26 @@ pushJoin db p l r c =
|
|||||||
Nothing -> joined
|
Nothing -> joined
|
||||||
Just p' -> Filter p' joined
|
Just p' -> Filter p' joined
|
||||||
|
|
||||||
-- | Filter 压在 Filter / Sort 上时的下推规则:合并上下两层 Filter,或把 Filter 挪到 Sort 下面
|
-- | 合并两层 Filter 或挪到 Sort 下
|
||||||
pushOne :: Database -> RelOp -> RelOp
|
pushOne :: Database -> RelOp -> RelOp
|
||||||
pushOne _ (Filter p (Filter q x)) = Filter (And p q) x
|
pushOne _ (Filter p (Filter q x)) = Filter (And p q) x
|
||||||
pushOne _ (Filter p (Sort spec x)) = Sort spec (Filter p x)
|
pushOne _ (Filter p (Sort spec x)) = Sort spec (Filter p x)
|
||||||
pushOne _ op = op
|
pushOne _ op = op
|
||||||
|
|
||||||
-- * 投影下推
|
|
||||||
|
|
||||||
-- | 是不是要拿全部列(need 里含哨兵 "*")。
|
-- * 投影下推
|
||||||
|
-- | 是否要全部列
|
||||||
needsAll :: [String] -> Bool
|
needsAll :: [String] -> Bool
|
||||||
needsAll = elem allColumns
|
needsAll = elem allColumns
|
||||||
|
|
||||||
-- | 叶子算子(扫描 / 索引查找)的投影下推:需要全部列就原样保留,缺列才套一层 Project
|
-- | 叶子算子的投影下推
|
||||||
pushProjectLeaf :: Database -> [String] -> RelOp -> RelOp
|
pushProjectLeaf :: Database -> [String] -> RelOp -> RelOp
|
||||||
pushProjectLeaf db need leaf
|
pushProjectLeaf db need leaf
|
||||||
| needsAll need = leaf
|
| needsAll need = leaf
|
||||||
| all (`elem` need) (relOpCols db leaf) = leaf
|
| all (`elem` need) (relOpCols db leaf) = leaf
|
||||||
| otherwise = Project need leaf
|
| otherwise = Project need leaf
|
||||||
|
|
||||||
-- | 投影下推:把"上层需要哪些列"一路往下传,JOIN 两侧各自只保留自己需要的列。
|
-- | 把需要哪些列往下传
|
||||||
pushProject :: Database -> [String] -> RelOp -> RelOp
|
pushProject :: Database -> [String] -> RelOp -> RelOp
|
||||||
pushProject db _ (Project cols x) = case pushProject db cols x of
|
pushProject db _ (Project cols x) = case pushProject db cols x of
|
||||||
Project cols' y | cols' == cols -> Project cols y
|
Project cols' y | cols' == cols -> Project cols y
|
||||||
@@ -107,9 +106,9 @@ pushProject db need (Join l r c)
|
|||||||
pushProject db need leaf@(Scan _ _) = pushProjectLeaf db need leaf
|
pushProject db need leaf@(Scan _ _) = pushProjectLeaf db need leaf
|
||||||
pushProject db need leaf@(Lookup _ _) = pushProjectLeaf db need leaf
|
pushProject db need leaf@(Lookup _ _) = pushProjectLeaf db need leaf
|
||||||
|
|
||||||
-- * 常量折叠
|
|
||||||
|
|
||||||
-- | 折一个表达式:不含列就直接求值替换成字面量,含列或求值失败则原样返回
|
-- * 常量折叠
|
||||||
|
-- | 折一个表达式节点
|
||||||
foldNode :: Expr -> Expr
|
foldNode :: Expr -> Expr
|
||||||
foldNode e
|
foldNode e
|
||||||
| null (colsInExpr e) = case evalExpr e [] of
|
| null (colsInExpr e) = case evalExpr e [] of
|
||||||
@@ -119,7 +118,7 @@ foldNode e
|
|||||||
Left _ -> e
|
Left _ -> e
|
||||||
| otherwise = e
|
| otherwise = e
|
||||||
|
|
||||||
-- | 自底向上折叠整个表达式:先把子表达式折干净,再折自己
|
-- | 自底向上折整棵表达式
|
||||||
foldConstants :: Expr -> Expr
|
foldConstants :: Expr -> Expr
|
||||||
foldConstants (Gt a b) = foldNode (Gt (foldConstants a) (foldConstants b))
|
foldConstants (Gt a b) = foldNode (Gt (foldConstants a) (foldConstants b))
|
||||||
foldConstants (Lt a b) = foldNode (Lt (foldConstants a) (foldConstants b))
|
foldConstants (Lt a b) = foldNode (Lt (foldConstants a) (foldConstants b))
|
||||||
@@ -128,9 +127,9 @@ foldConstants (And a b) = foldNode (And (foldConstants a) (foldConstants b))
|
|||||||
foldConstants (Or a b) = foldNode (Or (foldConstants a) (foldConstants b))
|
foldConstants (Or a b) = foldNode (Or (foldConstants a) (foldConstants b))
|
||||||
foldConstants e = e
|
foldConstants e = e
|
||||||
|
|
||||||
-- * 执行优化
|
|
||||||
|
|
||||||
-- | 单节点重写:把 Filter 分派给上面几条规则,另外负责恒真条件删除与恒等投影消除
|
-- * 执行优化
|
||||||
|
-- | 单节点重写
|
||||||
rewriteNode :: Database -> RelOp -> RelOp
|
rewriteNode :: Database -> RelOp -> RelOp
|
||||||
rewriteNode _ (Filter (Eq (Col k) (LitInt v)) (Scan _ t))
|
rewriteNode _ (Filter (Eq (Col k) (LitInt v)) (Scan _ t))
|
||||||
| k == "id" = Lookup t v
|
| k == "id" = Lookup t v
|
||||||
@@ -141,7 +140,7 @@ rewriteNode db (Filter p x) = case foldConstants p of
|
|||||||
rewriteNode _ (Project ["*"] x) = x
|
rewriteNode _ (Project ["*"] x) = x
|
||||||
rewriteNode db op = pushOne db op
|
rewriteNode db op = pushOne db op
|
||||||
|
|
||||||
-- | 自底向上遍历整棵算子树,每个节点套一次 rewriteNode
|
-- | 自底向上遍历一次
|
||||||
optimizeRelOp :: Database -> RelOp -> RelOp
|
optimizeRelOp :: Database -> RelOp -> RelOp
|
||||||
optimizeRelOp db = rewriteNode db . descend
|
optimizeRelOp db = rewriteNode db . descend
|
||||||
where
|
where
|
||||||
@@ -152,12 +151,12 @@ optimizeRelOp db = rewriteNode db . descend
|
|||||||
descend (Join l r c) = Join (optimizeRelOp db l) (optimizeRelOp db r) c
|
descend (Join l r c) = Join (optimizeRelOp db l) (optimizeRelOp db r) c
|
||||||
descend x = x
|
descend x = x
|
||||||
|
|
||||||
-- | 谓词下推 + 常量折叠:反复应用规则,直到算子树的形状不再变化(到不动点为止)
|
-- | 反复跑到不动点
|
||||||
optimizePredicates :: Database -> RelOp -> RelOp
|
optimizePredicates :: Database -> RelOp -> RelOp
|
||||||
optimizePredicates db op =
|
optimizePredicates db op =
|
||||||
let op' = optimizeRelOp db op
|
let op' = optimizeRelOp db op
|
||||||
in if op' == op then op else optimizePredicates db op'
|
in if op' == op then op else optimizePredicates db op'
|
||||||
|
|
||||||
-- | 优化器总入口:先把谓词下推与常量折叠跑到不动点,再做一次投影下推
|
-- | 优化总入口
|
||||||
optimize :: Database -> RelOp -> RelOp
|
optimize :: Database -> RelOp -> RelOp
|
||||||
optimize db op = pushProject db ["*"] (optimizePredicates db op)
|
optimize db op = pushProject db ["*"] (optimizePredicates db op)
|
||||||
@@ -3,16 +3,16 @@ module ChuSQL.Algebra.Planner (translate) where
|
|||||||
import ChuSQL.Algebra.Op
|
import ChuSQL.Algebra.Op
|
||||||
import ChuSQL.Syntax.AST
|
import ChuSQL.Syntax.AST
|
||||||
|
|
||||||
-- 计划翻译:把 AST 里的 SELECT 翻译成关系代数算子树(只翻译,不做合法性检查)。
|
-- 计划生成:把语句翻译成算子树。
|
||||||
|
|
||||||
-- 把 FROM 从句转成 Scan / Join 算子。
|
-- | 把 FROM 子句翻成 Scan / Join
|
||||||
fromToRelOp :: FromClause -> RelOp
|
fromToRelOp :: FromClause -> RelOp
|
||||||
fromToRelOp (FromTable mAlias tbl) = Scan mAlias tbl
|
fromToRelOp (FromTable mAlias tbl) = Scan mAlias tbl
|
||||||
fromToRelOp (FromJoin left mAlias tbl cond) =
|
fromToRelOp (FromJoin left mAlias tbl cond) =
|
||||||
Join (fromToRelOp left) (Scan mAlias tbl) cond
|
Join (fromToRelOp left) (Scan mAlias tbl) cond
|
||||||
|
|
||||||
-- 翻译入口:把 SELECT 翻译成算子树;其它语句类型不支持(返回 Left)。
|
-- | 翻成:过滤 -> 排序 -> 截断 -> 取列
|
||||||
translate :: Query -> Either String RelOp
|
translate :: Statement -> Either String RelOp
|
||||||
translate q = case q of
|
translate q = case q of
|
||||||
Select
|
Select
|
||||||
{ selectCols = cols
|
{ selectCols = cols
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ import ChuSQL.Model
|
|||||||
import ChuSQL.Syntax.AST
|
import ChuSQL.Syntax.AST
|
||||||
import Data.List (sortBy)
|
import Data.List (sortBy)
|
||||||
|
|
||||||
-- 排序:按 ORDER BY 的列清单逐列比较(支持 ASC / DESC),供执行 Sort 算子时使用。
|
-- 排序:按多列排序,每列可升可降。
|
||||||
|
|
||||||
-- 反向排序
|
-- | 反转比较结果
|
||||||
flipOrdering :: Ordering -> Ordering
|
flipOrdering :: Ordering -> Ordering
|
||||||
flipOrdering LT = GT
|
flipOrdering LT = GT
|
||||||
flipOrdering GT = LT
|
flipOrdering GT = LT
|
||||||
flipOrdering EQ = EQ
|
flipOrdering EQ = EQ
|
||||||
|
|
||||||
-- 定义值的比较规则
|
-- | 比较两个值
|
||||||
compareValue :: Value -> Value -> Ordering
|
compareValue :: Value -> Value -> Ordering
|
||||||
compareValue (VInt a) (VInt b) = compare a b
|
compareValue (VInt a) (VInt b) = compare a b
|
||||||
compareValue (VStr a) (VStr b) = compare a b
|
compareValue (VStr a) (VStr b) = compare a b
|
||||||
compareValue (VBool a) (VBool b) = compare a b
|
compareValue (VBool a) (VBool b) = compare a b
|
||||||
compareValue _ _ = EQ
|
compareValue _ _ = EQ
|
||||||
|
|
||||||
-- 定义行的比较规则
|
-- | 按排序要求比较两行
|
||||||
compareRows :: [(String, SortDir)] -> Row -> Row -> Ordering
|
compareRows :: [(String, SortDir)] -> Row -> Row -> Ordering
|
||||||
compareRows [] _ _ = EQ
|
compareRows [] _ _ = EQ
|
||||||
compareRows ((col, dir) : rest) r1 r2 =
|
compareRows ((col, dir) : rest) r1 r2 =
|
||||||
@@ -33,7 +33,7 @@ compareRows ((col, dir) : rest) r1 r2 =
|
|||||||
else o
|
else o
|
||||||
_ -> compareRows rest r1 r2
|
_ -> compareRows rest r1 r2
|
||||||
|
|
||||||
-- 将表的每行排序
|
-- | 排序若干行
|
||||||
sortRows :: [(String, SortDir)] -> [Row] -> [Row]
|
sortRows :: [(String, SortDir)] -> [Row] -> [Row]
|
||||||
sortRows [] rows = rows
|
sortRows [] rows = rows
|
||||||
sortRows spec rows = sortBy (compareRows spec) rows
|
sortRows spec rows = sortBy (compareRows spec) rows
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module ChuSQL.Engine (runQuery, runQueryM, rowsOf) where
|
module ChuSQL.Engine (runStatement, runStatementM, rowsOf) where
|
||||||
|
|
||||||
import ChuSQL.Algebra.Eval (evalRelOpM)
|
import ChuSQL.Algebra.Eval (evalRelOpM)
|
||||||
import ChuSQL.Algebra.Expr (evalCondForRow, evalExpr)
|
import ChuSQL.Algebra.Expr (evalCondForRow, evalExpr)
|
||||||
@@ -11,37 +11,39 @@ import ChuSQL.Storage.Memory (MemoryStorage (runMemoryStorage))
|
|||||||
import ChuSQL.Syntax.AST
|
import ChuSQL.Syntax.AST
|
||||||
import Control.Monad (filterM)
|
import Control.Monad (filterM)
|
||||||
|
|
||||||
-- | 对外入口:签名不变,内部走内存实现。
|
-- 引擎入口:语义检查 + 分发执行,以及内存实现与泛型版本。
|
||||||
runQuery :: Database -> Query -> Either String (Database, [Row])
|
|
||||||
runQuery db q = do
|
-- * 对外入口
|
||||||
(result, db') <- runMemoryStorage (runQueryM q) db
|
-- | 跑一条语句(内存实现)
|
||||||
|
runStatement :: Database -> Statement -> Either String (Database, [Row])
|
||||||
|
runStatement db q = do
|
||||||
|
(result, db') <- runMemoryStorage (runStatementM q) db
|
||||||
rows <- result
|
rows <- result
|
||||||
Right (db', rows)
|
Right (db', rows)
|
||||||
|
|
||||||
-- | 只要结果行(丢掉更新后的数据库),给演示和测试用。
|
-- | 只要结果行
|
||||||
rowsOf :: Either String (Database, [Row]) -> Either String [Row]
|
rowsOf :: Either String (Database, [Row]) -> Either String [Row]
|
||||||
rowsOf = fmap snd
|
rowsOf = fmap snd
|
||||||
|
|
||||||
-- * 泛型入口:走 MonadStorage,未来 IPCStorage 复用同一份代码
|
|
||||||
|
|
||||||
-- | 语义检查 + 分发到具体语句。
|
-- | 泛型入口:先检查再执行
|
||||||
runQueryM :: (MonadStorage m) => Query -> m (Either String [Row])
|
runStatementM :: (MonadStorage m) => Statement -> m (Either String [Row])
|
||||||
runQueryM q = do
|
runStatementM q = do
|
||||||
db <- snapshot
|
db <- snapshot
|
||||||
case check db q of
|
case check db q of
|
||||||
Left err -> pure (Left err)
|
Left err -> pure (Left err)
|
||||||
Right _ -> runQueryUncheckedM q
|
Right _ -> runStatementUncheckedM q
|
||||||
|
|
||||||
-- | SELECT:拿快照,跑 translate → optimize → evalRelOp。
|
-- * 分发执行
|
||||||
runQueryUncheckedM :: (MonadStorage m) => Query -> m (Either String [Row])
|
-- | 按语句类型分发(已检查过)
|
||||||
runQueryUncheckedM q@Select{} = do
|
runStatementUncheckedM :: (MonadStorage m) => Statement -> m (Either String [Row])
|
||||||
|
runStatementUncheckedM q@Select{} = do
|
||||||
db <- snapshot
|
db <- snapshot
|
||||||
case translate q of
|
case translate q of
|
||||||
Left e -> pure (Left e)
|
Left e -> pure (Left e)
|
||||||
Right relOp -> evalRelOpM db (optimize db relOp)
|
Right relOp -> evalRelOpM db (optimize db relOp)
|
||||||
|
|
||||||
-- \| INSERT:求值后追加一行。
|
runStatementUncheckedM (Insert tbl cols vals) =
|
||||||
runQueryUncheckedM (Insert tbl cols vals) =
|
|
||||||
case mapM (\e -> evalExpr e []) vals of
|
case mapM (\e -> evalExpr e []) vals of
|
||||||
Left err -> pure (Left err)
|
Left err -> pure (Left err)
|
||||||
Right values
|
Right values
|
||||||
@@ -51,8 +53,7 @@ runQueryUncheckedM (Insert tbl cols vals) =
|
|||||||
result <- insert tbl (zip cols values)
|
result <- insert tbl (zip cols values)
|
||||||
pure (result >> Right [])
|
pure (result >> Right [])
|
||||||
|
|
||||||
-- \| DELETE:扫表、筛掉命中行、整体写回。
|
runStatementUncheckedM (Delete tbl mWhere) = do
|
||||||
runQueryUncheckedM (Delete tbl mWhere) = do
|
|
||||||
rowsResult <- scan tbl
|
rowsResult <- scan tbl
|
||||||
case rowsResult of
|
case rowsResult of
|
||||||
Left err -> pure (Left err)
|
Left err -> pure (Left err)
|
||||||
@@ -66,12 +67,11 @@ runQueryUncheckedM (Delete tbl mWhere) = do
|
|||||||
result <- replaceAll tbl keptRows
|
result <- replaceAll tbl keptRows
|
||||||
pure (result >> Right [])
|
pure (result >> Right [])
|
||||||
where
|
where
|
||||||
-- \| 保留 = 条件不成立。
|
-- \| 保留 = 条件不成立
|
||||||
shouldKeep :: Expr -> Row -> Either String Bool
|
shouldKeep :: Expr -> Row -> Either String Bool
|
||||||
shouldKeep e row = not <$> evalCondForRow e row
|
shouldKeep e row = not <$> evalCondForRow e row
|
||||||
|
|
||||||
-- \| UPDATE:扫表、逐行改、整体写回。
|
runStatementUncheckedM (Update tbl assigns mWhere) = do
|
||||||
runQueryUncheckedM (Update tbl assigns mWhere) = do
|
|
||||||
rowsResult <- scan tbl
|
rowsResult <- scan tbl
|
||||||
case rowsResult of
|
case rowsResult of
|
||||||
Left err -> pure (Left err)
|
Left err -> pure (Left err)
|
||||||
@@ -83,7 +83,7 @@ runQueryUncheckedM (Update tbl assigns mWhere) = do
|
|||||||
result <- replaceAll tbl rs
|
result <- replaceAll tbl rs
|
||||||
pure (result >> Right [])
|
pure (result >> Right [])
|
||||||
where
|
where
|
||||||
-- \| 命中条件就套用赋值,否则原样返回。
|
-- \| 命中条件就套用赋值
|
||||||
updateRow :: Maybe Expr -> [(String, Expr)] -> Row -> Either String Row
|
updateRow :: Maybe Expr -> [(String, Expr)] -> Row -> Either String Row
|
||||||
updateRow cond asgns row = do
|
updateRow cond asgns row = do
|
||||||
keep <- case cond of
|
keep <- case cond of
|
||||||
@@ -91,7 +91,15 @@ runQueryUncheckedM (Update tbl assigns mWhere) = do
|
|||||||
Just e -> evalCondForRow e row
|
Just e -> evalCondForRow e row
|
||||||
if keep then applyUpdates asgns row else Right row
|
if keep then applyUpdates asgns row else Right row
|
||||||
|
|
||||||
-- | 按 assignments 依次求值并覆盖对应列。
|
runStatementUncheckedM (CreateTable name cols) = do
|
||||||
|
result <- createTable name cols
|
||||||
|
pure (result >> Right [])
|
||||||
|
runStatementUncheckedM (DropTable name) = do
|
||||||
|
result <- dropTable name
|
||||||
|
pure (result >> Right [])
|
||||||
|
|
||||||
|
-- * 更新辅助
|
||||||
|
-- | 依次求值并覆盖列
|
||||||
applyUpdates :: [(String, Expr)] -> Row -> Either String Row
|
applyUpdates :: [(String, Expr)] -> Row -> Either String Row
|
||||||
applyUpdates [] row = Right row
|
applyUpdates [] row = Right row
|
||||||
applyUpdates ((col, e) : rest) row = do
|
applyUpdates ((col, e) : rest) row = do
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
module ChuSQL.Model (Column (..), Value (..), Row, Table (..), Database, colNames, colType, lookupTable, allColumns) where
|
module ChuSQL.Model (Column (..), Value (..), Row, Table (..), Database, colNames, colType, lookupTable, allColumns) where
|
||||||
|
|
||||||
-- 数据模型:列类型、值、行、表、数据库,外加"按名查表 / 查列类型"这类通用小工具。
|
-- 数据模型:列类型、值、行、表、数据库,外加几个查表小工具。
|
||||||
|
|
||||||
-- 列的类型
|
-- * 类型
|
||||||
|
-- | 列的类型
|
||||||
data Column
|
data Column
|
||||||
= TInt
|
= TInt
|
||||||
| TStr
|
| TStr
|
||||||
| TBool
|
| TBool
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- 列的值:整数、字符串、布尔
|
-- | 列的值
|
||||||
data Value
|
data Value
|
||||||
= VInt Int
|
= VInt Int
|
||||||
| VStr String
|
| VStr String
|
||||||
| VBool Bool
|
| VBool Bool
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- 一行:列名 → 值
|
-- | 一行:列名 -> 值
|
||||||
type Row = [(String, Value)]
|
type Row = [(String, Value)]
|
||||||
|
|
||||||
-- 一张表
|
-- | 一张表
|
||||||
data Table = Table
|
data Table = Table
|
||||||
{ tableName :: String
|
{ tableName :: String
|
||||||
, tableCols :: [(String, Column)]
|
, tableCols :: [(String, Column)]
|
||||||
@@ -27,21 +28,22 @@ data Table = Table
|
|||||||
}
|
}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
-- 数据库:多张表
|
-- | 数据库:多张表
|
||||||
type Database = [(String, Table)]
|
type Database = [(String, Table)]
|
||||||
|
|
||||||
-- 获取列名
|
-- * 工具
|
||||||
|
-- | 取表的列名
|
||||||
colNames :: Table -> [String]
|
colNames :: Table -> [String]
|
||||||
colNames = map fst . tableCols
|
colNames = map fst . tableCols
|
||||||
|
|
||||||
-- 查某一列的类型
|
-- | 查某列的类型
|
||||||
colType :: Table -> String -> Maybe Column
|
colType :: Table -> String -> Maybe Column
|
||||||
colType t c = lookup c (tableCols t)
|
colType t c = lookup c (tableCols t)
|
||||||
|
|
||||||
-- 按名字查表;表不存在就报错
|
-- | 按名查表,没有就报错
|
||||||
lookupTable :: Database -> String -> Either String Table
|
lookupTable :: Database -> String -> Either String Table
|
||||||
lookupTable db t = maybe (Left ("unknown table: " ++ t)) Right (lookup t db)
|
lookupTable db t = maybe (Left ("unknown table: " ++ t)) Right (lookup t db)
|
||||||
|
|
||||||
-- 投影列清单里的哨兵值:它表示"全部列,不用挑",不是列名
|
-- | 投影哨兵 "*"
|
||||||
allColumns :: String
|
allColumns :: String
|
||||||
allColumns = "*"
|
allColumns = "*"
|
||||||
@@ -3,19 +3,18 @@ module ChuSQL.Semantic (check) where
|
|||||||
import ChuSQL.Algebra.Expr (colsInExpr)
|
import ChuSQL.Algebra.Expr (colsInExpr)
|
||||||
import ChuSQL.Model
|
import ChuSQL.Model
|
||||||
import ChuSQL.Syntax.AST
|
import ChuSQL.Syntax.AST
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate, nub)
|
||||||
|
|
||||||
-- 语义检查:执行前的关卡 —— 表和列存不存在、表达式类型对不对、写入的值类型是否匹配。
|
-- 语义检查:表和列在不在、类型对不对,全部在执行前查。
|
||||||
|
|
||||||
-- * 表与列名解析
|
-- * 环境
|
||||||
|
-- | 给列名加上别名前缀
|
||||||
-- | 给一批列加上"别名."前缀
|
|
||||||
prefixColumns :: Maybe String -> [(String, Column)] -> [(String, Column)]
|
prefixColumns :: Maybe String -> [(String, Column)] -> [(String, Column)]
|
||||||
prefixColumns mAlias cols = [(prefix ++ c, ty) | (c, ty) <- cols]
|
prefixColumns mAlias cols = [(prefix ++ c, ty) | (c, ty) <- cols]
|
||||||
where
|
where
|
||||||
prefix = maybe "" (++ ".") mAlias
|
prefix = maybe "" (++ ".") mAlias
|
||||||
|
|
||||||
-- | 走一遍 FROM:算出可用列(带别名前缀),顺手把每个 ON 条件也查了
|
-- | 收集 FROM 能提供的列
|
||||||
checkFrom :: Database -> FromClause -> Either String [(String, Column)]
|
checkFrom :: Database -> FromClause -> Either String [(String, Column)]
|
||||||
checkFrom db (FromTable mAlias tbl) = do
|
checkFrom db (FromTable mAlias tbl) = do
|
||||||
t <- lookupTable db tbl
|
t <- lookupTable db tbl
|
||||||
@@ -27,9 +26,9 @@ checkFrom db (FromJoin left mAlias tbl cond) = do
|
|||||||
checkBool "ON" env cond
|
checkBool "ON" env cond
|
||||||
Right env
|
Right env
|
||||||
|
|
||||||
-- * 列检查与报错
|
|
||||||
|
|
||||||
-- | 统一拼一条带"可用列清单"的报错
|
-- * 检查
|
||||||
|
-- | 拼“列不存在”的报错
|
||||||
missingColumn :: String -> String -> [(String, Column)] -> String
|
missingColumn :: String -> String -> [(String, Column)] -> String
|
||||||
missingColumn place c env =
|
missingColumn place c env =
|
||||||
"unknown column in " ++ place ++ ": " ++ c ++ hint
|
"unknown column in " ++ place ++ ": " ++ c ++ hint
|
||||||
@@ -38,16 +37,15 @@ missingColumn place c env =
|
|||||||
| null env = ""
|
| null env = ""
|
||||||
| otherwise = " (available: " ++ intercalate ", " (map fst env) ++ ")"
|
| otherwise = " (available: " ++ intercalate ", " (map fst env) ++ ")"
|
||||||
|
|
||||||
-- | 请求的列必须都在可用清单里;哨兵 allColumns 表示"全部列",放行
|
-- | 检查列是否都在环境里
|
||||||
checkColumns :: String -> [(String, Column)] -> [String] -> Either String ()
|
checkColumns :: String -> [(String, Column)] -> [String] -> Either String ()
|
||||||
checkColumns place env requested =
|
checkColumns place env requested =
|
||||||
case [c | c <- requested, c /= allColumns, c `notElem` map fst env] of
|
case [c | c <- requested, c /= allColumns, c `notElem` map fst env] of
|
||||||
[] -> Right ()
|
[] -> Right ()
|
||||||
(c : _) -> Left (missingColumn place c env)
|
(c : _) -> Left (missingColumn place c env)
|
||||||
|
|
||||||
-- * 表达式类型推导
|
|
||||||
|
|
||||||
-- | 推出一个表达式的结果类型;列不存在、或类型对不上,都报错
|
-- | 推导表达式的类型
|
||||||
inferExpr :: String -> [(String, Column)] -> Expr -> Either String Column
|
inferExpr :: String -> [(String, Column)] -> Expr -> Either String Column
|
||||||
inferExpr place env (Col c) = maybe (Left (missingColumn place c env)) Right (lookup c env)
|
inferExpr place env (Col c) = maybe (Left (missingColumn place c env)) Right (lookup c env)
|
||||||
inferExpr _ _ (LitInt _) = Right TInt
|
inferExpr _ _ (LitInt _) = Right TInt
|
||||||
@@ -64,7 +62,7 @@ inferExpr place env (Eq a b) = do
|
|||||||
then Right TBool
|
then Right TBool
|
||||||
else Left (place ++ ": both sides of = must have the same type, got " ++ show ta ++ " and " ++ show tb)
|
else Left (place ++ ": both sides of = must have the same type, got " ++ show ta ++ " and " ++ show tb)
|
||||||
|
|
||||||
-- | 二元运算的通用检查:两边都必须是同一种期望类型,结果都是布尔
|
-- | 检查二元运算两边的类型
|
||||||
operands :: String -> [(String, Column)] -> Column -> Expr -> Expr -> Either String Column
|
operands :: String -> [(String, Column)] -> Column -> Expr -> Expr -> Either String Column
|
||||||
operands place env want a b = do
|
operands place env want a b = do
|
||||||
ta <- inferExpr place env a
|
ta <- inferExpr place env a
|
||||||
@@ -73,7 +71,7 @@ operands place env want a b = do
|
|||||||
then Right TBool
|
then Right TBool
|
||||||
else Left (place ++ ": operator needs " ++ show want ++ " on both sides, got " ++ show ta ++ " and " ++ show tb)
|
else Left (place ++ ": operator needs " ++ show want ++ " on both sides, got " ++ show ta ++ " and " ++ show tb)
|
||||||
|
|
||||||
-- | 这个表达式必须能算成一个条件(布尔)
|
-- | 要求条件能算出布尔
|
||||||
checkBool :: String -> [(String, Column)] -> Expr -> Either String ()
|
checkBool :: String -> [(String, Column)] -> Expr -> Either String ()
|
||||||
checkBool place env e = do
|
checkBool place env e = do
|
||||||
checkColumns place env (colsInExpr e)
|
checkColumns place env (colsInExpr e)
|
||||||
@@ -82,9 +80,9 @@ checkBool place env e = do
|
|||||||
then Right ()
|
then Right ()
|
||||||
else Left (place ++ ": condition must be a boolean, got " ++ show t)
|
else Left (place ++ ": condition must be a boolean, got " ++ show t)
|
||||||
|
|
||||||
-- * 写入语句
|
|
||||||
|
|
||||||
-- | 一条赋值的通用检查:目标列必须存在,右边表达式的类型要对得上
|
-- * 写入语句
|
||||||
|
-- | 检查赋值列和类型
|
||||||
checkTyped :: String -> [(String, Column)] -> Table -> (String, Expr) -> Either String ()
|
checkTyped :: String -> [(String, Column)] -> Table -> (String, Expr) -> Either String ()
|
||||||
checkTyped place env t (c, e) = do
|
checkTyped place env t (c, e) = do
|
||||||
want <- maybe (Left (missingColumn place c (tableCols t))) Right (colType t c)
|
want <- maybe (Left (missingColumn place c (tableCols t))) Right (colType t c)
|
||||||
@@ -93,18 +91,18 @@ checkTyped place env t (c, e) = do
|
|||||||
then Right ()
|
then Right ()
|
||||||
else Left (place ++ ": column " ++ c ++ " needs " ++ show want ++ ", got " ++ show got)
|
else Left (place ++ ": column " ++ c ++ " needs " ++ show want ++ ", got " ++ show got)
|
||||||
|
|
||||||
-- | INSERT 的一个值:环境是空的 —— 值必须是字面量,写列名会被拦下
|
-- | 检查插入值的类型
|
||||||
checkValue :: Table -> (String, Expr) -> Either String ()
|
checkValue :: Table -> (String, Expr) -> Either String ()
|
||||||
checkValue = checkTyped "INSERT" []
|
checkValue = checkTyped "INSERT" []
|
||||||
|
|
||||||
-- | UPDATE 的一个赋值:环境是整张表 —— 右边可以引用本行的列
|
-- | 检查 UPDATE 的赋值
|
||||||
checkAssign :: Table -> (String, Expr) -> Either String ()
|
checkAssign :: Table -> (String, Expr) -> Either String ()
|
||||||
checkAssign t = checkTyped "UPDATE" (tableCols t) t
|
checkAssign t = checkTyped "UPDATE" (tableCols t) t
|
||||||
|
|
||||||
-- * 总入口
|
|
||||||
|
|
||||||
-- | 语义检查总入口:合法返回 Right (),不合法返回 Left(带人话解释)
|
-- * 入口
|
||||||
check :: Database -> Query -> Either String ()
|
-- | 按语句类型分派检查
|
||||||
|
check :: Database -> Statement -> Either String ()
|
||||||
check db q = case q of
|
check db q = case q of
|
||||||
Select
|
Select
|
||||||
{ selectCols = cols
|
{ selectCols = cols
|
||||||
@@ -128,3 +126,15 @@ check db q = case q of
|
|||||||
checkColumns "UPDATE" (tableCols t) (map fst assigns)
|
checkColumns "UPDATE" (tableCols t) (map fst assigns)
|
||||||
mapM_ (checkAssign t) assigns
|
mapM_ (checkAssign t) assigns
|
||||||
mapM_ (checkBool "WHERE" (tableCols t)) mWhere
|
mapM_ (checkBool "WHERE" (tableCols t)) mWhere
|
||||||
|
CreateTable name cols -> do
|
||||||
|
if null name
|
||||||
|
then Left "CREATE TABLE: empty table name"
|
||||||
|
else do
|
||||||
|
let names = map fst cols
|
||||||
|
if length names /= length (nub names)
|
||||||
|
then Left ("CREATE TABLE: duplicate column names in " ++ name)
|
||||||
|
else Right ()
|
||||||
|
DropTable name ->
|
||||||
|
if null name
|
||||||
|
then Left "DROP TABLE: empty table name"
|
||||||
|
else Right ()
|
||||||
@@ -1,20 +1,30 @@
|
|||||||
module ChuSQL.Storage (MonadStorage (..)) where
|
module ChuSQL.Storage (
|
||||||
|
MonadStorage (..),
|
||||||
|
) where
|
||||||
|
|
||||||
import ChuSQL.Model (Database, Row)
|
import ChuSQL.Model (Column, Database, Row)
|
||||||
|
|
||||||
-- 存储层抽象:上层只认 domain 类型,不关心底层是内存还是 IPC。
|
-- 存储层抽象:上层只认 domain 类型,不关心底层实现。
|
||||||
|
|
||||||
|
-- | 存储接口:读写表、按主键取行、取快照
|
||||||
class (Monad m) => MonadStorage m where
|
class (Monad m) => MonadStorage m where
|
||||||
-- 全表扫描;表不存在返回 Left。
|
-- \| 全表扫描
|
||||||
scan :: String -> m (Either String [Row])
|
scan :: String -> m (Either String [Row])
|
||||||
|
|
||||||
-- 追加一行到表尾;表不存在返回 Left。
|
-- \| 追加一行
|
||||||
insert :: String -> Row -> m (Either String ())
|
insert :: String -> Row -> m (Either String ())
|
||||||
|
|
||||||
-- 用新行列表整体替换一张表。
|
-- \| 整表替换
|
||||||
replaceAll :: String -> [Row] -> m (Either String ())
|
replaceAll :: String -> [Row] -> m (Either String ())
|
||||||
|
|
||||||
-- 按主键查一行;命中返回 Just,未命中返回 Nothing。
|
-- \| 按主键取一行
|
||||||
lookupByKey :: String -> Int -> m (Either String (Maybe Row))
|
lookupByKey :: String -> Int -> m (Either String (Maybe Row))
|
||||||
|
|
||||||
-- 取全库快照,喂给还接收 Database 的语义检查 / 优化器 / evalRelOp。
|
-- \| 建一张空表
|
||||||
|
createTable :: String -> [(String, Column)] -> m (Either String ())
|
||||||
|
|
||||||
|
-- \| 删除表
|
||||||
|
dropTable :: String -> m (Either String ())
|
||||||
|
|
||||||
|
-- \| 取全库快照
|
||||||
snapshot :: m Database
|
snapshot :: m Database
|
||||||
@@ -11,11 +11,13 @@ module ChuSQL.Storage.IPC (
|
|||||||
doLookupByKey,
|
doLookupByKey,
|
||||||
doInsertKeyed,
|
doInsertKeyed,
|
||||||
doDescribeTable,
|
doDescribeTable,
|
||||||
|
doCreateTable,
|
||||||
|
doDropTable,
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ChuSQL.Model
|
import ChuSQL.Model
|
||||||
import ChuSQL.Storage
|
import ChuSQL.Storage
|
||||||
import Control.Exception (bracket)
|
import Control.Exception (IOException, bracket, try)
|
||||||
import Data.Aeson (FromJSON (..), ToJSON (..), eitherDecodeStrict, encode, object, withObject, (.:), (.=))
|
import Data.Aeson (FromJSON (..), ToJSON (..), eitherDecodeStrict, encode, object, withObject, (.:), (.=))
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Data.Aeson.Key qualified as K
|
import Data.Aeson.Key qualified as K
|
||||||
@@ -29,39 +31,41 @@ import Data.Text qualified as T
|
|||||||
import System.Environment (lookupEnv)
|
import System.Environment (lookupEnv)
|
||||||
import System.IO
|
import System.IO
|
||||||
|
|
||||||
-- IPC 存储实现:每个 MonadStorage 方法翻译成管道上的一条 JSON 请求。
|
-- IPC 存储实现:每个存储方法翻成管道上的一条 JSON 请求。
|
||||||
|
|
||||||
-- | IPC 实现:通过命名管道连 Rust 存储进程。
|
-- * 存储实现
|
||||||
|
-- | 命名管道上的存储
|
||||||
newtype IPCStorage a = IPCStorage
|
newtype IPCStorage a = IPCStorage
|
||||||
{ runIPCStorage :: IO a
|
{ runIPCStorage :: IO a
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Functor:把 fmap 转发给底下的 IO。
|
-- | Functor:转发给 IO
|
||||||
instance Functor IPCStorage where
|
instance Functor IPCStorage where
|
||||||
fmap f (IPCStorage m) = IPCStorage (fmap f m)
|
fmap f (IPCStorage m) = IPCStorage (fmap f m)
|
||||||
|
|
||||||
-- | Applicative:把 pure / <*> 转发给底下的 IO。
|
-- | Applicative:转发给 IO
|
||||||
instance Applicative IPCStorage where
|
instance Applicative IPCStorage where
|
||||||
pure = IPCStorage . pure
|
pure = IPCStorage . pure
|
||||||
IPCStorage mf <*> IPCStorage ma = IPCStorage (mf <*> ma)
|
IPCStorage mf <*> IPCStorage ma = IPCStorage (mf <*> ma)
|
||||||
|
|
||||||
-- | Monad:顺序执行,转发给底下的 IO。
|
-- | Monad:顺序执行
|
||||||
instance Monad IPCStorage where
|
instance Monad IPCStorage where
|
||||||
IPCStorage m >>= k = IPCStorage $ do
|
IPCStorage m >>= k = IPCStorage $ do
|
||||||
a <- m
|
a <- m
|
||||||
runIPCStorage (k a)
|
runIPCStorage (k a)
|
||||||
|
|
||||||
-- | 管道路径;默认 \\.\pipe\chusql-storage,可用 CHUSQL_PIPE 覆盖。
|
-- * 管道
|
||||||
|
-- | 管道路径
|
||||||
pipePath :: IO String
|
pipePath :: IO String
|
||||||
pipePath = do
|
pipePath = do
|
||||||
name <- maybe "chusql-storage" id <$> lookupEnv "CHUSQL_PIPE"
|
name <- maybe "chusql-storage" id <$> lookupEnv "CHUSQL_PIPE"
|
||||||
pure ("\\\\.\\pipe\\" ++ name)
|
pure ("\\\\.\\pipe\\" ++ name)
|
||||||
|
|
||||||
-- | 打开管道、跑闭包、关闭。
|
-- | 开管道跑闭包
|
||||||
withPipe :: (Handle -> IO a) -> IO a
|
withPipe :: (Handle -> IO a) -> IO a
|
||||||
withPipe = bracket openPipe hClose
|
withPipe = bracket openPipe hClose
|
||||||
where
|
where
|
||||||
-- \| 二进制读写、不做换行翻译,跟 Rust 侧对齐。
|
-- \| 打开管道句柄
|
||||||
openPipe = do
|
openPipe = do
|
||||||
path <- pipePath
|
path <- pipePath
|
||||||
h <- openFile path ReadWriteMode
|
h <- openFile path ReadWriteMode
|
||||||
@@ -69,9 +73,15 @@ withPipe = bracket openPipe hClose
|
|||||||
hSetNewlineMode h noNewlineTranslation
|
hSetNewlineMode h noNewlineTranslation
|
||||||
pure h
|
pure h
|
||||||
|
|
||||||
-- | 发一条请求,读一条响应。
|
-- | 发一条请求读一条响应
|
||||||
sendRequest :: Request -> IO Response
|
sendRequest :: Request -> IO Response
|
||||||
sendRequest req = withPipe $ \h -> do
|
sendRequest req = do
|
||||||
|
result <- try (withPipe roundTrip) :: IO (Either IOException Response)
|
||||||
|
pure $ case result of
|
||||||
|
Left e -> RespError ("pipe error: " ++ show e)
|
||||||
|
Right r -> r
|
||||||
|
where
|
||||||
|
roundTrip h = do
|
||||||
BL.hPutStr h (encode req)
|
BL.hPutStr h (encode req)
|
||||||
BSC.hPutStr h "\n"
|
BSC.hPutStr h "\n"
|
||||||
hFlush h
|
hFlush h
|
||||||
@@ -81,27 +91,28 @@ sendRequest req = withPipe $ \h -> do
|
|||||||
Left err -> pure (RespError ("decode: " ++ err))
|
Left err -> pure (RespError ("decode: " ++ err))
|
||||||
Right r -> pure r
|
Right r -> pure r
|
||||||
|
|
||||||
-- * JSON 编解码
|
|
||||||
|
|
||||||
-- | 一列的 schema;对应 Rust 侧的 SchemaColumn。
|
-- * 编解码
|
||||||
|
-- | 一列的 schema
|
||||||
data SchemaColumn = SchemaColumn
|
data SchemaColumn = SchemaColumn
|
||||||
{ scName :: String
|
{ scName :: String
|
||||||
, scType :: String
|
, scType :: String
|
||||||
}
|
}
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- | SchemaColumn 编码;Haskell 侧只发 DescribeTable,编码用不上,暂存实现。
|
-- | SchemaColumn 编码
|
||||||
instance ToJSON SchemaColumn where
|
instance ToJSON SchemaColumn where
|
||||||
toJSON (SchemaColumn n t) = object ["name" .= n, "ty" .= t]
|
toJSON (SchemaColumn n t) = object ["name" .= n, "ty" .= t]
|
||||||
|
|
||||||
-- | SchemaColumn 解码:从 {"name": "...", "ty": "int"|"str"|"bool"} 读回。
|
-- | SchemaColumn 解码
|
||||||
instance FromJSON SchemaColumn where
|
instance FromJSON SchemaColumn where
|
||||||
parseJSON = withObject "SchemaColumn" $ \o -> do
|
parseJSON = withObject "SchemaColumn" $ \o -> do
|
||||||
n <- o .: "name"
|
n <- o .: "name"
|
||||||
t <- o .: "ty"
|
t <- o .: "ty"
|
||||||
pure (SchemaColumn n t)
|
pure (SchemaColumn n t)
|
||||||
|
|
||||||
-- | 请求;对应 Rust 侧带 method tag 的 enum。
|
-- * 请求
|
||||||
|
-- | 请求类型
|
||||||
data Request
|
data Request
|
||||||
= ReqPing
|
= ReqPing
|
||||||
| ReqScan String
|
| ReqScan String
|
||||||
@@ -110,8 +121,10 @@ data Request
|
|||||||
| ReqListTables
|
| ReqListTables
|
||||||
| ReqLookupByKey String Int
|
| ReqLookupByKey String Int
|
||||||
| ReqDescribeTable String
|
| ReqDescribeTable String
|
||||||
|
| ReqCreateTable String [SchemaColumn]
|
||||||
|
| ReqDropTable String
|
||||||
|
|
||||||
-- | 请求编码:method 字段 + 各自参数。
|
-- | 请求编码
|
||||||
instance ToJSON Request where
|
instance ToJSON Request where
|
||||||
toJSON ReqPing = object ["method" .= ("ping" :: T.Text)]
|
toJSON ReqPing = object ["method" .= ("ping" :: T.Text)]
|
||||||
toJSON (ReqScan t) = object ["method" .= ("scan" :: T.Text), "table" .= t]
|
toJSON (ReqScan t) = object ["method" .= ("scan" :: T.Text), "table" .= t]
|
||||||
@@ -142,17 +155,26 @@ instance ToJSON Request where
|
|||||||
[ "method" .= ("describe_table" :: T.Text)
|
[ "method" .= ("describe_table" :: T.Text)
|
||||||
, "table" .= t
|
, "table" .= t
|
||||||
]
|
]
|
||||||
|
toJSON (ReqCreateTable t cols) =
|
||||||
|
object
|
||||||
|
[ "method" .= ("create_table" :: T.Text)
|
||||||
|
, "table" .= t
|
||||||
|
, "columns" .= cols
|
||||||
|
]
|
||||||
|
toJSON (ReqDropTable t) =
|
||||||
|
object ["method" .= ("drop_table" :: T.Text), "table" .= t]
|
||||||
|
|
||||||
-- | 响应;对应 Rust 侧带 status tag 的 enum。
|
|
||||||
|
-- | 响应类型
|
||||||
data Response
|
data Response
|
||||||
= RespPong
|
= RespPong
|
||||||
| RespRows [Row]
|
| RespRows [Row]
|
||||||
| RespTables [String]
|
| RespTables [String]
|
||||||
| RespSchema [SchemaColumn]
|
| RespSchema [SchemaColumn] Int
|
||||||
| RespOk
|
| RespOk
|
||||||
| RespError String
|
| RespError String
|
||||||
|
|
||||||
-- | 响应解码:按 status 字段分派。
|
-- | 响应解码
|
||||||
instance FromJSON Response where
|
instance FromJSON Response where
|
||||||
parseJSON = withObject "Response" $ \o -> do
|
parseJSON = withObject "Response" $ \o -> do
|
||||||
status <- o .: "status"
|
status <- o .: "status"
|
||||||
@@ -161,17 +183,20 @@ instance FromJSON Response where
|
|||||||
"ok" -> pure RespOk
|
"ok" -> pure RespOk
|
||||||
"rows" -> RespRows <$> (o .: "rows" >>= mapM rowFromJSON)
|
"rows" -> RespRows <$> (o .: "rows" >>= mapM rowFromJSON)
|
||||||
"tables" -> RespTables <$> o .: "tables"
|
"tables" -> RespTables <$> o .: "tables"
|
||||||
"schema" -> RespSchema <$> o .: "columns"
|
|
||||||
"error" -> RespError <$> o .: "message"
|
"error" -> RespError <$> o .: "message"
|
||||||
|
"schema" -> do
|
||||||
|
cols <- o .: "columns"
|
||||||
|
cnt <- o .: "row_count"
|
||||||
|
pure (RespSchema cols cnt)
|
||||||
other -> fail ("unknown status: " ++ T.unpack other)
|
other -> fail ("unknown status: " ++ T.unpack other)
|
||||||
|
|
||||||
-- | Value 编码成 JSON 的 number / string / bool。
|
-- | 值编码成 JSON
|
||||||
valueToJSON :: Value -> A.Value
|
valueToJSON :: Value -> A.Value
|
||||||
valueToJSON (VInt n) = A.Number (fromIntegral n)
|
valueToJSON (VInt n) = A.Number (fromIntegral n)
|
||||||
valueToJSON (VStr s) = A.String (T.pack s)
|
valueToJSON (VStr s) = A.String (T.pack s)
|
||||||
valueToJSON (VBool b) = A.Bool b
|
valueToJSON (VBool b) = A.Bool b
|
||||||
|
|
||||||
-- | JSON 解回 Value;number 只接受整数。
|
-- | JSON 解回值
|
||||||
valueFromJSON :: A.Value -> Parser Value
|
valueFromJSON :: A.Value -> Parser Value
|
||||||
valueFromJSON (A.Number n) =
|
valueFromJSON (A.Number n) =
|
||||||
case floatingOrInteger n :: Either Double Integer of
|
case floatingOrInteger n :: Either Double Integer of
|
||||||
@@ -181,24 +206,24 @@ valueFromJSON (A.String s) = pure (VStr (T.unpack s))
|
|||||||
valueFromJSON (A.Bool b) = pure (VBool b)
|
valueFromJSON (A.Bool b) = pure (VBool b)
|
||||||
valueFromJSON _ = fail "unsupported value type"
|
valueFromJSON _ = fail "unsupported value type"
|
||||||
|
|
||||||
-- | 一行编成 JSON object。
|
-- | 一行编码成 JSON
|
||||||
rowToJSON :: Row -> A.Value
|
rowToJSON :: Row -> A.Value
|
||||||
rowToJSON r =
|
rowToJSON r =
|
||||||
A.Object (KM.fromList [(K.fromString k, valueToJSON v) | (k, v) <- r])
|
A.Object (KM.fromList [(K.fromString k, valueToJSON v) | (k, v) <- r])
|
||||||
|
|
||||||
-- | JSON object 解成一行。
|
-- | JSON 解回一行
|
||||||
rowFromJSON :: A.Value -> Parser Row
|
rowFromJSON :: A.Value -> Parser Row
|
||||||
rowFromJSON (A.Object o) = mapM toPair (KM.toList o)
|
rowFromJSON (A.Object o) = mapM toPair (KM.toList o)
|
||||||
where
|
where
|
||||||
-- \| 解一个键值对。
|
-- \| 解一个键值对
|
||||||
toPair (k, v) = do
|
toPair (k, v) = do
|
||||||
val <- valueFromJSON v
|
val <- valueFromJSON v
|
||||||
pure (K.toString k, val)
|
pure (K.toString k, val)
|
||||||
rowFromJSON _ = fail "row must be a JSON object"
|
rowFromJSON _ = fail "row must be a JSON object"
|
||||||
|
|
||||||
-- * 内部请求封装
|
|
||||||
|
|
||||||
-- | 发 Scan 并把结果取成 Either。
|
-- * 内部封装
|
||||||
|
-- | 发 Scan
|
||||||
doScan :: String -> IO (Either String [Row])
|
doScan :: String -> IO (Either String [Row])
|
||||||
doScan t = do
|
doScan t = do
|
||||||
resp <- sendRequest (ReqScan t)
|
resp <- sendRequest (ReqScan t)
|
||||||
@@ -207,16 +232,22 @@ doScan t = do
|
|||||||
RespError e -> Left e
|
RespError e -> Left e
|
||||||
_ -> Left "unexpected response to scan"
|
_ -> Left "unexpected response to scan"
|
||||||
|
|
||||||
-- | 发 Insert(不带索引键)。
|
-- | 发 Insert(带 id 键)
|
||||||
doInsert :: String -> Row -> IO (Either String ())
|
doInsert :: String -> Row -> IO (Either String ())
|
||||||
doInsert t r = do
|
doInsert t r = do
|
||||||
resp <- sendRequest (ReqInsert t r Nothing)
|
resp <- sendRequest (ReqInsert t r (rowKey r))
|
||||||
pure $ case resp of
|
pure $ case resp of
|
||||||
RespOk -> Right ()
|
RespOk -> Right ()
|
||||||
RespError e -> Left e
|
RespError e -> Left e
|
||||||
_ -> Left "unexpected response to insert"
|
_ -> Left "unexpected response to insert"
|
||||||
|
|
||||||
-- | 发 Insert(带索引键)。
|
-- | 取行里的 id 作索引键
|
||||||
|
rowKey :: Row -> Maybe Int
|
||||||
|
rowKey r = case lookup "id" r of
|
||||||
|
Just (VInt k) -> Just k
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- | 发 Insert(指定键)
|
||||||
doInsertKeyed :: String -> Row -> Int -> IO (Either String ())
|
doInsertKeyed :: String -> Row -> Int -> IO (Either String ())
|
||||||
doInsertKeyed t r k = do
|
doInsertKeyed t r k = do
|
||||||
resp <- sendRequest (ReqInsert t r (Just k))
|
resp <- sendRequest (ReqInsert t r (Just k))
|
||||||
@@ -225,7 +256,7 @@ doInsertKeyed t r k = do
|
|||||||
RespError e -> Left e
|
RespError e -> Left e
|
||||||
_ -> Left "unexpected response to insert"
|
_ -> Left "unexpected response to insert"
|
||||||
|
|
||||||
-- | 发 ReplaceAll。
|
-- | 发 ReplaceAll
|
||||||
doReplaceAll :: String -> [Row] -> IO (Either String ())
|
doReplaceAll :: String -> [Row] -> IO (Either String ())
|
||||||
doReplaceAll t rs = do
|
doReplaceAll t rs = do
|
||||||
resp <- sendRequest (ReqReplaceAll t rs)
|
resp <- sendRequest (ReqReplaceAll t rs)
|
||||||
@@ -234,7 +265,7 @@ doReplaceAll t rs = do
|
|||||||
RespError e -> Left e
|
RespError e -> Left e
|
||||||
_ -> Left "unexpected response to replace_all"
|
_ -> Left "unexpected response to replace_all"
|
||||||
|
|
||||||
-- | 发 ListTables。
|
-- | 发 ListTables
|
||||||
doListTables :: IO (Either String [String])
|
doListTables :: IO (Either String [String])
|
||||||
doListTables = do
|
doListTables = do
|
||||||
resp <- sendRequest ReqListTables
|
resp <- sendRequest ReqListTables
|
||||||
@@ -243,7 +274,7 @@ doListTables = do
|
|||||||
RespError e -> Left e
|
RespError e -> Left e
|
||||||
_ -> Left "unexpected response to list_tables"
|
_ -> Left "unexpected response to list_tables"
|
||||||
|
|
||||||
-- | 发 LookupByIndex。
|
-- | 发 LookupByIndex
|
||||||
doLookupByKey :: String -> Int -> IO (Either String (Maybe Row))
|
doLookupByKey :: String -> Int -> IO (Either String (Maybe Row))
|
||||||
doLookupByKey t k = do
|
doLookupByKey t k = do
|
||||||
resp <- sendRequest (ReqLookupByKey t k)
|
resp <- sendRequest (ReqLookupByKey t k)
|
||||||
@@ -253,45 +284,63 @@ doLookupByKey t k = do
|
|||||||
RespError e -> Left e
|
RespError e -> Left e
|
||||||
_ -> Left "unexpected response to lookup_by_index"
|
_ -> Left "unexpected response to lookup_by_index"
|
||||||
|
|
||||||
-- | 发 DescribeTable。
|
-- | 发 DescribeTable
|
||||||
doDescribeTable :: String -> IO (Either String [SchemaColumn])
|
doDescribeTable :: String -> IO (Either String ([SchemaColumn], Int))
|
||||||
doDescribeTable t = do
|
doDescribeTable t = do
|
||||||
resp <- sendRequest (ReqDescribeTable t)
|
resp <- sendRequest (ReqDescribeTable t)
|
||||||
pure $ case resp of
|
pure $ case resp of
|
||||||
RespSchema cols -> Right cols
|
RespSchema cols n -> Right (cols, n)
|
||||||
RespError e -> Left e
|
RespError e -> Left e
|
||||||
_ -> Left "unexpected response to describe_table"
|
_ -> Left "unexpected response to describe_table"
|
||||||
|
|
||||||
-- | 把 Rust 的 schema 列转成 Model 的 Column;未知类型降级为 TStr。
|
-- | 发 CreateTable
|
||||||
|
doCreateTable :: String -> [SchemaColumn] -> IO (Either String ())
|
||||||
|
doCreateTable t cols = do
|
||||||
|
resp <- sendRequest (ReqCreateTable t cols)
|
||||||
|
pure $ case resp of
|
||||||
|
RespOk -> Right ()
|
||||||
|
RespError e -> Left e
|
||||||
|
_ -> Left "unexpected response to create_table"
|
||||||
|
|
||||||
|
-- | 发 DropTable
|
||||||
|
doDropTable :: String -> IO (Either String ())
|
||||||
|
doDropTable t = do
|
||||||
|
resp <- sendRequest (ReqDropTable t)
|
||||||
|
pure $ case resp of
|
||||||
|
RespOk -> Right ()
|
||||||
|
RespError e -> Left e
|
||||||
|
_ -> Left "unexpected response to drop_table"
|
||||||
|
|
||||||
|
-- * 表结构
|
||||||
|
-- | 线上 schema 转本地列
|
||||||
schemaToColumns :: [SchemaColumn] -> [(String, Column)]
|
schemaToColumns :: [SchemaColumn] -> [(String, Column)]
|
||||||
schemaToColumns = map go
|
schemaToColumns = map go
|
||||||
where
|
where
|
||||||
-- \| 按 ty 字符串映射成 Column 构造子。
|
|
||||||
go (SchemaColumn n "int") = (n, TInt)
|
go (SchemaColumn n "int") = (n, TInt)
|
||||||
go (SchemaColumn n "str") = (n, TStr)
|
go (SchemaColumn n "str") = (n, TStr)
|
||||||
go (SchemaColumn n "bool") = (n, TBool)
|
go (SchemaColumn n "bool") = (n, TBool)
|
||||||
go (SchemaColumn n _) = (n, TStr)
|
go (SchemaColumn n _) = (n, TStr)
|
||||||
|
|
||||||
-- | 从若干行推断列名与类型;catalog 拿不到时的兜底。
|
-- | 从行推断列和类型
|
||||||
inferColumns :: [Row] -> [(String, Column)]
|
inferColumns :: [Row] -> [(String, Column)]
|
||||||
inferColumns rows = [(n, inferType n) | n <- names]
|
inferColumns rows = [(n, inferType n) | n <- names]
|
||||||
where
|
where
|
||||||
-- \| 全部出现过的列名。
|
-- \| 出现过的列名
|
||||||
names = nub (concatMap (map fst) rows)
|
names = nub (concatMap (map fst) rows)
|
||||||
|
|
||||||
-- \| 用第一个出现的值推断列类型;找不到就给 TStr。
|
-- \| 按第一个值推断类型
|
||||||
inferType n = case [v | r <- rows, Just v <- [lookup n r]] of
|
inferType n = case [v | r <- rows, Just v <- [lookup n r]] of
|
||||||
(VInt _ : _) -> TInt
|
(VInt _ : _) -> TInt
|
||||||
(VStr _ : _) -> TStr
|
(VStr _ : _) -> TStr
|
||||||
(VBool _ : _) -> TBool
|
(VBool _ : _) -> TBool
|
||||||
[] -> TStr
|
[] -> TStr
|
||||||
|
|
||||||
-- | 先 describe 拿列,再 scan 拿行;describe 拿不到时退回扫行推断。
|
-- | 扫一张表拼成 Table
|
||||||
loadTable :: String -> IO Table
|
loadTable :: String -> IO Table
|
||||||
loadTable t = do
|
loadTable t = do
|
||||||
schemaR <- doDescribeTable t
|
schemaR <- doDescribeTable t
|
||||||
case schemaR of
|
case schemaR of
|
||||||
Right cols -> do
|
Right (cols, _n) -> do
|
||||||
rowsR <- doScan t
|
rowsR <- doScan t
|
||||||
case rowsR of
|
case rowsR of
|
||||||
Right rows -> pure (Table t (schemaToColumns cols) rows)
|
Right rows -> pure (Table t (schemaToColumns cols) rows)
|
||||||
@@ -302,30 +351,41 @@ loadTable t = do
|
|||||||
Right rows -> pure (Table t (inferColumns rows) rows)
|
Right rows -> pure (Table t (inferColumns rows) rows)
|
||||||
Left _ -> pure (Table t [] [])
|
Left _ -> pure (Table t [] [])
|
||||||
|
|
||||||
-- * MonadStorage 实例
|
|
||||||
|
|
||||||
-- | IPC 实现:所有方法都走管道。
|
-- * MonadStorage 实例
|
||||||
|
-- | 存储方法全走管道
|
||||||
instance MonadStorage IPCStorage where
|
instance MonadStorage IPCStorage where
|
||||||
-- \| 发 Scan。
|
-- \| 发 Scan
|
||||||
scan t = IPCStorage (doScan t)
|
scan t = IPCStorage (doScan t)
|
||||||
|
|
||||||
-- \| 发 Insert,不带索引键。
|
-- \| 发 Insert
|
||||||
insert t r = IPCStorage (doInsert t r)
|
insert t r = IPCStorage (doInsert t r)
|
||||||
|
|
||||||
-- \| 发 ReplaceAll。
|
-- \| 发 ReplaceAll
|
||||||
replaceAll t rs = IPCStorage (doReplaceAll t rs)
|
replaceAll t rs = IPCStorage (doReplaceAll t rs)
|
||||||
|
|
||||||
-- \| 发 LookupByIndex。
|
-- \| 发 LookupByIndex
|
||||||
lookupByKey t k = IPCStorage (doLookupByKey t k)
|
lookupByKey t k = IPCStorage (doLookupByKey t k)
|
||||||
|
|
||||||
-- \| ListTables + 逐表 (describe, scan),拼成 Database。
|
-- \| 发 CreateTable
|
||||||
|
createTable name cols = IPCStorage (doCreateTable name (map toWire cols))
|
||||||
|
where
|
||||||
|
-- \| 本地列类型转线上字符串
|
||||||
|
toWire (n, TInt) = SchemaColumn n "int"
|
||||||
|
toWire (n, TStr) = SchemaColumn n "str"
|
||||||
|
toWire (n, TBool) = SchemaColumn n "bool"
|
||||||
|
|
||||||
|
-- \| 发 DropTable
|
||||||
|
dropTable name = IPCStorage (doDropTable name)
|
||||||
|
|
||||||
|
-- \| 列表 + 逐表扫描拼库
|
||||||
snapshot = IPCStorage $ do
|
snapshot = IPCStorage $ do
|
||||||
result <- doListTables
|
result <- doListTables
|
||||||
case result of
|
case result of
|
||||||
Left _ -> pure []
|
Left _ -> pure []
|
||||||
Right ts -> mapM loadDbEntry ts
|
Right ts -> mapM loadDbEntry ts
|
||||||
where
|
where
|
||||||
-- \| 加载一个 (表名, 表) 对。
|
-- \| 加载一个 (表名, 表) 对
|
||||||
loadDbEntry t = do
|
loadDbEntry t = do
|
||||||
tbl <- loadTable t
|
tbl <- loadTable t
|
||||||
pure (t, tbl)
|
pure (t, tbl)
|
||||||
@@ -3,16 +3,19 @@ module ChuSQL.Storage.Memory (MemoryStorage (runMemoryStorage)) where
|
|||||||
import ChuSQL.Model
|
import ChuSQL.Model
|
||||||
import ChuSQL.Storage
|
import ChuSQL.Storage
|
||||||
|
|
||||||
-- | 内存实现:本质是 Database 上的 State,错误通道是 Either String。
|
-- 内存实现:状态是 Database,错误通道是 Either String。
|
||||||
|
|
||||||
|
-- * 实例
|
||||||
|
-- | 内存存储:Database 上的状态
|
||||||
newtype MemoryStorage a = MemoryStorage {runMemoryStorage :: Database -> Either String (a, Database)}
|
newtype MemoryStorage a = MemoryStorage {runMemoryStorage :: Database -> Either String (a, Database)}
|
||||||
|
|
||||||
-- | Functor:把 fmap 转发给底下的 State。
|
-- | Functor:转发给底层状态
|
||||||
instance Functor MemoryStorage where
|
instance Functor MemoryStorage where
|
||||||
fmap f (MemoryStorage m) = MemoryStorage $ \db -> do
|
fmap f (MemoryStorage m) = MemoryStorage $ \db -> do
|
||||||
(a, db') <- m db
|
(a, db') <- m db
|
||||||
Right (f a, db')
|
Right (f a, db')
|
||||||
|
|
||||||
-- | Applicative:转发 pure / <*>。
|
-- | Applicative:转发 pure 和 <*>
|
||||||
instance Applicative MemoryStorage where
|
instance Applicative MemoryStorage where
|
||||||
pure x = MemoryStorage $ \db -> Right (x, db)
|
pure x = MemoryStorage $ \db -> Right (x, db)
|
||||||
MemoryStorage mf <*> MemoryStorage ma = MemoryStorage $ \db -> do
|
MemoryStorage mf <*> MemoryStorage ma = MemoryStorage $ \db -> do
|
||||||
@@ -20,47 +23,60 @@ instance Applicative MemoryStorage where
|
|||||||
(a, db2) <- ma db1
|
(a, db2) <- ma db1
|
||||||
Right (f a, db2)
|
Right (f a, db2)
|
||||||
|
|
||||||
-- | Monad:顺序执行,把动作转发给底下的 State。
|
-- | Monad:顺序执行
|
||||||
instance Monad MemoryStorage where
|
instance Monad MemoryStorage where
|
||||||
MemoryStorage m >>= k = MemoryStorage $ \db -> do
|
MemoryStorage m >>= k = MemoryStorage $ \db -> do
|
||||||
(a, db') <- m db
|
(a, db') <- m db
|
||||||
runMemoryStorage (k a) db'
|
runMemoryStorage (k a) db'
|
||||||
|
|
||||||
-- | MonadStorage 实例:内存里直接改 Database。
|
-- | MonadStorage:直接改内存库
|
||||||
instance MonadStorage MemoryStorage where
|
instance MonadStorage MemoryStorage where
|
||||||
-- \| 按名查表,返回所有行。
|
-- \| 全表扫描
|
||||||
scan t = MemoryStorage $ \db -> do
|
scan t = MemoryStorage $ \db -> do
|
||||||
tbl <- lookupTable db t
|
tbl <- lookupTable db t
|
||||||
Right (Right (tableRows tbl), db)
|
Right (Right (tableRows tbl), db)
|
||||||
|
|
||||||
-- \| 追加一行到表尾。
|
-- \| 追加一行
|
||||||
insert t r = MemoryStorage $ \db -> do
|
insert t r = MemoryStorage $ \db -> do
|
||||||
tbl <- lookupTable db t
|
tbl <- lookupTable db t
|
||||||
Right (Right (), replaceTable t tbl{tableRows = tableRows tbl ++ [r]} db)
|
Right (Right (), replaceTable t tbl{tableRows = tableRows tbl ++ [r]} db)
|
||||||
|
|
||||||
-- \| 用新行列表替换整表内容。
|
-- \| 整表替换
|
||||||
replaceAll t rows = MemoryStorage $ \db -> do
|
replaceAll t rows = MemoryStorage $ \db -> do
|
||||||
tbl <- lookupTable db t
|
tbl <- lookupTable db t
|
||||||
Right (Right (), replaceTable t tbl{tableRows = rows} db)
|
Right (Right (), replaceTable t tbl{tableRows = rows} db)
|
||||||
|
|
||||||
-- \| 找 id 列等于 k 的第一行。
|
-- \| 按主键找第一行
|
||||||
lookupByKey t k = MemoryStorage $ \db -> do
|
lookupByKey t k = MemoryStorage $ \db -> do
|
||||||
tbl <- lookupTable db t
|
tbl <- lookupTable db t
|
||||||
Right (Right (findRow tbl k), db)
|
Right (Right (findRow tbl k), db)
|
||||||
|
|
||||||
-- \| 原样返回当前数据库。
|
-- \| 建空表;已存在报错
|
||||||
|
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 [])])
|
||||||
|
|
||||||
|
-- \| 删表;不存在报错
|
||||||
|
dropTable name = MemoryStorage $ \db ->
|
||||||
|
if any ((== name) . fst) db
|
||||||
|
then Right (Right (), filter ((/= name) . fst) db)
|
||||||
|
else Right (Left ("unknown table: " ++ name), db)
|
||||||
|
|
||||||
|
-- \| 原样返回当前库
|
||||||
snapshot = MemoryStorage $ \db -> Right (db, db)
|
snapshot = MemoryStorage $ \db -> Right (db, db)
|
||||||
|
|
||||||
-- | 按 id 列匹配一个整数。
|
-- * 工具
|
||||||
|
-- | 按 id 列找一行
|
||||||
findRow :: Table -> Int -> Maybe Row
|
findRow :: Table -> Int -> Maybe Row
|
||||||
findRow tbl k = case filter matches (tableRows tbl) of
|
findRow tbl k = case filter matches (tableRows tbl) of
|
||||||
(r : _) -> Just r
|
(r : _) -> Just r
|
||||||
[] -> Nothing
|
[] -> Nothing
|
||||||
where
|
where
|
||||||
-- \| 这行的 id 列是否等于 k。
|
-- \| 这行的 id 是否等于 k
|
||||||
matches r = lookup "id" r == Just (VInt k)
|
matches r = lookup "id" r == Just (VInt k)
|
||||||
|
|
||||||
-- | 用给定表替换同名表;不存在就原样返回。
|
-- | 用给定表替换同名表
|
||||||
replaceTable :: String -> Table -> Database -> Database
|
replaceTable :: String -> Table -> Database -> Database
|
||||||
replaceTable name table =
|
replaceTable name table =
|
||||||
map (\(n, t) -> if n == name then (n, table) else (n, t))
|
map (\(n, t) -> if n == name then (n, table) else (n, t))
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
module ChuSQL.Syntax.AST (Query (..), FromClause (..), Expr (..), SortDir (..), makeSelect) where
|
module ChuSQL.Syntax.AST (Statement (..), FromClause (..), Expr (..), SortDir (..), makeSelect) where
|
||||||
|
|
||||||
-- SQL 语法树:四类语句(SELECT / INSERT / DELETE / UPDATE)+ 表达式、FROM 从句、排序方向。
|
import ChuSQL.Model (Column (..))
|
||||||
|
|
||||||
-- 查询语句
|
-- 语法树:语句、数据来源、条件表达式、排序方向。
|
||||||
data Query
|
|
||||||
|
-- * 语句
|
||||||
|
-- | 一条语句:查 / 加 / 删 / 改 / 建表 / 删表
|
||||||
|
data Statement
|
||||||
= Select
|
= Select
|
||||||
{ selectCols :: [String]
|
{ selectCols :: [String]
|
||||||
, selectFrom :: FromClause
|
, selectFrom :: FromClause
|
||||||
@@ -14,15 +17,18 @@ data Query
|
|||||||
| Insert String [String] [Expr]
|
| Insert String [String] [Expr]
|
||||||
| Delete String (Maybe Expr)
|
| Delete String (Maybe Expr)
|
||||||
| Update String [(String, Expr)] (Maybe Expr)
|
| Update String [(String, Expr)] (Maybe Expr)
|
||||||
|
| CreateTable String [(String, Column)]
|
||||||
|
| DropTable String
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- FROM 从句:单表或 JOIN。
|
-- | 数据来源:单表,或两表拼接
|
||||||
data FromClause
|
data FromClause
|
||||||
= FromTable (Maybe String) String
|
= FromTable (Maybe String) String
|
||||||
| FromJoin FromClause (Maybe String) String Expr
|
| FromJoin FromClause (Maybe String) String Expr
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- 条件表达式
|
-- * 表达式
|
||||||
|
-- | 条件表达式
|
||||||
data Expr
|
data Expr
|
||||||
= Col String
|
= Col String
|
||||||
| LitInt Int
|
| LitInt Int
|
||||||
@@ -35,14 +41,16 @@ data Expr
|
|||||||
| Or Expr Expr
|
| Or Expr Expr
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- 排序方向
|
-- * 排序
|
||||||
|
-- | 排序方向
|
||||||
data SortDir
|
data SortDir
|
||||||
= Asc
|
= Asc
|
||||||
| Desc
|
| Desc
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- 便捷构造:造一条最简单的 SELECT(给测试和演示用)。
|
-- * 构造
|
||||||
makeSelect :: [String] -> String -> Maybe Expr -> Query
|
-- | 造一个最简单的 SELECT
|
||||||
|
makeSelect :: [String] -> String -> Maybe Expr -> Statement
|
||||||
makeSelect cols tbl w =
|
makeSelect cols tbl w =
|
||||||
Select
|
Select
|
||||||
{ selectCols = cols
|
{ selectCols = cols
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{-# LANGUAGE ImportQualifiedPost #-}
|
{-# LANGUAGE ImportQualifiedPost #-}
|
||||||
|
|
||||||
module ChuSQL.Syntax.Parser (parseQuery) where
|
module ChuSQL.Syntax.Parser (parseStatement) where
|
||||||
|
|
||||||
|
import ChuSQL.Model (Column (..))
|
||||||
import ChuSQL.Syntax.AST
|
import ChuSQL.Syntax.AST
|
||||||
import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
|
import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
|
||||||
import Data.Char (isAlpha, isAlphaNum, toLower)
|
import Data.Char (isAlpha, isAlphaNum, toLower)
|
||||||
@@ -12,48 +13,50 @@ import Text.Megaparsec
|
|||||||
import Text.Megaparsec.Char
|
import Text.Megaparsec.Char
|
||||||
import Text.Megaparsec.Char.Lexer qualified as Lxr
|
import Text.Megaparsec.Char.Lexer qualified as Lxr
|
||||||
|
|
||||||
-- SQL 解析器:把 SQL 文本解析成 AST。词法层管空白、关键字、标识符、字面量与符号;
|
-- SQL 解析:词法 + 语法,把文本变成语句树。
|
||||||
-- 语法层按优先级 比较 > AND > OR 自底向上组装表达式。
|
|
||||||
|
|
||||||
|
-- * 词法
|
||||||
|
-- | 解析器类型
|
||||||
type Parser = Parsec Void String
|
type Parser = Parsec Void String
|
||||||
|
|
||||||
-- * 词法层
|
|
||||||
|
|
||||||
-- | 跳过空白(含换行)。
|
-- | 跳过空白
|
||||||
sc :: Parser ()
|
sc :: Parser ()
|
||||||
sc = Lxr.space space1 empty empty
|
sc = Lxr.space space1 empty empty
|
||||||
|
|
||||||
-- | 消费一个词法单元以及其后的空白。
|
-- | 词后面吃掉空白
|
||||||
lexeme :: Parser a -> Parser a
|
lexeme :: Parser a -> Parser a
|
||||||
lexeme = Lxr.lexeme sc
|
lexeme = Lxr.lexeme sc
|
||||||
|
|
||||||
-- | 匹配一个符号(如 ","、"("),并吃掉其后的空白。
|
-- | 读一个符号
|
||||||
symbol :: String -> Parser String
|
symbol :: String -> Parser String
|
||||||
symbol = Lxr.symbol sc
|
symbol = Lxr.symbol sc
|
||||||
|
|
||||||
-- | 大小写不敏感的关键字;其后不能紧跟标识符字符,避免把 @selection@ 读成 @select@。
|
{- | 大小写不敏感的关键字;其后不能紧跟标识符字符,避免把 @selection@ 读成 @select@。
|
||||||
-- 整体用 'try' 包住:否则 @ORDER@ 会被 @keyword "or"@ 吃掉前缀 @OR@ 再报错,导致无法回溯。
|
整体用 'try' 包住:否则 @ORDER@ 会被 @keyword "or"@ 吃掉前缀 @OR@ 再报错,导致无法回溯。
|
||||||
|
-}
|
||||||
|
-- | 读关键字(不分大小写)
|
||||||
keyword :: String -> Parser ()
|
keyword :: String -> Parser ()
|
||||||
keyword k = lexeme $ try $ do
|
keyword k = lexeme $ try $ do
|
||||||
_ <- string' k
|
_ <- string' k
|
||||||
notFollowedBy (satisfy isIdentChar)
|
notFollowedBy (satisfy isIdentChar)
|
||||||
|
|
||||||
-- | 标识符:字母或下划线开头,其后可跟字母、数字或下划线。
|
-- | 读一个名字
|
||||||
identifier :: Parser String
|
identifier :: Parser String
|
||||||
identifier = lexeme $ do
|
identifier = lexeme $ do
|
||||||
first <- satisfy (\ch -> isAlpha ch || ch == '_')
|
first <- satisfy (\ch -> isAlpha ch || ch == '_')
|
||||||
rest <- many (satisfy isIdentChar)
|
rest <- many (satisfy isIdentChar)
|
||||||
return (first : rest)
|
return (first : rest)
|
||||||
|
|
||||||
-- | 标识符允许的字符:字母、数字或下划线。
|
-- | 名字里允许的字符
|
||||||
isIdentChar :: Char -> Bool
|
isIdentChar :: Char -> Bool
|
||||||
isIdentChar ch = isAlphaNum ch || ch == '_'
|
isIdentChar ch = isAlphaNum ch || ch == '_'
|
||||||
|
|
||||||
-- | 十进制整数字面量。
|
-- | 读一个整数
|
||||||
integer :: Parser Int
|
integer :: Parser Int
|
||||||
integer = lexeme Lxr.decimal
|
integer = lexeme Lxr.decimal
|
||||||
|
|
||||||
-- | 单引号字符串字面量,遵循标准 SQL 转义规则:
|
-- | 读一个字符串字面量
|
||||||
stringLit :: Parser String
|
stringLit :: Parser String
|
||||||
stringLit = lexeme $ do
|
stringLit = lexeme $ do
|
||||||
_ <- char '\''
|
_ <- char '\''
|
||||||
@@ -61,8 +64,10 @@ stringLit = lexeme $ do
|
|||||||
_ <- char '\''
|
_ <- char '\''
|
||||||
return s
|
return s
|
||||||
|
|
||||||
-- | 字符串内的一个字符:@''@ 折叠成一个单引号,其余字符(含反斜杠)原样保留。
|
{- | 字符串内的一个字符:@''@ 折叠成一个单引号,其余字符(含反斜杠)原样保留。
|
||||||
-- 两个分支都不会在失败时消耗输入,因此可以安全地放进 'many'。
|
两个分支都不会在失败时消耗输入,因此可以安全地放进 'many'。
|
||||||
|
-}
|
||||||
|
-- | 字符串里的一个字符
|
||||||
stringChar :: Parser Char
|
stringChar :: Parser Char
|
||||||
stringChar =
|
stringChar =
|
||||||
choice
|
choice
|
||||||
@@ -70,31 +75,34 @@ stringChar =
|
|||||||
, satisfy (/= '\'')
|
, satisfy (/= '\'')
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | 解析 ASC/DESC
|
-- * 子句
|
||||||
|
-- | 升序或降序
|
||||||
sortDir :: Parser SortDir
|
sortDir :: Parser SortDir
|
||||||
sortDir = (Desc <$ keyword "desc") <|> (Asc <$ keyword "asc") <|> pure Asc
|
sortDir = (Desc <$ keyword "desc") <|> (Asc <$ keyword "asc") <|> pure Asc
|
||||||
|
|
||||||
-- | 解析 LIMIT
|
-- | LIMIT 后面的整数
|
||||||
limitClause :: Parser Int
|
limitClause :: Parser Int
|
||||||
limitClause = do
|
limitClause = do
|
||||||
keyword "limit"
|
keyword "limit"
|
||||||
integer
|
integer
|
||||||
|
|
||||||
-- | 解析JOIN和限定列名
|
-- | 能带表名的列名
|
||||||
qualifiedName :: Parser String
|
qualifiedName :: Parser String
|
||||||
qualifiedName = do
|
qualifiedName = do
|
||||||
first <- identifier
|
first <- identifier
|
||||||
rest <- many (symbol "." *> identifier)
|
rest <- many (symbol "." *> identifier)
|
||||||
return (intercalate "." (first : rest))
|
return (intercalate "." (first : rest))
|
||||||
|
|
||||||
-- * 表达式层
|
|
||||||
|
|
||||||
-- | 表达式入口,最低优先级。
|
-- * 表达式
|
||||||
|
-- | 读一个表达式
|
||||||
expr :: Parser Expr
|
expr :: Parser Expr
|
||||||
expr = makeExprParser atom operatorTable
|
expr = makeExprParser atom operatorTable
|
||||||
|
|
||||||
-- | 运算符表。注意 'makeExprParser' 要求各层按优先级从高到低排列:
|
{- | 运算符表。注意 'makeExprParser' 要求各层按优先级从高到低排列:
|
||||||
-- 比较运算符绑定最紧,'AND' 次之,'OR' 最松。
|
比较运算符绑定最紧,'AND' 次之,'OR' 最松。
|
||||||
|
-}
|
||||||
|
-- | 运算符优先级(从紧到松)
|
||||||
operatorTable :: [[Operator Parser Expr]]
|
operatorTable :: [[Operator Parser Expr]]
|
||||||
operatorTable =
|
operatorTable =
|
||||||
[
|
[
|
||||||
@@ -106,7 +114,7 @@ operatorTable =
|
|||||||
, [InfixL (Or <$ keyword "OR")] -- 最低优先级
|
, [InfixL (Or <$ keyword "OR")] -- 最低优先级
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | 原子表达式:字面量、列引用或括号表达式。
|
-- | 最小的表达式单位
|
||||||
atom :: Parser Expr
|
atom :: Parser Expr
|
||||||
atom =
|
atom =
|
||||||
choice
|
choice
|
||||||
@@ -116,23 +124,22 @@ atom =
|
|||||||
, between (symbol "(") (symbol ")") expr
|
, between (symbol "(") (symbol ")") expr
|
||||||
]
|
]
|
||||||
|
|
||||||
-- * 语句层
|
|
||||||
|
|
||||||
-- | 显式指定排序的语句
|
-- | 一个排序列
|
||||||
orderItem :: Parser (String, SortDir)
|
orderItem :: Parser (String, SortDir)
|
||||||
orderItem = do
|
orderItem = do
|
||||||
col <- qualifiedName
|
col <- qualifiedName
|
||||||
dir <- sortDir
|
dir <- sortDir
|
||||||
return (col, dir)
|
return (col, dir)
|
||||||
|
|
||||||
-- | @ORDER BY col [ASC|DESC], ...@。
|
-- | ORDER BY 的列表
|
||||||
orderByClause :: Parser [(String, SortDir)]
|
orderByClause :: Parser [(String, SortDir)]
|
||||||
orderByClause = do
|
orderByClause = do
|
||||||
keyword "order"
|
keyword "order"
|
||||||
keyword "by"
|
keyword "by"
|
||||||
sepBy1 orderItem (symbol ",")
|
sepBy1 orderItem (symbol ",")
|
||||||
|
|
||||||
-- | @col = expr@,用于 UPDATE 的 SET 子句。
|
-- | SET 里的一条赋值
|
||||||
assignment :: Parser (String, Expr)
|
assignment :: Parser (String, Expr)
|
||||||
assignment = do
|
assignment = do
|
||||||
col <- identifier
|
col <- identifier
|
||||||
@@ -140,9 +147,8 @@ assignment = do
|
|||||||
e <- expr
|
e <- expr
|
||||||
return (col, e)
|
return (col, e)
|
||||||
|
|
||||||
-- * 从句层
|
|
||||||
|
|
||||||
-- | SQL 保留字,不能用作表别名
|
-- | 保留字清单
|
||||||
reservedWords :: [String]
|
reservedWords :: [String]
|
||||||
reservedWords =
|
reservedWords =
|
||||||
[ "select"
|
[ "select"
|
||||||
@@ -163,24 +169,28 @@ reservedWords =
|
|||||||
, "delete"
|
, "delete"
|
||||||
, "update"
|
, "update"
|
||||||
, "set"
|
, "set"
|
||||||
|
, "create"
|
||||||
|
, "table"
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | 表别名。别名不能是保留字:否则 @FROM users WHERE age > 18@ 会把 @WHERE@
|
{- | 表别名。别名不能是保留字:否则 @FROM users WHERE age > 18@ 会把 @WHERE@
|
||||||
-- 当成 @users@ 的别名吃掉,后面真正的 WHERE 子句就再也解析不到。
|
当成 @users@ 的别名吃掉,后面真正的 WHERE 子句就再也解析不到。
|
||||||
-- 用 'try' 包住,一旦命中保留字就整体回溯,让 'optional' 正常返回 'Nothing'。
|
用 'try' 包住,一旦命中保留字就整体回溯,让 'optional' 正常返回 'Nothing'。
|
||||||
|
-}
|
||||||
|
-- | 别名(不许用保留字)
|
||||||
aliasName :: Parser String
|
aliasName :: Parser String
|
||||||
aliasName = try $ do
|
aliasName = try $ do
|
||||||
name <- identifier
|
name <- identifier
|
||||||
if map toLower name `elem` reservedWords then empty else return name
|
if map toLower name `elem` reservedWords then empty else return name
|
||||||
|
|
||||||
-- | @表名 [别名]@。
|
-- | 表名 + 可选别名
|
||||||
tableRef :: Parser (Maybe String, String)
|
tableRef :: Parser (Maybe String, String)
|
||||||
tableRef = do
|
tableRef = do
|
||||||
tbl <- identifier
|
tbl <- identifier
|
||||||
mAlias <- optional aliasName
|
mAlias <- optional aliasName
|
||||||
return (mAlias, tbl)
|
return (mAlias, tbl)
|
||||||
|
|
||||||
-- | @JOIN 表 [别名] ON 条件@。
|
-- | 一个 JOIN ... ON
|
||||||
joinClause :: Parser (Maybe String, String, Expr)
|
joinClause :: Parser (Maybe String, String, Expr)
|
||||||
joinClause = do
|
joinClause = do
|
||||||
keyword "join"
|
keyword "join"
|
||||||
@@ -189,14 +199,14 @@ joinClause = do
|
|||||||
cond <- expr
|
cond <- expr
|
||||||
return (mAlias, tbl, cond)
|
return (mAlias, tbl, cond)
|
||||||
|
|
||||||
-- | FROM 从句:一张表 + 任意多个 JOIN。
|
-- | FROM 子句(可含多个 JOIN)
|
||||||
fromClause :: Parser FromClause
|
fromClause :: Parser FromClause
|
||||||
fromClause = do
|
fromClause = do
|
||||||
(mAlias, tbl) <- tableRef
|
(mAlias, tbl) <- tableRef
|
||||||
joins <- many joinClause
|
joins <- many joinClause
|
||||||
return (foldl (\acc (a, t, c) -> FromJoin acc a t c) (FromTable mAlias tbl) joins)
|
return (foldl (\acc (a, t, c) -> FromJoin acc a t c) (FromTable mAlias tbl) joins)
|
||||||
|
|
||||||
-- | 选择列表:@*@ 或逗号分隔的列名。
|
-- | SELECT 的列清单
|
||||||
selectList :: Parser [String]
|
selectList :: Parser [String]
|
||||||
selectList =
|
selectList =
|
||||||
choice
|
choice
|
||||||
@@ -204,9 +214,46 @@ selectList =
|
|||||||
, (:) <$> qualifiedName <*> many (symbol "," *> qualifiedName)
|
, (:) <$> qualifiedName <*> many (symbol "," *> qualifiedName)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | @SELECT ... FROM ... [WHERE ...]@。
|
|
||||||
selectQuery :: Parser Query
|
-- | 读 CREATE TABLE
|
||||||
selectQuery = do
|
createTableStatement :: Parser Statement
|
||||||
|
createTableStatement = do
|
||||||
|
keyword "create"
|
||||||
|
keyword "table"
|
||||||
|
name <- identifier
|
||||||
|
cols <- between (symbol "(") (symbol ")") (sepBy1 columnDef (symbol ","))
|
||||||
|
return (CreateTable name cols)
|
||||||
|
|
||||||
|
-- | 读 DROP TABLE
|
||||||
|
dropTableStatement :: Parser Statement
|
||||||
|
dropTableStatement = do
|
||||||
|
keyword "drop"
|
||||||
|
keyword "table"
|
||||||
|
name <- identifier
|
||||||
|
return (DropTable name)
|
||||||
|
|
||||||
|
-- | 建表时的一列
|
||||||
|
columnDef :: Parser (String, Column)
|
||||||
|
columnDef = do
|
||||||
|
name <- identifier
|
||||||
|
ty <- columnType
|
||||||
|
return (name, ty)
|
||||||
|
|
||||||
|
-- | 列类型关键字
|
||||||
|
columnType :: Parser Column
|
||||||
|
columnType =
|
||||||
|
(keyword "integer" >> pure TInt)
|
||||||
|
<|> (keyword "int" >> pure TInt)
|
||||||
|
<|> (keyword "varchar" >> pure TStr)
|
||||||
|
<|> (keyword "text" >> pure TStr)
|
||||||
|
<|> (keyword "str" >> pure TStr)
|
||||||
|
<|> (keyword "boolean" >> pure TBool)
|
||||||
|
<|> (keyword "bool" >> pure TBool)
|
||||||
|
|
||||||
|
-- * 语句
|
||||||
|
-- | 读 SELECT
|
||||||
|
selectStatement :: Parser Statement
|
||||||
|
selectStatement = do
|
||||||
keyword "select"
|
keyword "select"
|
||||||
cols <- selectList
|
cols <- selectList
|
||||||
keyword "from"
|
keyword "from"
|
||||||
@@ -223,9 +270,9 @@ selectQuery = do
|
|||||||
, selectLimit = mLimit
|
, selectLimit = mLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | @INSERT ... INTO ... VALUES ...@。
|
-- | 读 INSERT
|
||||||
insertQuery :: Parser Query
|
insertStatement :: Parser Statement
|
||||||
insertQuery = do
|
insertStatement = do
|
||||||
keyword "INSERT"
|
keyword "INSERT"
|
||||||
keyword "INTO"
|
keyword "INTO"
|
||||||
tbl <- identifier
|
tbl <- identifier
|
||||||
@@ -242,18 +289,18 @@ insertQuery = do
|
|||||||
(sepBy1 atom (symbol ","))
|
(sepBy1 atom (symbol ","))
|
||||||
return (Insert tbl cols vals)
|
return (Insert tbl cols vals)
|
||||||
|
|
||||||
-- | @DELETE ... FROM ... [WHERE ...]@。
|
-- | 读 DELETE
|
||||||
deleteQuery :: Parser Query
|
deleteStatement :: Parser Statement
|
||||||
deleteQuery = do
|
deleteStatement = do
|
||||||
keyword "DELETE"
|
keyword "DELETE"
|
||||||
keyword "FROM"
|
keyword "FROM"
|
||||||
tbl <- identifier
|
tbl <- identifier
|
||||||
mWhere <- optional (keyword "WHERE" *> expr)
|
mWhere <- optional (keyword "WHERE" *> expr)
|
||||||
return (Delete tbl mWhere)
|
return (Delete tbl mWhere)
|
||||||
|
|
||||||
-- | UPDATE ... SET col = val, ... [WHERE ...]@
|
-- | 读 UPDATE
|
||||||
updateQuery :: Parser Query
|
updateStatement :: Parser Statement
|
||||||
updateQuery = do
|
updateStatement = do
|
||||||
keyword "update"
|
keyword "update"
|
||||||
tbl <- identifier
|
tbl <- identifier
|
||||||
keyword "set"
|
keyword "set"
|
||||||
@@ -261,9 +308,19 @@ updateQuery = do
|
|||||||
mWhere <- optional (keyword "where" *> expr)
|
mWhere <- optional (keyword "where" *> expr)
|
||||||
return (Update tbl assigns mWhere)
|
return (Update tbl assigns mWhere)
|
||||||
|
|
||||||
-- | 解析一条完整的语句;失败时返回可直接展示的错误信息。
|
-- * 入口
|
||||||
parseQuery :: String -> Either String Query
|
-- | 解析总入口
|
||||||
parseQuery input =
|
parseStatement :: String -> Either String Statement
|
||||||
case runParser (sc *> (selectQuery <|> insertQuery <|> deleteQuery <|> updateQuery) <* sc <* eof) "<query>" input of
|
parseStatement input =
|
||||||
|
case runParser (sc *> statementP <* sc <* eof) "<query>" input of
|
||||||
Left err -> Left (errorBundlePretty err)
|
Left err -> Left (errorBundlePretty err)
|
||||||
Right q -> Right q
|
Right q -> Right q
|
||||||
|
where
|
||||||
|
-- \| 依次尝试各种语句
|
||||||
|
statementP =
|
||||||
|
selectStatement
|
||||||
|
<|> insertStatement
|
||||||
|
<|> deleteStatement
|
||||||
|
<|> updateStatement
|
||||||
|
<|> createTableStatement
|
||||||
|
<|> dropTableStatement
|
||||||
+196
-136
@@ -62,19 +62,19 @@ main = hspec $ do
|
|||||||
|
|
||||||
describe "ChuSQL.Syntax.Parser" $ do
|
describe "ChuSQL.Syntax.Parser" $ do
|
||||||
it "parses a simple SELECT" $ do
|
it "parses a simple SELECT" $ do
|
||||||
parseQuery "SELECT name FROM users"
|
parseStatement "SELECT name FROM users"
|
||||||
`shouldBe` Right (makeSelect ["name"] "users" Nothing)
|
`shouldBe` Right (makeSelect ["name"] "users" Nothing)
|
||||||
|
|
||||||
it "parses SELECT *" $ do
|
it "parses SELECT *" $ do
|
||||||
parseQuery "SELECT * FROM users"
|
parseStatement "SELECT * FROM users"
|
||||||
`shouldBe` Right (makeSelect ["*"] "users" Nothing)
|
`shouldBe` Right (makeSelect ["*"] "users" Nothing)
|
||||||
|
|
||||||
it "parses multiple columns" $ do
|
it "parses multiple columns" $ do
|
||||||
parseQuery "SELECT name, age FROM users"
|
parseStatement "SELECT name, age FROM users"
|
||||||
`shouldBe` Right (makeSelect ["name", "age"] "users" Nothing)
|
`shouldBe` Right (makeSelect ["name", "age"] "users" Nothing)
|
||||||
|
|
||||||
it "parses WHERE with >" $ do
|
it "parses WHERE with >" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE age > 18"
|
parseStatement "SELECT name FROM users WHERE age > 18"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -83,7 +83,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses AND" $ do
|
it "parses AND" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE age > 18 AND age < 30"
|
parseStatement "SELECT name FROM users WHERE age > 18 AND age < 30"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -97,7 +97,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses OR" $ do
|
it "parses OR" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE age < 18 OR age > 60"
|
parseStatement "SELECT name FROM users WHERE age < 18 OR age > 60"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -111,7 +111,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "is case-insensitive for keywords" $ do
|
it "is case-insensitive for keywords" $ do
|
||||||
parseQuery "select name from users where age > 18"
|
parseStatement "select name from users where age > 18"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -120,11 +120,11 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "tolerates extra whitespace" $ do
|
it "tolerates extra whitespace" $ do
|
||||||
parseQuery "SELECT name FROM users"
|
parseStatement "SELECT name FROM users"
|
||||||
`shouldBe` Right (makeSelect ["name"] "users" Nothing)
|
`shouldBe` Right (makeSelect ["name"] "users" Nothing)
|
||||||
|
|
||||||
it "parses a string literal in WHERE" $ do
|
it "parses a string literal in WHERE" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE name = 'Alice'"
|
parseStatement "SELECT name FROM users WHERE name = 'Alice'"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -133,7 +133,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "uses standard SQL escaping: '' is one quote" $ do
|
it "uses standard SQL escaping: '' is one quote" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE name = 'It''s ok'"
|
parseStatement "SELECT name FROM users WHERE name = 'It''s ok'"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -142,7 +142,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "treats a backslash as an ordinary character" $ do
|
it "treats a backslash as an ordinary character" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE name = 'a\\b'"
|
parseStatement "SELECT name FROM users WHERE name = 'a\\b'"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -151,7 +151,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses an empty string literal" $ do
|
it "parses an empty string literal" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE name = ''"
|
parseStatement "SELECT name FROM users WHERE name = ''"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -160,7 +160,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses a string that is a single quote" $ do
|
it "parses a string that is a single quote" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE name = ''''"
|
parseStatement "SELECT name FROM users WHERE name = ''''"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -169,15 +169,15 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "rejects an unterminated string literal" $ do
|
it "rejects an unterminated string literal" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE name = 'oops"
|
parseStatement "SELECT name FROM users WHERE name = 'oops"
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "allows digits inside identifiers" $ do
|
it "allows digits inside identifiers" $ do
|
||||||
parseQuery "SELECT user1 FROM users"
|
parseStatement "SELECT user1 FROM users"
|
||||||
`shouldBe` Right (makeSelect ["user1"] "users" Nothing)
|
`shouldBe` Right (makeSelect ["user1"] "users" Nothing)
|
||||||
|
|
||||||
it "honours parentheses and AND/OR precedence" $ do
|
it "honours parentheses and AND/OR precedence" $ do
|
||||||
parseQuery "SELECT name FROM users WHERE (age > 18 OR age < 5) AND name = 'Bob'"
|
parseStatement "SELECT name FROM users WHERE (age > 18 OR age < 5) AND name = 'Bob'"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -194,16 +194,16 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "rejects trailing junk after a complete query" $ do
|
it "rejects trailing junk after a complete query" $ do
|
||||||
parseQuery "SELECT name FROM users extra junk"
|
parseStatement "SELECT name FROM users extra junk"
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left on missing table name" $ do
|
it "returns Left on missing table name" $ do
|
||||||
parseQuery "SELECT name FROM"
|
parseStatement "SELECT name FROM"
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
describe "ChuSQL.Syntax.Parser (JOIN)" $ do
|
describe "ChuSQL.Syntax.Parser (JOIN)" $ do
|
||||||
it "parses a simple JOIN without aliases" $ do
|
it "parses a simple JOIN without aliases" $ do
|
||||||
parseQuery "SELECT name FROM users JOIN orders ON id = user_id"
|
parseStatement "SELECT name FROM users JOIN orders ON id = user_id"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( Select
|
( Select
|
||||||
{ selectCols = ["name"]
|
{ selectCols = ["name"]
|
||||||
@@ -220,7 +220,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses JOIN with aliases" $ do
|
it "parses JOIN with aliases" $ do
|
||||||
parseQuery "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id"
|
parseStatement "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( Select
|
( Select
|
||||||
{ selectCols = ["u.name"]
|
{ selectCols = ["u.name"]
|
||||||
@@ -237,13 +237,13 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses qualified column names" $ do
|
it "parses qualified column names" $ do
|
||||||
case parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id" of
|
case parseStatement "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id" of
|
||||||
Right q -> selectCols q `shouldBe` ["u.name", "o.product"]
|
Right q -> selectCols q `shouldBe` ["u.name", "o.product"]
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
describe "ChuSQL.Engine (SELECT)" $ do
|
describe "ChuSQL.Engine (SELECT)" $ do
|
||||||
it "returns all rows without WHERE" $ do
|
it "returns all rows without WHERE" $ do
|
||||||
rowsOf (runQuery testDB (makeSelect ["*"] "users" Nothing))
|
rowsOf (runStatement testDB (makeSelect ["*"] "users" Nothing))
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("id", VInt 1), ("name", VStr "Alice"), ("age", VInt 25)]
|
[ [("id", VInt 1), ("name", VStr "Alice"), ("age", VInt 25)]
|
||||||
, [("id", VInt 2), ("name", VStr "Bob"), ("age", VInt 17)]
|
, [("id", VInt 2), ("name", VStr "Bob"), ("age", VInt 17)]
|
||||||
@@ -252,7 +252,7 @@ main = hspec $ do
|
|||||||
|
|
||||||
it "filters with WHERE" $ do
|
it "filters with WHERE" $ do
|
||||||
rowsOf
|
rowsOf
|
||||||
( runQuery
|
( runStatement
|
||||||
testDB
|
testDB
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -266,7 +266,7 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "projects only the requested columns" $ do
|
it "projects only the requested columns" $ do
|
||||||
rowsOf (runQuery testDB (makeSelect ["name"] "users" Nothing))
|
rowsOf (runStatement testDB (makeSelect ["name"] "users" Nothing))
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Bob")]
|
, [("name", VStr "Bob")]
|
||||||
@@ -274,11 +274,11 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "returns Left for an unknown table" $ do
|
it "returns Left for an unknown table" $ do
|
||||||
runQuery testDB (makeSelect ["name"] "nonexistent" Nothing)
|
runStatement testDB (makeSelect ["name"] "nonexistent" Nothing)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left for an unknown column" $ do
|
it "returns Left for an unknown column" $ do
|
||||||
runQuery
|
runStatement
|
||||||
testDB
|
testDB
|
||||||
( makeSelect
|
( makeSelect
|
||||||
["name"]
|
["name"]
|
||||||
@@ -288,31 +288,31 @@ main = hspec $ do
|
|||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left when WHERE is not a boolean" $ do
|
it "returns Left when WHERE is not a boolean" $ do
|
||||||
runQuery testDB (makeSelect ["name"] "users" (Just (Col "age")))
|
runStatement testDB (makeSelect ["name"] "users" (Just (Col "age")))
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left for an unknown projected column" $ do
|
it "returns Left for an unknown projected column" $ do
|
||||||
runQuery testDB (makeSelect ["nope"] "users" Nothing)
|
runStatement testDB (makeSelect ["nope"] "users" Nothing)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
describe "ChuSQL.Engine (INSERT)" $ do
|
describe "ChuSQL.Engine (INSERT)" $ do
|
||||||
it "parses a simple INSERT" $ do
|
it "parses a simple INSERT" $ do
|
||||||
parseQuery "INSERT INTO users (name, age) VALUES ('Dave', 22)"
|
parseStatement "INSERT INTO users (name, age) VALUES ('Dave', 22)"
|
||||||
`shouldBe` Right (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22])
|
`shouldBe` Right (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22])
|
||||||
|
|
||||||
it "parses INSERT with a single column" $ do
|
it "parses INSERT with a single column" $ do
|
||||||
parseQuery "INSERT INTO users (name) VALUES ('Eve')"
|
parseStatement "INSERT INTO users (name) VALUES ('Eve')"
|
||||||
`shouldBe` Right (Insert "users" ["name"] [LitStr "Eve"])
|
`shouldBe` Right (Insert "users" ["name"] [LitStr "Eve"])
|
||||||
|
|
||||||
it "parses INSERT case-insensitively" $ do
|
it "parses INSERT case-insensitively" $ do
|
||||||
parseQuery "insert into users (name, age) values ('Dave', 22)"
|
parseStatement "insert into users (name, age) values ('Dave', 22)"
|
||||||
`shouldBe` Right (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22])
|
`shouldBe` Right (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22])
|
||||||
|
|
||||||
it "executes INSERT and adds a row at the end" $ do
|
it "executes INSERT and adds a row at the end" $ do
|
||||||
case runQuery testDB (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22]) of
|
case runStatement testDB (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22]) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (runQuery db' (makeSelect ["name", "age"] "users" Nothing))
|
rowsOf (runStatement db' (makeSelect ["name", "age"] "users" Nothing))
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice"), ("age", VInt 25)]
|
[ [("name", VStr "Alice"), ("age", VInt 25)]
|
||||||
, [("name", VStr "Bob"), ("age", VInt 17)]
|
, [("name", VStr "Bob"), ("age", VInt 17)]
|
||||||
@@ -321,76 +321,76 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "does not modify the original database" $ do
|
it "does not modify the original database" $ do
|
||||||
case runQuery testDB (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22]) of
|
case runStatement testDB (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22]) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (_, _) -> do
|
Right (_, _) -> do
|
||||||
rowsOf (runQuery testDB (makeSelect ["name"] "users" Nothing))
|
rowsOf (runStatement testDB (makeSelect ["name"] "users" Nothing))
|
||||||
`shouldSatisfy` \r -> case r of
|
`shouldSatisfy` \r -> case r of
|
||||||
Right rows -> length rows == 3
|
Right rows -> length rows == 3
|
||||||
Left _ -> False
|
Left _ -> False
|
||||||
|
|
||||||
it "returns Left when column count does not match value count" $ do
|
it "returns Left when column count does not match value count" $ do
|
||||||
runQuery testDB (Insert "users" ["name", "age"] [LitStr "Dave"])
|
runStatement testDB (Insert "users" ["name", "age"] [LitStr "Dave"])
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left for an unknown table" $ do
|
it "returns Left for an unknown table" $ do
|
||||||
runQuery testDB (Insert "nonexistent" ["name"] [LitStr "X"])
|
runStatement testDB (Insert "nonexistent" ["name"] [LitStr "X"])
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left when a value has the wrong type for comparison" $ do
|
it "returns Left when a value has the wrong type for comparison" $ do
|
||||||
runQuery testDB (Insert "users" ["name"] [Col "other"])
|
runStatement testDB (Insert "users" ["name"] [Col "other"])
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
describe "ChuSQL.Engine (DELETE)" $ do
|
describe "ChuSQL.Engine (DELETE)" $ do
|
||||||
it "parses DELETE with WHERE" $ do
|
it "parses DELETE with WHERE" $ do
|
||||||
parseQuery "DELETE FROM users WHERE age < 18"
|
parseStatement "DELETE FROM users WHERE age < 18"
|
||||||
`shouldBe` Right (Delete "users" (Just (Lt (Col "age") (LitInt 18))))
|
`shouldBe` Right (Delete "users" (Just (Lt (Col "age") (LitInt 18))))
|
||||||
|
|
||||||
it "parses DELETE without WHERE" $ do
|
it "parses DELETE without WHERE" $ do
|
||||||
parseQuery "DELETE FROM users"
|
parseStatement "DELETE FROM users"
|
||||||
`shouldBe` Right (Delete "users" Nothing)
|
`shouldBe` Right (Delete "users" Nothing)
|
||||||
|
|
||||||
it "parses DELETE case-insensitively" $ do
|
it "parses DELETE case-insensitively" $ do
|
||||||
parseQuery "delete from users where age < 18"
|
parseStatement "delete from users where age < 18"
|
||||||
`shouldBe` Right (Delete "users" (Just (Lt (Col "age") (LitInt 18))))
|
`shouldBe` Right (Delete "users" (Just (Lt (Col "age") (LitInt 18))))
|
||||||
|
|
||||||
it "executes DELETE with WHERE, removing matching rows" $ do
|
it "executes DELETE with WHERE, removing matching rows" $ do
|
||||||
case runQuery testDB (Delete "users" (Just (Lt (Col "age") (LitInt 18)))) of
|
case runStatement testDB (Delete "users" (Just (Lt (Col "age") (LitInt 18)))) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (runQuery db' (makeSelect ["name"] "users" Nothing))
|
rowsOf (runStatement db' (makeSelect ["name"] "users" Nothing))
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Carol")]
|
, [("name", VStr "Carol")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "executes DELETE without WHERE, removing all rows" $ do
|
it "executes DELETE without WHERE, removing all rows" $ do
|
||||||
case runQuery testDB (Delete "users" Nothing) of
|
case runStatement testDB (Delete "users" Nothing) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (runQuery db' (makeSelect ["name"] "users" Nothing))
|
rowsOf (runStatement db' (makeSelect ["name"] "users" Nothing))
|
||||||
`shouldBe` Right []
|
`shouldBe` Right []
|
||||||
|
|
||||||
it "does not modify the original database" $ do
|
it "does not modify the original database" $ do
|
||||||
case runQuery testDB (Delete "users" Nothing) of
|
case runStatement testDB (Delete "users" Nothing) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (_, _) -> do
|
Right (_, _) -> do
|
||||||
rowsOf (runQuery testDB (makeSelect ["name"] "users" Nothing))
|
rowsOf (runStatement testDB (makeSelect ["name"] "users" Nothing))
|
||||||
`shouldSatisfy` \r -> case r of
|
`shouldSatisfy` \r -> case r of
|
||||||
Right rows -> length rows == 3
|
Right rows -> length rows == 3
|
||||||
Left _ -> False
|
Left _ -> False
|
||||||
|
|
||||||
it "returns Left for an unknown table" $ do
|
it "returns Left for an unknown table" $ do
|
||||||
runQuery testDB (Delete "nonexistent" Nothing)
|
runStatement testDB (Delete "nonexistent" Nothing)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left when WHERE references an unknown column" $ do
|
it "returns Left when WHERE references an unknown column" $ do
|
||||||
runQuery testDB (Delete "users" (Just (Gt (Col "unknown") (LitInt 18))))
|
runStatement testDB (Delete "users" (Just (Gt (Col "unknown") (LitInt 18))))
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
describe "ChuSQL.Engine (UPDATE)" $ do
|
describe "ChuSQL.Engine (UPDATE)" $ do
|
||||||
it "parses UPDATE with WHERE" $ do
|
it "parses UPDATE with WHERE" $ do
|
||||||
parseQuery "UPDATE users SET age = 26 WHERE name = 'Alice'"
|
parseStatement "UPDATE users SET age = 26 WHERE name = 'Alice'"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( Update
|
( Update
|
||||||
"users"
|
"users"
|
||||||
@@ -399,12 +399,12 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses UPDATE without WHERE" $ do
|
it "parses UPDATE without WHERE" $ do
|
||||||
parseQuery "UPDATE users SET age = 26"
|
parseStatement "UPDATE users SET age = 26"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
(Update "users" [("age", LitInt 26)] Nothing)
|
(Update "users" [("age", LitInt 26)] Nothing)
|
||||||
|
|
||||||
it "parses multiple assignments" $ do
|
it "parses multiple assignments" $ do
|
||||||
parseQuery "UPDATE users SET age = 26, name = 'Dave'"
|
parseStatement "UPDATE users SET age = 26, name = 'Dave'"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( Update
|
( Update
|
||||||
"users"
|
"users"
|
||||||
@@ -413,7 +413,7 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "parses UPDATE case-insensitively" $ do
|
it "parses UPDATE case-insensitively" $ do
|
||||||
parseQuery "update users set age = 26 where name = 'Alice'"
|
parseStatement "update users set age = 26 where name = 'Alice'"
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
( Update
|
( Update
|
||||||
"users"
|
"users"
|
||||||
@@ -422,10 +422,10 @@ main = hspec $ do
|
|||||||
)
|
)
|
||||||
|
|
||||||
it "executes UPDATE with WHERE, modifying matching rows" $ do
|
it "executes UPDATE with WHERE, modifying matching rows" $ do
|
||||||
case runQuery testDB (Update "users" [("age", LitInt 99)] (Just (Eq (Col "name") (LitStr "Alice")))) of
|
case runStatement testDB (Update "users" [("age", LitInt 99)] (Just (Eq (Col "name") (LitStr "Alice")))) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (runQuery db' (makeSelect ["name", "age"] "users" Nothing))
|
rowsOf (runStatement db' (makeSelect ["name", "age"] "users" Nothing))
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice"), ("age", VInt 99)]
|
[ [("name", VStr "Alice"), ("age", VInt 99)]
|
||||||
, [("name", VStr "Bob"), ("age", VInt 17)]
|
, [("name", VStr "Bob"), ("age", VInt 17)]
|
||||||
@@ -433,10 +433,10 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "executes UPDATE without WHERE, modifying all rows" $ do
|
it "executes UPDATE without WHERE, modifying all rows" $ do
|
||||||
case runQuery testDB (Update "users" [("age", LitInt 0)] Nothing) of
|
case runStatement testDB (Update "users" [("age", LitInt 0)] Nothing) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (runQuery db' (makeSelect ["name", "age"] "users" Nothing))
|
rowsOf (runStatement db' (makeSelect ["name", "age"] "users" Nothing))
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice"), ("age", VInt 0)]
|
[ [("name", VStr "Alice"), ("age", VInt 0)]
|
||||||
, [("name", VStr "Bob"), ("age", VInt 0)]
|
, [("name", VStr "Bob"), ("age", VInt 0)]
|
||||||
@@ -444,10 +444,10 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "applies multiple assignments to each row" $ do
|
it "applies multiple assignments to each row" $ do
|
||||||
case runQuery testDB (Update "users" [("age", LitInt 99), ("name", LitStr "X")] Nothing) of
|
case runStatement testDB (Update "users" [("age", LitInt 99), ("name", LitStr "X")] Nothing) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (runQuery db' (makeSelect ["name", "age"] "users" Nothing))
|
rowsOf (runStatement db' (makeSelect ["name", "age"] "users" Nothing))
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "X"), ("age", VInt 99)]
|
[ [("name", VStr "X"), ("age", VInt 99)]
|
||||||
, [("name", VStr "X"), ("age", VInt 99)]
|
, [("name", VStr "X"), ("age", VInt 99)]
|
||||||
@@ -455,51 +455,51 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "does not modify the original database" $ do
|
it "does not modify the original database" $ do
|
||||||
case runQuery testDB (Update "users" [("age", LitInt 0)] Nothing) of
|
case runStatement testDB (Update "users" [("age", LitInt 0)] Nothing) of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (_, _) -> do
|
Right (_, _) -> do
|
||||||
rowsOf (runQuery testDB (makeSelect ["name"] "users" Nothing))
|
rowsOf (runStatement testDB (makeSelect ["name"] "users" Nothing))
|
||||||
`shouldSatisfy` \r -> case r of
|
`shouldSatisfy` \r -> case r of
|
||||||
Right rows -> length rows == 3
|
Right rows -> length rows == 3
|
||||||
Left _ -> False
|
Left _ -> False
|
||||||
|
|
||||||
it "returns Left for an unknown table" $ do
|
it "returns Left for an unknown table" $ do
|
||||||
runQuery testDB (Update "nonexistent" [("age", LitInt 0)] Nothing)
|
runStatement testDB (Update "nonexistent" [("age", LitInt 0)] Nothing)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left when WHERE references an unknown column" $ do
|
it "returns Left when WHERE references an unknown column" $ do
|
||||||
runQuery testDB (Update "users" [("age", LitInt 0)] (Just (Gt (Col "unknown") (LitInt 1))))
|
runStatement testDB (Update "users" [("age", LitInt 0)] (Just (Gt (Col "unknown") (LitInt 1))))
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left when SET references an unknown column" $ do
|
it "returns Left when SET references an unknown column" $ do
|
||||||
runQuery testDB (Update "users" [("unknown", LitInt 0)] Nothing)
|
runStatement testDB (Update "users" [("unknown", LitInt 0)] Nothing)
|
||||||
`shouldSatisfy` \r -> case r of
|
`shouldSatisfy` \r -> case r of
|
||||||
Right _ -> True
|
Right _ -> True
|
||||||
Left _ -> True
|
Left _ -> True
|
||||||
|
|
||||||
describe "ChuSQL.Engine (ORDER BY)" $ do
|
describe "ChuSQL.Engine (ORDER BY)" $ do
|
||||||
it "parses ORDER BY ASC" $ do
|
it "parses ORDER BY ASC" $ do
|
||||||
case parseQuery "SELECT name FROM users ORDER BY age ASC" of
|
case parseStatement "SELECT name FROM users ORDER BY age ASC" of
|
||||||
Right q -> selectOrderBy q `shouldBe` [("age", Asc)]
|
Right q -> selectOrderBy q `shouldBe` [("age", Asc)]
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
it "parses ORDER BY DESC" $ do
|
it "parses ORDER BY DESC" $ do
|
||||||
case parseQuery "SELECT name FROM users ORDER BY age DESC" of
|
case parseStatement "SELECT name FROM users ORDER BY age DESC" of
|
||||||
Right q -> selectOrderBy q `shouldBe` [("age", Desc)]
|
Right q -> selectOrderBy q `shouldBe` [("age", Desc)]
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
it "defaults to ASC" $ do
|
it "defaults to ASC" $ do
|
||||||
case parseQuery "SELECT name FROM users ORDER BY age" of
|
case parseStatement "SELECT name FROM users ORDER BY age" of
|
||||||
Right q -> selectOrderBy q `shouldBe` [("age", Asc)]
|
Right q -> selectOrderBy q `shouldBe` [("age", Asc)]
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
it "parses multiple ORDER BY columns" $ do
|
it "parses multiple ORDER BY columns" $ do
|
||||||
case parseQuery "SELECT name FROM users ORDER BY age DESC, name ASC" of
|
case parseStatement "SELECT name FROM users ORDER BY age DESC, name ASC" of
|
||||||
Right q -> selectOrderBy q `shouldBe` [("age", Desc), ("name", Asc)]
|
Right q -> selectOrderBy q `shouldBe` [("age", Desc), ("name", Asc)]
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
it "parses without ORDER BY" $ do
|
it "parses without ORDER BY" $ do
|
||||||
case parseQuery "SELECT name FROM users" of
|
case parseStatement "SELECT name FROM users" of
|
||||||
Right q -> selectOrderBy q `shouldBe` []
|
Right q -> selectOrderBy q `shouldBe` []
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
@@ -512,7 +512,7 @@ main = hspec $ do
|
|||||||
, selectOrderBy = [("age", Asc)]
|
, selectOrderBy = [("age", Asc)]
|
||||||
, selectLimit = Nothing
|
, selectLimit = Nothing
|
||||||
}
|
}
|
||||||
rowsOf (runQuery testDB q)
|
rowsOf (runStatement testDB q)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Bob")]
|
[ [("name", VStr "Bob")]
|
||||||
, [("name", VStr "Alice")]
|
, [("name", VStr "Alice")]
|
||||||
@@ -528,7 +528,7 @@ main = hspec $ do
|
|||||||
, selectOrderBy = [("age", Desc)]
|
, selectOrderBy = [("age", Desc)]
|
||||||
, selectLimit = Nothing
|
, selectLimit = Nothing
|
||||||
}
|
}
|
||||||
rowsOf (runQuery testDB q)
|
rowsOf (runStatement testDB q)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Carol")]
|
[ [("name", VStr "Carol")]
|
||||||
, [("name", VStr "Alice")]
|
, [("name", VStr "Alice")]
|
||||||
@@ -544,7 +544,7 @@ main = hspec $ do
|
|||||||
, selectOrderBy = [("name", Asc)]
|
, selectOrderBy = [("name", Asc)]
|
||||||
, selectLimit = Nothing
|
, selectLimit = Nothing
|
||||||
}
|
}
|
||||||
rowsOf (runQuery testDB q)
|
rowsOf (runStatement testDB q)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Bob")]
|
, [("name", VStr "Bob")]
|
||||||
@@ -560,23 +560,23 @@ main = hspec $ do
|
|||||||
, selectOrderBy = [("nope", Asc)]
|
, selectOrderBy = [("nope", Asc)]
|
||||||
, selectLimit = Nothing
|
, selectLimit = Nothing
|
||||||
}
|
}
|
||||||
runQuery testDB q `shouldSatisfy` isLeft
|
runStatement testDB q `shouldSatisfy` isLeft
|
||||||
|
|
||||||
describe "ChuSQL.Engine (LIMIT)" $ do
|
describe "ChuSQL.Engine (LIMIT)" $ do
|
||||||
it "parses LIMIT" $ do
|
it "parses LIMIT" $ do
|
||||||
case parseQuery "SELECT * FROM users LIMIT 2" of
|
case parseStatement "SELECT * FROM users LIMIT 2" of
|
||||||
Right q -> selectLimit q `shouldBe` Just 2
|
Right q -> selectLimit q `shouldBe` Just 2
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
it "parses LIMIT after ORDER BY" $ do
|
it "parses LIMIT after ORDER BY" $ do
|
||||||
case parseQuery "SELECT * FROM users ORDER BY age DESC LIMIT 1" of
|
case parseStatement "SELECT * FROM users ORDER BY age DESC LIMIT 1" of
|
||||||
Right q -> do
|
Right q -> do
|
||||||
selectOrderBy q `shouldBe` [("age", Desc)]
|
selectOrderBy q `shouldBe` [("age", Desc)]
|
||||||
selectLimit q `shouldBe` Just 1
|
selectLimit q `shouldBe` Just 1
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
it "parses without LIMIT" $ do
|
it "parses without LIMIT" $ do
|
||||||
case parseQuery "SELECT * FROM users" of
|
case parseStatement "SELECT * FROM users" of
|
||||||
Right q -> selectLimit q `shouldBe` Nothing
|
Right q -> selectLimit q `shouldBe` Nothing
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
|
|
||||||
@@ -589,7 +589,7 @@ main = hspec $ do
|
|||||||
, selectOrderBy = []
|
, selectOrderBy = []
|
||||||
, selectLimit = Just 2
|
, selectLimit = Just 2
|
||||||
}
|
}
|
||||||
rowsOf (runQuery testDB q)
|
rowsOf (runStatement testDB q)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Bob")]
|
, [("name", VStr "Bob")]
|
||||||
@@ -604,7 +604,7 @@ main = hspec $ do
|
|||||||
, selectOrderBy = [("age", Desc)]
|
, selectOrderBy = [("age", Desc)]
|
||||||
, selectLimit = Just 2
|
, selectLimit = Just 2
|
||||||
}
|
}
|
||||||
rowsOf (runQuery testDB q)
|
rowsOf (runStatement testDB q)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Carol")]
|
[ [("name", VStr "Carol")]
|
||||||
, [("name", VStr "Alice")]
|
, [("name", VStr "Alice")]
|
||||||
@@ -619,7 +619,7 @@ main = hspec $ do
|
|||||||
, selectOrderBy = []
|
, selectOrderBy = []
|
||||||
, selectLimit = Just 10
|
, selectLimit = Just 10
|
||||||
}
|
}
|
||||||
rowsOf (runQuery testDB q)
|
rowsOf (runStatement testDB q)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Bob")]
|
, [("name", VStr "Bob")]
|
||||||
@@ -635,16 +635,16 @@ main = hspec $ do
|
|||||||
, selectOrderBy = []
|
, selectOrderBy = []
|
||||||
, selectLimit = Just 0
|
, selectLimit = Just 0
|
||||||
}
|
}
|
||||||
rowsOf (runQuery testDB q)
|
rowsOf (runStatement testDB q)
|
||||||
`shouldBe` Right []
|
`shouldBe` Right []
|
||||||
|
|
||||||
it "rejects negative LIMIT" $ do
|
it "rejects negative LIMIT" $ do
|
||||||
parseQuery "SELECT * FROM users LIMIT -1"
|
parseStatement "SELECT * FROM users LIMIT -1"
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
describe "ChuSQL.Engine (JOIN)" $ do
|
describe "ChuSQL.Engine (JOIN)" $ do
|
||||||
it "executes a two-table JOIN with aliases" $ do
|
it "executes a two-table JOIN with aliases" $ do
|
||||||
rowsOf (parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
||||||
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
||||||
@@ -652,14 +652,14 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "executes JOIN with WHERE" $ do
|
it "executes JOIN with WHERE" $ do
|
||||||
rowsOf (parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
||||||
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "executes JOIN with ORDER BY" $ do
|
it "executes JOIN with ORDER BY" $ do
|
||||||
rowsOf (parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.product DESC" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.product DESC" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("u.name", VStr "Bob"), ("o.product", VStr "Pen")]
|
[ [("u.name", VStr "Bob"), ("o.product", VStr "Pen")]
|
||||||
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
||||||
@@ -667,14 +667,14 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "executes JOIN with LIMIT" $ do
|
it "executes JOIN with LIMIT" $ do
|
||||||
rowsOf (parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id LIMIT 2" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id LIMIT 2" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
||||||
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "returns Left when JOIN table not found" $ do
|
it "returns Left when JOIN table not found" $ do
|
||||||
runQuery
|
runStatement
|
||||||
testDB
|
testDB
|
||||||
( Select
|
( Select
|
||||||
{ selectCols = ["u.name"]
|
{ selectCols = ["u.name"]
|
||||||
@@ -692,7 +692,7 @@ main = hspec $ do
|
|||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "returns Left when ON references an unknown column" $ do
|
it "returns Left when ON references an unknown column" $ do
|
||||||
runQuery
|
runStatement
|
||||||
testDB
|
testDB
|
||||||
( Select
|
( Select
|
||||||
{ selectCols = ["u.name"]
|
{ selectCols = ["u.name"]
|
||||||
@@ -711,7 +711,7 @@ main = hspec $ do
|
|||||||
|
|
||||||
describe "ChuSQL end-to-end" $ do
|
describe "ChuSQL end-to-end" $ do
|
||||||
it "parses and executes a simple query" $ do
|
it "parses and executes a simple query" $ do
|
||||||
rowsOf (parseQuery "SELECT name FROM users WHERE age > 18" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT name FROM users WHERE age > 18" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Carol")]
|
, [("name", VStr "Carol")]
|
||||||
@@ -719,8 +719,8 @@ main = hspec $ do
|
|||||||
|
|
||||||
it "parses and executes a complex query" $ do
|
it "parses and executes a complex query" $ do
|
||||||
rowsOf
|
rowsOf
|
||||||
( parseQuery "SELECT name FROM users WHERE age < 18 OR age > 28"
|
( parseStatement "SELECT name FROM users WHERE age < 18 OR age > 28"
|
||||||
>>= runQuery testDB
|
>>= runStatement testDB
|
||||||
)
|
)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Bob")]
|
[ [("name", VStr "Bob")]
|
||||||
@@ -728,14 +728,14 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "returns Left when parsing fails" $ do
|
it "returns Left when parsing fails" $ do
|
||||||
(parseQuery "SELECT name FROM" >>= runQuery testDB)
|
(parseStatement "SELECT name FROM" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "INSERT then SELECT end-to-end" $ do
|
it "INSERT then SELECT end-to-end" $ do
|
||||||
case parseQuery "INSERT INTO users (name, age) VALUES ('Dave', 22)" >>= runQuery testDB of
|
case parseStatement "INSERT INTO users (name, age) VALUES ('Dave', 22)" >>= runStatement testDB of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (parseQuery "SELECT name FROM users WHERE age > 18" >>= runQuery db')
|
rowsOf (parseStatement "SELECT name FROM users WHERE age > 18" >>= runStatement db')
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Carol")]
|
, [("name", VStr "Carol")]
|
||||||
@@ -743,19 +743,19 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "DELETE then SELECT end-to-end" $ do
|
it "DELETE then SELECT end-to-end" $ do
|
||||||
case parseQuery "DELETE FROM users WHERE age > 20" >>= runQuery testDB of
|
case parseStatement "DELETE FROM users WHERE age > 20" >>= runStatement testDB of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (parseQuery "SELECT name FROM users" >>= runQuery db')
|
rowsOf (parseStatement "SELECT name FROM users" >>= runStatement db')
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Bob")]
|
[ [("name", VStr "Bob")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "UPDATE then SELECT end-to-end" $ do
|
it "UPDATE then SELECT end-to-end" $ do
|
||||||
case parseQuery "UPDATE users SET age = 99 WHERE name = 'Alice'" >>= runQuery testDB of
|
case parseStatement "UPDATE users SET age = 99 WHERE name = 'Alice'" >>= runStatement testDB of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right (db', _) -> do
|
Right (db', _) -> do
|
||||||
rowsOf (parseQuery "SELECT name, age FROM users" >>= runQuery db')
|
rowsOf (parseStatement "SELECT name, age FROM users" >>= runStatement db')
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice"), ("age", VInt 99)]
|
[ [("name", VStr "Alice"), ("age", VInt 99)]
|
||||||
, [("name", VStr "Bob"), ("age", VInt 17)]
|
, [("name", VStr "Bob"), ("age", VInt 17)]
|
||||||
@@ -763,27 +763,27 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "ORDER BY with WHERE end-to-end" $ do
|
it "ORDER BY with WHERE end-to-end" $ do
|
||||||
rowsOf (parseQuery "SELECT name FROM users WHERE age > 18 ORDER BY age DESC" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT name FROM users WHERE age > 18 ORDER BY age DESC" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Carol")]
|
[ [("name", VStr "Carol")]
|
||||||
, [("name", VStr "Alice")]
|
, [("name", VStr "Alice")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "ORDER BY with LIMIT end-to-end" $ do
|
it "ORDER BY with LIMIT end-to-end" $ do
|
||||||
rowsOf (parseQuery "SELECT name FROM users ORDER BY age DESC LIMIT 2" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT name FROM users ORDER BY age DESC LIMIT 2" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Carol")]
|
[ [("name", VStr "Carol")]
|
||||||
, [("name", VStr "Alice")]
|
, [("name", VStr "Alice")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "WHERE ORDER BY LIMIT end-to-end" $ do
|
it "WHERE ORDER BY LIMIT end-to-end" $ do
|
||||||
rowsOf (parseQuery "SELECT name FROM users WHERE age > 15 ORDER BY age DESC LIMIT 1" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT name FROM users WHERE age > 15 ORDER BY age DESC LIMIT 1" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Carol")]
|
[ [("name", VStr "Carol")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "JOIN with WHERE and ORDER BY end-to-end" $ do
|
it "JOIN with WHERE and ORDER BY end-to-end" $ do
|
||||||
rowsOf (parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 15 ORDER BY o.product" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 15 ORDER BY o.product" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
[ [("u.name", VStr "Alice"), ("o.product", VStr "Book")]
|
||||||
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
, [("u.name", VStr "Alice"), ("o.product", VStr "Cup")]
|
||||||
@@ -804,7 +804,7 @@ main = hspec $ do
|
|||||||
sameResultAsUnoptimized "SELECT * FROM users"
|
sameResultAsUnoptimized "SELECT * FROM users"
|
||||||
|
|
||||||
it "pushes a single-side predicate into the left side of a JOIN" $ do
|
it "pushes a single-side predicate into the left side of a JOIN" $ do
|
||||||
case parseQuery "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" of
|
case parseStatement "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right q ->
|
Right q ->
|
||||||
case translate q of
|
case translate q of
|
||||||
@@ -813,7 +813,7 @@ main = hspec $ do
|
|||||||
filterPushedIntoLeft (optimize testDB relOp) `shouldBe` True
|
filterPushedIntoLeft (optimize testDB relOp) `shouldBe` True
|
||||||
|
|
||||||
it "removes a redundant SELECT * projection" $ do
|
it "removes a redundant SELECT * projection" $ do
|
||||||
case parseQuery "SELECT * FROM users" of
|
case parseStatement "SELECT * FROM users" of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right q ->
|
Right q ->
|
||||||
case translate q of
|
case translate q of
|
||||||
@@ -827,7 +827,7 @@ main = hspec $ do
|
|||||||
optimize testDB relOp `shouldBe` relOp
|
optimize testDB relOp `shouldBe` relOp
|
||||||
|
|
||||||
it "is idempotent after reaching a fixed point" $ do
|
it "is idempotent after reaching a fixed point" $ do
|
||||||
case parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" of
|
case parseStatement "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right q ->
|
Right q ->
|
||||||
case translate q of
|
case translate q of
|
||||||
@@ -872,11 +872,11 @@ main = hspec $ do
|
|||||||
sameResultAsUnoptimized "SELECT name FROM users WHERE 'abc' > 1"
|
sameResultAsUnoptimized "SELECT name FROM users WHERE 'abc' > 1"
|
||||||
|
|
||||||
it "returns no rows for a constant-false predicate" $ do
|
it "returns no rows for a constant-false predicate" $ do
|
||||||
rowsOf (parseQuery "SELECT name FROM users WHERE 1 > 2" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT name FROM users WHERE 1 > 2" >>= runStatement testDB)
|
||||||
`shouldBe` Right []
|
`shouldBe` Right []
|
||||||
|
|
||||||
it "returns every row for a constant-true predicate" $ do
|
it "returns every row for a constant-true predicate" $ do
|
||||||
rowsOf (parseQuery "SELECT name FROM users WHERE 1 = 1" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT name FROM users WHERE 1 = 1" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Bob")]
|
, [("name", VStr "Bob")]
|
||||||
@@ -961,123 +961,123 @@ main = hspec $ do
|
|||||||
|
|
||||||
describe "ChuSQL.Semantic" $ do
|
describe "ChuSQL.Semantic" $ do
|
||||||
it "rejects an unknown column in WHERE" $ do
|
it "rejects an unknown column in WHERE" $ do
|
||||||
(parseQuery "SELECT name FROM users WHERE nope > 18" >>= runQuery testDB)
|
(parseStatement "SELECT name FROM users WHERE nope > 18" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects an unknown column in WHERE even when the table is empty" $ do
|
it "rejects an unknown column in WHERE even when the table is empty" $ do
|
||||||
(parseQuery "SELECT name FROM users WHERE nope > 18" >>= runQuery emptyDB)
|
(parseStatement "SELECT name FROM users WHERE nope > 18" >>= runStatement emptyDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects an unknown column in an ON condition" $ do
|
it "rejects an unknown column in an ON condition" $ do
|
||||||
(parseQuery "SELECT u.name FROM users u JOIN orders o ON u.nope = o.user_id" >>= runQuery testDB)
|
(parseStatement "SELECT u.name FROM users u JOIN orders o ON u.nope = o.user_id" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects a type mismatch inside a comparison" $ do
|
it "rejects a type mismatch inside a comparison" $ do
|
||||||
(parseQuery "SELECT name FROM users WHERE name > 18" >>= runQuery testDB)
|
(parseStatement "SELECT name FROM users WHERE name > 18" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects a type mismatch even when the table is empty" $ do
|
it "rejects a type mismatch even when the table is empty" $ do
|
||||||
(parseQuery "SELECT name FROM users WHERE name > 18" >>= runQuery emptyDB)
|
(parseStatement "SELECT name FROM users WHERE name > 18" >>= runStatement emptyDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects a WHERE clause that is not a condition" $ do
|
it "rejects a WHERE clause that is not a condition" $ do
|
||||||
(parseQuery "SELECT name FROM users WHERE age" >>= runQuery testDB)
|
(parseStatement "SELECT name FROM users WHERE age" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects a non-boolean WHERE even when the table is empty" $ do
|
it "rejects a non-boolean WHERE even when the table is empty" $ do
|
||||||
(parseQuery "SELECT name FROM users WHERE age" >>= runQuery emptyDB)
|
(parseStatement "SELECT name FROM users WHERE age" >>= runStatement emptyDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects comparing two different types with =" $ do
|
it "rejects comparing two different types with =" $ do
|
||||||
(parseQuery "SELECT name FROM users WHERE name = 18" >>= runQuery testDB)
|
(parseStatement "SELECT name FROM users WHERE name = 18" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects an unknown column in INSERT" $ do
|
it "rejects an unknown column in INSERT" $ do
|
||||||
(parseQuery "INSERT INTO users (nickname) VALUES (1)" >>= runQuery testDB)
|
(parseStatement "INSERT INTO users (nickname) VALUES (1)" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects a value whose type does not match the column" $ do
|
it "rejects a value whose type does not match the column" $ do
|
||||||
(parseQuery "INSERT INTO users (name, age) VALUES ('Dave', 'abc')" >>= runQuery testDB)
|
(parseStatement "INSERT INTO users (name, age) VALUES ('Dave', 'abc')" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects an unknown column in UPDATE SET" $ do
|
it "rejects an unknown column in UPDATE SET" $ do
|
||||||
(parseQuery "UPDATE users SET nope = 1" >>= runQuery testDB)
|
(parseStatement "UPDATE users SET nope = 1" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects an unknown column in DELETE WHERE" $ do
|
it "rejects an unknown column in DELETE WHERE" $ do
|
||||||
(parseQuery "DELETE FROM users WHERE nope > 1" >>= runQuery testDB)
|
(parseStatement "DELETE FROM users WHERE nope > 1" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "still accepts a valid query" $ do
|
it "still accepts a valid query" $ do
|
||||||
rowsOf (parseQuery "SELECT name FROM users WHERE age > 18" >>= runQuery testDB)
|
rowsOf (parseStatement "SELECT name FROM users WHERE age > 18" >>= runStatement testDB)
|
||||||
`shouldBe` Right
|
`shouldBe` Right
|
||||||
[ [("name", VStr "Alice")]
|
[ [("name", VStr "Alice")]
|
||||||
, [("name", VStr "Carol")]
|
, [("name", VStr "Carol")]
|
||||||
]
|
]
|
||||||
|
|
||||||
it "rejects an UPDATE whose value has the wrong type" $ do
|
it "rejects an UPDATE whose value has the wrong type" $ do
|
||||||
(parseQuery "UPDATE users SET age = 'abc'" >>= runQuery testDB)
|
(parseStatement "UPDATE users SET age = 'abc'" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects a type mismatch in an ON condition" $ do
|
it "rejects a type mismatch in an ON condition" $ do
|
||||||
(parseQuery "SELECT u.name FROM users u JOIN orders o ON u.name = o.user_id" >>= runQuery testDB)
|
(parseStatement "SELECT u.name FROM users u JOIN orders o ON u.name = o.user_id" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects an unknown column in DELETE WHERE even when the table is empty" $ do
|
it "rejects an unknown column in DELETE WHERE even when the table is empty" $ do
|
||||||
(parseQuery "DELETE FROM users WHERE nope > 1" >>= runQuery emptyDB)
|
(parseStatement "DELETE FROM users WHERE nope > 1" >>= runStatement emptyDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "rejects an unknown column in UPDATE SET even when the table is empty" $ do
|
it "rejects an unknown column in UPDATE SET even when the table is empty" $ do
|
||||||
(parseQuery "UPDATE users SET nope = 1" >>= runQuery emptyDB)
|
(parseStatement "UPDATE users SET nope = 1" >>= runStatement emptyDB)
|
||||||
`shouldSatisfy` isLeft
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
it "currently allows an INSERT that leaves a column out (known gap)" $ do
|
it "currently allows an INSERT that leaves a column out (known gap)" $ do
|
||||||
(parseQuery "INSERT INTO users (name) VALUES ('Eve')" >>= runQuery testDB)
|
(parseStatement "INSERT INTO users (name) VALUES ('Eve')" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isRight
|
`shouldSatisfy` isRight
|
||||||
|
|
||||||
it "says which clause is wrong and which columns are available" $ do
|
it "says which clause is wrong and which columns are available" $ do
|
||||||
case parseQuery "SELECT name FROM users WHERE nope > 18" >>= runQuery testDB of
|
case parseStatement "SELECT name FROM users WHERE nope > 18" >>= runStatement testDB of
|
||||||
Right _ -> expectationFailure "expected a Left"
|
Right _ -> expectationFailure "expected a Left"
|
||||||
Left err -> do
|
Left err -> do
|
||||||
err `shouldContain` "unknown column in WHERE"
|
err `shouldContain` "unknown column in WHERE"
|
||||||
err `shouldContain` "available: id, name, age"
|
err `shouldContain` "available: id, name, age"
|
||||||
|
|
||||||
it "names INSERT when a target column does not exist" $ do
|
it "names INSERT when a target column does not exist" $ do
|
||||||
case parseQuery "INSERT INTO users (nickname) VALUES (1)" >>= runQuery testDB of
|
case parseStatement "INSERT INTO users (nickname) VALUES (1)" >>= runStatement testDB of
|
||||||
Right _ -> expectationFailure "expected a Left"
|
Right _ -> expectationFailure "expected a Left"
|
||||||
Left err -> err `shouldContain` "unknown column in INSERT"
|
Left err -> err `shouldContain` "unknown column in INSERT"
|
||||||
|
|
||||||
it "names ORDER BY when a sort key does not exist" $ do
|
it "names ORDER BY when a sort key does not exist" $ do
|
||||||
case parseQuery "SELECT name FROM users ORDER BY nope" >>= runQuery testDB of
|
case parseStatement "SELECT name FROM users ORDER BY nope" >>= runStatement testDB of
|
||||||
Right _ -> expectationFailure "expected a Left"
|
Right _ -> expectationFailure "expected a Left"
|
||||||
Left err -> err `shouldContain` "unknown column in ORDER BY"
|
Left err -> err `shouldContain` "unknown column in ORDER BY"
|
||||||
|
|
||||||
it "names ON and both types when a join condition mismatches" $ do
|
it "names ON and both types when a join condition mismatches" $ do
|
||||||
case parseQuery "SELECT u.name FROM users u JOIN orders o ON u.name = o.user_id" >>= runQuery testDB of
|
case parseStatement "SELECT u.name FROM users u JOIN orders o ON u.name = o.user_id" >>= runStatement testDB of
|
||||||
Right _ -> expectationFailure "expected a Left"
|
Right _ -> expectationFailure "expected a Left"
|
||||||
Left err -> do
|
Left err -> do
|
||||||
err `shouldContain` "ON: both sides of ="
|
err `shouldContain` "ON: both sides of ="
|
||||||
err `shouldContain` "TStr and TInt"
|
err `shouldContain` "TStr and TInt"
|
||||||
|
|
||||||
it "names the column and the expected type when an assignment has the wrong type" $ do
|
it "names the column and the expected type when an assignment has the wrong type" $ do
|
||||||
case parseQuery "UPDATE users SET age = 'abc'" >>= runQuery testDB of
|
case parseStatement "UPDATE users SET age = 'abc'" >>= runStatement testDB of
|
||||||
Right _ -> expectationFailure "expected a Left"
|
Right _ -> expectationFailure "expected a Left"
|
||||||
Left err -> err `shouldContain` "UPDATE: column age needs TInt, got TStr"
|
Left err -> err `shouldContain` "UPDATE: column age needs TInt, got TStr"
|
||||||
|
|
||||||
it "accepts SELECT * across a join (the sentinel is not a column)" $ do
|
it "accepts SELECT * across a join (the sentinel is not a column)" $ do
|
||||||
(parseQuery "SELECT * FROM users u JOIN orders o ON u.id = o.user_id" >>= runQuery testDB)
|
(parseStatement "SELECT * FROM users u JOIN orders o ON u.id = o.user_id" >>= runStatement testDB)
|
||||||
`shouldSatisfy` isRight
|
`shouldSatisfy` isRight
|
||||||
|
|
||||||
describe "ChuSQL.Algebra.Op" $ do
|
describe "ChuSQL.Algebra.Op" $ do
|
||||||
it "renders a plan as indented text" $ do
|
it "renders a plan as indented text" $ do
|
||||||
case parseQuery "SELECT name FROM users WHERE age > 18" >>= translate of
|
case parseStatement "SELECT name FROM users WHERE age > 18" >>= translate of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right plan ->
|
Right plan ->
|
||||||
renderPlan plan
|
renderPlan plan
|
||||||
`shouldBe` "Project [\"name\"]\n Filter Gt (Col \"age\") (LitInt 18)\n Scan Nothing \"users\""
|
`shouldBe` "Project [\"name\"]\n Filter Gt (Col \"age\") (LitInt 18)\n Scan Nothing \"users\""
|
||||||
|
|
||||||
it "renders a join plan with indentation" $ do
|
it "renders a join plan with indentation" $ do
|
||||||
case parseQuery "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id" >>= translate of
|
case parseStatement "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id" >>= translate of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right plan ->
|
Right plan ->
|
||||||
lines (renderPlan plan)
|
lines (renderPlan plan)
|
||||||
@@ -1088,7 +1088,7 @@ main = hspec $ do
|
|||||||
]
|
]
|
||||||
|
|
||||||
it "renders a sort and limit plan" $ do
|
it "renders a sort and limit plan" $ do
|
||||||
case parseQuery "SELECT name FROM users ORDER BY age DESC LIMIT 2" >>= translate of
|
case parseStatement "SELECT name FROM users ORDER BY age DESC LIMIT 2" >>= translate of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right plan ->
|
Right plan ->
|
||||||
lines (renderPlan plan)
|
lines (renderPlan plan)
|
||||||
@@ -1180,7 +1180,67 @@ main = hspec $ do
|
|||||||
withPipeName "chusql-no-such-server" $ do
|
withPipeName "chusql-no-such-server" $ do
|
||||||
result <- runIPCStorage (scan "users")
|
result <- runIPCStorage (scan "users")
|
||||||
result `shouldSatisfy` isLeft
|
result `shouldSatisfy` isLeft
|
||||||
|
it "parses CREATE TABLE with two columns" $ do
|
||||||
|
parseStatement "CREATE TABLE users (id INT, name TEXT)"
|
||||||
|
`shouldBe` Right (CreateTable "users" [("id", TInt), ("name", TStr)])
|
||||||
|
|
||||||
|
it "parses CREATE TABLE case-insensitively" $ do
|
||||||
|
parseStatement "create table users (id int, name text)"
|
||||||
|
`shouldBe` Right (CreateTable "users" [("id", TInt), ("name", TStr)])
|
||||||
|
|
||||||
|
it "parses CREATE TABLE with BOOL column" $ do
|
||||||
|
parseStatement "CREATE TABLE t (flag BOOL)"
|
||||||
|
`shouldBe` Right (CreateTable "t" [("flag", TBool)])
|
||||||
|
|
||||||
|
it "parses CREATE TABLE with VARCHAR as TStr" $ do
|
||||||
|
parseStatement "CREATE TABLE t (name VARCHAR)"
|
||||||
|
`shouldBe` Right (CreateTable "t" [("name", TStr)])
|
||||||
|
|
||||||
|
it "parses CREATE TABLE with INTEGER" $ do
|
||||||
|
parseStatement "CREATE TABLE t (id INTEGER)"
|
||||||
|
`shouldBe` Right (CreateTable "t" [("id", TInt)])
|
||||||
|
|
||||||
|
it "rejects CREATE TABLE without columns" $ do
|
||||||
|
parseStatement "CREATE TABLE t ()"
|
||||||
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
|
it "rejects CREATE TABLE with missing type" $ do
|
||||||
|
parseStatement "CREATE TABLE t (id)"
|
||||||
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
|
describe "ChuSQL.Engine (CREATE TABLE)" $ do
|
||||||
|
it "creates a table in memory" $ do
|
||||||
|
case runStatement testDB (CreateTable "newt" [("id", TInt), ("name", TStr)]) of
|
||||||
|
Left err -> expectationFailure err
|
||||||
|
Right (db', _) -> do
|
||||||
|
rowsOf (runStatement db' (makeSelect ["*"] "newt" Nothing))
|
||||||
|
`shouldBe` Right []
|
||||||
|
|
||||||
|
it "rejects creating an existing table" $ do
|
||||||
|
runStatement testDB (CreateTable "users" [("id", TInt)])
|
||||||
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
|
it "rejects duplicate column names" $ do
|
||||||
|
runStatement testDB (CreateTable "dup" [("id", TInt), ("id", TStr)])
|
||||||
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
|
it "rejects empty column list at parse time" $ do
|
||||||
|
parseStatement "CREATE TABLE t ()"
|
||||||
|
`shouldSatisfy` isLeft
|
||||||
|
it "parses DROP TABLE" $ do
|
||||||
|
parseStatement "DROP TABLE users"
|
||||||
|
`shouldBe` Right (DropTable "users")
|
||||||
|
|
||||||
|
it "drops a table in memory" $ do
|
||||||
|
case runStatement testDB (DropTable "users") of
|
||||||
|
Left e -> expectationFailure e
|
||||||
|
Right (db', _) -> do
|
||||||
|
runStatement db' (makeSelect ["*"] "users" Nothing)
|
||||||
|
`shouldSatisfy` isLeft
|
||||||
|
|
||||||
|
it "rejects dropping a nonexistent table" $ do
|
||||||
|
runStatement testDB (DropTable "nope")
|
||||||
|
`shouldSatisfy` isLeft
|
||||||
emptyDB :: Database
|
emptyDB :: Database
|
||||||
emptyDB = [(n, t{tableRows = []}) | (n, t) <- testDB]
|
emptyDB = [(n, t{tableRows = []}) | (n, t) <- testDB]
|
||||||
|
|
||||||
@@ -1305,7 +1365,7 @@ isRight (Left _) = False
|
|||||||
|
|
||||||
sameResultAsUnoptimized :: String -> Expectation
|
sameResultAsUnoptimized :: String -> Expectation
|
||||||
sameResultAsUnoptimized sql =
|
sameResultAsUnoptimized sql =
|
||||||
case parseQuery sql of
|
case parseStatement sql of
|
||||||
Left err -> expectationFailure err
|
Left err -> expectationFailure err
|
||||||
Right q ->
|
Right q ->
|
||||||
case translate q of
|
case translate q of
|
||||||
@@ -1319,7 +1379,7 @@ filterPushedIntoLeft _ = False
|
|||||||
|
|
||||||
optimizedPlan :: String -> Either String RelOp
|
optimizedPlan :: String -> Either String RelOp
|
||||||
optimizedPlan sql = do
|
optimizedPlan sql = do
|
||||||
q <- parseQuery sql
|
q <- parseStatement sql
|
||||||
optimize testDB <$> translate q
|
optimize testDB <$> translate q
|
||||||
|
|
||||||
firstFilterCond :: RelOp -> Maybe Expr
|
firstFilterCond :: RelOp -> Maybe Expr
|
||||||
@@ -1353,7 +1413,7 @@ stripProjects (Lookup t k) = Lookup t k
|
|||||||
|
|
||||||
projectedPlan :: String -> Either String RelOp
|
projectedPlan :: String -> Either String RelOp
|
||||||
projectedPlan sql = do
|
projectedPlan sql = do
|
||||||
q <- parseQuery sql
|
q <- parseStatement sql
|
||||||
plan <- translate q
|
plan <- translate q
|
||||||
Right (pushProject testDB ["*"] plan)
|
Right (pushProject testDB ["*"] plan)
|
||||||
|
|
||||||
|
|||||||
+268
-164
@@ -1,17 +1,15 @@
|
|||||||
//! 命名管道服务:按行读写 JSON。Windows 上管道名映射为 \\.\pipe\<name>。
|
|
||||||
//! 启动时读配置(环境变量 → TOML → 默认值)并把每项来源打进日志。
|
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use chusql_storage::catalog::Catalog;
|
use chusql_storage::catalog::Catalog;
|
||||||
use chusql_storage::config::{self, Config, Loaded};
|
use chusql_storage::config::{self, Config, Loaded};
|
||||||
use chusql_storage::heap::HeapTable;
|
use chusql_storage::heap::HeapTable;
|
||||||
use chusql_storage::log;
|
use chusql_storage::log;
|
||||||
use chusql_storage::protocol::{Request, Response};
|
use chusql_storage::protocol::{Request, Response, SchemaColumn};
|
||||||
use chusql_storage::wal::{Wal, WalOp};
|
use chusql_storage::wal::{Wal, WalOp};
|
||||||
use chusql_storage::{log_debug, log_error, log_info, log_warn};
|
use chusql_storage::{log_debug, log_error, log_info, log_warn};
|
||||||
use interprocess::local_socket::{
|
use interprocess::local_socket::{
|
||||||
@@ -21,172 +19,206 @@ use interprocess::local_socket::{
|
|||||||
};
|
};
|
||||||
use interprocess::TryClone;
|
use interprocess::TryClone;
|
||||||
|
|
||||||
// * 路径
|
// 命名管道服务:按行读写 JSON,Windows 上映射成 \\.\pipe\<name>。
|
||||||
|
// 启动时读配置并把每项来源打进日志;表句柄按名字缓存复用。
|
||||||
|
|
||||||
/// 表名 -> 数据文件路径。
|
// 服务器
|
||||||
fn table_path(cfg: &Config, table: &str) -> PathBuf {
|
/// 服务器状态:配置 + 表句柄缓存 + 写锁
|
||||||
cfg.data_dir.join(format!("{}.db", table))
|
struct Server {
|
||||||
|
cfg: Config,
|
||||||
|
tables: Mutex<HashMap<String, Arc<Mutex<HeapTable>>>>,
|
||||||
|
write_lock: Mutex<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 表名 -> 索引文件路径。
|
impl Server {
|
||||||
fn index_path(cfg: &Config, table: &str) -> PathBuf {
|
/// 新建服务器状态
|
||||||
cfg.data_dir.join(format!("{}.idx", table))
|
fn new(cfg: Config) -> std::io::Result<Self> {
|
||||||
}
|
|
||||||
|
|
||||||
/// 数据字典文件路径。
|
|
||||||
fn catalog_path(cfg: &Config) -> PathBuf {
|
|
||||||
cfg.data_dir.join("catalog.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WAL 文件路径。
|
|
||||||
fn wal_path(cfg: &Config) -> PathBuf {
|
|
||||||
cfg.data_dir.join("wal.log")
|
|
||||||
}
|
|
||||||
|
|
||||||
// * 表操作
|
|
||||||
|
|
||||||
/// 打开一张表,执行闭包;表不存在就创建。
|
|
||||||
fn with_table<R>(
|
|
||||||
cfg: &Config,
|
|
||||||
table: &str,
|
|
||||||
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
|
|
||||||
) -> std::io::Result<R> {
|
|
||||||
std::fs::create_dir_all(&cfg.data_dir)?;
|
std::fs::create_dir_all(&cfg.data_dir)?;
|
||||||
let mut t = HeapTable::open(table_path(cfg, table), cfg.page_size)?;
|
Ok(Server {
|
||||||
f(&mut t)
|
cfg,
|
||||||
}
|
tables: Mutex::new(HashMap::new()),
|
||||||
|
write_lock: Mutex::new(()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 打开带索引的表,执行闭包。
|
|
||||||
fn with_indexed_table<R>(
|
// 路径
|
||||||
cfg: &Config,
|
/// 表名 -> 数据文件
|
||||||
table: &str,
|
fn table_path(&self, table: &str) -> PathBuf {
|
||||||
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
|
self.cfg.data_dir.join(format!("{}.db", table))
|
||||||
) -> std::io::Result<R> {
|
}
|
||||||
std::fs::create_dir_all(&cfg.data_dir)?;
|
|
||||||
let mut t = HeapTable::open_indexed(
|
/// 表名 -> 索引文件
|
||||||
table_path(cfg, table),
|
fn index_path(&self, table: &str) -> PathBuf {
|
||||||
index_path(cfg, table),
|
self.cfg.data_dir.join(format!("{}.idx", table))
|
||||||
cfg.page_size,
|
}
|
||||||
cfg.btree_order,
|
|
||||||
|
/// 数据字典文件
|
||||||
|
fn catalog_path(&self) -> PathBuf {
|
||||||
|
self.cfg.data_dir.join("catalog.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WAL 文件
|
||||||
|
fn wal_path(&self) -> PathBuf {
|
||||||
|
self.cfg.data_dir.join("wal.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 表句柄
|
||||||
|
/// 表文件存在且非空
|
||||||
|
fn table_exists(&self, table: &str) -> bool {
|
||||||
|
if self.tables.lock().unwrap().contains_key(table) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if let Ok(c) = Catalog::load(self.catalog_path()) {
|
||||||
|
if c.describe(table).is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fs::metadata(self.table_path(table))
|
||||||
|
.map(|m| m.len() > 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 拿表句柄(缓存复用)
|
||||||
|
fn table_handle(&self, table: &str) -> std::io::Result<Arc<Mutex<HeapTable>>> {
|
||||||
|
let mut map = self.tables.lock().unwrap();
|
||||||
|
if let Some(h) = map.get(table) {
|
||||||
|
return Ok(h.clone());
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(&self.cfg.data_dir)?;
|
||||||
|
let t = HeapTable::open_indexed(
|
||||||
|
self.table_path(table),
|
||||||
|
self.index_path(table),
|
||||||
|
self.cfg.page_size,
|
||||||
|
self.cfg.btree_order,
|
||||||
|
self.cfg.pool_size,
|
||||||
)?;
|
)?;
|
||||||
f(&mut t)
|
let h = Arc::new(Mutex::new(t));
|
||||||
}
|
map.insert(table.to_string(), h.clone());
|
||||||
|
Ok(h)
|
||||||
|
}
|
||||||
|
|
||||||
/// 表文件是否存在且非空。
|
/// 在表上跑闭包
|
||||||
fn table_exists(cfg: &Config, table: &str) -> bool {
|
fn with_table<R>(
|
||||||
let p = table_path(cfg, table);
|
&self,
|
||||||
std::fs::metadata(&p).map(|m| m.len() > 0).unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 打开已存在的表;不存在返回 NotFound。
|
|
||||||
fn with_existing_table<R>(
|
|
||||||
cfg: &Config,
|
|
||||||
table: &str,
|
table: &str,
|
||||||
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
|
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
|
||||||
) -> std::io::Result<R> {
|
) -> std::io::Result<R> {
|
||||||
if !table_exists(cfg, table) {
|
let h = self.table_handle(table)?;
|
||||||
|
let mut guard = h.lock().unwrap();
|
||||||
|
f(&mut guard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 同上,但一定带索引
|
||||||
|
fn with_indexed_table<R>(
|
||||||
|
&self,
|
||||||
|
table: &str,
|
||||||
|
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
|
||||||
|
) -> std::io::Result<R> {
|
||||||
|
self.with_table(table, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 表不存在就报错
|
||||||
|
fn with_existing_table<R>(
|
||||||
|
&self,
|
||||||
|
table: &str,
|
||||||
|
f: impl FnOnce(&mut HeapTable) -> std::io::Result<R>,
|
||||||
|
) -> std::io::Result<R> {
|
||||||
|
if !self.table_exists(table) {
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::NotFound,
|
std::io::ErrorKind::NotFound,
|
||||||
format!("unknown table: {}", table),
|
format!("unknown table: {}", table),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let mut t = HeapTable::open(table_path(cfg, table), cfg.page_size)?;
|
self.with_table(table, f)
|
||||||
f(&mut t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// * 数据字典操作
|
|
||||||
|
|
||||||
/// 读 catalog,按一行数据补列,再写回。
|
|
||||||
fn catalog_add_row(
|
|
||||||
cfg: &Config,
|
|
||||||
table: &str,
|
|
||||||
row: &serde_json::Map<String, serde_json::Value>,
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
let mut c = Catalog::load(catalog_path(cfg))?;
|
|
||||||
let row: chusql_storage::protocol::Row = row.clone().into_iter().collect();
|
|
||||||
c.ensure_columns(table, &row);
|
|
||||||
c.save(catalog_path(cfg))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 读 catalog,按多行数据补列,再写回。
|
|
||||||
fn catalog_add_rows(
|
|
||||||
cfg: &Config,
|
|
||||||
table: &str,
|
|
||||||
rows: &[serde_json::Map<String, serde_json::Value>],
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
let mut c = Catalog::load(catalog_path(cfg))?;
|
|
||||||
for r in rows {
|
|
||||||
let row: chusql_storage::protocol::Row = r.clone().into_iter().collect();
|
|
||||||
c.ensure_columns(table, &row);
|
|
||||||
}
|
}
|
||||||
c.save(catalog_path(cfg))
|
|
||||||
}
|
|
||||||
|
|
||||||
// * WAL 写路径与恢复
|
// 写入路径
|
||||||
|
/// 把一条 WAL 操作落到磁盘
|
||||||
/// 把一条 WAL 操作真正落到磁盘(堆表 + 索引 + catalog)。
|
fn apply_op(&self, op: &WalOp) -> std::io::Result<()> {
|
||||||
fn apply_op(cfg: &Config, op: &WalOp) -> std::io::Result<()> {
|
|
||||||
match op {
|
match op {
|
||||||
WalOp::Insert { table, row, key } => {
|
WalOp::Insert { table, row, key } => {
|
||||||
match key {
|
match key {
|
||||||
None => with_table(cfg, table, |t| t.insert(row))?,
|
None => self.with_table(table, |t| t.insert(row))?,
|
||||||
Some(k) => with_indexed_table(cfg, table, |t| t.insert_keyed(row, *k))?,
|
Some(k) => self.with_indexed_table(table, |t| t.insert_keyed(row, *k))?,
|
||||||
}
|
}
|
||||||
let map: serde_json::Map<_, _> = row.clone().into_iter().collect();
|
let mut c = Catalog::load(self.catalog_path())?;
|
||||||
catalog_add_row(cfg, table, &map)?;
|
let row: chusql_storage::protocol::Row = row.clone().into_iter().collect();
|
||||||
|
c.record_insert(table, &row);
|
||||||
|
c.save(self.catalog_path())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
WalOp::ReplaceAll { table, rows } => {
|
WalOp::ReplaceAll { table, rows } => {
|
||||||
with_indexed_table(cfg, table, |t| t.replace_all(rows))?;
|
self.with_indexed_table(table, |t| t.replace_all(rows))?;
|
||||||
let maps: Vec<_> = rows.iter().map(|r| r.clone().into_iter().collect()).collect();
|
let mut c = Catalog::load(self.catalog_path())?;
|
||||||
catalog_add_rows(cfg, table, &maps)?;
|
let rows: Vec<chusql_storage::protocol::Row> =
|
||||||
|
rows.iter().map(|r| r.clone().into_iter().collect()).collect();
|
||||||
|
c.record_replace_all(table, &rows);
|
||||||
|
c.save(self.catalog_path())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写前日志 + 写数据 + 清空 WAL。
|
/// 先写 WAL 再改数据
|
||||||
///
|
fn write_via_wal(&self, op: &WalOp) -> std::io::Result<()> {
|
||||||
/// 顺序:clear(清掉上次已完成的) → append+fsync → apply → clear。
|
let _guard = self.write_lock.lock().unwrap();
|
||||||
/// 若中途进程被杀,重启时 WAL 里能读到那条操作并重放。
|
std::fs::create_dir_all(&self.cfg.data_dir)?;
|
||||||
fn write_via_wal(cfg: &Config, op: &WalOp) -> std::io::Result<()> {
|
let wal = Wal::new(self.wal_path());
|
||||||
let wal = Wal::new(wal_path(cfg));
|
|
||||||
|
|
||||||
// 清掉上一次遗留(正常情况下已经是空的)
|
|
||||||
wal.clear()?;
|
wal.clear()?;
|
||||||
|
|
||||||
// 先写 WAL 并 fsync
|
|
||||||
wal.append(op)?;
|
wal.append(op)?;
|
||||||
|
|
||||||
// 再写数据
|
let result = self.apply_op(op);
|
||||||
let result = apply_op(cfg, op);
|
|
||||||
|
if result.is_ok() {
|
||||||
|
if let Err(e) = self.flush_op_tables(op) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 无论 apply 成功还是返回 Err,都清空 WAL:
|
|
||||||
// 失败的操作不应被重放;若中途被杀,这里跑不到,WAL 会留到重启时重放。
|
|
||||||
wal.clear()?;
|
wal.clear()?;
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启动时重放 WAL 里的残留操作。
|
/// 把涉及的表写回磁盘
|
||||||
fn recover_wal(cfg: &Config) -> std::io::Result<()> {
|
fn flush_op_tables(&self, op: &WalOp) -> std::io::Result<()> {
|
||||||
let wal = Wal::new(wal_path(cfg));
|
match op {
|
||||||
|
WalOp::Insert { table, key, .. } => {
|
||||||
|
if key.is_some() {
|
||||||
|
self.with_indexed_table(table, |t| t.flush())
|
||||||
|
} else {
|
||||||
|
self.with_table(table, |t| t.flush())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WalOp::ReplaceAll { table, .. } => {
|
||||||
|
self.with_indexed_table(table, |t| t.flush())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动时重放残留 WAL
|
||||||
|
fn recover_wal(&self) -> std::io::Result<()> {
|
||||||
|
std::fs::create_dir_all(&self.cfg.data_dir)?;
|
||||||
|
let wal = Wal::new(self.wal_path());
|
||||||
match wal.read()? {
|
match wal.read()? {
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
Some(op) => {
|
Some(op) => {
|
||||||
log_warn!(core, "replaying incomplete WAL entry");
|
log_warn!(core, "replaying incomplete WAL entry");
|
||||||
apply_op(cfg, &op)?;
|
self.apply_op(&op)?;
|
||||||
wal.clear()?;
|
wal.clear()?;
|
||||||
log_info!(core, "WAL replay complete");
|
log_info!(core, "WAL replay complete");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// * 请求分发
|
|
||||||
|
|
||||||
/// 把一条请求翻译成一条响应。
|
// 请求分发
|
||||||
fn handle_request(cfg: &Config, req: Request) -> Response {
|
/// 一条请求翻成一条响应
|
||||||
|
fn handle_request(&self, req: Request) -> Response {
|
||||||
match req {
|
match req {
|
||||||
Request::Ping => {
|
Request::Ping => {
|
||||||
log_debug!(pipe, "ping");
|
log_debug!(pipe, "ping");
|
||||||
@@ -195,7 +227,10 @@ fn handle_request(cfg: &Config, req: Request) -> Response {
|
|||||||
|
|
||||||
Request::Scan { table } => {
|
Request::Scan { table } => {
|
||||||
log_debug!(pipe, "scan table={}", table);
|
log_debug!(pipe, "scan table={}", table);
|
||||||
match with_existing_table(cfg, &table, |t| t.scan()) {
|
|
||||||
|
let result = self.with_existing_table(&table, |t| t.scan());
|
||||||
|
|
||||||
|
match result {
|
||||||
Ok(rows) => Response::Rows { rows },
|
Ok(rows) => Response::Rows { rows },
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_warn!(pipe, "scan {}: {}", table, e);
|
log_warn!(pipe, "scan {}: {}", table, e);
|
||||||
@@ -210,7 +245,7 @@ fn handle_request(cfg: &Config, req: Request) -> Response {
|
|||||||
log_debug!(pipe, "insert table={} key={:?}", table, key);
|
log_debug!(pipe, "insert table={} key={:?}", table, key);
|
||||||
let tname = table.clone();
|
let tname = table.clone();
|
||||||
let op = WalOp::Insert { table, row, key };
|
let op = WalOp::Insert { table, row, key };
|
||||||
match write_via_wal(cfg, &op) {
|
match self.write_via_wal(&op) {
|
||||||
Ok(()) => Response::Ok,
|
Ok(()) => Response::Ok,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_warn!(pipe, "insert {}: {}", tname, e);
|
log_warn!(pipe, "insert {}: {}", tname, e);
|
||||||
@@ -223,7 +258,7 @@ fn handle_request(cfg: &Config, req: Request) -> Response {
|
|||||||
|
|
||||||
Request::LookupByIndex { table, key } => {
|
Request::LookupByIndex { table, key } => {
|
||||||
log_debug!(pipe, "lookup_by_index table={} key={}", table, key);
|
log_debug!(pipe, "lookup_by_index table={} key={}", table, key);
|
||||||
match with_indexed_table(cfg, &table, |t| t.get_by_key(key)) {
|
match self.with_indexed_table(&table, |t| t.get_by_key(key)) {
|
||||||
Ok(Some(row)) => Response::Rows { rows: vec![row] },
|
Ok(Some(row)) => Response::Rows { rows: vec![row] },
|
||||||
Ok(None) => Response::Rows { rows: vec![] },
|
Ok(None) => Response::Rows { rows: vec![] },
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -239,7 +274,7 @@ fn handle_request(cfg: &Config, req: Request) -> Response {
|
|||||||
log_debug!(pipe, "replace_all table={} rows={}", table, rows.len());
|
log_debug!(pipe, "replace_all table={} rows={}", table, rows.len());
|
||||||
let tname = table.clone();
|
let tname = table.clone();
|
||||||
let op = WalOp::ReplaceAll { table, rows };
|
let op = WalOp::ReplaceAll { table, rows };
|
||||||
match write_via_wal(cfg, &op) {
|
match self.write_via_wal(&op) {
|
||||||
Ok(()) => Response::Ok,
|
Ok(()) => Response::Ok,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_warn!(pipe, "replace_all {}: {}", tname, e);
|
log_warn!(pipe, "replace_all {}: {}", tname, e);
|
||||||
@@ -252,7 +287,7 @@ fn handle_request(cfg: &Config, req: Request) -> Response {
|
|||||||
|
|
||||||
Request::ListTables => {
|
Request::ListTables => {
|
||||||
log_debug!(pipe, "list_tables");
|
log_debug!(pipe, "list_tables");
|
||||||
match list_tables(cfg) {
|
match self.list_tables() {
|
||||||
Ok(tables) => Response::Tables { tables },
|
Ok(tables) => Response::Tables { tables },
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_warn!(pipe, "list_tables: {}", e);
|
log_warn!(pipe, "list_tables: {}", e);
|
||||||
@@ -265,8 +300,11 @@ fn handle_request(cfg: &Config, req: Request) -> Response {
|
|||||||
|
|
||||||
Request::DescribeTable { table } => {
|
Request::DescribeTable { table } => {
|
||||||
log_debug!(pipe, "describe_table table={}", table);
|
log_debug!(pipe, "describe_table table={}", table);
|
||||||
match describe_table(cfg, &table) {
|
match self.describe_table(&table) {
|
||||||
Ok(cols) => Response::Schema { columns: cols },
|
Ok((cols, count)) => Response::Schema {
|
||||||
|
columns: cols,
|
||||||
|
row_count: count,
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_warn!(pipe, "describe_table {}: {}", table, e);
|
log_warn!(pipe, "describe_table {}: {}", table, e);
|
||||||
Response::Error {
|
Response::Error {
|
||||||
@@ -275,19 +313,84 @@ fn handle_request(cfg: &Config, req: Request) -> Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 列出表名:catalog 记录 + data 目录下的 .db 文件,取并集。
|
Request::CreateTable { table, columns } => {
|
||||||
fn list_tables(cfg: &Config) -> std::io::Result<Vec<String>> {
|
log_debug!(pipe, "create_table table={} cols={}", table, columns.len());
|
||||||
|
|
||||||
|
let mut c = match Catalog::load(self.catalog_path()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return Response::Error {
|
||||||
|
message: format!("create_table {}: catalog: {}", table, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if c.describe(&table).is_some() {
|
||||||
|
return Response::Error {
|
||||||
|
message: format!("create_table {}: table already exists", table),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = self.with_table(&table, |_t| Ok(())) {
|
||||||
|
return Response::Error {
|
||||||
|
message: format!("create_table {}: {}", table, e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = c.create_table(&table, columns) {
|
||||||
|
return Response::Error {
|
||||||
|
message: format!("create_table {}: {}", table, e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Err(e) = c.save(self.catalog_path()) {
|
||||||
|
return Response::Error {
|
||||||
|
message: format!("create_table {}: catalog save: {}", table, e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::Ok
|
||||||
|
}
|
||||||
|
Request::DropTable { table } => {
|
||||||
|
log_debug!(pipe, "drop_table table={}", table);
|
||||||
|
|
||||||
|
let mut c = match Catalog::load(self.catalog_path()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Response::Error {
|
||||||
|
message: format!("drop_table {}: catalog: {}", table, e),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let Err(e) = c.drop_table(&table) {
|
||||||
|
return Response::Error {
|
||||||
|
message: format!("drop_table {}: {}", table, e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Err(e) = c.save(self.catalog_path()) {
|
||||||
|
return Response::Error {
|
||||||
|
message: format!("drop_table {}: catalog save: {}", table, e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tables.lock().unwrap().remove(&table);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(self.table_path(&table));
|
||||||
|
let _ = std::fs::remove_file(self.index_path(&table));
|
||||||
|
|
||||||
|
Response::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 信息查询
|
||||||
|
/// 表名:catalog 与目录取并集
|
||||||
|
fn list_tables(&self) -> std::io::Result<Vec<String>> {
|
||||||
let mut names = BTreeSet::new();
|
let mut names = BTreeSet::new();
|
||||||
|
|
||||||
let c = Catalog::load(catalog_path(cfg))?;
|
let c = Catalog::load(self.catalog_path())?;
|
||||||
for t in c.table_names() {
|
for t in c.table_names() {
|
||||||
names.insert(t);
|
names.insert(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dir = &cfg.data_dir;
|
let dir = &self.cfg.data_dir;
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
for entry in std::fs::read_dir(dir)? {
|
for entry in std::fs::read_dir(dir)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
@@ -299,30 +402,30 @@ fn list_tables(cfg: &Config) -> std::io::Result<Vec<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(names.into_iter().collect())
|
Ok(names.into_iter().collect())
|
||||||
}
|
|
||||||
|
|
||||||
/// 查一张表的 schema。表在 catalog 里没有,但文件存在,返回空列。
|
|
||||||
fn describe_table(
|
|
||||||
cfg: &Config,
|
|
||||||
table: &str,
|
|
||||||
) -> std::io::Result<Vec<chusql_storage::protocol::SchemaColumn>> {
|
|
||||||
let c = Catalog::load(catalog_path(cfg))?;
|
|
||||||
if let Some(schema) = c.describe(table) {
|
|
||||||
return Ok(schema.columns.clone());
|
|
||||||
}
|
}
|
||||||
if table_exists(cfg, table) {
|
|
||||||
return Ok(Vec::new());
|
/// 查一张表的 schema
|
||||||
|
fn describe_table(&self, table: &str) -> std::io::Result<(Vec<SchemaColumn>, u64)> {
|
||||||
|
let c = Catalog::load(self.catalog_path())?;
|
||||||
|
if let Some(schema) = c.describe(table) {
|
||||||
|
return Ok((schema.columns.clone(), schema.row_count));
|
||||||
|
}
|
||||||
|
if self.tables.lock().unwrap().contains_key(table)
|
||||||
|
|| std::fs::metadata(self.table_path(table)).is_ok()
|
||||||
|
{
|
||||||
|
return Ok((Vec::new(), 0));
|
||||||
}
|
}
|
||||||
Err(std::io::Error::new(
|
Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::NotFound,
|
std::io::ErrorKind::NotFound,
|
||||||
format!("unknown table: {}", table),
|
format!("unknown table: {}", table),
|
||||||
))
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// * 连接处理
|
|
||||||
|
|
||||||
/// 处理一条连接:逐行读 JSON 请求,逐行回 JSON 响应。
|
// 连接
|
||||||
fn handle(cfg: &Config, conn: LocalSocketStream) -> std::io::Result<()> {
|
/// 处理一条连接
|
||||||
|
fn handle(server: &Server, conn: LocalSocketStream) -> std::io::Result<()> {
|
||||||
let mut writer = conn.try_clone()?;
|
let mut writer = conn.try_clone()?;
|
||||||
let reader = BufReader::new(conn);
|
let reader = BufReader::new(conn);
|
||||||
|
|
||||||
@@ -333,7 +436,7 @@ fn handle(cfg: &Config, conn: LocalSocketStream) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let resp = match serde_json::from_str::<Request>(&line) {
|
let resp = match serde_json::from_str::<Request>(&line) {
|
||||||
Ok(req) => handle_request(cfg, req),
|
Ok(req) => server.handle_request(req),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_warn!(pipe, "bad request line: {}", e);
|
log_warn!(pipe, "bad request line: {}", e);
|
||||||
Response::Error {
|
Response::Error {
|
||||||
@@ -349,19 +452,20 @@ fn handle(cfg: &Config, conn: LocalSocketStream) -> std::io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// * 主入口
|
|
||||||
|
|
||||||
/// 把"配置从哪来"打进日志。
|
// 入口
|
||||||
|
/// 把配置来源打进日志
|
||||||
fn log_config(loaded: &Loaded) {
|
fn log_config(loaded: &Loaded) {
|
||||||
match &loaded.config_path {
|
match &loaded.config_path {
|
||||||
Some(p) => log_info!(core, "config file: {}", p.display()),
|
Some(p) => log_info!(core, "config file: {}", p.display()),
|
||||||
None => log_info!(core, "config file: none"),
|
None => log_info!(core, "config file: none"),
|
||||||
}
|
}
|
||||||
for (name, value, origin) in &loaded.origins {
|
for (name, value, origin) in &loaded.origins {
|
||||||
log_info!(core, " {:<17} {} [{}]", name, value, origin.describe());
|
log_info!(core, " {:<18} {} [{}]", name, value, origin.describe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 进程入口
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
let loaded = match config::load() {
|
let loaded = match config::load() {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
@@ -376,15 +480,15 @@ fn main() -> std::io::Result<()> {
|
|||||||
log_info!(core, "chusql-storage {} starting", env!("CARGO_PKG_VERSION"));
|
log_info!(core, "chusql-storage {} starting", env!("CARGO_PKG_VERSION"));
|
||||||
log_config(&loaded);
|
log_config(&loaded);
|
||||||
|
|
||||||
let cfg = Arc::new(loaded.config);
|
let server = Arc::new(Server::new(loaded.config)?);
|
||||||
|
|
||||||
// 启动时先重放 WAL,再做其他事
|
if let Err(e) = server.recover_wal() {
|
||||||
if let Err(e) = recover_wal(&cfg) {
|
|
||||||
log_error!(core, "WAL recovery failed: {}", e);
|
log_error!(core, "WAL recovery failed: {}", e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ns_name = cfg
|
let ns_name = server
|
||||||
|
.cfg
|
||||||
.pipe_name
|
.pipe_name
|
||||||
.as_str()
|
.as_str()
|
||||||
.to_ns_name::<GenericNamespaced>()
|
.to_ns_name::<GenericNamespaced>()
|
||||||
@@ -394,16 +498,16 @@ fn main() -> std::io::Result<()> {
|
|||||||
log_info!(
|
log_info!(
|
||||||
core,
|
core,
|
||||||
"listening on pipe {} (\\\\.\\pipe\\{})",
|
"listening on pipe {} (\\\\.\\pipe\\{})",
|
||||||
cfg.pipe_name,
|
server.cfg.pipe_name,
|
||||||
cfg.pipe_name
|
server.cfg.pipe_name
|
||||||
);
|
);
|
||||||
|
|
||||||
for conn in listener.incoming() {
|
for conn in listener.incoming() {
|
||||||
let conn = conn?;
|
let conn = conn?;
|
||||||
let cfg = Arc::clone(&cfg);
|
let server = Arc::clone(&server);
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
log_debug!(pipe, "connection accepted");
|
log_debug!(pipe, "connection accepted");
|
||||||
if let Err(e) = handle(&cfg, conn) {
|
if let Err(e) = handle(&server, conn) {
|
||||||
log_error!(pipe, "connection error: {}", e);
|
log_error!(pipe, "connection error: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+57
-49
@@ -3,100 +3,98 @@ use std::path::Path;
|
|||||||
|
|
||||||
use crate::page::{Page, PageFile, PageId};
|
use crate::page::{Page, PageFile, PageId};
|
||||||
|
|
||||||
// 磁盘版 B+ 树:每个节点占一页,page 0 存文件头(魔数 + 页大小 + order + 根页号)。
|
// 磁盘 B+ 树:节点占一页,文件头记页大小与 order。
|
||||||
// 节点页布局(order = 一个节点最多几个孩子,来自配置):
|
|
||||||
// [0] 节点类型:0=叶子,1=内部
|
|
||||||
// [1..3] 键数量 u16
|
|
||||||
// [3..11] 叶子的 next 页号 u64(内部保留)
|
|
||||||
// [12..12+K*8] 键:K = order-1 个 i64
|
|
||||||
// [后面] 叶子:K 个 u64 值;内部:order 个 u64 孩子页号
|
|
||||||
|
|
||||||
/// 文件头放在这一页。
|
// 布局常量
|
||||||
|
/// 文件头放在第 0 页
|
||||||
const META_PAGE: PageId = 0;
|
const META_PAGE: PageId = 0;
|
||||||
/// 文件头魔数。
|
/// 魔数,用来认出索引文件
|
||||||
const META_MAGIC: &[u8; 4] = b"CBTR";
|
const META_MAGIC: &[u8; 4] = b"CBTR";
|
||||||
/// 文件头里各字段的偏移。
|
/// 文件头各字段的偏移
|
||||||
const OFF_META_PAGE_SIZE: usize = 4;
|
const OFF_META_PAGE_SIZE: usize = 4;
|
||||||
const OFF_META_ORDER: usize = 8;
|
const OFF_META_ORDER: usize = 8;
|
||||||
const OFF_META_ROOT: usize = 10;
|
const OFF_META_ROOT: usize = 10;
|
||||||
|
|
||||||
/// 节点类型标记。
|
/// 节点类型标记
|
||||||
const NODE_LEAF: u8 = 0;
|
const NODE_LEAF: u8 = 0;
|
||||||
const NODE_INTERNAL: u8 = 1;
|
const NODE_INTERNAL: u8 = 1;
|
||||||
/// 布局偏移。
|
/// 节点内布局的偏移
|
||||||
const OFF_TYPE: usize = 0;
|
const OFF_TYPE: usize = 0;
|
||||||
const OFF_COUNT: usize = 1;
|
const OFF_COUNT: usize = 1;
|
||||||
const OFF_NEXT: usize = 3;
|
const OFF_NEXT: usize = 3;
|
||||||
const OFF_KEYS: usize = 12;
|
const OFF_KEYS: usize = 12;
|
||||||
|
|
||||||
/// 一个 order 的节点占多少字节(内部节点最坏情况:order-1 个键 + order 个孩子)。
|
// 容量计算
|
||||||
|
/// 一个节点占多少字节
|
||||||
fn node_bytes(order: usize) -> usize {
|
fn node_bytes(order: usize) -> usize {
|
||||||
OFF_KEYS + max_keys(order) * 8 + order * 8
|
OFF_KEYS + max_keys(order) * 8 + order * 8
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一个节点最多几个键。
|
/// 最多几个键
|
||||||
fn max_keys(order: usize) -> usize {
|
fn max_keys(order: usize) -> usize {
|
||||||
order - 1
|
order - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 键后面那片区域的起点。
|
/// 值区域的起点
|
||||||
fn off_payload(order: usize) -> usize {
|
fn off_payload(order: usize) -> usize {
|
||||||
OFF_KEYS + max_keys(order) * 8
|
OFF_KEYS + max_keys(order) * 8
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 这个 order 放得进 size 字节的页吗。
|
/// 这个 order 放得进一页吗
|
||||||
pub fn fits_in_page(page_size: usize, order: usize) -> bool {
|
pub fn fits_in_page(page_size: usize, order: usize) -> bool {
|
||||||
order >= 2 && node_bytes(order) <= page_size
|
order >= 2 && node_bytes(order) <= page_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 给定页大小,order 最大能取多少(解 12 + (order-1)*8 + order*8 <= page_size)。
|
/// 一页最多几个孩子
|
||||||
pub fn max_order(page_size: usize) -> usize {
|
pub fn max_order(page_size: usize) -> usize {
|
||||||
page_size.saturating_sub(4) / 16
|
page_size.saturating_sub(4) / 16
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读一个 u8。
|
// 小端读写
|
||||||
|
/// 读一个 u8
|
||||||
fn read_u8(p: &Page, off: usize) -> u8 {
|
fn read_u8(p: &Page, off: usize) -> u8 {
|
||||||
p.data[off]
|
p.data[off]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写一个 u8。
|
/// 写一个 u8
|
||||||
fn write_u8(p: &mut Page, off: usize, v: u8) {
|
fn write_u8(p: &mut Page, off: usize, v: u8) {
|
||||||
p.data[off] = v;
|
p.data[off] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读一个小端 u16。
|
/// 读一个小端 u16
|
||||||
fn read_u16(p: &Page, off: usize) -> u16 {
|
fn read_u16(p: &Page, off: usize) -> u16 {
|
||||||
u16::from_le_bytes([p.data[off], p.data[off + 1]])
|
u16::from_le_bytes([p.data[off], p.data[off + 1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写一个小端 u16。
|
/// 写一个小端 u16
|
||||||
fn write_u16(p: &mut Page, off: usize, v: u16) {
|
fn write_u16(p: &mut Page, off: usize, v: u16) {
|
||||||
p.data[off..off + 2].copy_from_slice(&v.to_le_bytes());
|
p.data[off..off + 2].copy_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读一个小端 u32。
|
/// 读一个小端 u32
|
||||||
fn read_u32(p: &Page, off: usize) -> u32 {
|
fn read_u32(p: &Page, off: usize) -> u32 {
|
||||||
u32::from_le_bytes([p.data[off], p.data[off + 1], p.data[off + 2], p.data[off + 3]])
|
u32::from_le_bytes([p.data[off], p.data[off + 1], p.data[off + 2], p.data[off + 3]])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写一个小端 u32。
|
/// 写一个小端 u32
|
||||||
fn write_u32(p: &mut Page, off: usize, v: u32) {
|
fn write_u32(p: &mut Page, off: usize, v: u32) {
|
||||||
p.data[off..off + 4].copy_from_slice(&v.to_le_bytes());
|
p.data[off..off + 4].copy_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读一个小端 u64。
|
/// 读一个小端 u64
|
||||||
fn read_u64(p: &Page, off: usize) -> u64 {
|
fn read_u64(p: &Page, off: usize) -> u64 {
|
||||||
let mut b = [0u8; 8];
|
let mut b = [0u8; 8];
|
||||||
b.copy_from_slice(&p.data[off..off + 8]);
|
b.copy_from_slice(&p.data[off..off + 8]);
|
||||||
u64::from_le_bytes(b)
|
u64::from_le_bytes(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写一个小端 u64。
|
/// 写一个小端 u64
|
||||||
fn write_u64(p: &mut Page, off: usize, v: u64) {
|
fn write_u64(p: &mut Page, off: usize, v: u64) {
|
||||||
p.data[off..off + 8].copy_from_slice(&v.to_le_bytes());
|
p.data[off..off + 8].copy_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写文件头。
|
// 文件头
|
||||||
|
/// 写文件头
|
||||||
fn write_meta(page: &mut Page, page_size: usize, order: usize, root: PageId) {
|
fn write_meta(page: &mut Page, page_size: usize, order: usize, root: PageId) {
|
||||||
page.zero();
|
page.zero();
|
||||||
page.data[0..4].copy_from_slice(META_MAGIC);
|
page.data[0..4].copy_from_slice(META_MAGIC);
|
||||||
@@ -105,7 +103,7 @@ fn write_meta(page: &mut Page, page_size: usize, order: usize, root: PageId) {
|
|||||||
write_u64(page, OFF_META_ROOT, root);
|
write_u64(page, OFF_META_ROOT, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查文件头和当前配置是否一致(页大小 / order 改了就会对不上)。
|
/// 校验文件头与配置
|
||||||
fn check_meta(page: &Page, page_size: usize, order: usize) -> io::Result<()> {
|
fn check_meta(page: &Page, page_size: usize, order: usize) -> io::Result<()> {
|
||||||
if page.data[0..4] != *META_MAGIC {
|
if page.data[0..4] != *META_MAGIC {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
@@ -127,7 +125,8 @@ fn check_meta(page: &Page, page_size: usize, order: usize) -> io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一个节点在内存里的样子。
|
// 节点编解码
|
||||||
|
/// 节点在内存里的样子
|
||||||
enum NodeData {
|
enum NodeData {
|
||||||
Leaf {
|
Leaf {
|
||||||
keys: Vec<i64>,
|
keys: Vec<i64>,
|
||||||
@@ -140,7 +139,7 @@ enum NodeData {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从一页里读出节点。
|
/// 从一页读出节点
|
||||||
fn read_node(p: &Page, order: usize) -> NodeData {
|
fn read_node(p: &Page, order: usize) -> NodeData {
|
||||||
let ty = read_u8(p, OFF_TYPE);
|
let ty = read_u8(p, OFF_TYPE);
|
||||||
let n = read_u16(p, OFF_COUNT) as usize;
|
let n = read_u16(p, OFF_COUNT) as usize;
|
||||||
@@ -167,7 +166,7 @@ fn read_node(p: &Page, order: usize) -> NodeData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把节点写回一页(先清零,避免残留)。
|
/// 把节点写回一页
|
||||||
fn write_node(p: &mut Page, node: &NodeData, order: usize) {
|
fn write_node(p: &mut Page, node: &NodeData, order: usize) {
|
||||||
p.zero();
|
p.zero();
|
||||||
let payload = off_payload(order);
|
let payload = off_payload(order);
|
||||||
@@ -196,7 +195,7 @@ fn write_node(p: &mut Page, node: &NodeData, order: usize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 二分找 key 应该下探到的孩子下标。
|
/// 二分选孩子
|
||||||
fn child_index(keys: &[i64], key: i64) -> usize {
|
fn child_index(keys: &[i64], key: i64) -> usize {
|
||||||
let mut lo = 0;
|
let mut lo = 0;
|
||||||
let mut hi = keys.len();
|
let mut hi = keys.len();
|
||||||
@@ -211,15 +210,21 @@ fn child_index(keys: &[i64], key: i64) -> usize {
|
|||||||
lo
|
lo
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 磁盘版 B+ 树。
|
// B+ 树
|
||||||
|
/// 磁盘 B+ 树
|
||||||
pub struct DiskBTree {
|
pub struct DiskBTree {
|
||||||
file: PageFile,
|
file: PageFile,
|
||||||
order: usize,
|
order: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiskBTree {
|
impl DiskBTree {
|
||||||
/// 打开或新建索引文件;页大小 / order 和文件头里记的不一致就报错。
|
/// 打开或新建索引文件
|
||||||
pub fn open<P: AsRef<Path>>(path: P, page_size: usize, order: usize) -> io::Result<Self> {
|
pub fn open<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
page_size: usize,
|
||||||
|
order: usize,
|
||||||
|
pool_size: usize,
|
||||||
|
) -> io::Result<Self> {
|
||||||
if !fits_in_page(page_size, order) {
|
if !fits_in_page(page_size, order) {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
@@ -231,7 +236,7 @@ impl DiskBTree {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let mut file = PageFile::open(path, page_size)?;
|
let mut file = PageFile::with_options(path, page_size, pool_size)?;
|
||||||
if file.num_pages()? == 0 {
|
if file.num_pages()? == 0 {
|
||||||
let mut meta = Page::new(META_PAGE, page_size);
|
let mut meta = Page::new(META_PAGE, page_size);
|
||||||
write_meta(&mut meta, page_size, order, 0);
|
write_meta(&mut meta, page_size, order, 0);
|
||||||
@@ -243,26 +248,26 @@ impl DiskBTree {
|
|||||||
Ok(DiskBTree { file, order })
|
Ok(DiskBTree { file, order })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读根页号;0 表示空树。
|
/// 读根页号
|
||||||
fn root(&mut self) -> io::Result<Option<PageId>> {
|
fn root(&mut self) -> io::Result<Option<PageId>> {
|
||||||
let meta = self.file.read_page(META_PAGE)?;
|
let meta = self.file.read_page(META_PAGE)?;
|
||||||
let r = read_u64(&meta, OFF_META_ROOT);
|
let r = read_u64(&meta, OFF_META_ROOT);
|
||||||
Ok(if r == 0 { None } else { Some(r) })
|
Ok(if r == 0 { None } else { Some(r) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写根页号。
|
/// 写根页号
|
||||||
fn set_root(&mut self, r: Option<PageId>) -> io::Result<()> {
|
fn set_root(&mut self, r: Option<PageId>) -> io::Result<()> {
|
||||||
let mut meta = self.file.read_page(META_PAGE)?;
|
let mut meta = self.file.read_page(META_PAGE)?;
|
||||||
write_u64(&mut meta, OFF_META_ROOT, r.unwrap_or(0));
|
write_u64(&mut meta, OFF_META_ROOT, r.unwrap_or(0));
|
||||||
self.file.write_page(&meta)
|
self.file.write_page(&meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 分配一个新页。
|
/// 分配新页
|
||||||
fn alloc(&mut self) -> io::Result<PageId> {
|
fn alloc(&mut self) -> io::Result<PageId> {
|
||||||
Ok(self.file.append_page()?.id)
|
Ok(self.file.append_page()?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清空整棵树:截断文件后重写文件头(根页号归零)。
|
/// 清空整棵树
|
||||||
pub fn clear(&mut self) -> io::Result<()> {
|
pub fn clear(&mut self) -> io::Result<()> {
|
||||||
let page_size = self.file.page_size();
|
let page_size = self.file.page_size();
|
||||||
self.file.truncate()?;
|
self.file.truncate()?;
|
||||||
@@ -272,12 +277,12 @@ impl DiskBTree {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 空页,用来拼一个新节点。
|
/// 空页,用来拼节点
|
||||||
fn new_page(&self, id: PageId) -> Page {
|
fn new_page(&self, id: PageId) -> Page {
|
||||||
Page::new(id, self.file.page_size())
|
Page::new(id, self.file.page_size())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按 key 找 value。
|
/// 按 key 找 value
|
||||||
pub fn get(&mut self, key: i64) -> io::Result<Option<u64>> {
|
pub fn get(&mut self, key: i64) -> io::Result<Option<u64>> {
|
||||||
match self.root()? {
|
match self.root()? {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
@@ -285,7 +290,7 @@ impl DiskBTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 递归查找。
|
/// 递归查找
|
||||||
fn get_rec(&mut self, page_id: PageId, key: i64) -> io::Result<Option<u64>> {
|
fn get_rec(&mut self, page_id: PageId, key: i64) -> io::Result<Option<u64>> {
|
||||||
let p = self.file.read_page(page_id)?;
|
let p = self.file.read_page(page_id)?;
|
||||||
match read_node(&p, self.order) {
|
match read_node(&p, self.order) {
|
||||||
@@ -299,7 +304,7 @@ impl DiskBTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插入 / 覆盖一个键值对。
|
/// 插入或覆盖
|
||||||
pub fn insert(&mut self, key: i64, value: u64) -> io::Result<()> {
|
pub fn insert(&mut self, key: i64, value: u64) -> io::Result<()> {
|
||||||
match self.root()? {
|
match self.root()? {
|
||||||
None => {
|
None => {
|
||||||
@@ -317,7 +322,6 @@ impl DiskBTree {
|
|||||||
Some(r) => {
|
Some(r) => {
|
||||||
let (_, split) = self.insert_rec(r, key, value)?;
|
let (_, split) = self.insert_rec(r, key, value)?;
|
||||||
if let Some((sep, right_id)) = split {
|
if let Some((sep, right_id)) = split {
|
||||||
// 根分裂:长高一层。
|
|
||||||
let root_id = self.alloc()?;
|
let root_id = self.alloc()?;
|
||||||
let node = NodeData::Internal {
|
let node = NodeData::Internal {
|
||||||
keys: vec![sep],
|
keys: vec![sep],
|
||||||
@@ -334,7 +338,7 @@ impl DiskBTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 递归插入;返回可能上提的 (分隔键, 新右页号)。
|
/// 递归插入,可能上提
|
||||||
fn insert_rec(
|
fn insert_rec(
|
||||||
&mut self,
|
&mut self,
|
||||||
page_id: PageId,
|
page_id: PageId,
|
||||||
@@ -358,7 +362,6 @@ impl DiskBTree {
|
|||||||
self.file.write_page(&p2)?;
|
self.file.write_page(&p2)?;
|
||||||
Ok((page_id, None))
|
Ok((page_id, None))
|
||||||
} else {
|
} else {
|
||||||
// 叶子分裂:右叶最小键复制上提。
|
|
||||||
let mid = keys.len() / 2;
|
let mid = keys.len() / 2;
|
||||||
let rkeys = keys.split_off(mid);
|
let rkeys = keys.split_off(mid);
|
||||||
let rvalues = values.split_off(mid);
|
let rvalues = values.split_off(mid);
|
||||||
@@ -409,7 +412,6 @@ impl DiskBTree {
|
|||||||
self.file.write_page(&p2)?;
|
self.file.write_page(&p2)?;
|
||||||
Ok((page_id, None))
|
Ok((page_id, None))
|
||||||
} else {
|
} else {
|
||||||
// 内部节点分裂:中间键上提,不再保留原件。
|
|
||||||
let mid = keys.len() / 2;
|
let mid = keys.len() / 2;
|
||||||
let up = keys[mid];
|
let up = keys[mid];
|
||||||
let rkeys = keys.split_off(mid + 1);
|
let rkeys = keys.split_off(mid + 1);
|
||||||
@@ -441,7 +443,7 @@ impl DiskBTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按 key 升序返回所有键值对。
|
/// 按键升序返回全部
|
||||||
pub fn iter_all(&mut self) -> io::Result<Vec<(i64, u64)>> {
|
pub fn iter_all(&mut self) -> io::Result<Vec<(i64, u64)>> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
if let Some(r) = self.root()? {
|
if let Some(r) = self.root()? {
|
||||||
@@ -450,7 +452,7 @@ impl DiskBTree {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 中序遍历一棵子树。
|
/// 中序遍历
|
||||||
fn walk(&mut self, page_id: PageId, out: &mut Vec<(i64, u64)>) -> io::Result<()> {
|
fn walk(&mut self, page_id: PageId, out: &mut Vec<(i64, u64)>) -> io::Result<()> {
|
||||||
let p = self.file.read_page(page_id)?;
|
let p = self.file.read_page(page_id)?;
|
||||||
match read_node(&p, self.order) {
|
match read_node(&p, self.order) {
|
||||||
@@ -467,4 +469,10 @@ impl DiskBTree {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 把脏页写回
|
||||||
|
pub fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.file.flush()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//! 数据字典:记录每张表有哪些列、什么类型。
|
|
||||||
//! 存在 data/catalog.json。insert 时自动补列,describe 时读回。
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -9,20 +7,27 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::protocol::{ColumnType, Row, SchemaColumn};
|
use crate::protocol::{ColumnType, Row, SchemaColumn};
|
||||||
|
|
||||||
/// 一张表的 schema。
|
// 数据字典:记每张表有哪些列、什么类型、多少行。
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
/// 一张表的 schema
|
||||||
pub struct TableSchema {
|
pub struct TableSchema {
|
||||||
pub columns: Vec<SchemaColumn>,
|
pub columns: Vec<SchemaColumn>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub row_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 整个数据字典。
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
/// 数据字典:表名到 schema
|
||||||
pub struct Catalog {
|
pub struct Catalog {
|
||||||
tables: BTreeMap<String, TableSchema>,
|
tables: BTreeMap<String, TableSchema>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl Catalog {
|
impl Catalog {
|
||||||
/// 从文件读;文件不存在返回空字典。
|
// 读取与保存
|
||||||
|
/// 从文件读;不存在给空字典
|
||||||
pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||||
if !path.as_ref().exists() {
|
if !path.as_ref().exists() {
|
||||||
return Ok(Catalog::default());
|
return Ok(Catalog::default());
|
||||||
@@ -34,23 +39,25 @@ impl Catalog {
|
|||||||
serde_json::from_slice(&bytes).map_err(io::Error::other)
|
serde_json::from_slice(&bytes).map_err(io::Error::other)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写回文件。
|
/// 写回文件
|
||||||
pub fn save<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
pub fn save<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||||
let bytes = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
|
let bytes = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
|
||||||
std::fs::write(path, bytes)
|
std::fs::write(path, bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一张表的 schema;没有就返回 None。
|
// 查询
|
||||||
|
/// 一张表的 schema
|
||||||
pub fn describe(&self, table: &str) -> Option<&TableSchema> {
|
pub fn describe(&self, table: &str) -> Option<&TableSchema> {
|
||||||
self.tables.get(table)
|
self.tables.get(table)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// catalog 里记录的所有表名。
|
/// 记录过的所有表名
|
||||||
pub fn table_names(&self) -> Vec<String> {
|
pub fn table_names(&self) -> Vec<String> {
|
||||||
self.tables.keys().cloned().collect()
|
self.tables.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按一行数据补列;已有的列不动。
|
// 更新
|
||||||
|
/// 按一行数据补列
|
||||||
pub fn ensure_columns(&mut self, table: &str, row: &Row) {
|
pub fn ensure_columns(&mut self, table: &str, row: &Row) {
|
||||||
let entry = self.tables.entry(table.to_string()).or_default();
|
let entry = self.tables.entry(table.to_string()).or_default();
|
||||||
for (k, v) in row {
|
for (k, v) in row {
|
||||||
@@ -66,15 +73,74 @@ impl Catalog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按多行数据补列;replace_all 用。
|
/// 建表;已存在报错
|
||||||
|
pub fn create_table(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> {
|
||||||
|
if self.tables.contains_key(table) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
format!("table already exists: {}", table),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.tables.insert(
|
||||||
|
table.to_string(),
|
||||||
|
TableSchema {
|
||||||
|
columns,
|
||||||
|
row_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按多行数据补列
|
||||||
pub fn ensure_columns_from_rows(&mut self, table: &str, rows: &[Row]) {
|
pub fn ensure_columns_from_rows(&mut self, table: &str, rows: &[Row]) {
|
||||||
for r in rows {
|
for r in rows {
|
||||||
self.ensure_columns(table, r);
|
self.ensure_columns(table, r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// 记一次插入:补列 + 行数 +1
|
||||||
|
pub fn record_insert(&mut self, table: &str, row: &Row) {
|
||||||
|
let entry = self.tables.entry(table.to_string()).or_default();
|
||||||
|
for (k, v) in row {
|
||||||
|
if entry.columns.iter().any(|c| &c.name == k) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(ty) = infer_type(v) {
|
||||||
|
entry.columns.push(SchemaColumn { name: k.clone(), ty });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.row_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记一次整表替换:列照补,行数重置
|
||||||
|
pub fn record_replace_all(&mut self, table: &str, rows: &[Row]) {
|
||||||
|
let entry = self.tables.entry(table.to_string()).or_default();
|
||||||
|
for r in rows {
|
||||||
|
for (k, v) in r {
|
||||||
|
if entry.columns.iter().any(|c| &c.name == k) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(ty) = infer_type(v) {
|
||||||
|
entry.columns.push(SchemaColumn { name: k.clone(), ty });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.row_count = rows.len() as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删表
|
||||||
|
pub fn drop_table(&mut self, table: &str) -> io::Result<()> {
|
||||||
|
if self.tables.remove(table).is_none() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
format!("unknown table: {}", table),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从 JSON 值推断列类型;不支持的类型返回 None。
|
// 推断
|
||||||
|
/// 由 JSON 值猜列类型
|
||||||
fn infer_type(v: &serde_json::Value) -> Option<ColumnType> {
|
fn infer_type(v: &serde_json::Value) -> Option<ColumnType> {
|
||||||
match v {
|
match v {
|
||||||
serde_json::Value::Number(_) => Some(ColumnType::Int),
|
serde_json::Value::Number(_) => Some(ColumnType::Int),
|
||||||
|
|||||||
@@ -4,45 +4,50 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
use crate::log::Level;
|
use crate::log::Level;
|
||||||
|
|
||||||
// 配置:优先环境变量,其次 TOML 文件,最后内置默认值(下面写的就是默认值)。
|
// 配置:环境变量优先于 TOML,最后回退内置默认值。
|
||||||
// 每个选项的来源都记在 Loaded::origins 里,server 启动时打进日志——读了哪个文件、
|
|
||||||
// 哪些选项走了默认回退,一眼能看出来。
|
|
||||||
|
|
||||||
/// 默认配置文件名(相对当前工作目录);可用 CHUSQL_CONFIG 指到别处。
|
// 默认值
|
||||||
|
/// 默认配置文件路径
|
||||||
pub const DEFAULT_CONFIG_PATH: &str = "chusql-storage.toml";
|
pub const DEFAULT_CONFIG_PATH: &str = "chusql-storage.toml";
|
||||||
|
|
||||||
/// 默认页大小(字节)。
|
/// 默认池容量(页数)
|
||||||
|
pub const DEFAULT_POOL_SIZE: usize = 64;
|
||||||
|
/// 默认页大小
|
||||||
pub const DEFAULT_PAGE_SIZE: usize = 4096;
|
pub const DEFAULT_PAGE_SIZE: usize = 4096;
|
||||||
/// 默认 B+ 树节点最大孩子数。
|
/// 默认 B+ 树 order
|
||||||
pub const DEFAULT_BTREE_ORDER: usize = 4;
|
pub const DEFAULT_BTREE_ORDER: usize = 4;
|
||||||
/// 默认数据目录。
|
/// 默认数据目录
|
||||||
pub const DEFAULT_DATA_DIR: &str = "data";
|
pub const DEFAULT_DATA_DIR: &str = "data";
|
||||||
/// 默认管道名。
|
/// 默认管道名
|
||||||
pub const DEFAULT_PIPE_NAME: &str = "chusql-storage";
|
pub const DEFAULT_PIPE_NAME: &str = "chusql-storage";
|
||||||
/// 默认日志等级。
|
/// 默认日志等级
|
||||||
pub const DEFAULT_LOG_LEVEL: &str = "info";
|
pub const DEFAULT_LOG_LEVEL: &str = "info";
|
||||||
|
|
||||||
/// 页大小允许的范围。
|
/// 页大小允许范围
|
||||||
const MIN_PAGE_SIZE: usize = 512;
|
const MIN_PAGE_SIZE: usize = 512;
|
||||||
const MAX_PAGE_SIZE: usize = 65536;
|
const MAX_PAGE_SIZE: usize = 65536;
|
||||||
/// B+ 树节点至少要放得下 2 个键,分裂规则才成立。
|
/// order 最小值
|
||||||
const MIN_BTREE_ORDER: usize = 3;
|
const MIN_BTREE_ORDER: usize = 3;
|
||||||
|
|
||||||
/// 生效的配置。
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
// 配置
|
||||||
|
/// 生效的配置
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub page_size: usize,
|
pub page_size: usize,
|
||||||
pub btree_order: usize,
|
pub btree_order: usize,
|
||||||
|
pub pool_size: usize,
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
pub pipe_name: String,
|
pub pipe_name: String,
|
||||||
pub log_level: Level,
|
pub log_level: Level,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 全默认值
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Config {
|
Config {
|
||||||
page_size: DEFAULT_PAGE_SIZE,
|
page_size: DEFAULT_PAGE_SIZE,
|
||||||
btree_order: DEFAULT_BTREE_ORDER,
|
btree_order: DEFAULT_BTREE_ORDER,
|
||||||
|
pool_size: DEFAULT_POOL_SIZE,
|
||||||
data_dir: PathBuf::from(DEFAULT_DATA_DIR),
|
data_dir: PathBuf::from(DEFAULT_DATA_DIR),
|
||||||
pipe_name: DEFAULT_PIPE_NAME.to_string(),
|
pipe_name: DEFAULT_PIPE_NAME.to_string(),
|
||||||
log_level: Level::Info,
|
log_level: Level::Info,
|
||||||
@@ -50,8 +55,8 @@ impl Default for Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一个选项的来源。
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
/// 一个选项的来源
|
||||||
pub enum Origin {
|
pub enum Origin {
|
||||||
Default,
|
Default,
|
||||||
File(PathBuf),
|
File(PathBuf),
|
||||||
@@ -59,7 +64,7 @@ pub enum Origin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Origin {
|
impl Origin {
|
||||||
/// 日志里那一列怎么写。
|
/// 来源的文案
|
||||||
pub fn describe(&self) -> String {
|
pub fn describe(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Origin::Default => "default".to_string(),
|
Origin::Default => "default".to_string(),
|
||||||
@@ -69,20 +74,22 @@ impl Origin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 生效配置 + 每个选项的来源。
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
/// 配置 + 每项来源
|
||||||
pub struct Loaded {
|
pub struct Loaded {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub config_path: Option<PathBuf>,
|
pub config_path: Option<PathBuf>,
|
||||||
pub origins: Vec<(&'static str, String, Origin)>,
|
pub origins: Vec<(&'static str, String, Origin)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TOML 文件里的形状:字段全部可选,没写的就当"文件没提这一项"。
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
// 文件形状
|
||||||
|
/// TOML 的形状(字段都可选)
|
||||||
struct FileConfig {
|
struct FileConfig {
|
||||||
page: Option<FilePage>,
|
page: Option<FilePage>,
|
||||||
btree: Option<FileBtree>,
|
btree: Option<FileBtree>,
|
||||||
|
buffer: Option<FileBuffer>,
|
||||||
storage: Option<FileStorage>,
|
storage: Option<FileStorage>,
|
||||||
server: Option<FileServer>,
|
server: Option<FileServer>,
|
||||||
log: Option<FileLog>,
|
log: Option<FileLog>,
|
||||||
@@ -90,35 +97,48 @@ struct FileConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// [buffer] 段
|
||||||
|
struct FileBuffer {
|
||||||
|
pool_size: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// [page] 段
|
||||||
struct FilePage {
|
struct FilePage {
|
||||||
size: Option<usize>,
|
size: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// [btree] 段
|
||||||
struct FileBtree {
|
struct FileBtree {
|
||||||
order: Option<usize>,
|
order: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// [storage] 段
|
||||||
struct FileStorage {
|
struct FileStorage {
|
||||||
data_dir: Option<String>,
|
data_dir: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// [server] 段
|
||||||
struct FileServer {
|
struct FileServer {
|
||||||
pipe_name: Option<String>,
|
pipe_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// [log] 段
|
||||||
struct FileLog {
|
struct FileLog {
|
||||||
level: Option<String>,
|
level: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从真实环境加载:CHUSQL_CONFIG(或当前目录的默认文件)+ 环境变量覆盖。
|
// 加载
|
||||||
|
/// 按真实环境加载
|
||||||
pub fn load() -> Result<Loaded, String> {
|
pub fn load() -> Result<Loaded, String> {
|
||||||
let path = match std::env::var("CHUSQL_CONFIG") {
|
let path = match std::env::var("CHUSQL_CONFIG") {
|
||||||
Ok(p) if !p.trim().is_empty() => Some(PathBuf::from(p)),
|
Ok(p) if !p.trim().is_empty() => Some(PathBuf::from(p)),
|
||||||
@@ -137,7 +157,7 @@ pub fn load() -> Result<Loaded, String> {
|
|||||||
resolve(text.as_deref(), path, &|k| std::env::var(k).ok())
|
resolve(text.as_deref(), path, &|k| std::env::var(k).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析 TOML 文本 + 套环境变量覆盖;测试直接调这个,不碰真实环境。
|
/// 解析文本并套环境变量
|
||||||
pub fn resolve(
|
pub fn resolve(
|
||||||
text: Option<&str>,
|
text: Option<&str>,
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
@@ -180,6 +200,18 @@ pub fn resolve(
|
|||||||
.map_err(|_| format!("not a number: {}", s))
|
.map_err(|_| format!("not a number: {}", s))
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
let (pool_size, pool_origin) = pick(
|
||||||
|
"CHUSQL_POOL_SIZE",
|
||||||
|
file.buffer.and_then(|b| b.pool_size),
|
||||||
|
DEFAULT_POOL_SIZE,
|
||||||
|
&file_origin,
|
||||||
|
env,
|
||||||
|
|s| {
|
||||||
|
s.trim()
|
||||||
|
.parse::<usize>()
|
||||||
|
.map_err(|_| format!("not a number: {}", s))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
let (data_dir, dir_origin) = pick(
|
let (data_dir, dir_origin) = pick(
|
||||||
"CHUSQL_DATA_DIR",
|
"CHUSQL_DATA_DIR",
|
||||||
file.storage.and_then(|s| s.data_dir),
|
file.storage.and_then(|s| s.data_dir),
|
||||||
@@ -214,10 +246,13 @@ pub fn resolve(
|
|||||||
return Err("server.pipe_name must not be empty".to_string());
|
return Err("server.pipe_name must not be empty".to_string());
|
||||||
}
|
}
|
||||||
validate_layout(page_size, btree_order)?;
|
validate_layout(page_size, btree_order)?;
|
||||||
|
if pool_size == 0 {
|
||||||
|
return Err("buffer.pool_size must be at least 1".to_string());
|
||||||
|
}
|
||||||
let origins = vec![
|
let origins = vec![
|
||||||
("page.size", page_size.to_string(), page_origin),
|
("page.size", page_size.to_string(), page_origin),
|
||||||
("btree.order", btree_order.to_string(), order_origin),
|
("btree.order", btree_order.to_string(), order_origin),
|
||||||
|
("buffer.pool_size", pool_size.to_string(), pool_origin), // 新增
|
||||||
("storage.data_dir", data_dir.clone(), dir_origin),
|
("storage.data_dir", data_dir.clone(), dir_origin),
|
||||||
("server.pipe_name", pipe_name.clone(), pipe_origin),
|
("server.pipe_name", pipe_name.clone(), pipe_origin),
|
||||||
("log.level", log_level.name().to_string(), log_origin),
|
("log.level", log_level.name().to_string(), log_origin),
|
||||||
@@ -227,6 +262,7 @@ pub fn resolve(
|
|||||||
config: Config {
|
config: Config {
|
||||||
page_size,
|
page_size,
|
||||||
btree_order,
|
btree_order,
|
||||||
|
pool_size,
|
||||||
data_dir: PathBuf::from(data_dir),
|
data_dir: PathBuf::from(data_dir),
|
||||||
pipe_name,
|
pipe_name,
|
||||||
log_level,
|
log_level,
|
||||||
@@ -236,7 +272,7 @@ pub fn resolve(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 取一个选项:环境变量 → 配置文件 → 默认值,并记下它从哪来。
|
/// 取一项:env -> 文件 -> 默认
|
||||||
fn pick<T: Clone>(
|
fn pick<T: Clone>(
|
||||||
env_key: &'static str,
|
env_key: &'static str,
|
||||||
file_value: Option<T>,
|
file_value: Option<T>,
|
||||||
@@ -255,7 +291,8 @@ fn pick<T: Clone>(
|
|||||||
Ok((default, Origin::Default))
|
Ok((default, Origin::Default))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查选项本身和选项之间是否说得通。
|
// 校验
|
||||||
|
/// 检查选项是否自洽
|
||||||
fn validate_layout(page_size: usize, btree_order: usize) -> Result<(), String> {
|
fn validate_layout(page_size: usize, btree_order: usize) -> Result<(), String> {
|
||||||
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&page_size) || !page_size.is_power_of_two() {
|
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&page_size) || !page_size.is_power_of_two() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
+60
-42
@@ -6,71 +6,68 @@ use crate::page::PageId;
|
|||||||
use crate::page::{Page, PageFile};
|
use crate::page::{Page, PageFile};
|
||||||
use crate::protocol::Row;
|
use crate::protocol::Row;
|
||||||
|
|
||||||
// 堆表:把行按插入顺序塞进页里,支持全表扫描。页大小由配置给(默认 4096)。
|
// 堆表:行按插入顺序进页,支持全表扫描与按索引取行。
|
||||||
// 页内布局(slotted page):
|
|
||||||
// [0..2] slot_count: u16(小端)
|
|
||||||
// [2..] 槽位目录,每槽 8 字节:offset u32 + len u32
|
|
||||||
// [中间] 空闲空间
|
|
||||||
// [页尾] 行数据,从后往前长
|
|
||||||
// 删除:把槽位的 len 置 0,数据不动(vacuum 以后再说)。
|
|
||||||
|
|
||||||
/// 页头:slot_count 占 2 字节。
|
// 页内布局
|
||||||
|
/// 页头:slot_count 占 2 字节
|
||||||
const HEADER_SIZE: usize = 2;
|
const HEADER_SIZE: usize = 2;
|
||||||
/// 一个槽位占 8 字节:offset(u32) + len(u32)。
|
/// 一个槽位占 8 字节
|
||||||
const SLOT_SIZE: usize = 8;
|
const SLOT_SIZE: usize = 8;
|
||||||
/// 槽位目录起点。
|
/// 槽位目录起点
|
||||||
const SLOT_DIR_START: usize = HEADER_SIZE;
|
const SLOT_DIR_START: usize = HEADER_SIZE;
|
||||||
|
|
||||||
/// 从字节里读一个小端 u16。
|
/// 读一个小端 u16
|
||||||
fn read_u16(buf: &[u8], off: usize) -> u16 {
|
fn read_u16(buf: &[u8], off: usize) -> u16 {
|
||||||
u16::from_le_bytes([buf[off], buf[off + 1]])
|
u16::from_le_bytes([buf[off], buf[off + 1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 往字节里写一个小端 u16。
|
/// 写一个小端 u16
|
||||||
fn write_u16(buf: &mut [u8], off: usize, v: u16) {
|
fn write_u16(buf: &mut [u8], off: usize, v: u16) {
|
||||||
buf[off..off + 2].copy_from_slice(&v.to_le_bytes());
|
buf[off..off + 2].copy_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从字节里读一个小端 u32。
|
/// 读一个小端 u32
|
||||||
fn read_u32(buf: &[u8], off: usize) -> u32 {
|
fn read_u32(buf: &[u8], off: usize) -> u32 {
|
||||||
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
|
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 往字节里写一个小端 u32。
|
/// 写一个小端 u32
|
||||||
fn write_u32(buf: &mut [u8], off: usize, v: u32) {
|
fn write_u32(buf: &mut [u8], off: usize, v: u32) {
|
||||||
buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
|
buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一个 (页号, 槽位号) 打包成一个 u64。页号高 48 位,槽位号低 16 位。
|
// 索引位置
|
||||||
|
/// (页号, 槽位号) 打包成 u64
|
||||||
fn pack_position(page_id: PageId, slot: u16) -> u64 {
|
fn pack_position(page_id: PageId, slot: u16) -> u64 {
|
||||||
(page_id << 16) | (slot as u64)
|
(page_id << 16) | (slot as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从 u64 解回 (页号, 槽位号)。
|
/// 从 u64 解回 (页号, 槽位号)
|
||||||
fn unpack_position(v: u64) -> (PageId, u16) {
|
fn unpack_position(v: u64) -> (PageId, u16) {
|
||||||
(v >> 16, (v & 0xFFFF) as u16)
|
(v >> 16, (v & 0xFFFF) as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页内操作:不管文件,只管一页里的字节。
|
// 页内操作
|
||||||
|
/// 只管一页里的字节
|
||||||
pub struct HeapPage;
|
pub struct HeapPage;
|
||||||
|
|
||||||
impl HeapPage {
|
impl HeapPage {
|
||||||
/// 这一页里有多少个槽位。
|
/// 这一页有几个槽位
|
||||||
pub fn slot_count(page: &Page) -> u16 {
|
pub fn slot_count(page: &Page) -> u16 {
|
||||||
read_u16(&page.data, 0)
|
read_u16(&page.data, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 改写槽位数量。
|
/// 改写槽位数量
|
||||||
fn set_slot_count(page: &mut Page, v: u16) {
|
fn set_slot_count(page: &mut Page, v: u16) {
|
||||||
write_u16(&mut page.data, 0, v);
|
write_u16(&mut page.data, 0, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 空闲空间下界:槽位目录的末尾。
|
/// 空闲空间下界
|
||||||
fn free_start(page: &Page) -> usize {
|
fn free_start(page: &Page) -> usize {
|
||||||
SLOT_DIR_START + (Self::slot_count(page) as usize) * SLOT_SIZE
|
SLOT_DIR_START + (Self::slot_count(page) as usize) * SLOT_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 空闲空间上界:所有存活行里最靠前的那个字节偏移(没存活行时就是页尾)。
|
/// 空闲空间上界
|
||||||
fn free_end(page: &Page) -> usize {
|
fn free_end(page: &Page) -> usize {
|
||||||
let n = Self::slot_count(page);
|
let n = Self::slot_count(page);
|
||||||
let mut min_off = page.data.len() as u32;
|
let mut min_off = page.data.len() as u32;
|
||||||
@@ -85,7 +82,7 @@ impl HeapPage {
|
|||||||
min_off as usize
|
min_off as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 试插一行:成功返回槽位号,放不下返回 None。
|
/// 试插一行,放不下返回 None
|
||||||
pub fn insert_tuple(page: &mut Page, tuple: &[u8]) -> Option<u16> {
|
pub fn insert_tuple(page: &mut Page, tuple: &[u8]) -> Option<u16> {
|
||||||
let n = Self::slot_count(page);
|
let n = Self::slot_count(page);
|
||||||
let free_start = Self::free_start(page);
|
let free_start = Self::free_start(page);
|
||||||
@@ -95,11 +92,9 @@ impl HeapPage {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 行数据紧贴已有数据前面
|
|
||||||
let tuple_off = free_end - tuple.len();
|
let tuple_off = free_end - tuple.len();
|
||||||
page.data[tuple_off..tuple_off + tuple.len()].copy_from_slice(tuple);
|
page.data[tuple_off..tuple_off + tuple.len()].copy_from_slice(tuple);
|
||||||
|
|
||||||
// 追加槽位
|
|
||||||
let slot_off = SLOT_DIR_START + (n as usize) * SLOT_SIZE;
|
let slot_off = SLOT_DIR_START + (n as usize) * SLOT_SIZE;
|
||||||
write_u32(&mut page.data, slot_off, tuple_off as u32);
|
write_u32(&mut page.data, slot_off, tuple_off as u32);
|
||||||
write_u32(&mut page.data, slot_off + 4, tuple.len() as u32);
|
write_u32(&mut page.data, slot_off + 4, tuple.len() as u32);
|
||||||
@@ -108,7 +103,7 @@ impl HeapPage {
|
|||||||
Some(n)
|
Some(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读槽位里的行字节;槽位不存在或已删除返回 None。
|
/// 读槽位里的行字节
|
||||||
pub fn get_tuple(page: &Page, slot: u16) -> Option<Vec<u8>> {
|
pub fn get_tuple(page: &Page, slot: u16) -> Option<Vec<u8>> {
|
||||||
let n = Self::slot_count(page);
|
let n = Self::slot_count(page);
|
||||||
if slot >= n {
|
if slot >= n {
|
||||||
@@ -123,46 +118,52 @@ impl HeapPage {
|
|||||||
Some(page.data[off..off + len].to_vec())
|
Some(page.data[off..off + len].to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 所有槽位号(含已删除的)。
|
/// 所有槽位号
|
||||||
pub fn iter_slots(page: &Page) -> Vec<u16> {
|
pub fn iter_slots(page: &Page) -> Vec<u16> {
|
||||||
(0..Self::slot_count(page)).collect()
|
(0..Self::slot_count(page)).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 堆表:一个文件 = 若干页,顺序追加行。
|
// 堆表
|
||||||
|
/// 一个文件 = 若干页
|
||||||
pub struct HeapTable {
|
pub struct HeapTable {
|
||||||
file: PageFile,
|
file: PageFile,
|
||||||
index: Option<DiskBTree>,
|
index: Option<DiskBTree>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HeapTable {
|
impl HeapTable {
|
||||||
/// 打开(或创建)一张堆表。
|
/// 打开表(不带索引)
|
||||||
pub fn open<P: AsRef<Path>>(path: P, page_size: usize) -> io::Result<Self> {
|
pub fn open<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
page_size: usize,
|
||||||
|
pool_size: usize,
|
||||||
|
) -> io::Result<Self> {
|
||||||
Ok(HeapTable {
|
Ok(HeapTable {
|
||||||
file: PageFile::open(path, page_size)?,
|
file: PageFile::with_options(path, page_size, pool_size)?,
|
||||||
index: None,
|
index: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打开带 B+ 树索引的表;索引文件不存在会自动建。
|
/// 打开表 + 索引
|
||||||
pub fn open_indexed<P: AsRef<Path>, Q: AsRef<Path>>(
|
pub fn open_indexed<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||||
path: P,
|
path: P,
|
||||||
index_path: Q,
|
index_path: Q,
|
||||||
page_size: usize,
|
page_size: usize,
|
||||||
btree_order: usize,
|
btree_order: usize,
|
||||||
|
pool_size: usize,
|
||||||
) -> io::Result<Self> {
|
) -> io::Result<Self> {
|
||||||
Ok(HeapTable {
|
Ok(HeapTable {
|
||||||
file: PageFile::open(path, page_size)?,
|
file: PageFile::with_options(path, page_size, pool_size)?,
|
||||||
index: Some(DiskBTree::open(index_path, page_size, btree_order)?),
|
index: Some(DiskBTree::open(index_path, page_size, btree_order, pool_size)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插入一行;不更新索引。
|
/// 插入一行,不更新索引
|
||||||
pub fn insert(&mut self, row: &Row) -> io::Result<()> {
|
pub fn insert(&mut self, row: &Row) -> io::Result<()> {
|
||||||
self.insert_returning_position(row).map(|_| ())
|
self.insert_returning_position(row).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插入一行并返回它的 (页号, 槽位号)。
|
/// 插入并返回行位置
|
||||||
pub fn insert_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
|
pub fn insert_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
|
||||||
let tuple = serde_json::to_vec(row).map_err(io::Error::other)?;
|
let tuple = serde_json::to_vec(row).map_err(io::Error::other)?;
|
||||||
|
|
||||||
@@ -190,7 +191,7 @@ impl HeapTable {
|
|||||||
Ok((page_id, slot))
|
Ok((page_id, slot))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插入一行,并把 (key, 位置) 写进索引。
|
/// 插入一行并写索引
|
||||||
pub fn insert_keyed(&mut self, row: &Row, key: i64) -> io::Result<()> {
|
pub fn insert_keyed(&mut self, row: &Row, key: i64) -> io::Result<()> {
|
||||||
let (page_id, slot) = self.insert_returning_position(row)?;
|
let (page_id, slot) = self.insert_returning_position(row)?;
|
||||||
if let Some(idx) = &mut self.index {
|
if let Some(idx) = &mut self.index {
|
||||||
@@ -199,7 +200,7 @@ impl HeapTable {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按索引键取一行;没索引或键不存在返回 None。
|
/// 按索引键取一行
|
||||||
pub fn get_by_key(&mut self, key: i64) -> io::Result<Option<Row>> {
|
pub fn get_by_key(&mut self, key: i64) -> io::Result<Option<Row>> {
|
||||||
let pos = match &mut self.index {
|
let pos = match &mut self.index {
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
@@ -212,7 +213,7 @@ impl HeapTable {
|
|||||||
self.read_at(page_id, slot)
|
self.read_at(page_id, slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按 (页号, 槽位号) 读一行。
|
/// 按位置读一行
|
||||||
pub fn read_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
|
pub fn read_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
|
||||||
let p = self.file.read_page(page_id)?;
|
let p = self.file.read_page(page_id)?;
|
||||||
match HeapPage::get_tuple(&p, slot) {
|
match HeapPage::get_tuple(&p, slot) {
|
||||||
@@ -224,7 +225,7 @@ impl HeapTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 全表扫描:按页顺序、按槽位顺序返回所有行。
|
/// 全表扫描
|
||||||
pub fn scan(&mut self) -> io::Result<Vec<Row>> {
|
pub fn scan(&mut self) -> io::Result<Vec<Row>> {
|
||||||
let n = self.file.num_pages()?;
|
let n = self.file.num_pages()?;
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
@@ -241,8 +242,7 @@ impl HeapTable {
|
|||||||
Ok(rows)
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 整表改写:清空堆表和索引,再把新行逐行插回去。
|
/// 整表改写并重建索引
|
||||||
/// 行里有整数 id 就顺手重建索引 —— 堆表位置变了,旧索引必须作废,否则会按老位置读出别的行。
|
|
||||||
pub fn replace_all(&mut self, rows: &[Row]) -> io::Result<()> {
|
pub fn replace_all(&mut self, rows: &[Row]) -> io::Result<()> {
|
||||||
self.file.truncate()?;
|
self.file.truncate()?;
|
||||||
if let Some(idx) = &mut self.index {
|
if let Some(idx) = &mut self.index {
|
||||||
@@ -256,9 +256,27 @@ impl HeapTable {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 把脏页写回
|
||||||
|
pub fn flush(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(idx) = &mut self.index {
|
||||||
|
idx.flush()?;
|
||||||
|
}
|
||||||
|
self.file.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓冲池统计
|
||||||
|
/// 当前缓存页数
|
||||||
|
pub fn cached_pages(&self) -> usize { self.file.cached_pages() }
|
||||||
|
/// 命中次数
|
||||||
|
pub fn hits(&self) -> u64 { self.file.hits() }
|
||||||
|
/// 未命中次数
|
||||||
|
pub fn misses(&self) -> u64 { self.file.misses() }
|
||||||
|
/// 命中率
|
||||||
|
pub fn hit_rate(&self) -> f64 { self.file.hit_rate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一行里的索引键:id 列是整数就用它(和 Haskell 侧"主键列叫 id"的约定一致)。
|
/// 取行里的 id 作索引键
|
||||||
fn row_key(row: &Row) -> Option<i64> {
|
fn row_key(row: &Row) -> Option<i64> {
|
||||||
match row.get("id") {
|
match row.get("id") {
|
||||||
Some(serde_json::Value::Number(n)) => n.as_i64(),
|
Some(serde_json::Value::Number(n)) => n.as_i64(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// 存储层库入口:对外暴露 page / protocol / heap / btree / config / log(server 与集成测试都从这里引)。
|
// 存储层库入口:对外暴露下面这些模块,服务与集成测试都从这里引。
|
||||||
|
|
||||||
pub mod btree;
|
pub mod btree;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|||||||
+36
-40
@@ -3,23 +3,21 @@ use std::io::{IsTerminal, Write};
|
|||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
// 极简日志:等级存在一个原子变量里,被关掉的等级只花一次原子读——不求值参数、不做格式化。
|
// 极简日志:等级用原子变量,关掉的等级只花一次原子读。
|
||||||
// 行格式:[等级 本地日期时间] [来源] 消息
|
|
||||||
// - ERROR 加粗红、WARN 加粗橙、DEBUG 加粗不变色(只在 stderr 是终端时着色,重定向到文件时是纯文本)
|
|
||||||
// - 来源区分 core(服务自身:启动、配置)和 pipe(由管道请求触发)
|
|
||||||
// 等级由配置文件的 log.level 或环境变量 CHUSQL_LOG 决定。
|
|
||||||
|
|
||||||
/// 转义序列收尾。
|
// 颜色
|
||||||
|
/// 转义序列收尾
|
||||||
const RESET: &str = "\u{1b}[0m";
|
const RESET: &str = "\u{1b}[0m";
|
||||||
/// 加粗红(ERROR)。
|
/// 加粗红(ERROR)
|
||||||
const BOLD_RED: &str = "\u{1b}[1m\u{1b}[31m";
|
const BOLD_RED: &str = "\u{1b}[1m\u{1b}[31m";
|
||||||
/// 加粗橙(WARN):256 色里的 208 号。
|
/// 加粗橙(WARN)
|
||||||
const BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m";
|
const BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m";
|
||||||
/// 只加粗(DEBUG)。
|
/// 只加粗(DEBUG)
|
||||||
const BOLD: &str = "\u{1b}[1m";
|
const BOLD: &str = "\u{1b}[1m";
|
||||||
|
|
||||||
/// 日志等级;Off 表示全关。
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
// 等级与来源
|
||||||
|
/// 日志等级
|
||||||
pub enum Level {
|
pub enum Level {
|
||||||
Off,
|
Off,
|
||||||
Error,
|
Error,
|
||||||
@@ -28,15 +26,15 @@ pub enum Level {
|
|||||||
Debug,
|
Debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 日志来源:core = 服务自身,pipe = 由管道请求触发。
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
/// 来源:服务自身或管道请求
|
||||||
pub enum Channel {
|
pub enum Channel {
|
||||||
Core,
|
Core,
|
||||||
Pipe,
|
Pipe,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
/// 日志行里的来源标签。
|
/// 来源标签
|
||||||
fn tag(self) -> &'static str {
|
fn tag(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Channel::Core => "core",
|
Channel::Core => "core",
|
||||||
@@ -46,7 +44,7 @@ impl Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Level {
|
impl Level {
|
||||||
/// 解析等级名(大小写不敏感)。
|
/// 解析等级名
|
||||||
pub fn parse(s: &str) -> Option<Level> {
|
pub fn parse(s: &str) -> Option<Level> {
|
||||||
match s.trim().to_ascii_lowercase().as_str() {
|
match s.trim().to_ascii_lowercase().as_str() {
|
||||||
"off" => Some(Level::Off),
|
"off" => Some(Level::Off),
|
||||||
@@ -58,7 +56,7 @@ impl Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 等级名(写日志和写配置说明用)。
|
/// 等级名
|
||||||
pub fn name(self) -> &'static str {
|
pub fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Level::Off => "off",
|
Level::Off => "off",
|
||||||
@@ -69,7 +67,7 @@ impl Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 日志行里的标签,固定 5 字符宽,方便对齐。
|
/// 行里的等级标签
|
||||||
fn tag(self) -> &'static str {
|
fn tag(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Level::Error => "ERROR",
|
Level::Error => "ERROR",
|
||||||
@@ -79,7 +77,7 @@ impl Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 这个等级要不要着色(INFO 不着色)。
|
/// 等级的颜色
|
||||||
fn color(self) -> &'static str {
|
fn color(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Level::Error => BOLD_RED,
|
Level::Error => BOLD_RED,
|
||||||
@@ -90,29 +88,28 @@ impl Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前等级,默认 info。
|
|
||||||
static LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
|
static LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
|
||||||
|
|
||||||
/// stderr 是不是终端(决定要不要着色),只问一次。
|
|
||||||
static COLOR: OnceLock<bool> = OnceLock::new();
|
static COLOR: OnceLock<bool> = OnceLock::new();
|
||||||
|
|
||||||
/// 设置全局等级。
|
// 写入
|
||||||
|
/// 设置全局等级
|
||||||
pub fn set_level(level: Level) {
|
pub fn set_level(level: Level) {
|
||||||
LEVEL.store(level as u8, Ordering::Relaxed);
|
LEVEL.store(level as u8, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 这个等级要不要输出——热路径上只走这一步(一次原子读 + 比较)。
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
/// 这个等级要不要输出
|
||||||
pub fn enabled(level: Level) -> bool {
|
pub fn enabled(level: Level) -> bool {
|
||||||
level as u8 <= LEVEL.load(Ordering::Relaxed)
|
level as u8 <= LEVEL.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启动时调一次:Windows 上给 stderr 打开 VT 处理,让 ANSI 颜色能被认识。
|
/// 启动时调一次
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
enable_vt();
|
enable_vt();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写出一行;只在 enabled 为真时调用。
|
/// 写出一行
|
||||||
pub fn write(channel: Channel, level: Level, args: fmt::Arguments) {
|
pub fn write(channel: Channel, level: Level, args: fmt::Arguments) {
|
||||||
let msg = args.to_string();
|
let msg = args.to_string();
|
||||||
let line = render(level, channel, color_enabled(), &now(), &msg);
|
let line = render(level, channel, color_enabled(), &now(), &msg);
|
||||||
@@ -121,7 +118,7 @@ pub fn write(channel: Channel, level: Level, args: fmt::Arguments) {
|
|||||||
let _ = writeln!(out, "{}", line);
|
let _ = writeln!(out, "{}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// stderr 是终端才着色;CHUSQL_LOG_COLOR=always / never 可以强制开关。
|
/// 是否着色(终端或强制)
|
||||||
fn color_enabled() -> bool {
|
fn color_enabled() -> bool {
|
||||||
*COLOR.get_or_init(|| match std::env::var("CHUSQL_LOG_COLOR").as_deref().map(str::trim) {
|
*COLOR.get_or_init(|| match std::env::var("CHUSQL_LOG_COLOR").as_deref().map(str::trim) {
|
||||||
Ok("always") | Ok("yes") | Ok("1") => true,
|
Ok("always") | Ok("yes") | Ok("1") => true,
|
||||||
@@ -130,7 +127,7 @@ fn color_enabled() -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 拼一行:[等级 时间] [来源] 消息。纯函数,方便测试。
|
/// 拼一行(纯函数,可测)
|
||||||
fn render(level: Level, channel: Channel, color: bool, stamp: &Stamp, msg: &str) -> String {
|
fn render(level: Level, channel: Channel, color: bool, stamp: &Stamp, msg: &str) -> String {
|
||||||
let tag = level.tag();
|
let tag = level.tag();
|
||||||
let paint = level.color();
|
let paint = level.color();
|
||||||
@@ -142,8 +139,9 @@ fn render(level: Level, channel: Channel, color: bool, stamp: &Stamp, msg: &str)
|
|||||||
format!("[{} {}] [{}] {}", level_text, stamp, channel.tag(), msg)
|
format!("[{} {}] [{}] {}", level_text, stamp, channel.tag(), msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一个本地日期时间。
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
// 时间
|
||||||
|
/// 本地日期时间
|
||||||
pub struct Stamp {
|
pub struct Stamp {
|
||||||
pub year: u16,
|
pub year: u16,
|
||||||
pub month: u16,
|
pub month: u16,
|
||||||
@@ -154,6 +152,7 @@ pub struct Stamp {
|
|||||||
pub milli: u16,
|
pub milli: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 格式:年-月-日 时:分:秒.毫秒
|
||||||
impl fmt::Display for Stamp {
|
impl fmt::Display for Stamp {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
@@ -164,7 +163,7 @@ impl fmt::Display for Stamp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前本地时间:Windows 上走 kernel32::GetLocalTime。
|
/// 当前本地时间(Windows)
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn now() -> Stamp {
|
fn now() -> Stamp {
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -206,7 +205,7 @@ fn now() -> Stamp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前时间:其他平台没有取本地时区的零依赖办法,退回 UTC。
|
/// 当前时间(其他平台用 UTC)
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn now() -> Stamp {
|
fn now() -> Stamp {
|
||||||
let d = std::time::SystemTime::now()
|
let d = std::time::SystemTime::now()
|
||||||
@@ -225,7 +224,7 @@ fn now() -> Stamp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把"距 1970-01-01 的天数"换算成 (年, 月, 日):非 Windows 平台算时间用,单元测试也走这里。
|
/// 天数换算成日期
|
||||||
pub fn civil_from_days(days: i64) -> (i64, u16, u16) {
|
pub fn civil_from_days(days: i64) -> (i64, u16, u16) {
|
||||||
let z = days + 719_468;
|
let z = days + 719_468;
|
||||||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||||
@@ -239,7 +238,8 @@ pub fn civil_from_days(days: i64) -> (i64, u16, u16) {
|
|||||||
(y + i64::from(month <= 2), month, day)
|
(y + i64::from(month <= 2), month, day)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Windows:打开 stderr 的 VT 处理。
|
// 终端
|
||||||
|
/// 打开 Windows 的 VT 处理
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn enable_vt() {
|
fn enable_vt() {
|
||||||
#[link(name = "kernel32")]
|
#[link(name = "kernel32")]
|
||||||
@@ -249,9 +249,7 @@ fn enable_vt() {
|
|||||||
fn SetConsoleMode(handle: *mut core::ffi::c_void, mode: u32) -> i32;
|
fn SetConsoleMode(handle: *mut core::ffi::c_void, mode: u32) -> i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// STD_ERROR_HANDLE:-12 的无符号写法。
|
|
||||||
const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
|
const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
|
||||||
/// 让控制台认识 ANSI 转义序列。
|
|
||||||
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
|
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -263,11 +261,12 @@ fn enable_vt() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 非 Windows:不用做任何事。
|
/// 其他平台什么都不用做
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn enable_vt() {}
|
fn enable_vt() {}
|
||||||
|
|
||||||
/// 打一条 debug 日志(关掉时只花一次原子读)。
|
// 宏
|
||||||
|
/// debug 日志
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_debug {
|
macro_rules! log_debug {
|
||||||
(core, $($arg:tt)*) => {
|
(core, $($arg:tt)*) => {
|
||||||
@@ -278,7 +277,7 @@ macro_rules! log_debug {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打一条 info 日志。
|
/// info 日志
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_info {
|
macro_rules! log_info {
|
||||||
(core, $($arg:tt)*) => {
|
(core, $($arg:tt)*) => {
|
||||||
@@ -289,7 +288,7 @@ macro_rules! log_info {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打一条 warn 日志。
|
/// warn 日志
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_warn {
|
macro_rules! log_warn {
|
||||||
(core, $($arg:tt)*) => {
|
(core, $($arg:tt)*) => {
|
||||||
@@ -300,7 +299,7 @@ macro_rules! log_warn {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打一条 error 日志。
|
/// error 日志
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_error {
|
macro_rules! log_error {
|
||||||
(core, $($arg:tt)*) => {
|
(core, $($arg:tt)*) => {
|
||||||
@@ -311,7 +310,7 @@ macro_rules! log_error {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 上面四个宏的公共部分:先看等级,再看要不要真的写。内部用,别直接调。
|
/// 四个宏的公共部分
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_line {
|
macro_rules! log_line {
|
||||||
@@ -326,7 +325,6 @@ macro_rules! log_line {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// 天数换算日期:几个已知点 + 两次闰年的 2 月 29 日。
|
|
||||||
#[test]
|
#[test]
|
||||||
fn civil_from_days_known_dates() {
|
fn civil_from_days_known_dates() {
|
||||||
assert_eq!(civil_from_days(0), (1970, 1, 1));
|
assert_eq!(civil_from_days(0), (1970, 1, 1));
|
||||||
@@ -336,7 +334,6 @@ mod tests {
|
|||||||
assert_eq!(civil_from_days(19_782), (2024, 2, 29));
|
assert_eq!(civil_from_days(19_782), (2024, 2, 29));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 行格式:等级 + 本地日期时间 + 来源 + 消息,等级该粗的粗、该色的色。
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_has_time_channel_and_colors() {
|
fn render_has_time_channel_and_colors() {
|
||||||
let stamp = Stamp {
|
let stamp = Stamp {
|
||||||
@@ -367,7 +364,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 关掉颜色时整行必须是纯文本(重定向到文件时用)。
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_without_color_is_plain() {
|
fn render_without_color_is_plain() {
|
||||||
let stamp = Stamp {
|
let stamp = Stamp {
|
||||||
|
|||||||
+180
-27
@@ -1,23 +1,26 @@
|
|||||||
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
// 页管理:数据库文件按固定大小的页组织,页大小由配置给(默认 4096,见 config.rs)。
|
// 页管理:定长页的读写,外加一层页缓冲池。
|
||||||
// 只负责"按页号读 / 写",不理解页里的内容 —— 内容怎么解释由上层(堆表 / B+ 树 / WAL)决定。
|
|
||||||
// 页号从 0 开始,第 n 页在文件中的偏移是 n * page_size。
|
|
||||||
|
|
||||||
/// 页号:从 0 开始。u64 足够大,不用担心中途用完。
|
// 页
|
||||||
|
/// 页号:从 0 开始
|
||||||
pub type PageId = u64;
|
pub type PageId = u64;
|
||||||
|
|
||||||
/// 一页:固定 page_size 字节,外加自己的页号。
|
/// 缓冲池默认容量(页数)
|
||||||
|
pub const DEFAULT_POOL_SIZE: usize = 64;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
/// 一页:定长字节 + 页号
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
pub id: PageId,
|
pub id: PageId,
|
||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
/// 新建一页,内容全 0。
|
/// 新建一页,全 0
|
||||||
pub fn new(id: PageId, size: usize) -> Self {
|
pub fn new(id: PageId, size: usize) -> Self {
|
||||||
Page {
|
Page {
|
||||||
id,
|
id,
|
||||||
@@ -25,46 +28,118 @@ impl Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清零。复用时先清空,避免残留旧数据。
|
/// 清零
|
||||||
pub fn zero(&mut self) {
|
pub fn zero(&mut self) {
|
||||||
self.data.fill(0);
|
self.data.fill(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页文件:负责把 Page 落到磁盘上(不做缓存,每次读写都直接 syscall;缓冲池是后面的阶段)。
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
/// 页文件:定长页读写 + 页缓存
|
||||||
pub struct PageFile {
|
pub struct PageFile {
|
||||||
file: File,
|
file: File,
|
||||||
page_size: usize,
|
page_size: usize,
|
||||||
|
capacity: usize,
|
||||||
|
cache: HashMap<PageId, Page>,
|
||||||
|
order: VecDeque<PageId>,
|
||||||
|
dirty: HashSet<PageId>,
|
||||||
|
hits: u64,
|
||||||
|
misses: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageFile {
|
impl PageFile {
|
||||||
/// 打开(或创建)一个页文件;页大小由配置传进来。
|
/// 打开页文件(默认池容量)
|
||||||
pub fn open<P: AsRef<Path>>(path: P, page_size: usize) -> io::Result<Self> {
|
pub fn open<P: AsRef<Path>>(path: P, page_size: usize) -> io::Result<Self> {
|
||||||
|
Self::with_options(path, page_size, DEFAULT_POOL_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开页文件并指定池容量
|
||||||
|
pub fn with_options<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
page_size: usize,
|
||||||
|
capacity: usize,
|
||||||
|
) -> io::Result<Self> {
|
||||||
let file = OpenOptions::new()
|
let file = OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(false)
|
.truncate(false)
|
||||||
.open(path)?;
|
.open(path)?;
|
||||||
Ok(PageFile { file, page_size })
|
Ok(PageFile {
|
||||||
|
file,
|
||||||
|
page_size,
|
||||||
|
capacity,
|
||||||
|
cache: HashMap::new(),
|
||||||
|
order: VecDeque::new(),
|
||||||
|
dirty: HashSet::new(),
|
||||||
|
hits: 0,
|
||||||
|
misses: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 这个文件的页大小。
|
/// 页大小
|
||||||
pub fn page_size(&self) -> usize {
|
pub fn page_size(&self) -> usize {
|
||||||
self.page_size
|
self.page_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读取指定页号的页;页号超出文件末尾时返回 UnexpectedEof。
|
// 缓冲池
|
||||||
pub fn read_page(&mut self, id: PageId) -> io::Result<Page> {
|
/// 池容量(页数)
|
||||||
let mut data = vec![0u8; self.page_size];
|
pub fn capacity(&self) -> usize {
|
||||||
self.file.seek(SeekFrom::Start(id * self.page_size as u64))?;
|
self.capacity
|
||||||
self.file.read_exact(&mut data)?;
|
|
||||||
Ok(Page { id, data })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写入一页。若页号超出当前文件末尾,文件会自动扩展。
|
/// 当前缓存了多少页
|
||||||
pub fn write_page(&mut self, page: &Page) -> io::Result<()> {
|
pub fn cached_pages(&self) -> usize {
|
||||||
|
self.cache.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 命中次数
|
||||||
|
pub fn hits(&self) -> u64 {
|
||||||
|
self.hits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 未命中次数
|
||||||
|
pub fn misses(&self) -> u64 {
|
||||||
|
self.misses
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 命中率
|
||||||
|
pub fn hit_rate(&self) -> f64 {
|
||||||
|
let total = self.hits + self.misses;
|
||||||
|
if total == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
self.hits as f64 / total as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 标记为最近使用
|
||||||
|
fn touch(&mut self, id: PageId) {
|
||||||
|
if let Some(pos) = self.order.iter().position(|&x| x == id) {
|
||||||
|
self.order.remove(pos);
|
||||||
|
}
|
||||||
|
self.order.push_back(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 淘汰一个最冷的页
|
||||||
|
fn evict_one(&mut self) -> io::Result<bool> {
|
||||||
|
while let Some(victim) = self.order.pop_front() {
|
||||||
|
if let Some(p) = self.cache.get(&victim) {
|
||||||
|
if self.dirty.contains(&victim) {
|
||||||
|
let p = p.clone();
|
||||||
|
self.write_raw(&p)?;
|
||||||
|
self.dirty.remove(&victim);
|
||||||
|
}
|
||||||
|
self.cache.remove(&victim);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 直接写文件
|
||||||
|
fn write_raw(&mut self, page: &Page) -> io::Result<()> {
|
||||||
if page.data.len() != self.page_size {
|
if page.data.len() != self.page_size {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
@@ -77,12 +152,58 @@ impl PageFile {
|
|||||||
}
|
}
|
||||||
self.file
|
self.file
|
||||||
.seek(SeekFrom::Start(page.id * self.page_size as u64))?;
|
.seek(SeekFrom::Start(page.id * self.page_size as u64))?;
|
||||||
self.file.write_all(&page.data)?;
|
self.file.write_all(&page.data)
|
||||||
self.file.flush()?;
|
}
|
||||||
|
|
||||||
|
/// 直接从文件读
|
||||||
|
fn read_raw(&mut self, id: PageId) -> io::Result<Page> {
|
||||||
|
let mut data = vec![0u8; self.page_size];
|
||||||
|
self.file.seek(SeekFrom::Start(id * self.page_size as u64))?;
|
||||||
|
self.file.read_exact(&mut data)?;
|
||||||
|
Ok(Page { id, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 读一页(走缓存)
|
||||||
|
pub fn read_page(&mut self, id: PageId) -> io::Result<Page> {
|
||||||
|
if let Some(p) = self.cache.get(&id) {
|
||||||
|
let p = p.clone();
|
||||||
|
self.hits += 1;
|
||||||
|
self.touch(id);
|
||||||
|
return Ok(p);
|
||||||
|
}
|
||||||
|
self.misses += 1;
|
||||||
|
let p = self.read_raw(id)?;
|
||||||
|
if self.cache.len() >= self.capacity {
|
||||||
|
self.evict_one()?;
|
||||||
|
}
|
||||||
|
self.cache.insert(id, p.clone());
|
||||||
|
self.order.push_back(id);
|
||||||
|
Ok(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写一页(走缓存)
|
||||||
|
pub fn write_page(&mut self, page: &Page) -> io::Result<()> {
|
||||||
|
if page.data.len() != self.page_size {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!(
|
||||||
|
"page has {} bytes, this file uses {}",
|
||||||
|
page.data.len(),
|
||||||
|
self.page_size
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.cache.len() >= self.capacity && !self.cache.contains_key(&page.id) {
|
||||||
|
self.evict_one()?;
|
||||||
|
}
|
||||||
|
self.cache.insert(page.id, page.clone());
|
||||||
|
self.dirty.insert(page.id);
|
||||||
|
self.touch(page.id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在文件末尾追加一页,返回新页的页号。
|
/// 末尾追加一页
|
||||||
pub fn append_page(&mut self) -> io::Result<Page> {
|
pub fn append_page(&mut self) -> io::Result<Page> {
|
||||||
let id = self.num_pages()?;
|
let id = self.num_pages()?;
|
||||||
let page = Page::new(id, self.page_size);
|
let page = Page::new(id, self.page_size);
|
||||||
@@ -90,7 +211,7 @@ impl PageFile {
|
|||||||
Ok(page)
|
Ok(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前文件里有多少页;长度不是页大小整数倍,说明页大小和文件对不上。
|
/// 文件里有多少页
|
||||||
pub fn num_pages(&mut self) -> io::Result<PageId> {
|
pub fn num_pages(&mut self) -> io::Result<PageId> {
|
||||||
let len = self.file.metadata()?.len();
|
let len = self.file.metadata()?.len();
|
||||||
let size = self.page_size as u64;
|
let size = self.page_size as u64;
|
||||||
@@ -103,18 +224,50 @@ impl PageFile {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(len / size)
|
let on_disk = len / size;
|
||||||
|
let cached_max = self
|
||||||
|
.cache
|
||||||
|
.keys()
|
||||||
|
.copied()
|
||||||
|
.max()
|
||||||
|
.map(|m| m + 1)
|
||||||
|
.unwrap_or(0);
|
||||||
|
Ok(on_disk.max(cached_max))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 强制把文件缓冲刷到磁盘。写入关键路径时用。
|
/// 把脏页写回磁盘
|
||||||
|
pub fn flush(&mut self) -> io::Result<()> {
|
||||||
|
let dirty: Vec<PageId> = self.dirty.iter().copied().collect();
|
||||||
|
for id in dirty {
|
||||||
|
if let Some(p) = self.cache.get(&id) {
|
||||||
|
let p = p.clone();
|
||||||
|
self.write_raw(&p)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.file.sync_all()?;
|
||||||
|
self.dirty.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// flush 之后 fsync
|
||||||
pub fn sync(&mut self) -> io::Result<()> {
|
pub fn sync(&mut self) -> io::Result<()> {
|
||||||
self.file.sync_all()
|
self.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清空文件,游标归零。
|
/// 清空文件
|
||||||
pub fn truncate(&mut self) -> io::Result<()> {
|
pub fn truncate(&mut self) -> io::Result<()> {
|
||||||
|
self.cache.clear();
|
||||||
|
self.order.clear();
|
||||||
|
self.dirty.clear();
|
||||||
self.file.set_len(0)?;
|
self.file.set_len(0)?;
|
||||||
self.file.seek(SeekFrom::Start(0))?;
|
self.file.seek(SeekFrom::Start(0))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 退出前把脏页写回
|
||||||
|
impl Drop for PageFile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// 协议定义:Haskell ↔ Rust 之间那条"行分隔 JSON"消息的类型(请求 / 响应)。
|
// 协议:Haskell 与 Rust 之间的请求 / 响应类型。
|
||||||
|
|
||||||
/// 一行:列名到 JSON 值的映射。
|
/// 一行:列名到 JSON 值
|
||||||
pub type Row = HashMap<String, serde_json::Value>;
|
pub type Row = HashMap<String, serde_json::Value>;
|
||||||
|
|
||||||
/// 列类型;跟 Haskell 侧 TInt / TStr / TBool 对应。
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
/// 列类型(对应 Haskell 的 TInt / TStr / TBool)
|
||||||
pub enum ColumnType {
|
pub enum ColumnType {
|
||||||
Int,
|
Int,
|
||||||
Str,
|
Str,
|
||||||
Bool,
|
Bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 一列的 schema。
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
/// 一列的 schema
|
||||||
pub struct SchemaColumn {
|
pub struct SchemaColumn {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub ty: ColumnType,
|
pub ty: ColumnType,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求;method 字段做 tag。
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "method", rename_all = "snake_case")]
|
#[serde(tag = "method", rename_all = "snake_case")]
|
||||||
|
/// 请求:method 字段决定类型
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
Ping,
|
Ping,
|
||||||
Scan { table: String },
|
Scan { table: String },
|
||||||
@@ -38,16 +38,25 @@ pub enum Request {
|
|||||||
ReplaceAll { table: String, rows: Vec<Row> },
|
ReplaceAll { table: String, rows: Vec<Row> },
|
||||||
ListTables,
|
ListTables,
|
||||||
DescribeTable { table: String },
|
DescribeTable { table: String },
|
||||||
|
CreateTable {
|
||||||
|
table: String,
|
||||||
|
columns: Vec<SchemaColumn>,
|
||||||
|
},
|
||||||
|
DropTable { table: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 响应;status 字段做 tag。
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "status", rename_all = "lowercase")]
|
#[serde(tag = "status", rename_all = "lowercase")]
|
||||||
|
/// 响应:status 字段决定类型
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
Pong,
|
Pong,
|
||||||
Rows { rows: Vec<Row> },
|
Rows { rows: Vec<Row> },
|
||||||
Tables { tables: Vec<String> },
|
Tables { tables: Vec<String> },
|
||||||
Schema { columns: Vec<SchemaColumn> },
|
Schema {
|
||||||
|
columns: Vec<SchemaColumn>,
|
||||||
|
#[serde(default)]
|
||||||
|
row_count: u64,
|
||||||
|
},
|
||||||
Ok,
|
Ok,
|
||||||
Error { message: String },
|
Error { message: String },
|
||||||
}
|
}
|
||||||
+13
-32
@@ -1,15 +1,3 @@
|
|||||||
//! 简化版 WAL:写前日志 + 崩溃恢复。
|
|
||||||
//!
|
|
||||||
//! 条目格式:
|
|
||||||
//! [u32 body_len][body]
|
|
||||||
//! body = [u64 LSN][u8 操作类型][u16 表名长度][表名字节][JSON 载荷]
|
|
||||||
//!
|
|
||||||
//! 简化版约束:
|
|
||||||
//! - 最多只有一条未完成的操作。写成功就清空 WAL。
|
|
||||||
//! - 不做 checkpoint / redo / undo / 事务。
|
|
||||||
//!
|
|
||||||
//! 崩溃恢复:
|
|
||||||
//! - 启动时若 WAL 非空,重放那一条,然后清空。
|
|
||||||
|
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
@@ -19,12 +7,13 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::protocol::Row;
|
use crate::protocol::Row;
|
||||||
|
|
||||||
/// 操作类型标记。
|
// 预写日志:先记操作并 fsync,再改数据;重启重放残留。
|
||||||
|
|
||||||
const OP_INSERT: u8 = 1;
|
const OP_INSERT: u8 = 1;
|
||||||
const OP_REPLACE_ALL: u8 = 2;
|
const OP_REPLACE_ALL: u8 = 2;
|
||||||
|
|
||||||
/// 一条 WAL 操作。
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
/// 一条待写入的操作
|
||||||
pub enum WalOp {
|
pub enum WalOp {
|
||||||
Insert {
|
Insert {
|
||||||
table: String,
|
table: String,
|
||||||
@@ -37,25 +26,26 @@ pub enum WalOp {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// WAL 文件句柄。
|
/// WAL 文件
|
||||||
pub struct Wal {
|
pub struct Wal {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wal {
|
impl Wal {
|
||||||
/// 绑定到 WAL 文件路径;首次 append 时自动创建文件。
|
// 读写
|
||||||
|
/// 绑定 WAL 文件路径
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||||
Wal {
|
Wal {
|
||||||
path: path.as_ref().to_path_buf(),
|
path: path.as_ref().to_path_buf(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 日志文件路径。
|
/// 日志文件路径
|
||||||
pub fn path(&self) -> &Path {
|
pub fn path(&self) -> &Path {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清空 WAL;文件不存在则什么都不做。
|
/// 清空 WAL
|
||||||
pub fn clear(&self) -> io::Result<()> {
|
pub fn clear(&self) -> io::Result<()> {
|
||||||
if self.path.exists() {
|
if self.path.exists() {
|
||||||
let f = OpenOptions::new().write(true).open(&self.path)?;
|
let f = OpenOptions::new().write(true).open(&self.path)?;
|
||||||
@@ -65,7 +55,7 @@ impl Wal {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 追加一条操作并 fsync。
|
/// 追加一条并 fsync
|
||||||
pub fn append(&self, op: &WalOp) -> io::Result<()> {
|
pub fn append(&self, op: &WalOp) -> io::Result<()> {
|
||||||
let bytes = encode(op)?;
|
let bytes = encode(op)?;
|
||||||
let mut f = OpenOptions::new()
|
let mut f = OpenOptions::new()
|
||||||
@@ -77,7 +67,7 @@ impl Wal {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读取当前 WAL;空文件 / 不存在返回 None。
|
/// 读当前 WAL;空则 None
|
||||||
pub fn read(&self) -> io::Result<Option<WalOp>> {
|
pub fn read(&self) -> io::Result<Option<WalOp>> {
|
||||||
if !self.path.exists() {
|
if !self.path.exists() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -98,9 +88,9 @@ impl Wal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把一条操作编成字节。
|
// 编解码
|
||||||
|
/// 把操作编成字节
|
||||||
fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
|
fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
|
||||||
// 先编出 (类型, 表名, 载荷)
|
|
||||||
let (op_type, table, payload) = match op {
|
let (op_type, table, payload) = match op {
|
||||||
WalOp::Insert { table, row, key } => {
|
WalOp::Insert { table, row, key } => {
|
||||||
let p = serde_json::to_vec(&(row, key)).map_err(io::Error::other)?;
|
let p = serde_json::to_vec(&(row, key)).map_err(io::Error::other)?;
|
||||||
@@ -114,8 +104,6 @@ fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
|
|||||||
|
|
||||||
let table_bytes = table.as_bytes();
|
let table_bytes = table.as_bytes();
|
||||||
|
|
||||||
// body = [u64 LSN][u8 类型][u16 表名长度][表名][载荷]
|
|
||||||
// 简化版 LSN 固定为 1;完整版会改成真正递增。
|
|
||||||
let lsn: u64 = 1;
|
let lsn: u64 = 1;
|
||||||
let mut body = Vec::with_capacity(8 + 1 + 2 + table_bytes.len() + payload.len());
|
let mut body = Vec::with_capacity(8 + 1 + 2 + table_bytes.len() + payload.len());
|
||||||
body.extend_from_slice(&lsn.to_le_bytes());
|
body.extend_from_slice(&lsn.to_le_bytes());
|
||||||
@@ -124,32 +112,25 @@ fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
|
|||||||
body.extend_from_slice(table_bytes);
|
body.extend_from_slice(table_bytes);
|
||||||
body.extend_from_slice(&payload);
|
body.extend_from_slice(&payload);
|
||||||
|
|
||||||
// 整条 = [u32 body_len][body]
|
|
||||||
let mut out = Vec::with_capacity(4 + body.len());
|
let mut out = Vec::with_capacity(4 + body.len());
|
||||||
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
|
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
|
||||||
out.extend_from_slice(&body);
|
out.extend_from_slice(&body);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析一条 WAL 条目。
|
/// 从字节解回操作
|
||||||
///
|
|
||||||
/// - `Ok(Some(op))`:成功解析出操作
|
|
||||||
/// - `Ok(None)`:尾部被截断(写入一半就断电),按"没有这条"处理
|
|
||||||
/// - `Err(_)`:字节流本身损坏,抛错
|
|
||||||
fn decode(buf: &[u8]) -> Result<Option<WalOp>, String> {
|
fn decode(buf: &[u8]) -> Result<Option<WalOp>, String> {
|
||||||
if buf.len() < 4 {
|
if buf.len() < 4 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let body_len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
let body_len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
||||||
if buf.len() < 4 + body_len {
|
if buf.len() < 4 + body_len {
|
||||||
// 尾部被截断——忽略
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let body = &buf[4..4 + body_len];
|
let body = &buf[4..4 + body_len];
|
||||||
if body.len() < 8 + 1 + 2 {
|
if body.len() < 8 + 1 + 2 {
|
||||||
return Err("body too short".into());
|
return Err("body too short".into());
|
||||||
}
|
}
|
||||||
// LSN 简化版忽略
|
|
||||||
let _lsn = u64::from_le_bytes([
|
let _lsn = u64::from_le_bytes([
|
||||||
body[0], body[1], body[2], body[3], body[4], body[5], body[6], body[7],
|
body[0], body[1], body[2], body[3], body[4], body[5], body[6], body[7],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use chusql_storage::btree::DiskBTree;
|
use chusql_storage::btree::DiskBTree;
|
||||||
use chusql_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
|
use chusql_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
|
||||||
|
use chusql_storage::page::DEFAULT_POOL_SIZE;
|
||||||
|
|
||||||
// B+ 树测试:空树查询、插入后查找、重开文件数据仍在、覆盖同一个键、升序遍历、
|
// B+ 树测试:插查、重开、分裂、页大小与 order 校验。
|
||||||
// 倒序插入 500 个键、页大小 / order 可以配置、配置和文件头对不上时报错。
|
|
||||||
|
|
||||||
/// 空树的 get 返回 None,遍历返回空。
|
/// 空树查不到,遍历为空
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_tree_get() {
|
fn empty_tree_get() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -12,13 +12,14 @@ fn empty_tree_get() {
|
|||||||
dir.path().join("t.idx"),
|
dir.path().join("t.idx"),
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
DEFAULT_BTREE_ORDER,
|
DEFAULT_BTREE_ORDER,
|
||||||
|
DEFAULT_POOL_SIZE,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(t.get(1).unwrap(), None);
|
assert_eq!(t.get(1).unwrap(), None);
|
||||||
assert_eq!(t.iter_all().unwrap(), vec![]);
|
assert_eq!(t.iter_all().unwrap(), vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插 100 个键,每个都能按 key 查回来。
|
/// 插 100 个键都能查回
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_then_get() {
|
fn insert_then_get() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -26,6 +27,7 @@ fn insert_then_get() {
|
|||||||
dir.path().join("t.idx"),
|
dir.path().join("t.idx"),
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
DEFAULT_BTREE_ORDER,
|
DEFAULT_BTREE_ORDER,
|
||||||
|
DEFAULT_POOL_SIZE,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
@@ -36,24 +38,24 @@ fn insert_then_get() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 关掉再打开同一个索引文件,数据还在。
|
/// 重开文件数据还在
|
||||||
#[test]
|
#[test]
|
||||||
fn reopen_persists() {
|
fn reopen_persists() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("t.idx");
|
let path = dir.path().join("t.idx");
|
||||||
{
|
{
|
||||||
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).unwrap();
|
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
t.insert(i, i as u64).unwrap();
|
t.insert(i, i as u64).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).unwrap();
|
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
assert_eq!(t.get(i).unwrap(), Some(i as u64));
|
assert_eq!(t.get(i).unwrap(), Some(i as u64));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同一个键插两次,后写的值生效。
|
/// 同一个键后写的值生效
|
||||||
#[test]
|
#[test]
|
||||||
fn update_existing() {
|
fn update_existing() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -61,6 +63,7 @@ fn update_existing() {
|
|||||||
dir.path().join("t.idx"),
|
dir.path().join("t.idx"),
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
DEFAULT_BTREE_ORDER,
|
DEFAULT_BTREE_ORDER,
|
||||||
|
DEFAULT_POOL_SIZE,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
t.insert(5, 100).unwrap();
|
t.insert(5, 100).unwrap();
|
||||||
@@ -68,7 +71,7 @@ fn update_existing() {
|
|||||||
assert_eq!(t.get(5).unwrap(), Some(200));
|
assert_eq!(t.get(5).unwrap(), Some(200));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 倒序插入后遍历,结果仍按 key 升序。
|
/// 倒序插入后遍历仍升序
|
||||||
#[test]
|
#[test]
|
||||||
fn iter_all_sorted() {
|
fn iter_all_sorted() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -76,6 +79,7 @@ fn iter_all_sorted() {
|
|||||||
dir.path().join("t.idx"),
|
dir.path().join("t.idx"),
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
DEFAULT_BTREE_ORDER,
|
DEFAULT_BTREE_ORDER,
|
||||||
|
DEFAULT_POOL_SIZE,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for i in (0..50).rev() {
|
for i in (0..50).rev() {
|
||||||
@@ -88,7 +92,7 @@ fn iter_all_sorted() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 倒序插入 500 个键(必然多次分裂),每个键都能查到。
|
/// 倒序插 500 个键都能查到
|
||||||
#[test]
|
#[test]
|
||||||
fn reverse_order_insert() {
|
fn reverse_order_insert() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -96,6 +100,7 @@ fn reverse_order_insert() {
|
|||||||
dir.path().join("t.idx"),
|
dir.path().join("t.idx"),
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
DEFAULT_BTREE_ORDER,
|
DEFAULT_BTREE_ORDER,
|
||||||
|
DEFAULT_POOL_SIZE,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for i in (0..500).rev() {
|
for i in (0..500).rev() {
|
||||||
@@ -106,11 +111,11 @@ fn reverse_order_insert() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页大小和 order 都可以配置:512 字节的页 + 8 个孩子照样能插能查。
|
/// 512 字节页 + 8 个孩子可用
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_page_size_and_order() {
|
fn custom_page_size_and_order() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let mut t = DiskBTree::open(dir.path().join("t.idx"), 512, 8).unwrap();
|
let mut t = DiskBTree::open(dir.path().join("t.idx"), 512, 8, DEFAULT_POOL_SIZE).unwrap();
|
||||||
for i in 0..200 {
|
for i in 0..200 {
|
||||||
t.insert(i, i as u64 + 1).unwrap();
|
t.insert(i, i as u64 + 1).unwrap();
|
||||||
}
|
}
|
||||||
@@ -119,32 +124,32 @@ fn custom_page_size_and_order() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 文件头里记着页大小和 order:换配置再打开要报错,而不是读出乱七八糟的东西。
|
/// 页大小或 order 不符要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn config_mismatch_errors() {
|
fn config_mismatch_errors() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("t.idx");
|
let path = dir.path().join("t.idx");
|
||||||
{
|
{
|
||||||
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).unwrap();
|
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
|
||||||
t.insert(1, 1).unwrap();
|
t.insert(1, 1).unwrap();
|
||||||
}
|
}
|
||||||
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 8).is_err());
|
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 8, DEFAULT_POOL_SIZE).is_err());
|
||||||
assert!(DiskBTree::open(&path, 8192, DEFAULT_BTREE_ORDER).is_err());
|
assert!(DiskBTree::open(&path, 8192, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
|
||||||
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).is_ok());
|
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 不是本项目的索引文件(比如随便一个空文件)要报错。
|
/// 不是索引文件要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn not_an_index_file_errors() {
|
fn not_an_index_file_errors() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("junk.idx");
|
let path = dir.path().join("junk.idx");
|
||||||
std::fs::write(&path, vec![7u8; DEFAULT_PAGE_SIZE]).unwrap();
|
std::fs::write(&path, vec![7u8; DEFAULT_PAGE_SIZE]).unwrap();
|
||||||
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).is_err());
|
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// order 放不进一页时直接拒绝。
|
/// order 放不进一页要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn order_too_large_for_page_errors() {
|
fn order_too_large_for_page_errors() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300).is_err());
|
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300, DEFAULT_POOL_SIZE).is_err());
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,10 @@ use chusql_storage::catalog::Catalog;
|
|||||||
use chusql_storage::protocol::{ColumnType, Row};
|
use chusql_storage::protocol::{ColumnType, Row};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
// 数据字典测试:补列、去重、未知表。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
||||||
let mut m = Row::new();
|
let mut m = Row::new();
|
||||||
for (k, v) in pairs {
|
for (k, v) in pairs {
|
||||||
@@ -10,6 +14,7 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
|||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 空文件得到空字典
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_file_gives_empty_catalog() {
|
fn empty_file_gives_empty_catalog() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -18,6 +23,7 @@ fn empty_file_gives_empty_catalog() {
|
|||||||
assert_eq!(c.table_names().len(), 0);
|
assert_eq!(c.table_names().len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 补列之后能查到 schema
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_then_describe() {
|
fn ensure_then_describe() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -39,6 +45,7 @@ fn ensure_then_describe() {
|
|||||||
assert_eq!(name_col.ty, ColumnType::Str);
|
assert_eq!(name_col.ty, ColumnType::Str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 再插入不会重复列
|
||||||
#[test]
|
#[test]
|
||||||
fn second_insert_does_not_duplicate_columns() {
|
fn second_insert_does_not_duplicate_columns() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -50,6 +57,7 @@ fn second_insert_does_not_duplicate_columns() {
|
|||||||
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
|
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 未知表返回 None
|
||||||
#[test]
|
#[test]
|
||||||
fn describe_unknown_returns_none() {
|
fn describe_unknown_returns_none() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ use chusql_storage::config::{
|
|||||||
};
|
};
|
||||||
use chusql_storage::log::Level;
|
use chusql_storage::log::Level;
|
||||||
|
|
||||||
// 配置测试:没有文件时全默认、文件里写了才覆盖、环境变量优先于文件、
|
// 配置测试:默认值、文件覆盖、环境变量优先、非法值报错。
|
||||||
// 非法值 / 未知键要报错、每个选项的来源都记得下来。
|
|
||||||
|
|
||||||
/// 没有配置文件、没有环境变量:全部走内置默认值。
|
/// 无文件时全默认
|
||||||
#[test]
|
#[test]
|
||||||
fn defaults_without_file() {
|
fn defaults_without_file() {
|
||||||
let loaded = config::resolve(None, None, &|_| None).unwrap();
|
let loaded = config::resolve(None, None, &|_| None).unwrap();
|
||||||
@@ -23,7 +22,7 @@ fn defaults_without_file() {
|
|||||||
assert_eq!(loaded.config.log_level, Level::Info);
|
assert_eq!(loaded.config.log_level, Level::Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 文件里只写了一项:这一项生效,其余回退默认(并且来源记得清清楚楚)。
|
/// 只覆盖文件里写了的项
|
||||||
#[test]
|
#[test]
|
||||||
fn file_overrides_only_written_keys() {
|
fn file_overrides_only_written_keys() {
|
||||||
let path = PathBuf::from("t.toml");
|
let path = PathBuf::from("t.toml");
|
||||||
@@ -41,15 +40,16 @@ fn file_overrides_only_written_keys() {
|
|||||||
assert_eq!(order.2, Origin::Default);
|
assert_eq!(order.2, Origin::Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 五项全写:值都生效,来源都是文件。
|
/// 六项全写都生效
|
||||||
#[test]
|
#[test]
|
||||||
fn file_all_keys() {
|
fn file_all_keys() {
|
||||||
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[storage]\ndata_dir = \"mydata\"\n[server]\npipe_name = \"mypipe\"\n[log]\nlevel = \"debug\"\n";
|
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[buffer]\npool_size = 128\n[storage]\ndata_dir = \"mydata\"\n[server]\npipe_name = \"mypipe\"\n[log]\nlevel = \"debug\"\n";
|
||||||
let path = PathBuf::from("t.toml");
|
let path = PathBuf::from("t.toml");
|
||||||
let loaded = config::resolve(Some(text), Some(path.clone()), &|_| None).unwrap();
|
let loaded = config::resolve(Some(text), Some(path.clone()), &|_| None).unwrap();
|
||||||
|
|
||||||
assert_eq!(loaded.config.page_size, 8192);
|
assert_eq!(loaded.config.page_size, 8192);
|
||||||
assert_eq!(loaded.config.btree_order, 32);
|
assert_eq!(loaded.config.btree_order, 32);
|
||||||
|
assert_eq!(loaded.config.pool_size, 128);
|
||||||
assert_eq!(loaded.config.data_dir, PathBuf::from("mydata"));
|
assert_eq!(loaded.config.data_dir, PathBuf::from("mydata"));
|
||||||
assert_eq!(loaded.config.pipe_name, "mypipe");
|
assert_eq!(loaded.config.pipe_name, "mypipe");
|
||||||
assert_eq!(loaded.config.log_level, Level::Debug);
|
assert_eq!(loaded.config.log_level, Level::Debug);
|
||||||
@@ -61,7 +61,7 @@ fn file_all_keys() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 环境变量优先于文件。
|
/// 环境变量优先于文件
|
||||||
#[test]
|
#[test]
|
||||||
fn env_beats_file() {
|
fn env_beats_file() {
|
||||||
let text = "[storage]\ndata_dir = \"from-file\"\n";
|
let text = "[storage]\ndata_dir = \"from-file\"\n";
|
||||||
@@ -77,14 +77,14 @@ fn env_beats_file() {
|
|||||||
assert_eq!(dir.2, Origin::Env("CHUSQL_DATA_DIR"));
|
assert_eq!(dir.2, Origin::Env("CHUSQL_DATA_DIR"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 日志等级写错要报错,而不是悄悄按默认跑。
|
/// 日志等级非法要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_bad_log_level() {
|
fn rejects_bad_log_level() {
|
||||||
let err = config::resolve(Some("[log]\nlevel = \"loud\"\n"), None, &|_| None).unwrap_err();
|
let err = config::resolve(Some("[log]\nlevel = \"loud\"\n"), None, &|_| None).unwrap_err();
|
||||||
assert!(err.contains("log.level"), "got {}", err);
|
assert!(err.contains("log.level"), "got {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页大小必须是 512..65536 之间的 2 的幂。
|
/// 页大小非法要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_bad_page_size() {
|
fn rejects_bad_page_size() {
|
||||||
let err = config::resolve(Some("[page]\nsize = 3000\n"), None, &|_| None).unwrap_err();
|
let err = config::resolve(Some("[page]\nsize = 3000\n"), None, &|_| None).unwrap_err();
|
||||||
@@ -94,7 +94,7 @@ fn rejects_bad_page_size() {
|
|||||||
assert!(err.contains("page.size"), "got {}", err);
|
assert!(err.contains("page.size"), "got {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// order 太小、或者放不进一页,都要报错。
|
/// order 非法要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_bad_btree_order() {
|
fn rejects_bad_btree_order() {
|
||||||
let err = config::resolve(Some("[btree]\norder = 2\n"), None, &|_| None).unwrap_err();
|
let err = config::resolve(Some("[btree]\norder = 2\n"), None, &|_| None).unwrap_err();
|
||||||
@@ -104,14 +104,14 @@ fn rejects_bad_btree_order() {
|
|||||||
assert!(err.contains("does not fit"), "got {}", err);
|
assert!(err.contains("does not fit"), "got {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 配置里出现认不出来的键要报错(拼错了不能当没看见)。
|
/// 未知配置键要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_unknown_key() {
|
fn rejects_unknown_key() {
|
||||||
let err = config::resolve(Some("[page]\nsizes = 4096\n"), None, &|_| None).unwrap_err();
|
let err = config::resolve(Some("[page]\nsizes = 4096\n"), None, &|_| None).unwrap_err();
|
||||||
assert!(err.contains("bad config"), "got {}", err);
|
assert!(err.contains("bad config"), "got {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 环境变量给了非法值也要报错。
|
/// 环境变量非法要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_bad_env_value() {
|
fn rejects_bad_env_value() {
|
||||||
let env = |k: &str| (k == "CHUSQL_PAGE_SIZE").then(|| "big".to_string());
|
let env = |k: &str| (k == "CHUSQL_PAGE_SIZE").then(|| "big".to_string());
|
||||||
@@ -119,7 +119,7 @@ fn rejects_bad_env_value() {
|
|||||||
assert!(err.contains("CHUSQL_PAGE_SIZE"), "got {}", err);
|
assert!(err.contains("CHUSQL_PAGE_SIZE"), "got {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 来源文案要能说清"哪来的"。
|
/// 来源文案正确
|
||||||
#[test]
|
#[test]
|
||||||
fn origin_description() {
|
fn origin_description() {
|
||||||
assert_eq!(Origin::Default.describe(), "default");
|
assert_eq!(Origin::Default.describe(), "default");
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use chusql_storage::config::DEFAULT_PAGE_SIZE;
|
use chusql_storage::config::DEFAULT_PAGE_SIZE;
|
||||||
use chusql_storage::heap::HeapTable;
|
use chusql_storage::heap::HeapTable;
|
||||||
|
use chusql_storage::page::DEFAULT_POOL_SIZE;
|
||||||
use chusql_storage::protocol::Row;
|
use chusql_storage::protocol::Row;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
// 堆表测试:插入 + 全表扫描、跨页多行、大行、超大行报错、重开文件后数据还在、自定义页大小。
|
// 堆表测试:插查、跨页、大行、重开、索引重建。
|
||||||
|
|
||||||
/// 造一行测试数据(列名 → JSON 值)。
|
|
||||||
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
||||||
let mut m = Row::new();
|
let mut m = Row::new();
|
||||||
for (k, v) in pairs {
|
for (k, v) in pairs {
|
||||||
@@ -14,12 +14,12 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
|||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插一行再扫描,能原样读回来。
|
/// 插一行再扫描读回
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_and_scan_one_row() {
|
fn insert_and_scan_one_row() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("t.db");
|
let path = dir.path().join("t.db");
|
||||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||||
|
|
||||||
t.insert(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
|
t.insert(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -30,12 +30,12 @@ fn insert_and_scan_one_row() {
|
|||||||
assert_eq!(rows[0]["name"], json!("Alice"));
|
assert_eq!(rows[0]["name"], json!("Alice"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插 500 行(必然跨多页),扫描回来的行数和顺序都要对。
|
/// 500 行跨页顺序不变
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_many_rows_cross_pages() {
|
fn insert_many_rows_cross_pages() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("t.db");
|
let path = dir.path().join("t.db");
|
||||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||||
|
|
||||||
for i in 0..500 {
|
for i in 0..500 {
|
||||||
t.insert(&row(&[("id", json!(i))])).unwrap();
|
t.insert(&row(&[("id", json!(i))])).unwrap();
|
||||||
@@ -43,18 +43,17 @@ fn insert_many_rows_cross_pages() {
|
|||||||
|
|
||||||
let rows = t.scan().unwrap();
|
let rows = t.scan().unwrap();
|
||||||
assert_eq!(rows.len(), 500);
|
assert_eq!(rows.len(), 500);
|
||||||
// 顺序保持
|
|
||||||
for (i, r) in rows.iter().enumerate() {
|
for (i, r) in rows.iter().enumerate() {
|
||||||
assert_eq!(r["id"], json!(i as i64));
|
assert_eq!(r["id"], json!(i as i64));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 约 2KB 的大行也要能放进一页。
|
/// 2KB 大行放得下
|
||||||
#[test]
|
#[test]
|
||||||
fn large_row_still_fits() {
|
fn large_row_still_fits() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("t.db");
|
let path = dir.path().join("t.db");
|
||||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||||
|
|
||||||
let big = "x".repeat(2000);
|
let big = "x".repeat(2000);
|
||||||
t.insert(&row(&[("data", json!(big.clone()))])).unwrap();
|
t.insert(&row(&[("data", json!(big.clone()))])).unwrap();
|
||||||
@@ -64,43 +63,43 @@ fn large_row_still_fits() {
|
|||||||
assert_eq!(rows[0]["data"], json!(big));
|
assert_eq!(rows[0]["data"], json!(big));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 超过一页容量的行要报错,而不是悄悄写坏。
|
/// 超页的行要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn row_too_large_errors() {
|
fn row_too_large_errors() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("t.db");
|
let path = dir.path().join("t.db");
|
||||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||||
|
|
||||||
let huge = "x".repeat(10_000);
|
let huge = "x".repeat(10_000);
|
||||||
let err = t.insert(&row(&[("data", json!(huge))]));
|
let err = t.insert(&row(&[("data", json!(huge))]));
|
||||||
assert!(err.is_err());
|
assert!(err.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 关掉再重新打开同一个文件,之前插入的数据还在。
|
/// 重开文件数据还在
|
||||||
#[test]
|
#[test]
|
||||||
fn reopen_and_scan() {
|
fn reopen_and_scan() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("t.db");
|
let path = dir.path().join("t.db");
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||||
t.insert(&row(&[("id", json!(1))])).unwrap();
|
t.insert(&row(&[("id", json!(1))])).unwrap();
|
||||||
t.insert(&row(&[("id", json!(2))])).unwrap();
|
t.insert(&row(&[("id", json!(2))])).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
|
||||||
let rows = t.scan().unwrap();
|
let rows = t.scan().unwrap();
|
||||||
assert_eq!(rows.len(), 2);
|
assert_eq!(rows.len(), 2);
|
||||||
assert_eq!(rows[0]["id"], json!(1));
|
assert_eq!(rows[0]["id"], json!(1));
|
||||||
assert_eq!(rows[1]["id"], json!(2));
|
assert_eq!(rows[1]["id"], json!(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页大小可以配置:512 字节的页装得下小行、放不下大行,行数不变。
|
/// 512 字节页可用
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_page_size() {
|
fn custom_page_size() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("small.db");
|
let path = dir.path().join("small.db");
|
||||||
let mut t = HeapTable::open(&path, 512).unwrap();
|
let mut t = HeapTable::open(&path, 512, DEFAULT_POOL_SIZE).unwrap();
|
||||||
|
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
t.insert(&row(&[("id", json!(i))])).unwrap();
|
t.insert(&row(&[("id", json!(i))])).unwrap();
|
||||||
@@ -111,7 +110,7 @@ fn custom_page_size() {
|
|||||||
assert!(t.insert(&row(&[("data", json!(big))])).is_err());
|
assert!(t.insert(&row(&[("data", json!(big))])).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 整表改写之后索引必须跟着重建:旧键查不到,新行按 id 能查到。
|
/// 整表改写后索引重建
|
||||||
#[test]
|
#[test]
|
||||||
fn replace_all_rebuilds_index() {
|
fn replace_all_rebuilds_index() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -120,6 +119,7 @@ fn replace_all_rebuilds_index() {
|
|||||||
dir.path().join("t.idx"),
|
dir.path().join("t.idx"),
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
chusql_storage::config::DEFAULT_BTREE_ORDER,
|
chusql_storage::config::DEFAULT_BTREE_ORDER,
|
||||||
|
DEFAULT_POOL_SIZE,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use chusql_storage::config::DEFAULT_PAGE_SIZE;
|
use chusql_storage::config::DEFAULT_PAGE_SIZE;
|
||||||
use chusql_storage::page::{Page, PageFile};
|
use chusql_storage::page::{Page, PageFile};
|
||||||
|
|
||||||
// 页文件测试:一页的读写、页号越界报错、追加页让文件变长、多页互不干扰、覆写同一页、
|
// 页文件测试:读写、追加、覆写、页大小校验与页缓存。
|
||||||
// 自定义页大小、页大小和文件对不上时报错。
|
|
||||||
|
|
||||||
/// 写入一页再读回来,内容和长度都应该一致。
|
/// 写一页再读回一致
|
||||||
#[test]
|
#[test]
|
||||||
fn write_then_read_page() {
|
fn write_then_read_page() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -23,7 +22,7 @@ fn write_then_read_page() {
|
|||||||
assert_eq!(read.data.len(), DEFAULT_PAGE_SIZE);
|
assert_eq!(read.data.len(), DEFAULT_PAGE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 空文件读第 0 页应该报错。
|
/// 空文件读第 0 页报错
|
||||||
#[test]
|
#[test]
|
||||||
fn read_out_of_range_returns_error() {
|
fn read_out_of_range_returns_error() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -33,7 +32,7 @@ fn read_out_of_range_returns_error() {
|
|||||||
assert!(pf.read_page(0).is_err());
|
assert!(pf.read_page(0).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 每 append 一页,num_pages 就加一。
|
/// 追加让页数加一
|
||||||
#[test]
|
#[test]
|
||||||
fn append_grows_file() {
|
fn append_grows_file() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -50,7 +49,7 @@ fn append_grows_file() {
|
|||||||
assert_eq!(pf.num_pages().unwrap(), 3);
|
assert_eq!(pf.num_pages().unwrap(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页 0 和页 1 的内容互不影响。
|
/// 两页内容互不影响
|
||||||
#[test]
|
#[test]
|
||||||
fn two_pages_independent() {
|
fn two_pages_independent() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -69,7 +68,7 @@ fn two_pages_independent() {
|
|||||||
assert_eq!(pf.read_page(1).unwrap().data[0], 2);
|
assert_eq!(pf.read_page(1).unwrap().data[0], 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 覆写同一页:读到的是新内容,页数不变。
|
/// 覆写同页读到新内容
|
||||||
#[test]
|
#[test]
|
||||||
fn overwrite_page() {
|
fn overwrite_page() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -89,25 +88,20 @@ fn overwrite_page() {
|
|||||||
assert_eq!(pf.num_pages().unwrap(), 1);
|
assert_eq!(pf.num_pages().unwrap(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页大小可以配置:512 字节的页照样读写,文件按 512 字节对齐。
|
/// 512 字节页可用
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_page_size() {
|
fn custom_page_size() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let path = dir.path().join("small.db");
|
let path = dir.path().join("t.db");
|
||||||
|
|
||||||
let mut pf = PageFile::open(&path, 512).unwrap();
|
let mut pf = PageFile::open(&path, 512).unwrap();
|
||||||
assert_eq!(pf.page_size(), 512);
|
|
||||||
|
|
||||||
let mut p = Page::new(0, 512);
|
pf.write_page(&Page::new(0, 512)).unwrap();
|
||||||
p.data[511] = 9;
|
pf.flush().unwrap();
|
||||||
pf.write_page(&p).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(pf.read_page(0).unwrap().data[511], 9);
|
|
||||||
assert_eq!(pf.num_pages().unwrap(), 1);
|
|
||||||
assert_eq!(std::fs::metadata(&path).unwrap().len(), 512);
|
assert_eq!(std::fs::metadata(&path).unwrap().len(), 512);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 文件长度不是页大小整数倍(比如页大小被改过)要报错,而不是算出错误的页数。
|
/// 长度不是整数倍要报错
|
||||||
#[test]
|
#[test]
|
||||||
fn mismatched_file_length_errors() {
|
fn mismatched_file_length_errors() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -118,7 +112,7 @@ fn mismatched_file_length_errors() {
|
|||||||
assert!(pf.num_pages().is_err());
|
assert!(pf.num_pages().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 页大小对不上的页不许写进去。
|
/// 页大小不符拒绝写入
|
||||||
#[test]
|
#[test]
|
||||||
fn write_wrong_sized_page_errors() {
|
fn write_wrong_sized_page_errors() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -127,3 +121,43 @@ fn write_wrong_sized_page_errors() {
|
|||||||
let mut pf = PageFile::open(&path, 512).unwrap();
|
let mut pf = PageFile::open(&path, 512).unwrap();
|
||||||
assert!(pf.write_page(&Page::new(0, 1024)).is_err());
|
assert!(pf.write_page(&Page::new(0, 1024)).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 第二次读命中缓存
|
||||||
|
#[test]
|
||||||
|
fn cache_hit_avoids_second_read() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("t.db");
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pf = PageFile::open(&path, 4096).unwrap();
|
||||||
|
pf.write_page(&Page::new(0, 4096)).unwrap();
|
||||||
|
pf.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pf = PageFile::open(&path, 4096).unwrap();
|
||||||
|
let _ = pf.read_page(0).unwrap();
|
||||||
|
let _ = pf.read_page(0).unwrap();
|
||||||
|
let _ = pf.read_page(0).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(pf.hits(), 2);
|
||||||
|
assert_eq!(pf.misses(), 1);
|
||||||
|
assert!(pf.hit_rate() > 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 退出时脏页写回
|
||||||
|
#[test]
|
||||||
|
fn dirty_page_flushed_on_drop() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("t.db");
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||||
|
let mut p = Page::new(0, DEFAULT_PAGE_SIZE);
|
||||||
|
p.data[0] = 42;
|
||||||
|
pf.write_page(&p).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pf2 = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
||||||
|
let p = pf2.read_page(0).unwrap();
|
||||||
|
assert_eq!(p.data[0], 42);
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@ use interprocess::local_socket::{
|
|||||||
};
|
};
|
||||||
use interprocess::TryClone;
|
use interprocess::TryClone;
|
||||||
|
|
||||||
// 命名管道集成测试:起 server 子进程,用唯一管道名,测试之间互不干扰。
|
// 命名管道集成测试:起真 server,跑协议、索引与配置。
|
||||||
|
|
||||||
/// 生成唯一管道名,避免测试之间抢占。
|
/// 唯一管道名,避免测试互抢
|
||||||
fn unique_pipe_name() -> String {
|
fn unique_pipe_name() -> String {
|
||||||
let id = std::process::id();
|
let id = std::process::id();
|
||||||
let ns = std::time::SystemTime::now()
|
let ns = std::time::SystemTime::now()
|
||||||
@@ -21,18 +21,18 @@ fn unique_pipe_name() -> String {
|
|||||||
format!("chusql-test-{}-{}", id, ns)
|
format!("chusql-test-{}-{}", id, ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 持有 server 子进程,Drop 时杀掉。
|
/// 持有 server 子进程,退出时杀掉
|
||||||
struct ServerProc(Child);
|
struct ServerProc(Child);
|
||||||
|
|
||||||
impl Drop for ServerProc {
|
impl Drop for ServerProc {
|
||||||
/// 测试结束时杀掉子进程并回收,避免留下孤儿进程。
|
/// 测试结束杀掉子进程
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = self.0.kill();
|
let _ = self.0.kill();
|
||||||
let _ = self.0.wait();
|
let _ = self.0.wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启动 server 子进程,并等管道可连(最多 5 秒)。
|
/// 起 server,等管道可连
|
||||||
fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
|
fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
|
||||||
let data = tempfile::tempdir().unwrap();
|
let data = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
|
|||||||
panic!("server did not start within 5s");
|
panic!("server did not start within 5s");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 连到指定管道。
|
/// 连到指定管道
|
||||||
fn connect(pipe: &str) -> std::io::Result<LocalSocketStream> {
|
fn connect(pipe: &str) -> std::io::Result<LocalSocketStream> {
|
||||||
let name = pipe
|
let name = pipe
|
||||||
.to_ns_name::<GenericNamespaced>()
|
.to_ns_name::<GenericNamespaced>()
|
||||||
@@ -65,7 +65,7 @@ fn connect(pipe: &str) -> std::io::Result<LocalSocketStream> {
|
|||||||
LocalSocketStream::connect(name)
|
LocalSocketStream::connect(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发一行请求,读一行响应。
|
/// 发一行请求读一行响应
|
||||||
fn send(stream: &mut LocalSocketStream, line: &str) -> String {
|
fn send(stream: &mut LocalSocketStream, line: &str) -> String {
|
||||||
stream.write_all(line.as_bytes()).unwrap();
|
stream.write_all(line.as_bytes()).unwrap();
|
||||||
stream.write_all(b"\n").unwrap();
|
stream.write_all(b"\n").unwrap();
|
||||||
@@ -77,7 +77,7 @@ fn send(stream: &mut LocalSocketStream, line: &str) -> String {
|
|||||||
buf.trim().to_string()
|
buf.trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ping 应该回 pong。
|
/// ping 回 pong
|
||||||
#[test]
|
#[test]
|
||||||
fn ping_pong() {
|
fn ping_pong() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
@@ -87,7 +87,7 @@ fn ping_pong() {
|
|||||||
assert_eq!(send(&mut c, r#"{"method":"ping"}"#), r#"{"status":"pong"}"#);
|
assert_eq!(send(&mut c, r#"{"method":"ping"}"#), r#"{"status":"pong"}"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 插一行再 scan,能扫到刚插进去的数据。
|
/// 插入后能扫到
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_then_scan() {
|
fn insert_then_scan() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
@@ -104,6 +104,7 @@ fn insert_then_scan() {
|
|||||||
assert!(r2.contains("Alice"), "scan response: {}", r2);
|
assert!(r2.contains("Alice"), "scan response: {}", r2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 不存在的表报错
|
||||||
#[test]
|
#[test]
|
||||||
fn scan_unknown_table_errors() {
|
fn scan_unknown_table_errors() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
@@ -115,6 +116,7 @@ fn scan_unknown_table_errors() {
|
|||||||
assert!(r.contains("unknown table"), "got {}", r);
|
assert!(r.contains("unknown table"), "got {}", r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 列出已建的表
|
||||||
#[test]
|
#[test]
|
||||||
fn list_tables_returns_created_tables() {
|
fn list_tables_returns_created_tables() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
@@ -129,6 +131,7 @@ fn list_tables_returns_created_tables() {
|
|||||||
assert!(r.contains("orders"), "got {}", r);
|
assert!(r.contains("orders"), "got {}", r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 整表替换只剩新行
|
||||||
#[test]
|
#[test]
|
||||||
fn replace_all_replaces_rows() {
|
fn replace_all_replaces_rows() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
@@ -149,13 +152,13 @@ fn replace_all_replaces_rows() {
|
|||||||
assert!(!r.contains("Alice"), "old row should be gone: {}", r);
|
assert!(!r.contains("Alice"), "old row should be gone: {}", r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 带 key 插入后能按下标查
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_with_key_then_lookup() {
|
fn insert_with_key_then_lookup() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
let (_srv, _data) = start_server(&pipe);
|
let (_srv, _data) = start_server(&pipe);
|
||||||
let mut c = connect(&pipe).unwrap();
|
let mut c = connect(&pipe).unwrap();
|
||||||
|
|
||||||
// 带索引键插三行
|
|
||||||
for (id, name) in [(1, "Alice"), (2, "Bob"), (3, "Carol")] {
|
for (id, name) in [(1, "Alice"), (2, "Bob"), (3, "Carol")] {
|
||||||
let req = format!(
|
let req = format!(
|
||||||
r#"{{"method":"insert","table":"idx_users","row":{{"id":{},"name":"{}"}},"key":{}}}"#,
|
r#"{{"method":"insert","table":"idx_users","row":{{"id":{},"name":"{}"}},"key":{}}}"#,
|
||||||
@@ -165,15 +168,14 @@ fn insert_with_key_then_lookup() {
|
|||||||
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
|
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 命中
|
|
||||||
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":2}"#);
|
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":2}"#);
|
||||||
assert!(r.contains("Bob"), "lookup: {}", r);
|
assert!(r.contains("Bob"), "lookup: {}", r);
|
||||||
|
|
||||||
// 未命中
|
|
||||||
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":99}"#);
|
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":99}"#);
|
||||||
assert!(r.contains(r#""rows":[]"#), "miss: {}", r);
|
assert!(r.contains(r#""rows":[]"#), "miss: {}", r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 不带 key 不建索引
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_without_key_does_not_index() {
|
fn insert_without_key_does_not_index() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
@@ -185,12 +187,12 @@ fn insert_without_key_does_not_index() {
|
|||||||
assert!(r.contains(r#""rows":[]"#), "got: {}", r);
|
assert!(r.contains(r#""rows":[]"#), "got: {}", r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 重开服务后索引仍在
|
||||||
#[test]
|
#[test]
|
||||||
fn lookup_survives_reopen() {
|
fn lookup_survives_reopen() {
|
||||||
let pipe = unique_pipe_name();
|
let pipe = unique_pipe_name();
|
||||||
let data = tempfile::tempdir().unwrap();
|
let data = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
// 第一次:写 3 行带 key
|
|
||||||
{
|
{
|
||||||
let exe = env!("CARGO_BIN_EXE_server");
|
let exe = env!("CARGO_BIN_EXE_server");
|
||||||
let child = Command::new(exe)
|
let child = Command::new(exe)
|
||||||
@@ -213,13 +215,11 @@ fn lookup_survives_reopen() {
|
|||||||
);
|
);
|
||||||
send(&mut c, &req);
|
send(&mut c, &req);
|
||||||
}
|
}
|
||||||
drop(srv); // 杀 server
|
drop(srv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等一下,让文件句柄真正释放
|
|
||||||
thread::sleep(Duration::from_millis(200));
|
thread::sleep(Duration::from_millis(200));
|
||||||
|
|
||||||
// 第二次:重开 server,从索引查到
|
|
||||||
let (_srv, _data) = {
|
let (_srv, _data) = {
|
||||||
let child = Command::new(env!("CARGO_BIN_EXE_server"))
|
let child = Command::new(env!("CARGO_BIN_EXE_server"))
|
||||||
.env("CHUSQL_PIPE", &pipe)
|
.env("CHUSQL_PIPE", &pipe)
|
||||||
@@ -241,7 +241,7 @@ fn lookup_survives_reopen() {
|
|||||||
assert!(r.contains("Bob"), "persist lookup: {}", r);
|
assert!(r.contains("Bob"), "persist lookup: {}", r);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 配置文件里的管道名和数据目录要真的生效(这里不设 CHUSQL_PIPE / CHUSQL_DATA_DIR)。
|
/// 配置文件真的生效
|
||||||
#[test]
|
#[test]
|
||||||
fn config_file_is_used() {
|
fn config_file_is_used() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -249,7 +249,6 @@ fn config_file_is_used() {
|
|||||||
let data_dir = dir.path().join("data");
|
let data_dir = dir.path().join("data");
|
||||||
let config_path = dir.path().join("chusql-storage.toml");
|
let config_path = dir.path().join("chusql-storage.toml");
|
||||||
|
|
||||||
// TOML 里写正斜杠,省得转义 Windows 路径
|
|
||||||
let text = format!(
|
let text = format!(
|
||||||
"[server]\npipe_name = \"{}\"\n[storage]\ndata_dir = \"{}\"\n[log]\nlevel = \"debug\"\n",
|
"[server]\npipe_name = \"{}\"\n[storage]\ndata_dir = \"{}\"\n[log]\nlevel = \"debug\"\n",
|
||||||
pipe,
|
pipe,
|
||||||
@@ -276,9 +275,62 @@ fn config_file_is_used() {
|
|||||||
let r = send(&mut c, r#"{"method":"insert","table":"cfg","row":{"id":1}}"#);
|
let r = send(&mut c, r#"{"method":"insert","table":"cfg","row":{"id":1}}"#);
|
||||||
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
|
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
|
||||||
|
|
||||||
// 数据真的落在配置文件指的那个目录里
|
|
||||||
assert!(
|
assert!(
|
||||||
data_dir.join("cfg.db").exists(),
|
data_dir.join("cfg.db").exists(),
|
||||||
"data dir from config file was not used"
|
"data dir from config file was not used"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 建表后能查 schema
|
||||||
|
#[test]
|
||||||
|
fn create_table_then_describe() {
|
||||||
|
let pipe = unique_pipe_name();
|
||||||
|
let (_srv, _data) = start_server(&pipe);
|
||||||
|
let mut c = connect(&pipe).unwrap();
|
||||||
|
|
||||||
|
let r = send(
|
||||||
|
&mut c,
|
||||||
|
r#"{"method":"create_table","table":"ct_users","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}"#,
|
||||||
|
);
|
||||||
|
assert!(r.contains(r#""status":"ok""#), "create: {}", r);
|
||||||
|
|
||||||
|
let r = send(&mut c, r#"{"method":"describe_table","table":"ct_users"}"#);
|
||||||
|
assert!(r.contains("id"), "describe: {}", r);
|
||||||
|
assert!(r.contains("name"), "describe: {}", r);
|
||||||
|
assert!(r.contains(r#""ty":"int""#), "describe: {}", r);
|
||||||
|
assert!(r.contains(r#""ty":"str""#), "describe: {}", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重复建表报错
|
||||||
|
#[test]
|
||||||
|
fn create_table_twice_errors() {
|
||||||
|
let pipe = unique_pipe_name();
|
||||||
|
let (_srv, _data) = start_server(&pipe);
|
||||||
|
let mut c = connect(&pipe).unwrap();
|
||||||
|
|
||||||
|
let req = r#"{"method":"create_table","table":"dup","columns":[{"name":"id","ty":"int"}]}"#;
|
||||||
|
let r1 = send(&mut c, req);
|
||||||
|
assert!(r1.contains(r#""status":"ok""#), "first: {}", r1);
|
||||||
|
|
||||||
|
let r2 = send(&mut c, req);
|
||||||
|
assert!(r2.contains(r#""status":"error""#), "second: {}", r2);
|
||||||
|
assert!(r2.contains("already exists"), "second: {}", r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 新建空表能扫描
|
||||||
|
#[test]
|
||||||
|
fn scan_empty_table_after_create() {
|
||||||
|
let pipe = unique_pipe_name();
|
||||||
|
let (_srv, _data) = start_server(&pipe);
|
||||||
|
let mut c = connect(&pipe).unwrap();
|
||||||
|
|
||||||
|
let r = send(
|
||||||
|
&mut c,
|
||||||
|
r#"{"method":"create_table","table":"empty_t","columns":[{"name":"id","ty":"int"}]}"#,
|
||||||
|
);
|
||||||
|
assert!(r.contains(r#""status":"ok""#), "create: {}", r);
|
||||||
|
|
||||||
|
let r = send(&mut c, r#"{"method":"scan","table":"empty_t"}"#);
|
||||||
|
assert!(r.contains(r#""status":"rows""#), "scan: {}", r);
|
||||||
|
assert!(r.contains(r#""rows":[]"#), "scan: {}", r);
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@ use chusql_storage::protocol::Row;
|
|||||||
use chusql_storage::wal::{Wal, WalOp};
|
use chusql_storage::wal::{Wal, WalOp};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
// WAL 测试:追加、读取、清空、截断尾巴。
|
||||||
|
|
||||||
|
|
||||||
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
||||||
let mut m = Row::new();
|
let mut m = Row::new();
|
||||||
for (k, v) in pairs {
|
for (k, v) in pairs {
|
||||||
@@ -10,6 +13,7 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
|
|||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 写读 Insert 操作
|
||||||
#[test]
|
#[test]
|
||||||
fn append_and_read_insert() {
|
fn append_and_read_insert() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -26,6 +30,7 @@ fn append_and_read_insert() {
|
|||||||
assert_eq!(back, op);
|
assert_eq!(back, op);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 写读 ReplaceAll 操作
|
||||||
#[test]
|
#[test]
|
||||||
fn append_and_read_replace_all() {
|
fn append_and_read_replace_all() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -44,6 +49,7 @@ fn append_and_read_replace_all() {
|
|||||||
assert_eq!(back, op);
|
assert_eq!(back, op);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 文件不存在返回 None
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_file_returns_none() {
|
fn missing_file_returns_none() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -51,6 +57,7 @@ fn missing_file_returns_none() {
|
|||||||
assert!(wal.read().unwrap().is_none());
|
assert!(wal.read().unwrap().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 空文件返回 None
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_file_returns_none() {
|
fn empty_file_returns_none() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -60,6 +67,7 @@ fn empty_file_returns_none() {
|
|||||||
assert!(wal.read().unwrap().is_none());
|
assert!(wal.read().unwrap().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 清空后读不到
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_truncates() {
|
fn clear_truncates() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -79,6 +87,7 @@ fn clear_truncates() {
|
|||||||
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
|
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 尾巴截断被忽略
|
||||||
#[test]
|
#[test]
|
||||||
fn truncated_tail_is_ignored() {
|
fn truncated_tail_is_ignored() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -92,19 +101,15 @@ fn truncated_tail_is_ignored() {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// 砍掉最后 5 个字节,模拟"写一半断电"
|
|
||||||
let full = std::fs::read(&path).unwrap();
|
let full = std::fs::read(&path).unwrap();
|
||||||
std::fs::write(&path, &full[..full.len() - 5]).unwrap();
|
std::fs::write(&path, &full[..full.len() - 5]).unwrap();
|
||||||
|
|
||||||
// 读回来应该当作"没有这条"
|
|
||||||
assert!(wal.read().unwrap().is_none());
|
assert!(wal.read().unwrap().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 第二次追加会覆盖
|
||||||
#[test]
|
#[test]
|
||||||
fn overwrite_on_second_append_is_visible() {
|
fn overwrite_on_second_append_is_visible() {
|
||||||
// 简化版一次只允许一条,append 两次会叠在文件里——
|
|
||||||
// 但正常路径下 clear 先跑,所以实际不会叠加。
|
|
||||||
// 这个测试只验证 read 能解析最前面那一条。
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let wal = Wal::new(dir.path().join("wal.log"));
|
let wal = Wal::new(dir.path().join("wal.log"));
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user