做完rust全链路

This commit is contained in:
LIm1tZz7 committed 2026-09-24 21:28:49 +08:00
1 parent e428265357
commit 2a4b446252
34 files changed
+1608 -883

No files matched your search

+17 -5
View File
@@ -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
+11 -9
View File
@@ -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))
+11 -9
View File
@@ -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
+11 -8
View File
@@ -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))
+4 -4
View File
@@ -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
+28 -29
View File
@@ -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)
+4 -4
View File
@@ -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
+5 -5
View File
@@ -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
+31 -23
View File
@@ -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
+12 -10
View File
@@ -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 = "*"
+30 -20
View File
@@ -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 ()
+18 -8
View File
@@ -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
+112 -52
View File
@@ -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)
+29 -13
View File
@@ -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))
+17 -9
View File
@@ -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
+109 -52
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
} }
+77 -11
View File
@@ -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),
+58 -21
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
}
+16 -7
View File
@@ -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
View File
@@ -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],
]); ]);
+26 -21
View File
@@ -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());
} }
+8
View File
@@ -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();
+13 -13
View File
@@ -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");
+17 -17
View File
@@ -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();
+52 -18
View File
@@ -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);
}
+71 -19
View File
@@ -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);
}
+10 -5
View File
@@ -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"));