使用React js完成web前端

This commit is contained in:
LIm1tZz7 committed 2026-09-27 22:08:23 +08:00
1 parent 9e8c5f1ef0
commit 7600f0f9ed
130 files changed
+12708 -788

No files matched your search

+3 -14
View File
@@ -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
-3
View File
@@ -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) ->
+1 -6
View File
@@ -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
+5 -13
View File
@@ -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
+2 -11
View File
@@ -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)
+11 -8
View File
@@ -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"
+10 -22
View File
@@ -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
+21 -41
View File
@@ -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
+20 -13
View File
@@ -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 =
+3 -16
View File
@@ -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 =
+22 -30
View File
@@ -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
+102
View File
@@ -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
View File
@@ -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