使用React js完成web前端
This commit is contained in:
1 parent
9e8c5f1ef0
commit
7600f0f9ed
130 files changed
+12708
-788
No files matched your search
@@ -11,8 +11,6 @@ import qualified Data.HashMap.Strict as HM
|
||||
|
||||
-- 执行:按算子树算出结果行(Scan 阶段会加别名前缀)。
|
||||
|
||||
-- * 工具
|
||||
|
||||
-- | 只保留清单里的列
|
||||
project :: [String] -> Row -> Row
|
||||
project ["*"] row = row
|
||||
@@ -28,13 +26,10 @@ evalScan db mAlias tbl = do
|
||||
table <- lookupTable db tbl
|
||||
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
|
||||
@@ -55,12 +50,10 @@ equiKeys lrows rrows (Eq (Col a) (Col b))
|
||||
rcols = rowsCols rrows
|
||||
equiKeys _ _ _ = Nothing
|
||||
|
||||
-- | 哈希连接:右表建哈希表,左表逐行探测。
|
||||
-- 输出顺序与嵌套循环一致(左表顺序为主,右表顺序为辅)。
|
||||
-- | 哈希连接,输出顺序同嵌套循环
|
||||
hashJoin :: EquiKeys -> [Row] -> [Row] -> [Row]
|
||||
hashJoin keys lrows rrows = concatMap probe lrows
|
||||
where
|
||||
-- 同一键的右行按原顺序排在桶里
|
||||
buckets =
|
||||
HM.fromListWith
|
||||
(flip (++))
|
||||
@@ -88,12 +81,9 @@ 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
|
||||
-- 纯求值没有索引可问:退回"扫描 + 按条件筛",和未优化时的 Filter 完全等价
|
||||
evalRelOp db (Lookup mAlias tbl col k) = do
|
||||
rows <- evalScan db mAlias tbl
|
||||
filterM (evalCondForRow (lookupCondition mAlias col k)) rows
|
||||
@@ -114,7 +104,7 @@ evalRelOp db (Join left right cond) = do
|
||||
rrows <- evalRelOp db right
|
||||
joinRows cond lrows rrows
|
||||
|
||||
-- | 单子求值:Lookup 先问存储有没有索引,没有就退回扫描
|
||||
-- | 单子求值:点查先问存储有无索引
|
||||
evalRelOpM :: (MonadStorage m) => RelOp -> m (Either String [Row])
|
||||
evalRelOpM (Scan mAlias t) = do
|
||||
result <- scan t
|
||||
@@ -127,7 +117,6 @@ 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
|
||||
|
||||
@@ -6,7 +6,6 @@ import Control.Monad (join)
|
||||
|
||||
-- 表达式求值:拿一行数据算出一个值。
|
||||
|
||||
-- * 求值
|
||||
-- | 在一行上求出表达式的值
|
||||
evalExpr :: Expr -> Row -> Either String Value
|
||||
evalExpr (Col name) row =
|
||||
@@ -20,7 +19,6 @@ evalExpr (Eq a b) row = liftBinOp eqOp a b row
|
||||
evalExpr (And a b) row = liftBinOp (boolOp (&&)) a b row
|
||||
evalExpr (Or a b) row = liftBinOp (boolOp (||)) a b row
|
||||
|
||||
-- * 列引用
|
||||
-- | 表达式用到了哪些列
|
||||
colsInExpr :: Expr -> [String]
|
||||
colsInExpr (Col c) = [c]
|
||||
@@ -39,7 +37,6 @@ evalCondForRow e row = do
|
||||
VBool b -> Right b
|
||||
_ -> Left "type error: WHERE condition must be a boolean"
|
||||
|
||||
-- * 内部
|
||||
-- | 把二元运算提升到 Either
|
||||
liftBinOp ::
|
||||
(Value -> Value -> Either String Value) ->
|
||||
|
||||
@@ -4,14 +4,9 @@ import ChuSQL.Syntax.AST (Expr, SortDir)
|
||||
|
||||
-- 关系代数算子,以及把算子树渲染成文本。
|
||||
|
||||
-- | 算子树的一个节点
|
||||
data RelOp
|
||||
= Scan (Maybe String) String
|
||||
| -- \| 按某一列的值点查一行:别名、表、列、值。
|
||||
-- 只是"这里可以走点查"的意思,到底有没有索引由存储层回答
|
||||
-- (那个列上没有索引就退回全表扫描,见 Eval)。
|
||||
-- 别名要留着,回来的一行才能和 `Scan 别名 表` 长得一样。
|
||||
Lookup (Maybe String) String String Int
|
||||
| Lookup (Maybe String) String String Int
|
||||
| Filter Expr RelOp
|
||||
| Project [String] RelOp
|
||||
| Sort [(String, SortDir)] RelOp
|
||||
|
||||
@@ -7,7 +7,6 @@ import ChuSQL.Syntax.AST (Expr (..))
|
||||
|
||||
-- 查询优化:谓词下推 + 投影下推 + 常量折叠,跑到不动点。
|
||||
|
||||
-- * 谓词下推
|
||||
-- | 算子会产出哪些列
|
||||
relOpCols :: Database -> RelOp -> [String]
|
||||
relOpCols db (Scan mAlias tbl) =
|
||||
@@ -73,8 +72,6 @@ pushOne _ (Filter p (Filter q x)) = Filter (And p q) x
|
||||
pushOne _ (Filter p (Sort spec x)) = Sort spec (Filter p x)
|
||||
pushOne _ op = op
|
||||
|
||||
|
||||
-- * 投影下推
|
||||
-- | 是否要全部列
|
||||
needsAll :: [String] -> Bool
|
||||
needsAll = elem allColumns
|
||||
@@ -106,8 +103,6 @@ pushProject db need (Join l r c)
|
||||
pushProject db need leaf@(Scan _ _) = pushProjectLeaf db need leaf
|
||||
pushProject db need leaf@(Lookup _ _ _ _) = pushProjectLeaf db need leaf
|
||||
|
||||
|
||||
-- * 常量折叠
|
||||
-- | 折一个表达式节点
|
||||
foldNode :: Expr -> Expr
|
||||
foldNode e
|
||||
@@ -127,14 +122,8 @@ foldConstants (And a b) = foldNode (And (foldConstants a) (foldConstants b))
|
||||
foldConstants (Or a b) = foldNode (Or (foldConstants a) (foldConstants b))
|
||||
foldConstants e = e
|
||||
|
||||
|
||||
-- * 执行优化
|
||||
-- | 单节点重写
|
||||
rewriteNode :: Database -> RelOp -> RelOp
|
||||
-- 单列等值条件改成点查:这里是"可以走索引"的意思,
|
||||
-- 到底走不走得成由存储层回答(这个列上没有索引就退回全表扫描,见 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
|
||||
|
||||
@@ -12,8 +12,6 @@ import ChuSQL.Syntax.AST
|
||||
|
||||
-- 引擎入口:语义检查 + 分发执行,以及内存实现与泛型版本。
|
||||
|
||||
-- * 对外入口
|
||||
|
||||
-- | 跑一条语句(内存实现)
|
||||
runStatement :: Database -> Statement -> Either String (Database, [Row])
|
||||
runStatement db q = do
|
||||
@@ -28,15 +26,11 @@ rowsOf = fmap snd
|
||||
-- | 泛型入口:先检查再执行
|
||||
runStatementM :: (MonadStorage m) => Statement -> m (Either String [Row])
|
||||
runStatementM q = do
|
||||
-- 只要结构:语义检查和查询优化都只看列,一行数据都不碰,
|
||||
-- 所以这里不用取整库快照(那会把所有行拉一遍)
|
||||
db <- schema
|
||||
case check db q of
|
||||
Left err -> pure (Left err)
|
||||
Right _ -> runStatementUncheckedM db q
|
||||
|
||||
-- * 分发执行
|
||||
|
||||
-- | 按语句类型分发(已检查过)
|
||||
runStatementUncheckedM :: (MonadStorage m) => Database -> Statement -> m (Either String [Row])
|
||||
runStatementUncheckedM db q@Select{} =
|
||||
@@ -44,7 +38,6 @@ runStatementUncheckedM db q@Select{} =
|
||||
Left e -> pure (Left e)
|
||||
Right relOp -> evalRelOpM (optimize db relOp)
|
||||
runStatementUncheckedM _ (Insert tbl cols rows) =
|
||||
-- 多行一次交给存储:N 行只落一次盘(键怎么算、索引怎么写是存储层的事)
|
||||
case mapM toRow rows of
|
||||
Left err -> pure (Left err)
|
||||
Right rs -> do
|
||||
@@ -65,8 +58,6 @@ runStatementUncheckedM _ (Delete tbl mWhere) = do
|
||||
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
|
||||
@@ -111,14 +102,15 @@ runStatementUncheckedM _ (CreateIndex tbl col) = do
|
||||
runStatementUncheckedM _ (DropIndex tbl col) = do
|
||||
result <- dropIndex tbl col
|
||||
pure (result >> Right [])
|
||||
runStatementUncheckedM _ (DropColumn tbl col) = do
|
||||
result <- dropColumn 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
|
||||
|
||||
@@ -6,31 +6,25 @@ import Data.Maybe (fromMaybe)
|
||||
|
||||
-- 数据模型:列类型、值、行、表、数据库,外加几个查表小工具。
|
||||
|
||||
-- * 类型
|
||||
-- | 列的类型
|
||||
data Column
|
||||
= TInt
|
||||
| TStr
|
||||
| TBool
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 列的值
|
||||
data Value
|
||||
= VInt Int
|
||||
| VStr String
|
||||
| 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)]
|
||||
|
||||
-- | 一张表
|
||||
data Table = Table
|
||||
{ tableName :: String
|
||||
, tableCols :: [(String, Column)]
|
||||
@@ -38,10 +32,8 @@ data Table = Table
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- | 数据库:多张表
|
||||
type Database = [(String, Table)]
|
||||
|
||||
-- * 工具
|
||||
-- | 取表的列名
|
||||
colNames :: Table -> [String]
|
||||
colNames = map fst . tableCols
|
||||
@@ -54,16 +46,15 @@ colType t c = lookup c (tableCols t)
|
||||
lookupTable :: Database -> String -> Either String Table
|
||||
lookupTable db t = maybe (Left ("unknown table: " ++ t)) Right (lookup t db)
|
||||
|
||||
-- | 投影哨兵 "*"
|
||||
allColumns :: String
|
||||
allColumns = "*"
|
||||
|
||||
-- | 给列名加别名前缀:`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)
|
||||
@@ -7,7 +7,6 @@ import Data.List (intercalate, nub)
|
||||
|
||||
-- 语义检查:表和列在不在、类型对不对,全部在执行前查。
|
||||
|
||||
-- * 环境
|
||||
-- | 给列名加上别名前缀
|
||||
prefixColumns :: Maybe String -> [(String, Column)] -> [(String, Column)]
|
||||
prefixColumns mAlias cols = [(prefix ++ c, ty) | (c, ty) <- cols]
|
||||
@@ -26,8 +25,6 @@ checkFrom db (FromJoin left mAlias tbl cond) = do
|
||||
checkBool "ON" env cond
|
||||
Right env
|
||||
|
||||
|
||||
-- * 检查
|
||||
-- | 拼“列不存在”的报错
|
||||
missingColumn :: String -> String -> [(String, Column)] -> String
|
||||
missingColumn place c env =
|
||||
@@ -44,7 +41,6 @@ checkColumns place env requested =
|
||||
[] -> Right ()
|
||||
(c : _) -> Left (missingColumn place c env)
|
||||
|
||||
|
||||
-- | 推导表达式的类型
|
||||
inferExpr :: String -> [(String, Column)] -> Expr -> Either String Column
|
||||
inferExpr place env (Col c) = maybe (Left (missingColumn place c env)) Right (lookup c env)
|
||||
@@ -80,8 +76,6 @@ checkBool place env e = do
|
||||
then Right ()
|
||||
else Left (place ++ ": condition must be a boolean, got " ++ show t)
|
||||
|
||||
|
||||
-- * 写入语句
|
||||
-- | 检查赋值列和类型
|
||||
checkTyped :: String -> [(String, Column)] -> Table -> (String, Expr) -> Either String ()
|
||||
checkTyped place env t (c, e) = do
|
||||
@@ -99,8 +93,6 @@ checkValue = checkTyped "INSERT" []
|
||||
checkAssign :: Table -> (String, Expr) -> Either String ()
|
||||
checkAssign t = checkTyped "UPDATE" (tableCols t) t
|
||||
|
||||
|
||||
-- * 入口
|
||||
-- | 按语句类型分派检查
|
||||
check :: Database -> Statement -> Either String ()
|
||||
check db q = case q of
|
||||
@@ -150,6 +142,17 @@ check db q = case q of
|
||||
if col `elem` tableCols' t
|
||||
then Right ()
|
||||
else Left ("DROP INDEX: unknown column: " ++ col)
|
||||
DropColumn tbl col -> do
|
||||
t <- lookupTable db tbl
|
||||
if col `notElem` tableCols' t
|
||||
then Left ("DROP COLUMN: unknown column: " ++ col)
|
||||
else
|
||||
if col == idColumn
|
||||
then Left "DROP COLUMN: the built-in id column cannot be dropped"
|
||||
else Right ()
|
||||
where
|
||||
-- \| 表的列名清单
|
||||
tableCols' = map fst . tableCols
|
||||
|
||||
idColumn :: String
|
||||
idColumn = "id"
|
||||
@@ -7,27 +7,19 @@ import ChuSQL.Model (Column, Database, Row, Table (..), Value (..))
|
||||
|
||||
-- 存储层抽象:上层只认 domain 类型,不关心底层实现。
|
||||
|
||||
-- | 一次索引点查的结果
|
||||
data IndexResult
|
||||
= -- \| 有索引,查过了:Just 命中一行,Nothing 没这行
|
||||
IndexRow (Maybe Row)
|
||||
| -- \| 没有这个索引 —— 调用方应该退回全表扫描
|
||||
NoIndex
|
||||
= IndexRow (Maybe Row)
|
||||
| NoIndex
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 存储接口:读写表、按索引取行、取快照
|
||||
class (Monad m) => MonadStorage m where
|
||||
-- \| 全表扫描
|
||||
scan :: String -> m (Either String [Row])
|
||||
|
||||
-- \| 追加一行。
|
||||
-- 要写哪些索引由**存储层按自己的元数据**决定(表上有哪些索引它自己清楚),
|
||||
-- 调用方不需要指定 key。
|
||||
-- \| 追加一行
|
||||
insert :: String -> Row -> m (Either String ())
|
||||
|
||||
-- \| 一批行一起追加。
|
||||
-- 默认逐行调 `insert`;能批量的实现(比如走 WAL 的 IPC 存储)会覆盖它,
|
||||
-- 好处是 N 行只落一次盘。
|
||||
-- \| 一批行一起追加
|
||||
insertMany :: String -> [Row] -> m (Either String ())
|
||||
insertMany t = go
|
||||
where
|
||||
@@ -39,9 +31,7 @@ class (Monad m) => MonadStorage m where
|
||||
Left e -> pure (Left e)
|
||||
Right _ -> go rs
|
||||
|
||||
-- \| 按 `id` 批量删行。
|
||||
-- 默认实现是"全读出来、筛掉、整表写回"(对内存实现足够);
|
||||
-- 真正能在原地删的实现会覆盖它。
|
||||
-- \| 按 id 批量删行
|
||||
deleteKeys :: String -> [Int] -> m (Either String ())
|
||||
deleteKeys t ks = do
|
||||
rows <- scan t
|
||||
@@ -57,8 +47,7 @@ class (Monad m) => MonadStorage m where
|
||||
-- \| 整表替换
|
||||
replaceAll :: String -> [Row] -> m (Either String ())
|
||||
|
||||
-- \| 按某一列的索引取一行。
|
||||
-- 默认回答"没有索引"(内存实现不需要索引),于是上层会自己扫一遍。
|
||||
-- \| 按某一列的索引取一行
|
||||
lookupByColumn :: String -> String -> Int -> m (Either String IndexResult)
|
||||
lookupByColumn _ _ _ = pure (Right NoIndex)
|
||||
|
||||
@@ -76,14 +65,13 @@ class (Monad m) => MonadStorage m where
|
||||
dropIndex :: String -> String -> m (Either String ())
|
||||
dropIndex _ _ = pure (Right ())
|
||||
|
||||
-- \| 删一列,连它的索引与数据一起去掉
|
||||
dropColumn :: String -> String -> m (Either String ())
|
||||
|
||||
-- \| 取全库快照(表名 + 列 + 全部行)
|
||||
snapshot :: m Database
|
||||
|
||||
-- \| 取全库**结构**:只要表名和列,不要行。
|
||||
--
|
||||
-- 语义检查和查询优化都只看列名/列类型,一行数据都不碰;
|
||||
-- 而"取全库快照"要把所有行拉一遍,在单语句路径上是白花的钱。
|
||||
-- 默认实现退化成整库快照再抹掉行,有更便宜做法的实现(比如走数据字典)可以覆盖。
|
||||
-- \| 取全库结构:只要表名和列,不要行
|
||||
schema :: m Database
|
||||
schema = fmap (map withoutRows) snapshot
|
||||
where
|
||||
|
||||
@@ -38,31 +38,22 @@ import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
-- IPC 存储实现:每个存储方法翻成管道上的一条 JSON 请求。
|
||||
|
||||
-- * 存储实现
|
||||
|
||||
-- | 命名管道上的存储
|
||||
newtype IPCStorage a = IPCStorage
|
||||
{ runIPCStorage :: IO a
|
||||
}
|
||||
|
||||
-- | Functor:转发给 IO
|
||||
instance Functor IPCStorage where
|
||||
fmap f (IPCStorage m) = IPCStorage (fmap f m)
|
||||
|
||||
-- | Applicative:转发给 IO
|
||||
instance Applicative IPCStorage where
|
||||
pure = IPCStorage . pure
|
||||
IPCStorage mf <*> IPCStorage ma = IPCStorage (mf <*> ma)
|
||||
|
||||
-- | Monad:顺序执行
|
||||
instance Monad IPCStorage where
|
||||
IPCStorage m >>= k = IPCStorage $ do
|
||||
a <- m
|
||||
runIPCStorage (k a)
|
||||
|
||||
-- * 全局连接缓存
|
||||
|
||||
-- | 全局句柄缓存:按管道路径缓存,路径变了就换连接。
|
||||
{-# NOINLINE connectionRef #-}
|
||||
connectionRef :: MVar (Maybe (FilePath, Handle))
|
||||
connectionRef = unsafePerformIO (newMVar Nothing)
|
||||
@@ -100,15 +91,13 @@ dropConnection = modifyMVar_ connectionRef $ \cached -> do
|
||||
closeConnection :: IO ()
|
||||
closeConnection = dropConnection
|
||||
|
||||
-- * 管道
|
||||
|
||||
-- | 管道路径
|
||||
pipePath :: IO String
|
||||
pipePath = do
|
||||
name <- maybe "chusql-storage" id <$> lookupEnv "CHUSQL_PIPE"
|
||||
pure ("\\\\.\\pipe\\" ++ name)
|
||||
|
||||
-- | 发一条请求读一条响应;管道不通就回 RespError,并丢掉缓存等下次重连。
|
||||
-- | 发一条请求,管道不通就重连
|
||||
sendRequest :: Request -> IO Response
|
||||
sendRequest req = do
|
||||
path <- pipePath
|
||||
@@ -132,29 +121,21 @@ roundTrip req path = do
|
||||
Left err -> pure (RespError ("decode: " ++ err))
|
||||
Right r -> pure r
|
||||
|
||||
-- * 编解码
|
||||
|
||||
-- | 一列的 schema
|
||||
data SchemaColumn = SchemaColumn
|
||||
{ scName :: String
|
||||
, scType :: String
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | SchemaColumn 编码
|
||||
instance ToJSON SchemaColumn where
|
||||
toJSON (SchemaColumn n t) = object ["name" .= n, "ty" .= t]
|
||||
|
||||
-- | SchemaColumn 解码
|
||||
instance FromJSON SchemaColumn where
|
||||
parseJSON = withObject "SchemaColumn" $ \o -> do
|
||||
n <- o .: "name"
|
||||
t <- o .: "ty"
|
||||
pure (SchemaColumn n t)
|
||||
|
||||
-- * 请求
|
||||
|
||||
-- | 请求类型
|
||||
data Request
|
||||
= ReqPing
|
||||
| ReqScan String
|
||||
@@ -169,9 +150,9 @@ data Request
|
||||
| ReqDropTable String
|
||||
| ReqCreateIndex String String
|
||||
| ReqDropIndex String String
|
||||
| ReqDropColumn String String
|
||||
| ReqListCatalog
|
||||
|
||||
-- | 请求编码
|
||||
instance ToJSON Request where
|
||||
toJSON ReqPing = object ["method" .= ("ping" :: T.Text)]
|
||||
toJSON (ReqScan t) = object ["method" .= ("scan" :: T.Text), "table" .= t]
|
||||
@@ -232,9 +213,14 @@ instance ToJSON Request where
|
||||
, "table" .= t
|
||||
, "column" .= c
|
||||
]
|
||||
toJSON (ReqDropColumn t c) =
|
||||
object
|
||||
[ "method" .= ("drop_column" :: T.Text)
|
||||
, "table" .= t
|
||||
, "column" .= c
|
||||
]
|
||||
toJSON ReqListCatalog = object ["method" .= ("list_catalog" :: T.Text)]
|
||||
|
||||
-- | 一张表的线上信息:列 + 行数 + 索引 + 列统计
|
||||
data TableInfo = TableInfo
|
||||
{ tiTable :: String
|
||||
, tiColumns :: [SchemaColumn]
|
||||
@@ -244,7 +230,6 @@ data TableInfo = TableInfo
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 响应类型
|
||||
data Response
|
||||
= RespPong
|
||||
| RespRows [Row]
|
||||
@@ -255,7 +240,6 @@ data Response
|
||||
| RespError String
|
||||
| RespCatalog [TableInfo]
|
||||
|
||||
-- | 响应解码
|
||||
instance FromJSON Response where
|
||||
parseJSON = withObject "Response" $ \o -> do
|
||||
status <- o .: "status"
|
||||
@@ -272,7 +256,7 @@ instance FromJSON Response where
|
||||
RespCatalog <$> mapM parseTable xs
|
||||
other -> fail ("unknown status: " ++ T.unpack other)
|
||||
where
|
||||
-- \| 解一张表的 schema(新字段都给了默认值,老响应也能解)
|
||||
-- \| 解一张表的 schema
|
||||
parseTable :: A.Value -> Parser TableInfo
|
||||
parseTable = withObject "TableInfo" $ \o -> do
|
||||
t <- o .: "table"
|
||||
@@ -323,12 +307,7 @@ rowFromJSON (A.Object o) = mapM toPair (KM.toList o)
|
||||
pure (K.toString k, val)
|
||||
rowFromJSON _ = fail "row must be a JSON object"
|
||||
|
||||
-- * 内部封装
|
||||
|
||||
-- | 发一条请求,把响应翻成 Either。
|
||||
--
|
||||
-- 每个操作的区别只有两处:发什么请求、认哪种响应。所以固定套路收在这里:
|
||||
-- 对面报错 → `Left`;认得这种响应 → `Right`;其余一律算协议错(比如对面版本对不上)。
|
||||
-- | 发请求并把响应翻成 Either
|
||||
ask :: String -> Request -> (Response -> Maybe a) -> IO (Either String a)
|
||||
ask what req recognize = do
|
||||
resp <- sendRequest req
|
||||
@@ -349,7 +328,7 @@ doScan t = ask "scan" (ReqScan t) $ \resp -> case resp of
|
||||
RespRows rows -> Just rows
|
||||
_ -> Nothing
|
||||
|
||||
-- | 发 Insert(索引由存储层自己维护,这里不传 key)
|
||||
-- | 发 Insert(索引由存储层维护)
|
||||
doInsert :: String -> Row -> IO (Either String ())
|
||||
doInsert t r = ask "insert" (ReqInsert t r) okOnly
|
||||
|
||||
@@ -371,14 +350,13 @@ doListTables = ask "list_tables" ReqListTables $ \resp -> case resp of
|
||||
RespTables ts -> Just ts
|
||||
_ -> Nothing
|
||||
|
||||
-- | 一次拿全库 schema(表名 + 列 + 行数 + 索引 + 统计)。
|
||||
-- | 一次拿全库 schema
|
||||
doListCatalog :: IO (Either String [TableInfo])
|
||||
doListCatalog = ask "list_catalog" ReqListCatalog $ \resp -> case resp of
|
||||
RespCatalog xs -> Just xs
|
||||
_ -> Nothing
|
||||
|
||||
-- | 按某一列的索引取一行。
|
||||
-- 返回 `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)
|
||||
@@ -400,6 +378,10 @@ 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
|
||||
|
||||
-- | 删一列(列定义、这一列上的索引、每行里的那一格一起没)
|
||||
doDropColumn :: String -> String -> IO (Either String ())
|
||||
doDropColumn t c = ask "drop_column" (ReqDropColumn t c) okOnly
|
||||
|
||||
-- | 发 CreateTable
|
||||
doCreateTable :: String -> [SchemaColumn] -> IO (Either String ())
|
||||
doCreateTable t cols = ask "create_table" (ReqCreateTable t cols) okOnly
|
||||
@@ -408,8 +390,6 @@ doCreateTable t cols = ask "create_table" (ReqCreateTable t cols) okOnly
|
||||
doDropTable :: String -> IO (Either String ())
|
||||
doDropTable t = ask "drop_table" (ReqDropTable t) okOnly
|
||||
|
||||
-- * 表结构
|
||||
|
||||
-- | 线上 schema 转本地列
|
||||
schemaToColumns :: [SchemaColumn] -> [(String, Column)]
|
||||
schemaToColumns = map go
|
||||
@@ -419,9 +399,6 @@ schemaToColumns = map go
|
||||
go (SchemaColumn n "bool") = (n, TBool)
|
||||
go (SchemaColumn n _) = (n, TStr)
|
||||
|
||||
-- * MonadStorage 实例
|
||||
|
||||
-- | 存储方法全走管道
|
||||
instance MonadStorage IPCStorage where
|
||||
-- \| 发 Scan
|
||||
scan t = IPCStorage (doScan t)
|
||||
@@ -458,6 +435,9 @@ instance MonadStorage IPCStorage where
|
||||
-- \| 删索引
|
||||
dropIndex t c = IPCStorage (doDropIndex t c)
|
||||
|
||||
-- \| 删一列
|
||||
dropColumn t c = IPCStorage (doDropColumn t c)
|
||||
|
||||
-- \| 列表 + 逐表扫描拼库
|
||||
snapshot = IPCStorage $ do
|
||||
result <- doListCatalog
|
||||
@@ -465,7 +445,7 @@ instance MonadStorage IPCStorage where
|
||||
Left _ -> pure []
|
||||
Right xs -> mapM loadEntry xs
|
||||
where
|
||||
-- \| 加载一张表(schema 来自 catalog,行来自 scan)
|
||||
-- \| 加载一张表,行来自 scan
|
||||
loadEntry info = do
|
||||
rows <- doScan (tiTable info)
|
||||
pure
|
||||
|
||||
@@ -3,23 +3,15 @@ module ChuSQL.Storage.Memory (MemoryStorage (runMemoryStorage)) where
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Storage
|
||||
|
||||
-- 内存实现:状态是 Database,错误通道是 Either String。
|
||||
--
|
||||
-- 它只实现"必须有"的那几个:批量插入、行级删除、索引、统计全走类的默认实现
|
||||
-- (内存里数据本来就在手上,扫一遍就是最快的做法)。
|
||||
-- 内存实现:Database 上的状态,错误通道是 Either String。
|
||||
|
||||
-- * 实例
|
||||
|
||||
-- | 内存存储:Database 上的状态
|
||||
newtype MemoryStorage a = MemoryStorage {runMemoryStorage :: Database -> Either String (a, Database)}
|
||||
|
||||
-- | Functor:转发给底层状态
|
||||
instance Functor MemoryStorage where
|
||||
fmap f (MemoryStorage m) = MemoryStorage $ \db -> do
|
||||
(a, db') <- m db
|
||||
Right (f a, db')
|
||||
|
||||
-- | Applicative:转发 pure 和 <*>
|
||||
instance Applicative MemoryStorage where
|
||||
pure x = MemoryStorage $ \db -> Right (x, db)
|
||||
MemoryStorage mf <*> MemoryStorage ma = MemoryStorage $ \db -> do
|
||||
@@ -27,13 +19,11 @@ instance Applicative MemoryStorage where
|
||||
(a, db2) <- ma db1
|
||||
Right (f a, db2)
|
||||
|
||||
-- | Monad:顺序执行
|
||||
instance Monad MemoryStorage where
|
||||
MemoryStorage m >>= k = MemoryStorage $ \db -> do
|
||||
(a, db') <- m db
|
||||
runMemoryStorage (k a) db'
|
||||
|
||||
-- | MonadStorage:直接改内存库
|
||||
instance MonadStorage MemoryStorage where
|
||||
-- \| 全表扫描
|
||||
scan t = MemoryStorage $ \db -> do
|
||||
@@ -62,11 +52,28 @@ instance MonadStorage MemoryStorage where
|
||||
then Right (Right (), filter ((/= name) . fst) db)
|
||||
else Right (Left ("unknown table: " ++ name), db)
|
||||
|
||||
-- \| 删一列:表定义里去掉这一列,每一行里的那一格也一起去掉
|
||||
dropColumn name col = MemoryStorage $ \db ->
|
||||
case lookup name db of
|
||||
Nothing -> Right (Left ("unknown table: " ++ name), db)
|
||||
Just tbl ->
|
||||
if col `notElem` map fst (tableCols tbl)
|
||||
then Right (Left ("unknown column: " ++ col), db)
|
||||
else
|
||||
Right
|
||||
( Right ()
|
||||
, replaceTable
|
||||
name
|
||||
tbl
|
||||
{ tableCols = filter ((/= col) . fst) (tableCols tbl)
|
||||
, tableRows = map (filter ((/= col) . fst)) (tableRows tbl)
|
||||
}
|
||||
db
|
||||
)
|
||||
|
||||
-- \| 原样返回当前库
|
||||
snapshot = MemoryStorage $ \db -> Right (db, db)
|
||||
|
||||
-- * 工具
|
||||
|
||||
-- | 用给定表替换同名表
|
||||
replaceTable :: String -> Table -> Database -> Database
|
||||
replaceTable name table =
|
||||
|
||||
@@ -4,9 +4,6 @@ import ChuSQL.Model (Column (..))
|
||||
|
||||
-- 语法树:语句、数据来源、条件表达式、排序方向。
|
||||
|
||||
-- * 语句
|
||||
|
||||
-- | 一条语句:查 / 加 / 删 / 改 / 建表 / 删表 / 建索引 / 删索引
|
||||
data Statement
|
||||
= Select
|
||||
{ selectCols :: [String]
|
||||
@@ -15,26 +12,21 @@ data Statement
|
||||
, selectOrderBy :: [(String, SortDir)]
|
||||
, selectLimit :: Maybe Int
|
||||
}
|
||||
| -- \| 表、列、若干行(`VALUES (...), (...)` 可以一次给多行)
|
||||
Insert String [String] [[Expr]]
|
||||
| Insert String [String] [[Expr]]
|
||||
| Delete String (Maybe Expr)
|
||||
| Update String [(String, Expr)] (Maybe Expr)
|
||||
| CreateTable String [(String, Column)]
|
||||
| DropTable String
|
||||
| -- \| 给某一列建索引(列名即索引名)
|
||||
CreateIndex String String
|
||||
| CreateIndex String String
|
||||
| DropIndex String String
|
||||
| DropColumn String String
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | 数据来源:单表,或两表拼接
|
||||
data FromClause
|
||||
= FromTable (Maybe String) String
|
||||
| FromJoin FromClause (Maybe String) String Expr
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- * 表达式
|
||||
|
||||
-- | 条件表达式
|
||||
data Expr
|
||||
= Col String
|
||||
| LitInt Int
|
||||
@@ -47,16 +39,11 @@ data Expr
|
||||
| Or Expr Expr
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- * 排序
|
||||
|
||||
-- | 排序方向
|
||||
data SortDir
|
||||
= Asc
|
||||
| Desc
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- * 构造
|
||||
|
||||
-- | 造一个最简单的 SELECT
|
||||
makeSelect :: [String] -> String -> Maybe Expr -> Statement
|
||||
makeSelect cols tbl w =
|
||||
|
||||
@@ -15,11 +15,8 @@ import Text.Megaparsec.Char.Lexer qualified as Lxr
|
||||
|
||||
-- SQL 解析:词法 + 语法,把文本变成语句树。
|
||||
|
||||
-- * 词法
|
||||
-- | 解析器类型
|
||||
type Parser = Parsec Void String
|
||||
|
||||
|
||||
-- | 跳过空白
|
||||
sc :: Parser ()
|
||||
sc = Lxr.space space1 empty empty
|
||||
@@ -32,9 +29,6 @@ lexeme = Lxr.lexeme sc
|
||||
symbol :: String -> Parser String
|
||||
symbol = Lxr.symbol sc
|
||||
|
||||
{- | 大小写不敏感的关键字;其后不能紧跟标识符字符,避免把 @selection@ 读成 @select@。
|
||||
整体用 'try' 包住:否则 @ORDER@ 会被 @keyword "or"@ 吃掉前缀 @OR@ 再报错,导致无法回溯。
|
||||
-}
|
||||
-- | 读关键字(不分大小写)
|
||||
keyword :: String -> Parser ()
|
||||
keyword k = lexeme $ try $ do
|
||||
@@ -64,9 +58,6 @@ stringLit = lexeme $ do
|
||||
_ <- char '\''
|
||||
return s
|
||||
|
||||
{- | 字符串内的一个字符:@''@ 折叠成一个单引号,其余字符(含反斜杠)原样保留。
|
||||
两个分支都不会在失败时消耗输入,因此可以安全地放进 'many'。
|
||||
-}
|
||||
-- | 字符串里的一个字符
|
||||
stringChar :: Parser Char
|
||||
stringChar =
|
||||
@@ -75,7 +66,6 @@ stringChar =
|
||||
, satisfy (/= '\'')
|
||||
]
|
||||
|
||||
-- * 子句
|
||||
-- | 升序或降序
|
||||
sortDir :: Parser SortDir
|
||||
sortDir = (Desc <$ keyword "desc") <|> (Asc <$ keyword "asc") <|> pure Asc
|
||||
@@ -94,31 +84,29 @@ qualifiedName = do
|
||||
return (intercalate "." (first : rest))
|
||||
|
||||
|
||||
-- * 表达式
|
||||
-- | 读一个表达式
|
||||
expr :: Parser Expr
|
||||
expr = makeExprParser atom operatorTable
|
||||
|
||||
{- | 运算符表。注意 'makeExprParser' 要求各层按优先级从高到低排列:
|
||||
比较运算符绑定最紧,'AND' 次之,'OR' 最松。
|
||||
-}
|
||||
-- | 运算符优先级(从紧到松)
|
||||
operatorTable :: [[Operator Parser Expr]]
|
||||
operatorTable =
|
||||
[
|
||||
[ InfixN (Gt <$ symbol ">") -- 最高优先级
|
||||
[ InfixN (Gt <$ symbol ">")
|
||||
, InfixN (Lt <$ symbol "<")
|
||||
, InfixN (Eq <$ symbol "=")
|
||||
]
|
||||
, [InfixL (And <$ keyword "AND")]
|
||||
, [InfixL (Or <$ keyword "OR")] -- 最低优先级
|
||||
, [InfixL (Or <$ keyword "OR")]
|
||||
]
|
||||
|
||||
-- | 最小的表达式单位
|
||||
atom :: Parser Expr
|
||||
atom =
|
||||
choice
|
||||
[ LitInt <$> integer
|
||||
[ LitBool True <$ keyword "TRUE"
|
||||
, LitBool False <$ keyword "FALSE"
|
||||
, LitInt <$> integer
|
||||
, LitStr <$> stringLit
|
||||
, Col <$> qualifiedName
|
||||
, between (symbol "(") (symbol ")") expr
|
||||
@@ -148,7 +136,6 @@ assignment = do
|
||||
return (col, e)
|
||||
|
||||
|
||||
-- | 保留字清单
|
||||
reservedWords :: [String]
|
||||
reservedWords =
|
||||
[ "select"
|
||||
@@ -173,12 +160,10 @@ reservedWords =
|
||||
, "table"
|
||||
, "index"
|
||||
, "drop"
|
||||
, "alter"
|
||||
, "column"
|
||||
]
|
||||
|
||||
{- | 表别名。别名不能是保留字:否则 @FROM users WHERE age > 18@ 会把 @WHERE@
|
||||
当成 @users@ 的别名吃掉,后面真正的 WHERE 子句就再也解析不到。
|
||||
用 'try' 包住,一旦命中保留字就整体回溯,让 'optional' 正常返回 'Nothing'。
|
||||
-}
|
||||
-- | 别名(不许用保留字)
|
||||
aliasName :: Parser String
|
||||
aliasName = try $ do
|
||||
@@ -234,7 +219,7 @@ dropTableStatement = do
|
||||
name <- identifier
|
||||
return (DropTable name)
|
||||
|
||||
-- | 读 CREATE INDEX(列名即索引名:一列最多一个索引)
|
||||
-- | 读 CREATE INDEX(列名即索引名)
|
||||
createIndexStatement :: Parser Statement
|
||||
createIndexStatement = do
|
||||
keyword "create"
|
||||
@@ -254,6 +239,17 @@ dropIndexStatement = do
|
||||
col <- between (symbol "(") (symbol ")") identifier
|
||||
return (DropIndex tbl col)
|
||||
|
||||
-- | 读 ALTER TABLE ... DROP COLUMN
|
||||
alterDropColumnStatement :: Parser Statement
|
||||
alterDropColumnStatement = do
|
||||
keyword "alter"
|
||||
keyword "table"
|
||||
tbl <- identifier
|
||||
keyword "drop"
|
||||
keyword "column"
|
||||
col <- identifier
|
||||
return (DropColumn tbl col)
|
||||
|
||||
-- | 建表时的一列
|
||||
columnDef :: Parser (String, Column)
|
||||
columnDef = do
|
||||
@@ -272,7 +268,6 @@ columnType =
|
||||
<|> (keyword "boolean" >> pure TBool)
|
||||
<|> (keyword "bool" >> pure TBool)
|
||||
|
||||
-- * 语句
|
||||
-- | 读 SELECT
|
||||
selectStatement :: Parser Statement
|
||||
selectStatement = do
|
||||
@@ -292,7 +287,7 @@ selectStatement = do
|
||||
, selectLimit = mLimit
|
||||
}
|
||||
|
||||
-- | 读 INSERT:`VALUES (...)` 后面可以跟多个括号,一次插多行
|
||||
-- | 读 INSERT,可一次插多行
|
||||
insertStatement :: Parser Statement
|
||||
insertStatement = do
|
||||
keyword "INSERT"
|
||||
@@ -330,7 +325,6 @@ updateStatement = do
|
||||
mWhere <- optional (keyword "where" *> expr)
|
||||
return (Update tbl assigns mWhere)
|
||||
|
||||
-- * 入口
|
||||
-- | 解析总入口
|
||||
parseStatement :: String -> Either String Statement
|
||||
parseStatement input =
|
||||
@@ -338,10 +332,7 @@ 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
|
||||
@@ -350,4 +341,5 @@ parseStatement input =
|
||||
<|> try createIndexStatement
|
||||
<|> try createTableStatement
|
||||
<|> try dropIndexStatement
|
||||
<|> try alterDropColumnStatement
|
||||
<|> dropTableStatement
|
||||
@@ -0,0 +1,102 @@
|
||||
-- ============================================================================
|
||||
-- ChuSQL DROP TABLE + 统计信息 手测脚本
|
||||
-- 用法:
|
||||
-- 1. 另开终端起 Rust server:cd chusql-storage && cargo run --bin chusql-storage
|
||||
-- 2. GHCi 里执行::script ghci-scripts/drop-table-test.ghci
|
||||
-- ============================================================================
|
||||
|
||||
:set -XOverloadedStrings
|
||||
|
||||
import ChuSQL.Storage
|
||||
import ChuSQL.Storage.IPC
|
||||
import ChuSQL.Model
|
||||
import ChuSQL.Engine
|
||||
import ChuSQL.Syntax.AST
|
||||
import ChuSQL.Syntax.Parser
|
||||
|
||||
-- 辅助:把一条 SQL 字符串跑到底
|
||||
let runSql sql = case parseQuery sql of { Left e -> putStrLn ("parse error: " ++ e); Right q -> runIPCStorage (runQueryM q) >>= print }
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. 清场:如果 demo3 存在就删掉
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [2] cleanup"
|
||||
runSql "DROP TABLE demo3"
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. 建表
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [3] CREATE TABLE"
|
||||
runSql "CREATE TABLE demo3 (id INT, name TEXT)"
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. describe:列应为 id/name,行数应为 0
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [4] describe (row_count should be 0)"
|
||||
doDescribeTable "demo3"
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. 空表能扫
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [5] scan empty table"
|
||||
runIPCStorage (scan "demo3")
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. 插 3 行
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [6] INSERT 3 rows"
|
||||
runSql "INSERT INTO demo3 (id, name) VALUES (1, 'Alice')"
|
||||
runSql "INSERT INTO demo3 (id, name) VALUES (2, 'Bob')"
|
||||
runSql "INSERT INTO demo3 (id, name) VALUES (3, 'Carol')"
|
||||
|
||||
-- ============================================================================
|
||||
-- 7. describe:行数应为 3
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [7] describe (row_count should be 3)"
|
||||
doDescribeTable "demo3"
|
||||
|
||||
-- ============================================================================
|
||||
-- 8. 扫回三行
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [8] scan 3 rows"
|
||||
runIPCStorage (scan "demo3")
|
||||
|
||||
-- ============================================================================
|
||||
-- 9. SQL SELECT,走索引
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [9] SELECT WHERE id = 2"
|
||||
runSql "SELECT name FROM demo3 WHERE id = 2"
|
||||
|
||||
-- ============================================================================
|
||||
-- 10. 重复建表应报错
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [10] CREATE TABLE again (should fail)"
|
||||
runSql "CREATE TABLE demo3 (x INT)"
|
||||
|
||||
-- ============================================================================
|
||||
-- 11. 删表
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [11] DROP TABLE"
|
||||
runSql "DROP TABLE demo3"
|
||||
|
||||
-- ============================================================================
|
||||
-- 12. 删后再查,应报 unknown table
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [12a] scan after drop (should fail)"
|
||||
runIPCStorage (scan "demo3")
|
||||
putStrLn ">>> [12b] describe after drop (should fail)"
|
||||
doDescribeTable "demo3"
|
||||
|
||||
-- ============================================================================
|
||||
-- 13. 删不存在的表应报错
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [13] DROP TABLE nope (should fail)"
|
||||
runSql "DROP TABLE nope"
|
||||
|
||||
-- ============================================================================
|
||||
-- 14. 列出所有表,不应含 demo3
|
||||
-- ============================================================================
|
||||
putStrLn ">>> [14] list_tables"
|
||||
doListTables
|
||||
|
||||
putStrLn ">>> done. check chusql-storage/data/ for disk state"
|
||||
+56
-13
@@ -23,6 +23,8 @@ import System.IO (Handle)
|
||||
import System.Process (CreateProcess (..), ProcessHandle, StdStream (NoStream), createProcess, proc, terminateProcess, waitForProcess)
|
||||
import Test.Hspec
|
||||
|
||||
-- 引擎层的 hspec 测试:语法、执行、优化器与 IPC 行为。
|
||||
|
||||
users :: Table
|
||||
users =
|
||||
Table
|
||||
@@ -366,7 +368,6 @@ main = hspec $ do
|
||||
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
|
||||
|
||||
@@ -390,6 +391,36 @@ main = hspec $ do
|
||||
`shouldBe` Right []
|
||||
rowsOf (parseStatement "DROP INDEX ON users (age)" >>= runStatement testDB)
|
||||
`shouldBe` Right []
|
||||
|
||||
describe "ChuSQL.Engine (DROP COLUMN)" $ do
|
||||
it "parses ALTER TABLE ... DROP COLUMN case-insensitively" $ do
|
||||
parseStatement "ALTER TABLE users DROP COLUMN age"
|
||||
`shouldBe` Right (DropColumn "users" "age")
|
||||
parseStatement "alter table users drop column age"
|
||||
`shouldBe` Right (DropColumn "users" "age")
|
||||
|
||||
it "after execution the column definition and every row cell are gone, other columns untouched" $ do
|
||||
case runStatement testDB (DropColumn "users" "age") of
|
||||
Left err -> expectationFailure err
|
||||
Right (db', _) -> do
|
||||
case lookup "users" db' of
|
||||
Nothing -> expectationFailure "users table is gone"
|
||||
Just t -> do
|
||||
map fst (tableCols t) `shouldBe` ["id", "name"]
|
||||
tableRows t `shouldSatisfy` all (all (\(k, _) -> k /= "age"))
|
||||
let rowCountBefore = maybe 0 (length . tableRows) (lookup "users" testDB)
|
||||
length (tableRows t) `shouldBe` rowCountBefore
|
||||
tableRows t `shouldBe` map (filter ((/= "age") . fst)) (maybe [] tableRows (lookup "users" testDB))
|
||||
|
||||
it "unknown column or unknown table reports an error" $ do
|
||||
(parseStatement "ALTER TABLE users DROP COLUMN nope" >>= runStatement testDB)
|
||||
`shouldSatisfy` isLeft
|
||||
(parseStatement "ALTER TABLE nope DROP COLUMN age" >>= runStatement testDB)
|
||||
`shouldSatisfy` isLeft
|
||||
|
||||
it "the built-in id column cannot be dropped (the UI locates a row by it)" $ do
|
||||
(parseStatement "ALTER TABLE users DROP COLUMN id" >>= runStatement testDB)
|
||||
`shouldSatisfy` isLeft
|
||||
describe "ChuSQL.Engine (DELETE)" $ do
|
||||
it "parses DELETE with WHERE" $ do
|
||||
parseStatement "DELETE FROM users WHERE age < 18"
|
||||
@@ -1009,16 +1040,13 @@ main = hspec $ 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
|
||||
@@ -1281,13 +1309,11 @@ main = hspec $ do
|
||||
(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)
|
||||
@@ -1296,7 +1322,6 @@ main = hspec $ do
|
||||
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)
|
||||
@@ -1353,6 +1378,24 @@ main = hspec $ do
|
||||
parseStatement "CREATE TABLE t (flag BOOL)"
|
||||
`shouldBe` Right (CreateTable "t" [("flag", TBool)])
|
||||
|
||||
it "parses TRUE / FALSE as boolean literals (case-insensitive)" $ do
|
||||
parseStatement "INSERT INTO t (flag) VALUES (TRUE)"
|
||||
`shouldBe` Right (Insert "t" ["flag"] [[LitBool True]])
|
||||
parseStatement "INSERT INTO t (flag) VALUES (false)"
|
||||
`shouldBe` Right (Insert "t" ["flag"] [[LitBool False]])
|
||||
|
||||
it "boolean literals work in WHERE (and don't need a column named true)" $ do
|
||||
parseStatement "SELECT * FROM t WHERE flag = TRUE"
|
||||
`shouldBe` Right (makeSelect ["*"] "t" (Just (Eq (Col "flag") (LitBool True))))
|
||||
|
||||
it "inserts and reads back a boolean column end-to-end" $ do
|
||||
let db = [("t", Table "t" [("id", TInt), ("flag", TBool)] [])]
|
||||
case runStatement db (Insert "t" ["id", "flag"] [[LitInt 1, LitBool True]]) of
|
||||
Left e -> expectationFailure e
|
||||
Right (db', _) ->
|
||||
rowsOf (runStatement db' (makeSelect ["flag"] "t" Nothing))
|
||||
`shouldBe` Right [[("flag", VBool True)]]
|
||||
|
||||
it "parses CREATE TABLE with VARCHAR as TStr" $ do
|
||||
parseStatement "CREATE TABLE t (name VARCHAR)"
|
||||
`shouldBe` Right (CreateTable "t" [("name", TStr)])
|
||||
@@ -1418,17 +1461,17 @@ locateServer = do
|
||||
Nothing -> pure (Left "chusql-storage directory not found (run these tests inside the repository)")
|
||||
Just dir -> do
|
||||
built <-
|
||||
try (createProcess (proc "cargo" ["build", "--bin", "server"]){cwd = Just dir, std_out = NoStream, std_err = NoStream}) ::
|
||||
try (createProcess (proc "cargo" ["build", "--bin", "chusql-storage"]){cwd = Just dir, std_out = NoStream, std_err = NoStream}) ::
|
||||
IO (Either IOException (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle))
|
||||
case built of
|
||||
Left e -> pure (Left ("cannot run cargo (Rust is required for the IPC tests): " ++ show e))
|
||||
Right (_, _, _, ph) -> do
|
||||
ec <- waitForProcess ph
|
||||
case ec of
|
||||
ExitFailure n -> pure (Left ("cargo build --bin server failed with exit code " ++ show n))
|
||||
ExitFailure n -> pure (Left ("cargo build --bin chusql-storage failed with exit code " ++ show n))
|
||||
ExitSuccess -> do
|
||||
found <- firstExisting [dir </> "target" </> "debug" </> "server.exe", dir </> "target" </> "debug" </> "server"]
|
||||
pure (maybe (Left "cargo build finished but no server executable was found") Right found)
|
||||
found <- firstExisting [dir </> "target" </> "debug" </> "chusql-storage.exe", dir </> "target" </> "debug" </> "chusql-storage"]
|
||||
pure (maybe (Left "cargo build finished but no storage executable was found") Right found)
|
||||
where
|
||||
firstDir [] = pure Nothing
|
||||
firstDir (d : ds) = do
|
||||
@@ -1463,7 +1506,7 @@ startServerAt bin dir = do
|
||||
waitUntilReady srv 100
|
||||
pure srv
|
||||
|
||||
-- | 每次调用都不同的后缀:CPU 时间精度不够,再加上一个唯一编号。
|
||||
-- | 每次调用都不同的后缀:CPU 时间加唯一编号
|
||||
uniqueStamp :: IO String
|
||||
uniqueStamp = do
|
||||
cpu <- getCPUTime
|
||||
@@ -1522,7 +1565,7 @@ withPipeName name act = do
|
||||
sortRow :: Row -> Row
|
||||
sortRow = sortOn fst
|
||||
|
||||
-- | 从一次索引点查的结果里取出那一行(没索引、没查到都给 Nothing)
|
||||
-- | 取出索引点查返回的那一行,取不到给 Nothing
|
||||
indexRow :: Either String IndexResult -> Maybe Row
|
||||
indexRow (Right (IndexRow r)) = r
|
||||
indexRow _ = Nothing
|
||||
|
||||
Reference in new issue
Block a user