rust部分推进:进程间通信(Windows平台命名管线)、内存页表、B+树、WAL恢复系统、动态配置
This commit is contained in:
1 parent
91ce26d7f0
commit
e428265357
41 files changed
+4546
-159
No files matched your search
@@ -18,11 +18,18 @@ library
|
||||
ChuSQL.Engine
|
||||
ChuSQL.Model
|
||||
ChuSQL.Semantic
|
||||
ChuSQL.Storage
|
||||
ChuSQL.Storage.IPC
|
||||
ChuSQL.Storage.Memory
|
||||
ChuSQL.Syntax.AST
|
||||
ChuSQL.Syntax.Parser
|
||||
build-depends: base >= 4.14 && < 5
|
||||
, aeson
|
||||
, bytestring
|
||||
, megaparsec
|
||||
, parser-combinators
|
||||
, scientific
|
||||
, text
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
||||
|
||||
@@ -40,6 +47,9 @@ test-suite chusql-engine-test
|
||||
hs-source-dirs: test
|
||||
build-depends: base
|
||||
, chusql-engine
|
||||
, directory
|
||||
, filepath
|
||||
, hspec
|
||||
, process
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
||||
@@ -1,25 +1,41 @@
|
||||
module ChuSQL.Algebra.Eval (evalRelOp) where
|
||||
module ChuSQL.Algebra.Eval (evalRelOp, evalRelOpM) where
|
||||
|
||||
import ChuSQL.Algebra.Expr (evalCondForRow)
|
||||
import ChuSQL.Algebra.Op
|
||||
import ChuSQL.Algebra.Sort (sortRows)
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage (MonadStorage (..))
|
||||
import ChuSQL.Syntax.AST (Expr (..))
|
||||
import Control.Monad (filterM)
|
||||
|
||||
-- 投影
|
||||
-- 执行:按关系算子树真正算出结果行(Scan 阶段会给列名加上别名前缀)。
|
||||
|
||||
-- 投影:只保留清单里的列;哨兵 "*" 表示全部列。
|
||||
project :: [String] -> Row -> Row
|
||||
project ["*"] row = row
|
||||
project cs row = [(c, v) | (c, v) <- row, c `elem` cs]
|
||||
|
||||
-- 执行关系代数运算
|
||||
-- 给一行里的每个列名加上别名前缀(如 "u.")。
|
||||
addPrefix :: String -> Row -> Row
|
||||
addPrefix prefix = map (\(k, v) -> (prefix ++ k, v))
|
||||
|
||||
evalRelOp :: Database -> RelOp -> Either String [Row]
|
||||
evalRelOp db (Scan mAlias tbl) = do
|
||||
-- 索引查找的约定:主键列名是 "id"(优化器只在这种情况下才把过滤改写成 Lookup)。
|
||||
keyCondition :: Int -> Expr
|
||||
keyCondition k = Eq (Col "id") (LitInt k)
|
||||
|
||||
-- 扫描一张表;有别名就给每个列名加上前缀,JOIN 两边同名列靠它区分。
|
||||
evalScan :: Database -> Maybe String -> String -> Either String [Row]
|
||||
evalScan db mAlias tbl = do
|
||||
table <- lookupTable db tbl
|
||||
let prefix = maybe "" (++ ".") mAlias
|
||||
Right (map (addPrefix prefix) (tableRows table))
|
||||
|
||||
-- 执行关系代数运算:Scan / Lookup / Filter / Project / Sort / Limit / Join 各一支。
|
||||
evalRelOp :: Database -> RelOp -> Either String [Row]
|
||||
evalRelOp db (Scan mAlias tbl) = evalScan db mAlias tbl
|
||||
evalRelOp db (Lookup tbl k) = do
|
||||
rows <- evalScan db Nothing tbl
|
||||
filterM (evalCondForRow (keyCondition k)) rows
|
||||
evalRelOp db (Filter e op) = do
|
||||
rows <- evalRelOp db op
|
||||
filterM (evalCondForRow e) rows
|
||||
@@ -37,3 +53,35 @@ evalRelOp db (Join left right cond) = do
|
||||
rrows <- evalRelOp db right
|
||||
let cross = [l ++ r | l <- lrows, r <- rrows] -- 笛卡尔积
|
||||
filterM (evalCondForRow cond) cross
|
||||
|
||||
-- | Monadic 版本:Lookup 直接问存储要一行(走索引),其余算子照原样递归。
|
||||
evalRelOpM :: (MonadStorage m) => Database -> RelOp -> m (Either String [Row])
|
||||
evalRelOpM _ (Lookup t k) = do
|
||||
result <- lookupByKey t k
|
||||
pure $ case result of
|
||||
Left e -> Left e
|
||||
Right Nothing -> Right []
|
||||
Right (Just r) -> Right [r]
|
||||
evalRelOpM db (Scan a t) = pure (evalScan db a t)
|
||||
evalRelOpM db (Filter p x) = do
|
||||
result <- evalRelOpM db x
|
||||
pure $ do
|
||||
rows <- result
|
||||
filterM (evalCondForRow p) rows
|
||||
evalRelOpM db (Project cols x) = do
|
||||
result <- evalRelOpM db x
|
||||
pure (map (project cols) <$> result)
|
||||
evalRelOpM db (Sort specs x) = do
|
||||
result <- evalRelOpM db x
|
||||
pure (sortRows specs <$> result)
|
||||
evalRelOpM db (Limit n x) = do
|
||||
result <- evalRelOpM db x
|
||||
pure (take n <$> result)
|
||||
evalRelOpM db (Join l r c) = do
|
||||
ls <- evalRelOpM db l
|
||||
rs <- evalRelOpM db r
|
||||
pure $ do
|
||||
leftRows <- ls
|
||||
rightRows <- rs
|
||||
let cross = [x ++ y | x <- leftRows, y <- rightRows]
|
||||
filterM (evalCondForRow c) cross
|
||||
@@ -4,6 +4,8 @@ import ChuSQL.Model
|
||||
import ChuSQL.Syntax.AST
|
||||
import Control.Monad (join)
|
||||
|
||||
-- 表达式求值:在一行(列名 → 值)上算出单个值;另外提供"静态收集表达式里用到哪些列"的工具。
|
||||
|
||||
-- 表达式求值
|
||||
evalExpr :: Expr -> Row -> Either String Value
|
||||
evalExpr (Col name) row =
|
||||
@@ -17,7 +19,7 @@ evalExpr (Eq a b) row = liftBinOp eqOp a b row
|
||||
evalExpr (And a b) row = liftBinOp (boolOp (&&)) a b row
|
||||
evalExpr (Or a b) row = liftBinOp (boolOp (||)) a b row
|
||||
|
||||
-- | 收集表达式里用到的列名
|
||||
-- 收集表达式里用到的列名
|
||||
colsInExpr :: Expr -> [String]
|
||||
colsInExpr (Col c) = [c]
|
||||
colsInExpr (Gt a b) = colsInExpr a ++ colsInExpr b
|
||||
|
||||
@@ -2,9 +2,12 @@ module ChuSQL.Algebra.Op (RelOp (..), renderPlan) where
|
||||
|
||||
import ChuSQL.Syntax.AST (Expr, SortDir)
|
||||
|
||||
-- 关系代数算子:执行计划用 RelOp 表示;另外提供一个把计划打印成缩进文本的辅助函数。
|
||||
|
||||
-- 关系代数算子
|
||||
data RelOp
|
||||
= Scan (Maybe String) String
|
||||
| Lookup String Int
|
||||
| Filter Expr RelOp
|
||||
| Project [String] RelOp
|
||||
| Sort [(String, SortDir)] RelOp
|
||||
@@ -12,11 +15,12 @@ data RelOp
|
||||
| Join RelOp RelOp Expr
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 把算子树画成缩进文本(演示和调试用)
|
||||
-- 把算子树画成缩进文本(演示和调试用)
|
||||
renderPlan :: RelOp -> String
|
||||
renderPlan = go 0
|
||||
where
|
||||
go ind op = case op of
|
||||
Lookup t k -> "Lookup " ++ show t ++ " " ++ show k
|
||||
Scan a t -> pad ++ "Scan " ++ show a ++ " " ++ show t
|
||||
Filter e x -> pad ++ "Filter " ++ show e ++ "\n" ++ go (ind + 1) x
|
||||
Project c x -> pad ++ "Project " ++ show c ++ "\n" ++ go (ind + 1) x
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
module ChuSQL.Algebra.Optimize (optimize, pushProject) where
|
||||
|
||||
import ChuSQL.Algebra.Expr (colsInExpr, evalExpr)
|
||||
import ChuSQL.Algebra.Op (RelOp (..))
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Syntax.AST (Expr (..))
|
||||
import ChuSQL.Algebra.Expr (colsInExpr, evalExpr)
|
||||
|
||||
-- 查询优化:谓词下推 + 投影下推 + 常量折叠,反复应用规则直到算子树形状不再变化。
|
||||
|
||||
-- * 谓词下推
|
||||
|
||||
@@ -15,6 +17,7 @@ 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) =
|
||||
case lookup tbl db of
|
||||
Nothing -> []
|
||||
@@ -73,10 +76,18 @@ pushOne _ op = op
|
||||
|
||||
-- * 投影下推
|
||||
|
||||
-- | 工具函数
|
||||
-- | 是不是要拿全部列(need 里含哨兵 "*")。
|
||||
needsAll :: [String] -> Bool
|
||||
needsAll = elem allColumns
|
||||
|
||||
-- | 叶子算子(扫描 / 索引查找)的投影下推:需要全部列就原样保留,缺列才套一层 Project
|
||||
pushProjectLeaf :: Database -> [String] -> RelOp -> RelOp
|
||||
pushProjectLeaf db need leaf
|
||||
| needsAll need = leaf
|
||||
| all (`elem` need) (relOpCols db leaf) = leaf
|
||||
| otherwise = Project need leaf
|
||||
|
||||
-- | 投影下推:把"上层需要哪些列"一路往下传,JOIN 两侧各自只保留自己需要的列。
|
||||
pushProject :: Database -> [String] -> RelOp -> RelOp
|
||||
pushProject db _ (Project cols x) = case pushProject db cols x of
|
||||
Project cols' y | cols' == cols -> Project cols y
|
||||
@@ -93,10 +104,8 @@ pushProject db need (Join l r c)
|
||||
lNeed = if needsAll lCols then ["*"] else [x | x <- tot, x `elem` lCols]
|
||||
rNeed = if needsAll rCols then ["*"] else [x | x <- tot, x `elem` rCols]
|
||||
in Join (pushProject db lNeed l) (pushProject db rNeed r) c
|
||||
pushProject db need scan@(Scan _ _)
|
||||
| needsAll need = scan
|
||||
| all (`elem` need) (relOpCols db scan) = scan
|
||||
| otherwise = Project need scan
|
||||
pushProject db need leaf@(Scan _ _) = pushProjectLeaf db need leaf
|
||||
pushProject db need leaf@(Lookup _ _) = pushProjectLeaf db need leaf
|
||||
|
||||
-- * 常量折叠
|
||||
|
||||
@@ -123,6 +132,8 @@ foldConstants e = e
|
||||
|
||||
-- | 单节点重写:把 Filter 分派给上面几条规则,另外负责恒真条件删除与恒等投影消除
|
||||
rewriteNode :: Database -> RelOp -> RelOp
|
||||
rewriteNode _ (Filter (Eq (Col k) (LitInt v)) (Scan _ t))
|
||||
| k == "id" = Lookup t v
|
||||
rewriteNode db (Filter p (Join l r c)) = pushJoin db p l r c
|
||||
rewriteNode db (Filter p x) = case foldConstants p of
|
||||
LitBool True -> x
|
||||
|
||||
@@ -3,12 +3,15 @@ module ChuSQL.Algebra.Planner (translate) where
|
||||
import ChuSQL.Algebra.Op
|
||||
import ChuSQL.Syntax.AST
|
||||
|
||||
-- 内部查询语句转执行计划
|
||||
-- 计划翻译:把 AST 里的 SELECT 翻译成关系代数算子树(只翻译,不做合法性检查)。
|
||||
|
||||
-- 把 FROM 从句转成 Scan / Join 算子。
|
||||
fromToRelOp :: FromClause -> RelOp
|
||||
fromToRelOp (FromTable mAlias tbl) = Scan mAlias tbl
|
||||
fromToRelOp (FromJoin left mAlias tbl cond) =
|
||||
Join (fromToRelOp left) (Scan mAlias tbl) cond
|
||||
|
||||
-- 翻译入口:把 SELECT 翻译成算子树;其它语句类型不支持(返回 Left)。
|
||||
translate :: Query -> Either String RelOp
|
||||
translate q = case q of
|
||||
Select
|
||||
|
||||
@@ -4,20 +4,22 @@ import ChuSQL.Model
|
||||
import ChuSQL.Syntax.AST
|
||||
import Data.List (sortBy)
|
||||
|
||||
-- | 反向排序
|
||||
-- 排序:按 ORDER BY 的列清单逐列比较(支持 ASC / DESC),供执行 Sort 算子时使用。
|
||||
|
||||
-- 反向排序
|
||||
flipOrdering :: Ordering -> Ordering
|
||||
flipOrdering LT = GT
|
||||
flipOrdering GT = LT
|
||||
flipOrdering EQ = EQ
|
||||
|
||||
-- | 定义值的比较规则
|
||||
-- 定义值的比较规则
|
||||
compareValue :: Value -> Value -> Ordering
|
||||
compareValue (VInt a) (VInt b) = compare a b
|
||||
compareValue (VStr a) (VStr b) = compare a b
|
||||
compareValue (VBool a) (VBool b) = compare a b
|
||||
compareValue _ _ = EQ
|
||||
|
||||
-- | 定义行的比较规则
|
||||
-- 定义行的比较规则
|
||||
compareRows :: [(String, SortDir)] -> Row -> Row -> Ordering
|
||||
compareRows [] _ _ = EQ
|
||||
compareRows ((col, dir) : rest) r1 r2 =
|
||||
@@ -31,7 +33,7 @@ compareRows ((col, dir) : rest) r1 r2 =
|
||||
else o
|
||||
_ -> compareRows rest r1 r2
|
||||
|
||||
-- | 将表的每行排序
|
||||
-- 将表的每行排序
|
||||
sortRows :: [(String, SortDir)] -> [Row] -> [Row]
|
||||
sortRows [] rows = rows
|
||||
sortRows spec rows = sortBy (compareRows spec) rows
|
||||
@@ -1,78 +1,100 @@
|
||||
module ChuSQL.Engine (runQuery, rowsOf) where
|
||||
module ChuSQL.Engine (runQuery, runQueryM, rowsOf) where
|
||||
|
||||
import ChuSQL.Algebra.Eval (evalRelOp)
|
||||
import ChuSQL.Algebra.Eval (evalRelOpM)
|
||||
import ChuSQL.Algebra.Expr (evalCondForRow, evalExpr)
|
||||
import ChuSQL.Algebra.Optimize (optimize)
|
||||
import ChuSQL.Algebra.Planner (translate)
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Semantic (check)
|
||||
import ChuSQL.Storage (MonadStorage (..))
|
||||
import ChuSQL.Storage.Memory (MemoryStorage (runMemoryStorage))
|
||||
import ChuSQL.Syntax.AST
|
||||
import Control.Monad (filterM)
|
||||
|
||||
-- | 带检查的查询入口:先做语义检查,再交给下面的执行
|
||||
-- | 对外入口:签名不变,内部走内存实现。
|
||||
runQuery :: Database -> Query -> Either String (Database, [Row])
|
||||
runQuery db q = do
|
||||
_ <- check db q
|
||||
runQueryUnchecked db q
|
||||
(result, db') <- runMemoryStorage (runQueryM q) db
|
||||
rows <- result
|
||||
Right (db', rows)
|
||||
|
||||
-- | 只要结果行(丢掉更新后的数据库),给演示和测试用
|
||||
-- | 只要结果行(丢掉更新后的数据库),给演示和测试用。
|
||||
rowsOf :: Either String (Database, [Row]) -> Either String [Row]
|
||||
rowsOf = fmap snd
|
||||
|
||||
-- 把表整体换掉(调用方已经查过,表一定存在)
|
||||
replaceTable :: String -> Table -> Database -> Database
|
||||
replaceTable name table = map (\(n, t) -> if n == name then (n, table) else (n, t))
|
||||
-- * 泛型入口:走 MonadStorage,未来 IPCStorage 复用同一份代码
|
||||
|
||||
-- 修改表
|
||||
applyUpdates :: [(String, Expr)] -> Row -> Either String Row
|
||||
applyUpdates [] row = Right row
|
||||
applyUpdates ((col, e) : rest) row = do
|
||||
v <- evalExpr e row
|
||||
let row' = setColumn col v row
|
||||
applyUpdates rest row'
|
||||
-- | 语义检查 + 分发到具体语句。
|
||||
runQueryM :: (MonadStorage m) => Query -> m (Either String [Row])
|
||||
runQueryM q = do
|
||||
db <- snapshot
|
||||
case check db q of
|
||||
Left err -> pure (Left err)
|
||||
Right _ -> runQueryUncheckedM q
|
||||
|
||||
-- | SELECT:拿快照,跑 translate → optimize → evalRelOp。
|
||||
runQueryUncheckedM :: (MonadStorage m) => Query -> m (Either String [Row])
|
||||
runQueryUncheckedM q@Select{} = do
|
||||
db <- snapshot
|
||||
case translate q of
|
||||
Left e -> pure (Left e)
|
||||
Right relOp -> evalRelOpM db (optimize db relOp)
|
||||
|
||||
-- \| INSERT:求值后追加一行。
|
||||
runQueryUncheckedM (Insert tbl cols vals) =
|
||||
case mapM (\e -> evalExpr e []) vals of
|
||||
Left err -> pure (Left err)
|
||||
Right values
|
||||
| length cols /= length values ->
|
||||
pure (Left "column count does not match value count")
|
||||
| otherwise -> do
|
||||
result <- insert tbl (zip cols values)
|
||||
pure (result >> Right [])
|
||||
|
||||
-- \| DELETE:扫表、筛掉命中行、整体写回。
|
||||
runQueryUncheckedM (Delete tbl mWhere) = do
|
||||
rowsResult <- scan tbl
|
||||
case rowsResult of
|
||||
Left err -> pure (Left err)
|
||||
Right rows -> do
|
||||
let kept = case mWhere of
|
||||
Nothing -> Right []
|
||||
Just e -> filterM (shouldKeep e) rows
|
||||
case kept of
|
||||
Left err -> pure (Left err)
|
||||
Right keptRows -> do
|
||||
result <- replaceAll tbl keptRows
|
||||
pure (result >> Right [])
|
||||
where
|
||||
setColumn c v r = map (\(k, val) -> if k == c then (k, v) else (k, val)) r
|
||||
|
||||
-- 实际执行(调用方请确保已过 check)
|
||||
runQueryUnchecked :: Database -> Query -> Either String (Database, [Row])
|
||||
runQueryUnchecked db q@Select{} = do
|
||||
relOp <- translate q
|
||||
rows <- evalRelOp db (optimize db relOp)
|
||||
Right (db, rows)
|
||||
|
||||
-- 处理插入
|
||||
runQueryUnchecked db (Insert tbl cols vals) = do
|
||||
values <- mapM (\e -> evalExpr e []) vals
|
||||
if length cols /= length values
|
||||
then Left "column count does not match value count"
|
||||
else do
|
||||
table <- lookupTable db tbl
|
||||
let newRow = zip cols values
|
||||
Right (replaceTable tbl table{tableRows = tableRows table ++ [newRow]} db, [])
|
||||
|
||||
-- 处理删除
|
||||
runQueryUnchecked db (Delete tbl mWhere) = do
|
||||
table <- lookupTable db tbl
|
||||
rows <- case mWhere of
|
||||
Nothing -> Right []
|
||||
Just e -> filterM (shouldKeep e) (tableRows table)
|
||||
Right (replaceTable tbl table{tableRows = rows} db, [])
|
||||
where
|
||||
-- 保留 = 条件不成立
|
||||
-- \| 保留 = 条件不成立。
|
||||
shouldKeep :: Expr -> Row -> Either String Bool
|
||||
shouldKeep e row = not <$> evalCondForRow e row
|
||||
|
||||
-- 处理表更新
|
||||
runQueryUnchecked db (Update tbl assigns mWhere) = do
|
||||
table <- lookupTable db tbl
|
||||
rows <- mapM (updateRow mWhere assigns) (tableRows table)
|
||||
Right (replaceTable tbl table{tableRows = rows} db, [])
|
||||
-- \| UPDATE:扫表、逐行改、整体写回。
|
||||
runQueryUncheckedM (Update tbl assigns mWhere) = do
|
||||
rowsResult <- scan tbl
|
||||
case rowsResult of
|
||||
Left err -> pure (Left err)
|
||||
Right rows -> do
|
||||
let newRows = mapM (updateRow mWhere assigns) rows
|
||||
case newRows of
|
||||
Left err -> pure (Left err)
|
||||
Right rs -> do
|
||||
result <- replaceAll tbl rs
|
||||
pure (result >> Right [])
|
||||
where
|
||||
-- \| 命中条件就套用赋值,否则原样返回。
|
||||
updateRow :: Maybe Expr -> [(String, Expr)] -> Row -> Either String Row
|
||||
updateRow cond asgns row = do
|
||||
keep <- case cond of
|
||||
Nothing -> Right True
|
||||
Just e -> evalCondForRow e row
|
||||
if keep
|
||||
then applyUpdates asgns row
|
||||
else Right row
|
||||
if keep then applyUpdates asgns row else Right row
|
||||
|
||||
-- | 按 assignments 依次求值并覆盖对应列。
|
||||
applyUpdates :: [(String, Expr)] -> Row -> Either String Row
|
||||
applyUpdates [] row = Right row
|
||||
applyUpdates ((col, e) : rest) row = do
|
||||
v <- evalExpr e row
|
||||
let row' = map (\(k, val) -> if k == col then (k, v) else (k, val)) row
|
||||
applyUpdates rest row'
|
||||
@@ -1,5 +1,7 @@
|
||||
module ChuSQL.Model (Column (..), Value (..), Row, Table (..), Database, colNames, colType, lookupTable, allColumns) where
|
||||
|
||||
-- 数据模型:列类型、值、行、表、数据库,外加"按名查表 / 查列类型"这类通用小工具。
|
||||
|
||||
-- 列的类型
|
||||
data Column
|
||||
= TInt
|
||||
@@ -32,14 +34,14 @@ type Database = [(String, Table)]
|
||||
colNames :: Table -> [String]
|
||||
colNames = map fst . tableCols
|
||||
|
||||
-- | 查某一列的类型
|
||||
-- 查某一列的类型
|
||||
colType :: Table -> String -> Maybe Column
|
||||
colType t c = lookup c (tableCols t)
|
||||
|
||||
-- | 按名字查表;表不存在就报错
|
||||
-- 按名字查表;表不存在就报错
|
||||
lookupTable :: Database -> String -> Either String Table
|
||||
lookupTable db t = maybe (Left ("unknown table: " ++ t)) Right (lookup t db)
|
||||
|
||||
-- | 投影列清单里的哨兵值:它表示"全部列,不用挑",不是列名
|
||||
-- 投影列清单里的哨兵值:它表示"全部列,不用挑",不是列名
|
||||
allColumns :: String
|
||||
allColumns = "*"
|
||||
@@ -5,6 +5,8 @@ import ChuSQL.Model
|
||||
import ChuSQL.Syntax.AST
|
||||
import Data.List (intercalate)
|
||||
|
||||
-- 语义检查:执行前的关卡 —— 表和列存不存在、表达式类型对不对、写入的值类型是否匹配。
|
||||
|
||||
-- * 表与列名解析
|
||||
|
||||
-- | 给一批列加上"别名."前缀
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module ChuSQL.Storage (MonadStorage (..)) where
|
||||
|
||||
import ChuSQL.Model (Database, Row)
|
||||
|
||||
-- 存储层抽象:上层只认 domain 类型,不关心底层是内存还是 IPC。
|
||||
class (Monad m) => MonadStorage m where
|
||||
-- 全表扫描;表不存在返回 Left。
|
||||
scan :: String -> m (Either String [Row])
|
||||
|
||||
-- 追加一行到表尾;表不存在返回 Left。
|
||||
insert :: String -> Row -> m (Either String ())
|
||||
|
||||
-- 用新行列表整体替换一张表。
|
||||
replaceAll :: String -> [Row] -> m (Either String ())
|
||||
|
||||
-- 按主键查一行;命中返回 Just,未命中返回 Nothing。
|
||||
lookupByKey :: String -> Int -> m (Either String (Maybe Row))
|
||||
|
||||
-- 取全库快照,喂给还接收 Database 的语义检查 / 优化器 / evalRelOp。
|
||||
snapshot :: m Database
|
||||
Whitespace-only changes.
@@ -0,0 +1,331 @@
|
||||
{-# LANGUAGE ImportQualifiedPost #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChuSQL.Storage.IPC (
|
||||
IPCStorage (..),
|
||||
Request (..),
|
||||
Response (..),
|
||||
SchemaColumn (..),
|
||||
sendRequest,
|
||||
doListTables,
|
||||
doLookupByKey,
|
||||
doInsertKeyed,
|
||||
doDescribeTable,
|
||||
) where
|
||||
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage
|
||||
import Control.Exception (bracket)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), eitherDecodeStrict, encode, object, withObject, (.:), (.=))
|
||||
import Data.Aeson qualified as A
|
||||
import Data.Aeson.Key qualified as K
|
||||
import Data.Aeson.KeyMap qualified as KM
|
||||
import Data.Aeson.Types (Parser)
|
||||
import Data.ByteString.Char8 qualified as BSC
|
||||
import Data.ByteString.Lazy qualified as BL
|
||||
import Data.List (nub)
|
||||
import Data.Scientific (floatingOrInteger)
|
||||
import Data.Text qualified as T
|
||||
import System.Environment (lookupEnv)
|
||||
import System.IO
|
||||
|
||||
-- IPC 存储实现:每个 MonadStorage 方法翻译成管道上的一条 JSON 请求。
|
||||
|
||||
-- | IPC 实现:通过命名管道连 Rust 存储进程。
|
||||
newtype IPCStorage a = IPCStorage
|
||||
{ runIPCStorage :: IO a
|
||||
}
|
||||
|
||||
-- | Functor:把 fmap 转发给底下的 IO。
|
||||
instance Functor IPCStorage where
|
||||
fmap f (IPCStorage m) = IPCStorage (fmap f m)
|
||||
|
||||
-- | Applicative:把 pure / <*> 转发给底下的 IO。
|
||||
instance Applicative IPCStorage where
|
||||
pure = IPCStorage . pure
|
||||
IPCStorage mf <*> IPCStorage ma = IPCStorage (mf <*> ma)
|
||||
|
||||
-- | Monad:顺序执行,转发给底下的 IO。
|
||||
instance Monad IPCStorage where
|
||||
IPCStorage m >>= k = IPCStorage $ do
|
||||
a <- m
|
||||
runIPCStorage (k a)
|
||||
|
||||
-- | 管道路径;默认 \\.\pipe\chusql-storage,可用 CHUSQL_PIPE 覆盖。
|
||||
pipePath :: IO String
|
||||
pipePath = do
|
||||
name <- maybe "chusql-storage" id <$> lookupEnv "CHUSQL_PIPE"
|
||||
pure ("\\\\.\\pipe\\" ++ name)
|
||||
|
||||
-- | 打开管道、跑闭包、关闭。
|
||||
withPipe :: (Handle -> IO a) -> IO a
|
||||
withPipe = bracket openPipe hClose
|
||||
where
|
||||
-- \| 二进制读写、不做换行翻译,跟 Rust 侧对齐。
|
||||
openPipe = do
|
||||
path <- pipePath
|
||||
h <- openFile path ReadWriteMode
|
||||
hSetBuffering h LineBuffering
|
||||
hSetNewlineMode h noNewlineTranslation
|
||||
pure h
|
||||
|
||||
-- | 发一条请求,读一条响应。
|
||||
sendRequest :: Request -> IO Response
|
||||
sendRequest req = withPipe $ \h -> do
|
||||
BL.hPutStr h (encode req)
|
||||
BSC.hPutStr h "\n"
|
||||
hFlush h
|
||||
line <- BSC.hGetLine h
|
||||
let cleaned = BSC.dropWhileEnd (== '\r') line
|
||||
case eitherDecodeStrict cleaned of
|
||||
Left err -> pure (RespError ("decode: " ++ err))
|
||||
Right r -> pure r
|
||||
|
||||
-- * JSON 编解码
|
||||
|
||||
-- | 一列的 schema;对应 Rust 侧的 SchemaColumn。
|
||||
data SchemaColumn = SchemaColumn
|
||||
{ scName :: String
|
||||
, scType :: String
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | SchemaColumn 编码;Haskell 侧只发 DescribeTable,编码用不上,暂存实现。
|
||||
instance ToJSON SchemaColumn where
|
||||
toJSON (SchemaColumn n t) = object ["name" .= n, "ty" .= t]
|
||||
|
||||
-- | SchemaColumn 解码:从 {"name": "...", "ty": "int"|"str"|"bool"} 读回。
|
||||
instance FromJSON SchemaColumn where
|
||||
parseJSON = withObject "SchemaColumn" $ \o -> do
|
||||
n <- o .: "name"
|
||||
t <- o .: "ty"
|
||||
pure (SchemaColumn n t)
|
||||
|
||||
-- | 请求;对应 Rust 侧带 method tag 的 enum。
|
||||
data Request
|
||||
= ReqPing
|
||||
| ReqScan String
|
||||
| ReqInsert String Row (Maybe Int)
|
||||
| ReqReplaceAll String [Row]
|
||||
| ReqListTables
|
||||
| ReqLookupByKey String Int
|
||||
| ReqDescribeTable String
|
||||
|
||||
-- | 请求编码:method 字段 + 各自参数。
|
||||
instance ToJSON Request where
|
||||
toJSON ReqPing = object ["method" .= ("ping" :: T.Text)]
|
||||
toJSON (ReqScan t) = object ["method" .= ("scan" :: T.Text), "table" .= t]
|
||||
toJSON (ReqInsert t r mk) =
|
||||
object $
|
||||
[ "method" .= ("insert" :: T.Text)
|
||||
, "table" .= t
|
||||
, "row" .= rowToJSON r
|
||||
]
|
||||
++ case mk of
|
||||
Nothing -> []
|
||||
Just k -> ["key" .= k]
|
||||
toJSON (ReqReplaceAll t rs) =
|
||||
object
|
||||
[ "method" .= ("replace_all" :: T.Text)
|
||||
, "table" .= t
|
||||
, "rows" .= map rowToJSON rs
|
||||
]
|
||||
toJSON ReqListTables = object ["method" .= ("list_tables" :: T.Text)]
|
||||
toJSON (ReqLookupByKey t k) =
|
||||
object
|
||||
[ "method" .= ("lookup_by_index" :: T.Text)
|
||||
, "table" .= t
|
||||
, "key" .= k
|
||||
]
|
||||
toJSON (ReqDescribeTable t) =
|
||||
object
|
||||
[ "method" .= ("describe_table" :: T.Text)
|
||||
, "table" .= t
|
||||
]
|
||||
|
||||
-- | 响应;对应 Rust 侧带 status tag 的 enum。
|
||||
data Response
|
||||
= RespPong
|
||||
| RespRows [Row]
|
||||
| RespTables [String]
|
||||
| RespSchema [SchemaColumn]
|
||||
| RespOk
|
||||
| RespError String
|
||||
|
||||
-- | 响应解码:按 status 字段分派。
|
||||
instance FromJSON Response where
|
||||
parseJSON = withObject "Response" $ \o -> do
|
||||
status <- o .: "status"
|
||||
case status :: T.Text of
|
||||
"pong" -> pure RespPong
|
||||
"ok" -> pure RespOk
|
||||
"rows" -> RespRows <$> (o .: "rows" >>= mapM rowFromJSON)
|
||||
"tables" -> RespTables <$> o .: "tables"
|
||||
"schema" -> RespSchema <$> o .: "columns"
|
||||
"error" -> RespError <$> o .: "message"
|
||||
other -> fail ("unknown status: " ++ T.unpack other)
|
||||
|
||||
-- | Value 编码成 JSON 的 number / string / bool。
|
||||
valueToJSON :: Value -> A.Value
|
||||
valueToJSON (VInt n) = A.Number (fromIntegral n)
|
||||
valueToJSON (VStr s) = A.String (T.pack s)
|
||||
valueToJSON (VBool b) = A.Bool b
|
||||
|
||||
-- | JSON 解回 Value;number 只接受整数。
|
||||
valueFromJSON :: A.Value -> Parser Value
|
||||
valueFromJSON (A.Number n) =
|
||||
case floatingOrInteger n :: Either Double Integer of
|
||||
Right i -> pure (VInt (fromIntegral i))
|
||||
Left _ -> fail "non-integer number"
|
||||
valueFromJSON (A.String s) = pure (VStr (T.unpack s))
|
||||
valueFromJSON (A.Bool b) = pure (VBool b)
|
||||
valueFromJSON _ = fail "unsupported value type"
|
||||
|
||||
-- | 一行编成 JSON object。
|
||||
rowToJSON :: Row -> A.Value
|
||||
rowToJSON r =
|
||||
A.Object (KM.fromList [(K.fromString k, valueToJSON v) | (k, v) <- r])
|
||||
|
||||
-- | JSON object 解成一行。
|
||||
rowFromJSON :: A.Value -> Parser Row
|
||||
rowFromJSON (A.Object o) = mapM toPair (KM.toList o)
|
||||
where
|
||||
-- \| 解一个键值对。
|
||||
toPair (k, v) = do
|
||||
val <- valueFromJSON v
|
||||
pure (K.toString k, val)
|
||||
rowFromJSON _ = fail "row must be a JSON object"
|
||||
|
||||
-- * 内部请求封装
|
||||
|
||||
-- | 发 Scan 并把结果取成 Either。
|
||||
doScan :: String -> IO (Either String [Row])
|
||||
doScan t = do
|
||||
resp <- sendRequest (ReqScan t)
|
||||
pure $ case resp of
|
||||
RespRows rows -> Right rows
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to scan"
|
||||
|
||||
-- | 发 Insert(不带索引键)。
|
||||
doInsert :: String -> Row -> IO (Either String ())
|
||||
doInsert t r = do
|
||||
resp <- sendRequest (ReqInsert t r Nothing)
|
||||
pure $ case resp of
|
||||
RespOk -> Right ()
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to insert"
|
||||
|
||||
-- | 发 Insert(带索引键)。
|
||||
doInsertKeyed :: String -> Row -> Int -> IO (Either String ())
|
||||
doInsertKeyed t r k = do
|
||||
resp <- sendRequest (ReqInsert t r (Just k))
|
||||
pure $ case resp of
|
||||
RespOk -> Right ()
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to insert"
|
||||
|
||||
-- | 发 ReplaceAll。
|
||||
doReplaceAll :: String -> [Row] -> IO (Either String ())
|
||||
doReplaceAll t rs = do
|
||||
resp <- sendRequest (ReqReplaceAll t rs)
|
||||
pure $ case resp of
|
||||
RespOk -> Right ()
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to replace_all"
|
||||
|
||||
-- | 发 ListTables。
|
||||
doListTables :: IO (Either String [String])
|
||||
doListTables = do
|
||||
resp <- sendRequest ReqListTables
|
||||
pure $ case resp of
|
||||
RespTables ts -> Right ts
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to list_tables"
|
||||
|
||||
-- | 发 LookupByIndex。
|
||||
doLookupByKey :: String -> Int -> IO (Either String (Maybe Row))
|
||||
doLookupByKey t k = do
|
||||
resp <- sendRequest (ReqLookupByKey t k)
|
||||
pure $ case resp of
|
||||
RespRows [] -> Right Nothing
|
||||
RespRows (r : _) -> Right (Just r)
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to lookup_by_index"
|
||||
|
||||
-- | 发 DescribeTable。
|
||||
doDescribeTable :: String -> IO (Either String [SchemaColumn])
|
||||
doDescribeTable t = do
|
||||
resp <- sendRequest (ReqDescribeTable t)
|
||||
pure $ case resp of
|
||||
RespSchema cols -> Right cols
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to describe_table"
|
||||
|
||||
-- | 把 Rust 的 schema 列转成 Model 的 Column;未知类型降级为 TStr。
|
||||
schemaToColumns :: [SchemaColumn] -> [(String, Column)]
|
||||
schemaToColumns = map go
|
||||
where
|
||||
-- \| 按 ty 字符串映射成 Column 构造子。
|
||||
go (SchemaColumn n "int") = (n, TInt)
|
||||
go (SchemaColumn n "str") = (n, TStr)
|
||||
go (SchemaColumn n "bool") = (n, TBool)
|
||||
go (SchemaColumn n _) = (n, TStr)
|
||||
|
||||
-- | 从若干行推断列名与类型;catalog 拿不到时的兜底。
|
||||
inferColumns :: [Row] -> [(String, Column)]
|
||||
inferColumns rows = [(n, inferType n) | n <- names]
|
||||
where
|
||||
-- \| 全部出现过的列名。
|
||||
names = nub (concatMap (map fst) rows)
|
||||
|
||||
-- \| 用第一个出现的值推断列类型;找不到就给 TStr。
|
||||
inferType n = case [v | r <- rows, Just v <- [lookup n r]] of
|
||||
(VInt _ : _) -> TInt
|
||||
(VStr _ : _) -> TStr
|
||||
(VBool _ : _) -> TBool
|
||||
[] -> TStr
|
||||
|
||||
-- | 先 describe 拿列,再 scan 拿行;describe 拿不到时退回扫行推断。
|
||||
loadTable :: String -> IO Table
|
||||
loadTable t = do
|
||||
schemaR <- doDescribeTable t
|
||||
case schemaR of
|
||||
Right cols -> do
|
||||
rowsR <- doScan t
|
||||
case rowsR of
|
||||
Right rows -> pure (Table t (schemaToColumns cols) rows)
|
||||
Left _ -> pure (Table t (schemaToColumns cols) [])
|
||||
Left _ -> do
|
||||
rowsR <- doScan t
|
||||
case rowsR of
|
||||
Right rows -> pure (Table t (inferColumns rows) rows)
|
||||
Left _ -> pure (Table t [] [])
|
||||
|
||||
-- * MonadStorage 实例
|
||||
|
||||
-- | IPC 实现:所有方法都走管道。
|
||||
instance MonadStorage IPCStorage where
|
||||
-- \| 发 Scan。
|
||||
scan t = IPCStorage (doScan t)
|
||||
|
||||
-- \| 发 Insert,不带索引键。
|
||||
insert t r = IPCStorage (doInsert t r)
|
||||
|
||||
-- \| 发 ReplaceAll。
|
||||
replaceAll t rs = IPCStorage (doReplaceAll t rs)
|
||||
|
||||
-- \| 发 LookupByIndex。
|
||||
lookupByKey t k = IPCStorage (doLookupByKey t k)
|
||||
|
||||
-- \| ListTables + 逐表 (describe, scan),拼成 Database。
|
||||
snapshot = IPCStorage $ do
|
||||
result <- doListTables
|
||||
case result of
|
||||
Left _ -> pure []
|
||||
Right ts -> mapM loadDbEntry ts
|
||||
where
|
||||
-- \| 加载一个 (表名, 表) 对。
|
||||
loadDbEntry t = do
|
||||
tbl <- loadTable t
|
||||
pure (t, tbl)
|
||||
@@ -0,0 +1,66 @@
|
||||
module ChuSQL.Storage.Memory (MemoryStorage (runMemoryStorage)) where
|
||||
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage
|
||||
|
||||
-- | 内存实现:本质是 Database 上的 State,错误通道是 Either String。
|
||||
newtype MemoryStorage a = MemoryStorage {runMemoryStorage :: Database -> Either String (a, Database)}
|
||||
|
||||
-- | Functor:把 fmap 转发给底下的 State。
|
||||
instance Functor MemoryStorage where
|
||||
fmap f (MemoryStorage m) = MemoryStorage $ \db -> do
|
||||
(a, db') <- m db
|
||||
Right (f a, db')
|
||||
|
||||
-- | Applicative:转发 pure / <*>。
|
||||
instance Applicative MemoryStorage where
|
||||
pure x = MemoryStorage $ \db -> Right (x, db)
|
||||
MemoryStorage mf <*> MemoryStorage ma = MemoryStorage $ \db -> do
|
||||
(f, db1) <- mf db
|
||||
(a, db2) <- ma db1
|
||||
Right (f a, db2)
|
||||
|
||||
-- | Monad:顺序执行,把动作转发给底下的 State。
|
||||
instance Monad MemoryStorage where
|
||||
MemoryStorage m >>= k = MemoryStorage $ \db -> do
|
||||
(a, db') <- m db
|
||||
runMemoryStorage (k a) db'
|
||||
|
||||
-- | MonadStorage 实例:内存里直接改 Database。
|
||||
instance MonadStorage MemoryStorage where
|
||||
-- \| 按名查表,返回所有行。
|
||||
scan t = MemoryStorage $ \db -> do
|
||||
tbl <- lookupTable db t
|
||||
Right (Right (tableRows tbl), db)
|
||||
|
||||
-- \| 追加一行到表尾。
|
||||
insert t r = MemoryStorage $ \db -> do
|
||||
tbl <- lookupTable db t
|
||||
Right (Right (), replaceTable t tbl{tableRows = tableRows tbl ++ [r]} db)
|
||||
|
||||
-- \| 用新行列表替换整表内容。
|
||||
replaceAll t rows = MemoryStorage $ \db -> do
|
||||
tbl <- lookupTable db t
|
||||
Right (Right (), replaceTable t tbl{tableRows = rows} db)
|
||||
|
||||
-- \| 找 id 列等于 k 的第一行。
|
||||
lookupByKey t k = MemoryStorage $ \db -> do
|
||||
tbl <- lookupTable db t
|
||||
Right (Right (findRow tbl k), db)
|
||||
|
||||
-- \| 原样返回当前数据库。
|
||||
snapshot = MemoryStorage $ \db -> Right (db, db)
|
||||
|
||||
-- | 按 id 列匹配一个整数。
|
||||
findRow :: Table -> Int -> Maybe Row
|
||||
findRow tbl k = case filter matches (tableRows tbl) of
|
||||
(r : _) -> Just r
|
||||
[] -> Nothing
|
||||
where
|
||||
-- \| 这行的 id 列是否等于 k。
|
||||
matches r = lookup "id" r == Just (VInt k)
|
||||
|
||||
-- | 用给定表替换同名表;不存在就原样返回。
|
||||
replaceTable :: String -> Table -> Database -> Database
|
||||
replaceTable name table =
|
||||
map (\(n, t) -> if n == name then (n, table) else (n, t))
|
||||
@@ -1,5 +1,7 @@
|
||||
module ChuSQL.Syntax.AST (Query (..), FromClause (..), Expr (..), SortDir (..), makeSelect) where
|
||||
|
||||
-- SQL 语法树:四类语句(SELECT / INSERT / DELETE / UPDATE)+ 表达式、FROM 从句、排序方向。
|
||||
|
||||
-- 查询语句
|
||||
data Query
|
||||
= Select
|
||||
@@ -14,7 +16,7 @@ data Query
|
||||
| Update String [(String, Expr)] (Maybe Expr)
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- 从句
|
||||
-- FROM 从句:单表或 JOIN。
|
||||
data FromClause
|
||||
= FromTable (Maybe String) String
|
||||
| FromJoin FromClause (Maybe String) String Expr
|
||||
@@ -33,11 +35,13 @@ data Expr
|
||||
| Or Expr Expr
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- 排序方向
|
||||
data SortDir
|
||||
= Asc
|
||||
| Desc
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- 便捷构造:造一条最简单的 SELECT(给测试和演示用)。
|
||||
makeSelect :: [String] -> String -> Maybe Expr -> Query
|
||||
makeSelect cols tbl w =
|
||||
Select
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{-# LANGUAGE ImportQualifiedPost #-}
|
||||
|
||||
{- | SQL 语法分析器:把 SQL 文本解析为 'ChuSQL.Syntax.AST.Query'。
|
||||
|
||||
词法层负责空白、关键字、标识符、字面量与符号;语法层按
|
||||
优先级 比较 > AND > OR 自底向上组装表达式。
|
||||
-}
|
||||
module ChuSQL.Syntax.Parser (parseQuery) where
|
||||
|
||||
import ChuSQL.Syntax.AST
|
||||
@@ -17,6 +12,9 @@ import Text.Megaparsec
|
||||
import Text.Megaparsec.Char
|
||||
import Text.Megaparsec.Char.Lexer qualified as Lxr
|
||||
|
||||
-- SQL 解析器:把 SQL 文本解析成 AST。词法层管空白、关键字、标识符、字面量与符号;
|
||||
-- 语法层按优先级 比较 > AND > OR 自底向上组装表达式。
|
||||
|
||||
type Parser = Parsec Void String
|
||||
|
||||
-- * 词法层
|
||||
@@ -29,12 +27,12 @@ sc = Lxr.space space1 empty empty
|
||||
lexeme :: Parser a -> Parser a
|
||||
lexeme = Lxr.lexeme sc
|
||||
|
||||
-- | 匹配一个符号(如 ","、"("),并吃掉其后的空白。
|
||||
symbol :: String -> Parser String
|
||||
symbol = Lxr.symbol sc
|
||||
|
||||
{- | 大小写不敏感的关键字;其后不能紧跟标识符字符,避免把 @selection@ 读成 @select@。
|
||||
整体用 'try' 包住:否则 @ORDER@ 会被 @keyword "or"@ 吃掉前缀 @OR@ 再报错,导致无法回溯。
|
||||
-}
|
||||
-- | 大小写不敏感的关键字;其后不能紧跟标识符字符,避免把 @selection@ 读成 @select@。
|
||||
-- 整体用 'try' 包住:否则 @ORDER@ 会被 @keyword "or"@ 吃掉前缀 @OR@ 再报错,导致无法回溯。
|
||||
keyword :: String -> Parser ()
|
||||
keyword k = lexeme $ try $ do
|
||||
_ <- string' k
|
||||
@@ -47,6 +45,7 @@ identifier = lexeme $ do
|
||||
rest <- many (satisfy isIdentChar)
|
||||
return (first : rest)
|
||||
|
||||
-- | 标识符允许的字符:字母、数字或下划线。
|
||||
isIdentChar :: Char -> Bool
|
||||
isIdentChar ch = isAlphaNum ch || ch == '_'
|
||||
|
||||
@@ -62,9 +61,8 @@ stringLit = lexeme $ do
|
||||
_ <- char '\''
|
||||
return s
|
||||
|
||||
{- | 字符串内的一个字符:@''@ 折叠成一个单引号,其余字符(含反斜杠)原样保留。
|
||||
两个分支都不会在失败时消耗输入,因此可以安全地放进 'many'。
|
||||
-}
|
||||
-- | 字符串内的一个字符:@''@ 折叠成一个单引号,其余字符(含反斜杠)原样保留。
|
||||
-- 两个分支都不会在失败时消耗输入,因此可以安全地放进 'many'。
|
||||
stringChar :: Parser Char
|
||||
stringChar =
|
||||
choice
|
||||
@@ -95,9 +93,8 @@ qualifiedName = do
|
||||
expr :: Parser Expr
|
||||
expr = makeExprParser atom operatorTable
|
||||
|
||||
{- | 运算符表。注意 'makeExprParser' 要求各层 **按优先级从高到低** 排列:
|
||||
比较运算符绑定最紧,'AND' 次之,'OR' 最松。
|
||||
-}
|
||||
-- | 运算符表。注意 'makeExprParser' 要求各层按优先级从高到低排列:
|
||||
-- 比较运算符绑定最紧,'AND' 次之,'OR' 最松。
|
||||
operatorTable :: [[Operator Parser Expr]]
|
||||
operatorTable =
|
||||
[
|
||||
@@ -128,12 +125,14 @@ orderItem = do
|
||||
dir <- sortDir
|
||||
return (col, dir)
|
||||
|
||||
-- | @ORDER BY col [ASC|DESC], ...@。
|
||||
orderByClause :: Parser [(String, SortDir)]
|
||||
orderByClause = do
|
||||
keyword "order"
|
||||
keyword "by"
|
||||
sepBy1 orderItem (symbol ",")
|
||||
|
||||
-- | @col = expr@,用于 UPDATE 的 SET 子句。
|
||||
assignment :: Parser (String, Expr)
|
||||
assignment = do
|
||||
col <- identifier
|
||||
@@ -141,7 +140,7 @@ assignment = do
|
||||
e <- expr
|
||||
return (col, e)
|
||||
|
||||
-- | 从句语法
|
||||
-- * 从句层
|
||||
|
||||
-- | SQL 保留字,不能用作表别名
|
||||
reservedWords :: [String]
|
||||
@@ -166,21 +165,22 @@ reservedWords =
|
||||
, "set"
|
||||
]
|
||||
|
||||
{- | 表别名。别名不能是保留字:否则 @FROM users WHERE age > 18@ 会把 @WHERE@
|
||||
当成 @users@ 的别名吃掉,后面真正的 WHERE 子句就再也解析不到。
|
||||
用 'try' 包住,一旦命中保留字就整体回溯,让 'optional' 正常返回 'Nothing'。
|
||||
-}
|
||||
-- | 表别名。别名不能是保留字:否则 @FROM users WHERE age > 18@ 会把 @WHERE@
|
||||
-- 当成 @users@ 的别名吃掉,后面真正的 WHERE 子句就再也解析不到。
|
||||
-- 用 'try' 包住,一旦命中保留字就整体回溯,让 'optional' 正常返回 'Nothing'。
|
||||
aliasName :: Parser String
|
||||
aliasName = try $ do
|
||||
name <- identifier
|
||||
if map toLower name `elem` reservedWords then empty else return name
|
||||
|
||||
-- | @表名 [别名]@。
|
||||
tableRef :: Parser (Maybe String, String)
|
||||
tableRef = do
|
||||
tbl <- identifier
|
||||
mAlias <- optional aliasName
|
||||
return (mAlias, tbl)
|
||||
|
||||
-- | @JOIN 表 [别名] ON 条件@。
|
||||
joinClause :: Parser (Maybe String, String, Expr)
|
||||
joinClause = do
|
||||
keyword "join"
|
||||
@@ -189,6 +189,7 @@ joinClause = do
|
||||
cond <- expr
|
||||
return (mAlias, tbl, cond)
|
||||
|
||||
-- | FROM 从句:一张表 + 任意多个 JOIN。
|
||||
fromClause :: Parser FromClause
|
||||
fromClause = do
|
||||
(mAlias, tbl) <- tableRef
|
||||
|
||||
+223
-52
@@ -4,15 +4,24 @@ import ChuSQL.Algebra.Eval (evalRelOp)
|
||||
import ChuSQL.Algebra.Op (RelOp (..), renderPlan)
|
||||
import ChuSQL.Algebra.Optimize (optimize, pushProject)
|
||||
import ChuSQL.Algebra.Planner (translate)
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Syntax.AST
|
||||
import ChuSQL.Engine
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage (MonadStorage (..))
|
||||
import ChuSQL.Storage.IPC (IPCStorage (runIPCStorage), Request (ReqPing), Response (RespPong), doListTables, sendRequest)
|
||||
import ChuSQL.Syntax.AST
|
||||
import ChuSQL.Syntax.Parser
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Exception (IOException, bracket, try)
|
||||
import Data.List (isInfixOf, sortOn)
|
||||
import System.CPUTime (getCPUTime)
|
||||
import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getTemporaryDirectory, removePathForcibly)
|
||||
import System.Environment (getEnvironment, lookupEnv, setEnv, unsetEnv)
|
||||
import System.Exit (ExitCode (..))
|
||||
import System.FilePath ((</>))
|
||||
import System.IO (Handle)
|
||||
import System.Process (CreateProcess (..), ProcessHandle, StdStream (NoStream), createProcess, proc, terminateProcess, waitForProcess)
|
||||
import Test.Hspec
|
||||
|
||||
-- ============================================================
|
||||
-- Test database
|
||||
-- ============================================================
|
||||
users :: Table
|
||||
users =
|
||||
Table
|
||||
@@ -40,14 +49,8 @@ orders =
|
||||
testDB :: Database
|
||||
testDB = [("users", users), ("orders", orders)]
|
||||
|
||||
-- ============================================================
|
||||
-- Main
|
||||
-- ============================================================
|
||||
main :: IO ()
|
||||
main = hspec $ do
|
||||
-- ============================================================
|
||||
-- AST
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Syntax.AST" $ do
|
||||
it "equal Selects are equal" $ do
|
||||
makeSelect ["name"] "users" Nothing
|
||||
@@ -57,9 +60,6 @@ main = hspec $ do
|
||||
makeSelect ["name"] "users" Nothing
|
||||
`shouldNotBe` makeSelect ["age"] "users" Nothing
|
||||
|
||||
-- ============================================================
|
||||
-- Parser
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Syntax.Parser" $ do
|
||||
it "parses a simple SELECT" $ do
|
||||
parseQuery "SELECT name FROM users"
|
||||
@@ -201,9 +201,6 @@ main = hspec $ do
|
||||
parseQuery "SELECT name FROM"
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
-- ============================================================
|
||||
-- Parser: JOIN
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Syntax.Parser (JOIN)" $ do
|
||||
it "parses a simple JOIN without aliases" $ do
|
||||
parseQuery "SELECT name FROM users JOIN orders ON id = user_id"
|
||||
@@ -244,9 +241,6 @@ main = hspec $ do
|
||||
Right q -> selectCols q `shouldBe` ["u.name", "o.product"]
|
||||
Left err -> expectationFailure err
|
||||
|
||||
-- ============================================================
|
||||
-- Engine: SELECT
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Engine (SELECT)" $ do
|
||||
it "returns all rows without WHERE" $ do
|
||||
rowsOf (runQuery testDB (makeSelect ["*"] "users" Nothing))
|
||||
@@ -301,9 +295,6 @@ main = hspec $ do
|
||||
runQuery testDB (makeSelect ["nope"] "users" Nothing)
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
-- ============================================================
|
||||
-- Engine: INSERT
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Engine (INSERT)" $ do
|
||||
it "parses a simple INSERT" $ do
|
||||
parseQuery "INSERT INTO users (name, age) VALUES ('Dave', 22)"
|
||||
@@ -350,9 +341,6 @@ main = hspec $ do
|
||||
runQuery testDB (Insert "users" ["name"] [Col "other"])
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
-- ============================================================
|
||||
-- Engine: DELETE
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Engine (DELETE)" $ do
|
||||
it "parses DELETE with WHERE" $ do
|
||||
parseQuery "DELETE FROM users WHERE age < 18"
|
||||
@@ -400,9 +388,6 @@ main = hspec $ do
|
||||
runQuery testDB (Delete "users" (Just (Gt (Col "unknown") (LitInt 18))))
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
-- ============================================================
|
||||
-- Engine: UPDATE
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Engine (UPDATE)" $ do
|
||||
it "parses UPDATE with WHERE" $ do
|
||||
parseQuery "UPDATE users SET age = 26 WHERE name = 'Alice'"
|
||||
@@ -492,9 +477,6 @@ main = hspec $ do
|
||||
Right _ -> True
|
||||
Left _ -> True
|
||||
|
||||
-- ============================================================
|
||||
-- Engine: ORDER BY
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Engine (ORDER BY)" $ do
|
||||
it "parses ORDER BY ASC" $ do
|
||||
case parseQuery "SELECT name FROM users ORDER BY age ASC" of
|
||||
@@ -580,9 +562,6 @@ main = hspec $ do
|
||||
}
|
||||
runQuery testDB q `shouldSatisfy` isLeft
|
||||
|
||||
-- ============================================================
|
||||
-- Engine: LIMIT
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Engine (LIMIT)" $ do
|
||||
it "parses LIMIT" $ do
|
||||
case parseQuery "SELECT * FROM users LIMIT 2" of
|
||||
@@ -663,9 +642,6 @@ main = hspec $ do
|
||||
parseQuery "SELECT * FROM users LIMIT -1"
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
-- ============================================================
|
||||
-- Engine: JOIN
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Engine (JOIN)" $ 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)
|
||||
@@ -733,9 +709,6 @@ main = hspec $ do
|
||||
)
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
-- ============================================================
|
||||
-- End-to-end
|
||||
-- ============================================================
|
||||
describe "ChuSQL end-to-end" $ do
|
||||
it "parses and executes a simple query" $ do
|
||||
rowsOf (parseQuery "SELECT name FROM users WHERE age > 18" >>= runQuery testDB)
|
||||
@@ -937,7 +910,8 @@ main = hspec $ do
|
||||
let plan =
|
||||
Project
|
||||
["name"]
|
||||
( Limit 2
|
||||
( Limit
|
||||
2
|
||||
( Sort
|
||||
[("age", Desc)]
|
||||
(Filter (Gt (Col "age") (LitInt 18)) (Scan Nothing "users"))
|
||||
@@ -979,10 +953,12 @@ main = hspec $ do
|
||||
|
||||
it "keeps projection with ORDER BY and LIMIT results identical" $ do
|
||||
sameResultAsUnoptimized "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.product DESC LIMIT 2"
|
||||
it "rewrites Filter id = k into Lookup" $ do
|
||||
fmap planRoot (optimizedPlan "SELECT name FROM users WHERE id = 2")
|
||||
`shouldBe` Right (Lookup "users" 2)
|
||||
it "keeps Lookup result identical to unoptimized" $ do
|
||||
sameResultAsUnoptimized "SELECT name FROM users WHERE id = 2"
|
||||
|
||||
-- ============================================================
|
||||
-- Semantic
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Semantic" $ do
|
||||
it "rejects an unknown column in WHERE" $ do
|
||||
(parseQuery "SELECT name FROM users WHERE nope > 18" >>= runQuery testDB)
|
||||
@@ -1092,9 +1068,6 @@ main = hspec $ do
|
||||
(parseQuery "SELECT * FROM users u JOIN orders o ON u.id = o.user_id" >>= runQuery testDB)
|
||||
`shouldSatisfy` isRight
|
||||
|
||||
-- ============================================================
|
||||
-- Op
|
||||
-- ============================================================
|
||||
describe "ChuSQL.Algebra.Op" $ do
|
||||
it "renders a plan as indented text" $ do
|
||||
case parseQuery "SELECT name FROM users WHERE age > 18" >>= translate of
|
||||
@@ -1125,12 +1098,203 @@ main = hspec $ do
|
||||
, " Scan Nothing \"users\""
|
||||
]
|
||||
|
||||
describe "ChuSQL.Storage.IPC" $ do
|
||||
it "scan returns rows with all three value types after insert" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
let row = [("id", VInt 7), ("name", VStr "Alice"), ("flag", VBool True)]
|
||||
runIPCStorage (insert "users" row) `shouldReturn` Right ()
|
||||
rows <- runIPCStorage (scan "users")
|
||||
(map sortRow <$> rows) `shouldBe` Right [sortRow row]
|
||||
|
||||
it "scan on a missing table returns Left" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
result <- runIPCStorage (scan "no_such_table")
|
||||
result `shouldSatisfy` isLeft
|
||||
|
||||
it "scan returns all 20 inserted rows in order" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
mapM_ (\i -> runIPCStorage (insert "many" [("id", VInt i)])) [1 .. 20 :: Int]
|
||||
rows <- runIPCStorage (scan "many")
|
||||
fmap length rows `shouldBe` Right 20
|
||||
fmap (sortOn show . map (lookup "id")) rows
|
||||
`shouldBe` Right (sortOn show (map (Just . VInt) [1 .. 20]))
|
||||
|
||||
it "replaceAll leaves only the new rows" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
runIPCStorage (insert "t" [("id", VInt 1)]) `shouldReturn` Right ()
|
||||
runIPCStorage (replaceAll "t" [[("id", VInt 9)], [("id", VInt 8)]]) `shouldReturn` Right ()
|
||||
rows <- runIPCStorage (scan "t")
|
||||
fmap (sortOn show . map (lookup "id")) rows
|
||||
`shouldBe` Right (sortOn show [Just (VInt 9), Just (VInt 8)])
|
||||
|
||||
it "lookupByKey finds a row inserted with an id over the pipe" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
let row = [("id", VInt 7), ("name", VStr "Zoe")]
|
||||
runIPCStorage (insert "keyed" row) `shouldReturn` Right ()
|
||||
found <- runIPCStorage (lookupByKey "keyed" 7)
|
||||
fmap (fmap sortRow) found `shouldBe` Right (Just (sortRow row))
|
||||
missing <- runIPCStorage (lookupByKey "keyed" 8)
|
||||
missing `shouldBe` Right Nothing
|
||||
|
||||
it "replaceAll rebuilds the index over the pipe" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
runIPCStorage (insert "rb" [("id", VInt 1)]) `shouldReturn` Right ()
|
||||
runIPCStorage (replaceAll "rb" [[("id", VInt 9), ("name", VStr "Zoe")]]) `shouldReturn` Right ()
|
||||
old <- runIPCStorage (lookupByKey "rb" 1)
|
||||
old `shouldBe` Right Nothing
|
||||
new <- runIPCStorage (lookupByKey "rb" 9)
|
||||
fmap (fmap sortRow) new
|
||||
`shouldBe` Right (Just (sortRow [("id", VInt 9), ("name", VStr "Zoe")]))
|
||||
|
||||
it "snapshot lists tables and scans each one to build the database" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
runIPCStorage (insert "a" [("id", VInt 1)]) `shouldReturn` Right ()
|
||||
runIPCStorage (insert "b" [("id", VInt 2)]) `shouldReturn` Right ()
|
||||
db <- runIPCStorage snapshot
|
||||
map fst db `shouldBe` ["a", "b"]
|
||||
map (length . tableRows . snd) db `shouldBe` [1, 1]
|
||||
map (map (lookup "id") . tableRows . snd) db `shouldBe` [[Just (VInt 1)], [Just (VInt 2)]]
|
||||
|
||||
it "doListTables returns the tables that exist" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
runIPCStorage (insert "t1" [("id", VInt 1)]) `shouldReturn` Right ()
|
||||
runIPCStorage (insert "t2" [("id", VInt 2)]) `shouldReturn` Right ()
|
||||
doListTables `shouldReturn` Right ["t1", "t2"]
|
||||
|
||||
it "rows survive a restart on the same data directory" $ do
|
||||
located <- locateServer
|
||||
case located of
|
||||
Left err -> pendingWith err
|
||||
Right bin -> do
|
||||
srv1 <- startServer bin
|
||||
let dir = dataDir srv1
|
||||
withServerEnv srv1 $ runIPCStorage (insert "persist" [("id", VInt 42)]) `shouldReturn` Right ()
|
||||
stopServer srv1
|
||||
srv2 <- startServerAt bin dir
|
||||
rows <- withServerEnv srv2 $ runIPCStorage (scan "persist")
|
||||
stopServer srv2
|
||||
cleanServerDir srv1
|
||||
(map sortRow <$> rows) `shouldBe` Right [sortRow [("id", VInt 42)]]
|
||||
|
||||
it "a missing server yields Left without throwing" $ do
|
||||
withPipeName "chusql-no-such-server" $ do
|
||||
result <- runIPCStorage (scan "users")
|
||||
result `shouldSatisfy` isLeft
|
||||
|
||||
emptyDB :: Database
|
||||
emptyDB = [(n, t {tableRows = []}) | (n, t) <- testDB]
|
||||
emptyDB = [(n, t{tableRows = []}) | (n, t) <- testDB]
|
||||
|
||||
data IPCServer = IPCServer
|
||||
{ pipeName :: String
|
||||
, dataDir :: FilePath
|
||||
, processHandle :: ProcessHandle
|
||||
}
|
||||
|
||||
locateServer :: IO (Either String FilePath)
|
||||
locateServer = do
|
||||
mdir <- firstDir [".." </> "chusql-storage", "chusql-storage"]
|
||||
case mdir of
|
||||
Nothing -> pure (Left "chusql-storage directory not found (run these tests inside the repository)")
|
||||
Just dir -> do
|
||||
built <-
|
||||
try (createProcess (proc "cargo" ["build", "--bin", "server"]){cwd = Just dir, std_out = NoStream, std_err = NoStream}) ::
|
||||
IO (Either IOException (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle))
|
||||
case built of
|
||||
Left e -> pure (Left ("cannot run cargo (Rust is required for the IPC tests): " ++ show e))
|
||||
Right (_, _, _, ph) -> do
|
||||
ec <- waitForProcess ph
|
||||
case ec of
|
||||
ExitFailure n -> pure (Left ("cargo build --bin server failed with exit code " ++ show n))
|
||||
ExitSuccess -> do
|
||||
found <- firstExisting [dir </> "target" </> "debug" </> "server.exe", dir </> "target" </> "debug" </> "server"]
|
||||
pure (maybe (Left "cargo build finished but no server executable was found") Right found)
|
||||
where
|
||||
firstDir [] = pure Nothing
|
||||
firstDir (d : ds) = do
|
||||
ok <- doesDirectoryExist d
|
||||
if ok then pure (Just d) else firstDir ds
|
||||
firstExisting [] = pure Nothing
|
||||
firstExisting (p : ps) = do
|
||||
ok <- doesFileExist p
|
||||
if ok then pure (Just p) else firstExisting ps
|
||||
|
||||
startServer :: FilePath -> IO IPCServer
|
||||
startServer bin = do
|
||||
stamp <- show <$> getCPUTime
|
||||
tmp <- getTemporaryDirectory
|
||||
let dir = tmp </> "chusql-hs-IPC" </> stamp
|
||||
createDirectoryIfMissing True dir
|
||||
startServerAt bin dir
|
||||
|
||||
startServerAt :: FilePath -> FilePath -> IO IPCServer
|
||||
startServerAt bin dir = do
|
||||
stamp <- show <$> getCPUTime
|
||||
let name = "chusql-hs-test-" ++ stamp
|
||||
parentEnv <- getEnvironment
|
||||
(_, _, _, ph) <-
|
||||
createProcess
|
||||
(proc bin [])
|
||||
{ env = Just (("CHUSQL_PIPE", name) : ("CHUSQL_DATA_DIR", dir) : parentEnv)
|
||||
, std_out = NoStream
|
||||
, std_err = NoStream
|
||||
}
|
||||
let srv = IPCServer name dir ph
|
||||
waitUntilReady srv 100
|
||||
pure srv
|
||||
|
||||
waitUntilReady :: IPCServer -> Int -> IO ()
|
||||
waitUntilReady srv tries = do
|
||||
probe <- try (withServerEnv srv (sendRequest ReqPing)) :: IO (Either IOException Response)
|
||||
case probe of
|
||||
Right RespPong -> pure ()
|
||||
_ | tries > 0 -> threadDelay 50000 >> waitUntilReady srv (tries - 1)
|
||||
Right _ -> ioError (userError "server is up but ping did not answer pong")
|
||||
Left e -> ioError e
|
||||
|
||||
stopServer :: IPCServer -> IO ()
|
||||
stopServer srv = do
|
||||
_ <- try (terminateProcess (processHandle srv)) :: IO (Either IOException ())
|
||||
_ <- try (waitForProcess (processHandle srv)) :: IO (Either IOException ExitCode)
|
||||
pure ()
|
||||
|
||||
cleanServerDir :: IPCServer -> IO ()
|
||||
cleanServerDir srv = do
|
||||
_ <- try (removePathForcibly (dataDir srv)) :: IO (Either IOException ())
|
||||
pure ()
|
||||
|
||||
withTestServer :: (IPCServer -> IO ()) -> IO ()
|
||||
withTestServer act = do
|
||||
located <- locateServer
|
||||
case located of
|
||||
Left err
|
||||
| "cannot run cargo" `isInfixOf` err -> pendingWith err
|
||||
| otherwise -> expectationFailure err
|
||||
Right bin -> bracket (startServer bin) (\s -> stopServer s >> cleanServerDir s) act
|
||||
|
||||
withServerEnv :: IPCServer -> IO a -> IO a
|
||||
withServerEnv srv act = do
|
||||
oldPipe <- lookupEnv "CHUSQL_PIPE"
|
||||
oldDir <- lookupEnv "CHUSQL_DATA_DIR"
|
||||
setEnv "CHUSQL_PIPE" (pipeName srv)
|
||||
setEnv "CHUSQL_DATA_DIR" (dataDir srv)
|
||||
r <- act
|
||||
restore "CHUSQL_PIPE" oldPipe
|
||||
restore "CHUSQL_DATA_DIR" oldDir
|
||||
pure r
|
||||
where
|
||||
restore k = maybe (unsetEnv k) (setEnv k)
|
||||
|
||||
withPipeName :: String -> IO a -> IO a
|
||||
withPipeName name act = do
|
||||
old <- lookupEnv "CHUSQL_PIPE"
|
||||
setEnv "CHUSQL_PIPE" name
|
||||
r <- act
|
||||
maybe (unsetEnv "CHUSQL_PIPE") (setEnv "CHUSQL_PIPE") old
|
||||
pure r
|
||||
|
||||
sortRow :: Row -> Row
|
||||
sortRow = sortOn fst
|
||||
|
||||
-- ============================================================
|
||||
-- Helpers
|
||||
-- ============================================================
|
||||
isLeft :: Either a b -> Bool
|
||||
isLeft (Left _) = True
|
||||
isLeft (Right _) = False
|
||||
@@ -1167,6 +1331,7 @@ firstFilterCond (Join l r _) = case firstFilterCond l of
|
||||
Just p -> Just p
|
||||
Nothing -> firstFilterCond r
|
||||
firstFilterCond (Scan _ _) = Nothing
|
||||
firstFilterCond (Lookup _ _) = Nothing
|
||||
|
||||
anyFilter :: RelOp -> Bool
|
||||
anyFilter (Filter _ _) = True
|
||||
@@ -1175,6 +1340,7 @@ anyFilter (Sort _ x) = anyFilter x
|
||||
anyFilter (Limit _ x) = anyFilter x
|
||||
anyFilter (Join l r _) = anyFilter l || anyFilter r
|
||||
anyFilter (Scan _ _) = False
|
||||
anyFilter (Lookup _ _) = False
|
||||
|
||||
stripProjects :: RelOp -> RelOp
|
||||
stripProjects (Project _ x) = stripProjects x
|
||||
@@ -1183,6 +1349,7 @@ stripProjects (Sort spec x) = Sort spec (stripProjects x)
|
||||
stripProjects (Limit n x) = Limit n (stripProjects x)
|
||||
stripProjects (Join l r c) = Join (stripProjects l) (stripProjects r) c
|
||||
stripProjects (Scan a t) = Scan a t
|
||||
stripProjects (Lookup t k) = Lookup t k
|
||||
|
||||
projectedPlan :: String -> Either String RelOp
|
||||
projectedPlan sql = do
|
||||
@@ -1205,3 +1372,7 @@ countFilters _ = 0
|
||||
leftSideFilters :: RelOp -> Maybe Int
|
||||
leftSideFilters (Project _ (Join l _ _)) = Just (countFilters l)
|
||||
leftSideFilters _ = Nothing
|
||||
|
||||
planRoot :: RelOp -> RelOp
|
||||
planRoot (Project _ x) = planRoot x
|
||||
planRoot x = x
|
||||
Reference in new issue
Block a user