优化和收尾
This commit is contained in:
1 parent
7f5bcc289e
commit
9e8c5f1ef0
33 files changed
+3332
-598
No files matched your search
@@ -26,10 +26,12 @@ library
|
||||
build-depends: base >= 4.14 && < 5
|
||||
, aeson
|
||||
, bytestring
|
||||
, hashable
|
||||
, megaparsec
|
||||
, parser-combinators
|
||||
, scientific
|
||||
, text
|
||||
, unordered-containers
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import ChuSQL.Algebra.Expr (evalCondForRow)
|
||||
import ChuSQL.Algebra.Op
|
||||
import ChuSQL.Algebra.Sort (sortRows)
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage (MonadStorage (..))
|
||||
import ChuSQL.Storage (IndexResult (..), MonadStorage (..))
|
||||
import ChuSQL.Syntax.AST (Expr (..))
|
||||
import Control.Monad (filterM, foldM)
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
-- 执行:按算子树算出结果行(Scan 阶段会加别名前缀)。
|
||||
|
||||
@@ -18,19 +19,55 @@ project ["*"] row = row
|
||||
project cs row = [(c, v) | (c, v) <- row, c `elem` cs]
|
||||
|
||||
-- | 给列名加别名前缀
|
||||
addPrefix :: String -> Row -> Row
|
||||
addPrefix prefix = map (\(k, v) -> (prefix ++ k, v))
|
||||
|
||||
-- | 主键匹配条件(id = k)
|
||||
keyCondition :: Int -> Expr
|
||||
keyCondition k = Eq (Col "id") (LitInt k)
|
||||
addPrefix :: Maybe String -> Row -> Row
|
||||
addPrefix mAlias = map (\(k, v) -> (qualify mAlias k, v))
|
||||
|
||||
-- | 扫描一张表
|
||||
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))
|
||||
Right (map (addPrefix mAlias) (tableRows table))
|
||||
|
||||
-- | 点查退化成"扫描 + 按条件筛"时用的条件:`列 = 值`(列名带上别名前缀)
|
||||
lookupCondition :: Maybe String -> String -> Int -> Expr
|
||||
lookupCondition mAlias col k = Eq (Col (qualify mAlias col)) (LitInt k)
|
||||
|
||||
-- * 等值连接
|
||||
|
||||
-- | 等值连接的两侧键:左行取哪列、右行取哪列
|
||||
data EquiKeys = EquiKeys
|
||||
{ ekLeft :: String
|
||||
, ekRight :: String
|
||||
}
|
||||
|
||||
-- | 一批行的列名(空批给空清单,此时一律退回嵌套循环)
|
||||
rowsCols :: [Row] -> [String]
|
||||
rowsCols (r : _) = map fst r
|
||||
rowsCols [] = []
|
||||
|
||||
-- | 认出 a = b 这种两列等值条件,并且能分清谁在左谁在右
|
||||
equiKeys :: [Row] -> [Row] -> Expr -> Maybe EquiKeys
|
||||
equiKeys lrows rrows (Eq (Col a) (Col b))
|
||||
| a /= b, a `elem` lcols, b `elem` rcols = Just (EquiKeys a b)
|
||||
| a /= b, b `elem` lcols, a `elem` rcols = Just (EquiKeys b a)
|
||||
where
|
||||
lcols = rowsCols lrows
|
||||
rcols = rowsCols rrows
|
||||
equiKeys _ _ _ = Nothing
|
||||
|
||||
-- | 哈希连接:右表建哈希表,左表逐行探测。
|
||||
-- 输出顺序与嵌套循环一致(左表顺序为主,右表顺序为辅)。
|
||||
hashJoin :: EquiKeys -> [Row] -> [Row] -> [Row]
|
||||
hashJoin keys lrows rrows = concatMap probe lrows
|
||||
where
|
||||
-- 同一键的右行按原顺序排在桶里
|
||||
buckets =
|
||||
HM.fromListWith
|
||||
(flip (++))
|
||||
[(v, [r]) | r <- rrows, Just v <- [lookup (ekRight keys) r]]
|
||||
probe l = case lookup (ekLeft keys) l of
|
||||
Nothing -> []
|
||||
Just v -> [l ++ r | r <- HM.lookupDefault [] v buckets]
|
||||
|
||||
-- | 把两边的行按条件配对(边配边筛,不先建整张积表)
|
||||
filterPairs :: (Row -> Either String Bool) -> [Row] -> [Row] -> Either String [Row]
|
||||
@@ -45,14 +82,21 @@ filterPairs cond lrows rrows = go lrows []
|
||||
ok <- cond row
|
||||
pure (if ok then row : acc else acc)
|
||||
|
||||
-- | 一次连接:能认出等值键就走哈希,否则退回嵌套循环
|
||||
joinRows :: Expr -> [Row] -> [Row] -> Either String [Row]
|
||||
joinRows cond lrows rrows = case equiKeys lrows rrows cond of
|
||||
Just keys -> Right (hashJoin keys lrows rrows)
|
||||
Nothing -> filterPairs (evalCondForRow cond) lrows rrows
|
||||
|
||||
-- * 求值
|
||||
|
||||
-- | 纯求值(不需要存储)
|
||||
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
|
||||
-- 纯求值没有索引可问:退回"扫描 + 按条件筛",和未优化时的 Filter 完全等价
|
||||
evalRelOp db (Lookup mAlias tbl col k) = do
|
||||
rows <- evalScan db mAlias tbl
|
||||
filterM (evalCondForRow (lookupCondition mAlias col k)) rows
|
||||
evalRelOp db (Filter e op) = do
|
||||
rows <- evalRelOp db op
|
||||
filterM (evalCondForRow e) rows
|
||||
@@ -68,9 +112,9 @@ evalRelOp db (Limit n op) = do
|
||||
evalRelOp db (Join left right cond) = do
|
||||
lrows <- evalRelOp db left
|
||||
rrows <- evalRelOp db right
|
||||
filterPairs (evalCondForRow cond) lrows rrows
|
||||
joinRows cond lrows rrows
|
||||
|
||||
-- | 单子求值:Lookup 问存储
|
||||
-- | 单子求值:Lookup 先问存储有没有索引,没有就退回扫描
|
||||
evalRelOpM :: (MonadStorage m) => RelOp -> m (Either String [Row])
|
||||
evalRelOpM (Scan mAlias t) = do
|
||||
result <- scan t
|
||||
@@ -78,13 +122,19 @@ evalRelOpM (Scan mAlias t) = do
|
||||
rows <- result
|
||||
case mAlias of
|
||||
Nothing -> Right rows
|
||||
Just a -> Right (map (addPrefix (a ++ ".")) rows)
|
||||
evalRelOpM (Lookup t k) = do
|
||||
result <- lookupByKey t k
|
||||
pure $ case result of
|
||||
Left e -> Left e
|
||||
Right Nothing -> Right []
|
||||
Right (Just row) -> Right [row]
|
||||
Just _ -> Right (map (addPrefix mAlias) rows)
|
||||
evalRelOpM (Lookup mAlias t col k) = do
|
||||
result <- lookupByColumn t col k
|
||||
case result of
|
||||
Left e -> pure (Left e)
|
||||
-- 这个列上没有索引:老老实实扫一遍(结果和未优化时一样)
|
||||
Right NoIndex -> do
|
||||
rows <- evalRelOpM (Scan mAlias t)
|
||||
pure $ do
|
||||
rs <- rows
|
||||
filterM (evalCondForRow (lookupCondition mAlias col k)) rs
|
||||
Right (IndexRow Nothing) -> pure (Right [])
|
||||
Right (IndexRow (Just row)) -> pure (Right [addPrefix mAlias row])
|
||||
evalRelOpM (Filter p x) = do
|
||||
result <- evalRelOpM x
|
||||
pure $ do
|
||||
@@ -105,4 +155,4 @@ evalRelOpM (Join l r cond) = do
|
||||
pure $ do
|
||||
left <- ls
|
||||
right <- rs
|
||||
filterPairs (evalCondForRow cond) left right
|
||||
joinRows cond left right
|
||||
@@ -7,7 +7,11 @@ import ChuSQL.Syntax.AST (Expr, SortDir)
|
||||
-- | 算子树的一个节点
|
||||
data RelOp
|
||||
= Scan (Maybe String) String
|
||||
| Lookup String Int
|
||||
| -- \| 按某一列的值点查一行:别名、表、列、值。
|
||||
-- 只是"这里可以走点查"的意思,到底有没有索引由存储层回答
|
||||
-- (那个列上没有索引就退回全表扫描,见 Eval)。
|
||||
-- 别名要留着,回来的一行才能和 `Scan 别名 表` 长得一样。
|
||||
Lookup (Maybe String) String String Int
|
||||
| Filter Expr RelOp
|
||||
| Project [String] RelOp
|
||||
| Sort [(String, SortDir)] RelOp
|
||||
@@ -21,7 +25,7 @@ renderPlan = go 0
|
||||
where
|
||||
go ind op = case op of
|
||||
Scan a t -> pad ++ "Scan " ++ show a ++ " " ++ show t
|
||||
Lookup t k -> pad ++ "Lookup " ++ show t ++ " " ++ show k
|
||||
Lookup _ t c k -> pad ++ "Lookup " ++ show t ++ "." ++ show c ++ " = " ++ show k
|
||||
Filter e x -> pad ++ "Filter " ++ show e ++ "\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
|
||||
|
||||
@@ -16,7 +16,7 @@ relOpCols db (Scan mAlias tbl) =
|
||||
Just t -> map (prefix ++) (colNames t)
|
||||
where
|
||||
prefix = maybe "" (++ ".") mAlias
|
||||
relOpCols db (Lookup tbl _) = relOpCols db (Scan Nothing tbl)
|
||||
relOpCols db (Lookup mAlias tbl _ _) = relOpCols db (Scan mAlias tbl)
|
||||
relOpCols db (Filter _ x) = relOpCols db x
|
||||
relOpCols _ (Project ["*"] _) = ["*"]
|
||||
relOpCols _ (Project cols _) = cols
|
||||
@@ -104,7 +104,7 @@ pushProject db need (Join l r c)
|
||||
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 leaf@(Scan _ _) = pushProjectLeaf db need leaf
|
||||
pushProject db need leaf@(Lookup _ _) = pushProjectLeaf db need leaf
|
||||
pushProject db need leaf@(Lookup _ _ _ _) = pushProjectLeaf db need leaf
|
||||
|
||||
|
||||
-- * 常量折叠
|
||||
@@ -131,8 +131,12 @@ foldConstants e = e
|
||||
-- * 执行优化
|
||||
-- | 单节点重写
|
||||
rewriteNode :: Database -> RelOp -> RelOp
|
||||
rewriteNode _ (Filter (Eq (Col k) (LitInt v)) (Scan _ t))
|
||||
| k == "id" = Lookup t v
|
||||
-- 单列等值条件改成点查:这里是"可以走索引"的意思,
|
||||
-- 到底走不走得成由存储层回答(这个列上没有索引就退回全表扫描,见 Eval),
|
||||
-- 所以优化器不必先知道表上到底有哪些索引。
|
||||
-- 别名要留在节点里:回来的一行必须和 `Scan 别名 表` 长得一样,不然后面取列就对不上了。
|
||||
rewriteNode _ (Filter (Eq (Col k) (LitInt v)) (Scan mAlias t)) =
|
||||
Lookup mAlias t (unqualify mAlias k) 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
|
||||
|
||||
@@ -9,7 +9,6 @@ import ChuSQL.Semantic (check)
|
||||
import ChuSQL.Storage (MonadStorage (..))
|
||||
import ChuSQL.Storage.Memory (MemoryStorage (runMemoryStorage))
|
||||
import ChuSQL.Syntax.AST
|
||||
import Control.Monad (filterM)
|
||||
|
||||
-- 引擎入口:语义检查 + 分发执行,以及内存实现与泛型版本。
|
||||
|
||||
@@ -29,7 +28,9 @@ rowsOf = fmap snd
|
||||
-- | 泛型入口:先检查再执行
|
||||
runStatementM :: (MonadStorage m) => Statement -> m (Either String [Row])
|
||||
runStatementM q = do
|
||||
db <- snapshot
|
||||
-- 只要结构:语义检查和查询优化都只看列,一行数据都不碰,
|
||||
-- 所以这里不用取整库快照(那会把所有行拉一遍)
|
||||
db <- schema
|
||||
case check db q of
|
||||
Left err -> pure (Left err)
|
||||
Right _ -> runStatementUncheckedM db q
|
||||
@@ -42,36 +43,43 @@ runStatementUncheckedM db q@Select{} =
|
||||
case translate q of
|
||||
Left e -> pure (Left e)
|
||||
Right relOp -> evalRelOpM (optimize db relOp)
|
||||
runStatementUncheckedM _ (Insert tbl cols vals) =
|
||||
case mapM (\e -> evalExpr e []) vals of
|
||||
runStatementUncheckedM _ (Insert tbl cols rows) =
|
||||
-- 多行一次交给存储:N 行只落一次盘(键怎么算、索引怎么写是存储层的事)
|
||||
case mapM toRow rows of
|
||||
Left err -> pure (Left err)
|
||||
Right values
|
||||
| length cols /= length values ->
|
||||
pure (Left "column count does not match value count")
|
||||
| otherwise -> do
|
||||
let row = zip cols values
|
||||
key = case lookup "id" row of
|
||||
Just (VInt k) -> Just k
|
||||
_ -> Nothing
|
||||
result <- insert tbl row key
|
||||
pure (result >> Right [])
|
||||
Right rs -> do
|
||||
result <- insertMany tbl rs
|
||||
pure (result >> Right [])
|
||||
where
|
||||
-- \| 一行字面量求值成一行数据
|
||||
toRow :: [Expr] -> Either String Row
|
||||
toRow vals = do
|
||||
values <- mapM (\e -> evalExpr e []) vals
|
||||
if length cols /= length values
|
||||
then Left "column count does not match value count"
|
||||
else Right (zip cols values)
|
||||
runStatementUncheckedM _ (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 [])
|
||||
Right rows -> case splitByCondition mWhere rows of
|
||||
Left err -> pure (Left err)
|
||||
Right (doomed, kept) ->
|
||||
-- 要删的行都有 id 就走行级删(存储层原地删,代价只和删几行有关);
|
||||
-- 没有 id 的表定位不到行,只能整表写回。
|
||||
case mapM rowId doomed of
|
||||
Right ids -> do
|
||||
result <- deleteKeys tbl ids
|
||||
pure (result >> Right [])
|
||||
Left _ -> do
|
||||
result <- replaceAll tbl kept
|
||||
pure (result >> Right [])
|
||||
where
|
||||
-- \| 保留 = 条件不成立
|
||||
shouldKeep :: Expr -> Row -> Either String Bool
|
||||
shouldKeep e row = not <$> evalCondForRow e row
|
||||
-- \| 这一行的 id(要拿来当删除键)
|
||||
rowId :: Row -> Either String Int
|
||||
rowId r = case lookup "id" r of
|
||||
Just (VInt k) -> Right k
|
||||
_ -> Left "row has no integer id"
|
||||
runStatementUncheckedM _ (Update tbl assigns mWhere) = do
|
||||
rowsResult <- scan tbl
|
||||
case rowsResult of
|
||||
@@ -97,9 +105,27 @@ runStatementUncheckedM _ (CreateTable name cols) = do
|
||||
runStatementUncheckedM _ (DropTable name) = do
|
||||
result <- dropTable name
|
||||
pure (result >> Right [])
|
||||
runStatementUncheckedM _ (CreateIndex tbl col) = do
|
||||
result <- createIndex tbl col
|
||||
pure (result >> Right [])
|
||||
runStatementUncheckedM _ (DropIndex tbl col) = do
|
||||
result <- dropIndex tbl col
|
||||
pure (result >> Right [])
|
||||
|
||||
-- * 更新辅助
|
||||
|
||||
-- | 按条件把行分成"要删的"和"留着的"两拨(没有条件就全都要删)
|
||||
splitByCondition :: Maybe Expr -> [Row] -> Either String ([Row], [Row])
|
||||
splitByCondition cond = go [] []
|
||||
where
|
||||
-- \| 一行一行过,保持原顺序;条件求值出错就立刻停(和 filterM 一样)
|
||||
go doomed kept [] = Right (reverse doomed, reverse kept)
|
||||
go doomed kept (r : rs) = do
|
||||
hit <- case cond of
|
||||
Nothing -> Right True
|
||||
Just e -> evalCondForRow e r
|
||||
go (if hit then r : doomed else doomed) (if hit then kept else r : kept) rs
|
||||
|
||||
-- | 依次求值并覆盖列
|
||||
applyUpdates :: [(String, Expr)] -> Row -> Either String Row
|
||||
applyUpdates [] row = Right row
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
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, qualify, unqualify) where
|
||||
|
||||
import Data.Hashable (Hashable (..))
|
||||
import Data.List (stripPrefix)
|
||||
import Data.Maybe (fromMaybe)
|
||||
|
||||
-- 数据模型:列类型、值、行、表、数据库,外加几个查表小工具。
|
||||
|
||||
@@ -17,6 +21,12 @@ data Value
|
||||
| VBool Bool
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 值要能当哈希键用(等值连接建哈希表)
|
||||
instance Hashable Value where
|
||||
hashWithSalt s (VInt n) = hashWithSalt (hashWithSalt s (0 :: Int)) n
|
||||
hashWithSalt s (VStr t) = hashWithSalt (hashWithSalt s (1 :: Int)) t
|
||||
hashWithSalt s (VBool b) = hashWithSalt (hashWithSalt s (2 :: Int)) b
|
||||
|
||||
-- | 一行:列名 -> 值
|
||||
type Row = [(String, Value)]
|
||||
|
||||
@@ -47,3 +57,13 @@ lookupTable db t = maybe (Left ("unknown table: " ++ t)) Right (lookup t db)
|
||||
-- | 投影哨兵 "*"
|
||||
allColumns :: String
|
||||
allColumns = "*"
|
||||
|
||||
-- | 给列名加别名前缀:`Just "u"` + `name` -> `u.name`(没有别名就原样)
|
||||
qualify :: Maybe String -> String -> String
|
||||
qualify Nothing c = c
|
||||
qualify (Just a) c = a ++ "." ++ c
|
||||
|
||||
-- | 去掉别名前缀:`Just "u"` + `u.name` -> `name`(没有这个前缀就原样返回)
|
||||
unqualify :: Maybe String -> String -> String
|
||||
unqualify Nothing c = c
|
||||
unqualify (Just a) c = fromMaybe c (stripPrefix (a ++ ".") c)
|
||||
@@ -114,10 +114,12 @@ check db q = case q of
|
||||
checkColumns "SELECT" env cols
|
||||
checkColumns "ORDER BY" env (map fst orderBy)
|
||||
mapM_ (checkBool "WHERE" env) mWhere
|
||||
Insert tbl cols vals -> do
|
||||
Insert tbl cols rows -> do
|
||||
t <- lookupTable db tbl
|
||||
checkColumns "INSERT" (tableCols t) cols
|
||||
mapM_ (checkValue t) (zip cols vals)
|
||||
if null rows
|
||||
then Left "INSERT: no values"
|
||||
else mapM_ (mapM_ (checkValue t) . zip cols) rows
|
||||
Delete tbl mWhere -> do
|
||||
t <- lookupTable db tbl
|
||||
mapM_ (checkBool "WHERE" (tableCols t)) mWhere
|
||||
@@ -138,3 +140,16 @@ check db q = case q of
|
||||
if null name
|
||||
then Left "DROP TABLE: empty table name"
|
||||
else Right ()
|
||||
CreateIndex tbl col -> do
|
||||
t <- lookupTable db tbl
|
||||
if col `elem` tableCols' t
|
||||
then Right ()
|
||||
else Left ("CREATE INDEX: unknown column: " ++ col)
|
||||
DropIndex tbl col -> do
|
||||
t <- lookupTable db tbl
|
||||
if col `elem` tableCols' t
|
||||
then Right ()
|
||||
else Left ("DROP INDEX: unknown column: " ++ col)
|
||||
where
|
||||
-- \| 表的列名清单
|
||||
tableCols' = map fst . tableCols
|
||||
@@ -1,24 +1,66 @@
|
||||
module ChuSQL.Storage (
|
||||
MonadStorage (..),
|
||||
IndexResult (..),
|
||||
) where
|
||||
|
||||
import ChuSQL.Model (Column, Database, Row)
|
||||
import ChuSQL.Model (Column, Database, Row, Table (..), Value (..))
|
||||
|
||||
-- 存储层抽象:上层只认 domain 类型,不关心底层实现。
|
||||
|
||||
-- | 存储接口:读写表、按主键取行、取快照
|
||||
-- | 一次索引点查的结果
|
||||
data IndexResult
|
||||
= -- \| 有索引,查过了:Just 命中一行,Nothing 没这行
|
||||
IndexRow (Maybe Row)
|
||||
| -- \| 没有这个索引 —— 调用方应该退回全表扫描
|
||||
NoIndex
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 存储接口:读写表、按索引取行、取快照
|
||||
class (Monad m) => MonadStorage m where
|
||||
-- \| 全表扫描
|
||||
scan :: String -> m (Either String [Row])
|
||||
|
||||
-- \| 追加一行
|
||||
insert :: String -> Row -> Maybe Int -> m (Either String ())
|
||||
-- \| 追加一行。
|
||||
-- 要写哪些索引由**存储层按自己的元数据**决定(表上有哪些索引它自己清楚),
|
||||
-- 调用方不需要指定 key。
|
||||
insert :: String -> Row -> m (Either String ())
|
||||
|
||||
-- \| 一批行一起追加。
|
||||
-- 默认逐行调 `insert`;能批量的实现(比如走 WAL 的 IPC 存储)会覆盖它,
|
||||
-- 好处是 N 行只落一次盘。
|
||||
insertMany :: String -> [Row] -> m (Either String ())
|
||||
insertMany t = go
|
||||
where
|
||||
-- \| 一行一行来,遇到错就停
|
||||
go [] = pure (Right ())
|
||||
go (r : rs) = do
|
||||
x <- insert t r
|
||||
case x of
|
||||
Left e -> pure (Left e)
|
||||
Right _ -> go rs
|
||||
|
||||
-- \| 按 `id` 批量删行。
|
||||
-- 默认实现是"全读出来、筛掉、整表写回"(对内存实现足够);
|
||||
-- 真正能在原地删的实现会覆盖它。
|
||||
deleteKeys :: String -> [Int] -> m (Either String ())
|
||||
deleteKeys t ks = do
|
||||
rows <- scan t
|
||||
case rows of
|
||||
Left e -> pure (Left e)
|
||||
Right rs -> replaceAll t [r | r <- rs, not (doomed r)]
|
||||
where
|
||||
-- \| 这一行的 id 在待删清单里吗
|
||||
doomed r = case lookup "id" r of
|
||||
Just (VInt k) -> k `elem` ks
|
||||
_ -> False
|
||||
|
||||
-- \| 整表替换
|
||||
replaceAll :: String -> [Row] -> m (Either String ())
|
||||
|
||||
-- \| 按主键取一行
|
||||
lookupByKey :: String -> Int -> m (Either String (Maybe Row))
|
||||
-- \| 按某一列的索引取一行。
|
||||
-- 默认回答"没有索引"(内存实现不需要索引),于是上层会自己扫一遍。
|
||||
lookupByColumn :: String -> String -> Int -> m (Either String IndexResult)
|
||||
lookupByColumn _ _ _ = pure (Right NoIndex)
|
||||
|
||||
-- \| 建一张空表
|
||||
createTable :: String -> [(String, Column)] -> m (Either String ())
|
||||
@@ -26,5 +68,24 @@ class (Monad m) => MonadStorage m where
|
||||
-- \| 删除表
|
||||
dropTable :: String -> m (Either String ())
|
||||
|
||||
-- \| 取全库快照
|
||||
-- \| 给某一列建索引(要求这一列整数取值唯一)
|
||||
createIndex :: String -> String -> m (Either String ())
|
||||
createIndex _ _ = pure (Right ())
|
||||
|
||||
-- \| 去掉某一列的索引
|
||||
dropIndex :: String -> String -> m (Either String ())
|
||||
dropIndex _ _ = pure (Right ())
|
||||
|
||||
-- \| 取全库快照(表名 + 列 + 全部行)
|
||||
snapshot :: m Database
|
||||
|
||||
-- \| 取全库**结构**:只要表名和列,不要行。
|
||||
--
|
||||
-- 语义检查和查询优化都只看列名/列类型,一行数据都不碰;
|
||||
-- 而"取全库快照"要把所有行拉一遍,在单语句路径上是白花的钱。
|
||||
-- 默认实现退化成整库快照再抹掉行,有更便宜做法的实现(比如走数据字典)可以覆盖。
|
||||
schema :: m Database
|
||||
schema = fmap (map withoutRows) snapshot
|
||||
where
|
||||
-- \| 抹掉行,只留表名与列定义
|
||||
withoutRows (name, t) = (name, t {tableRows = []})
|
||||
@@ -6,11 +6,14 @@ module ChuSQL.Storage.IPC (
|
||||
Request (..),
|
||||
Response (..),
|
||||
SchemaColumn (..),
|
||||
TableInfo (..),
|
||||
sendRequest,
|
||||
closeConnection,
|
||||
doListTables,
|
||||
doLookupByKey,
|
||||
doInsertWith,
|
||||
doLookupByColumn,
|
||||
doInsert,
|
||||
doInsertMany,
|
||||
doDeleteKeys,
|
||||
doDescribeTable,
|
||||
doDropTable,
|
||||
doListCatalog,
|
||||
@@ -20,7 +23,7 @@ import ChuSQL.Model
|
||||
import ChuSQL.Storage
|
||||
import Control.Concurrent.MVar (MVar, modifyMVar, modifyMVar_, newMVar)
|
||||
import Control.Exception (IOException, 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.Key qualified as K
|
||||
import Data.Aeson.KeyMap qualified as KM
|
||||
@@ -155,28 +158,41 @@ instance FromJSON SchemaColumn where
|
||||
data Request
|
||||
= ReqPing
|
||||
| ReqScan String
|
||||
| ReqInsert String Row (Maybe Int)
|
||||
| ReqInsert String Row
|
||||
| ReqInsertBatch String [Row]
|
||||
| ReqDeleteKeys String [Int]
|
||||
| ReqReplaceAll String [Row]
|
||||
| ReqListTables
|
||||
| ReqLookupByKey String Int
|
||||
| ReqLookupByColumn String String Int
|
||||
| ReqDescribeTable String
|
||||
| ReqCreateTable String [SchemaColumn]
|
||||
| ReqDropTable String
|
||||
| ReqCreateIndex String String
|
||||
| ReqDropIndex String String
|
||||
| ReqListCatalog
|
||||
|
||||
-- | 请求编码
|
||||
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 $
|
||||
toJSON (ReqInsert t r) =
|
||||
object
|
||||
[ "method" .= ("insert" :: T.Text)
|
||||
, "table" .= t
|
||||
, "row" .= rowToJSON r
|
||||
]
|
||||
++ case mk of
|
||||
Nothing -> []
|
||||
Just k -> ["key" .= k]
|
||||
toJSON (ReqInsertBatch t rs) =
|
||||
object
|
||||
[ "method" .= ("insert_batch" :: T.Text)
|
||||
, "table" .= t
|
||||
, "rows" .= map rowToJSON rs
|
||||
]
|
||||
toJSON (ReqDeleteKeys t ks) =
|
||||
object
|
||||
[ "method" .= ("delete_keys" :: T.Text)
|
||||
, "table" .= t
|
||||
, "keys" .= ks
|
||||
]
|
||||
toJSON (ReqReplaceAll t rs) =
|
||||
object
|
||||
[ "method" .= ("replace_all" :: T.Text)
|
||||
@@ -184,10 +200,11 @@ instance ToJSON Request where
|
||||
, "rows" .= map rowToJSON rs
|
||||
]
|
||||
toJSON ReqListTables = object ["method" .= ("list_tables" :: T.Text)]
|
||||
toJSON (ReqLookupByKey t k) =
|
||||
toJSON (ReqLookupByColumn t c k) =
|
||||
object
|
||||
[ "method" .= ("lookup_by_index" :: T.Text)
|
||||
, "table" .= t
|
||||
, "column" .= c
|
||||
, "key" .= k
|
||||
]
|
||||
toJSON (ReqDescribeTable t) =
|
||||
@@ -203,17 +220,40 @@ instance ToJSON Request where
|
||||
]
|
||||
toJSON (ReqDropTable t) =
|
||||
object ["method" .= ("drop_table" :: T.Text), "table" .= t]
|
||||
toJSON (ReqCreateIndex t c) =
|
||||
object
|
||||
[ "method" .= ("create_index" :: T.Text)
|
||||
, "table" .= t
|
||||
, "column" .= c
|
||||
]
|
||||
toJSON (ReqDropIndex t c) =
|
||||
object
|
||||
[ "method" .= ("drop_index" :: T.Text)
|
||||
, "table" .= t
|
||||
, "column" .= c
|
||||
]
|
||||
toJSON ReqListCatalog = object ["method" .= ("list_catalog" :: T.Text)]
|
||||
|
||||
-- | 一张表的线上信息:列 + 行数 + 索引 + 列统计
|
||||
data TableInfo = TableInfo
|
||||
{ tiTable :: String
|
||||
, tiColumns :: [SchemaColumn]
|
||||
, tiRows :: Int
|
||||
, tiIndexes :: [String]
|
||||
, tiStats :: [(String, Int, Bool)]
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 响应类型
|
||||
data Response
|
||||
= RespPong
|
||||
| RespRows [Row]
|
||||
| RespTables [String]
|
||||
| RespSchema [SchemaColumn] Int
|
||||
| RespSchema TableInfo
|
||||
| RespNoIndex
|
||||
| RespOk
|
||||
| RespError String
|
||||
| RespCatalog [(String, [SchemaColumn], Int)]
|
||||
| RespCatalog [TableInfo]
|
||||
|
||||
-- | 响应解码
|
||||
instance FromJSON Response where
|
||||
@@ -222,25 +262,35 @@ instance FromJSON Response where
|
||||
case status :: T.Text of
|
||||
"pong" -> pure RespPong
|
||||
"ok" -> pure RespOk
|
||||
"no_index" -> pure RespNoIndex
|
||||
"rows" -> RespRows <$> (o .: "rows" >>= mapM rowFromJSON)
|
||||
"tables" -> RespTables <$> o .: "tables"
|
||||
"error" -> RespError <$> o .: "message"
|
||||
"schema" -> do
|
||||
cols <- o .: "columns"
|
||||
cnt <- o .: "row_count"
|
||||
pure (RespSchema cols cnt)
|
||||
"schema" -> RespSchema <$> parseTable (A.Object o)
|
||||
"catalog" -> do
|
||||
xs <- o .: "schemas"
|
||||
parsed <- mapM parseTableSchema xs
|
||||
pure (RespCatalog parsed)
|
||||
xs <- o .: "schemas" :: Parser [A.Value]
|
||||
RespCatalog <$> mapM parseTable xs
|
||||
other -> fail ("unknown status: " ++ T.unpack other)
|
||||
where
|
||||
-- \| 解析一条 list_catalog 记录。
|
||||
parseTableSchema = withObject "TableSchema" $ \o -> do
|
||||
-- \| 解一张表的 schema(新字段都给了默认值,老响应也能解)
|
||||
parseTable :: A.Value -> Parser TableInfo
|
||||
parseTable = withObject "TableInfo" $ \o -> do
|
||||
t <- o .: "table"
|
||||
cols <- o .: "columns"
|
||||
cnt <- o .: "row_count"
|
||||
pure (t, cols, cnt)
|
||||
idx <- o .:? "indexes" .!= []
|
||||
sts <- o .:? "stats" .!= []
|
||||
idxCols <- mapM (\v -> withObject "IndexWire" (.: "column") v) (idx :: [A.Value])
|
||||
stats <- mapM parseStat (sts :: [A.Value])
|
||||
pure (TableInfo t cols cnt idxCols stats)
|
||||
|
||||
-- \| 解一条列统计
|
||||
parseStat :: A.Value -> Parser (String, Int, Bool)
|
||||
parseStat = withObject "ColumnStat" $ \o -> do
|
||||
name <- o .: "name"
|
||||
distinct <- o .: "distinct"
|
||||
capped <- o .:? "capped" .!= False
|
||||
pure (name, distinct, capped)
|
||||
|
||||
-- | 值编码成 JSON
|
||||
valueToJSON :: Value -> A.Value
|
||||
@@ -275,87 +325,88 @@ rowFromJSON _ = fail "row must be a JSON object"
|
||||
|
||||
-- * 内部封装
|
||||
|
||||
-- | 发一条请求,把响应翻成 Either。
|
||||
--
|
||||
-- 每个操作的区别只有两处:发什么请求、认哪种响应。所以固定套路收在这里:
|
||||
-- 对面报错 → `Left`;认得这种响应 → `Right`;其余一律算协议错(比如对面版本对不上)。
|
||||
ask :: String -> Request -> (Response -> Maybe a) -> IO (Either String a)
|
||||
ask what req recognize = do
|
||||
resp <- sendRequest req
|
||||
pure $ case resp of
|
||||
RespError e -> Left e
|
||||
other -> case recognize other of
|
||||
Just a -> Right a
|
||||
Nothing -> Left ("unexpected response to " ++ what)
|
||||
|
||||
-- | 只认"成了"(多数写操作都这样)
|
||||
okOnly :: Response -> Maybe ()
|
||||
okOnly RespOk = Just ()
|
||||
okOnly _ = Nothing
|
||||
|
||||
-- | 发 Scan
|
||||
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"
|
||||
doScan t = ask "scan" (ReqScan t) $ \resp -> case resp of
|
||||
RespRows rows -> Just rows
|
||||
_ -> Nothing
|
||||
|
||||
-- | 发 Insert,可带索引键。
|
||||
doInsertWith :: String -> Row -> Maybe Int -> IO (Either String ())
|
||||
doInsertWith t r mk = do
|
||||
resp <- sendRequest (ReqInsert t r mk)
|
||||
pure $ case resp of
|
||||
RespOk -> Right ()
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to insert"
|
||||
-- | 发 Insert(索引由存储层自己维护,这里不传 key)
|
||||
doInsert :: String -> Row -> IO (Either String ())
|
||||
doInsert t r = ask "insert" (ReqInsert t r) okOnly
|
||||
|
||||
-- | 发 InsertBatch:一批行一次请求
|
||||
doInsertMany :: String -> [Row] -> IO (Either String ())
|
||||
doInsertMany t rs = ask "insert_batch" (ReqInsertBatch t rs) okOnly
|
||||
|
||||
-- | 发 DeleteKeys
|
||||
doDeleteKeys :: String -> [Int] -> IO (Either String ())
|
||||
doDeleteKeys t ks = ask "delete_keys" (ReqDeleteKeys t ks) okOnly
|
||||
|
||||
-- | 发 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"
|
||||
doReplaceAll t rs = ask "replace_all" (ReqReplaceAll t rs) okOnly
|
||||
|
||||
-- | 发 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"
|
||||
doListTables = ask "list_tables" ReqListTables $ \resp -> case resp of
|
||||
RespTables ts -> Just ts
|
||||
_ -> Nothing
|
||||
|
||||
-- | 一次拿全库 schema(表名 + 列 + 行数)。
|
||||
doListCatalog :: IO (Either String [(String, [SchemaColumn], Int)])
|
||||
doListCatalog = do
|
||||
resp <- sendRequest ReqListCatalog
|
||||
pure $ case resp of
|
||||
RespCatalog xs -> Right xs
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to list_catalog"
|
||||
-- | 一次拿全库 schema(表名 + 列 + 行数 + 索引 + 统计)。
|
||||
doListCatalog :: IO (Either String [TableInfo])
|
||||
doListCatalog = ask "list_catalog" ReqListCatalog $ \resp -> case resp of
|
||||
RespCatalog xs -> Just xs
|
||||
_ -> Nothing
|
||||
|
||||
-- | 发 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"
|
||||
-- | 按某一列的索引取一行。
|
||||
-- 返回 `NoIndex` 表示"这个列上没有索引",上层据此退回全表扫描。
|
||||
doLookupByColumn :: String -> String -> Int -> IO (Either String IndexResult)
|
||||
doLookupByColumn t c k = ask "lookup_by_index" (ReqLookupByColumn t c k) $ \resp -> case resp of
|
||||
RespRows [] -> Just (IndexRow Nothing)
|
||||
RespRows (r : _) -> Just (IndexRow (Just r))
|
||||
RespNoIndex -> Just NoIndex
|
||||
_ -> Nothing
|
||||
|
||||
-- | 发 DescribeTable
|
||||
doDescribeTable :: String -> IO (Either String ([SchemaColumn], Int))
|
||||
doDescribeTable t = do
|
||||
resp <- sendRequest (ReqDescribeTable t)
|
||||
pure $ case resp of
|
||||
RespSchema cols n -> Right (cols, n)
|
||||
RespError e -> Left e
|
||||
_ -> Left "unexpected response to describe_table"
|
||||
doDescribeTable :: String -> IO (Either String TableInfo)
|
||||
doDescribeTable t = ask "describe_table" (ReqDescribeTable t) $ \resp -> case resp of
|
||||
RespSchema info -> Just info
|
||||
_ -> Nothing
|
||||
|
||||
-- | 给某一列建索引
|
||||
doCreateIndex :: String -> String -> IO (Either String ())
|
||||
doCreateIndex t c = ask "create_index" (ReqCreateIndex t c) okOnly
|
||||
|
||||
-- | 去掉某一列的索引
|
||||
doDropIndex :: String -> String -> IO (Either String ())
|
||||
doDropIndex t c = ask "drop_index" (ReqDropIndex t c) okOnly
|
||||
|
||||
-- | 发 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"
|
||||
doCreateTable t cols = ask "create_table" (ReqCreateTable t cols) okOnly
|
||||
|
||||
-- | 发 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"
|
||||
doDropTable t = ask "drop_table" (ReqDropTable t) okOnly
|
||||
|
||||
-- * 表结构
|
||||
|
||||
@@ -375,14 +426,20 @@ instance MonadStorage IPCStorage where
|
||||
-- \| 发 Scan
|
||||
scan t = IPCStorage (doScan t)
|
||||
|
||||
-- \| 发 Insert
|
||||
insert t r mk = IPCStorage (doInsertWith t r mk)
|
||||
-- \| 发 Insert(索引由存储层自己维护)
|
||||
insert t r = IPCStorage (doInsert t r)
|
||||
|
||||
-- \| 一批行一次请求:N 行只落一次盘
|
||||
insertMany t rs = IPCStorage (doInsertMany t rs)
|
||||
|
||||
-- \| 按 id 批量删行
|
||||
deleteKeys t ks = IPCStorage (doDeleteKeys t ks)
|
||||
|
||||
-- \| 发 ReplaceAll
|
||||
replaceAll t rs = IPCStorage (doReplaceAll t rs)
|
||||
|
||||
-- \| 发 LookupByIndex
|
||||
lookupByKey t k = IPCStorage (doLookupByKey t k)
|
||||
-- \| 按某一列的索引取一行(没有索引就回 NoIndex)
|
||||
lookupByColumn t c k = IPCStorage (doLookupByColumn t c k)
|
||||
|
||||
-- \| 发 CreateTable
|
||||
createTable name cols = IPCStorage (doCreateTable name (map toWire cols))
|
||||
@@ -395,6 +452,12 @@ instance MonadStorage IPCStorage where
|
||||
-- \| 发 DropTable
|
||||
dropTable name = IPCStorage (doDropTable name)
|
||||
|
||||
-- \| 建索引
|
||||
createIndex t c = IPCStorage (doCreateIndex t c)
|
||||
|
||||
-- \| 删索引
|
||||
dropIndex t c = IPCStorage (doDropIndex t c)
|
||||
|
||||
-- \| 列表 + 逐表扫描拼库
|
||||
snapshot = IPCStorage $ do
|
||||
result <- doListCatalog
|
||||
@@ -403,6 +466,19 @@ instance MonadStorage IPCStorage where
|
||||
Right xs -> mapM loadEntry xs
|
||||
where
|
||||
-- \| 加载一张表(schema 来自 catalog,行来自 scan)
|
||||
loadEntry (t, cols, _n) = do
|
||||
rows <- doScan t
|
||||
pure (t, Table t (schemaToColumns cols) (either (const []) id rows))
|
||||
loadEntry info = do
|
||||
rows <- doScan (tiTable info)
|
||||
pure
|
||||
( tiTable info
|
||||
, Table (tiTable info) (schemaToColumns (tiColumns info)) (either (const []) id rows)
|
||||
)
|
||||
|
||||
-- \| 只问数据字典要结构,一行数据都不拉过来
|
||||
schema = IPCStorage $ do
|
||||
result <- doListCatalog
|
||||
pure $ case result of
|
||||
Left _ -> []
|
||||
Right xs ->
|
||||
[ (tiTable i, Table (tiTable i) (schemaToColumns (tiColumns i)) [])
|
||||
| i <- xs
|
||||
]
|
||||
@@ -2,9 +2,11 @@ module ChuSQL.Storage.Memory (MemoryStorage (runMemoryStorage)) where
|
||||
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage
|
||||
import Data.List (find)
|
||||
|
||||
-- 内存实现:状态是 Database,错误通道是 Either String。
|
||||
--
|
||||
-- 它只实现"必须有"的那几个:批量插入、行级删除、索引、统计全走类的默认实现
|
||||
-- (内存里数据本来就在手上,扫一遍就是最快的做法)。
|
||||
|
||||
-- * 实例
|
||||
|
||||
@@ -39,7 +41,7 @@ instance MonadStorage MemoryStorage where
|
||||
Right (Right (tableRows tbl), db)
|
||||
|
||||
-- \| 追加一行
|
||||
insert t r _ = MemoryStorage $ \db -> do
|
||||
insert t r = MemoryStorage $ \db -> do
|
||||
tbl <- lookupTable db t
|
||||
Right (Right (), replaceTable t tbl{tableRows = tableRows tbl ++ [r]} db)
|
||||
|
||||
@@ -48,11 +50,6 @@ instance MonadStorage MemoryStorage where
|
||||
tbl <- lookupTable db t
|
||||
Right (Right (), replaceTable t tbl{tableRows = rows} db)
|
||||
|
||||
-- \| 按主键找第一行
|
||||
lookupByKey t k = MemoryStorage $ \db -> do
|
||||
tbl <- lookupTable db t
|
||||
Right (Right (findRow tbl k), db)
|
||||
|
||||
-- \| 建空表;已存在报错
|
||||
createTable name cols = MemoryStorage $ \db ->
|
||||
if any ((== name) . fst) db
|
||||
@@ -70,14 +67,8 @@ instance MonadStorage MemoryStorage where
|
||||
|
||||
-- * 工具
|
||||
|
||||
-- | 按 id 列找一行
|
||||
findRow :: Table -> Int -> Maybe Row
|
||||
findRow tbl k = find matches (tableRows tbl)
|
||||
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))
|
||||
|
||||
@@ -6,7 +6,7 @@ import ChuSQL.Model (Column (..))
|
||||
|
||||
-- * 语句
|
||||
|
||||
-- | 一条语句:查 / 加 / 删 / 改 / 建表 / 删表
|
||||
-- | 一条语句:查 / 加 / 删 / 改 / 建表 / 删表 / 建索引 / 删索引
|
||||
data Statement
|
||||
= Select
|
||||
{ selectCols :: [String]
|
||||
@@ -15,11 +15,15 @@ data Statement
|
||||
, selectOrderBy :: [(String, SortDir)]
|
||||
, selectLimit :: Maybe Int
|
||||
}
|
||||
| Insert String [String] [Expr]
|
||||
| -- \| 表、列、若干行(`VALUES (...), (...)` 可以一次给多行)
|
||||
Insert String [String] [[Expr]]
|
||||
| Delete String (Maybe Expr)
|
||||
| Update String [(String, Expr)] (Maybe Expr)
|
||||
| CreateTable String [(String, Column)]
|
||||
| DropTable String
|
||||
| -- \| 给某一列建索引(列名即索引名)
|
||||
CreateIndex String String
|
||||
| DropIndex String String
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 数据来源:单表,或两表拼接
|
||||
|
||||
@@ -171,6 +171,8 @@ reservedWords =
|
||||
, "set"
|
||||
, "create"
|
||||
, "table"
|
||||
, "index"
|
||||
, "drop"
|
||||
]
|
||||
|
||||
{- | 表别名。别名不能是保留字:否则 @FROM users WHERE age > 18@ 会把 @WHERE@
|
||||
@@ -232,6 +234,26 @@ dropTableStatement = do
|
||||
name <- identifier
|
||||
return (DropTable name)
|
||||
|
||||
-- | 读 CREATE INDEX(列名即索引名:一列最多一个索引)
|
||||
createIndexStatement :: Parser Statement
|
||||
createIndexStatement = do
|
||||
keyword "create"
|
||||
keyword "index"
|
||||
keyword "on"
|
||||
tbl <- identifier
|
||||
col <- between (symbol "(") (symbol ")") identifier
|
||||
return (CreateIndex tbl col)
|
||||
|
||||
-- | 读 DROP INDEX
|
||||
dropIndexStatement :: Parser Statement
|
||||
dropIndexStatement = do
|
||||
keyword "drop"
|
||||
keyword "index"
|
||||
keyword "on"
|
||||
tbl <- identifier
|
||||
col <- between (symbol "(") (symbol ")") identifier
|
||||
return (DropIndex tbl col)
|
||||
|
||||
-- | 建表时的一列
|
||||
columnDef :: Parser (String, Column)
|
||||
columnDef = do
|
||||
@@ -270,7 +292,7 @@ selectStatement = do
|
||||
, selectLimit = mLimit
|
||||
}
|
||||
|
||||
-- | 读 INSERT
|
||||
-- | 读 INSERT:`VALUES (...)` 后面可以跟多个括号,一次插多行
|
||||
insertStatement :: Parser Statement
|
||||
insertStatement = do
|
||||
keyword "INSERT"
|
||||
@@ -282,12 +304,12 @@ insertStatement = do
|
||||
(symbol ")")
|
||||
(sepBy1 identifier (symbol ","))
|
||||
keyword "VALUES"
|
||||
vals <-
|
||||
between
|
||||
(symbol "(")
|
||||
(symbol ")")
|
||||
(sepBy1 atom (symbol ","))
|
||||
return (Insert tbl cols vals)
|
||||
rows <- sepBy1 valueRow (symbol ",")
|
||||
return (Insert tbl cols rows)
|
||||
|
||||
-- | 一行值:一对括号里的若干字面量
|
||||
valueRow :: Parser [Expr]
|
||||
valueRow = between (symbol "(") (symbol ")") (sepBy1 atom (symbol ","))
|
||||
|
||||
-- | 读 DELETE
|
||||
deleteStatement :: Parser Statement
|
||||
@@ -316,11 +338,16 @@ parseStatement input =
|
||||
Left err -> Left (errorBundlePretty err)
|
||||
Right q -> Right q
|
||||
where
|
||||
-- \| 依次尝试各种语句
|
||||
-- \| 依次尝试各种语句。
|
||||
-- `CREATE` / `DROP` 后面跟的词决定是哪条语句,而 `keyword` 的 `try` 只能回溯到
|
||||
-- "这个词之前"——`CREATE` 一旦被吃掉就没得退了。所以带 `INDEX` 的那两个各自
|
||||
-- 用 `try` 包住并排在前面:`CREATE INDEX ...` 先试,退回来再试 `CREATE TABLE ...`。
|
||||
statementP =
|
||||
selectStatement
|
||||
<|> insertStatement
|
||||
<|> deleteStatement
|
||||
<|> updateStatement
|
||||
<|> createTableStatement
|
||||
<|> try createIndexStatement
|
||||
<|> try createTableStatement
|
||||
<|> try dropIndexStatement
|
||||
<|> dropTableStatement
|
||||
+169
-34
@@ -6,7 +6,7 @@ import ChuSQL.Algebra.Optimize (optimize, pushProject)
|
||||
import ChuSQL.Algebra.Planner (translate)
|
||||
import ChuSQL.Engine
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage (MonadStorage (..))
|
||||
import ChuSQL.Storage (IndexResult (..), MonadStorage (..))
|
||||
import ChuSQL.Storage.IPC (IPCStorage (runIPCStorage), Request (ReqPing), Response (RespPong), doListTables, sendRequest)
|
||||
import ChuSQL.Syntax.AST
|
||||
import ChuSQL.Syntax.Parser
|
||||
@@ -299,18 +299,18 @@ main = hspec $ do
|
||||
describe "ChuSQL.Engine (INSERT)" $ do
|
||||
it "parses a simple INSERT" $ do
|
||||
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
|
||||
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
|
||||
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
|
||||
case runStatement 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
|
||||
Right (db', _) -> do
|
||||
rowsOf (runStatement db' (makeSelect ["name", "age"] "users" Nothing))
|
||||
@@ -322,7 +322,7 @@ main = hspec $ do
|
||||
]
|
||||
|
||||
it "does not modify the original database" $ do
|
||||
case runStatement 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
|
||||
Right (_, _) -> do
|
||||
rowsOf (runStatement testDB (makeSelect ["name"] "users" Nothing))
|
||||
@@ -331,15 +331,15 @@ main = hspec $ do
|
||||
Left _ -> False
|
||||
|
||||
it "returns Left when column count does not match value count" $ do
|
||||
runStatement testDB (Insert "users" ["name", "age"] [LitStr "Dave"])
|
||||
runStatement testDB (Insert "users" ["name", "age"] [[LitStr "Dave"]])
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
it "returns Left for an unknown table" $ do
|
||||
runStatement testDB (Insert "nonexistent" ["name"] [LitStr "X"])
|
||||
runStatement testDB (Insert "nonexistent" ["name"] [[LitStr "X"]])
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
it "returns Left when a value has the wrong type for comparison" $ do
|
||||
runStatement testDB (Insert "users" ["name"] [Col "other"])
|
||||
runStatement testDB (Insert "users" ["name"] [[Col "other"]])
|
||||
`shouldSatisfy` isLeft
|
||||
it "writes id column to index and queries it back" $ do
|
||||
case parseStatement "INSERT INTO users (id, name, age) VALUES (42, 'Zoe', 20)" >>= runStatement testDB of
|
||||
@@ -347,6 +347,49 @@ main = hspec $ do
|
||||
Right (db', _) -> do
|
||||
rowsOf (parseStatement "SELECT name FROM users WHERE id = 42" >>= runStatement db')
|
||||
`shouldBe` Right [[("name", VStr "Zoe")]]
|
||||
|
||||
it "parses multi-row INSERT into one statement" $ do
|
||||
parseStatement "INSERT INTO users (id, name) VALUES (10, 'A'), (11, 'B'), (12, 'C')"
|
||||
`shouldBe` Right
|
||||
( Insert
|
||||
"users"
|
||||
["id", "name"]
|
||||
[ [LitInt 10, LitStr "A"]
|
||||
, [LitInt 11, LitStr "B"]
|
||||
, [LitInt 12, LitStr "C"]
|
||||
]
|
||||
)
|
||||
|
||||
it "inserts all rows of a multi-row INSERT" $ do
|
||||
case parseStatement "INSERT INTO users (id, name, age) VALUES (7, 'A', 1), (8, 'B', 2)" >>= runStatement testDB of
|
||||
Left err -> expectationFailure err
|
||||
Right (db', _) -> do
|
||||
rowsOf (parseStatement "SELECT name FROM users WHERE age < 3" >>= runStatement db')
|
||||
`shouldBe` Right [[("name", VStr "A")], [("name", VStr "B")]]
|
||||
-- 多行里只要有一行列数不对,整条都要拒绝
|
||||
runStatement testDB (Insert "users" ["id", "name"] [[LitInt 1, LitStr "A"], [LitInt 2]])
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
describe "ChuSQL.Engine (INDEX)" $ do
|
||||
it "parses CREATE INDEX and DROP INDEX" $ do
|
||||
parseStatement "CREATE INDEX ON users (age)"
|
||||
`shouldBe` Right (CreateIndex "users" "age")
|
||||
parseStatement "DROP INDEX ON users (age)"
|
||||
`shouldBe` Right (DropIndex "users" "age")
|
||||
|
||||
it "rejects an index on an unknown column" $ do
|
||||
(parseStatement "CREATE INDEX ON users (nope)" >>= runStatement testDB)
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
it "rejects an index on an unknown table" $ do
|
||||
(parseStatement "CREATE INDEX ON nope (age)" >>= runStatement testDB)
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
it "accepts index DDL in the memory storage (no-op there)" $ do
|
||||
rowsOf (parseStatement "CREATE INDEX ON users (age)" >>= runStatement testDB)
|
||||
`shouldBe` Right []
|
||||
rowsOf (parseStatement "DROP INDEX ON users (age)" >>= runStatement testDB)
|
||||
`shouldBe` Right []
|
||||
describe "ChuSQL.Engine (DELETE)" $ do
|
||||
it "parses DELETE with WHERE" $ do
|
||||
parseStatement "DELETE FROM users WHERE age < 18"
|
||||
@@ -961,10 +1004,27 @@ main = hspec $ 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)
|
||||
`shouldBe` Right (Lookup Nothing "users" "id" 2)
|
||||
it "keeps Lookup result identical to unoptimized" $ do
|
||||
sameResultAsUnoptimized "SELECT name FROM users WHERE id = 2"
|
||||
|
||||
it "rewrites an equality on any column into Lookup" $ do
|
||||
-- 能不能真走索引由存储层回答;优化器只负责说"这里可以点查"
|
||||
fmap planRoot (optimizedPlan "SELECT name FROM users WHERE age = 25")
|
||||
`shouldBe` Right (Lookup Nothing "users" "age" 25)
|
||||
|
||||
it "keeps a point lookup on a non-indexed column identical to unoptimized" $ do
|
||||
-- 内存实现没有索引,这条会走"退回扫描"的分支
|
||||
sameResultAsUnoptimized "SELECT name FROM users WHERE age = 25"
|
||||
|
||||
it "keeps an aliased point lookup identical to unoptimized" $ do
|
||||
-- 别名要留住:回来的一行必须和 Scan 别名 表 长得一样,不然投影取不到列
|
||||
sameResultAsUnoptimized "SELECT u.name FROM users u WHERE u.id = 1"
|
||||
|
||||
it "returns the row for an aliased point lookup" $ do
|
||||
rowsOf (parseStatement "SELECT u.name FROM users u WHERE u.id = 1" >>= runStatement testDB)
|
||||
`shouldBe` Right [[("u.name", VStr "Alice")]]
|
||||
|
||||
describe "ChuSQL.Semantic" $ do
|
||||
it "rejects an unknown column in WHERE" $ do
|
||||
(parseStatement "SELECT name FROM users WHERE nope > 18" >>= runStatement testDB)
|
||||
@@ -1108,7 +1168,7 @@ main = hspec $ 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 (Just 7)) `shouldReturn` Right ()
|
||||
runIPCStorage (insert "users" row) `shouldReturn` Right ()
|
||||
rows <- runIPCStorage (scan "users")
|
||||
(map sortRow <$> rows) `shouldBe` Right [sortRow row]
|
||||
|
||||
@@ -1119,7 +1179,7 @@ main = hspec $ do
|
||||
|
||||
it "scan returns all 20 inserted rows in order" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
mapM_ (\i -> runIPCStorage (insert "many" [("id", VInt i)] (Just i))) [1 .. 20 :: Int]
|
||||
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
|
||||
@@ -1127,35 +1187,35 @@ main = hspec $ do
|
||||
|
||||
it "replaceAll leaves only the new rows" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
runIPCStorage (insert "t" [("id", VInt 1)] (Just 1)) `shouldReturn` Right ()
|
||||
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
|
||||
it "lookupByColumn 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 (Just 7)) `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
|
||||
runIPCStorage (insert "keyed" row) `shouldReturn` Right ()
|
||||
found <- runIPCStorage (lookupByColumn "keyed" "id" 7)
|
||||
fmap sortRow (indexRow found) `shouldBe` Just (sortRow row)
|
||||
missing <- runIPCStorage (lookupByColumn "keyed" "id" 8)
|
||||
missing `shouldBe` Right (IndexRow Nothing)
|
||||
|
||||
it "replaceAll rebuilds the index over the pipe" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
runIPCStorage (insert "rb" [("id", VInt 1)] (Just 1)) `shouldReturn` Right ()
|
||||
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")]))
|
||||
old <- runIPCStorage (lookupByColumn "rb" "id" 1)
|
||||
old `shouldBe` Right (IndexRow Nothing)
|
||||
new <- runIPCStorage (lookupByColumn "rb" "id" 9)
|
||||
fmap sortRow (indexRow new)
|
||||
`shouldBe` 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)] (Just 1)) `shouldReturn` Right ()
|
||||
runIPCStorage (insert "b" [("id", VInt 2)] (Just 2)) `shouldReturn` Right ()
|
||||
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]
|
||||
@@ -1163,8 +1223,8 @@ main = hspec $ do
|
||||
|
||||
it "doListTables returns the tables that exist" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
runIPCStorage (insert "t1" [("id", VInt 1)] (Just 1)) `shouldReturn` Right ()
|
||||
runIPCStorage (insert "t2" [("id", VInt 2)] (Just 2)) `shouldReturn` Right ()
|
||||
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
|
||||
@@ -1174,7 +1234,7 @@ main = hspec $ do
|
||||
Right bin -> do
|
||||
srv1 <- startServer bin
|
||||
let dir = dataDir srv1
|
||||
withServerEnv srv1 $ runIPCStorage (insert "persist" [("id", VInt 42)] (Just 42)) `shouldReturn` Right ()
|
||||
withServerEnv srv1 $ runIPCStorage (insert "persist" [("id", VInt 42)]) `shouldReturn` Right ()
|
||||
stopServer srv1
|
||||
srv2 <- startServerAt bin dir
|
||||
rows <- withServerEnv srv2 $ runIPCStorage (scan "persist")
|
||||
@@ -1196,7 +1256,7 @@ main = hspec $ do
|
||||
_ <-
|
||||
runIPCStorage
|
||||
( runStatementM
|
||||
(Insert "ipc_idx" ["id", "name"] [LitInt 42, LitStr "Zoe"])
|
||||
(Insert "ipc_idx" ["id", "name"] [[LitInt 42, LitStr "Zoe"]])
|
||||
)
|
||||
result <-
|
||||
runIPCStorage
|
||||
@@ -1211,6 +1271,76 @@ main = hspec $ do
|
||||
)
|
||||
)
|
||||
result `shouldBe` Right [[("name", VStr "Zoe")]]
|
||||
|
||||
it "CREATE INDEX makes a query on that column use the index" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
_ <- runIPCStorage (runStatementM (CreateTable "ix_t" [("id", TInt), ("code", TInt)]))
|
||||
_ <-
|
||||
runIPCStorage
|
||||
( runStatementM
|
||||
(Insert "ix_t" ["id", "code"] [[LitInt 1, LitInt 500], [LitInt 2, LitInt 501]])
|
||||
)
|
||||
|
||||
-- 还没建索引:存储层明确回"我帮不上忙",上层会退回扫描
|
||||
withoutIndex <- runIPCStorage (lookupByColumn "ix_t" "code" 501)
|
||||
withoutIndex `shouldBe` Right NoIndex
|
||||
scanned <- runIPCStorage (runStatementM (makeSelect ["code"] "ix_t" (Just (Eq (Col "code") (LitInt 501)))))
|
||||
scanned `shouldBe` Right [[("code", VInt 501)]]
|
||||
|
||||
-- 建索引之后就真的走索引了
|
||||
created <- runIPCStorage (runStatementM (CreateIndex "ix_t" "code"))
|
||||
created `shouldBe` Right []
|
||||
withIndex <- runIPCStorage (lookupByColumn "ix_t" "code" 501)
|
||||
fmap sortRow (indexRow withIndex)
|
||||
`shouldBe` Just (sortRow [("id", VInt 2), ("code", VInt 501)])
|
||||
indexed <- runIPCStorage (runStatementM (makeSelect ["code"] "ix_t" (Just (Eq (Col "code") (LitInt 501)))))
|
||||
indexed `shouldBe` Right [[("code", VInt 501)]]
|
||||
|
||||
-- 建完索引之后插入的行也要进索引
|
||||
_ <- runIPCStorage (runStatementM (Insert "ix_t" ["id", "code"] [[LitInt 3, LitInt 502]]))
|
||||
fresh <- runIPCStorage (lookupByColumn "ix_t" "code" 502)
|
||||
fmap sortRow (indexRow fresh)
|
||||
`shouldBe` Just (sortRow [("id", VInt 3), ("code", VInt 502)])
|
||||
|
||||
it "CREATE INDEX over the pipe rejects a column with duplicates" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
_ <- runIPCStorage (runStatementM (CreateTable "dup_ix" [("id", TInt), ("age", TInt)]))
|
||||
_ <-
|
||||
runIPCStorage
|
||||
( runStatementM
|
||||
(Insert "dup_ix" ["id", "age"] [[LitInt 1, LitInt 30], [LitInt 2, LitInt 30]])
|
||||
)
|
||||
res <- runIPCStorage (runStatementM (CreateIndex "dup_ix" "age"))
|
||||
res `shouldSatisfy` isLeft
|
||||
|
||||
it "DELETE removes only the matching rows over the pipe" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
_ <- runIPCStorage (runStatementM (CreateTable "del_t" [("id", TInt), ("name", TStr)]))
|
||||
_ <-
|
||||
runIPCStorage
|
||||
( runStatementM
|
||||
( Insert
|
||||
"del_t"
|
||||
["id", "name"]
|
||||
[[LitInt 1, LitStr "A"], [LitInt 2, LitStr "B"], [LitInt 3, LitStr "C"]]
|
||||
)
|
||||
)
|
||||
_ <- runIPCStorage (runStatementM (Delete "del_t" (Just (Eq (Col "id") (LitInt 2)))))
|
||||
rows <- runIPCStorage (runStatementM (makeSelect ["name"] "del_t" Nothing))
|
||||
rows `shouldBe` Right [[("name", VStr "A")], [("name", VStr "C")]]
|
||||
gone <- runIPCStorage (lookupByColumn "del_t" "id" 2)
|
||||
gone `shouldBe` Right (IndexRow Nothing)
|
||||
|
||||
it "multi-row INSERT goes over the pipe in one statement" $ do
|
||||
withTestServer $ \srv -> withServerEnv srv $ do
|
||||
_ <- runIPCStorage (runStatementM (CreateTable "many_t" [("id", TInt)]))
|
||||
_ <-
|
||||
runIPCStorage
|
||||
( runStatementM
|
||||
(Insert "many_t" ["id"] [[LitInt i] | i <- [1 .. 50]])
|
||||
)
|
||||
rows <- runIPCStorage (runStatementM (makeSelect ["id"] "many_t" Nothing))
|
||||
fmap length rows `shouldBe` Right 50
|
||||
it "parses CREATE TABLE with two columns" $ do
|
||||
parseStatement "CREATE TABLE users (id INT, name TEXT)"
|
||||
`shouldBe` Right (CreateTable "users" [("id", TInt), ("name", TStr)])
|
||||
@@ -1392,6 +1522,11 @@ withPipeName name act = do
|
||||
sortRow :: Row -> Row
|
||||
sortRow = sortOn fst
|
||||
|
||||
-- | 从一次索引点查的结果里取出那一行(没索引、没查到都给 Nothing)
|
||||
indexRow :: Either String IndexResult -> Maybe Row
|
||||
indexRow (Right (IndexRow r)) = r
|
||||
indexRow _ = Nothing
|
||||
|
||||
isLeft :: Either a b -> Bool
|
||||
isLeft (Left _) = True
|
||||
isLeft (Right _) = False
|
||||
@@ -1428,7 +1563,7 @@ firstFilterCond (Join l r _) = case firstFilterCond l of
|
||||
Just p -> Just p
|
||||
Nothing -> firstFilterCond r
|
||||
firstFilterCond (Scan _ _) = Nothing
|
||||
firstFilterCond (Lookup _ _) = Nothing
|
||||
firstFilterCond (Lookup _ _ _ _) = Nothing
|
||||
|
||||
anyFilter :: RelOp -> Bool
|
||||
anyFilter (Filter _ _) = True
|
||||
@@ -1437,7 +1572,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
|
||||
anyFilter (Lookup _ _ _ _) = False
|
||||
|
||||
stripProjects :: RelOp -> RelOp
|
||||
stripProjects (Project _ x) = stripProjects x
|
||||
@@ -1446,7 +1581,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
|
||||
stripProjects (Lookup a t c k) = Lookup a t c k
|
||||
|
||||
projectedPlan :: String -> Either String RelOp
|
||||
projectedPlan sql = do
|
||||
|
||||
Reference in new issue
Block a user