优化和收尾

This commit is contained in:
LIm1tZz7 committed 2026-09-25 14:21:55 +08:00
1 parent 7f5bcc289e
commit 9e8c5f1ef0
33 files changed
+3332 -598

No files matched your search

+2
View File
@@ -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
+72 -22
View File
@@ -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
+6 -2
View File
@@ -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
+8 -4
View File
@@ -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
+52 -26
View File
@@ -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
+21 -1
View File
@@ -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)
+17 -2
View File
@@ -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
+68 -7
View File
@@ -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 = []})
+168 -92
View File
@@ -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
]
+5 -14
View File
@@ -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 -2
View File
@@ -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)
-- | 数据来源:单表,或两表拼接
+36 -9
View File
@@ -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
View File
@@ -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