大改,整理项目结构,完成rust ffi调用,写好tcp接口

This commit is contained in:
LIm1tZz7 committed 2026-10-02 15:07:38 +08:00
1 parent df6d348995
commit 9758439b82
118 files changed
+8103 -6321

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
module Main where
import ChuSQL.Core.Engine.Algebra.Op (renderPlan)
import ChuSQL.Core.Engine.Algebra.Optimize (optimize)
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Engine
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser
-- 命令行演示入口:跑几条语句并打印结果与优化前后的计划。
users :: Table
users =
Table
{ tableName = "users"
, tableCols = [("id", TInt), ("name", TStr), ("age", TInt)]
, tableRows =
[ [("id", VInt 1), ("name", VStr "Alice"), ("age", VInt 25)]
, [("id", VInt 2), ("name", VStr "Bob"), ("age", VInt 17)]
, [("id", VInt 3), ("name", VStr "Carol"), ("age", VInt 30)]
]
}
orders :: Table
orders =
Table
{ tableName = "orders"
, tableCols = [("id", TInt), ("user_id", TInt), ("product", TStr)]
, tableRows =
[ [("id", VInt 1), ("user_id", VInt 1), ("product", VStr "Book")]
, [("id", VInt 2), ("user_id", VInt 2), ("product", VStr "Pen")]
, [("id", VInt 3), ("user_id", VInt 1), ("product", VStr "Cup")]
]
}
db :: Database
db = [("users", users), ("orders", orders)]
-- | 跑演示语句,打印结果与优化前后的计划
main :: IO ()
main = do
putStrLn "==================== Engine ====================="
let q1 = makeSelect ["name"] "users" (Just (Gt (Col "age") (LitInt 18)))
print (rowsOf (runStatement db q1))
let q2 = makeSelect ["*"] "users" Nothing
print (rowsOf (runStatement db q2))
let q3 = makeSelect ["name"] "nonexistent" Nothing
print (rowsOf (runStatement db q3))
let q4 =
makeSelect
["name"]
"users"
( Just
( Or
(Lt (Col "age") (LitInt 18))
(Gt (Col "age") (LitInt 30))
)
)
print (rowsOf (runStatement db q4))
let parsed = parseStatement "SELECT name FROM users WHERE age > 18 AND name = 'Alice'"
print (rowsOf (parsed >>= runStatement db))
putStrLn "\n==================== Optimizer ====================="
let planSql =
"SELECT u.name, o.product FROM users u \
\JOIN orders o ON u.id = o.user_id WHERE u.age > 18"
case parseStatement planSql of
Left err -> putStrLn err
Right query -> case translate query of
Left err -> putStrLn err
Right plan -> do
putStrLn "--- plan before optimization ---"
putStrLn (renderPlan plan)
putStrLn "--- plan after optimization ---"
putStrLn (renderPlan (optimize db plan))
putStrLn "--- rows ---"
print (rowsOf (runStatement db query))
@@ -0,0 +1,70 @@
cabal-version: 2.4
name: chusql-core-engine
version: 1.0.0
synopsis: ChuSQL 执行引擎:SQL 词法与语法分析、表达式求值
description: ChuSQL 服务端执行引擎。负责 SQL 词法分析与 AST 构建、
WHERE 条件表达式求值、关系代数运算与内存表管理。
build-type: Simple
library
hs-source-dirs: src
exposed-modules: ChuSQL.Core.Engine.Algebra.Eval
ChuSQL.Core.Engine.Algebra.Expr
ChuSQL.Core.Engine.Algebra.Op
ChuSQL.Core.Engine.Algebra.Optimize
ChuSQL.Core.Engine.Algebra.Planner
ChuSQL.Core.Engine.Algebra.Sort
ChuSQL.Core.Engine
ChuSQL.Core.Engine.Semantic
ChuSQL.Core.Engine.Storage
ChuSQL.Core.Engine.Storage.FFI
ChuSQL.Core.Engine.Storage.IPC
ChuSQL.Core.Engine.Storage.Memory
ChuSQL.Core.Engine.Syntax.AST
ChuSQL.Core.Engine.Syntax.Parser
build-depends: base >= 4.14 && < 5
, aeson
, bytestring
, hashable
, chusql-core-model
, megaparsec
, parser-combinators
, scientific
, text
, unordered-containers
extra-libraries: chusql_core_storage
-- 运行期找 libchusql_core_storage.so:$ORIGIN 让二进制与动态库同目录即可
if os(linux)
ld-options: -Wl,-rpath,$ORIGIN
if os(osx)
ld-options: -Wl,-rpath,@loader_path
default-language: Haskell2010
ghc-options: -Wall
executable chusql-core-engine
main-is: Main.hs
hs-source-dirs: app
build-depends: base
, chusql-core-engine
, chusql-core-model
default-language: Haskell2010
ghc-options: -Wall
-- 运行期找 libchusql_core_storage.so:$ORIGIN 让二进制与动态库同目录即可
if os(linux)
ld-options: -Wl,-rpath,$ORIGIN
if os(osx)
ld-options: -Wl,-rpath,@loader_path
test-suite chusql-core-engine-test
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
build-depends: base
, chusql-core-engine
, chusql-core-model
, directory
, filepath
, hspec
default-language: Haskell2010
ghc-options: -Wall
@@ -0,0 +1,374 @@
module ChuSQL.Core.Engine (runStatement, runStatementM, rowsOf) where
import ChuSQL.Core.Engine.Algebra.Eval (evalCondForRowM, evalExprM, evalRelOpM)
import ChuSQL.Core.Engine.Algebra.Expr (evalCondForRow, evalExpr)
import ChuSQL.Core.Engine.Algebra.Optimize (optimize)
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Semantic (prepare)
import ChuSQL.Core.Engine.Storage (MonadStorage (..))
import ChuSQL.Core.Engine.Storage.Memory (MemoryStorage (runMemoryStorage))
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseExpression)
import Data.Maybe (mapMaybe)
-- 引擎入口:检查语义、按语句分发执行,另有内存实现与泛型版本。
-- | 跑一条语句(内存实现)
runStatement :: Database -> Statement -> Either String (Database, [Row])
runStatement db q = do
(result, db') <- runMemoryStorage (runStatementM q) db
rows <- result
Right (db', rows)
-- | 只要结果行
rowsOf :: Either String (Database, [Row]) -> Either String [Row]
rowsOf = fmap snd
-- | 泛型入口:先检查再执行
runStatementM :: (MonadStorage m) => Statement -> m (Either String [Row])
runStatementM (CreateDatabase name) = fmap (fmap (const [])) (createDatabase name)
runStatementM (DropDatabase name) = fmap (fmap (const [])) (dropDatabase name)
runStatementM (UseDatabase name) = fmap (fmap (const [])) (useDatabase name)
runStatementM ShowDatabases = fmap (fmap (map (\name -> [("database", VStr name)]))) listDatabases
runStatementM q = do
db <- schema
case prepare db q of
Left err -> pure (Left err)
Right resolved -> runStatementUncheckedM db resolved
-- | 按语句类型分发(已检查过)
runStatementUncheckedM :: (MonadStorage m) => Database -> Statement -> m (Either String [Row])
runStatementUncheckedM db q@Select{} =
case translate q of
Left e -> pure (Left e)
Right relOp -> evalRelOpM db (optimize db relOp)
runStatementUncheckedM db q@SelectExpr{} =
case translate q of
Left e -> pure (Left e)
Right relOp -> evalRelOpM db (optimize db relOp)
runStatementUncheckedM db (Insert tbl cols rows) =
case lookupTable db tbl of
Left e -> pure (Left e)
Right table -> do
current <- existingRows tbl table
case current >>= \current' -> buildRows table current' cols rows of
Left err -> pure (Left err)
Right rs -> do
result <- insertMany tbl rs
pure (result >> Right [])
runStatementUncheckedM db (Delete tbl mWhere) = do
rowsResult <- scan tbl
case rowsResult of
Left err -> pure (Left err)
Right rows -> do
split <- splitByConditionM db mWhere rows
case split of
Left err -> pure (Left err)
Right (doomed, kept) ->
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
-- | 取行里的整数 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 db (Update tbl assigns mWhere) =
case lookupTable db tbl of
Left e -> pure (Left e)
Right table -> do
rowsResult <- scan tbl
case rowsResult of
Left err -> pure (Left err)
Right rows -> do
newRows <- mapM (updateRowM db table mWhere assigns) rows
case sequence newRows of
Left err -> pure (Left err)
Right rs -> do
result <- replaceAll tbl rs
pure (result >> Right [])
runStatementUncheckedM _ (CreateTable name cols) = do
result <- createTable name cols
pure (result >> Right [])
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 [])
runStatementUncheckedM _ (DropColumn tbl col) = do
result <- dropColumn tbl col
pure (result >> Right [])
runStatementUncheckedM db (AddColumn tbl def) =
alterTable db tbl (\table -> addColumnTo def (tableCols table) (tableRows table))
runStatementUncheckedM db (RenameColumn tbl old new) =
alterTable db tbl (\table -> renameColumnIn old new (tableCols table) (tableRows table))
runStatementUncheckedM db (AlterColumnType tbl col ty) =
alterTable db tbl (\table -> retypeColumn col ty (tableCols table) (tableRows table))
runStatementUncheckedM db (AlterColumnDefault tbl col def) =
alterTable db tbl (\table -> setColumnDefault col def (tableCols table) (tableRows table))
runStatementUncheckedM db (AlterColumnNull tbl col nullable) =
alterTable db tbl (\table -> setColumnNullable col nullable (tableCols table) (tableRows table))
runStatementUncheckedM _ CreateUser{} = pure (Left "CREATE USER is executed by the account service")
runStatementUncheckedM _ AlterUser{} = pure (Left "ALTER USER is executed by the account service")
runStatementUncheckedM _ DropUser{} = pure (Left "DROP USER is executed by the account service")
runStatementUncheckedM _ CreateRole{} = pure (Left "CREATE ROLE is executed by the privilege service")
runStatementUncheckedM _ DropRole{} = pure (Left "DROP ROLE is executed by the privilege service")
runStatementUncheckedM _ GrantPrivileges{} = pure (Left "GRANT is executed by the privilege service")
runStatementUncheckedM _ RevokePrivileges{} = pure (Left "REVOKE is executed by the privilege service")
runStatementUncheckedM _ GrantRole{} = pure (Left "GRANT is executed by the privilege service")
runStatementUncheckedM _ RevokeRole{} = pure (Left "REVOKE is executed by the privilege service")
runStatementUncheckedM _ q@CreateDatabase{} = runStatementM q
runStatementUncheckedM _ q@DropDatabase{} = runStatementM q
runStatementUncheckedM _ q@UseDatabase{} = runStatementM q
runStatementUncheckedM _ ShowDatabases = runStatementM ShowDatabases
-- | 只有需要读全表才能判断约束时才扫表
existingRows :: (MonadStorage m) => String -> Table -> m (Either String [Row])
existingRows tbl table
| needsRows table = scan tbl
| otherwise = pure (Right [])
where
-- | 这一表有没有要判约束的列
needsRows t = any (constraintColumn . snd) (tableCols t)
-- | 这一列带不带要判的约束
constraintColumn c = columnAutoIncrement c || columnUnique c || columnPrimaryKey c
-- | 插一行:补齐缺列、给自增值、收类型、查约束
buildRows :: Table -> [Row] -> [String] -> [[Expr]] -> Either String [Row]
buildRows table current cols rows = do
values <- mapM (mapM (\e -> evalExpr e [])) rows
if any (\vals -> length vals /= length cols) values
then Left "column count does not match value count"
else do
let auto = autoColumn table
go auto (autoStart auto current) values []
where
-- | 逐行构造,最后统一查一遍约束
go _ _ [] acc = do
let built = reverse acc
mapM_ (runChecks table) built
enforceNotNull (tableCols table) built
enforceUnique (tableCols table) (current ++ built)
Right built
go auto counter (vals : rest) acc = do
(row, counter') <- buildRow table auto cols counter vals
go auto counter' rest (row : acc)
-- | 按列定义补出一行
buildRow :: Table -> Maybe (String, Column) -> [String] -> Int -> [Value] -> Either String (Row, Int)
buildRow table auto cols counter vals = go (tableCols table) counter []
where
provided = zip cols vals
-- | 按列定义逐列补值
go [] c acc = Right (reverse acc, c)
go ((name, col) : rest) c acc = do
(v, c') <- pick name col c
v' <- coerceValue (columnType col) v
go rest c' ((name, v') : acc)
-- | 决定这一列取什么值
pick name col c
| Just (autoName, _) <- auto
, name == autoName
, maybe True (== VNull) (lookup name provided) = Right (VInt c, c + 1)
| Just v <- lookup name provided = Right (v, bump name col c v)
| Just d <- columnDefault col = Right (d, c)
| columnNullable col = Right (VNull, c)
| otherwise = Left ("INSERT: column " ++ name ++ " is NOT NULL and has no default")
-- | 自增值跟着已有的值往前推
bump name _ c v
| Just (autoName, _) <- auto
, name == autoName = max (c + 1) (valueInt v + 1)
| otherwise = c
-- | 取自增值,不是整数就当 0
valueInt :: Value -> Int
valueInt (VInt n) = n
valueInt _ = 0
-- | 自增列(至多一个)
autoColumn :: Table -> Maybe (String, Column)
autoColumn table = case [(n, c) | (n, c) <- tableCols table, columnAutoIncrement c] of
(entry : _) -> Just entry
[] -> Nothing
-- | 下一个自增值
autoStart :: Maybe (String, Column) -> [Row] -> Int
autoStart Nothing _ = 1
autoStart (Just (name, _)) rows = case [n | r <- rows, Just (VInt n) <- [lookup name r]] of
[] -> 1
ns -> maximum ns + 1
-- | 执行 CHECK 约束
runChecks :: Table -> Row -> Either String ()
runChecks table row = mapM_ check (tableCols table)
where
-- | 查这一列的 CHECK
check (name, col) = case columnCheck col of
Nothing -> Right ()
Just text -> case parseExpression text of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ firstLine err)
Right e -> case evalCondForRow e row of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ err)
Right True -> Right ()
Right False -> Left ("CHECK on " ++ name ++ " failed")
-- | 只取第一行(报错提示用)
firstLine :: String -> String
firstLine = takeWhile (/= '\n')
-- | NOT NULL 检查
enforceNotNull :: [(String, Column)] -> [Row] -> Either String ()
enforceNotNull cols rows = mapM_ check cols
where
-- | 查这一列的 NOT NULL
check (name, col)
| columnNullable col = Right ()
| any (\r -> lookup name r == Just VNull) rows = Left ("NOT NULL: column " ++ name ++ " cannot be null")
| otherwise = Right ()
-- | 唯一性检查(NULL 之间不算冲突)
enforceUnique :: [(String, Column)] -> [Row] -> Either String ()
enforceUnique cols rows = mapM_ check targets
where
targets = [(n, c) | (n, c) <- cols, columnUnique c || columnPrimaryKey c]
-- | 查这一列有没有重复值
check (name, _) = go name [] (mapMaybe (lookup name) rows)
-- | 逐个值比过去
go _ _ [] = Right ()
-- | NULL 不算冲突,跳过
go name seen (VNull : rest) = go name seen rest
go name seen (v : rest)
| v `elem` seen = Left ("UNIQUE: duplicate value in column " ++ name)
| otherwise = go name (v : seen) rest
-- | UPDATE 的一行(条件与赋值都可能带子查询)
updateRowM :: (MonadStorage m) => Database -> Table -> Maybe Expr -> [(String, Expr)] -> Row -> m (Either String Row)
updateRowM db table cond asgns row = do
keep <- case cond of
Nothing -> pure (Right True)
Just e -> evalCondForRowM db e row
case keep of
Left err -> pure (Left err)
Right False -> pure (Right row)
Right True -> do
row' <- applyUpdatesM db table asgns row
pure (row' >>= \r -> runChecks table r >> Right r)
-- | 依次求值、收类型并覆盖列(表达式可以带子查询)
applyUpdatesM :: (MonadStorage m) => Database -> Table -> [(String, Expr)] -> Row -> m (Either String Row)
applyUpdatesM _ _ [] row = pure (Right row)
applyUpdatesM db table ((col, e) : rest) row = do
v <- evalExprM db e row
case v of
Left err -> pure (Left err)
Right val -> case colType table col of
Just def -> case coerceValue (columnType def) val of
Left err -> pure (Left err)
Right v' -> applyUpdatesM db table rest (setColumn col v' row)
Nothing -> applyUpdatesM db table rest (setColumn col val row)
-- | 覆盖一列
setColumn :: String -> Value -> Row -> Row
setColumn col v = map (\(k, val) -> if k == col then (k, v) else (k, val))
-- | 按条件把行分成待删和保留两拨(条件可以带子查询)
splitByConditionM :: (MonadStorage m) => Database -> Maybe Expr -> [Row] -> m (Either String ([Row], [Row]))
splitByConditionM db cond = go [] []
where
-- | 逐行分流
go doomed kept [] = pure (Right (reverse doomed, reverse kept))
go doomed kept (r : rs) = do
hit <- case cond of
Nothing -> pure (Right True)
Just e -> evalCondForRowM db e r
case hit of
Left err -> pure (Left err)
Right h -> go (if h then r : doomed else doomed) (if h then kept else r : kept) rs
-- | 表结构变更的统一入口:扫全表,改完整体回写
alterTable ::
(MonadStorage m) =>
Database ->
String ->
(Table -> Either String ([(String, Column)], [Row])) ->
m (Either String [Row])
alterTable db tbl change = case lookupTable db tbl of
Left e -> pure (Left e)
Right table -> do
rowsResult <- scan tbl
case rowsResult of
Left e -> pure (Left e)
Right rows -> case change table{tableRows = rows} >>= checkAltered of
Left e -> pure (Left e)
Right (cols, newRows) -> do
result <- replaceSchema tbl cols newRows
pure (result >> Right [])
where
-- | 改完再统一查一遍约束
checkAltered (cols, rows) = do
let table = Table tbl cols rows
mapM_ (runChecks table) rows
enforceNotNull cols rows
enforceUnique cols rows
Right (cols, rows)
-- | ADD COLUMN:老行补默认值或 NULL
addColumnTo :: (String, Column) -> [(String, Column)] -> [Row] -> Either String ([(String, Column)], [Row])
addColumnTo (name, col) cols rows
| name `elem` map fst cols = Left ("ADD COLUMN: column already exists: " ++ name)
| otherwise = do
values <- fill
Right (cols ++ [(name, col)], [r ++ [(name, v)] | (r, v) <- zip rows values])
where
fill
| columnAutoIncrement col = mapM (coerceValue (columnType col) . VInt) [1 .. length rows]
| Just d <- columnDefault col = mapM (\_ -> coerceValue (columnType col) d) rows
| columnNullable col = mapM (\_ -> coerceValue (columnType col) VNull) rows
| null rows = Right []
| otherwise = Left ("ADD COLUMN: column " ++ name ++ " is NOT NULL and has no default")
-- | RENAME COLUMN:列定义与每一行的键一起改
renameColumnIn :: String -> String -> [(String, Column)] -> [Row] -> Either String ([(String, Column)], [Row])
renameColumnIn old new cols rows
| old `notElem` map fst cols = Left ("RENAME COLUMN: unknown column: " ++ old)
| new `elem` map fst cols = Left ("RENAME COLUMN: column already exists: " ++ new)
| otherwise = Right (renameKeys cols, map renameKeys rows)
where
renameKeys = map (\(k, v) -> if k == old then (new, v) else (k, v))
-- | ALTER COLUMN TYPE:老值收进新类型
retypeColumn :: String -> ColumnType -> [(String, Column)] -> [Row] -> Either String ([(String, Column)], [Row])
retypeColumn col ty cols rows
| col `notElem` map fst cols = Left ("ALTER COLUMN: unknown column: " ++ col)
| otherwise = do
rows' <- mapM convert rows
Right (map (\entry@(n, c) -> if n == col then (n, c{columnType = ty}) else entry) cols, rows')
where
-- | 把这一列的老值收进新类型
convert row = case lookup col row of
Nothing -> Right row
Just v -> do
v' <- coerceValue ty v
Right (map (\(k, x) -> if k == col then (k, v') else (k, x)) row)
-- | ALTER COLUMN SET/DROP DEFAULT
setColumnDefault :: String -> Maybe Value -> [(String, Column)] -> [Row] -> Either String ([(String, Column)], [Row])
setColumnDefault col def cols rows
| col `notElem` map fst cols = Left ("ALTER COLUMN: unknown column: " ++ col)
| otherwise = Right (map (\entry@(n, c) -> if n == col then (n, c{columnDefault = def}) else entry) cols, rows)
-- | ALTER COLUMN SET/DROP NOT NULL
setColumnNullable :: String -> Bool -> [(String, Column)] -> [Row] -> Either String ([(String, Column)], [Row])
setColumnNullable col nullable cols rows
| col `notElem` map fst cols = Left ("ALTER COLUMN: unknown column: " ++ col)
| otherwise = Right (map (\entry@(n, c) -> if n == col then (n, c{columnNullable = nullable}) else entry) cols, rows)
@@ -0,0 +1,494 @@
module ChuSQL.Core.Engine.Algebra.Eval (evalRelOp, evalRelOpM, evalExprM, evalCondForRowM) where
import ChuSQL.Core.Engine.Algebra.Expr (evalCondForRow, evalExpr)
import ChuSQL.Core.Engine.Algebra.Optimize (optimize, relOpCols)
import ChuSQL.Core.Engine.Algebra.Op
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Engine.Algebra.Sort (compareValue, sortRows)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Storage (IndexResult (..), MonadStorage (..))
import ChuSQL.Core.Engine.Storage.Memory (runMemoryStorage)
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), JoinKind (..), Subquery (..))
import Control.Monad (filterM, foldM)
import qualified Data.HashMap.Strict as HM
-- 执行:按算子树算出结果行,外层行供相关子查询引用。
-- | 只保留清单里的列,顺序照清单
project :: [String] -> Row -> Row
project ["*"] row = row
project cs row = [(c, v) | c <- cs, Just v <- [lookup c row]]
-- | 给列名加别名前缀
addPrefix :: Maybe String -> Row -> Row
addPrefix mAlias = map (\(k, v) -> (qualify mAlias k, v))
-- | 给一批行加别名前缀
prefixRows :: Maybe String -> [Row] -> [Row]
prefixRows mAlias = map (addPrefix mAlias)
-- | 值 → 字面量
valueLiteral :: Value -> Expr
valueLiteral (VInt n) = LitInt n
valueLiteral (VFloat d) = LitFloat d
valueLiteral (VStr s) = LitStr s
valueLiteral (VBool b) = LitBool b
valueLiteral VNull = LitNull
-- | 点查退化成扫描时用的条件
lookupCondition :: Maybe String -> String -> Value -> Expr
lookupCondition mAlias col k = Eq (Col (qualify mAlias col)) (valueLiteral k)
-- | 范围扫描退化成扫描时用的条件(闭端写成 > 或 = 的并)
rangeCondition :: Maybe String -> String -> Maybe (Value, Bool) -> Maybe (Value, Bool) -> Expr
rangeCondition mAlias col lo hi = case (lo, hi) of
(Just l, Just h) -> And (bound Gt l) (bound Lt h)
(Just l, Nothing) -> bound Gt l
(Nothing, Just h) -> bound Lt h
(Nothing, Nothing) -> LitBool True
where
c = Col (qualify mAlias col)
-- | 一个端点的比较,闭端再加上等号
bound rel (v, inclusive)
| inclusive = Or (rel c (valueLiteral v)) (Eq c (valueLiteral v))
| otherwise = rel c (valueLiteral v)
-- | 一批行的列名(空批给空清单)
rowsCols :: [Row] -> [String]
rowsCols (r : _) = map fst r
rowsCols [] = []
-- | 参与等值匹配的键:NULL 不参与,整数值的浮点归一
joinKey :: Value -> Maybe Value
joinKey VNull = Nothing
joinKey (VFloat d)
| not (isNaN d)
, not (isInfinite d)
, fromIntegral (rounded :: Integer) == d =
Just (VInt (fromIntegral rounded))
where
rounded = round d :: Integer
joinKey v = Just v
-- | 取一行的连接键
joinKeyOf :: String -> Row -> Maybe Value
joinKeyOf c row = lookup c row >>= joinKey
-- | 等值连接的两侧列名
data EquiKeys = EquiKeys
{ ekLeft :: String
, ekRight :: String
}
-- | 认出 a = b 这种两列等值条件,并且能分清谁在左谁在右
equiKeys :: [String] -> [String] -> Expr -> Maybe EquiKeys
equiKeys lcols rcols (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)
equiKeys _ _ _ = Nothing
-- | 连接两批行:等值键走哈希,否则嵌套循环
joinRowsIn ::
(MonadStorage m) =>
Database ->
Row ->
JoinKind ->
[String] ->
Expr ->
[Row] ->
[Row] ->
m (Either String [Row])
joinRowsIn db outer kind rcols cond lrows rrows =
case equiKeys (rowsCols lrows) rcols cond of
Just ks -> pure (Right (hashJoin kind ks ncols lrows rrows))
Nothing -> filterPairsM db outer kind ncols cond lrows rrows
where
-- 右表为空时行上没有列名,用算子推导出来的列名补 NULL
ncols = if null rrows then rcols else rowsCols rrows
-- | 左连接配不上的左行补 NULL,内连接丢弃
unmatched :: JoinKind -> Row -> [String] -> [Row]
unmatched InnerJoin _ _ = []
unmatched LeftJoin l cols = [l ++ [(c, VNull) | c <- cols]]
-- | 哈希连接,输出顺序同嵌套循环
hashJoin :: JoinKind -> EquiKeys -> [String] -> [Row] -> [Row] -> [Row]
hashJoin kind ks ncols lrows rrows = concatMap probe lrows
where
buckets = HM.fromListWith (flip (++)) [(v, [r]) | r <- rrows, Just v <- [joinKeyOf (ekRight ks) r]]
-- | 拿左行的键去桶里找
probe l =
let miss = case kind of
InnerJoin -> []
LeftJoin -> [l ++ nullRow]
in case joinKeyOf (ekLeft ks) l of
Nothing -> miss
Just v -> case HM.lookupDefault [] v buckets of
[] -> miss
rs -> [l ++ r | r <- rs]
nullRow = [(c, VNull) | c <- ncols]
-- | 把两边的行按条件配对(边配边筛,不先建整张积表)
filterPairsM ::
(MonadStorage m) =>
Database ->
Row ->
JoinKind ->
[String] ->
Expr ->
[Row] ->
[Row] ->
m (Either String [Row])
filterPairsM db outer kind ncols cond lrows rrows = go lrows []
where
-- | 逐左行配对,左连接没配上就补 NULL
go [] acc = pure (Right (reverse acc))
go (l : ls) acc = do
hits <- keep l rrows []
case hits of
Left e -> pure (Left e)
Right hs -> do
let hs' = case (kind, hs) of
(LeftJoin, []) -> [l ++ nullRow]
_ -> hs
go ls (reverse hs' ++ acc)
-- | 拿一行右行去比条件
keep _ [] acc = pure (Right (reverse acc))
keep l (r : rs) acc = do
ok <- evalCondForRowM db cond (l ++ r ++ outer)
case ok of
Left e -> pure (Left e)
Right True -> keep l rs ((l ++ r) : acc)
Right False -> keep l rs acc
nullRow = [(c, VNull) | c <- ncols]
-- | 索引连接:右表是裸 Scan 时逐左行点查
probeIndexJoin ::
(MonadStorage m) =>
Database ->
Row ->
JoinKind ->
Maybe String ->
String ->
EquiKeys ->
[String] ->
Expr ->
Row ->
m (Either String (Maybe [Row]))
probeIndexJoin db outer kind mAlias t ks rcols cond l = case lookup (ekLeft ks) l of
Nothing -> pure (Right (Just (unmatched kind l rcols)))
Just k
| not (indexSafe db t (unqualify mAlias (ekRight ks)) k) -> pure (Right Nothing)
| otherwise -> do
res <- lookupByColumn t (unqualify mAlias (ekRight ks)) k
case res of
Left e -> pure (Left e)
Right NoIndex -> pure (Right Nothing)
Right (IndexRows rows) -> do
pairs <- joinRowsIn db outer kind rcols cond [l] (prefixRows mAlias rows)
pure (fmap Just pairs)
-- | 列与键都不是浮点时才敢用索引
indexSafe :: Database -> String -> String -> Value -> Bool
indexSafe db t col k = case lookupTable db t of
Left _ -> False
Right tbl -> case lookup col (tableCols tbl) of
Nothing -> False
Just c -> not (floatType (columnType c)) && not (floatValue k)
-- | 是不是浮点列类型
floatType :: ColumnType -> Bool
floatType CFloat = True
floatType CDouble = True
floatType (CDecimal _ _) = True
floatType _ = False
-- | 是不是浮点值
floatValue :: Value -> Bool
floatValue (VFloat _) = True
floatValue _ = False
-- | 分组键的值
groupKey :: [String] -> Row -> Either String [Value]
groupKey keys row = mapM get keys
where
-- | 取这一列的分组值
get c = maybe (Left ("unknown column: " ++ c)) Right (lookup c row)
-- | 按分组键把行分组,保留分组首次出现的顺序
groupRows :: [String] -> [Row] -> Either String [([Value], [Row])]
groupRows keys rows = go rows (HM.empty, [])
where
-- | 逐行塞进对应分组,并记住分组首次出现的顺序
go [] (acc, order) = Right [(k, reverse (HM.lookupDefault [] k acc)) | k <- reverse order]
go (r : rs) (acc, order) = do
k <- groupKey keys r
let order' = if HM.member k acc then order else k : order
go rs (HM.insertWith (++) k [r] acc, order')
-- | 一组行上求同一个表达式(外层列从 outer 取)
valuesOf :: Row -> Expr -> [Row] -> Either String [Value]
valuesOf outer e rows = mapM (evalExpr e . (++ outer)) rows
-- | 去掉 NULL
nonNull :: [Value] -> [Value]
nonNull = filter (/= VNull)
-- | 数值相加:整数配整数还是整数
addValue :: Value -> Value -> Either String Value
addValue (VInt a) (VInt b) = Right (VInt (a + b))
addValue (VInt a) (VFloat b) = Right (VFloat (fromIntegral a + b))
addValue (VFloat a) (VInt b) = Right (VFloat (a + fromIntegral b))
addValue (VFloat a) (VFloat b) = Right (VFloat (a + b))
addValue _ _ = Left "type error: expected two numbers"
-- | 能当数用的值
numberOf :: Value -> Either String Double
numberOf (VInt n) = Right (fromIntegral n)
numberOf (VFloat d) = Right d
numberOf _ = Left "type error: expected a number"
-- | 一组值里的极值(NULL 已经先剔掉)
extremeOf :: (Value -> Value -> Value) -> [Value] -> Either String Value
extremeOf _ [] = Right VNull
extremeOf pick (x : xs) = Right (foldl pick x xs)
-- | 一个聚合算子在一组行上的结果
evalAgg :: Row -> Expr -> [Row] -> Either String Value
evalAgg outer agg rows = case agg of
CountAll -> Right (VInt (length rows))
CountOf e -> do
vs <- valuesOf outer e rows
Right (VInt (length (nonNull vs)))
SumOf e -> do
vs <- valuesOf outer e rows
case nonNull vs of
[] -> Right VNull
xs -> foldM addValue (VInt 0) xs
AvgOf e -> do
vs <- valuesOf outer e rows
case nonNull vs of
[] -> Right VNull
xs -> do
total <- foldM addValue (VInt 0) xs
sumOf <- numberOf total
Right (VFloat (sumOf / fromIntegral (length xs)))
MinOf e -> valuesOf outer e rows >>= extremeOf lower . nonNull
MaxOf e -> valuesOf outer e rows >>= extremeOf higher . nonNull
_ -> Left "only COUNT, SUM, AVG, MIN and MAX can be aggregated"
where
-- | 取更小的那个
lower a b = if compareValue b a == LT then b else a
-- | 取更大的那个
higher a b = if compareValue b a == GT then b else a
-- | 一组的输出行:分组列 + 各聚合值
oneGroup :: Row -> [String] -> [(String, Expr)] -> ([Value], [Row]) -> Either String Row
oneGroup outer keys aggs (keyValues, rows) = do
values <- mapM (\(_, e) -> evalAgg outer e rows) aggs
Right (zip keys keyValues ++ zip (map fst aggs) values)
-- | 分组求值:没有分组列时,即使没有行也要出一行
evalAggregate :: Row -> [String] -> [(String, Expr)] -> [Row] -> Either String [Row]
evalAggregate outer keys aggs rows = do
groups <- groupRows keys rows
let top = if null keys && null groups then [([], [])] else groups
mapM (oneGroup outer keys aggs) top
-- | 单子过滤:条件本身是单子求值的
filterRowsM :: (Monad m) => (Row -> m (Either String Bool)) -> [Row] -> m (Either String [Row])
filterRowsM p = go []
where
-- | 逐行留下通过条件的
go acc [] = pure (Right (reverse acc))
go acc (r : rs) = do
keep <- p r
case keep of
Left e -> pure (Left e)
Right True -> go (r : acc) rs
Right False -> go acc rs
-- | 按投影顺序逐项求值(可以带子查询)
computeM :: (MonadStorage m) => Database -> Row -> [(String, Expr)] -> Row -> m (Either String Row)
computeM db outer items row = go items []
where
-- | 逐项求值并攒成一行
go [] acc = pure (Right (reverse acc))
go ((label, e) : rest) acc = do
v <- evalExprM db e (row ++ outer)
case v of
Left err -> pure (Left err)
Right val -> go rest ((label, val) : acc)
-- | 表达式求值(先把子查询求成字面量)
evalExprM :: (MonadStorage m) => Database -> Expr -> Row -> m (Either String Value)
evalExprM db e env = do
e' <- substSubqueries db e env
pure (e' >>= \x -> evalExpr x env)
-- | 条件求值(先把子查询求成字面量)
evalCondForRowM :: (MonadStorage m) => Database -> Expr -> Row -> m (Either String Bool)
evalCondForRowM db e env = do
e' <- substSubqueries db e env
pure (e' >>= \x -> evalCondForRow x env)
-- | 子查询结果里唯一那一列的值
rowValue :: Row -> Either String Value
rowValue [(_, v)] = Right v
rowValue _ = Left "subquery must return exactly one column"
-- | 标量子查询:0 行给 NULL,多行报错
scalarResult :: Either String [Row] -> Either String Expr
scalarResult rows = do
rs <- rows
case rs of
[] -> Right LitNull
[r] -> valueLiteral <$> rowValue r
_ -> Left "scalar subquery returned more than one row"
-- | 把子查询求值成字面量;子查询的错直接往上传
substSubqueries :: (MonadStorage m) => Database -> Expr -> Row -> m (Either String Expr)
substSubqueries db e env = case e of
ScalarSub sq -> scalarResult <$> runSubqueryRows db sq env
InSub a sq negated -> do
lhs <- substSubqueries db a env
rows <- runSubqueryRows db sq env
pure (do
a' <- lhs
rs <- rows
vs <- mapM rowValue rs
Right (InList a' (map valueLiteral vs) negated))
ExistsSub sq negated -> do
rows <- runSubqueryRows db sq env
pure (fmap (\rs -> LitBool (if negated then null rs else not (null rs))) rows)
InList a es negated -> do
lhs <- substSubqueries db a env
es' <- mapM (\x -> substSubqueries db x env) es
pure (do
a' <- lhs
items <- sequence es'
Right (InList a' items negated))
Add a b -> bin Add a b
Sub a b -> bin Sub a b
Mul a b -> bin Mul a b
Div a b -> bin Div a b
Neg a -> un Neg a
Gt a b -> bin Gt a b
Lt a b -> bin Lt a b
Eq a b -> bin Eq a b
And a b -> bin And a b
Or a b -> bin Or a b
IsNull a -> un IsNull a
IsNotNull a -> un IsNotNull a
CountOf a -> un CountOf a
SumOf a -> un SumOf a
AvgOf a -> un AvgOf a
MinOf a -> un MinOf a
MaxOf a -> un MaxOf a
_ -> pure (Right e)
where
-- | 递归处理两个子表达式
bin ctor a b = do
ra <- substSubqueries db a env
case ra of
Left err -> pure (Left err)
Right a' -> do
rb <- substSubqueries db b env
pure ((ctor a') <$> rb)
-- | 递归处理一个子表达式
un ctor a = do
ra <- substSubqueries db a env
pure (ctor <$> ra)
-- | 跑子查询:翻译成算子树再求值,外层行做相关子查询的绑定
runSubqueryRows :: (MonadStorage m) => Database -> Subquery -> Row -> m (Either String [Row])
runSubqueryRows db sq outer = case translate (subqueryStatement sq) of
Left e -> pure (Left e)
Right op -> evalRelOpIn db outer (optimize db op)
-- | 单子求值:Scan/点查/范围查都问存储
evalRelOpM :: (MonadStorage m) => Database -> RelOp -> m (Either String [Row])
evalRelOpM db = evalRelOpIn db []
-- | 纯求值(内存存储,子查询走同一条路径)
evalRelOp :: Database -> RelOp -> Either String [Row]
evalRelOp db op = do
(result, _) <- runMemoryStorage (evalRelOpIn db [] op) db
result
-- | 带外层行的执行器
evalRelOpIn :: (MonadStorage m) => Database -> Row -> RelOp -> m (Either String [Row])
evalRelOpIn _ _ Unit = pure (Right [[]])
evalRelOpIn db outer (Compute items op) = do
rows <- evalRelOpIn db outer op
case rows of
Left e -> pure (Left e)
Right rs -> do
computed <- mapM (computeM db outer items) rs
pure (sequence computed)
evalRelOpIn db outer (Aggregate keys aggs op) = do
rows <- evalRelOpIn db outer op
pure (rows >>= evalAggregate outer keys aggs)
evalRelOpIn _ _ (Scan mAlias tbl cols) = do
res <- maybe (scan tbl) (scanColumns tbl) cols
pure (fmap (prefixRows mAlias) res)
evalRelOpIn db outer (Lookup mAlias tbl col k) = do
res <- lookupByColumn tbl col k
case res of
Left e -> pure (Left e)
Right NoIndex -> scanFiltered db outer mAlias tbl (lookupCondition mAlias col k)
Right (IndexRows rows) -> pure (Right (prefixRows mAlias rows))
evalRelOpIn db outer (Range mAlias tbl col lo hi) = do
res <- scanRange tbl col lo hi
case res of
Left e -> pure (Left e)
Right Nothing -> scanFiltered db outer mAlias tbl (rangeCondition mAlias col lo hi)
Right (Just rows) -> pure (Right (prefixRows mAlias rows))
evalRelOpIn db outer (Filter e op) = do
rows <- evalRelOpIn db outer op
case rows of
Left err -> pure (Left err)
Right rs -> filterRowsM (\r -> evalCondForRowM db e (r ++ outer)) rs
evalRelOpIn db outer (Project cols op) = do
rows <- evalRelOpIn db outer op
pure (fmap (map (project cols)) rows)
evalRelOpIn db outer (Sort spec op) = do
rows <- evalRelOpIn db outer op
pure (fmap (sortRows spec) rows)
evalRelOpIn db outer (Limit n op) = do
rows <- evalRelOpIn db outer op
pure (fmap (take n) rows)
evalRelOpIn db outer (Join kind l r c) = do
lres <- evalRelOpIn db outer l
case lres of
Left e -> pure (Left e)
Right ls -> do
let nested = do
rres <- evalRelOpIn db outer r
case rres of
Left e -> pure (Left e)
Right rs -> joinRowsIn db outer kind (relOpCols db r) c ls rs
case (r, equiKeys (relOpCols db l) (relOpCols db r) c) of
(Scan mAlias t _, Just ks) -> do
probed <- mapM (probeIndexJoin db outer kind mAlias t ks (relOpCols db r) c) ls
case sequence probed of
Left e -> pure (Left e)
Right ms -> case sequence ms of
Just rows -> pure (Right (concat rows))
Nothing -> nested
_ -> nested
-- | 扫描后按条件过滤(点查/范围查退化成扫描时用)
scanFiltered ::
(MonadStorage m) =>
Database ->
Row ->
Maybe String ->
String ->
Expr ->
m (Either String [Row])
scanFiltered _ outer mAlias tbl cond = do
res <- scan tbl
pure $ do
rows <- res
filterM (evalCondForRow cond . (++ outer)) (prefixRows mAlias rows)
@@ -0,0 +1,294 @@
module ChuSQL.Core.Engine.Algebra.Expr (evalExpr, evalCondForRow, colsInExpr, aggregatesIn, hasAggregate, bareColumns, hasSubquery, inValues, threeValuedNot) where
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import Control.Monad (join)
-- 表达式求值:三值逻辑,NULL 参与运算结果仍是 NULL。
-- | 在一行上求出表达式的值
evalExpr :: Expr -> Row -> Either String Value
evalExpr (Col name) row =
maybe (Left ("unknown column: " ++ name)) Right (lookup name row)
evalExpr LitNull _ = Right VNull
evalExpr (LitInt n) _ = Right (VInt n)
evalExpr (LitFloat d) _ = Right (VFloat d)
evalExpr (LitStr s) _ = Right (VStr s)
evalExpr (LitBool b) _ = Right (VBool b)
evalExpr (LitDate s) _ = Right (VStr s)
evalExpr (LitTimestamp s) _ = Right (VStr s)
evalExpr (LitBlob s) _ = Right (VStr s)
evalExpr (Add a b) row = liftArith addArith a b row
evalExpr (Sub a b) row = liftArith subArith a b row
evalExpr (Mul a b) row = liftArith mulArith a b row
evalExpr (Div a b) row = liftArith divArith a b row
evalExpr (Neg a) row = do
v <- evalExpr a row
case v of
VNull -> Right VNull
VInt n -> Right (VInt (negate n))
VFloat d -> Right (VFloat (negate d))
_ -> Left "type error: expected a number"
evalExpr (Gt a b) row = liftCompare (> EQ) a b row
evalExpr (Lt a b) row = liftCompare (< EQ) a b row
evalExpr (Eq a b) row = liftCompare (== EQ) a b row
evalExpr (And a b) row = liftBool andValues a b row
evalExpr (Or a b) row = liftBool orValues a b row
evalExpr (IsNull a) row = do
v <- evalExpr a row
Right (VBool (v == VNull))
evalExpr (IsNotNull a) row = do
v <- evalExpr a row
Right (VBool (v /= VNull))
evalExpr (InList a es negated) row = do
v <- evalExpr a row
vs <- mapM (\e -> evalExpr e row) es
r <- inValues v vs
Right (if negated then threeValuedNot r else r)
evalExpr (ScalarSub _) _ = Left subqueryNeedsDatabase
evalExpr (InSub _ _ _) _ = Left subqueryNeedsDatabase
evalExpr (ExistsSub _ _) _ = Left subqueryNeedsDatabase
evalExpr agg _ = Left ("aggregate functions are not allowed here: " ++ show agg)
-- | 子查询要连上存储才能跑
subqueryNeedsDatabase :: String
subqueryNeedsDatabase = "subquery needs a database to run"
-- | IN 三值逻辑:命中为真,碰 NULL 得 NULL
inValues :: Value -> [Value] -> Either String Value
inValues VNull _ = Right VNull
inValues v vs = do
results <- mapM (compareValues (== EQ) v) vs
Right $
if VBool True `elem` results
then VBool True
else if VNull `elem` results then VNull else VBool False
-- | NOT 的三值取反
threeValuedNot :: Value -> Value
threeValuedNot (VBool b) = VBool (not b)
threeValuedNot _ = VNull
-- | 表达式用到了哪些列
colsInExpr :: Expr -> [String]
colsInExpr (Col c) = [c]
colsInExpr (Add a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Sub a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Mul a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Div a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Neg a) = colsInExpr a
colsInExpr (Gt a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Lt a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Eq a b) = colsInExpr a ++ colsInExpr b
colsInExpr (And a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Or a b) = colsInExpr a ++ colsInExpr b
colsInExpr (IsNull a) = colsInExpr a
colsInExpr (IsNotNull a) = colsInExpr a
colsInExpr (CountOf a) = colsInExpr a
colsInExpr (SumOf a) = colsInExpr a
colsInExpr (AvgOf a) = colsInExpr a
colsInExpr (MinOf a) = colsInExpr a
colsInExpr (MaxOf a) = colsInExpr a
colsInExpr (ScalarSub sq) = subqueryRefs sq
colsInExpr (InSub a sq _) = colsInExpr a ++ subqueryRefs sq
colsInExpr (InList a es _) = colsInExpr a ++ concatMap colsInExpr es
colsInExpr (ExistsSub sq _) = subqueryRefs sq
colsInExpr _ = []
-- | 表达式里有没有子查询
hasSubquery :: Expr -> Bool
hasSubquery e = case e of
ScalarSub _ -> True
InSub _ _ _ -> True
InList a es _ -> any hasSubquery (a : es)
ExistsSub _ _ -> True
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
Div a b -> both a b
Neg a -> hasSubquery a
Gt a b -> both a b
Lt a b -> both a b
Eq a b -> both a b
And a b -> both a b
Or a b -> both a b
IsNull a -> hasSubquery a
IsNotNull a -> hasSubquery a
CountOf a -> hasSubquery a
SumOf a -> hasSubquery a
AvgOf a -> hasSubquery a
MinOf a -> hasSubquery a
MaxOf a -> hasSubquery a
_ -> False
where
-- | 两个子表达式里任意一个有
both a b = hasSubquery a || hasSubquery b
-- | 表达式里出现的聚合调用(按出现顺序)
aggregatesIn :: Expr -> [Expr]
aggregatesIn e = case e of
a@CountAll -> [a]
a@(CountOf _) -> [a]
a@(SumOf _) -> [a]
a@(AvgOf _) -> [a]
a@(MinOf _) -> [a]
a@(MaxOf _) -> [a]
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
Div a b -> both a b
Neg a -> aggregatesIn a
Gt a b -> both a b
Lt a b -> both a b
Eq a b -> both a b
And a b -> both a b
Or a b -> both a b
IsNull a -> aggregatesIn a
IsNotNull a -> aggregatesIn a
_ -> []
where
-- | 两个子表达式的聚合拼起来
both a b = aggregatesIn a ++ aggregatesIn b
-- | 表达式里有没有聚合调用
hasAggregate :: Expr -> Bool
hasAggregate = not . null . aggregatesIn
-- | 聚合之外的列引用(聚合参数里的列不算)
bareColumns :: Expr -> [String]
bareColumns e = case e of
Col c -> [c]
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
Div a b -> both a b
Neg a -> bareColumns a
Gt a b -> both a b
Lt a b -> both a b
Eq a b -> both a b
And a b -> both a b
Or a b -> both a b
IsNull a -> bareColumns a
IsNotNull a -> bareColumns a
ScalarSub sq -> subqueryRefs sq
InSub a sq _ -> bareColumns a ++ subqueryRefs sq
InList a es _ -> bareColumns a ++ concatMap bareColumns es
ExistsSub sq _ -> subqueryRefs sq
_ -> []
where
-- | 两个子表达式的裸列拼起来
both a b = bareColumns a ++ bareColumns b
-- | 判断条件真假:只有 TRUE 通过,NULL 当作不通过
evalCondForRow :: Expr -> Row -> Either String Bool
evalCondForRow e row = do
v <- evalExpr e row
case v of
VBool b -> Right b
VNull -> Right False
_ -> Left "type error: WHERE condition must be a boolean"
-- | 把二元运算提升到 Either
liftBinOp ::
(Value -> Value -> Either String Value) ->
Expr ->
Expr ->
Row ->
Either String Value
liftBinOp f a b row = join (f <$> evalExpr a row <*> evalExpr b row)
-- | 整数算术与浮点算术成对出现
data Arith = Arith
{ arithInt :: Int -> Int -> Either String Int
, arithFloat :: Double -> Double -> Either String Double
}
-- | 一边是 NULL 就出 NULL,两边都是整数就出整数
liftArith :: Arith -> Expr -> Expr -> Row -> Either String Value
liftArith ops a b row = liftBinOp go a b row
where
-- | NULL 出 NULL,整数配整数走整数算术
go VNull _ = Right VNull
go _ VNull = Right VNull
go (VInt x) (VInt y) = VInt <$> arithInt ops x y
go x y = case (asDouble x, asDouble y) of
(Just p, Just q) -> VFloat <$> arithFloat ops p q
_ -> Left "type error: expected two numbers"
-- | 能当数用的值
asDouble :: Value -> Maybe Double
asDouble (VInt n) = Just (fromIntegral n)
asDouble (VFloat d) = Just d
asDouble _ = Nothing
-- | 加法
addArith :: Arith
addArith = Arith (\x y -> Right (x + y)) (\x y -> Right (x + y))
-- | 减法
subArith :: Arith
subArith = Arith (\x y -> Right (x - y)) (\x y -> Right (x - y))
-- | 乘法
mulArith :: Arith
mulArith = Arith (\x y -> Right (x * y)) (\x y -> Right (x * y))
-- | 整数除法向零截断,溢出返回错误;浮点除法不能除以零
divArith :: Arith
divArith = Arith intDiv floatDiv
where
-- | 整数除法:除零与溢出都报错
intDiv _ 0 = Left "division by zero"
intDiv x y
| x == minBound && y == -1 = Left "integer division overflow"
| otherwise = Right (x `quot` y)
-- | 浮点除法:不能除以零
floatDiv _ 0 = Left "division by zero"
floatDiv x y = Right (x / y)
-- | 比较:任一侧是 NULL 就给 NULL
liftCompare ::
(Ordering -> Bool) ->
Expr ->
Expr ->
Row ->
Either String Value
liftCompare rel a b row = liftBinOp (compareValues rel) a b row
-- | 比较两个值:NULL 给 NULL,其余按数、串、布尔比
compareValues :: (Ordering -> Bool) -> Value -> Value -> Either String Value
compareValues rel x y
| x == VNull || y == VNull = Right VNull
| Just p <- asDouble x, Just q <- asDouble y = Right (VBool (rel (compare p q)))
| (VStr p, VStr q) <- (x, y) = Right (VBool (rel (compare p q)))
| (VBool p, VBool q) <- (x, y) = Right (VBool (rel (compare p q)))
| otherwise = Left "type error: cannot compare these values"
-- | 布尔二元运算
liftBool ::
(Value -> Value -> Either String Value) ->
Expr ->
Expr ->
Row ->
Either String Value
liftBool f = liftBinOp f
-- | 逻辑与的三值真值表
andValues :: Value -> Value -> Either String Value
andValues (VBool False) _ = Right (VBool False)
andValues _ (VBool False) = Right (VBool False)
andValues (VBool True) (VBool True) = Right (VBool True)
andValues VNull (VBool True) = Right VNull
andValues (VBool True) VNull = Right VNull
andValues VNull VNull = Right VNull
andValues _ _ = Left "type error: expected two booleans"
-- | 逻辑或的三值真值表
orValues :: Value -> Value -> Either String Value
orValues (VBool True) _ = Right (VBool True)
orValues _ (VBool True) = Right (VBool True)
orValues (VBool False) (VBool False) = Right (VBool False)
orValues VNull (VBool False) = Right VNull
orValues (VBool False) VNull = Right VNull
orValues VNull VNull = Right VNull
orValues _ _ = Left "type error: expected two booleans"
@@ -0,0 +1,52 @@
module ChuSQL.Core.Engine.Algebra.Op (RelOp (..), renderPlan) where
import ChuSQL.Core.Model (Value)
import ChuSQL.Core.Engine.Syntax.AST (Expr, JoinKind (..), SortDir)
-- 关系代数算子定义与算子树文本渲染。
data RelOp
= Scan (Maybe String) String (Maybe [String])
| Lookup (Maybe String) String String Value
| Range (Maybe String) String String (Maybe (Value, Bool)) (Maybe (Value, Bool))
| Filter Expr RelOp
| Project [String] RelOp
| Compute [(String, Expr)] RelOp
| Aggregate [String] [(String, Expr)] RelOp
| Unit
| Sort [(String, SortDir)] RelOp
| Limit Int RelOp
| Join JoinKind RelOp RelOp Expr
deriving (Show, Eq)
-- | 连接的显示名
kindLabel :: JoinKind -> String
kindLabel InnerJoin = "Join"
kindLabel LeftJoin = "LeftJoin"
-- | 渲染成缩进文本
renderPlan :: RelOp -> String
renderPlan = go 0
where
-- | 按缩进渲染一个算子
go ind op = case op of
Scan a t cols -> pad ++ "Scan " ++ show a ++ " " ++ show t ++ maybe "" ((" columns=" ++) . show) cols
Lookup _ t c k -> pad ++ "Lookup " ++ show t ++ "." ++ show c ++ " = " ++ show k
Range _ t c lo hi -> pad ++ "Range " ++ show t ++ "." ++ show c ++ " " ++ show lo ++ " .. " ++ show hi
Filter e x -> pad ++ "Filter " ++ show e ++ "\n" ++ go (ind + 1) x
Project c x -> pad ++ "Project " ++ show c ++ "\n" ++ go (ind + 1) x
Compute c x -> pad ++ "Compute " ++ show c ++ "\n" ++ go (ind + 1) x
Aggregate keys aggs x ->
pad
++ "Aggregate keys="
++ show keys
++ " aggs="
++ show [label ++ " <- " ++ show e | (label, e) <- aggs]
++ "\n"
++ go (ind + 1) x
Unit -> pad ++ "Unit"
Sort s x -> pad ++ "Sort " ++ show s ++ "\n" ++ go (ind + 1) x
Limit n x -> pad ++ "Limit " ++ show n ++ "\n" ++ go (ind + 1) x
Join k l r c -> pad ++ kindLabel k ++ " on " ++ show c ++ "\n" ++ go (ind + 1) l ++ "\n" ++ go (ind + 1) r
where
pad = replicate (ind * 2) ' '
@@ -0,0 +1,238 @@
module ChuSQL.Core.Engine.Algebra.Optimize (optimize, pushProject, relOpCols) where
import ChuSQL.Core.Engine.Algebra.Expr (colsInExpr, evalExpr)
import ChuSQL.Core.Engine.Algebra.Op (RelOp (..))
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), JoinKind (..))
import Data.List (nub)
-- 查询优化:谓词下推、投影下推与常量折叠。
-- | 算子会产出哪些列
relOpCols :: Database -> RelOp -> [String]
relOpCols _ Unit = []
relOpCols _ (Compute items _) = map fst items
relOpCols _ (Aggregate keys aggs _) = keys ++ map fst aggs
relOpCols _ (Scan mAlias _ (Just cols)) = map (qualify mAlias) cols
relOpCols db (Scan mAlias tbl Nothing) =
case lookup tbl db of
Nothing -> []
Just t -> map (prefix ++) (colNames t)
where
prefix = maybe "" (++ ".") mAlias
relOpCols db (Lookup mAlias tbl _ _) = relOpCols db (Scan mAlias tbl Nothing)
relOpCols db (Range mAlias tbl _ _ _) = relOpCols db (Scan mAlias tbl Nothing)
relOpCols db (Filter _ x) = relOpCols db x
relOpCols _ (Project ["*"] _) = ["*"]
relOpCols _ (Project cols _) = cols
relOpCols db (Sort _ x) = relOpCols db x
relOpCols db (Limit _ x) = relOpCols db x
relOpCols db (Join _ l r _) = relOpCols db l ++ relOpCols db r
-- | need 是否都在 have 里
isSubsetOf :: [String] -> [String] -> Bool
isSubsetOf _ ["*"] = True
isSubsetOf need have = all (`elem` have) need
-- | 把 AND 链拆成清单
splitConj :: Expr -> [Expr]
splitConj (And a b) = splitConj a ++ splitConj b
splitConj e = [e]
-- | 把条件清单用 AND 串起来
combineConj :: [Expr] -> Maybe Expr
combineConj [] = Nothing
combineConj (e : es) = Just (foldl And e es)
-- | 条件按列归属分两边
partitionPred :: [String] -> [Expr] -> ([Expr], [Expr])
partitionPred have = foldr pick ([], [])
where
-- | 按列归属收到左边或右边
pick e (yes, no)
| colsInExpr e `isSubsetOf` have = (e : yes, no)
| otherwise = (yes, e : no)
-- | 依次套上若干 Filter
applyFilters :: [Expr] -> RelOp -> RelOp
applyFilters [] op = op
applyFilters (e : es) op = applyFilters es (Filter e op)
-- | Filter 压在内连接上时下推
pushJoin :: Database -> Expr -> RelOp -> RelOp -> Expr -> RelOp
pushJoin db p l r c =
let preds = splitConj p
lCols = relOpCols db l
rCols = relOpCols db r
(lPs, rest1) = partitionPred lCols preds
(rPs, bothPs) = partitionPred rCols rest1
l' = applyFilters lPs l
r' = applyFilters rPs r
joined = Join InnerJoin l' r' c
in case combineConj bothPs of
Nothing -> joined
Just p' -> Filter p' joined
-- | Filter 压在左连接上时只下推纯左表的谓词
pushLeftJoin :: Database -> Expr -> RelOp -> RelOp -> Expr -> RelOp
pushLeftJoin db p l r c =
let (lPs, rest) = partitionPred (relOpCols db l) (splitConj p)
joined = Join LeftJoin (applyFilters lPs l) r c
in case combineConj rest of
Nothing -> joined
Just p' -> Filter p' joined
-- | 合并两层 Filter 或挪到 Sort 下
pushOne :: Database -> RelOp -> RelOp
pushOne _ (Filter p (Filter q x))
| not (hasDivision p || hasDivision q) = Filter (And p q) x
pushOne _ (Filter p (Sort spec x))
| not (hasDivision p) = Sort spec (Filter p x)
pushOne _ op = op
-- | 除法可能报错,不能提前或延后求值
hasDivision :: Expr -> Bool
hasDivision e = case e of
Div _ _ -> True
Neg a -> hasDivision a
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
Gt a b -> both a b
Lt a b -> both a b
Eq a b -> both a b
And a b -> both a b
Or a b -> both a b
_ -> False
where
-- | 两个子表达式里任意一个有除法
both a b = hasDivision a || hasDivision b
-- | 检查子计划中可能报错的除法
planHasDivision :: RelOp -> Bool
planHasDivision (Filter p x) = hasDivision p || planHasDivision x
planHasDivision (Join _ l r c) = hasDivision c || planHasDivision l || planHasDivision r
planHasDivision (Compute items x) = any (hasDivision . snd) items || planHasDivision x
planHasDivision (Project _ x) = planHasDivision x
planHasDivision (Sort _ x) = planHasDivision x
planHasDivision (Limit _ x) = planHasDivision x
planHasDivision _ = False
-- | 是否要全部列
needsAll :: [String] -> Bool
needsAll = elem allColumns
-- | 叶子算子的投影下推
pushProjectLeaf :: Database -> [String] -> RelOp -> RelOp
pushProjectLeaf db need leaf
| needsAll need = leaf
| all (`elem` need) (relOpCols db leaf) = leaf
| otherwise = Project need leaf
-- | 把需要哪些列往下传
pushProject :: Database -> [String] -> RelOp -> RelOp
pushProject _ _ Unit = Unit
pushProject db _ (Compute items x) = Compute items (pushProject db (concatMap (colsInExpr . snd) items) x)
pushProject db _ (Aggregate keys aggs x) =
Aggregate keys aggs (pushProject db (keys ++ concatMap (colsInExpr . snd) aggs) x)
pushProject db _ (Project cols x) = case pushProject db cols x of
Project cols' y | cols' == cols -> Project cols y
other -> Project cols other
pushProject db need (Filter p x) = Filter p (pushProject db (need ++ colsInExpr p) x)
pushProject db need (Sort spec x) = Sort spec (pushProject db (need ++ map fst spec) x)
pushProject db need (Limit n x) = Limit n (pushProject db need x)
pushProject db need (Join kind l r c)
| needsAll need = Join kind (pushProject db need l) (pushProject db need r) c
| otherwise =
let tot = need ++ colsInExpr c
lCols = relOpCols db l
rCols = relOpCols db r
lNeed = if needsAll lCols then ["*"] else [x | x <- tot, x `elem` lCols]
rNeed = if needsAll rCols then ["*"] else [x | x <- tot, x `elem` rCols]
in Join kind (pushProject db lNeed l) (pushProject db rNeed r) c
pushProject db need leaf@(Scan a t _)
| needsAll need = leaf
| all (`elem` need) (relOpCols db leaf) = leaf
| otherwise = Scan a t (Just (nub (map (unqualify a) need)))
pushProject db need leaf@(Lookup _ _ _ _) = pushProjectLeaf db need leaf
pushProject db need leaf@(Range _ _ _ _ _) = pushProjectLeaf db need leaf
-- | 折一个表达式节点
foldNode :: Expr -> Expr
foldNode e
| null (colsInExpr e) = case evalExpr e [] of
Right VNull -> LitNull
Right (VInt n) -> LitInt n
Right (VFloat d) -> LitFloat d
Right (VStr s) -> LitStr s
Right (VBool b) -> LitBool b
Left _ -> e
| otherwise = e
-- | 自底向上折整棵表达式
foldConstants :: Expr -> Expr
foldConstants (Add a b) = foldNode (Add (foldConstants a) (foldConstants b))
foldConstants (Sub a b) = foldNode (Sub (foldConstants a) (foldConstants b))
foldConstants (Mul a b) = foldNode (Mul (foldConstants a) (foldConstants b))
foldConstants (Div a b) = foldNode (Div (foldConstants a) (foldConstants b))
foldConstants (Neg a) = foldNode (Neg (foldConstants a))
foldConstants (Gt a b) = foldNode (Gt (foldConstants a) (foldConstants b))
foldConstants (Lt a b) = foldNode (Lt (foldConstants a) (foldConstants b))
foldConstants (Eq a b) = foldNode (Eq (foldConstants a) (foldConstants b))
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
rewriteNode _ (Filter (Eq (Col k) (LitInt v)) (Scan mAlias t Nothing)) =
Lookup mAlias t (unqualify mAlias k) (VInt v)
rewriteNode _ (Filter (Eq (Col k) (LitStr v)) (Scan mAlias t Nothing)) =
Lookup mAlias t (unqualify mAlias k) (VStr v)
rewriteNode _ (Filter (And (Gt (Col k1) (LitInt lo)) (Lt (Col k2) (LitInt hi))) (Scan mAlias t Nothing))
| k1 == k2 = Range mAlias t (unqualify mAlias k1) (Just (VInt lo, False)) (Just (VInt hi, False))
rewriteNode _ (Filter (And (Gt (Col k1) (LitStr lo)) (Lt (Col k2) (LitStr hi))) (Scan mAlias t Nothing))
| k1 == k2 = Range mAlias t (unqualify mAlias k1) (Just (VStr lo, False)) (Just (VStr hi, False))
rewriteNode _ (Filter (Gt (Col k) (LitInt v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) (Just (VInt v, False)) Nothing
rewriteNode _ (Filter (Gt (Col k) (LitStr v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) (Just (VStr v, False)) Nothing
rewriteNode _ (Filter (Lt (Col k) (LitInt v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, False))
rewriteNode _ (Filter (Lt (Col k) (LitStr v)) (Scan mAlias t Nothing)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, False))
rewriteNode db (Filter p (Join kind l r c))
| not (hasDivision p || planHasDivision (Join kind l r c)) =
case kind of
InnerJoin -> pushJoin db p l r c
LeftJoin -> pushLeftJoin db p l r c
rewriteNode db (Filter p x) = case foldConstants p of
LitBool True -> x
p' -> pushOne db (Filter p' x)
rewriteNode _ (Project ["*"] x) = x
rewriteNode _ (Compute items x) = Compute [(label, foldConstants e) | (label, e) <- items] x
rewriteNode db op = pushOne db op
-- | 自底向上遍历一次
optimizeRelOp :: Database -> RelOp -> RelOp
optimizeRelOp db = rewriteNode db . descend
where
-- | 先递归处理子算子
descend (Filter p x) = Filter p (optimizeRelOp db x)
descend (Project cols x) = Project cols (optimizeRelOp db x)
descend (Compute items x) = Compute items (optimizeRelOp db x)
descend (Aggregate keys aggs x) = Aggregate keys aggs (optimizeRelOp db x)
descend (Sort spec x) = Sort spec (optimizeRelOp db x)
descend (Limit n x) = Limit n (optimizeRelOp db x)
descend (Join kind l r c) = Join kind (optimizeRelOp db l) (optimizeRelOp db r) c
descend x = x
-- | 反复跑到不动点
optimizePredicates :: Database -> RelOp -> RelOp
optimizePredicates db op =
let op' = optimizeRelOp db op
in if op' == op then op else optimizePredicates db op'
-- | 优化总入口
optimize :: Database -> RelOp -> RelOp
optimize db op = pushProject db ["*"] (optimizePredicates db op)
@@ -0,0 +1,88 @@
module ChuSQL.Core.Engine.Algebra.Planner (translate) where
import ChuSQL.Core.Engine.Algebra.Expr (aggregatesIn)
import ChuSQL.Core.Engine.Algebra.Op
import ChuSQL.Core.Engine.Syntax.AST
-- 计划生成:把语句翻译成算子树。
-- | 把 FROM 子句翻成 Scan / Join
fromToRelOp :: FromClause -> RelOp
fromToRelOp FromUnit = Unit
fromToRelOp (FromTable mAlias tbl) = Scan mAlias tbl Nothing
fromToRelOp (FromJoin kind left mAlias tbl cond) =
Join kind (fromToRelOp left) (Scan mAlias tbl Nothing) cond
-- | 带上 WHERE 的来源
source :: FromClause -> Maybe Expr -> RelOp
source fromC mWhere = case mWhere of
Nothing -> fromToRelOp fromC
Just w -> Filter w (fromToRelOp fromC)
-- | 给用到的聚合起内部列名,同一个聚合只算一次
nameAggs :: [(String, Expr)] -> [(String, Expr)]
nameAggs items = go (1 :: Int) [] (concatMap (aggregatesIn . snd) items)
where
-- | 逐个聚合起名,重复的不再起名
go :: Int -> [(String, Expr)] -> [Expr] -> [(String, Expr)]
go _ acc [] = reverse acc
go n acc (e : es)
| any ((== e) . snd) acc = go n acc es
| otherwise = go (n + 1) (("agg" ++ show n, e) : acc) es
-- | 把聚合调用换成对聚合结果的引用
rewriteAggs :: [(String, Expr)] -> Expr -> Expr
rewriteAggs aggs e = case [label | (label, agg) <- aggs, agg == e] of
(label : _) -> Col label
[] -> case e of
Add a b -> Add (go a) (go b)
Sub a b -> Sub (go a) (go b)
Mul a b -> Mul (go a) (go b)
Div a b -> Div (go a) (go b)
Neg a -> Neg (go a)
Gt a b -> Gt (go a) (go b)
Lt a b -> Lt (go a) (go b)
Eq a b -> Eq (go a) (go b)
And a b -> And (go a) (go b)
Or a b -> Or (go a) (go b)
IsNull a -> IsNull (go a)
IsNotNull a -> IsNotNull (go a)
other -> other
where
go = rewriteAggs aggs
-- | 过滤 + 分组:没有分组也没有聚合时原样返回
grouped :: [String] -> [(String, Expr)] -> RelOp -> (RelOp, [(String, Expr)])
grouped keys items base
| null keys && null aggs = (base, items)
| otherwise = (Aggregate keys aggs base, [(label, rewriteAggs aggs e) | (label, e) <- items])
where
aggs = nameAggs items
-- | 排序 + 截断
limited :: [(String, SortDir)] -> Maybe Int -> RelOp -> RelOp
limited orderBy mLimit op = case mLimit of
Nothing -> sorted
Just n -> Limit n sorted
where
sorted = case orderBy of
[] -> op
spec -> Sort spec op
-- | 翻成:过滤 -> 分组 -> 排序 -> 截断 -> 取列
translate :: Statement -> Either String RelOp
translate q = case q of
Select
{ selectCols = cols
, selectFrom = fromC
, selectWhere = mWhere
, selectGroupBy = groupBy
, selectOrderBy = orderBy
, selectLimit = mLimit
} -> do
let (plan, _) = grouped groupBy [] (source fromC mWhere)
Right (Project cols (limited orderBy mLimit plan))
SelectExpr items fromC mWhere groupBy orderBy mLimit -> do
let (plan, items') = grouped groupBy items (source fromC mWhere)
Right (Compute items' (limited orderBy mLimit plan))
_ -> Left "only SELECT is supported by translate"
@@ -0,0 +1,52 @@
module ChuSQL.Core.Engine.Algebra.Sort (sortRows, compareValue) where
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import Data.List (sortBy)
-- 排序:按多列规格排出结果行,每列可升可降。
-- | 反转比较结果
flipOrdering :: Ordering -> Ordering
flipOrdering LT = GT
flipOrdering GT = LT
flipOrdering EQ = EQ
-- | 比较两个值:NULL 最小,数值跨整数与小数比较
compareValue :: Value -> Value -> Ordering
compareValue VNull VNull = EQ
compareValue VNull _ = LT
compareValue _ VNull = GT
compareValue (VInt a) (VInt b) = compare a b
compareValue (VFloat a) (VFloat b) = compare a b
compareValue (VInt a) (VFloat b) = compare (fromIntegral a) b
compareValue (VFloat a) (VInt b) = compare a (fromIntegral b)
compareValue (VStr a) (VStr b) = compare a b
compareValue (VBool a) (VBool b) = compare a b
compareValue _ _ = EQ
-- | 取出排序键(按 spec 顺序,每行只查一次)
sortKey :: [(String, SortDir)] -> Row -> [Maybe Value]
sortKey spec row = [lookup col row | (col, _) <- spec]
-- | 比较两个排序键
compareKeys :: [(String, SortDir)] -> [Maybe Value] -> [Maybe Value] -> Ordering
compareKeys [] _ _ = EQ
compareKeys ((_, dir) : rest) (a : as) (b : bs) =
case (a, b) of
(Just v1, Just v2) ->
case compareValue v1 v2 of
EQ -> compareKeys rest as bs
o -> if dir == Desc then flipOrdering o else o
_ -> compareKeys rest as bs
compareKeys _ _ _ = EQ
-- | 按排序规格排序若干行
sortRows :: [(String, SortDir)] -> [Row] -> [Row]
sortRows [] rows = rows
sortRows spec rows = map snd (sortBy cmp (map decorate rows))
where
-- | 取键并带上原行
decorate row = (sortKey spec row, row)
-- | 比较两个带键的行
cmp (k1, _) (k2, _) = compareKeys spec k1 k2
@@ -0,0 +1,729 @@
module ChuSQL.Core.Engine.Semantic (check, prepare) where
import ChuSQL.Core.Engine.Algebra.Expr (bareColumns, colsInExpr, hasAggregate)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseExpression)
import Data.Char (isAlphaNum, isAscii, toLower)
import Data.List (intercalate, isInfixOf, isPrefixOf, nub)
import Data.Maybe (fromMaybe)
-- 语义检查:表和列在不在、类型对不对,全部在执行前查。
-- | 推导出来的类型:NULL 没有类型,可以放进任何列
data InferredType
= InferNull
| InferType ColumnType
deriving (Show, Eq)
-- | 给列名加上别名前缀
prefixColumns :: Maybe String -> [(String, Column)] -> [(String, Column)]
prefixColumns mAlias cols = [(prefix ++ c, ty) | (c, ty) <- cols]
where
prefix = maybe "" (++ ".") mAlias
-- | 检查期的作用域:本层优先,外层兜底
data Scope = Scope
{ scopeInner :: [(String, Column)]
, scopeOuter :: [(String, Column)]
}
-- | 本层先查,查不到再落外层;本层同名引起歧义时不往外层让
scopeAt :: String -> Scope -> String -> Either String (String, Column)
scopeAt place scope c
| null (scopeOuter scope) = columnAt place (scopeInner scope) c
| otherwise = case columnAt place (scopeInner scope) c of
Right entry -> Right entry
Left err
| "ambiguous column:" `isInfixOf` err -> Left err
| otherwise -> columnAt place (scopeOuter scope) c
-- | 收集 FROM 能提供的列,含外层作用域
checkFrom :: Database -> [(String, Column)] -> FromClause -> Either String [(String, Column)]
checkFrom _ _ FromUnit = Right []
checkFrom db _ (FromTable mAlias tbl) = do
t <- lookupTable db tbl
Right (prefixColumns (Just (fromMaybe tbl mAlias)) (tableCols t))
checkFrom db outer (FromJoin _ left mAlias tbl cond) = do
lcols <- checkFrom db outer left
t <- lookupTable db tbl
let env = lcols ++ prefixColumns (Just (fromMaybe tbl mAlias)) (tableCols t)
checkBool db "ON" (Scope env outer) cond
Right env
-- | 合并两层列,本层优先且不重复
scopedEnv :: [(String, Column)] -> [(String, Column)] -> [(String, Column)]
scopedEnv env outer = env ++ [o | o <- outer, o `notElem` env]
-- | 把作用域摊平成一层的列清单
scopeFlat :: Scope -> [(String, Column)]
scopeFlat scope = scopedEnv (scopeInner scope) (scopeOuter scope)
-- | 拼“列不存在”的报错
missingColumn :: String -> String -> [(String, Column)] -> String
missingColumn place c env =
"unknown column in " ++ place ++ ": " ++ c ++ hint
where
hint
| null env = ""
| otherwise = " (available: " ++ intercalate ", " (map (reverse . takeWhile (/= '.') . reverse . fst) env) ++ "; qualified: " ++ intercalate ", " (map fst env) ++ ")"
-- | 检查列是否都在环境里
checkColumns :: String -> Scope -> [String] -> Either String ()
checkColumns place env = mapM_ (\c -> if c == allColumns then Right () else () <$ scopeAt place env c)
-- | 在一层里查一列,报错时带上子句名与可用列
columnAt :: String -> [(String, Column)] -> String -> Either String (String, Column)
columnAt place env c = case resolveColumn c env of
Right entry -> Right entry
Left err | "ambiguous column:" `isPrefixOf` err -> Left (place ++ ": " ++ err)
Left _ -> Left (missingColumn place c env)
-- | 聚合不能出现在 WHERE 里
checkNoAggregate :: String -> Expr -> Either String ()
checkNoAggregate place e
| hasAggregate e = Left (place ++ ": aggregate functions are not allowed here")
| otherwise = Right ()
-- | 分组查询里,没被聚合的列必须出现在 GROUP BY 里
checkGrouping :: Scope -> [String] -> [(String, Expr)] -> Either String ()
checkGrouping env keys items
| null keys && not (any (hasAggregate . snd) items) = Right ()
| otherwise = do
checkColumns "GROUP BY" env keys
mapM_ okCol (nub (concatMap (bareColumns . snd) items))
where
-- | 这一列能不能出现在分组查询里
okCol c
| c == allColumns = Left "SELECT: * cannot be selected together with GROUP BY"
| c `elem` keys = Right ()
| otherwise = Left ("SELECT: column " ++ c ++ " must appear in GROUP BY or be aggregated")
-- | 渲染一个推导类型
renderType :: InferredType -> String
renderType InferNull = "NULL"
renderType (InferType t) = typeLabel t
-- | 推导表达式的类型(子查询也在这里整条查一遍)
inferExpr :: Database -> String -> Scope -> Expr -> Either String InferredType
inferExpr _ place env (Col c) = InferType . columnType . snd <$> scopeAt place env c
inferExpr _ _ _ LitNull = Right InferNull
inferExpr _ _ _ (LitInt _) = Right (InferType CInt)
inferExpr _ _ _ (LitFloat _) = Right (InferType CFloat)
inferExpr _ _ _ (LitStr _) = Right (InferType CStr)
inferExpr _ _ _ (LitBool _) = Right (InferType CBool)
inferExpr _ _ _ (LitDate _) = Right (InferType CDate)
inferExpr _ _ _ (LitTimestamp _) = Right (InferType CTimestamp)
inferExpr _ _ _ (LitBlob _) = Right (InferType CBlob)
inferExpr db place env (Add a b) = arithmetic db place env a b
inferExpr db place env (Sub a b) = arithmetic db place env a b
inferExpr db place env (Mul a b) = arithmetic db place env a b
inferExpr db place env (Div a b) = arithmetic db place env a b
inferExpr db place env (Neg a) = do
t <- inferExpr db place env a
case t of
InferNull -> Right InferNull
InferType x
| numericType x -> Right t
| otherwise -> Left (place ++ ": operator needs TInt on both sides, got TInt and " ++ typeLabel x)
inferExpr db place env (Gt a b) = comparison db place env a b
inferExpr db place env (Lt a b) = comparison db place env a b
inferExpr db place env (Eq a b) = comparison db place env a b
inferExpr db place env (And a b) = boolean db place env a b
inferExpr db place env (Or a b) = boolean db place env a b
inferExpr db place env (IsNull a) = inferExpr db place env a >> Right (InferType CBool)
inferExpr db place env (IsNotNull a) = inferExpr db place env a >> Right (InferType CBool)
inferExpr _ _ _ CountAll = Right (InferType CInt)
inferExpr db place env (CountOf a) = do
_ <- aggregateArg db place env a
Right (InferType CInt)
inferExpr db place env (SumOf a) = aggregateArg db place env a >>= numericResult "SUM" place
inferExpr db place env (AvgOf a) = do
t <- aggregateArg db place env a
case t of
InferNull -> Right InferNull
InferType x
| numericType x -> Right (InferType CFloat)
| otherwise -> Left (place ++ ": AVG needs a number, got " ++ typeLabel x)
inferExpr db place env (MinOf a) = aggregateArg db place env a
inferExpr db place env (MaxOf a) = aggregateArg db place env a
inferExpr db place env (ScalarSub sq) = do
cols <- inferSubqueryOutput db env sq
case cols of
[(_, t)] -> Right t
_ -> Left (place ++ ": scalar subquery must return exactly one column")
inferExpr db place env (InSub a sq _) = do
ta <- inferExpr db place env a
cols <- inferSubqueryOutput db env sq
case cols of
[(_, t)]
| compatible ta t -> Right (InferType CBool)
| otherwise -> Left (place ++ ": IN subquery column has type " ++ renderType t ++ ", expected " ++ renderType ta)
_ -> Left (place ++ ": IN subquery must return exactly one column")
inferExpr db place env (InList a es _) = do
ta <- inferExpr db place env a
ts <- mapM (inferExpr db place env) es
mapM_ (itemType place ta) ts
Right (InferType CBool)
inferExpr _ _ _ (ExistsSub _ _) = Right (InferType CBool)
-- | IN 列表里的每一项都要跟左边的类型对得上
itemType :: String -> InferredType -> InferredType -> Either String ()
itemType place want got
| compatible want got = Right ()
| otherwise = Left (place ++ ": IN list item has type " ++ renderType got ++ ", expected " ++ renderType want)
-- | 推导子查询的输出列与类型
inferSubqueryOutput :: Database -> Scope -> Subquery -> Either String [(String, InferredType)]
inferSubqueryOutput db outerScope sq = case subqueryStatement sq of
q@Select { selectCols = cols } -> do
env <- checkFrom db enclosing (selectFrom q)
checkResolvedWith db enclosing q
mapM (columnTypeOf env) cols
q@SelectExpr { selectItems = items } -> do
env <- checkFrom db enclosing (selectFrom q)
checkResolvedWith db enclosing q
mapM (\(l, e) -> (\t -> (l, t)) <$> inferExpr db "SELECT" (Scope env enclosing) e) items
_ -> Left "subquery must be a SELECT"
where
enclosing = scopeFlat outerScope
-- | 查这一列的推导类型
columnTypeOf env c = do
(_, col) <- scopeAt "SELECT" (Scope env enclosing) c
Right (c, InferType (columnType col))
-- | 聚合的参数:列要在,且不能嵌套聚合
aggregateArg :: Database -> String -> Scope -> Expr -> Either String InferredType
aggregateArg db place env a
| hasAggregate a = Left (place ++ ": nested aggregates are not allowed")
| otherwise = inferExpr db place env a
-- | 数值聚合的结果类型:整数进整数出,其它往浮点靠
numericResult :: String -> String -> InferredType -> Either String InferredType
numericResult fn place t = case t of
InferNull -> Right InferNull
InferType x
| numericType x -> Right (if integerType x then InferType x else InferType CFloat)
| otherwise -> Left (place ++ ": " ++ fn ++ " needs a number, got " ++ typeLabel x)
-- | 算术结果:有小数就出小数,否则是整数
arithResult :: InferredType -> InferredType -> InferredType
arithResult InferNull _ = InferNull
arithResult _ InferNull = InferNull
arithResult (InferType a) (InferType b)
| integerType a && integerType b = InferType CInt
| otherwise = InferType CFloat
-- | 算术两边的类型检查
arithmetic :: Database -> String -> Scope -> Expr -> Expr -> Either String InferredType
arithmetic db place env a b = do
ta <- inferExpr db place env a
tb <- inferExpr db place env b
let want = if any isFloatSide [ta, tb] then CFloat else CInt
mapM_ (numericOperand place (InferType want) (InferType want)) [ta, tb]
Right (arithResult ta tb)
where
-- | 是不是浮点类型
isFloatSide (InferType x) = numericType x && not (integerType x)
isFloatSide _ = False
-- | 一侧必须是数
numericOperand :: String -> InferredType -> InferredType -> InferredType -> Either String ()
numericOperand place want other t = case t of
InferNull -> Right ()
InferType x | numericType x -> Right ()
_ -> Left (place ++ ": operator needs " ++ renderType want ++ " on both sides, got " ++ renderType other ++ " and " ++ renderType t)
-- | 比较两边的类型检查
comparison :: Database -> String -> Scope -> Expr -> Expr -> Either String InferredType
comparison db place env a b = do
ta <- inferExpr db place env a
tb <- inferExpr db place env b
if compatible ta tb
then Right (InferType CBool)
else Left (place ++ ": both sides of = must have the same type, got " ++ renderType ta ++ " and " ++ renderType tb)
-- | 两类能不能比
compatible :: InferredType -> InferredType -> Bool
compatible InferNull _ = True
compatible _ InferNull = True
compatible (InferType a) (InferType b)
| numericType a && numericType b = True
| typeClassOf a == typeClassOf b = True
| typeClassOf a == TemporalClass && typeClassOf b == TextClass = True
| typeClassOf b == TemporalClass && typeClassOf a == TextClass = True
| otherwise = False
-- | 逻辑运算两边的类型检查
boolean :: Database -> String -> Scope -> Expr -> Expr -> Either String InferredType
boolean db place env a b = do
ta <- inferExpr db place env a
tb <- inferExpr db place env b
mapM_ (boolOperand place ta) [ta, tb]
Right (InferType CBool)
where
-- | 挨个查操作数是不是布尔
boolOperand _ _ InferNull = Right ()
boolOperand _ _ (InferType CBool) = Right ()
boolOperand p other t = Left (p ++ ": operator needs TBool on both sides, got " ++ renderType other ++ " and " ++ renderType t)
-- | 要求条件能算出布尔
checkBool :: Database -> String -> Scope -> Expr -> Either String ()
checkBool db place env e = do
checkColumns place env (colsInExpr e)
t <- inferExpr db place env e
case t of
InferNull -> Right ()
InferType CBool -> Right ()
InferType x -> Left (place ++ ": condition must be a boolean, got " ++ typeLabel x)
-- | 检查赋值列和类型
checkTyped :: Database -> String -> Scope -> Table -> (String, Expr) -> Either String ()
checkTyped db place env t (c, e) = do
want <- maybe (Left (missingColumn place c (tableCols t))) Right (colType t c)
got <- inferExpr db place env e
case got of
InferNull -> Right ()
InferType x
| assignable (columnType want) x -> Right ()
| otherwise -> Left (place ++ ": column " ++ c ++ " needs " ++ show want ++ ", got " ++ typeLabel x)
-- | 检查插入值的类型
checkValue :: Database -> Table -> (String, Expr) -> Either String ()
checkValue db = checkTyped db "INSERT" (Scope [] [])
-- | 检查 UPDATE 的赋值
checkAssign :: Database -> Table -> (String, Expr) -> Either String ()
checkAssign db t = checkTyped db "UPDATE" (Scope (tableCols t) []) t
-- | 按语句类型分派检查
check :: Database -> Statement -> Either String ()
check db q = () <$ prepare db q
-- | 执行前解析列名并检查类型
prepare :: Database -> Statement -> Either String Statement
prepare db q = do
resolved <- resolveStatement db q
checkResolved db resolved
pure resolved
-- | 检查已解析的语句
checkResolved :: Database -> Statement -> Either String ()
checkResolved db = checkResolvedWith db []
-- | 检查已解析的语句;outer 是相关子查询能看见的外层列
checkResolvedWith :: Database -> [(String, Column)] -> Statement -> Either String ()
checkResolvedWith db outer q = case q of
Select
{ selectCols = cols
, selectFrom = fromC
, selectWhere = mWhere
, selectGroupBy = groupBy
, selectOrderBy = orderBy
} -> do
env <- checkFrom db outer fromC
let full = Scope env outer
checkColumns "SELECT" full cols
checkGrouping full groupBy (map (\c -> (c, Col c)) cols)
checkColumns "ORDER BY" full (map fst orderBy)
mapM_ (checkNoAggregate "WHERE") mWhere
mapM_ (checkBool db "WHERE" full) mWhere
SelectExpr items fromC mWhere groupBy orderBy _ -> do
env <- checkFrom db outer fromC
let full = Scope env outer
mapM_ (\item -> () <$ inferExpr db "SELECT" full (snd item)) items
checkGrouping full groupBy items
checkColumns "ORDER BY" full (map fst orderBy)
mapM_ (checkNoAggregate "WHERE") mWhere
mapM_ (checkBool db "WHERE" full) mWhere
Insert tbl cols rows -> do
t <- lookupTable db tbl
checkColumns "INSERT" (Scope (tableCols t) []) cols
if null rows
then Left "INSERT: no values"
else mapM_ (mapM_ (checkValue db t) . zip cols) rows
checkInsertTargets t cols
Delete tbl mWhere -> do
t <- lookupTable db tbl
mapM_ (checkBool db "WHERE" (Scope (tableCols t) [])) mWhere
Update tbl assigns mWhere -> do
t <- lookupTable db tbl
checkColumns "UPDATE" (Scope (tableCols t) []) (map fst assigns)
mapM_ (checkAssign db t) assigns
mapM_ (checkBool db "WHERE" (Scope (tableCols t) [])) mWhere
CreateTable name cols -> do
if null name
then Left "CREATE TABLE: empty table name"
else do
let names = map fst cols
if length names /= length (nub names)
then Left ("CREATE TABLE: duplicate column names in " ++ name)
else Right ()
checkColumnDefs (map fst cols) cols
DropTable name ->
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)
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 ()
AddColumn tbl def -> do
t <- lookupTable db tbl
if fst def `elem` tableCols' t
then Left ("ADD COLUMN: column already exists: " ++ fst def)
else Right ()
checkColumnDefs (tableCols' t ++ [fst def]) [def]
RenameColumn tbl old new -> do
t <- lookupTable db tbl
if old `notElem` tableCols' t
then Left ("RENAME COLUMN: unknown column: " ++ old)
else Right ()
if old == idColumn
then Left "RENAME COLUMN: the built-in id column cannot be renamed"
else
if new `elem` tableCols' t
then Left ("RENAME COLUMN: column already exists: " ++ new)
else Right ()
AlterColumnType tbl col ty -> do
t <- lookupTable db tbl
c <- requireColumn "ALTER COLUMN" t col
if columnAutoIncrement c && not (integerType ty)
then Left "ALTER COLUMN: AUTO_INCREMENT column must stay an integer type"
else Right ()
AlterColumnDefault tbl col def -> do
t <- lookupTable db tbl
c <- requireColumn "ALTER COLUMN" t col
mapM_ (\v -> checkDefault c v) def
AlterColumnNull tbl col nullable -> do
t <- lookupTable db tbl
c <- requireColumn "ALTER COLUMN" t col
if not nullable && columnAutoIncrement c
then Left "ALTER COLUMN: AUTO_INCREMENT column cannot be nullable"
else Right ()
CreateUser name password -> checkAccount name password
AlterUser name password -> checkAccount name password
DropUser name -> checkUserName name
CreateRole name -> checkRoleName name
DropRole name -> checkRoleName name
GrantPrivileges privs obj role -> do
mapM_ checkPrivilege privs
checkGrantObject obj
checkRoleName role
RevokePrivileges privs obj role -> do
mapM_ checkPrivilege privs
checkGrantObject obj
checkRoleName role
GrantRole role members -> do
checkRoleName role
mapM_ checkUserName members
RevokeRole role members -> do
checkRoleName role
mapM_ checkUserName members
CreateDatabase name -> checkDatabaseName name
DropDatabase name -> checkDatabaseName name
UseDatabase name -> checkDatabaseName name
ShowDatabases -> Right ()
where
tableCols' = map fst . tableCols
-- | 查一列,没有就报错
requireColumn :: String -> Table -> String -> Either String Column
requireColumn place t c = maybe (Left (place ++ ": unknown column: " ++ c)) Right (colType t c)
-- | 插入时没提到的列能不能自己补上
checkInsertTargets :: Table -> [String] -> Either String ()
checkInsertTargets t cols = mapM_ checkTarget (tableCols t)
where
checkTarget (name, c)
| name `elem` cols = Right ()
| columnAutoIncrement c = Right ()
| columnNullable c = Right ()
| Just _ <- columnDefault c = Right ()
| otherwise = Left ("INSERT: column " ++ name ++ " is NOT NULL and has no default")
-- | 检查一列定义是否合法(names 给 CHECK 用)
checkColumnDefs :: [String] -> [(String, Column)] -> Either String ()
checkColumnDefs names cols = do
mapM_ (\(_, c) -> mapM_ (checkDefault c) (columnDefault c)) cols
mapM_ (\(_, c) -> checkAutoIncrement c) cols
if length [() | (_, c) <- cols, columnAutoIncrement c] > 1
then Left "CREATE TABLE: at most one AUTO_INCREMENT column is allowed"
else Right ()
checkChecks names cols
-- | 自增列必须是整数列
checkAutoIncrement :: Column -> Either String ()
checkAutoIncrement c
| columnAutoIncrement c && not (integerType (columnType c)) = Left "AUTO_INCREMENT: column must be an integer type"
| otherwise = Right ()
-- | 默认值必须能放进这一列
checkDefault :: Column -> Value -> Either String ()
checkDefault c v
| VNull <- v = Right ()
| valueFits (columnType c) v = Right ()
| otherwise = Left ("DEFAULT: value does not fit column type " ++ show c)
-- | CHECK 里的列必须存在
checkChecks :: [String] -> [(String, Column)] -> Either String ()
checkChecks names = mapM_ checkOne
where
-- | 查这一列的 CHECK
checkOne (_, c) = case columnCheck c of
Nothing -> Right ()
Just text -> case parseExpression text of
Left err -> Left ("CHECK: " ++ firstLine err)
Right e -> mapM_ (known e) (colsInExpr e)
-- | 这个列名在不在定义里
known _ col
| col `elem` names = Right ()
| otherwise = Left ("CHECK: unknown column: " ++ col)
firstLine = takeWhile (/= '\n')
-- | 账号名上限
accountNameLimit :: Int
accountNameLimit = 64
-- | 检查库名是否合法
checkDatabaseName :: String -> Either String ()
checkDatabaseName name
| null name || length name > 64 || not (all (\c -> isAscii c && (isAlphaNum c || c == '_')) name) = Left "invalid database name"
| otherwise = Right ()
-- | 口令上限
passwordLimit :: Int
passwordLimit = 256
-- | 账号名:ASCII 字母数字加点、短横线、下划线
checkUserName :: String -> Either String ()
checkUserName name
| null name = Left "USER: empty account name"
| length name > accountNameLimit = Left "USER: account name is too long"
| not (all allowed name) = Left "USER: account name may only contain ASCII letters, digits, dot, dash or underscore"
| otherwise = Right ()
where
-- | 账号名字符规则
allowed c = isAscii c && (isAlphaNum c || c `elem` ("_.-" :: String))
-- | 建号 / 改口令:名字合法且口令不空
checkAccount :: String -> String -> Either String ()
checkAccount name password = do
checkUserName name
if null password
then Left "USER: password must not be empty"
else
if length password > passwordLimit
then Left "USER: password is too long"
else Right ()
-- | 检查角色名是否合法(不许用权限关键字)
checkRoleName :: String -> Either String ()
checkRoleName name
| null name = Left "ROLE: empty role name"
| length name > accountNameLimit = Left "ROLE: role name is too long"
| not (all allowed name) = Left "ROLE: role name may only contain ASCII letters, digits, dot, dash or underscore"
| map toLower name `elem` privilegeNames = Left "ROLE: role name must not be a privilege keyword"
| otherwise = Right ()
where
-- | 角色名字符规则
allowed c = isAscii c && (isAlphaNum c || c `elem` ("_.-" :: String))
-- | 权限关键字(小写,只用于比较)
privilegeNames :: [String]
privilegeNames = ["select", "insert", "update", "delete", "all"]
-- | 权限名只能是这几个
checkPrivilege :: String -> Either String ()
checkPrivilege name
| map toLower name `elem` privilegeNames = Right ()
| otherwise = Left ("GRANT: unknown privilege: " ++ name)
-- | 授权对象:* 表示全部表,否则是一张表(可带一个库前缀)
checkGrantObject :: String -> Either String ()
checkGrantObject "*" = Right ()
checkGrantObject obj
| null obj = Left "GRANT: object name must not be empty"
| length obj > 128 = Left "GRANT: object name is too long"
| not (all allowed obj) = Left "GRANT: object name may only contain ASCII letters, digits, dot, dash or underscore"
| otherwise = Right ()
where
-- | 授权对象字符规则
allowed c = isAscii c && (isAlphaNum c || c `elem` ("_.-" :: String))
-- | 内置 id 列名
idColumn :: String
idColumn = "id"
-- | 把列引用统一为执行期名字(子查询一起解析)
resolveExpr ::
Database ->
[(String, Column)] ->
[(String, Column)] ->
(String -> Either String String) ->
Expr ->
Either String Expr
resolveExpr db env outer f e = case e of
Col c -> Col <$> f c
Add a b -> binary Add a b
Sub a b -> binary Sub a b
Mul a b -> binary Mul a b
Div a b -> binary Div a b
Neg a -> Neg <$> resolveExpr db env outer f a
Gt a b -> binary Gt a b
Lt a b -> binary Lt a b
Eq a b -> binary Eq a b
And a b -> binary And a b
Or a b -> binary Or a b
IsNull a -> IsNull <$> resolveExpr db env outer f a
IsNotNull a -> IsNotNull <$> resolveExpr db env outer f a
CountAll -> Right CountAll
CountOf a -> CountOf <$> resolveExpr db env outer f a
SumOf a -> SumOf <$> resolveExpr db env outer f a
AvgOf a -> AvgOf <$> resolveExpr db env outer f a
MinOf a -> MinOf <$> resolveExpr db env outer f a
MaxOf a -> MaxOf <$> resolveExpr db env outer f a
ScalarSub sq -> ScalarSub <$> resolveSubquery db (env ++ outer) sq
InSub a sq negated -> InSub <$> resolveExpr db env outer f a <*> resolveSubquery db (env ++ outer) sq <*> pure negated
InList a es negated -> InList <$> resolveExpr db env outer f a <*> mapM (resolveExpr db env outer f) es <*> pure negated
ExistsSub sq negated -> ExistsSub <$> resolveSubquery db (env ++ outer) sq <*> pure negated
_ -> Right e
where
-- | 递归处理两个子表达式
binary ctor a b = ctor <$> resolveExpr db env outer f a <*> resolveExpr db env outer f b
-- | 解析子查询,并记下它引用的外层列
resolveSubquery :: Database -> [(String, Column)] -> Subquery -> Either String Subquery
resolveSubquery db outer sq = do
stmt <- resolveSubqueryStatement db outer (subqueryStatement sq)
pure (Subquery stmt (outerRefsOf db stmt))
-- | 子查询里指向外层作用域的列(内层查不到的列就是外层引用)
outerRefsOf :: Database -> Statement -> [String]
outerRefsOf db stmt = nub [c | c <- statementColumns stmt, not (inInner c)]
where
inner = case checkFrom db [] (statementFrom stmt) of
Right env -> env
Left _ -> []
-- | 这一列能不能在内层解析到
inInner c = case resolveColumn c inner of
Right _ -> True
Left _ -> False
-- | 一条语句里出现的所有列引用
statementColumns :: Statement -> [String]
statementColumns stmt = case stmt of
Select cols fromC mWhere groupBy orderBy _ ->
cols ++ fromColumns fromC ++ concatMap colsInExpr mWhere ++ groupBy ++ map fst orderBy
SelectExpr items fromC mWhere groupBy orderBy _ ->
concatMap (colsInExpr . snd) items ++ fromColumns fromC ++ concatMap colsInExpr mWhere ++ groupBy ++ map fst orderBy
_ -> []
where
-- | 取 FROM 里出现的列
fromColumns FromUnit = []
fromColumns (FromTable _ _) = []
fromColumns (FromJoin _ l _ _ c) = fromColumns l ++ colsInExpr c
-- | 取语句的 FROM 子句,没有就是 FromUnit
statementFrom :: Statement -> FromClause
statementFrom Select { selectFrom = fromC } = fromC
statementFrom SelectExpr { selectFrom = fromC } = fromC
statementFrom _ = FromUnit
-- | 解析子查询里的 SELECT
resolveSubqueryStatement :: Database -> [(String, Column)] -> Statement -> Either String Statement
resolveSubqueryStatement db outer q@Select{} = resolveQuery db outer q (map (\c -> (c, Col c)) (selectCols q))
resolveSubqueryStatement db outer q@SelectExpr{} = resolveQuery db outer q (selectItems q)
resolveSubqueryStatement _ _ _ = Left "subquery must be a SELECT"
-- | 列名解析入口:查询走连接解析,改数据的语句按本表列解析
resolveStatement :: Database -> Statement -> Either String Statement
resolveStatement db q = case q of
Select{} -> resolveSelect db q
SelectExpr{} -> resolveSelect db q
Update tbl assigns mWhere -> do
t <- lookupTable db tbl
let env = tableCols t
name = nameIn env "UPDATE"
assigns' <- mapM (\(c, e) -> (\e' -> (c, e')) <$> resolveExpr db env [] name e) assigns
w <- mapM (resolveExpr db env [] (nameIn env "WHERE")) mWhere
pure (Update tbl assigns' w)
Delete tbl mWhere -> do
t <- lookupTable db tbl
w <- mapM (resolveExpr db (tableCols t) [] (nameIn (tableCols t) "WHERE")) mWhere
pure (Delete tbl w)
Insert tbl cols rows -> do
t <- lookupTable db tbl
let env = tableCols t
rows' <- mapM (mapM (resolveExpr db env [] (nameIn env "INSERT"))) rows
pure (Insert tbl cols rows')
other -> Right other
-- | 查一列的物理名
nameIn :: [(String, Column)] -> String -> String -> Either String String
nameIn env place c = fst <$> columnAt place env c
-- | 内层先查,查不到再落到外层;内层同名引起的歧义不往外层让
scopedColumnAt :: String -> [(String, Column)] -> [(String, Column)] -> String -> Either String (String, Column)
scopedColumnAt place inner outer c = scopeAt place (Scope inner outer) c
-- | 单表保留原表头,连接使用唯一前缀
resolveSelect :: Database -> Statement -> Either String Statement
resolveSelect db q@Select{} = resolveQuery db [] q (map (\c -> (c, Col c)) (selectCols q))
resolveSelect db q@SelectExpr{} = resolveQuery db [] q (selectItems q)
resolveSelect _ q = Right q
-- | 解析投影、条件、排序与连接引用
resolveQuery :: Database -> [(String, Column)] -> Statement -> [(String, Expr)] -> Either String Statement
resolveQuery db outer q items = do
env <- checkFrom db outer (selectFrom q)
-- | 单表查询里把裸列名换成物理列名
let physical c = case selectFrom q of
FromTable Nothing tbl -> unqualify (Just tbl) c
_ -> c
-- | 查一列并换算成物理名
name place c = physical . fst <$> scopedColumnAt place env outer c
-- | 换算一个投影项
output (_, Col c) = do
k <- name "SELECT" c
pure (k, Col k)
output (label, e) = do
e' <- resolveExpr db env outer (name "SELECT") e
pure (label, e')
projected <- if items == [("*", Col "*")]
then if null env && selectFrom q == FromUnit
then Left "SELECT: * requires FROM"
else Right [(physical c, Col (physical c)) | (c, _) <- env]
else mapM output items
condition <- mapM (resolveExpr db env outer (name "WHERE")) (selectWhere q)
grouping <- mapM (name "GROUP BY") (selectGroupBy q)
ordering <- mapM (\(c, d) -> (\k -> (k, d)) <$> name "ORDER BY" c) (selectOrderBy q)
source <- case selectFrom q of
single@FromTable{} -> Right single
other -> resolveFrom db outer other
pure $ case q of
Select{} -> Select (map fst projected) source condition grouping ordering (selectLimit q)
_ -> SelectExpr projected source condition grouping ordering (selectLimit q)
-- | 连接两侧默认取表名作别名;ON 里的子查询也能引用外层列
resolveFrom :: Database -> [(String, Column)] -> FromClause -> Either String FromClause
resolveFrom _ _ FromUnit = Right FromUnit
resolveFrom _ _ (FromTable alias tbl) = Right (FromTable (Just (fromMaybe tbl alias)) tbl)
resolveFrom db outer fromC@(FromJoin kind left alias tbl cond) = do
env <- checkFrom db outer fromC
l <- resolveFrom db outer left
c <- resolveExpr db env outer (fmap fst . columnAt "ON" env) cond
pure (FromJoin kind l (Just (fromMaybe tbl alias)) tbl c)
@@ -0,0 +1,110 @@
module ChuSQL.Core.Engine.Storage (
MonadStorage (..),
IndexResult (..),
) where
import ChuSQL.Core.Model (Column, Database, Row, Table (..), Value (..))
-- 存储层抽象:MonadStorage 类与 IndexResult。
data IndexResult
= IndexRows [Row]
| NoIndex
deriving (Show, Eq)
class (Monad m) => MonadStorage m where
-- \| 建一个库
createDatabase :: String -> m (Either String ())
createDatabase _ = pure (Left "database management is unavailable in this storage")
-- \| 删一个库
dropDatabase :: String -> m (Either String ())
dropDatabase _ = pure (Left "database management is unavailable in this storage")
-- \| 切当前库
useDatabase :: String -> m (Either String ())
useDatabase _ = pure (Left "database selection is unavailable in this storage")
-- \| 列出现有库
listDatabases :: m (Either String [String])
listDatabases = pure (Right [])
-- \| 全表扫描
scan :: String -> m (Either String [Row])
-- \| 只返回指定列;空清单保留每行的存在性。
scanColumns :: String -> [String] -> m (Either String [Row])
scanColumns t cols = do
result <- scan t
pure (fmap (map (\row -> [(c, v) | c <- cols, Just v <- [lookup c row]])) result)
-- \| 追加一行
insert :: String -> Row -> m (Either String ())
-- \| 一批行一起追加
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
-- | 这一行要不要删
doomed r = case lookup "id" r of
Just (VInt k) -> k `elem` ks
_ -> False
-- \| 整表替换
replaceAll :: String -> [Row] -> m (Either String ())
-- \| 按某一列的索引取一行(键可以是整数或字符串)
lookupByColumn :: String -> String -> Value -> m (Either String IndexResult)
lookupByColumn _ _ _ = pure (Right NoIndex)
-- \| 按某一列的索引做范围扫描;没有可用索引就回 Nothing
scanRange ::
String ->
String ->
Maybe (Value, Bool) ->
Maybe (Value, Bool) ->
m (Either String (Maybe [Row]))
scanRange _ _ _ _ = pure (Right Nothing)
-- \| 建一张空表
createTable :: String -> [(String, Column)] -> m (Either String ())
-- \| 删除表
dropTable :: String -> m (Either String ())
-- \| 给某一列建索引(id 列要求唯一)
createIndex :: String -> String -> m (Either String ())
createIndex _ _ = pure (Right ())
-- \| 去掉某一列的索引
dropIndex :: String -> String -> m (Either String ())
dropIndex _ _ = pure (Right ())
-- \| 删一列,连它的索引与数据一起去掉
dropColumn :: String -> String -> m (Either String ())
-- \| ALTER 用:整表换列定义与全部行
replaceSchema :: String -> [(String, Column)] -> [Row] -> m (Either String ())
-- \| 取全库快照(表名 + 列 + 全部行)
snapshot :: m Database
-- \| 取全库结构:只要表名和列,不要行
schema :: m Database
schema = fmap (map withoutRows) snapshot
where
-- | 表定义去掉所有行
withoutRows (name, t) = (name, t {tableRows = []})
@@ -0,0 +1,90 @@
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE ImportQualifiedPost #-}
module ChuSQL.Core.Engine.Storage.FFI (
StorageHandle,
storageVersion,
openStorage,
closeStorage,
storageRequest,
) where
import Control.Exception (SomeException, try)
import Data.ByteString qualified as BS
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Text.Encoding.Error (lenientDecode)
import Foreign
import Foreign.C
-- Rust 存储库 cdylib 的 C ABI 绑定(ffi.rs),收发单行 JSON。
-- | 存储实例句柄(Rust 侧的 *mut Storage)
newtype StorageHandle = StorageHandle (Ptr ())
foreign import ccall unsafe "chusql_storage_version"
cStorageVersion :: IO CString
foreign import ccall safe "chusql_storage_open"
cStorageOpen :: CString -> IO (Ptr ())
foreign import ccall safe "chusql_storage_close"
cStorageClose :: Ptr () -> IO ()
foreign import ccall safe "chusql_storage_request"
cStorageRequest :: Ptr () -> Ptr CChar -> CSize -> Ptr (Ptr CChar) -> Ptr CSize -> IO CInt
foreign import ccall unsafe "chusql_storage_free"
cStorageFree :: Ptr CChar -> CSize -> IO ()
foreign import ccall unsafe "chusql_storage_last_error"
cStorageLastError :: Ptr CSize -> IO (Ptr CChar)
-- | 库版本(Rust 侧的 CARGO_PKG_VERSION)
storageVersion :: IO String
storageVersion = cStorageVersion >>= peekCString
-- | 打开存储实例,失败给错误文本
openStorage :: Maybe FilePath -> IO (Either String StorageHandle)
openStorage path = do
result <- try (withCStringOrNull path cStorageOpen) :: IO (Either SomeException (Ptr ()))
case result of
Left e -> pure (Left ("storage library call failed: " ++ show e))
Right ptr
| ptr == nullPtr -> Left <$> lastError
| otherwise -> pure (Right (StorageHandle ptr))
where
-- | 没有路径就传空指针,否则转成 C 字符串
withCStringOrNull Nothing f = f nullPtr
withCStringOrNull (Just s) f = withCString s f
-- | 关掉存储实例
closeStorage :: StorageHandle -> IO ()
closeStorage (StorageHandle handle) = cStorageClose handle
-- | 一条 JSON 请求换一条 JSON 响应
storageRequest :: StorageHandle -> BS.ByteString -> IO (Either String BS.ByteString)
storageRequest (StorageHandle handle) payload =
BS.useAsCStringLen payload $ \(inPtr, inLen) ->
alloca $ \outPtrRef ->
alloca $ \outLenRef -> do
code <- cStorageRequest handle (castPtr inPtr) (fromIntegral inLen) outPtrRef outLenRef
if code /= 0
then Left <$> lastError
else do
outPtr <- peek outPtrRef
outLen <- peek outLenRef
bytes <- BS.packCStringLen (castPtr outPtr, fromIntegral outLen)
cStorageFree outPtr outLen
pure (Right bytes)
-- | 当前线程最近一次失败的文本
lastError :: IO String
lastError = alloca $ \lenRef -> do
ptr <- cStorageLastError lenRef
if ptr == nullPtr
then pure "storage: unknown error"
else do
len <- peek lenRef
raw <- BS.packCStringLen (ptr, fromIntegral len)
pure (T.unpack (TE.decodeUtf8With lenientDecode raw))
@@ -0,0 +1,368 @@
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
module ChuSQL.Core.Engine.Storage.IPC (
IPCStorage (..),
Request (..),
Response (..),
Account (..),
SchemaColumn (..),
TableInfo (..),
StorageLink (..),
setStorageLink,
localStorageLink,
sendRequest,
sendRawRequest,
closeConnection,
setDatabaseName,
getDatabaseName,
doListTables,
doLookupByColumn,
doInsert,
doInsertMany,
doDeleteKeys,
doDescribeTable,
doDropTable,
doListCatalog,
) where
import ChuSQL.Core.Model
import ChuSQL.Core.Protocol (Account (..), Request (..), Response (..), SchemaColumn (..), TableInfo (..))
import ChuSQL.Core.Engine.Storage
import ChuSQL.Core.Engine.Storage.FFI (closeStorage, openStorage, storageRequest)
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, readMVar)
import Data.Aeson (eitherDecodeStrict, encode)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.Maybe (fromMaybe)
import System.IO.Unsafe (unsafePerformIO)
-- IPC 存储实现:把 MonadStorage 的操作变成发给存储库的 JSON 请求。
newtype IPCStorage a = IPCStorage
{ runIPCStorage :: IO a
}
instance Functor IPCStorage where
fmap f (IPCStorage m) = IPCStorage (fmap f m)
instance Applicative IPCStorage where
pure = IPCStorage . pure
IPCStorage mf <*> IPCStorage ma = IPCStorage (mf <*> ma)
instance Monad IPCStorage where
IPCStorage m >>= k = IPCStorage $ do
a <- m
runIPCStorage (k a)
-- | 一条到存储库的链路:管请求与关闭
data StorageLink = StorageLink
{ slRequest :: BS.ByteString -> IO (Either String BS.ByteString)
, slClose :: IO ()
}
{-# NOINLINE linkRef #-}
-- | 进程全局的存储链路
linkRef :: MVar (Maybe StorageLink)
linkRef = unsafePerformIO (newMVar Nothing)
-- | 装入一条链路;一个进程只装一次
setStorageLink :: StorageLink -> IO ()
setStorageLink link = modifyMVar_ linkRef (const (pure (Just link)))
-- | 打开本地的存储动态库,得到一条链路
localStorageLink :: Maybe FilePath -> IO (Either String StorageLink)
localStorageLink path = fmap (fmap toLink) (openStorage path)
where
-- | 把句柄包成一条链路
toLink handle =
StorageLink
{ slRequest = storageRequest handle
, slClose = closeStorage handle
}
-- | 关掉当前链路(进程退出前调用);没有链路就什么都不做
closeConnection :: IO ()
closeConnection = modifyMVar_ linkRef $ \current -> do
case current of
Nothing -> pure ()
Just link -> slClose link
pure Nothing
-- | 发一条请求并解码应答;链路没装就报错,不崩
sendRequest :: Request -> IO Response
sendRequest req = do
current <- readMVar linkRef
case current of
Nothing -> pure (RespError "storage not configured")
Just link -> do
raw <- slRequest link (BL.toStrict (encode req))
case raw of
Left e -> pure (RespError ("storage error: " ++ e))
Right bytes -> case eitherDecodeStrict (BS.dropWhileEnd (== 13) bytes) of
Left e -> pure (RespError ("decode error: " ++ e))
Right resp -> pure resp
-- | 原样转发一条载荷给存储库
sendRawRequest :: BS.ByteString -> IO (Either String BS.ByteString)
sendRawRequest payload = do
current <- readMVar linkRef
case current of
Nothing -> pure (Left "storage not configured")
Just link -> slRequest link payload
{-# NOINLINE databaseRef #-}
-- | 进程全局的当前库名
databaseRef :: MVar (Maybe String)
databaseRef = unsafePerformIO (newMVar Nothing)
-- | 设定当前库名(会话层决定,本层只记住并发给存储)
setDatabaseName :: Maybe String -> IO ()
setDatabaseName name = modifyMVar_ databaseRef (const (pure name))
-- | 当前库名
getDatabaseName :: IO (Maybe String)
getDatabaseName = readMVar databaseRef
-- | 给语句类请求盖上当前库名
stampDatabase :: Maybe String -> Request -> Request
stampDatabase Nothing req = req
stampDatabase (Just db) req
| globalRequest req = req
| otherwise = ReqInDatabase db req
-- | 是不是不该带库名的全局请求
globalRequest :: Request -> Bool
globalRequest ReqDatabase{} = True
globalRequest ReqAllCatalog = True
globalRequest ReqAccountsList = True
globalRequest ReqAccountCreate{} = True
globalRequest ReqAccountReset{} = True
globalRequest ReqAccountLogin{} = True
globalRequest ReqAccountDrop{} = True
globalRequest ReqPing = True
globalRequest _ = False
-- | 发请求并把响应翻成 Either
ask :: String -> Request -> (Response -> Maybe a) -> IO (Either String a)
ask what req recognize = do
current <- getDatabaseName
resp <- sendRequest (stampDatabase current 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 = ask "scan" (ReqScan t) $ \resp -> case resp of
RespRows rows -> Just rows
_ -> Nothing
-- | 发 Insert(索引由存储层维护)
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 = ask "replace_all" (ReqReplaceAll t rs) okOnly
-- | 发 ListTables
doListTables :: IO (Either String [String])
doListTables = ask "list_tables" ReqListTables $ \resp -> case resp of
RespTables ts -> Just ts
_ -> Nothing
-- | 一次拿全库 schema
doListCatalog :: IO (Either String [TableInfo])
doListCatalog = ask "list_catalog" ReqListCatalog $ \resp -> case resp of
RespCatalog xs -> Just xs
_ -> Nothing
-- | 按某一列的索引取一行(键可以是整数或字符串)
doLookupByColumn :: String -> String -> Value -> IO (Either String IndexResult)
doLookupByColumn t c k = ask "lookup_by_index" (ReqLookupByColumn t c k) $ \resp -> case resp of
RespRows rows -> Just (IndexRows rows)
RespNoIndex -> Just NoIndex
_ -> Nothing
-- | 范围扫描:没有可用索引就回 Nothing,让上层退回扫描
doScanRange ::
String ->
String ->
Maybe (Value, Bool) ->
Maybe (Value, Bool) ->
IO (Either String (Maybe [Row]))
doScanRange t c lo hi = ask "range_by_index" (ReqRangeByIndex t c lo hi) $ \resp -> case resp of
RespRows rows -> Just (Just rows)
RespNoIndex -> Just Nothing
_ -> Nothing
-- | 发 DescribeTable
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
-- | 删一列(列定义、这一列上的索引、每行里的那一格一起没)
doDropColumn :: String -> String -> IO (Either String ())
doDropColumn t c = ask "drop_column" (ReqDropColumn t c) okOnly
-- | 发 ReplaceSchema(ALTER 用)
doReplaceSchema :: String -> [SchemaColumn] -> [Row] -> IO (Either String ())
doReplaceSchema t cols rs = ask "replace_schema" (ReqReplaceSchema t cols rs) okOnly
-- | 发 CreateTable
doCreateTable :: String -> [SchemaColumn] -> IO (Either String ())
doCreateTable t cols = ask "create_table" (ReqCreateTable t cols) okOnly
-- | 发 DropTable
doDropTable :: String -> IO (Either String ())
doDropTable t = ask "drop_table" (ReqDropTable t) okOnly
-- | 线上 schema 转本地列
schemaToColumns :: [SchemaColumn] -> [(String, Column)]
schemaToColumns = map toColumn
-- | 线上 schema 转本地 column
toColumn :: SchemaColumn -> (String, Column)
toColumn sc =
( scName sc
, Column
{ columnType = fromMaybe CStr (parseColumnType (scType sc))
, columnNullable = scNullable sc
, columnDefault = scDefault sc
, columnAutoIncrement = scAutoIncrement sc
, columnUnique = scUnique sc
, columnPrimaryKey = scPrimaryKey sc
, columnCheck = scCheck sc
}
)
-- | 本地列转线上 schema
toWire :: (String, Column) -> SchemaColumn
toWire (n, c) =
SchemaColumn
{ scName = n
, scType = typeName (columnType c)
, scNullable = columnNullable c
, scDefault = columnDefault c
, scAutoIncrement = columnAutoIncrement c
, scPrimaryKey = columnPrimaryKey c
, scUnique = columnUnique c
, scCheck = columnCheck c
}
instance MonadStorage IPCStorage where
-- \| 建一个库
createDatabase name = IPCStorage (ask "create_database" (ReqDatabase "create_database" name) okOnly)
-- \| 删一个库
dropDatabase name = IPCStorage (ask "drop_database" (ReqDatabase "drop_database" name) okOnly)
-- \| 切当前库
useDatabase name = IPCStorage (ask "use_database" (ReqDatabase "use_database" name) okOnly)
-- \| 列出现有库
listDatabases = IPCStorage $ ask "list_databases" (ReqDatabase "list_databases" "") $ \resp -> case resp of
RespTables names -> Just names
_ -> Nothing
-- \| 发 Scan
scan t = IPCStorage (doScan t)
-- \| 只取指定列
scanColumns t cols = IPCStorage $ ask "scan" (ReqScanColumns t cols) $ \resp -> case resp of
RespRows rows -> Just [[(c, v) | c <- cols, Just v <- [lookup c row]] | row <- rows]
_ -> Nothing
-- \| 发 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)
-- \| 按某一列的索引取一行(没有索引就回 NoIndex)
lookupByColumn t c k = IPCStorage (doLookupByColumn t c k)
-- \| 范围扫描(没有索引就回 Nothing)
scanRange t c lo hi = IPCStorage (doScanRange t c lo hi)
-- \| 发 CreateTable
createTable name cols = IPCStorage (doCreateTable name (map toWire cols))
-- \| ALTER:整表换列定义与全部行
replaceSchema name cols rows = IPCStorage (doReplaceSchema name (map toWire cols) rows)
-- \| 发 DropTable
dropTable name = IPCStorage (doDropTable name)
-- \| 建索引
createIndex t c = IPCStorage (doCreateIndex t c)
-- \| 删索引
dropIndex t c = IPCStorage (doDropIndex t c)
-- \| 删一列
dropColumn t c = IPCStorage (doDropColumn t c)
-- \| 列表 + 逐表扫描拼库
snapshot = IPCStorage $ do
result <- doListCatalog
case result of
Left _ -> pure []
Right xs -> mapM loadEntry xs
where
loadEntry info = do
rows <- doScan (tiTable info)
pure
( tiTable info
, Table (tiTable info) (schemaToColumns (tiColumns info)) (either (const []) id rows)
)
-- \| 只问数据字典要结构,设了当前库只留它的表
schema = IPCStorage $ do
current <- getDatabaseName
result <- ask "all_catalogs" ReqAllCatalog $ \resp -> case resp of
RespCatalog infos -> Just infos
_ -> Nothing
pure $ case result of
Left _ -> []
Right xs -> concatMap (visibleTables current) xs
-- | 没设库名就照单全收;设了库名只收这个库的表,键名去掉库名前缀
visibleTables :: Maybe String -> TableInfo -> [(String, Table)]
visibleTables current info
| Just db <- current =
if unqualify (Just db) (tiTable info) /= tiTable info
then [bare named]
else []
| otherwise = [bare info]
where
named = info {tiTable = unqualify current (tiTable info)}
-- | 组装成不带行的表
bare i = (tiTable i, Table (tiTable i) (schemaToColumns (tiColumns i)) [])
@@ -0,0 +1,86 @@
module ChuSQL.Core.Engine.Storage.Memory (MemoryStorage (runMemoryStorage)) where
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Storage
-- 内存实现:Database 上的状态,错误通道是 Either String。
newtype MemoryStorage a = MemoryStorage {runMemoryStorage :: Database -> Either String (a, Database)}
instance Functor MemoryStorage where
fmap f (MemoryStorage m) = MemoryStorage $ \db -> do
(a, db') <- m db
Right (f a, db')
instance Applicative MemoryStorage where
pure x = MemoryStorage $ \db -> Right (x, db)
MemoryStorage mf <*> MemoryStorage ma = MemoryStorage $ \db -> do
(f, db1) <- mf db
(a, db2) <- ma db1
Right (f a, db2)
instance Monad MemoryStorage where
MemoryStorage m >>= k = MemoryStorage $ \db -> do
(a, db') <- m db
runMemoryStorage (k a) db'
instance MonadStorage MemoryStorage where
-- \| 全表扫描
scan t = MemoryStorage $ \db -> do
tbl <- lookupTable db t
Right (Right (tableRows tbl), db)
-- \| 追加一行
insert t r = MemoryStorage $ \db -> do
tbl <- lookupTable db t
Right (Right (), replaceTable t tbl{tableRows = tableRows tbl ++ [r]} db)
-- \| 整表替换
replaceAll t rows = MemoryStorage $ \db -> do
tbl <- lookupTable db t
Right (Right (), replaceTable t tbl{tableRows = rows} db)
-- \| 建空表;已存在报错
createTable name cols = MemoryStorage $ \db ->
if any ((== name) . fst) db
then Right (Left ("table already exists: " ++ name), db)
else Right (Right (), db ++ [(name, Table name cols [])])
-- \| 删表;不存在报错
dropTable name = MemoryStorage $ \db ->
if any ((== name) . fst) db
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)
-- \| 改表结构:列定义与行一起换
replaceSchema name cols rows = MemoryStorage $ \db -> case lookup name db of
Nothing -> Right (Left ("unknown table: " ++ name), db)
Just tbl -> Right (Right (), replaceTable name tbl{tableCols = cols, tableRows = rows} db)
-- | 用给定表替换同名表
replaceTable :: String -> Table -> Database -> Database
replaceTable name table =
map (\(n, t) -> if n == name then (n, table) else (n, t))
@@ -0,0 +1,129 @@
module ChuSQL.Core.Engine.Syntax.AST (Statement (..), FromClause (..), JoinKind (..), Expr (..), Subquery (..), SortDir (..), makeSelect) where
import ChuSQL.Core.Model (Column (..), ColumnType, Value (..))
-- 语法树:语句、数据来源、条件表达式、排序方向。
data Statement
= Select
{ selectCols :: [String]
, selectFrom :: FromClause
, selectWhere :: Maybe Expr
, selectGroupBy :: [String]
, selectOrderBy :: [(String, SortDir)]
, selectLimit :: Maybe Int
}
| SelectExpr
{ selectItems :: [(String, Expr)]
, selectFrom :: FromClause
, selectWhere :: Maybe Expr
, selectGroupBy :: [String]
, selectOrderBy :: [(String, SortDir)]
, selectLimit :: Maybe Int
}
| 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
| DropColumn String String
| AddColumn String (String, Column)
| RenameColumn String String String
| AlterColumnType String String ColumnType
| AlterColumnDefault String String (Maybe Value)
| AlterColumnNull String String Bool
| CreateUser String String
| AlterUser String String
| DropUser String
-- | 新角色:权限的载体,用户靠成员资格继承它的权限
| CreateRole String
-- | 删角色:连同它的授权与成员一起删
| DropRole String
-- | GRANT 权限 ON 表 TO 角色
| GrantPrivileges [String] String String
-- | REVOKE 权限 ON 表 FROM 角色
| RevokePrivileges [String] String String
-- | GRANT 角色 TO 用户:给用户加角色
| GrantRole String [String]
-- | REVOKE <角色> FROM <用户>[, ...]
| RevokeRole String [String]
| CreateDatabase String
| DropDatabase String
| UseDatabase String
| ShowDatabases
deriving (Show, Eq)
-- | 连接类型:内连接与左外连接
data JoinKind
= InnerJoin
| LeftJoin
deriving (Show, Eq)
-- | 数据来源:单表、带连接的来源,或空
data FromClause
= FromTable (Maybe String) String
| FromJoin JoinKind FromClause (Maybe String) String Expr
| FromUnit
deriving (Show, Eq)
-- | 子查询:语句,加上它对外层列的引用(自由变量,语义检查时填)
data Subquery = Subquery
{ subqueryStatement :: Statement
, subqueryRefs :: [String]
}
deriving (Show, Eq)
-- | 条件表达式:列、字面量、算术、比较、逻辑、聚合与子查询
data Expr
= Col String
| LitNull
| LitInt Int
| LitFloat Double
| LitStr String
| LitBool Bool
| LitDate String
| LitTimestamp String
| LitBlob String
| Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
| Div Expr Expr
| Neg Expr
| Gt Expr Expr
| Lt Expr Expr
| Eq Expr Expr
| And Expr Expr
| Or Expr Expr
| IsNull Expr
| IsNotNull Expr
| CountAll
| CountOf Expr
| SumOf Expr
| AvgOf Expr
| MinOf Expr
| MaxOf Expr
| ScalarSub Subquery
| InSub Expr Subquery Bool
| InList Expr [Expr] Bool
| ExistsSub Subquery Bool
deriving (Show, Eq)
-- | 排序方向:升序或降序
data SortDir
= Asc
| Desc
deriving (Show, Eq)
-- | 造一个最简单的 SELECT
makeSelect :: [String] -> String -> Maybe Expr -> Statement
makeSelect cols tbl w =
Select
{ selectCols = cols
, selectFrom = FromTable Nothing tbl
, selectWhere = w
, selectGroupBy = []
, selectOrderBy = []
, selectLimit = Nothing
}
@@ -0,0 +1,661 @@
{-# LANGUAGE ImportQualifiedPost #-}
module ChuSQL.Core.Engine.Syntax.Parser (parseStatement, parseExpression) where
import ChuSQL.Core.Model (Column (..), ColumnType (..), Value (..), plainColumn)
import ChuSQL.Core.Engine.Syntax.AST
import Control.Monad (void)
import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
import Data.Char (isAlpha, isAlphaNum, isHexDigit, isSpace, toLower, toUpper)
import Data.List (intercalate)
import Data.Maybe (fromMaybe)
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer qualified as Lxr
-- SQL 解析:词法 + 语法,把文本变成语句树。
type Parser = Parsec Void String
-- | 跳过空白
sc :: Parser ()
sc = Lxr.space space1 (Lxr.skipLineComment "--") (Lxr.skipBlockComment "/*" "*/")
-- | 词后面吃掉空白
lexeme :: Parser a -> Parser a
lexeme = Lxr.lexeme sc
-- | 读一个符号
symbol :: String -> Parser String
symbol = Lxr.symbol sc
-- | 读关键字(不分大小写)
keyword :: String -> Parser ()
keyword k = lexeme $ try $ do
_ <- string' k
notFollowedBy (satisfy isIdentChar)
-- | 读一个名字
identifier :: Parser String
identifier = lexeme $ do
first <- satisfy (\ch -> isAlpha ch || ch == '_')
rest <- many (satisfy isIdentChar)
return (first : rest)
-- | 账号名:裸名字或单引号字符串
userName :: Parser String
userName = identifier <|> stringLit
-- | 名字里允许的字符
isIdentChar :: Char -> Bool
isIdentChar ch = isAlphaNum ch || ch == '_'
-- | 读一个整数
integer :: Parser Int
integer = lexeme Lxr.decimal
-- | 读一个浮点数(必须有小数点或指数)
floatLit :: Parser Double
floatLit = lexeme Lxr.float
-- | 读一个字符串字面量
stringLit :: Parser String
stringLit = lexeme $ do
_ <- char '\''
s <- many stringChar
_ <- char '\''
return s
-- | 字符串里的一个字符
stringChar :: Parser Char
stringChar =
choice
[ '\'' <$ try (string "''")
, satisfy (/= '\'')
]
-- | X'ABCD' 形式的二进制字面量,统一存成大写十六进制文本
blobLit :: Parser String
blobLit = lexeme $ do
_ <- char 'x' <|> char 'X'
_ <- char '\''
s <- many (satisfy isHexDigit)
_ <- char '\''
return (map toUpper s)
-- | 升序或降序
sortDir :: Parser SortDir
sortDir = (Desc <$ keyword "desc") <|> (Asc <$ keyword "asc") <|> pure Asc
-- | LIMIT 后面的整数
limitClause :: Parser Int
limitClause = do
keyword "limit"
integer
-- | 能带表名的列名
qualifiedName :: Parser String
qualifiedName = do
first <- identifier
rest <- many (symbol "." *> identifier)
return (intercalate "." (first : rest))
-- | 读一个表达式
expr :: Parser Expr
expr = makeExprParser atom operatorTable
-- | 运算符优先级(从紧到松)
operatorTable :: [[Operator Parser Expr]]
operatorTable =
[ [Prefix (Neg <$ symbol "-")]
, [InfixL (Mul <$ symbol "*"), InfixL (Div <$ symbol "/")]
, [InfixL (Add <$ symbol "+"), InfixL (Sub <$ symbol "-")]
, [ InfixN (Gt <$ symbol ">")
, InfixN (Lt <$ symbol "<")
, InfixN (Eq <$ symbol "=")
]
, [ Postfix inPredicate
, Postfix (IsNotNull <$ try (keyword "is" *> keyword "not" *> keyword "null"))
, Postfix (IsNull <$ (keyword "is" *> keyword "null"))
]
, [InfixL (And <$ keyword "AND")]
, [InfixL (Or <$ keyword "OR")]
]
-- | 最小的表达式单位
atom :: Parser Expr
atom =
choice
[ LitNull <$ keyword "null"
, LitBool True <$ keyword "TRUE"
, LitBool False <$ keyword "FALSE"
, try (LitDate <$> (keyword "date" *> stringLit))
, try (LitTimestamp <$> (keyword "timestamp" *> stringLit))
, try (LitBlob <$> blobLit)
, LitFloat <$> try floatLit
, LitInt <$> integer
, LitStr <$> stringLit
, try aggregateCall
, try existsPredicate
, Col <$> qualifiedName
, try scalarSubquery
, between (symbol "(") (symbol ")") expr
]
-- | [NOT] EXISTS (SELECT ...)
existsPredicate :: Parser Expr
existsPredicate = do
negated <- (True <$ try (keyword "not" *> keyword "exists")) <|> (False <$ keyword "exists")
sq <- between (symbol "(") (symbol ")") subquerySelect
return (ExistsSub sq negated)
-- | 标量子查询:(SELECT ...)
scalarSubquery :: Parser Expr
scalarSubquery = ScalarSub <$> between (symbol "(") (symbol ")") subquerySelect
-- | 读 [NOT] IN:子查询或值清单
inPredicate :: Parser (Expr -> Expr)
inPredicate = do
negated <- (True <$ try (keyword "not" *> keyword "in")) <|> (False <$ keyword "in")
body <-
between (symbol "(") (symbol ")") $
choice
[ Left <$> try subquerySelect
, Right <$> sepBy1 expr (symbol ",")
]
return $ \e -> case body of
Left sq -> InSub e sq negated
Right es -> InList e es negated
-- | 括号里的子查询;对外层列的引用留给语义检查填
subquerySelect :: Parser Subquery
subquerySelect = Subquery <$> selectStatement <*> pure []
-- | 聚合调用:COUNT/SUM/AVG/MIN/MAX
aggregateCall :: Parser Expr
aggregateCall =
choice
[ CountAll <$ try (keyword "count" *> between (symbol "(") (symbol ")") (symbol "*"))
, CountOf <$> aggArg "count"
, SumOf <$> aggArg "sum"
, AvgOf <$> aggArg "avg"
, MinOf <$> aggArg "min"
, MaxOf <$> aggArg "max"
]
where
-- | 读聚合函数的参数
aggArg name = try (keyword name *> between (symbol "(") (symbol ")") expr)
-- | 只认字面量,给 DEFAULT 用
literalValue :: Parser Value
literalValue =
choice
[ VNull <$ keyword "null"
, VBool True <$ keyword "TRUE"
, VBool False <$ keyword "FALSE"
, try (negateLit <$> floatLit)
, VInt <$> integer
, VFloat <$> try floatLit
, VStr <$> stringLit
]
where
-- | 负浮点字面量
negateLit d = VFloat (negate d)
-- | 一个排序列
orderItem :: Parser (String, SortDir)
orderItem = do
col <- qualifiedName
dir <- sortDir
return (col, dir)
-- | ORDER BY 的列表
orderByClause :: Parser [(String, SortDir)]
orderByClause = do
keyword "order"
keyword "by"
sepBy1 orderItem (symbol ",")
-- | GROUP BY 的列表
groupByClause :: Parser [String]
groupByClause = do
keyword "group"
keyword "by"
sepBy1 qualifiedName (symbol ",")
-- | SET 里的一条赋值
assignment :: Parser (String, Expr)
assignment = do
col <- identifier
_ <- symbol "="
e <- expr
return (col, e)
-- | 保留字清单,别名不许用
reservedWords :: [String]
reservedWords =
[ "select"
, "as"
, "from"
, "where"
, "order"
, "by"
, "limit"
, "join"
, "left"
, "outer"
, "inner"
, "exists"
, "in"
, "on"
, "and"
, "or"
, "asc"
, "desc"
, "insert"
, "into"
, "values"
, "delete"
, "update"
, "set"
, "create"
, "table"
, "index"
, "drop"
, "alter"
, "add"
, "rename"
, "to"
, "column"
, "user"
, "identified"
, "null"
, "is"
, "not"
, "default"
, "check"
, "unique"
, "primary"
, "key"
, "auto_increment"
, "type"
, "group"
]
-- | 别名(不许用保留字)
aliasName :: Parser String
aliasName = try $ do
name <- identifier
if map toLower name `elem` reservedWords then empty else return name
-- | 表名 + 可选别名
tableRef :: Parser (Maybe String, String)
tableRef = do
tbl <- tableName
mAlias <- optional ((keyword "as" *> aliasName) <|> aliasName)
return (mAlias, tbl)
-- | 连接类型:内连接 / 左外连接
joinKind :: Parser JoinKind
joinKind =
try (keyword "left" *> optional (keyword "outer") *> keyword "join" *> return LeftJoin)
<|> try (keyword "inner" *> keyword "join" *> return InnerJoin)
<|> (keyword "join" *> return InnerJoin)
-- | 一个 JOIN ... ON
joinClause :: Parser (JoinKind, Maybe String, String, Expr)
joinClause = do
kind <- joinKind
(mAlias, tbl) <- tableRef
keyword "on"
cond <- expr
return (kind, mAlias, tbl, cond)
-- | FROM 子句(可含多个 JOIN)
fromClause :: Parser FromClause
fromClause = do
(mAlias, tbl) <- tableRef
joins <- many joinClause
return (foldl (\acc (k, a, t, c) -> FromJoin k acc a t c) (FromTable mAlias tbl) joins)
-- | SELECT 的列清单
selectList :: Parser [(String, Expr)]
selectList =
choice
[ [("*", Col "*")] <$ symbol "*"
, sepBy1 selectItem (symbol ",")
]
-- | 投影标签保留表达式原文
selectItem :: Parser (String, Expr)
selectItem = do
(source, e) <- match expr
pure (case e of Col c -> (c, e); _ -> (reverse (dropWhile isSpace (reverse source)), e))
-- | 读 CREATE TABLE
createTableStatement :: Parser Statement
createTableStatement = do
keyword "create"
keyword "table"
name <- tableName
cols <- between (symbol "(") (symbol ")") (sepBy1 columnDef (symbol ","))
return (CreateTable name cols)
-- | 读 DROP TABLE
dropTableStatement :: Parser Statement
dropTableStatement = do
keyword "drop"
keyword "table"
name <- tableName
return (DropTable name)
-- | 读 CREATE INDEX(列名即索引名)
createIndexStatement :: Parser Statement
createIndexStatement = do
keyword "create"
keyword "index"
keyword "on"
tbl <- tableName
col <- between (symbol "(") (symbol ")") identifier
return (CreateIndex tbl col)
-- | 读 DROP INDEX
dropIndexStatement :: Parser Statement
dropIndexStatement = do
keyword "drop"
keyword "index"
keyword "on"
tbl <- tableName
col <- between (symbol "(") (symbol ")") identifier
return (DropIndex tbl col)
-- | 读 ALTER TABLE 的加列、删列、改名与改属性
alterTableStatement :: Parser Statement
alterTableStatement = do
keyword "alter"
keyword "table"
tbl <- tableName
choice
[ do
keyword "add"
void (optional (keyword "column"))
(name, col) <- columnDef
pure (AddColumn tbl (name, col))
, do
keyword "drop"
void (optional (keyword "column"))
DropColumn tbl <$> identifier
, do
keyword "rename"
void (optional (keyword "column"))
old <- identifier
keyword "to"
RenameColumn tbl old <$> identifier
, do
keyword "alter"
void (optional (keyword "column"))
col <- identifier
choice
[ AlterColumnType tbl col <$> (keyword "type" *> columnTypeP)
, try (AlterColumnNull tbl col False <$ (keyword "set" *> keyword "not" *> keyword "null"))
, try (AlterColumnNull tbl col True <$ (keyword "drop" *> keyword "not" *> keyword "null"))
, try (AlterColumnDefault tbl col . Just <$> (keyword "set" *> keyword "default" *> literalValue))
, AlterColumnDefault tbl col Nothing <$ (keyword "drop" *> keyword "default")
]
]
-- | 读 CREATE USER,口令是明文
createUserStatement :: Parser Statement
createUserStatement = do
keyword "create"
keyword "user"
name <- userName
pw <- identifiedBy
return (CreateUser name pw)
-- | 读 IDENTIFIED BY '口令'
identifiedBy :: Parser String
identifiedBy = do
keyword "identified"
keyword "by"
stringLit
-- | 读 ALTER USER 改口令
alterUserStatement :: Parser Statement
alterUserStatement = do
keyword "alter"
keyword "user"
name <- userName
pw <- identifiedBy
return (AlterUser name pw)
-- | 读 DROP USER
dropUserStatement :: Parser Statement
dropUserStatement = do
keyword "drop"
keyword "user"
name <- userName
return (DropUser name)
-- | 读 CREATE ROLE
createRoleStatement :: Parser Statement
createRoleStatement = do
keyword "create"
keyword "role"
CreateRole <$> userName
-- | 读 DROP ROLE
dropRoleStatement :: Parser Statement
dropRoleStatement = do
keyword "drop"
keyword "role"
DropRole <$> userName
-- | 权限名,统一收成大写
privilegeName :: Parser String
privilegeName =
choice
[ "SELECT" <$ keyword "select"
, "INSERT" <$ keyword "insert"
, "UPDATE" <$ keyword "update"
, "DELETE" <$ keyword "delete"
, "ALL" <$ keyword "all"
]
-- | ON 后面的授权对象:* 或一张表
grantObject :: Parser String
grantObject = ("*" <$ symbol "*") <|> tableName
-- | 读 GRANT:带 ON 的授权限,不带的加角色
grantStatement :: Parser Statement
grantStatement = do
keyword "grant"
choice
[ try $ do
privs <- sepBy1 privilegeName (symbol ",")
keyword "on"
obj <- grantObject
keyword "to"
GrantPrivileges privs obj <$> userName
, GrantRole <$> userName <*> (keyword "to" *> sepBy1 userName (symbol ","))
]
-- | 读 REVOKE:与 GRANT 对称
revokeStatement :: Parser Statement
revokeStatement = do
keyword "revoke"
choice
[ try $ do
privs <- sepBy1 privilegeName (symbol ",")
keyword "on"
obj <- grantObject
keyword "from"
RevokePrivileges privs obj <$> userName
, RevokeRole <$> userName <*> (keyword "from" *> sepBy1 userName (symbol ","))
]
-- | 建表时的一列:类型后面跟若干约束
columnDef :: Parser (String, Column)
columnDef = do
name <- identifier
ty <- columnTypeP
mods <- many columnConstraint
return (name, foldl (\c f -> f c) (plainColumn ty) mods)
-- | 一条列约束
columnConstraint :: Parser (Column -> Column)
columnConstraint =
choice
[ try (keyword "not" *> keyword "null" *> pure (\c -> c{columnNullable = False}))
, keyword "null" *> pure (\c -> c{columnNullable = True})
, keyword "auto_increment" *> pure (\c -> c{columnAutoIncrement = True, columnNullable = False})
, try (keyword "primary" *> keyword "key" *> pure (\c -> c{columnPrimaryKey = True, columnUnique = True, columnNullable = False}))
, keyword "unique" *> pure (\c -> c{columnUnique = True})
, keyword "default" *> ((\v c -> c{columnDefault = Just v}) <$> literalValue)
, keyword "check" *> ((\s c -> c{columnCheck = Just s}) <$> checkText)
]
-- | CHECK 后面括号里的表达式原文,交给执行期再解析
checkText :: Parser String
checkText = between (symbol "(") (symbol ")") (match expr) >>= \(source, _) -> pure source
-- | 括号里的长度参数
sizeParen :: Parser Int
sizeParen = try (between (symbol "(") (symbol ")") integer)
-- | 列类型关键字
columnTypeP :: Parser ColumnType
columnTypeP =
choice
[ keyword "integer" >> pure CInt
, keyword "int" >> pure CInt
, keyword "bigint" >> pure CBigInt
, keyword "smallint" >> pure CSmallInt
, (keyword "varchar" >>) (maybe CStr CVarchar <$> optional sizeParen)
, (keyword "char" >>) (maybe (CChar 1) CChar <$> optional sizeParen)
, keyword "text" >> pure CStr
, keyword "str" >> pure CStr
, keyword "boolean" >> pure CBool
, keyword "bool" >> pure CBool
, keyword "float" >> pure CFloat
, keyword "real" >> pure CFloat
, try (keyword "double" *> keyword "precision" *> pure CDouble)
, keyword "double" >> pure CDouble
, (keyword "decimal" >>) decimalType
, (keyword "numeric" >>) decimalType
, keyword "date" >> pure CDate
, keyword "timestamp" >> pure CTimestamp
, keyword "blob" >> pure CBlob
]
-- | DECIMAL 的精度与小数位
decimalType :: Parser ColumnType
decimalType = do
args <- optional (try (between (symbol "(") (symbol ")") decimalArgs))
pure $ case args of
Nothing -> CDecimal 10 0
Just (p, s) -> CDecimal p s
-- | DECIMAL 的精度与小数位参数
decimalArgs :: Parser (Int, Int)
decimalArgs = do
p <- integer
s <- fromMaybe 0 <$> optional (symbol "," *> integer)
pure (p, s)
-- | 读 SELECT
selectStatement :: Parser Statement
selectStatement = do
keyword "select"
cols <- selectList
fromC <- fromMaybe FromUnit <$> optional (keyword "from" *> fromClause)
mWhere <- optional (keyword "where" *> expr)
groupBy <- fromMaybe [] <$> optional groupByClause
orderBy <- fromMaybe [] <$> optional orderByClause
mLimit <- optional limitClause
pure $ if all (\(name, e) -> e == Col name) cols
then Select (map fst cols) fromC mWhere groupBy orderBy mLimit
else SelectExpr cols fromC mWhere groupBy orderBy mLimit
-- | 读 INSERT,可一次插多行
insertStatement :: Parser Statement
insertStatement = do
keyword "INSERT"
keyword "INTO"
tbl <- tableName
cols <-
between
(symbol "(")
(symbol ")")
(sepBy1 identifier (symbol ","))
keyword "VALUES"
rows <- sepBy1 valueRow (symbol ",")
return (Insert tbl cols rows)
-- | 一行值:一对括号里的若干字面量
valueRow :: Parser [Expr]
valueRow = between (symbol "(") (symbol ")") (sepBy1 expr (symbol ","))
-- | 读 DELETE
deleteStatement :: Parser Statement
deleteStatement = do
keyword "DELETE"
keyword "FROM"
tbl <- tableName
mWhere <- optional (keyword "WHERE" *> expr)
return (Delete tbl mWhere)
-- | 读 UPDATE
updateStatement :: Parser Statement
updateStatement = do
keyword "update"
tbl <- tableName
keyword "set"
assigns <- sepBy1 assignment (symbol ",")
mWhere <- optional (keyword "where" *> expr)
return (Update tbl assigns mWhere)
-- | 解析总入口
parseStatement :: String -> Either String Statement
parseStatement input =
case runParser (sc *> statementP <* sc <* eof) "<query>" input of
Left err -> Left (errorBundlePretty err)
Right q -> Right q
where
-- | 按关键字分派到各语句解析器
statementP =
selectStatement
<|> try (CreateDatabase <$> (keyword "create" *> keyword "database" *> identifier))
<|> try (DropDatabase <$> (keyword "drop" *> keyword "database" *> identifier))
<|> (UseDatabase <$> (keyword "use" *> identifier))
<|> (ShowDatabases <$ (keyword "show" *> keyword "databases"))
<|> insertStatement
<|> deleteStatement
<|> updateStatement
<|> try createUserStatement
<|> try alterUserStatement
<|> try dropUserStatement
<|> try createRoleStatement
<|> try dropRoleStatement
<|> try grantStatement
<|> try revokeStatement
<|> try createIndexStatement
<|> try createTableStatement
<|> try dropIndexStatement
<|> try alterTableStatement
<|> dropTableStatement
-- | 解析一个裸表达式(CHECK 约束原文用)
parseExpression :: String -> Either String Expr
parseExpression input =
case runParser (sc *> expr <* sc <* eof) "<expr>" input of
Left err -> Left (errorBundlePretty err)
Right e -> Right e
-- | 表名至多有一个库前缀。
tableName :: Parser String
tableName = do
first <- identifier
databaseTable <- optional (symbol "." *> identifier)
pure (maybe first (\t -> map toLower first ++ "." ++ t) databaseTable)
+8
View File
@@ -0,0 +1,8 @@
resolver: lts-24.59
extra-lib-dirs:
- ../storage/target/release
packages:
- .
- ../model
+12
View File
@@ -0,0 +1,12 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/topics/lock_files
packages: []
snapshots:
- completed:
sha256: 0f728e30e843d7f00460ad56858b995a3b227fca683659c7df9f2fc3988a3ce1
size: 732465
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/59.yaml
original: lts-24.59
File diff suppressed because it is too large. Load diff
+21
View File
@@ -0,0 +1,21 @@
cabal-version: 2.4
name: chusql-core-model
version: 1.0.0
synopsis: ChuSQL 共用数据模型与线上协议类型
description: ChuSQL 各进程共用的纯数据类型:列与值的模型、存储层线上协议
(请求/响应/表结构与账号)以及查询结果集。这里不放任何执行
逻辑,前端只依赖这个包就不会把引擎链进自己的二进制。
build-type: Simple
library
hs-source-dirs: src
exposed-modules: ChuSQL.Core.Model
ChuSQL.Core.Protocol
build-depends: base >= 4.14 && < 5
, aeson
, hashable
, scientific
, text
default-language: Haskell2010
ghc-options: -Wall
+422
View File
@@ -0,0 +1,422 @@
{-# LANGUAGE PatternSynonyms #-}
module ChuSQL.Core.Model
( ColumnType (..)
, Column (..)
, pattern TInt
, pattern TStr
, pattern TBool
, TypeClass (..)
, Value (..)
, Row
, Table (..)
, Database
, colNames
, colType
, lookupTable
, allColumns
, qualify
, unqualify
, resolveColumn
, plainColumn
, typeLabel
, typeName
, parseColumnType
, typeClassOf
, assignable
, integerType
, numericType
, valueFits
, coerceValue
) where
import Data.Char (isDigit, isSpace, toLower)
import Data.Hashable (Hashable (..))
import Data.List (isSuffixOf, stripPrefix)
import Data.Maybe (fromMaybe)
-- 数据模型:列类型、列约束、值、行、表、数据库,外加查表小工具。
-- | 列的基础类型
data ColumnType
= CInt
| CBigInt
| CSmallInt
| CStr
| CVarchar Int
| CChar Int
| CBool
| CFloat
| CDouble
| CDecimal Int Int
| CDate
| CTimestamp
| CBlob
deriving (Show, Eq)
-- | 列定义:类型 + 约束;约束全部取默认值就是普通可空列。
data Column = Column
{ columnType :: ColumnType
, columnNullable :: Bool
, columnDefault :: Maybe Value
, columnAutoIncrement :: Bool
, columnUnique :: Bool
, columnPrimaryKey :: Bool
, columnCheck :: Maybe String
}
deriving (Eq)
-- | 普通可空列
plainColumn :: ColumnType -> Column
plainColumn t = Column t True Nothing False False False Nothing
-- | 普通整型列的匹配模式
pattern TInt :: Column
pattern TInt <- Column CInt _ _ _ _ _ _
where
TInt = plainColumn CInt
-- | 普通字符串列的匹配模式
pattern TStr :: Column
pattern TStr <- Column CStr _ _ _ _ _ _
where
TStr = plainColumn CStr
-- | 普通布尔列的匹配模式
pattern TBool :: Column
pattern TBool <- Column CBool _ _ _ _ _ _
where
TBool = plainColumn CBool
-- | 错误消息里用的类型名
typeLabel :: ColumnType -> String
typeLabel CInt = "TInt"
typeLabel CBigInt = "TBigInt"
typeLabel CSmallInt = "TSmallInt"
typeLabel CStr = "TStr"
typeLabel (CVarchar n) = "TVarchar " ++ show n
typeLabel (CChar n) = "TChar " ++ show n
typeLabel CBool = "TBool"
typeLabel CFloat = "TFloat"
typeLabel CDouble = "TDouble"
typeLabel (CDecimal p s) = "TDecimal " ++ show p ++ " " ++ show s
typeLabel CDate = "TDate"
typeLabel CTimestamp = "TTimestamp"
typeLabel CBlob = "TBlob"
-- | 线上/磁盘上的类型名
typeName :: ColumnType -> String
typeName CInt = "int"
typeName CBigInt = "bigint"
typeName CSmallInt = "smallint"
typeName CStr = "str"
typeName (CVarchar n) = "varchar(" ++ show n ++ ")"
typeName (CChar n) = "char(" ++ show n ++ ")"
typeName CBool = "bool"
typeName CFloat = "float"
typeName CDouble = "double"
typeName (CDecimal p s) = "decimal(" ++ show p ++ "," ++ show s ++ ")"
typeName CDate = "date"
typeName CTimestamp = "timestamp"
typeName CBlob = "blob"
-- | 解析类型名,可带长度/精度参数;不认识的给 Nothing
parseColumnType :: String -> Maybe ColumnType
parseColumnType raw = case (base, args) of
("int", []) -> Just CInt
("integer", []) -> Just CInt
("bigint", []) -> Just CBigInt
("smallint", []) -> Just CSmallInt
("str", []) -> Just CStr
("text", []) -> Just CStr
("varchar", []) -> Just CStr
("varchar", [n]) -> CVarchar <$> positive n
("char", []) -> Just (CChar 1)
("char", [n]) -> CChar <$> positive n
("bool", []) -> Just CBool
("boolean", []) -> Just CBool
("float", []) -> Just CFloat
("real", []) -> Just CFloat
("double", []) -> Just CDouble
("decimal", []) -> Just (CDecimal 10 0)
("numeric", []) -> Just (CDecimal 10 0)
("decimal", [p]) -> flip CDecimal 0 <$> positive p
("numeric", [p]) -> flip CDecimal 0 <$> positive p
("decimal", [p, s]) -> CDecimal <$> positive p <*> plain s
("numeric", [p, s]) -> CDecimal <$> positive p <*> plain s
("date", []) -> Just CDate
("timestamp", []) -> Just CTimestamp
("blob", []) -> Just CBlob
_ -> Nothing
where
-- | 去掉空白的类型名
cleaned = map toLower (filter (not . isSpace) raw)
-- | 基础名与参数段
(base, rest) = span (/= '(') cleaned
-- | 括号里的参数表
args = case stripSuffixMaybe ")" (drop 1 rest) of
Nothing -> []
Just inner -> map (filter (not . isSpace)) (splitOn ',' inner)
-- | 去掉结尾
stripSuffixMaybe :: String -> String -> Maybe String
stripSuffixMaybe suffix s
| suffix `isSuffixOf` s = Just (take (length s - length suffix) s)
| otherwise = Nothing
-- | 按逗号切开
splitOn :: Char -> String -> [String]
splitOn sep s = case break (== sep) s of
(part, []) -> [part]
(part, _ : rest) -> part : splitOn sep rest
-- | 正数参数
positive :: String -> Maybe Int
positive s
| not (null s) && all isDigit s = Just (read s)
| otherwise = Nothing
-- | 非负整数参数
plain :: String -> Maybe Int
plain = positive
-- | 写出普通列就只说类型名,带约束才补在后面
instance Show Column where
show c = typeLabel (columnType c) ++ notNull ++ def ++ auto ++ uni ++ key ++ check
where
notNull = if columnNullable c then "" else " NOT NULL"
def = maybe "" (\v -> " DEFAULT " ++ show v) (columnDefault c)
auto = if columnAutoIncrement c then " AUTO_INCREMENT" else ""
uni = if columnUnique c then " UNIQUE" else ""
key = if columnPrimaryKey c then " PRIMARY KEY" else ""
check = maybe "" (\s -> " CHECK (" ++ s ++ ")") (columnCheck c)
-- | 类型大类
data TypeClass
= NumericClass
| TextClass
| TemporalClass
| BooleanClass
deriving (Show, Eq)
-- | 整型大类判断
typeClassOf :: ColumnType -> TypeClass
typeClassOf CInt = NumericClass
typeClassOf CBigInt = NumericClass
typeClassOf CSmallInt = NumericClass
typeClassOf CFloat = NumericClass
typeClassOf CDouble = NumericClass
typeClassOf CDecimal{} = NumericClass
typeClassOf CStr = TextClass
typeClassOf CVarchar{} = TextClass
typeClassOf CChar{} = TextClass
typeClassOf CBlob = TextClass
typeClassOf CDate = TemporalClass
typeClassOf CTimestamp = TemporalClass
typeClassOf CBool = BooleanClass
-- | 同大类可赋值;日期/时间列接受字符串字面量
assignable :: ColumnType -> ColumnType -> Bool
assignable target source
| target == source = True
| typeClassOf target == typeClassOf source = True
| typeClassOf target == TemporalClass && typeClassOf source == TextClass = True
| otherwise = False
-- | 是不是整数类型
integerType :: ColumnType -> Bool
integerType CInt = True
integerType CBigInt = True
integerType CSmallInt = True
integerType _ = False
-- | 是不是数值类型
numericType :: ColumnType -> Bool
numericType t = typeClassOf t == NumericClass
-- | 值能不能放进这一列(先不看可空)
valueFits :: ColumnType -> Value -> Bool
valueFits _ VNull = True
valueFits CInt (VInt _) = True
valueFits CBigInt (VInt _) = True
valueFits CSmallInt (VInt _) = True
valueFits CFloat (VInt _) = True
valueFits CFloat (VFloat _) = True
valueFits CDouble (VInt _) = True
valueFits CDouble (VFloat _) = True
valueFits CDecimal{} (VInt _) = True
valueFits CDecimal{} (VFloat _) = True
valueFits CStr (VStr _) = True
valueFits CVarchar{} (VStr _) = True
valueFits CChar{} (VStr _) = True
valueFits CBool (VBool _) = True
valueFits CDate (VStr _) = True
valueFits CTimestamp (VStr _) = True
valueFits CBlob (VStr _) = True
valueFits _ _ = False
-- | 把值收进列类型:NULL 原样通过,类型不符报错
coerceValue :: ColumnType -> Value -> Either String Value
coerceValue _ VNull = Right VNull
coerceValue t v = case t of
CInt -> toInt t v
CBigInt -> toInt t v
CSmallInt -> toInt t v
CFloat -> toFloat t v
CDouble -> toFloat t v
CDecimal p s -> toDecimal p s v
CStr -> toText t v
CVarchar n -> toSized n v
CChar n -> toSized n v
CBool -> toBool t v
CDate -> toDate v
CTimestamp -> toTimestamp v
CBlob -> toText t v
-- | 整数收进整型列
toInt :: ColumnType -> Value -> Either String Value
toInt _ (VInt n) = Right (VInt n)
toInt t (VFloat d) = case properFraction d :: (Int, Double) of
(n, frac)
| frac == 0 -> Right (VInt n)
| otherwise -> Left ("cannot put a fractional number into " ++ typeLabel t)
toInt t _ = Left ("cannot put this value into " ++ typeLabel t)
-- | 数值收进浮点列
toFloat :: ColumnType -> Value -> Either String Value
toFloat _ (VInt n) = Right (VFloat (fromIntegral n))
toFloat _ (VFloat d) = Right (VFloat d)
toFloat t _ = Left ("cannot put this value into " ++ typeLabel t)
-- | DECIMAL 按小数位四舍五入
toDecimal :: Int -> Int -> Value -> Either String Value
toDecimal p s v = case v of
VInt n -> rounded (fromIntegral n)
VFloat d -> rounded d
_ -> Left ("cannot put this value into " ++ typeLabel (CDecimal p s))
where
-- | 缩放因子
scale = (10 :: Double) ^ max 0 s
-- | 按比例四舍五入
rounded d = Right (VFloat (fromIntegral (round (d * scale) :: Integer) / scale))
-- | 字符串收进文本列
toText :: ColumnType -> Value -> Either String Value
toText _ (VStr s) = Right (VStr s)
toText t _ = Left ("cannot put this value into " ++ typeLabel t)
-- | 按列长度限制收字符串
toSized :: Int -> Value -> Either String Value
toSized n (VStr s)
| length s <= n = Right (VStr s)
| otherwise = Left ("value is longer than " ++ show n ++ " characters")
toSized _ _ = Left "cannot put this value into a character column"
-- | 布尔收进布尔列
toBool :: ColumnType -> Value -> Either String Value
toBool _ (VBool b) = Right (VBool b)
toBool t _ = Left ("cannot put this value into " ++ typeLabel t)
-- | 字符串收进日期列
toDate :: Value -> Either String Value
toDate (VStr s)
| validDateText s = Right (VStr s)
| otherwise = Left "date must look like YYYY-MM-DD"
toDate _ = Left "date must be a string"
-- | 字符串收进时间戳列
toTimestamp :: Value -> Either String Value
toTimestamp (VStr s)
| validTimestampText s = Right (VStr s)
| otherwise = Left "timestamp must look like YYYY-MM-DD HH:MM:SS"
toTimestamp _ = Left "timestamp must be a string"
-- | YYYY-MM-DD
validDateText :: String -> Bool
validDateText s = case splitOn '-' s of
[y, m, d] -> allOf 4 y && allOf 2 m && allOf 2 d
_ -> False
-- | 日期时间格式;中间允许空格或 T
validTimestampText :: String -> Bool
validTimestampText s = case break (\c -> c == ' ' || c == 'T') s of
(d, _ : tm) -> validDateText d && validTimeText tm
_ -> False
-- | HH:MM:SS
validTimeText :: String -> Bool
validTimeText t = case splitOn ':' t of
[h, m, sec] -> allOf 2 h && allOf 2 m && allOf 2 sec
_ -> False
-- | 一段字符串是不是固定长度的数字
allOf :: Int -> String -> Bool
allOf n s = length s == n && all isDigit s
-- | 引擎里的一行值
data Value
= VNull
| VInt Int
| VFloat Double
| VStr String
| VBool Bool
deriving (Show, Eq)
-- | 值按类型打散搅拌
instance Hashable Value where
hashWithSalt s VNull = hashWithSalt (hashWithSalt s (0 :: Int)) ()
hashWithSalt s (VInt n) = hashWithSalt (hashWithSalt s (1 :: Int)) n
hashWithSalt s (VFloat d) = hashWithSalt (hashWithSalt s (2 :: Int)) d
hashWithSalt s (VStr t) = hashWithSalt (hashWithSalt s (3 :: Int)) t
hashWithSalt s (VBool b) = hashWithSalt (hashWithSalt s (4 :: Int)) b
-- | 一行数据
type Row = [(String, Value)]
-- | 一张表:名字、列定义与全部行
data Table = Table
{ tableName :: String
, tableCols :: [(String, Column)]
, tableRows :: [Row]
}
deriving (Show)
-- | 库:库名到表的映射
type Database = [(String, Table)]
-- | 取表的列名
colNames :: Table -> [String]
colNames = map fst . tableCols
-- | 查某列的定义
colType :: Table -> String -> Maybe Column
colType t c = lookup c (tableCols t)
-- | 按名查表,没有就报错
lookupTable :: Database -> String -> Either String Table
lookupTable db t = maybe (Left ("unknown table: " ++ t)) Right (lookup t db)
-- | 通配列名
allColumns :: String
allColumns = "*"
-- | 给列名加别名前缀
qualify :: Maybe String -> String -> String
qualify Nothing c = c
qualify (Just a) c = a ++ "." ++ c
-- | 去掉别名前缀
unqualify :: Maybe String -> String -> String
unqualify Nothing c = c
unqualify (Just a) c = fromMaybe c (stripPrefix (a ++ ".") c)
-- | 裸列仅允许唯一匹配,限定名必须精确匹配
resolveColumn :: String -> [(String, a)] -> Either String (String, a)
resolveColumn name env = case [(k, v) | (k, v) <- env, matches k] of
[entry] -> Right entry
[] -> Left ("unknown column: " ++ name)
_ -> Left ("ambiguous column: " ++ name)
where
-- | 裸名或带后缀的名称
matches k = k == name || ('.' : name) `isSuffixOf` k
@@ -0,0 +1,357 @@
{-# LANGUAGE OverloadedStrings #-}
module ChuSQL.Core.Protocol (
SchemaColumn (..),
Request (..),
Response (..),
TableInfo (..),
Account (..),
QueryResult (..),
valueToJSON,
valueFromJSON,
rowToJSON,
rowFromJSON,
queryResultJson,
) where
import ChuSQL.Core.Model (Row, Value (..))
import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.!=), (.=))
import qualified Data.Aeson as A
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import Data.Aeson.Types (Parser)
import Data.Scientific (floatingOrInteger, fromFloatDigits)
import qualified Data.Text as T
import Data.Text (Text)
-- 线上协议类型:存储层请求/响应与查询结果集。
-- | 数据字典里一列的线上形状
data SchemaColumn = SchemaColumn
{ scName :: String
, scType :: String
, scNullable :: Bool
, scDefault :: Maybe Value
, scAutoIncrement :: Bool
, scPrimaryKey :: Bool
, scUnique :: Bool
, scCheck :: Maybe String
}
deriving (Show, Eq)
-- | 列编码成 JSON
instance ToJSON SchemaColumn where
toJSON sc =
object
[ "name" .= scName sc
, "ty" .= scType sc
, "nullable" .= scNullable sc
, "default" .= fmap valueToJSON (scDefault sc)
, "auto_increment" .= scAutoIncrement sc
, "primary_key" .= scPrimaryKey sc
, "unique" .= scUnique sc
, "check" .= scCheck sc
]
-- | JSON 解回列
instance FromJSON SchemaColumn where
parseJSON = withObject "SchemaColumn" $ \o -> do
n <- o .: "name"
t <- o .: "ty"
nullable <- o .:? "nullable" .!= True
defJson <- o .:? "default"
def <- mapM valueFromJSON defJson
auto <- o .:? "auto_increment" .!= False
pk <- o .:? "primary_key" .!= False
uniq <- o .:? "unique" .!= False
chk <- o .:? "check"
pure (SchemaColumn n t nullable def auto pk uniq chk)
-- | 存储层请求
data Request
= ReqPing
| ReqDatabase String String
| ReqAllCatalog
| ReqInDatabase String Request
| ReqAccountsList
| ReqAccountCreate T.Text T.Text
| ReqAccountReset T.Text T.Text
| ReqAccountLogin T.Text (Maybe T.Text)
| ReqAccountDrop T.Text
| ReqScan String
| ReqScanColumns String [String]
| ReqInsert String Row
| ReqInsertBatch String [Row]
| ReqDeleteKeys String [Int]
| ReqReplaceAll String [Row]
| ReqListTables
| ReqLookupByColumn String String Value
| ReqRangeByIndex String String (Maybe (Value, Bool)) (Maybe (Value, Bool))
| ReqDescribeTable String
| ReqCreateTable String [SchemaColumn]
| ReqDropTable String
| ReqCreateIndex String String
| ReqDropIndex String String
| ReqDropColumn String String
| ReqReplaceSchema String [SchemaColumn] [Row]
| ReqListCatalog
-- | 请求编码成 JSON
instance ToJSON Request where
toJSON (ReqDatabase method name) = object ["method" .= method, "database" .= name]
toJSON ReqAllCatalog = object ["method" .= ("all_catalogs" :: T.Text)]
toJSON (ReqInDatabase name req) = case toJSON req of
A.Object fields -> A.Object (KM.insert "database" (A.String (T.pack name)) fields)
other -> other
toJSON ReqAccountsList = object ["method" .= ("accounts_list" :: T.Text)]
toJSON (ReqAccountCreate u h) = object ["method" .= ("account_create" :: T.Text), "user" .= u, "password_hash" .= h]
toJSON (ReqAccountReset u h) = object ["method" .= ("account_reset" :: T.Text), "user" .= u, "password_hash" .= h]
toJSON (ReqAccountLogin u at) = object ["method" .= ("account_login" :: T.Text), "user" .= u, "at" .= at]
toJSON (ReqAccountDrop u) = object ["method" .= ("account_drop" :: T.Text), "user" .= u]
toJSON ReqPing = object ["method" .= ("ping" :: T.Text)]
toJSON (ReqScan t) = object ["method" .= ("scan" :: T.Text), "table" .= t]
toJSON (ReqScanColumns t cols) = object ["method" .= ("scan" :: T.Text), "table" .= t, "columns" .= cols]
toJSON (ReqInsert t r) =
object
[ "method" .= ("insert" :: T.Text)
, "table" .= t
, "row" .= rowToJSON r
]
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)
, "table" .= t
, "rows" .= map rowToJSON rs
]
toJSON ReqListTables = object ["method" .= ("list_tables" :: T.Text)]
toJSON (ReqLookupByColumn t c k) =
object
[ "method" .= ("lookup_by_index" :: T.Text)
, "table" .= t
, "column" .= c
, "key" .= valueToJSON k
]
toJSON (ReqRangeByIndex t c lo hi) =
object
[ "method" .= ("range_by_index" :: T.Text)
, "table" .= t
, "column" .= c
, "lo" .= fmap (valueToJSON . fst) lo
, "lo_inclusive" .= maybe True snd lo
, "hi" .= fmap (valueToJSON . fst) hi
, "hi_inclusive" .= maybe True snd hi
]
toJSON (ReqDescribeTable t) =
object
[ "method" .= ("describe_table" :: T.Text)
, "table" .= t
]
toJSON (ReqCreateTable t cols) =
object
[ "method" .= ("create_table" :: T.Text)
, "table" .= t
, "columns" .= cols
]
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 (ReqDropColumn t c) =
object
[ "method" .= ("drop_column" :: T.Text)
, "table" .= t
, "column" .= c
]
toJSON (ReqReplaceSchema t cols rs) =
object
[ "method" .= ("replace_schema" :: T.Text)
, "table" .= t
, "columns" .= cols
, "rows" .= map rowToJSON rs
]
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)
-- | 表信息编码成 JSON
instance ToJSON TableInfo where
toJSON info =
object
[ "table" .= tiTable info
, "columns" .= tiColumns info
, "row_count" .= tiRows info
, "indexes" .= map (\name -> object ["column" .= name]) (tiIndexes info)
, "stats" .= map statJson (tiStats info)
]
where
statJson (name, distinct, capped) =
object ["name" .= name, "distinct" .= distinct, "capped" .= capped]
-- | JSON 解回表信息
instance FromJSON TableInfo where
parseJSON = withObject "TableInfo" $ \o -> do
table <- o .: "table"
columns <- o .: "columns"
count <- o .: "row_count"
indexes <- o .:? "indexes" .!= []
stats <- o .:? "stats" .!= []
indexColumns <- mapM (\v -> withObject "IndexWire" (.: "column") v) (indexes :: [A.Value])
parsedStats <- mapM parseStat (stats :: [A.Value])
pure (TableInfo table columns count indexColumns parsedStats)
where
parseStat = withObject "ColumnStat" $ \o -> do
name <- o .: "name"
distinct <- o .: "distinct"
capped <- o .:? "capped" .!= False
pure (name, distinct, capped)
-- | 一条账号记录
data Account = Account
{ accountId :: Integer
, accountUser :: T.Text
, accountHash :: T.Text
, accountRevision :: Integer
, accountRegisteredAt :: T.Text
, accountLastLoginAt :: Maybe T.Text
} deriving (Eq)
-- | 账号的展示:只露账号名与版本号
instance Show Account where
show a = "Account " ++ show (accountUser a) ++ " revision=" ++ show (accountRevision a)
-- | JSON 解回账号
instance FromJSON Account where
parseJSON = withObject "Account" $ \o -> Account <$> o .: "id" <*> o .: "user"
<*> o .: "password_hash" <*> o .: "revision"
<*> o .:? "registered_at" .!= "" <*> o .:? "last_login_at"
-- | 账号编码成 JSON
instance ToJSON Account where
toJSON a = object ["id" .= accountId a, "user" .= accountUser a,
"password_hash" .= accountHash a, "revision" .= accountRevision a,
"registered_at" .= accountRegisteredAt a, "last_login_at" .= accountLastLoginAt a]
-- | 存储层响应
data Response
= RespPong
| RespAccounts [Account]
| RespRows [Row]
| RespTables [String]
| RespSchema TableInfo
| RespNoIndex
| RespOk
| RespError String
| RespCatalog [TableInfo]
-- | JSON 解回响应
instance FromJSON Response where
parseJSON = withObject "Response" $ \o -> do
status <- o .: "status"
case status :: T.Text of
"accounts" -> RespAccounts <$> o .: "accounts"
"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" -> RespSchema <$> parseJSON (A.Object o)
"catalog" -> do
xs <- o .: "schemas" :: Parser [A.Value]
RespCatalog <$> mapM parseJSON xs
other -> fail ("unknown status: " ++ T.unpack other)
-- | 值编码成 JSON
valueToJSON :: Value -> A.Value
valueToJSON VNull = A.Null
valueToJSON (VInt n) = A.Number (fromIntegral n)
valueToJSON (VFloat d) = A.Number (fromFloatDigits d)
valueToJSON (VStr s) = A.String (T.pack s)
valueToJSON (VBool b) = A.Bool b
-- | JSON 解回值
valueFromJSON :: A.Value -> Parser Value
valueFromJSON A.Null = pure VNull
valueFromJSON (A.Number n) =
case floatingOrInteger n :: Either Double Integer of
Right i -> pure (VInt (fromIntegral i))
Left d -> pure (VFloat d)
valueFromJSON (A.String s) = pure (VStr (T.unpack s))
valueFromJSON (A.Bool b) = pure (VBool b)
valueFromJSON _ = fail "unsupported value type"
-- | 一行编码成 JSON
rowToJSON :: Row -> A.Value
rowToJSON r =
A.Object (KM.fromList [(K.fromString k, valueToJSON v) | (k, v) <- r])
-- | JSON 解回一行
rowFromJSON :: A.Value -> Parser Row
rowFromJSON (A.Object o) = mapM toPair (KM.toList o)
where
-- | 键值对解成一个字段
toPair (k, v) = do
val <- valueFromJSON v
pure (K.toString k, val)
rowFromJSON _ = fail "row must be a JSON object"
-- | 一条语句的结果集:列名与排好的行
data QueryResult = QueryResult
{ qrColumns :: [Text]
, qrRows :: [[Value]]
, qrRowCount :: Int
, qrTruncated :: Bool
, qrDatabase :: Maybe Text
}
deriving (Eq, Show)
-- | JSON 解回结果集
instance FromJSON QueryResult where
parseJSON = withObject "QueryResult" $ \o -> do
cols <- o .: "columns"
rows <- o .: "rows"
QueryResult cols <$> mapM (mapM valueFromJSON) rows
<*> o .:? "rowCount" .!= 0
<*> o .:? "truncated" .!= False
<*> o .:? "database"
-- | 结果还原成 JSON(JSON 输出与 TCP 结果集共用)
queryResultJson :: QueryResult -> A.Value
queryResultJson result =
object
[ "columns" .= qrColumns result
, "rows" .= map (map valueToJSON) (qrRows result)
, "rowCount" .= qrRowCount result
, "truncated" .= qrTruncated result
, "database" .= qrDatabase result
]
+4
View File
@@ -0,0 +1,4 @@
resolver: lts-24.59
packages:
- .
+12
View File
@@ -0,0 +1,12 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/topics/lock_files
packages: []
snapshots:
- completed:
sha256: 0f728e30e843d7f00460ad56858b995a3b227fca683659c7df9f2fc3988a3ce1
size: 732465
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/59.yaml
original: lts-24.59
+294
View File
@@ -0,0 +1,294 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "chusql-core-storage"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"tempfile",
"toml",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "chusql-core-storage"
version = "0.1.0"
edition = "2024"
[lib]
name = "chusql_core_storage"
crate-type = ["cdylib", "rlib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
[dev-dependencies]
tempfile = "3"
@@ -0,0 +1,38 @@
# ChuSQL 存储服务配置示例
#
# 用法:安装时把本文件放到固定的用户配置目录(Windows 是 %APPDATA%\ChuSQL\chusql.toml,
# 其它平台是 ${XDG_CONFIG_HOME:-$HOME/.config}/ChuSQL/chusql.toml);
# 临时换一份就用命令行参数 --config <path>(没有环境变量通道了)。
#
# 每一项都可以删掉:没写的项回退到内置默认值(下面注释里的值就是默认值)。
# 优先级:本文件 > 内置默认值(没有配置键走环境变量了)。
# 启动时 server 会把"读了哪个文件、每个选项从哪来"打进日志([INFO] 开头那几行)。
[page]
# 页大小(字节):512 ~ 65536 之间的 2 的幂。
# 注意:改它会让已有数据文件(.db)读不出来(文件长度对不上页大小会直接报错)。
size = 4096
[btree]
# B+ 树一个节点最多几个孩子(>= 3,且必须能放进一页:12 + (order-1)*8 + order*8 <= size)。
# 注意:改它会让已有索引文件(.idx)对不上文件头里记的值,打开时报错。
order = 4
[buffer]
# 缓冲池里最多放几页(>= 1)。
pool_size = 1024
[storage]
# 数据目录:一张表一个 <表名>.db,一个索引一个 <表名>.idx。
# 不写就跟随平台惯例(Windows %LOCALAPPDATA%\ChuSQL\data,
# Unix ${XDG_DATA_HOME:-$HOME/.local/share}/chusql/data,跟安装脚本装的位置一致);
# 要固定就写绝对路径,例如:
# data_dir = "/srv/chusql/data"
[log]
# 日志等级:off / error / warn / info / debug。debug 会多打每个请求和连接。
level = "info"
# 颜色没有配置项也没有环境变量:要写的那条流是终端就着色,重定向到文件就不着色。
# 行格式:[等级 本地日期时间] [来源] 消息;来源 core=服务自身、request=由存储请求触发
# ERROR 加粗红、WARN 加粗橙、DEBUG 加粗不变色、INFO 不加样式
+507
View File
@@ -0,0 +1,507 @@
use std::io;
use std::path::Path;
use crate::page::{Page, PageFile, PageId};
// 磁盘 B+ 树:键是变长字节串,允许重复键,叶子链支持范围扫描。
const META_PAGE: PageId = 0;
const META_MAGIC: &[u8; 4] = b"CBK2";
const OFF_PAGE_SIZE: usize = 4;
const OFF_MAX_KEY: usize = 8;
const OFF_ROOT: usize = 10;
const OFF_ORDER: usize = 18;
const NODE_LEAF: u8 = 0;
const NODE_INTERNAL: u8 = 1;
const OFF_KIND: usize = 0;
const OFF_COUNT: usize = 1;
const OFF_NEXT: usize = 3;
const HEADER: usize = 11;
const SLOT: usize = 2;
const CELL_FIXED: usize = 2 + 8;
/// 键长上限:节点里的格子必须放得进一页
pub const MAX_KEY_BYTES: usize = 64;
/// 一个节点最多几个键
fn max_keys(order: usize) -> usize {
order - 1
}
/// 一个格子最多占多少字节
fn max_cell_bytes() -> usize {
CELL_FIXED + MAX_KEY_BYTES
}
/// 这个 order 放得进一页吗
pub fn fits_in_page(page_size: usize, order: usize) -> bool {
order >= 2 && HEADER + max_keys(order) * (SLOT + max_cell_bytes()) <= page_size
}
/// 一页最多几个孩子
pub fn max_order(page_size: usize) -> usize {
page_size.saturating_sub(HEADER) / (SLOT + max_cell_bytes()) + 1
}
/// 读一个 u8
fn read_u8(p: &Page, off: usize) -> u8 {
p.data[off]
}
/// 写一个 u8
fn write_u8(p: &mut Page, off: usize, v: u8) {
p.data[off] = v;
}
/// 读一个小端 u16
fn read_u16(p: &Page, off: usize) -> u16 {
u16::from_le_bytes([p.data[off], p.data[off + 1]])
}
/// 写一个小端 u16
fn write_u16(p: &mut Page, off: usize, v: u16) {
p.data[off..off + 2].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u32
fn read_u32(p: &Page, off: usize) -> u32 {
u32::from_le_bytes([p.data[off], p.data[off + 1], p.data[off + 2], p.data[off + 3]])
}
/// 写一个小端 u32
fn write_u32(p: &mut Page, off: usize, v: u32) {
p.data[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u64
fn read_u64(p: &Page, off: usize) -> u64 {
let mut b = [0u8; 8];
b.copy_from_slice(&p.data[off..off + 8]);
u64::from_le_bytes(b)
}
/// 写一个小端 u64
fn write_u64(p: &mut Page, off: usize, v: u64) {
p.data[off..off + 8].copy_from_slice(&v.to_le_bytes());
}
/// 写文件头
fn write_meta(page: &mut Page, page_size: usize, order: usize, root: PageId) {
page.zero();
page.data[0..4].copy_from_slice(META_MAGIC);
write_u32(page, OFF_PAGE_SIZE, page_size as u32);
write_u16(page, OFF_MAX_KEY, MAX_KEY_BYTES as u16);
write_u64(page, OFF_ROOT, root);
write_u16(page, OFF_ORDER, order as u16);
}
/// 校验文件头与配置
fn check_meta(page: &Page, page_size: usize, order: usize) -> io::Result<()> {
if page.data[0..4] != *META_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"not a chusql index file (bad magic)",
));
}
let stored_page = read_u32(page, OFF_PAGE_SIZE) as usize;
let stored_order = read_u16(page, OFF_ORDER) as usize;
if stored_page != page_size || stored_order != order {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"index file was built with page.size={} btree.order={}, config says page.size={} btree.order={}",
stored_page, stored_order, page_size, order
),
));
}
Ok(())
}
/// 一个节点:叶子存行位置,内部节点存孩子页号
struct Node {
kind: u8,
next: PageId,
keys: Vec<Vec<u8>>,
values: Vec<u64>,
}
/// 一个格子占多少字节
fn cell_bytes(key: &[u8]) -> usize {
CELL_FIXED + key.len()
}
/// 节点占多少字节
fn node_bytes(node: &Node) -> usize {
HEADER + node.keys.len() * SLOT + node.keys.iter().map(|k| cell_bytes(k)).sum::<usize>()
}
/// 把节点写回一页(格子从页尾往前摆,槽位从头部往后长)
fn put_node(p: &mut Page, node: &Node) -> io::Result<()> {
if node.keys.len() > u16::MAX as usize || node_bytes(node) > p.data.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"index node does not fit in a page",
));
}
p.zero();
write_u8(p, OFF_KIND, node.kind);
write_u16(p, OFF_COUNT, node.keys.len() as u16);
// 内部节点比键多一个孩子,最后一个放在 next 槽位里
let trailer = if node.kind == NODE_INTERNAL {
node.values.last().copied().unwrap_or(0)
} else {
node.next
};
write_u64(p, OFF_NEXT, trailer);
let mut cursor = p.data.len();
for (i, key) in node.keys.iter().enumerate() {
let size = cell_bytes(key);
cursor -= size;
write_u16(p, cursor, key.len() as u16);
p.data[cursor + 2..cursor + 2 + key.len()].copy_from_slice(key);
write_u64(p, cursor + 2 + key.len(), node.values[i]);
write_u16(p, HEADER + i * SLOT, cursor as u16);
}
Ok(())
}
/// 从一页读出节点
fn get_node(p: &Page) -> Node {
let kind = read_u8(p, OFF_KIND);
let count = read_u16(p, OFF_COUNT) as usize;
let trailer = read_u64(p, OFF_NEXT);
let mut keys = Vec::with_capacity(count);
let mut values = Vec::with_capacity(count + 1);
for i in 0..count {
let off = read_u16(p, HEADER + i * SLOT) as usize;
let klen = read_u16(p, off) as usize;
keys.push(p.data[off + 2..off + 2 + klen].to_vec());
values.push(read_u64(p, off + 2 + klen));
}
if kind == NODE_INTERNAL {
values.push(trailer);
Node { kind, next: 0, keys, values }
} else {
Node { kind, next: trailer, keys, values }
}
}
/// 叶子里的插入位置:同键按 value 排,保证顺序稳定
fn leaf_slot(keys: &[Vec<u8>], values: &[u64], key: &[u8], value: u64) -> usize {
let mut i = keys.partition_point(|k| k.as_slice() < key);
while i < keys.len() && keys[i].as_slice() == key && values[i] < value {
i += 1;
}
i
}
/// 内部节点里选哪个孩子
fn child_slot(keys: &[Vec<u8>], key: &[u8]) -> usize {
keys.partition_point(|k| k.as_slice() <= key)
}
/// 切开一个节点:返回上提的键与右半节点
fn split_node(node: &mut Node) -> (Vec<u8>, Node) {
let mid = node.keys.len() / 2;
if node.kind == NODE_LEAF {
let sep = node.keys[mid].clone();
let right = Node {
kind: NODE_LEAF,
next: node.next,
keys: node.keys.split_off(mid),
values: node.values.split_off(mid),
};
(sep, right)
} else {
let sep = node.keys[mid].clone();
let right_keys = node.keys.split_off(mid + 1);
let right_values = node.values.split_off(mid + 1);
node.keys.pop();
(
sep,
Node { kind: NODE_INTERNAL, next: 0, keys: right_keys, values: right_values },
)
}
}
/// 磁盘 B+ 树
pub struct DiskBTree {
file: PageFile,
order: usize,
}
impl DiskBTree {
/// 打开或新建索引文件
pub fn open<P: AsRef<Path>>(
path: P,
page_size: usize,
order: usize,
pool_size: usize,
) -> io::Result<Self> {
if !fits_in_page(page_size, order) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"btree order {} does not fit in a {}-byte page (max {})",
order,
page_size,
max_order(page_size)
),
));
}
let mut file = PageFile::with_options(path, page_size, pool_size)?;
if file.num_pages()? == 0 {
let mut meta = Page::new(META_PAGE, page_size);
write_meta(&mut meta, page_size, order, 0);
file.write_page(&meta)?;
} else {
let meta = file.read_page(META_PAGE)?;
check_meta(&meta, page_size, order)?;
}
Ok(DiskBTree { file, order })
}
/// 键太长直接报错,避免写坏节点
fn check_key(&self, key: &[u8]) -> io::Result<()> {
if key.len() > MAX_KEY_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"index key is {} bytes, at most {} are supported",
key.len(),
MAX_KEY_BYTES
),
));
}
Ok(())
}
/// 读根页页号;空树是 None
fn root(&mut self) -> io::Result<Option<PageId>> {
let r = self.file.with_page(META_PAGE, |p| read_u64(p, OFF_ROOT))?;
Ok(if r == 0 { None } else { Some(r) })
}
/// 写根页页号
fn set_root(&mut self, r: Option<PageId>) -> io::Result<()> {
self.file
.update_page(META_PAGE, |p| write_u64(p, OFF_ROOT, r.unwrap_or(0)))
}
/// 追加一个新节点页
fn alloc(&mut self, node: &Node) -> io::Result<PageId> {
let page = self.file.append_page()?;
let id = page.id;
self.file.update_page(id, |p| put_node(p, node))??;
Ok(id)
}
/// 按页号读节点
fn load(&mut self, id: PageId) -> io::Result<Node> {
self.file.with_page(id, get_node)
}
/// 按页号写节点
fn store(&mut self, id: PageId, node: &Node) -> io::Result<()> {
self.file.update_page(id, |p| put_node(p, node))?
}
/// 清空整棵树
pub fn clear(&mut self) -> io::Result<()> {
let page_size = self.file.page_size();
self.file.truncate()?;
let mut meta = Page::new(META_PAGE, page_size);
write_meta(&mut meta, page_size, self.order, 0);
self.file.write_page(&meta)?;
Ok(())
}
/// 树是空的吗(只看最左叶子,不用整树遍历)
pub fn is_empty(&mut self) -> io::Result<bool> {
let Some(mut id) = self.root()? else {
return Ok(true);
};
loop {
let node = self.load(id)?;
if node.kind == NODE_LEAF {
return Ok(node.keys.is_empty());
}
id = node.values[0];
}
}
/// 按 key 取第一个位置
pub fn get(&mut self, key: &[u8]) -> io::Result<Option<u64>> {
Ok(self.get_all(key)?.into_iter().next())
}
/// 按 key 取全部位置(重复键按值升序)
pub fn get_all(&mut self, key: &[u8]) -> io::Result<Vec<u64>> {
let Some(mut id) = self.root()? else {
return Ok(Vec::new());
};
loop {
let node = self.load(id)?;
if node.kind == NODE_LEAF {
let start = node.keys.partition_point(|k| k.as_slice() < key);
let mut out = Vec::new();
for i in start..node.keys.len() {
if node.keys[i].as_slice() != key {
break;
}
out.push(node.values[i]);
}
return Ok(out);
}
id = node.values[child_slot(&node.keys, key)];
}
}
/// 插入一个键值对(键可重复)
pub fn insert(&mut self, key: &[u8], value: u64) -> io::Result<()> {
self.check_key(key)?;
match self.root()? {
None => {
let node = Node {
kind: NODE_LEAF,
next: 0,
keys: vec![key.to_vec()],
values: vec![value],
};
let id = self.alloc(&node)?;
self.set_root(Some(id))
}
Some(root) => match self.insert_rec(root, key, value)? {
None => Ok(()),
Some((sep, right_id)) => {
let node = Node {
kind: NODE_INTERNAL,
next: 0,
keys: vec![sep],
values: vec![root, right_id],
};
let id = self.alloc(&node)?;
self.set_root(Some(id))
}
},
}
}
/// 递归插入,溢出就把右半节点与上提键交给上层
fn insert_rec(
&mut self,
id: PageId,
key: &[u8],
value: u64,
) -> io::Result<Option<(Vec<u8>, PageId)>> {
let mut node = self.load(id)?;
if node.kind == NODE_LEAF {
let at = leaf_slot(&node.keys, &node.values, key, value);
node.keys.insert(at, key.to_vec());
node.values.insert(at, value);
} else {
let child = node.values[child_slot(&node.keys, key)];
if let Some((sep, right_id)) = self.insert_rec(child, key, value)? {
let at = node.keys.partition_point(|k| k.as_slice() <= sep.as_slice());
node.keys.insert(at, sep);
node.values.insert(at + 1, right_id);
}
}
if node.keys.len() <= max_keys(self.order) {
self.store(id, &node)?;
return Ok(None);
}
let (sep, mut right) = split_node(&mut node);
let right_id = self.alloc(&right)?;
if node.kind == NODE_LEAF {
right.next = node.next;
node.next = right_id;
}
self.store(id, &node)?;
Ok(Some((sep, right_id)))
}
/// 删掉一个 (key, value)
pub fn delete(&mut self, key: &[u8], value: u64) -> io::Result<bool> {
let Some(root) = self.root()? else {
return Ok(false);
};
self.delete_rec(root, key, value)
}
/// 递归删一个 (key, value)
fn delete_rec(&mut self, id: PageId, key: &[u8], value: u64) -> io::Result<bool> {
let mut node = self.load(id)?;
if node.kind == NODE_LEAF {
let at = leaf_slot(&node.keys, &node.values, key, value);
if at < node.keys.len() && node.keys[at].as_slice() == key && node.values[at] == value {
node.keys.remove(at);
node.values.remove(at);
self.store(id, &node)?;
return Ok(true);
}
return Ok(false);
}
let child = node.values[child_slot(&node.keys, key)];
self.delete_rec(child, key, value)
}
/// 叶子链上按顺序取全部 (键, 值)
pub fn iter_all(&mut self) -> io::Result<Vec<(Vec<u8>, u64)>> {
self.scan(None, None)
}
/// 范围扫描:lo / hi 都是闭区间,None 表示不限
pub fn scan(
&mut self,
lo: Option<&[u8]>,
hi: Option<&[u8]>,
) -> io::Result<Vec<(Vec<u8>, u64)>> {
let Some(mut id) = self.root()? else {
return Ok(Vec::new());
};
loop {
let node = self.load(id)?;
if node.kind == NODE_LEAF {
id = match lo {
None => id,
Some(start) => {
let at = node.keys.partition_point(|k| k.as_slice() < start);
if at < node.keys.len() { id } else { node.next }
}
};
break;
}
id = match lo {
None => node.values[0],
Some(start) => node.values[child_slot(&node.keys, start)],
};
}
let mut out = Vec::new();
while id != 0 {
let node = self.load(id)?;
for i in 0..node.keys.len() {
if lo.is_some_and(|start| node.keys[i].as_slice() < start) {
continue;
}
if hi.is_some_and(|end| node.keys[i].as_slice() > end) {
return Ok(out);
}
out.push((node.keys[i].clone(), node.values[i]));
}
id = node.next;
}
Ok(out)
}
/// 脏页写回
pub fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
/// 当前缓存页数
pub fn cached_pages(&self) -> usize {
self.file.cached_pages()
}
}
+382
View File
@@ -0,0 +1,382 @@
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::hash::{Hash, Hasher};
use std::io;
use std::io::Write;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::protocol::{Row, SchemaColumn};
// 数据字典:记每张表有哪些列、什么类型、多少行、有哪些索引,以及列级统计。
const STATS_DISTINCT_CAP: u64 = 4096;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexSchema {
pub column: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ColumnStat {
pub distinct: u64,
#[serde(default)]
pub capped: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TableSchema {
#[serde(default)]
pub system: bool,
pub columns: Vec<SchemaColumn>,
#[serde(default)]
pub dropped_columns: BTreeSet<String>,
#[serde(default)]
pub compacted: bool,
#[serde(default)]
pub row_count: u64,
#[serde(default)]
pub indexes: Vec<IndexSchema>,
#[serde(default)]
pub stats: BTreeMap<String, ColumnStat>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Catalog {
tables: BTreeMap<String, TableSchema>,
#[serde(skip)]
seen: BTreeMap<String, HashSet<u64>>,
}
impl Catalog {
/// 从文件读;不存在给空字典
pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
if !path.as_ref().exists() {
return Ok(Catalog::default());
}
let bytes = std::fs::read(path)?;
if bytes.is_empty() {
return Ok(Catalog::default());
}
serde_json::from_slice(&bytes).map_err(io::Error::other)
}
/// 返回所有表的 (表名, schema)。
pub fn all_tables(&self) -> Vec<(&str, &TableSchema)> {
self.tables.iter().map(|(k, v)| (k.as_str(), v)).collect()
}
/// 写回文件
pub fn save<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let bytes = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
let pending = path.as_ref().with_extension("json.pending");
let mut file = std::fs::File::create(&pending)?;
file.write_all(&bytes)?;
file.sync_all()?;
drop(file);
std::fs::rename(pending, &path)?;
std::fs::OpenOptions::new().read(true).write(true).open(path)?.sync_all()
}
/// 一张表的 schema
pub fn describe(&self, table: &str) -> Option<&TableSchema> {
self.tables.get(table)
}
/// 记录过的所有表名
pub fn table_names(&self) -> Vec<String> {
self.tables.keys().cloned().collect()
}
/// 按一行数据补列
pub fn ensure_columns(&mut self, table: &str, row: &Row) {
let entry = self.tables.entry(table.to_string()).or_default();
add_missing_columns(entry, row);
}
/// 建表;已存在报错
pub fn create_table(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> {
if self.tables.contains_key(table) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("table already exists: {}", table),
));
}
self.tables.insert(
table.to_string(),
TableSchema {
system: false,
columns,
dropped_columns: BTreeSet::new(),
compacted: false,
row_count: 0,
indexes: Vec::new(),
stats: BTreeMap::new(),
},
);
Ok(())
}
/// 标记内部账号表
pub fn mark_system(&mut self, table: &str) -> io::Result<()> {
let entry = self.tables.get_mut(table)
.ok_or_else(|| io::Error::other("missing system table"))?;
entry.system = true;
Ok(())
}
/// 记一次插入,返回列有没有变化
pub fn record_insert(&mut self, table: &str, row: &Row) -> bool {
let changed = {
let entry = self.tables.entry(table.to_string()).or_default();
let changed = add_missing_columns(entry, row);
entry.row_count += 1;
changed
};
self.count_values(table, row);
changed
}
/// 记一次删除:行数减少 + 更新列统计
pub fn record_delete(&mut self, table: &str, rows: &[Row]) {
let entry = self.tables.entry(table.to_string()).or_default();
entry.row_count = entry.row_count.saturating_sub(rows.len() as u64);
let seen = self.seen.entry(table.to_string()).or_default();
for r in rows {
for (name, v) in r {
let stat = entry.stats.entry(name.clone()).or_default();
if stat.capped {
continue;
}
if seen.remove(&fold(name, value_hash(v))) {
stat.distinct = stat.distinct.saturating_sub(1);
}
}
}
}
/// 重数一遍统计与行数
pub fn rebuild_stats(&mut self, table: &str, rows: &[Row]) -> bool {
self.seen.remove(table);
{
let entry = self.tables.entry(table.to_string()).or_default();
entry.stats.clear();
entry.row_count = 0;
}
let mut changed = false;
for row in rows {
changed = self.record_insert(table, row) || changed;
}
changed
}
/// 这一列第一次见到这个值就加一
fn count_values(&mut self, table: &str, row: &Row) {
let entry = self.tables.entry(table.to_string()).or_default();
let seen = self.seen.entry(table.to_string()).or_default();
for (name, v) in row {
let stat = entry.stats.entry(name.clone()).or_default();
if stat.capped {
continue;
}
if seen.insert(fold(name, value_hash(v))) {
stat.distinct += 1;
if stat.distinct >= STATS_DISTINCT_CAP {
stat.capped = true;
seen.clear();
}
}
}
}
/// 删表
pub fn drop_table(&mut self, table: &str) -> io::Result<()> {
if self.tables.remove(table).is_none() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("unknown table: {}", table),
));
}
self.seen.remove(table);
Ok(())
}
/// ALTER:整列定义换成新的,并清掉二级索引与统计
pub fn set_columns(&mut self, table: &str, columns: Vec<SchemaColumn>) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
entry.columns = columns;
entry.dropped_columns.clear();
entry.indexes.clear();
entry.stats.clear();
self.seen.remove(table);
Ok(())
}
/// 老数据的列类型名规范化;返回有没有改动
pub fn normalize_types(&mut self) -> bool {
let mut changed = false;
for entry in self.tables.values_mut() {
for column in entry.columns.iter_mut() {
let fixed = normalize_type(&column.ty);
if fixed != column.ty {
column.ty = fixed;
changed = true;
}
}
}
changed
}
/// 删一列;之前没有也返回成功(重放幂等)
pub fn remove_column(&mut self, table: &str, column: &str) -> io::Result<bool> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
let had_column = {
let before = entry.columns.len();
entry.columns.retain(|c| c.name != column);
entry.columns.len() != before
};
entry.stats.remove(column);
entry.dropped_columns.insert(column.to_string());
entry.compacted = false;
entry.indexes.retain(|i| i.column != column);
self.seen.remove(table);
Ok(had_column)
}
/// 记住这一版已整理过(隐藏列名要留住)
pub fn mark_compacted(&mut self, table: &str) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
entry.compacted = true;
self.seen.remove(table);
Ok(())
}
/// 还有隐藏列字节待整理的表
pub fn dropped_tables(&self) -> Vec<String> {
self.tables
.iter()
.filter(|(_, e)| !e.dropped_columns.is_empty() && !e.compacted)
.map(|(t, _)| t.clone())
.collect()
}
/// 记下一条索引定义
pub fn add_index(&mut self, table: &str, column: &str) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
if entry.indexes.iter().any(|i| i.column == column) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("index on column \"{}\" already exists", column),
));
}
entry.indexes.push(IndexSchema {
column: column.to_string(),
});
Ok(())
}
/// 去掉一条索引定义
pub fn remove_index(&mut self, table: &str, column: &str) -> io::Result<()> {
let entry = self
.tables
.get_mut(table)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown table: {}", table)))?;
match entry.indexes.iter().position(|i| i.column == column) {
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no index on column \"{}\"", column),
)),
Some(i) => {
entry.indexes.remove(i);
Ok(())
}
}
}
}
/// 把行里"字典还不知道的列"补进来;返回有没有变化
fn add_missing_columns(entry: &mut TableSchema, row: &Row) -> bool {
let mut changed = false;
for (k, v) in row {
if entry.dropped_columns.contains(k) || entry.columns.iter().any(|c| &c.name == k) {
continue;
}
if let Some(ty) = infer_type(v) {
entry.columns.push(SchemaColumn {
name: k.clone(),
ty,
..Default::default()
});
changed = true;
}
}
changed
}
/// 由 JSON 值猜列类型
fn infer_type(v: &serde_json::Value) -> Option<String> {
match v {
serde_json::Value::Number(_) => Some("int".to_string()),
serde_json::Value::String(_) => Some("str".to_string()),
serde_json::Value::Bool(_) => Some("bool".to_string()),
_ => None,
}
}
/// 类型别名统一成规范名,参数原样保留
pub fn normalize_type(ty: &str) -> String {
let trimmed = ty.trim();
let (base, args) = match trimmed.find('(') {
Some(i) => (&trimmed[..i], &trimmed[i..]),
None => (trimmed, ""),
};
match base.trim().to_ascii_lowercase().as_str() {
"integer" => format!("int{args}"),
"int" => format!("int{args}"),
"bigint" => format!("bigint{args}"),
"smallint" => format!("smallint{args}"),
"str" => format!("str{args}"),
"text" => format!("str{args}"),
"varchar" => format!("varchar{args}"),
"char" => format!("char{args}"),
"bool" => format!("bool{args}"),
"boolean" => format!("bool{args}"),
"float" => format!("float{args}"),
"real" => format!("float{args}"),
"double" => format!("double{args}"),
"decimal" => format!("decimal{args}"),
"numeric" => format!("decimal{args}"),
"date" => format!("date{args}"),
"timestamp" => format!("timestamp{args}"),
"blob" => format!("blob{args}"),
_ => trimmed.to_string(),
}
}
/// 一个值的哈希,统计不同值个数用
fn value_hash(v: &serde_json::Value) -> u64 {
let mut h = DefaultHasher::new();
v.to_string().hash(&mut h);
h.finish()
}
/// 列名与值哈希合成一个哈希
fn fold(column: &str, value_hash: u64) -> u64 {
let mut h = DefaultHasher::new();
column.hash(&mut h);
value_hash.hash(&mut h);
h.finish()
}
+340
View File
@@ -0,0 +1,340 @@
use std::path::PathBuf;
use serde::Deserialize;
use crate::log::Level;
// 全局配置:只从 TOML 文件读,缺项回退内置默认值;
// 存储相关分区由本模块解析校验,其它层的分区一概不碰。
/// 配置文件名
pub const CONFIG_FILE_NAME: &str = "chusql.toml";
/// 配置目录名(随系统惯例放在用户配置目录下)
pub const APP_DIR_NAME: &str = "ChuSQL";
/// Unix 下的数据目录名(XDG 惯例小写,跟安装脚本的默认安装目录一致)
pub const UNIX_APP_DIR_NAME: &str = "chusql";
/// 数据子目录名
pub const DATA_DIR_NAME: &str = "data";
pub const DEFAULT_POOL_SIZE: usize = 1024;
pub const DEFAULT_PAGE_SIZE: usize = 4096;
pub const DEFAULT_BTREE_ORDER: usize = 4;
pub const DEFAULT_LOG_LEVEL: &str = "info";
const MIN_PAGE_SIZE: usize = 512;
const MAX_PAGE_SIZE: usize = 65536;
const MIN_BTREE_ORDER: usize = 3;
/// 默认配置文件路径(按平台惯例)
pub fn default_config_path() -> PathBuf {
#[cfg(windows)]
{
let appdata = std::env::var_os("APPDATA").filter(|v| !v.is_empty());
if let Some(appdata) = appdata {
return PathBuf::from(appdata).join(APP_DIR_NAME).join(CONFIG_FILE_NAME);
}
}
#[cfg(not(windows))]
{
let xdg = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty());
if let Some(xdg) = xdg {
return PathBuf::from(xdg).join(APP_DIR_NAME).join(CONFIG_FILE_NAME);
}
let home = std::env::var_os("HOME").filter(|v| !v.is_empty());
if let Some(home) = home {
return PathBuf::from(home)
.join(".config")
.join(APP_DIR_NAME)
.join(CONFIG_FILE_NAME);
}
}
PathBuf::from(APP_DIR_NAME).join(CONFIG_FILE_NAME)
}
/// Windows 默认数据目录
#[cfg(windows)]
pub fn default_data_dir() -> PathBuf {
let local = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty());
if let Some(local) = local {
return PathBuf::from(local).join(APP_DIR_NAME).join(DATA_DIR_NAME);
}
let roaming = std::env::var_os("APPDATA").filter(|v| !v.is_empty());
if let Some(roaming) = roaming {
return PathBuf::from(roaming).join(APP_DIR_NAME).join(DATA_DIR_NAME);
}
PathBuf::from(APP_DIR_NAME).join(DATA_DIR_NAME)
}
/// Unix 默认数据目录(XDG 惯例)
#[cfg(not(windows))]
pub fn default_data_dir() -> PathBuf {
let xdg = std::env::var_os("XDG_DATA_HOME").filter(|v| !v.is_empty());
if let Some(xdg) = xdg {
return PathBuf::from(xdg)
.join(UNIX_APP_DIR_NAME)
.join(DATA_DIR_NAME);
}
let home = std::env::var_os("HOME").filter(|v| !v.is_empty());
if let Some(home) = home {
return PathBuf::from(home)
.join(".local")
.join("share")
.join(UNIX_APP_DIR_NAME)
.join(DATA_DIR_NAME);
}
PathBuf::from(UNIX_APP_DIR_NAME).join(DATA_DIR_NAME)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub page_size: usize,
pub btree_order: usize,
pub pool_size: usize,
pub data_dir: PathBuf,
pub log_level: Level,
}
impl Default for Config {
/// 内置默认配置
fn default() -> Self {
Config {
page_size: DEFAULT_PAGE_SIZE,
btree_order: DEFAULT_BTREE_ORDER,
pool_size: DEFAULT_POOL_SIZE,
data_dir: default_data_dir(),
log_level: Level::Info,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Origin {
Default,
File(PathBuf),
}
impl Origin {
/// 来源的文案
pub fn describe(&self) -> String {
match self {
Origin::Default => "default".to_string(),
Origin::File(p) => format!("file {}", p.display()),
}
}
}
#[derive(Debug, Clone)]
pub struct Loaded {
pub config: Config,
pub config_path: Option<PathBuf>,
pub origins: Vec<(&'static str, String, Origin)>,
}
#[derive(Debug, Default, Deserialize)]
struct FileConfig {
page: Option<FilePage>,
btree: Option<FileBtree>,
buffer: Option<FileBuffer>,
storage: Option<FileStorage>,
log: Option<FileLog>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileBuffer {
pool_size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FilePage {
size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileBtree {
order: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileStorage {
data_dir: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileLog {
level: Option<String>,
}
/// 加载配置:显式路径优先,其次默认位置
pub fn load(explicit: Option<&str>) -> Result<Loaded, String> {
let explicit = explicit.map(str::trim).filter(|p| !p.is_empty());
let path = match explicit {
Some(p) => {
let p = PathBuf::from(p);
if !p.is_file() {
return Err(format!("config file not found: {}", p.display()));
}
Some(p)
}
None => {
let p = default_config_path();
if p.is_file() { Some(p) } else { None }
}
};
let text = match &path {
Some(p) => Some(
std::fs::read_to_string(p)
.map_err(|e| format!("cannot read config file {}: {}", p.display(), e))?,
),
None => None,
};
resolve(text.as_deref(), path)
}
/// 解析文本:文件 -> 默认
pub fn resolve(text: Option<&str>, path: Option<PathBuf>) -> Result<Loaded, String> {
let file: FileConfig = match text {
None => FileConfig::default(),
Some(t) => toml::from_str(t).map_err(|e| match &path {
Some(p) => format!("bad config file {}: {}", p.display(), e),
None => format!("bad config: {}", e),
})?,
};
let file_origin = Origin::File(
path.clone().unwrap_or_else(default_config_path),
);
let (page_size, page_origin) = pick(
file.page.and_then(|p| p.size),
DEFAULT_PAGE_SIZE,
&file_origin,
);
let (btree_order, order_origin) = pick(
file.btree.and_then(|b| b.order),
DEFAULT_BTREE_ORDER,
&file_origin,
);
let (pool_size, pool_origin) = pick(
file.buffer.and_then(|b| b.pool_size),
DEFAULT_POOL_SIZE,
&file_origin,
);
let (data_dir, dir_origin) = pick(
file.storage.as_ref().and_then(|s| s.data_dir.clone()),
default_data_dir().to_string_lossy().into_owned(),
&file_origin,
);
// 管名已经没有了:存储不再是独立进程,管道/套接字那一层随 FFI 一起删掉
let (log_level, log_origin) = pick(
file.log.and_then(|l| l.level),
DEFAULT_LOG_LEVEL.to_string(),
&file_origin,
);
let data_dir = data_dir.trim().to_string();
let log_level = log_level.trim().to_string();
let log_level = Level::parse(&log_level)
.ok_or_else(|| format!("log.level is not one of off/error/warn/info/debug: {}", log_level))?;
if data_dir.is_empty() {
return Err("storage.data_dir must not be empty".to_string());
}
validate_layout(page_size, btree_order)?;
if pool_size == 0 {
return Err("buffer.pool_size must be at least 1".to_string());
}
let origins = vec![
("page.size", page_size.to_string(), page_origin),
("btree.order", btree_order.to_string(), order_origin),
("buffer.pool_size", pool_size.to_string(), pool_origin),
("storage.data_dir", data_dir.clone(), dir_origin),
("log.level", log_level.name().to_string(), log_origin),
];
Ok(Loaded {
config: Config {
page_size,
btree_order,
pool_size,
data_dir: PathBuf::from(data_dir),
log_level,
},
config_path: if text.is_some() { path } else { None },
origins,
})
}
/// 取一项:文件 -> 默认
fn pick<T>(file_value: Option<T>, default: T, file_origin: &Origin) -> (T, Origin) {
if let Some(value) = file_value {
return (value, file_origin.clone());
}
(default, Origin::Default)
}
/// 检查选项是否自洽
fn validate_layout(page_size: usize, btree_order: usize) -> Result<(), String> {
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&page_size) || !page_size.is_power_of_two() {
return Err(format!(
"page.size must be a power of two between {} and {}, got {}",
MIN_PAGE_SIZE, MAX_PAGE_SIZE, page_size
));
}
if btree_order < MIN_BTREE_ORDER {
return Err(format!(
"btree.order must be at least {}, got {}",
MIN_BTREE_ORDER, btree_order
));
}
if !crate::btree::fits_in_page(page_size, btree_order) {
return Err(format!(
"btree.order {} does not fit in a {}-byte page (max {})",
btree_order,
page_size,
crate::btree::max_order(page_size)
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// 别的层的分区不能被当成错误
#[test]
fn ignores_sections_owned_by_other_layers() {
let text = r#"
[page]
size = 8192
[storage]
data_dir = "../data"
[server]
host = "127.0.0.1"
port = 7777
max_message = 1048576
[web]
port = 7778
user = "root"
"#;
let loaded = resolve(Some(text), Some(PathBuf::from("chusql.toml"))).unwrap();
assert_eq!(loaded.config.page_size, 8192);
assert_eq!(loaded.config.data_dir, PathBuf::from("../data"));
}
/// 自己分区里的拼写错误仍然要报错
#[test]
fn still_rejects_typos_inside_its_own_sections() {
let text = "[page]\npage_size = 8192\n";
assert!(resolve(Some(text), None).is_err());
}
}
+154
View File
@@ -0,0 +1,154 @@
use std::cell::RefCell;
use std::ffi::{c_char, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;
use std::slice;
use crate::runtime::Storage;
// C ABI:Haskell 侧唯一存储入口,请求与响应都是 UTF-8 JSON。
// 不 panic 穿过边界,失败走线程局部 last_error。
const OK: i32 = 0;
const ERR: i32 = -1;
thread_local! {
static LAST_ERROR: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// 记下本线程最近的失败信息
fn set_error(message: impl AsRef<str>) {
let text = message.as_ref().as_bytes().to_vec();
LAST_ERROR.with(|slot| *slot.borrow_mut() = text);
}
/// 版本号字符串(静态常量,调用方不要释放)。
#[unsafe(no_mangle)]
pub extern "C" fn chusql_storage_version() -> *const c_char {
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
VERSION.as_ptr() as *const c_char
}
/// 打开数据目录;失败返回 NULL
///
/// # Safety
/// `config_path` 要么是 NULL,要么指向以 NUL 结尾的有效字符串。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_open(config_path: *const c_char) -> *mut Storage {
let opened = catch_unwind(AssertUnwindSafe(|| {
let path = if config_path.is_null() {
None
} else {
let text = unsafe { CStr::from_ptr(config_path) }
.to_str()
.map_err(|_| "config path is not valid UTF-8".to_string())?;
if text.is_empty() { None } else { Some(text.to_string()) }
};
Storage::open(path.as_deref()).map(Box::new)
}));
match opened {
Ok(Ok(storage)) => Box::into_raw(storage),
Ok(Err(e)) => {
set_error(e);
ptr::null_mut()
}
Err(_) => {
set_error("panic in chusql_storage_open");
ptr::null_mut()
}
}
}
/// 关闭句柄(NULL 安全)
///
/// # Safety
/// `handle` 必须是 chusql_storage_open 返回且未被关闭过的句柄。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_close(handle: *mut Storage) {
if handle.is_null() {
return;
}
let _ = catch_unwind(AssertUnwindSafe(|| unsafe { drop(Box::from_raw(handle)) }));
}
/// 处理一条请求,响应缓冲须归还
///
/// # Safety
/// `handle` 必须是有效句柄;`request` 指向 `request_len` 个字节;`out`/`out_len` 可写。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_request(
handle: *const Storage,
request: *const u8,
request_len: usize,
out: *mut *mut u8,
out_len: *mut usize,
) -> i32 {
if !out.is_null() {
unsafe { *out = ptr::null_mut() };
}
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
if handle.is_null() || request.is_null() || out.is_null() || out_len.is_null() {
set_error("invalid argument");
return ERR;
}
let storage = unsafe { &*handle };
let bytes = unsafe { slice::from_raw_parts(request, request_len) };
let text = match std::str::from_utf8(bytes) {
Ok(text) => text,
Err(e) => {
set_error(format!("request is not valid UTF-8: {}", e));
return ERR;
}
};
let response = match catch_unwind(AssertUnwindSafe(|| storage.request_line(text))) {
Ok(response) => response,
Err(_) => {
set_error("panic in chusql_storage_request");
return ERR;
}
};
let mut buffer = response.into_bytes().into_boxed_slice();
let ptr = buffer.as_mut_ptr();
let len = buffer.len();
std::mem::forget(buffer);
unsafe {
*out = ptr;
*out_len = len;
}
OK
}
/// 归还响应缓冲
///
/// # Safety
/// `data`/`len` 必须是 chusql_storage_request 原样返回且未被释放过的一对值。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_free(data: *mut u8, len: usize) {
if data.is_null() {
return;
}
let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
let slice = ptr::slice_from_raw_parts_mut(data, len);
let _owned: Box<[u8]> = Box::from_raw(slice);
}));
}
/// 取本线程最近的失败信息
///
/// # Safety
/// `out_len` 可写,或为 NULL。
#[unsafe(no_mangle)]
pub unsafe extern "C" fn chusql_storage_last_error(out_len: *mut usize) -> *const u8 {
LAST_ERROR.with(|slot| {
let bytes = slot.borrow();
if !out_len.is_null() {
unsafe { *out_len = bytes.len() };
}
if bytes.is_empty() { ptr::null() } else { bytes.as_ptr() }
})
}
+672
View File
@@ -0,0 +1,672 @@
use std::collections::HashSet;
use std::io;
use std::path::Path;
use serde::de::{IgnoredAny, MapAccess, Visitor};
use serde::Deserializer;
use crate::btree::DiskBTree;
use crate::page::PageId;
use crate::page::{Page, PageFile};
use crate::protocol::Row;
// 堆表:行按插入顺序进页,支持全表扫描、按索引取行、行级删除。
const HEADER_SIZE: usize = 2;
const SLOT_SIZE: usize = 8;
const SLOT_DIR_START: usize = HEADER_SIZE;
/// 未选字段只校验并跳过,不分配值。
fn decode_row(bytes: &[u8], columns: Option<&[String]>, hidden: &HashSet<String>) -> io::Result<Row> {
struct RowVisitor<'a>(Option<&'a [String]>, &'a HashSet<String>);
impl<'de> Visitor<'de> for RowVisitor<'_> {
type Value = Row;
/// 说明期望的输入形态
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a row object")
}
/// 逐字段解出一行
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Row, M::Error> {
let mut row = Row::new();
while let Some(key) = map.next_key::<String>()? {
if !self.1.contains(&key) && self.0.is_none_or(|cols| cols.contains(&key)) {
row.insert(key, map.next_value()?);
} else {
map.next_value::<IgnoredAny>()?;
}
}
Ok(row)
}
}
let mut decoder = serde_json::Deserializer::from_slice(bytes);
let row = decoder.deserialize_map(RowVisitor(columns, hidden)).map_err(io::Error::other)?;
decoder.end().map_err(io::Error::other)?;
Ok(row)
}
/// 读一个小端 u16
fn read_u16(buf: &[u8], off: usize) -> u16 {
u16::from_le_bytes([buf[off], buf[off + 1]])
}
/// 写一个小端 u16
fn write_u16(buf: &mut [u8], off: usize, v: u16) {
buf[off..off + 2].copy_from_slice(&v.to_le_bytes());
}
/// 读一个小端 u32
fn read_u32(buf: &[u8], off: usize) -> u32 {
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
}
/// 写一个小端 u32
fn write_u32(buf: &mut [u8], off: usize, v: u32) {
buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
/// (页号, 槽位号) 打包成 u64
fn pack_position(page_id: PageId, slot: u16) -> u64 {
(page_id << 16) | (slot as u64)
}
/// 从 u64 解回 (页号, 槽位号)
fn unpack_position(v: u64) -> (PageId, u16) {
(v >> 16, (v & 0xFFFF) as u16)
}
/// 取行里某一列的整数值(不是整数就没有)
fn row_int(row: &Row, column: &str) -> Option<i64> {
match row.get(column) {
Some(serde_json::Value::Number(n)) => n.as_i64(),
_ => None,
}
}
/// i64 键 → 8 字节大端(保序,负数也在前)
fn key_of(value: i64) -> [u8; 8] {
(value as u64 ^ (1u64 << 63)).to_be_bytes()
}
/// JSON 值 → 索引键字节(与写索引时同一套编码)
fn value_key(v: &serde_json::Value) -> Option<Vec<u8>> {
match v {
serde_json::Value::Number(n) => n.as_i64().map(|k| key_of(k).to_vec()),
serde_json::Value::String(s) => Some(s.as_bytes().to_vec()),
serde_json::Value::Bool(b) => Some(vec![u8::from(*b)]),
_ => None,
}
}
/// 取行里某一列的索引键;NULL 与缺列不入索引
fn row_key(row: &Row, column: &str) -> Option<Vec<u8>> {
row.get(column).and_then(value_key)
}
pub struct HeapPage;
impl HeapPage {
/// 这一页有几个槽位
pub fn slot_count(page: &Page) -> u16 {
read_u16(&page.data, 0)
}
/// 改写槽位数量
fn set_slot_count(page: &mut Page, v: u16) {
write_u16(&mut page.data, 0, v);
}
/// 空闲空间下界
fn free_start(page: &Page) -> usize {
SLOT_DIR_START + (Self::slot_count(page) as usize) * SLOT_SIZE
}
/// 空闲空间上界
fn free_end(page: &Page) -> usize {
let n = Self::slot_count(page);
let mut min_off = page.data.len() as u32;
for i in 0..n as usize {
let slot_off = SLOT_DIR_START + i * SLOT_SIZE;
let off = read_u32(&page.data, slot_off);
let len = read_u32(&page.data, slot_off + 4);
if len > 0 && off < min_off {
min_off = off;
}
}
min_off as usize
}
/// 试插一行,放不下返回 None
pub fn insert_tuple(page: &mut Page, tuple: &[u8]) -> Option<u16> {
let n = Self::slot_count(page);
let free_start = Self::free_start(page);
let free_end = Self::free_end(page);
if free_start + SLOT_SIZE + tuple.len() > free_end {
return None;
}
let tuple_off = free_end - tuple.len();
page.data[tuple_off..tuple_off + tuple.len()].copy_from_slice(tuple);
let slot_off = SLOT_DIR_START + (n as usize) * SLOT_SIZE;
write_u32(&mut page.data, slot_off, tuple_off as u32);
write_u32(&mut page.data, slot_off + 4, tuple.len() as u32);
Self::set_slot_count(page, n + 1);
Some(n)
}
/// 删掉一个槽位,只把长度置 0
pub fn delete_tuple(page: &mut Page, slot: u16) -> bool {
let n = Self::slot_count(page);
if slot >= n {
return false;
}
let slot_off = SLOT_DIR_START + (slot as usize) * SLOT_SIZE;
if read_u32(&page.data, slot_off + 4) == 0 {
return false;
}
write_u32(&mut page.data, slot_off + 4, 0);
true
}
/// 读槽位里的行字节
pub fn get_tuple(page: &Page, slot: u16) -> Option<Vec<u8>> {
let n = Self::slot_count(page);
if slot >= n {
return None;
}
let slot_off = SLOT_DIR_START + (slot as usize) * SLOT_SIZE;
let off = read_u32(&page.data, slot_off) as usize;
let len = read_u32(&page.data, slot_off + 4) as usize;
if len == 0 {
return None;
}
Some(page.data[off..off + len].to_vec())
}
/// 所有槽位号
pub fn iter_slots(page: &Page) -> Vec<u16> {
(0..Self::slot_count(page)).collect()
}
}
struct NamedIndex {
column: String,
tree: DiskBTree,
}
pub struct HeapTable {
hidden_columns: HashSet<String>,
file: PageFile,
indexes: Vec<NamedIndex>,
}
impl HeapTable {
/// 打开表(不带索引)
pub fn open<P: AsRef<Path>>(path: P, page_size: usize, pool_size: usize) -> io::Result<Self> {
Ok(HeapTable {
hidden_columns: HashSet::new(),
file: PageFile::with_options(path, page_size, pool_size)?,
indexes: Vec::new(),
})
}
/// 打开表并挂上 id 列索引
pub fn open_indexed<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
index_path: Q,
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<Self> {
Self::open_with_indexes(
path,
&[("id".to_string(), index_path.as_ref().to_path_buf())],
page_size,
btree_order,
pool_size,
)
}
/// 打开表并挂上多组索引
pub fn open_with_indexes<P: AsRef<Path>>(
path: P,
indexes: &[(String, std::path::PathBuf)],
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<Self> {
let mut t = HeapTable {
hidden_columns: HashSet::new(),
file: PageFile::with_options(path, page_size, pool_size)?,
indexes: Vec::new(),
};
let mut filled = false;
for (col, p) in indexes {
match t.attach_index(col, p, page_size, btree_order, pool_size) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::InvalidData => {
// 格式不兼容(例如上一版落的索引文件):删掉重开
let _ = std::fs::remove_file(p);
t.attach_index(col, p, page_size, btree_order, pool_size)?;
}
Err(err) => return Err(err),
}
// 空索引(新文件、被截断、强杀留下)都按堆数据补齐
if t.index_empty(col)? {
t.fill_index(col)?;
filled = true;
}
}
if filled {
t.flush()?;
}
Ok(t)
}
/// 挂上一棵索引树,不填数据
fn attach_index<P: AsRef<Path>>(
&mut self,
column: &str,
path: P,
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<()> {
let tree = DiskBTree::open(path, page_size, btree_order, pool_size)?;
self.indexes.push(NamedIndex {
column: column.to_string(),
tree,
});
Ok(())
}
/// 这一列有索引吗
pub fn has_index(&self, column: &str) -> bool {
self.indexes.iter().any(|i| i.column == column)
}
/// 建索引:id 列必须唯一,其余列可重复
pub fn build_index(
&mut self,
column: &str,
path: &Path,
page_size: usize,
btree_order: usize,
pool_size: usize,
) -> io::Result<()> {
if self.has_index(column) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("index on column \"{}\" already exists", column),
));
}
let mut seen: HashSet<Vec<u8>> = HashSet::new();
let mut entries: Vec<(Vec<u8>, u64)> = Vec::new();
for (page_id, slot, r) in self.scan_with_positions()? {
let Some(k) = row_key(&r, column) else {
continue;
};
// 行身份索引仍然唯一;其余列允许重复值
if column == "id" && !seen.insert(k.clone()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("column \"{}\" has duplicate values", column),
));
}
entries.push((k, pack_position(page_id, slot)));
}
self.attach_index(column, path, page_size, btree_order, pool_size)?;
let i = self.indexes.len() - 1;
self.indexes[i].tree.clear()?;
for (k, pos) in entries {
self.indexes[i].tree.insert(&k, pos)?;
}
Ok(())
}
/// 这一列的索引是空的吗
fn index_empty(&mut self, column: &str) -> io::Result<bool> {
match self.indexes.iter_mut().find(|i| i.column == column) {
Some(idx) => idx.tree.is_empty(),
None => Ok(false),
}
}
/// 按堆里的行把某一列索引补齐
fn fill_index(&mut self, column: &str) -> io::Result<()> {
let rows = self.scan_with_positions()?;
let Some(i) = self.indexes.iter().position(|idx| idx.column == column) else {
return Ok(());
};
for (page_id, slot, row) in rows {
if let Some(k) = row_key(&row, column) {
self.indexes[i]
.tree
.insert(&k, pack_position(page_id, slot))?;
}
}
Ok(())
}
/// 摘掉一个索引(不删文件)
pub fn detach_index(&mut self, column: &str) -> bool {
match self.indexes.iter().position(|i| i.column == column) {
None => false,
Some(i) => {
self.indexes.remove(i);
true
}
}
}
/// 设置隐藏列集合
pub fn set_hidden_columns(&mut self, columns: HashSet<String>) {
self.hidden_columns = columns;
}
/// 隐藏一列并摘掉它的索引
pub fn hide_column(&mut self, column: &str) {
self.detach_index(column);
self.hidden_columns.insert(column.to_string());
}
/// 插入一行,并顺手写各索引
pub fn insert_row(&mut self, row: &Row) -> io::Result<()> {
self.insert_row_returning_position(row).map(|_| ())
}
/// 插入一行并返回它的位置
pub fn insert_row_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
let mut pending: Vec<(usize, Vec<u8>)> = Vec::new();
for (i, idx) in self.indexes.iter_mut().enumerate() {
let Some(k) = row_key(row, &idx.column) else {
continue;
};
if idx.column == "id" && idx.tree.get(&k)?.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"duplicate value on the id index".to_string(),
));
}
pending.push((i, k));
}
let (page_id, slot) = self.insert_returning_position(row)?;
let pos = pack_position(page_id, slot);
for (i, k) in pending {
self.indexes[i].tree.insert(&k, pos)?;
}
Ok((page_id, slot))
}
/// 批量插入,落盘由调用方负责
pub fn insert_rows(&mut self, rows: &[Row]) -> io::Result<()> {
for r in rows {
self.insert_row(r)?;
}
Ok(())
}
/// 编码并验证一行能放进页
pub fn encode_row(row: &Row, page_size: usize) -> io::Result<Vec<u8>> {
let tuple = serde_json::to_vec(row).map_err(io::Error::other)?;
if HEADER_SIZE + SLOT_SIZE + tuple.len() > page_size {
return Err(io::Error::other(format!(
"tuple too large: {} bytes",
tuple.len()
)));
}
Ok(tuple)
}
/// 插入一行,只进堆表、不碰索引
fn insert_returning_position(&mut self, row: &Row) -> io::Result<(PageId, u16)> {
let tuple = Self::encode_row(row, self.file.page_size())?;
let n = self.file.num_pages()?;
if n > 0 {
let last_id = n - 1;
let mut slot = None;
self.file.update_page(last_id, |p| {
slot = HeapPage::insert_tuple(p, &tuple);
})?;
if let Some(s) = slot {
return Ok((last_id, s));
}
}
let mut p = self.file.append_page()?;
let page_id = p.id;
let slot = HeapPage::insert_tuple(&mut p, &tuple).expect("pre-checked fits");
self.file.write_page(&p)?;
Ok((page_id, slot))
}
/// 按 `id` 取一行
pub fn get_by_key(&mut self, key: i64) -> io::Result<Option<Row>> {
self.get_by_column_key("id", key)
}
/// 按某一列的整数索引取一行
pub fn get_by_column_key(&mut self, column: &str, key: i64) -> io::Result<Option<Row>> {
self.get_by_bytes(column, &key_of(key))
}
/// 按某一列的字符串索引取一行
pub fn get_by_string_key(&mut self, column: &str, key: &str) -> io::Result<Option<Row>> {
self.get_by_bytes(column, key.as_bytes())
}
/// 按某一列的整数索引取全部行
pub fn get_all_by_column_key(&mut self, column: &str, key: i64) -> io::Result<Vec<Row>> {
self.get_all_by_bytes(column, &key_of(key))
}
/// 按某一列的字符串索引取全部行
pub fn get_all_by_string_key(&mut self, column: &str, key: &str) -> io::Result<Vec<Row>> {
self.get_all_by_bytes(column, key.as_bytes())
}
/// 按索引键的字节取第一行
fn get_by_bytes(&mut self, column: &str, key: &[u8]) -> io::Result<Option<Row>> {
Ok(self.get_all_by_bytes(column, key)?.into_iter().next())
}
/// 按索引键的字节取全部行(重复值都给出来)
fn get_all_by_bytes(&mut self, column: &str, key: &[u8]) -> io::Result<Vec<Row>> {
let Some(idx) = self.indexes.iter_mut().find(|i| i.column == column) else {
return Ok(Vec::new());
};
let positions = idx.tree.get_all(key)?;
let mut rows = Vec::new();
for pos in positions {
let (page_id, slot) = unpack_position(pos);
if let Some(row) = self.read_at(page_id, slot)? {
rows.push(row);
}
}
Ok(rows)
}
/// 按索引做范围扫描;没索引返回 None
pub fn scan_range(
&mut self,
column: &str,
lo: Option<(&serde_json::Value, bool)>,
hi: Option<(&serde_json::Value, bool)>,
) -> io::Result<Option<Vec<Row>>> {
let Some(i) = self.indexes.iter().position(|idx| idx.column == column) else {
return Ok(None);
};
let lo_bound = lo.and_then(|(v, inclusive)| value_key(v).map(|k| (k, inclusive)));
let hi_bound = hi.and_then(|(v, inclusive)| value_key(v).map(|k| (k, inclusive)));
// 树按闭区间取,排他端在取回来之后再筛
let pairs = self.indexes[i].tree.scan(
lo_bound.as_ref().map(|(k, _)| k.as_slice()),
hi_bound.as_ref().map(|(k, _)| k.as_slice()),
)?;
let mut rows = Vec::new();
for (key, pos) in pairs {
if let Some((bound, inclusive)) = &lo_bound {
let outside = if *inclusive { &key < bound } else { &key <= bound };
if outside {
continue;
}
}
if let Some((bound, inclusive)) = &hi_bound {
let outside = if *inclusive { &key > bound } else { &key >= bound };
if outside {
continue;
}
}
let (page_id, slot) = unpack_position(pos);
if let Some(row) = self.read_at(page_id, slot)? {
rows.push(row);
}
}
Ok(Some(rows))
}
/// 按位置读一行
pub fn read_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
let bytes = self.file.with_page(page_id, |p| HeapPage::get_tuple(p, slot))?;
match bytes {
None => Ok(None),
Some(bytes) => {
let row = decode_row(&bytes, None, &self.hidden_columns)?;
Ok(Some(row))
}
}
}
/// 按位置删一行并返回它
pub fn delete_at(&mut self, page_id: PageId, slot: u16) -> io::Result<Option<Row>> {
let row = self.read_at(page_id, slot)?;
let existed = self.file.update_page(page_id, |p| HeapPage::delete_tuple(p, slot))?;
if !existed {
return Ok(None);
}
if let Some(r) = &row {
let pos = pack_position(page_id, slot);
for idx in &mut self.indexes {
if let Some(k) = row_key(r, &idx.column) {
idx.tree.delete(&k, pos)?;
}
}
}
Ok(row)
}
/// 按 id 批量删行,返回删掉的行
pub fn delete_by_keys(&mut self, keys: &[i64]) -> io::Result<Vec<Row>> {
let mut positions: Vec<(PageId, u16)> = Vec::new();
if self.has_index("id") {
for &k in keys {
let pos = self
.indexes
.iter_mut()
.find(|i| i.column == "id")
.and_then(|i| i.tree.get(&key_of(k)).ok().flatten());
if let Some(p) = pos {
positions.push(unpack_position(p));
}
}
} else {
let mut wanted: Vec<i64> = keys.to_vec();
wanted.sort_unstable();
for (page_id, slot, r) in self.scan_with_positions()? {
if row_int(&r, "id").is_some_and(|k| wanted.binary_search(&k).is_ok()) {
positions.push((page_id, slot));
}
}
}
let mut deleted: Vec<Row> = Vec::new();
for (page_id, slot) in positions {
if let Some(r) = self.delete_at(page_id, slot)? {
deleted.push(r);
}
}
Ok(deleted)
}
/// 全表扫描
pub fn scan(&mut self) -> io::Result<Vec<Row>> {
self.scan_columns(None)
}
/// 全表扫描,只解码指定列
pub fn scan_columns(&mut self, columns: Option<&[String]>) -> io::Result<Vec<Row>> {
Ok(self
.scan_projected_with_positions(columns)?
.into_iter()
.map(|(_, _, r)| r)
.collect())
}
/// 全表扫描,同时给出每行的位置(建索引用)
pub fn scan_with_positions(&mut self) -> io::Result<Vec<(PageId, u16, Row)>> {
self.scan_projected_with_positions(None)
}
/// 全表扫描,带位置与列投影
fn scan_projected_with_positions(&mut self, columns: Option<&[String]>) -> io::Result<Vec<(PageId, u16, Row)>> {
let n = self.file.num_pages()?;
let mut out = Vec::new();
for i in 0..n {
let tuples = self.file.with_page(i, |p| {
let mut v = Vec::new();
for slot in HeapPage::iter_slots(p) {
if let Some(bytes) = HeapPage::get_tuple(p, slot) {
v.push((slot, bytes));
}
}
v
})?;
for (slot, bytes) in tuples {
let row = decode_row(&bytes, columns, &self.hidden_columns)?;
out.push((i, slot, row));
}
}
Ok(out)
}
/// 整表改写并重建所有索引
pub fn replace_all(&mut self, rows: &[Row]) -> io::Result<()> {
self.file.truncate()?;
for idx in &mut self.indexes {
idx.tree.clear()?;
}
for r in rows {
let (page_id, slot) = self.insert_returning_position(r)?;
let pos = pack_position(page_id, slot);
for idx in &mut self.indexes {
if let Some(k) = row_key(r, &idx.column) {
idx.tree.insert(&k, pos)?;
}
}
}
Ok(())
}
/// 把脏页写回(索引也一起)
pub fn flush(&mut self) -> io::Result<()> {
for idx in &mut self.indexes {
idx.tree.flush()?;
}
self.file.flush()
}
/// 当前缓存页数
pub fn cached_pages(&self) -> usize { self.file.cached_pages() }
/// 命中次数
pub fn hits(&self) -> u64 { self.file.hits() }
/// 未命中次数
pub fn misses(&self) -> u64 { self.file.misses() }
/// 命中率
pub fn hit_rate(&self) -> f64 { self.file.hit_rate() }
}
+206
View File
@@ -0,0 +1,206 @@
use crate::protocol::SchemaColumn;
// 读取 scripts/init.sql,只认 CREATE TABLE 与 CREATE INDEX。
// 系统库的 schema 由这份脚本声明,进程按它建系统表。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitColumn {
pub name: String,
pub ty: String,
pub nullable: bool,
pub auto_increment: bool,
pub primary_key: bool,
pub unique: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitTable {
pub name: String,
pub columns: Vec<InitColumn>,
pub indexes: Vec<String>,
}
impl InitColumn {
/// 转成数据字典里的列
pub fn to_schema(&self) -> SchemaColumn {
SchemaColumn {
name: self.name.clone(),
ty: self.ty.clone(),
nullable: self.nullable,
default: None,
auto_increment: self.auto_increment,
primary_key: self.primary_key,
unique: self.unique,
check: None,
}
}
}
/// 解析整份脚本;不认识的语句直接报错,避免悄悄漏建表
pub fn parse(text: &str) -> Result<Vec<InitTable>, String> {
let mut tables: Vec<InitTable> = Vec::new();
for statement in statements(text) {
let words = statement.split_whitespace().collect::<Vec<_>>();
let head = |index: usize| words.get(index).map(|word| word.to_ascii_uppercase());
match (head(0).as_deref(), head(1).as_deref()) {
(Some("CREATE"), Some("TABLE")) => {
let name = clean_name(words.get(2).ok_or("CREATE TABLE needs a name")?);
let body = statement.split_once('(').ok_or("CREATE TABLE needs a column list")?.1;
let inner = body.rsplit_once(')').map(|(inner, _)| inner).unwrap_or(body);
tables.push(InitTable { name, columns: columns(inner)?, indexes: Vec::new() });
}
(Some("CREATE"), Some("INDEX")) => {
let table = clean_name(words.get(2).ok_or("CREATE INDEX needs a table")?);
let column = statement
.split_once('(')
.and_then(|(_, rest)| rest.split_once(')'))
.map(|(column, _)| clean_name(column))
.ok_or("CREATE INDEX needs a column")?;
let entry = tables
.iter_mut()
.find(|entry| entry.name.eq_ignore_ascii_case(&table))
.ok_or_else(|| format!("CREATE INDEX on unknown table: {table}"))?;
entry.indexes.push(column);
}
_ => return Err(format!("unsupported statement: {}", statement.trim())),
}
}
Ok(tables)
}
/// 解析表定义里的列清单
fn columns(body: &str) -> Result<Vec<InitColumn>, String> {
let mut out = Vec::new();
for part in split_top_level(body) {
let words = part.split_whitespace().collect::<Vec<_>>();
if words.is_empty() {
continue;
}
let keyword = words[0].to_ascii_uppercase();
if matches!(keyword.as_str(), "PRIMARY" | "UNIQUE" | "KEY" | "CONSTRAINT" | "INDEX" | "CHECK" | "FOREIGN") {
return Err(format!("table-level constraint is not supported: {}", part.trim()));
}
let name = clean_name(words[0]);
let ty = words
.get(1)
.ok_or_else(|| format!("column {name} needs a type"))?
.to_string();
let upper = part.to_ascii_uppercase();
out.push(InitColumn {
name,
ty,
nullable: !upper.contains("NOT NULL"),
auto_increment: upper.contains("AUTO_INCREMENT"),
primary_key: upper.contains("PRIMARY KEY"),
unique: upper.contains("UNIQUE"),
});
}
Ok(out)
}
/// 按顶层的逗号切开(类型里的括号不算)
fn split_top_level(body: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut depth = 0;
for ch in body.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
parts.push(std::mem::take(&mut current));
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
parts.push(current);
}
parts
}
/// 去掉反引号与空白
fn clean_name(raw: &str) -> String {
raw.trim().trim_matches('`').trim_matches('"').trim().to_string()
}
/// 去掉注释后按分号切开
fn statements(text: &str) -> Vec<String> {
let mut cleaned = String::new();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '-' && chars.peek() == Some(&'-') {
for next in chars.by_ref() {
if next == '\n' {
cleaned.push('\n');
break;
}
}
} else if ch == '/' && chars.peek() == Some(&'*') {
chars.next();
while let Some(next) = chars.next() {
if next == '*' && chars.peek() == Some(&'/') {
chars.next();
break;
}
}
} else {
cleaned.push(ch);
}
}
cleaned
.split(';')
.map(|statement| statement.trim().to_string())
.filter(|statement| !statement.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
/// 仓库里的系统库脚本必须始终能解析
const SYSTEM_SCHEMA: &str = include_str!("../../../scripts/init.sql");
/// 仓库自带脚本必须能解析出账号表
#[test]
fn parses_the_repository_init_script() {
let tables = parse(SYSTEM_SCHEMA).expect("scripts/init.sql must parse");
let users = tables
.iter()
.find(|table| table.name == "__chusql_users")
.expect("init.sql must declare the account table");
let names: Vec<&str> = users.columns.iter().map(|column| column.name.as_str()).collect();
assert_eq!(names, ["id", "user", "password_hash", "registered_at", "last_login_at", "revision"]);
assert_eq!(users.columns[0].ty, "int");
assert!(users.columns[0].primary_key && users.columns[0].auto_increment && !users.columns[0].nullable);
assert_eq!(users.columns[1].ty, "varchar(64)");
assert!(users.columns[1].unique);
assert_eq!(users.columns[4].ty, "timestamp");
assert!(users.columns[4].nullable);
assert_eq!(users.indexes, ["user"]);
}
/// 未知语句与表级约束要报错
#[test]
fn rejects_unknown_statements_and_table_level_constraints() {
assert!(parse("DROP TABLE users;").is_err());
assert!(parse("CREATE TABLE t (id int, PRIMARY KEY (id));").is_err());
assert!(parse("CREATE INDEX missing (user);").is_err());
}
/// 注释与空语句要被跳过
#[test]
fn ignores_comments_and_blank_statements() {
let tables = parse("-- note\nCREATE TABLE t (a int NOT NULL);\n\n/* done */\n").unwrap();
assert_eq!(tables.len(), 1);
assert_eq!(tables[0].columns[0].name, "a");
assert!(!tables[0].columns[0].nullable);
}
}
+13
View File
@@ -0,0 +1,13 @@
// 存储层库入口:对外暴露下面这些模块,服务与集成测试都从这里引。
pub mod btree;
pub mod catalog;
pub mod config;
pub mod ffi;
pub mod heap;
pub mod initsql;
pub mod log;
pub mod page;
pub mod protocol;
pub mod runtime;
pub mod wal;
+368
View File
@@ -0,0 +1,368 @@
use std::fmt;
use std::io::{IsTerminal, Write};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::OnceLock;
// 极简日志:等级用原子变量,关掉的等级只花一次原子读。
const RESET: &str = "\u{1b}[0m";
const BOLD_RED: &str = "\u{1b}[1m\u{1b}[31m";
const BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m";
const BOLD: &str = "\u{1b}[1m";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Off,
Error,
Warn,
Info,
Debug,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Channel {
Core,
Request,
}
impl Channel {
/// 来源标签
fn tag(self) -> &'static str {
match self {
Channel::Core => "core",
Channel::Request => "request",
}
}
}
impl Level {
/// 解析等级名
pub fn parse(s: &str) -> Option<Level> {
match s.trim().to_ascii_lowercase().as_str() {
"off" => Some(Level::Off),
"error" => Some(Level::Error),
"warn" | "warning" => Some(Level::Warn),
"info" => Some(Level::Info),
"debug" => Some(Level::Debug),
_ => None,
}
}
/// 等级名
pub fn name(self) -> &'static str {
match self {
Level::Off => "off",
Level::Error => "error",
Level::Warn => "warn",
Level::Info => "info",
Level::Debug => "debug",
}
}
/// 行里的等级标签
fn tag(self) -> &'static str {
match self {
Level::Error => "ERROR",
Level::Warn => "WARN ",
Level::Info => "INFO ",
_ => "DEBUG",
}
}
/// 等级的颜色
fn color(self) -> &'static str {
match self {
Level::Error => BOLD_RED,
Level::Warn => BOLD_ORANGE,
Level::Debug => BOLD,
_ => "",
}
}
}
static LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
static COLOR_STDERR: OnceLock<bool> = OnceLock::new();
static COLOR_STDOUT: OnceLock<bool> = OnceLock::new();
/// 设置全局等级
pub fn set_level(level: Level) {
LEVEL.store(level as u8, Ordering::Relaxed);
}
#[inline]
/// 这个等级要不要输出
pub fn enabled(level: Level) -> bool {
level as u8 <= LEVEL.load(Ordering::Relaxed)
}
/// 启动时调一次
pub fn init() {
enable_vt();
}
/// 写出一行日志,按等级选流向
pub fn write(channel: Channel, level: Level, args: fmt::Arguments) {
let msg = args.to_string();
let to_stderr = streams_to_stderr(level);
let line = render(level, channel, color_enabled(to_stderr), &now(), &msg);
if to_stderr {
let mut out = std::io::stderr().lock();
let _ = writeln!(out, "{}", line);
} else {
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "{}", line);
}
}
/// 这个等级是否写 stderr
fn streams_to_stderr(level: Level) -> bool {
matches!(level, Level::Error | Level::Warn)
}
/// 按目标流是否终端决定是否着色
fn color_enabled(to_stderr: bool) -> bool {
let cached = if to_stderr { &COLOR_STDERR } else { &COLOR_STDOUT };
*cached.get_or_init(|| if to_stderr { std::io::stderr().is_terminal() } else { std::io::stdout().is_terminal() })
}
/// 拼一行(纯函数,可测)
fn render(level: Level, channel: Channel, color: bool, stamp: &Stamp, msg: &str) -> String {
let tag = level.tag();
let paint = level.color();
let level_text = if color && !paint.is_empty() {
format!("{}{}{}", paint, tag, RESET)
} else {
tag.to_string()
};
format!("[{} {}] [{}] {}", level_text, stamp, channel.tag(), msg)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stamp {
pub year: u16,
pub month: u16,
pub day: u16,
pub hour: u16,
pub minute: u16,
pub second: u16,
pub milli: u16,
}
impl fmt::Display for Stamp {
/// 格式化成时间戳文本
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",
self.year, self.month, self.day, self.hour, self.minute, self.second, self.milli
)
}
}
/// 当前本地时间(Windows)
#[cfg(windows)]
fn now() -> Stamp {
#[repr(C)]
struct SystemTime {
year: u16,
month: u16,
day_of_week: u16,
day: u16,
hour: u16,
minute: u16,
second: u16,
milliseconds: u16,
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetLocalTime(out: *mut SystemTime);
}
let mut t = SystemTime {
year: 0,
month: 0,
day_of_week: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
milliseconds: 0,
};
unsafe { GetLocalTime(&mut t) };
Stamp {
year: t.year,
month: t.month,
day: t.day,
hour: t.hour,
minute: t.minute,
second: t.second,
milli: t.milliseconds,
}
}
/// 当前时间(其他平台用 UTC)
#[cfg(not(windows))]
fn now() -> Stamp {
let d = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = d.as_secs();
let (year, month, day) = civil_from_days((secs / 86_400) as i64);
Stamp {
year: year as u16,
month,
day,
hour: ((secs % 86_400) / 3600) as u16,
minute: ((secs % 3_600) / 60) as u16,
second: (secs % 60) as u16,
milli: d.subsec_millis() as u16,
}
}
/// 天数换算成日期
pub fn civil_from_days(days: i64) -> (i64, u16, u16) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = (doy - (153 * mp + 2) / 5 + 1) as u16;
let month = if mp < 10 { mp + 3 } else { mp - 9 } as u16;
(y + i64::from(month <= 2), month, day)
}
/// 打开 Windows 的 VT 处理
#[cfg(windows)]
fn enable_vt() {
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetStdHandle(which: u32) -> *mut core::ffi::c_void;
fn GetConsoleMode(handle: *mut core::ffi::c_void, mode: *mut u32) -> i32;
fn SetConsoleMode(handle: *mut core::ffi::c_void, mode: u32) -> i32;
}
const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
unsafe {
let handle = GetStdHandle(STD_ERROR_HANDLE);
let mut mode = 0u32;
if GetConsoleMode(handle, &mut mode) != 0 {
let _ = SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
/// 其他平台什么都不用做
#[cfg(not(windows))]
fn enable_vt() {}
#[macro_export]
macro_rules! log_debug {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Debug, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Debug, $($arg)*)
};
}
#[macro_export]
macro_rules! log_info {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Info, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Info, $($arg)*)
};
}
#[macro_export]
macro_rules! log_warn {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Warn, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Warn, $($arg)*)
};
}
#[macro_export]
macro_rules! log_error {
(core, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Core, $crate::log::Level::Error, $($arg)*)
};
(request, $($arg:tt)*) => {
$crate::log_line!($crate::log::Channel::Request, $crate::log::Level::Error, $($arg)*)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! log_line {
($chan:expr, $lvl:expr, $($arg:tt)*) => {
if $crate::log::enabled($lvl) {
$crate::log::write($chan, $lvl, format_args!($($arg)*))
}
};
}
#[cfg(test)]
mod tests {
use super::*;
/// 已知日期换算要正确
#[test]
fn civil_from_days_known_dates() {
assert_eq!(civil_from_days(0), (1970, 1, 1));
assert_eq!(civil_from_days(10_957), (2000, 1, 1));
assert_eq!(civil_from_days(11_016), (2000, 2, 29));
assert_eq!(civil_from_days(19_723), (2024, 1, 1));
assert_eq!(civil_from_days(19_782), (2024, 2, 29));
}
/// 行里要有等级、时间与来源
#[test]
fn render_has_time_channel_and_colors() {
let stamp = Stamp {
year: 2026,
month: 9,
day: 24,
hour: 13,
minute: 45,
second: 12,
milli: 130,
};
assert_eq!(
render(Level::Error, Channel::Request, true, &stamp, "boom"),
"[\u{1b}[1m\u{1b}[31mERROR\u{1b}[0m 2026-09-24 13:45:12.130] [request] boom"
);
assert_eq!(
render(Level::Warn, Channel::Core, true, &stamp, "careful"),
"[\u{1b}[1m\u{1b}[38;5;208mWARN \u{1b}[0m 2026-09-24 13:45:12.130] [core] careful"
);
assert_eq!(
render(Level::Debug, Channel::Request, true, &stamp, "detail"),
"[\u{1b}[1mDEBUG\u{1b}[0m 2026-09-24 13:45:12.130] [request] detail"
);
assert_eq!(
render(Level::Info, Channel::Core, true, &stamp, "hello"),
"[INFO 2026-09-24 13:45:12.130] [core] hello"
);
}
/// warn 与 error 走 stderr
#[test]
fn warnings_and_errors_go_to_stderr() {
assert!(streams_to_stderr(Level::Error));
assert!(streams_to_stderr(Level::Warn));
assert!(!streams_to_stderr(Level::Info));
assert!(!streams_to_stderr(Level::Debug));
}
}
+289
View File
@@ -0,0 +1,289 @@
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
// 页管理:定长页的读写,外加一层页缓冲池。
pub type PageId = u64;
pub const DEFAULT_POOL_SIZE: usize = 64;
#[derive(Debug, Clone)]
pub struct Page {
pub id: PageId,
pub data: Vec<u8>,
}
impl Page {
/// 新建一页,全 0
pub fn new(id: PageId, size: usize) -> Self {
Page {
id,
data: vec![0u8; size],
}
}
/// 清零
pub fn zero(&mut self) {
self.data.fill(0);
}
}
#[derive(Debug)]
pub struct PageFile {
file: File,
page_size: usize,
capacity: usize,
cache: HashMap<PageId, Page>,
last_used: HashMap<PageId, u64>,
clock: u64,
dirty: HashSet<PageId>,
hits: u64,
misses: u64,
}
impl PageFile {
/// 打开页文件(默认池容量)
pub fn open<P: AsRef<Path>>(path: P, page_size: usize) -> io::Result<Self> {
Self::with_options(path, page_size, DEFAULT_POOL_SIZE)
}
/// 打开页文件并指定池容量
pub fn with_options<P: AsRef<Path>>(
path: P,
page_size: usize,
capacity: usize,
) -> io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
Ok(PageFile {
file,
page_size,
capacity,
cache: HashMap::new(),
last_used: HashMap::new(),
clock: 0,
dirty: HashSet::new(),
hits: 0,
misses: 0,
})
}
/// 页大小
pub fn page_size(&self) -> usize {
self.page_size
}
/// 池容量(页数)
pub fn capacity(&self) -> usize {
self.capacity
}
/// 当前缓存了多少页
pub fn cached_pages(&self) -> usize {
self.cache.len()
}
/// 命中次数
pub fn hits(&self) -> u64 {
self.hits
}
/// 未命中次数
pub fn misses(&self) -> u64 {
self.misses
}
/// 命中率
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
/// 标记为最近使用
fn touch(&mut self, id: PageId) {
self.clock += 1;
self.last_used.insert(id, self.clock);
}
/// 淘汰一个最冷的页
fn evict_one(&mut self) -> io::Result<bool> {
let victim = self
.cache
.keys()
.min_by_key(|id| self.last_used.get(id).copied().unwrap_or(0))
.copied();
let Some(victim) = victim else {
return Ok(false);
};
if self.dirty.contains(&victim) {
let p = self.cache[&victim].clone();
self.write_raw(&p)?;
self.dirty.remove(&victim);
}
self.cache.remove(&victim);
self.last_used.remove(&victim);
Ok(true)
}
/// 直接写文件
fn write_raw(&mut self, page: &Page) -> io::Result<()> {
if page.data.len() != self.page_size {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"page has {} bytes, this file uses {}",
page.data.len(),
self.page_size
),
));
}
self.file
.seek(SeekFrom::Start(page.id * self.page_size as u64))?;
self.file.write_all(&page.data)
}
/// 直接从文件读
fn read_raw(&mut self, id: PageId) -> io::Result<Page> {
let mut data = vec![0u8; self.page_size];
self.file.seek(SeekFrom::Start(id * self.page_size as u64))?;
self.file.read_exact(&mut data)?;
Ok(Page { id, data })
}
/// 读一页(走缓存)
pub fn read_page(&mut self, id: PageId) -> io::Result<Page> {
self.note_access(id)?;
Ok(self.cache[&id].clone())
}
/// 只读地用一页(走缓存,不整页拷贝)
pub fn with_page<R>(&mut self, id: PageId, f: impl FnOnce(&Page) -> R) -> io::Result<R> {
self.note_access(id)?;
Ok(f(&self.cache[&id]))
}
/// 就地改一页(走缓存,不整页拷贝)
pub fn update_page<R>(&mut self, id: PageId, f: impl FnOnce(&mut Page) -> R) -> io::Result<R> {
self.note_access(id)?;
let r = f(self.cache.get_mut(&id).expect("cached above"));
self.dirty.insert(id);
Ok(r)
}
/// 记一次访问:命中 / 未命中都记数
fn note_access(&mut self, id: PageId) -> io::Result<()> {
if self.cache.contains_key(&id) {
self.hits += 1;
} else {
self.misses += 1;
let p = self.read_raw(id)?;
if self.cache.len() >= self.capacity {
self.evict_one()?;
}
self.cache.insert(id, p);
}
self.touch(id);
Ok(())
}
/// 写一页(走缓存)
pub fn write_page(&mut self, page: &Page) -> io::Result<()> {
if page.data.len() != self.page_size {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"page has {} bytes, this file uses {}",
page.data.len(),
self.page_size
),
));
}
if self.cache.len() >= self.capacity && !self.cache.contains_key(&page.id) {
self.evict_one()?;
}
self.cache.insert(page.id, page.clone());
self.dirty.insert(page.id);
self.touch(page.id);
Ok(())
}
/// 末尾追加一页
pub fn append_page(&mut self) -> io::Result<Page> {
let id = self.num_pages()?;
let page = Page::new(id, self.page_size);
self.write_page(&page)?;
Ok(page)
}
/// 文件里有多少页
pub fn num_pages(&mut self) -> io::Result<PageId> {
let len = self.file.metadata()?.len();
let size = self.page_size as u64;
if len % size != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"file length {} is not a multiple of page size {} (did page.size change?)",
len, size
),
));
}
let on_disk = len / size;
let cached_max = self
.cache
.keys()
.copied()
.max()
.map(|m| m + 1)
.unwrap_or(0);
Ok(on_disk.max(cached_max))
}
/// 把脏页写回磁盘
pub fn flush(&mut self) -> io::Result<()> {
let dirty: Vec<PageId> = self.dirty.iter().copied().collect();
for id in dirty {
if let Some(p) = self.cache.get(&id) {
let p = p.clone();
self.write_raw(&p)?;
}
}
self.file.sync_all()?;
self.dirty.clear();
Ok(())
}
/// flush 之后 fsync
pub fn sync(&mut self) -> io::Result<()> {
self.flush()
}
/// 清空文件
pub fn truncate(&mut self) -> io::Result<()> {
self.cache.clear();
self.last_used.clear();
self.dirty.clear();
self.file.set_len(0)?;
self.file.seek(SeekFrom::Start(0))?;
Ok(())
}
}
impl Drop for PageFile {
/// 析构时把脏页写回
fn drop(&mut self) {
let _ = self.flush();
}
}
+236
View File
@@ -0,0 +1,236 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// 协议:Haskell 与 Rust 之间的请求 / 响应类型。
pub type Row = HashMap<String, serde_json::Value>;
pub const USERS_TABLE: &str = "__chusql_users";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Account {
pub id: i64,
pub user: String,
pub password_hash: String,
pub revision: u64,
#[serde(default)]
pub registered_at: String,
#[serde(default)]
pub last_login_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchemaColumn {
pub name: String,
pub ty: String,
#[serde(default = "default_true")]
pub nullable: bool,
#[serde(default)]
pub default: Option<serde_json::Value>,
#[serde(default)]
pub auto_increment: bool,
#[serde(default)]
pub primary_key: bool,
#[serde(default)]
pub unique: bool,
#[serde(default)]
pub check: Option<String>,
}
impl Default for SchemaColumn {
/// 列定义的默认值
fn default() -> Self {
SchemaColumn {
name: String::new(),
ty: String::new(),
nullable: true,
default: None,
auto_increment: false,
primary_key: false,
unique: false,
check: None,
}
}
}
/// serde 里缺省为真
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ColumnStatWire {
pub name: String,
pub distinct: u64,
#[serde(default)]
pub capped: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IndexWire {
pub column: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum Request {
AccountsList,
AccountCreate { user: String, password_hash: String },
AccountReset { user: String, password_hash: String },
AccountLogin {
user: String,
#[serde(default)]
at: Option<String>,
},
AccountDrop { user: String },
Ping,
Scan {
table: String,
#[serde(default)]
columns: Option<Vec<String>>,
},
Insert {
table: String,
row: Row,
},
InsertBatch {
table: String,
rows: Vec<Row>,
},
DeleteKeys {
table: String,
keys: Vec<i64>,
},
LookupByIndex {
table: String,
#[serde(default = "default_id_column")]
column: String,
key: serde_json::Value,
},
RangeByIndex {
table: String,
column: String,
#[serde(default)]
lo: Option<serde_json::Value>,
#[serde(default = "default_true")]
lo_inclusive: bool,
#[serde(default)]
hi: Option<serde_json::Value>,
#[serde(default = "default_true")]
hi_inclusive: bool,
},
ReplaceAll {
table: String,
rows: Vec<Row>,
},
ListTables,
DescribeTable {
table: String,
},
CreateTable {
table: String,
columns: Vec<SchemaColumn>,
},
DropTable {
table: String,
},
ListCatalog,
CreateIndex {
table: String,
column: String,
},
DropIndex {
table: String,
column: String,
},
DropColumn {
table: String,
column: String,
},
Compact {
table: String,
},
ReplaceSchema {
table: String,
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
},
}
impl Request {
/// 拒绝可能影响文件路径的列名
pub fn valid_columns(&self) -> bool {
let valid = |name: &str| !name.is_empty()
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
match self {
Self::CreateIndex { column, .. } | Self::DropIndex { column, .. }
| Self::DropColumn { column, .. } | Self::LookupByIndex { column, .. }
| Self::RangeByIndex { column, .. } => valid(column),
Self::CreateTable { columns, .. } => columns.iter().all(|c| valid(&c.name)),
Self::ReplaceSchema { columns, .. } => columns.iter().all(|c| valid(&c.name)),
_ => true,
}
}
/// 普通表操作的统一访问边界
pub fn table(&self) -> Option<&str> {
match self {
Self::Scan { table, .. } | Self::Insert { table, .. }
| Self::InsertBatch { table, .. } | Self::DeleteKeys { table, .. }
| Self::LookupByIndex { table, .. } | Self::ReplaceAll { table, .. }
| Self::RangeByIndex { table, .. }
| Self::DescribeTable { table } | Self::CreateTable { table, .. }
| Self::DropTable { table } | Self::CreateIndex { table, .. }
| Self::DropIndex { table, .. } | Self::DropColumn { table, .. }
| Self::Compact { table }
| Self::ReplaceSchema { table, .. } => Some(table),
Self::Ping | Self::ListTables | Self::ListCatalog | Self::AccountsList
| Self::AccountCreate { .. } | Self::AccountReset { .. } | Self::AccountLogin { .. }
| Self::AccountDrop { .. } => None,
}
}
}
/// 是不是保留系统表名
pub fn reserved_table(table: &str) -> bool {
table.to_ascii_lowercase().starts_with("__chusql_")
}
/// 没写 column 时按 `id` 算
fn default_id_column() -> String {
"id".to_string()
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum Response {
Accounts { accounts: Vec<Account> },
Pong,
Rows { rows: Vec<Row> },
Tables { tables: Vec<String> },
Schema {
table: String,
columns: Vec<SchemaColumn>,
#[serde(default)]
row_count: u64,
#[serde(default)]
indexes: Vec<IndexWire>,
#[serde(default)]
stats: Vec<ColumnStatWire>,
},
NoIndex,
Ok,
Error { message: String },
Catalog { schemas: Vec<TableSchemaWire> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchemaWire {
pub table: String,
pub columns: Vec<SchemaColumn>,
#[serde(default)]
pub row_count: u64,
#[serde(default)]
pub indexes: Vec<IndexWire>,
#[serde(default)]
pub stats: Vec<ColumnStatWire>,
}
File diff suppressed because it is too large. Load diff
+296
View File
@@ -0,0 +1,296 @@
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::protocol::{Account, Row, SchemaColumn, USERS_TABLE};
// 预写日志:先记操作并 fsync,再改数据;重启重放残留。
const OP_INSERT: u8 = 1;
const OP_REPLACE_ALL: u8 = 2;
const OP_INSERT_BATCH: u8 = 3;
const OP_DELETE_KEYS: u8 = 4;
const OP_DROP_COLUMN: u8 = 5;
const OP_ACCOUNTS: u8 = 6;
const OP_REPLACE_SCHEMA: u8 = 7;
const OP_HIDE_COLUMN: u8 = 8;
const OP_COMPACT: u8 = 9;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WalOp {
HideColumn { table: String, column: String },
Compact { table: String, rows: Vec<Row> },
Accounts { accounts: Vec<Account> },
Insert {
table: String,
row: Row,
},
InsertBatch {
table: String,
rows: Vec<Row>,
},
DeleteKeys {
table: String,
keys: Vec<i64>,
},
ReplaceAll {
table: String,
rows: Vec<Row>,
},
DropColumn {
table: String,
column: String,
rows: Vec<Row>,
},
ReplaceSchema {
table: String,
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
},
}
#[derive(Debug, Serialize, Deserialize)]
struct DropColumnPayload {
column: String,
rows: Vec<Row>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ReplaceSchemaPayload {
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
}
pub struct Wal {
path: PathBuf,
handle: Mutex<Option<File>>,
}
impl Wal {
/// 绑定 WAL 文件路径
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Wal {
path: path.as_ref().to_path_buf(),
handle: Mutex::new(None),
}
}
/// 日志文件路径
pub fn path(&self) -> &Path {
&self.path
}
/// 拿常驻句柄(没有就开一个)
fn with_handle<R>(&self, f: impl FnOnce(&mut File) -> io::Result<R>) -> io::Result<R> {
let mut slot = self.handle.lock().unwrap();
if slot.is_none() {
*slot = Some(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&self.path)?,
);
}
f(slot.as_mut().expect("just opened"))
}
/// 截断到空并落盘
pub fn clear(&self) -> io::Result<()> {
self.with_handle(|f| {
truncate(f)?;
f.sync_all()
})
}
/// 只截断,落盘交给紧随的 append
pub fn truncate(&self) -> io::Result<()> {
self.with_handle(truncate)
}
/// 追加一条并 fsync
pub fn append(&self, op: &WalOp) -> io::Result<()> {
let bytes = encode(op)?;
self.with_handle(|f| {
f.write_all(&bytes)?;
f.sync_data()
})
}
/// 读当前 WAL;空则 None
pub fn read(&self) -> io::Result<Option<WalOp>> {
if !self.path.exists() {
return Ok(None);
}
let mut f = File::open(&self.path)?;
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
if buf.is_empty() {
return Ok(None);
}
match decode(&buf) {
Ok(opt) => Ok(opt),
Err(e) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("corrupt WAL: {}", e),
)),
}
}
}
/// 清空文件并把读写位置归零
fn truncate(f: &mut File) -> io::Result<()> {
f.set_len(0)?;
f.seek(SeekFrom::Start(0))?;
Ok(())
}
/// 把操作编成字节
fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
let (op_type, table, payload) = match op {
WalOp::HideColumn { table, column } => (OP_HIDE_COLUMN, table.clone(),
serde_json::to_vec(column).map_err(io::Error::other)?),
WalOp::Compact { table, rows } => {
let p = serde_json::to_vec(rows).map_err(io::Error::other)?;
(OP_COMPACT, table.clone(), p)
}
WalOp::Accounts { accounts } => (OP_ACCOUNTS, USERS_TABLE.to_string(),
serde_json::to_vec(accounts).map_err(io::Error::other)?),
WalOp::Insert { table, row } => {
let p = serde_json::to_vec(row).map_err(io::Error::other)?;
(OP_INSERT, table.clone(), p)
}
WalOp::InsertBatch { table, rows } => {
let p = serde_json::to_vec(rows).map_err(io::Error::other)?;
(OP_INSERT_BATCH, table.clone(), p)
}
WalOp::DeleteKeys { table, keys } => {
let p = serde_json::to_vec(keys).map_err(io::Error::other)?;
(OP_DELETE_KEYS, table.clone(), p)
}
WalOp::ReplaceAll { table, rows } => {
let p = serde_json::to_vec(rows).map_err(io::Error::other)?;
(OP_REPLACE_ALL, table.clone(), p)
}
WalOp::DropColumn { table, column, rows } => {
let p = serde_json::to_vec(&DropColumnPayload {
column: column.clone(),
rows: rows.clone(),
})
.map_err(io::Error::other)?;
(OP_DROP_COLUMN, table.clone(), p)
}
WalOp::ReplaceSchema { table, columns, rows } => {
let p = serde_json::to_vec(&ReplaceSchemaPayload {
columns: columns.clone(),
rows: rows.clone(),
})
.map_err(io::Error::other)?;
(OP_REPLACE_SCHEMA, table.clone(), p)
}
};
let table_bytes = table.as_bytes();
let lsn: u64 = 1;
let mut body = Vec::with_capacity(8 + 1 + 2 + table_bytes.len() + payload.len());
body.extend_from_slice(&lsn.to_le_bytes());
body.push(op_type);
body.extend_from_slice(&(table_bytes.len() as u16).to_le_bytes());
body.extend_from_slice(table_bytes);
body.extend_from_slice(&payload);
let mut out = Vec::with_capacity(4 + body.len());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
Ok(out)
}
/// 从字节解回操作
fn decode(buf: &[u8]) -> Result<Option<WalOp>, String> {
if buf.len() < 4 {
return Ok(None);
}
let body_len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
if buf.len() < 4 + body_len {
return Ok(None);
}
let body = &buf[4..4 + body_len];
if body.len() < 8 + 1 + 2 {
return Err("body too short".into());
}
let _lsn = u64::from_le_bytes([
body[0], body[1], body[2], body[3], body[4], body[5], body[6], body[7],
]);
let op_type = body[8];
let table_len = u16::from_le_bytes([body[9], body[10]]) as usize;
if body.len() < 11 + table_len {
return Err("table name truncated".into());
}
let table = String::from_utf8(body[11..11 + table_len].to_vec())
.map_err(|e| format!("bad utf8 table name: {}", e))?;
let payload = &body[11 + table_len..];
let op = match op_type {
OP_HIDE_COLUMN => {
let column = serde_json::from_slice(payload)
.map_err(|e| format!("bad hide_column payload: {e}"))?;
WalOp::HideColumn { table, column }
}
OP_COMPACT => {
let rows: Vec<Row> = serde_json::from_slice(payload)
.map_err(|e| format!("bad compact payload: {e}"))?;
WalOp::Compact { table, rows }
}
OP_ACCOUNTS => {
if table != USERS_TABLE { return Err("invalid account WAL table".into()); }
let accounts = serde_json::from_slice(payload)
.map_err(|e| format!("bad account payload: {e}"))?;
WalOp::Accounts { accounts }
}
OP_INSERT => {
let row: Row =
serde_json::from_slice(payload).map_err(|e| format!("bad insert payload: {}", e))?;
WalOp::Insert { table, row }
}
OP_INSERT_BATCH => {
let rows: Vec<Row> = serde_json::from_slice(payload)
.map_err(|e| format!("bad insert_batch payload: {}", e))?;
WalOp::InsertBatch { table, rows }
}
OP_DELETE_KEYS => {
let keys: Vec<i64> = serde_json::from_slice(payload)
.map_err(|e| format!("bad delete_keys payload: {}", e))?;
WalOp::DeleteKeys { table, keys }
}
OP_REPLACE_ALL => {
let rows: Vec<Row> = serde_json::from_slice(payload)
.map_err(|e| format!("bad replace_all payload: {}", e))?;
WalOp::ReplaceAll { table, rows }
}
OP_DROP_COLUMN => {
let p: DropColumnPayload = serde_json::from_slice(payload)
.map_err(|e| format!("bad drop_column payload: {}", e))?;
WalOp::DropColumn {
table,
column: p.column,
rows: p.rows,
}
}
OP_REPLACE_SCHEMA => {
let p: ReplaceSchemaPayload = serde_json::from_slice(payload)
.map_err(|e| format!("bad replace_schema payload: {}", e))?;
WalOp::ReplaceSchema {
table,
columns: p.columns,
rows: p.rows,
}
}
other => return Err(format!("unknown op type: {}", other)),
};
Ok(Some(op))
}
+257
View File
@@ -0,0 +1,257 @@
use chusql_core_storage::btree::{fits_in_page, DiskBTree, MAX_KEY_BYTES};
use chusql_core_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
use chusql_core_storage::page::DEFAULT_POOL_SIZE;
// B+ 树测试:字节键、重复键、范围扫描、重开与配置校验。
/// 按默认参数打开指定索引文件
fn open(dir: &std::path::Path, name: &str) -> DiskBTree {
DiskBTree::open(dir.join(name), DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap()
}
/// 整数键编码成保序的 8 字节(与堆表一致)
fn key(n: i64) -> [u8; 8] {
(n as u64 ^ (1u64 << 63)).to_be_bytes()
}
/// 空树查不到,遍历为空
#[test]
fn empty_tree_get() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "t.idx");
assert_eq!(t.get(&key(1)).unwrap(), None);
assert_eq!(t.iter_all().unwrap(), vec![]);
}
/// 插 100 个键都能查回
#[test]
fn insert_then_get() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "t.idx");
for i in 0..100 {
t.insert(&key(i), i as u64 * 10).unwrap();
}
for i in 0..100 {
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64 * 10));
}
}
/// 重开文件数据还在
#[test]
fn reopen_persists() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.idx");
{
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
t.insert(&key(i), i as u64).unwrap();
}
}
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64));
}
}
/// 同一个键可以挂多个位置,按值升序返回
#[test]
fn duplicate_keys_keep_every_value() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "dup.idx");
t.insert(b"same", 30).unwrap();
t.insert(b"same", 10).unwrap();
t.insert(b"same", 20).unwrap();
assert_eq!(t.get_all(b"same").unwrap(), vec![10, 20, 30]);
assert_eq!(t.get(b"same").unwrap(), Some(10));
assert!(t.delete(b"same", 10).unwrap());
assert_eq!(t.get_all(b"same").unwrap(), vec![20, 30]);
assert!(!t.delete(b"same", 10).unwrap());
}
/// 倒序插入后遍历仍升序
#[test]
fn iter_all_sorted() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "t.idx");
for i in (0..50).rev() {
t.insert(&key(i), i as u64).unwrap();
}
let all = t.iter_all().unwrap();
assert_eq!(all.len(), 50);
for (i, (k, _)) in all.iter().enumerate() {
assert_eq!(k.as_slice(), key(i as i64).as_slice());
}
}
/// 页大小或 order 不符要报错
#[test]
fn config_mismatch_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.idx");
{
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
t.insert(&key(1), 1).unwrap();
}
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 8, DEFAULT_POOL_SIZE).is_err());
assert!(DiskBTree::open(&path, 8192, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_ok());
}
/// 不是索引文件要报错
#[test]
fn not_an_index_file_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("junk.idx");
std::fs::write(&path, vec![7u8; DEFAULT_PAGE_SIZE]).unwrap();
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
}
/// order 放不进一页要报错
#[test]
fn order_too_large_for_page_errors() {
let dir = tempfile::tempdir().unwrap();
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300, DEFAULT_POOL_SIZE).is_err());
assert!(!fits_in_page(64, DEFAULT_BTREE_ORDER));
}
/// 删掉一个键值之后查不到,别人不受影响
#[test]
fn delete_removes_only_that_key() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "t.idx");
for i in 0..200 {
t.insert(&key(i), i as u64 + 1).unwrap();
}
assert!(t.delete(&key(50), 51).unwrap(), "deleting an existing entry should return true");
assert_eq!(t.get(&key(50)).unwrap(), None);
assert_eq!(t.get(&key(49)).unwrap(), Some(50));
assert_eq!(t.get(&key(51)).unwrap(), Some(52));
assert!(!t.delete(&key(50), 51).unwrap());
let all = t.iter_all().unwrap();
assert_eq!(all.len(), 199);
assert!(all.iter().all(|(k, _)| k.as_slice() != key(50).as_slice()));
}
/// 删过之后树还能继续插
#[test]
fn insert_after_delete_still_works() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "t.idx");
for i in 0..300 {
t.insert(&key(i), i as u64).unwrap();
}
for i in (0..300).step_by(2) {
assert!(t.delete(&key(i), i as u64).unwrap());
}
for i in 1000..1100 {
t.insert(&key(i), i as u64).unwrap();
}
for i in 0..300 {
let want = if i % 2 == 0 { None } else { Some(i as u64) };
assert_eq!(t.get(&key(i)).unwrap(), want, "key {}", i);
}
for i in 1000..1100 {
assert_eq!(t.get(&key(i)).unwrap(), Some(i as u64));
}
}
/// 变长键能插能查,最长键也支持
#[test]
fn variable_length_keys_round_trip() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "var.idx");
t.insert(b"a", 1).unwrap();
t.insert(b"alice", 2).unwrap();
t.insert(&[b'z'; MAX_KEY_BYTES], 3).unwrap();
t.flush().unwrap();
assert_eq!(t.get(b"a").unwrap(), Some(1));
assert_eq!(t.get(b"alice").unwrap(), Some(2));
assert_eq!(t.get(b"bob").unwrap(), None);
assert_eq!(t.get(&[b'z'; MAX_KEY_BYTES]).unwrap(), Some(3));
assert_eq!(t.get(&[b'z'; MAX_KEY_BYTES - 1]).unwrap(), None);
}
/// 超出键长上限直接报错,不写坏节点
#[test]
fn oversized_key_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "long.idx");
let too_long = vec![b'k'; MAX_KEY_BYTES + 1];
assert!(t.insert(&too_long, 1).is_err());
assert_eq!(t.get(&too_long).unwrap(), None);
}
/// 插满一页后会分裂,顺序仍然正确
#[test]
fn splits_keep_keys_sorted() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "split.idx");
let mut expected: Vec<Vec<u8>> = Vec::new();
for i in 0..600 {
let k = format!("key-{:04}", (i * 7919) % 600).into_bytes();
t.insert(&k, i as u64).unwrap();
expected.push(k);
}
expected.sort();
expected.dedup();
let all: Vec<Vec<u8>> = t.iter_all().unwrap().into_iter().map(|(k, _)| k).collect();
assert_eq!(all, expected);
for k in &expected {
assert!(t.get(k).unwrap().is_some(), "missing {:?}", String::from_utf8_lossy(k));
}
}
/// 范围扫描走叶子链,两端都是闭区间
#[test]
fn range_scan_walks_the_leaf_chain() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "range.idx");
for word in ["apple", "banana", "cherry", "date", "elderberry"] {
t.insert(word.as_bytes(), word.len() as u64).unwrap();
}
let mid: Vec<String> = t
.scan(Some(b"banana"), Some(b"date"))
.unwrap()
.into_iter()
.map(|(k, _)| String::from_utf8(k).unwrap())
.collect();
assert_eq!(mid, vec!["banana", "cherry", "date"]);
let from: Vec<String> = t
.scan(Some(b"cherry"), None)
.unwrap()
.into_iter()
.map(|(k, _)| String::from_utf8(k).unwrap())
.collect();
assert_eq!(from, vec!["cherry", "date", "elderberry"]);
let until: Vec<String> = t
.scan(None, Some(b"banana"))
.unwrap()
.into_iter()
.map(|(k, _)| String::from_utf8(k).unwrap())
.collect();
assert_eq!(until, vec!["apple", "banana"]);
assert!(t.scan(Some(b"z"), None).unwrap().is_empty());
}
/// 清空之后还能接着用
#[test]
fn clear_resets_the_tree() {
let dir = tempfile::tempdir().unwrap();
let mut t = open(dir.path(), "clear.idx");
t.insert(b"a", 1).unwrap();
t.insert(b"b", 2).unwrap();
t.clear().unwrap();
assert!(t.iter_all().unwrap().is_empty());
t.insert(b"c", 3).unwrap();
assert_eq!(t.get(b"c").unwrap(), Some(3));
}
+118
View File
@@ -0,0 +1,118 @@
use chusql_core_storage::catalog::Catalog;
use chusql_core_storage::protocol::Row;
use serde_json::json;
// 数据字典测试:补列、去重、未知表。
/// 用键值对构造一行
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
m.insert((*k).to_string(), v.clone());
}
m
}
/// 空文件得到空字典,未知表返回 None
#[test]
fn empty_file_gives_empty_catalog_and_unknown_table_returns_none() { let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
let c = Catalog::load(&path).unwrap();
assert_eq!(c.table_names().len(), 0);
assert!(c.describe("nope").is_none());
}
/// 补列之后能查到 schema
#[test]
fn ensure_then_describe() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1)), ("name", json!("Alice"))]));
c.save(&path).unwrap();
let c2 = Catalog::load(&path).unwrap();
let s = c2.describe("users").unwrap();
assert_eq!(s.columns.len(), 2);
let names: Vec<&str> = s.columns.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"id"));
assert!(names.contains(&"name"));
let id_col = s.columns.iter().find(|c| c.name == "id").unwrap();
assert_eq!(id_col.ty, "int");
let name_col = s.columns.iter().find(|c| c.name == "name").unwrap();
assert_eq!(name_col.ty, "str");
}
/// 再插入不会重复列
#[test]
fn second_insert_does_not_duplicate_columns() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("users", &row(&[("id", json!(1))]));
c.ensure_columns("users", &row(&[("id", json!(2))]));
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
}
/// 删列同时清掉统计与索引
#[test]
fn remove_column_drops_definition_stats_and_index() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
c.ensure_columns("t", &row(&[("id", json!(1)), ("code", json!(5))]));
c.add_index("t", "code").unwrap();
c.rebuild_stats("t", &[row(&[("id", json!(1)), ("code", json!(5))])]);
let removed = c.remove_column("t", "code").unwrap();
assert!(removed, "first remove should hit the column");
let s = c.describe("t").unwrap();
assert_eq!(s.columns.len(), 1);
assert_eq!(s.columns[0].name, "id");
assert!(s.indexes.is_empty(), "index on code should be gone");
assert!(!s.stats.contains_key("code"), "stats for code should be gone");
let again = c.remove_column("t", "code").unwrap();
assert!(!again);
assert!(c.remove_column("nope", "code").is_err());
}
/// 老类型名并到新写法,参数保留,未知类型原样
#[test]
fn normalize_type_maps_legacy_names() {
use chusql_core_storage::catalog::normalize_type;
assert_eq!(normalize_type("integer"), "int");
assert_eq!(normalize_type("TEXT"), "str");
assert_eq!(normalize_type("BOOLEAN"), "bool");
assert_eq!(normalize_type("varchar(20)"), "varchar(20)");
assert_eq!(normalize_type("numeric(8,2)"), "decimal(8,2)");
assert_eq!(normalize_type("real"), "float");
assert_eq!(normalize_type("weird"), "weird");
}
/// 老格式字典(列里没有新字段)能读进来,规范化后写回新写法
#[test]
fn legacy_catalog_loads_and_normalizes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
std::fs::write(
&path,
r#"{"tables":{"legacy":{"columns":[{"name":"id","ty":"integer"},{"name":"name","ty":"text"}]}}}"#,
)
.unwrap();
let mut c = Catalog::load(&path).unwrap();
let s = c.describe("legacy").unwrap();
assert_eq!(s.columns.len(), 2);
assert!(s.columns[0].nullable, "老列默认可空");
assert!(c.normalize_types());
let types: Vec<&str> = c.describe("legacy").unwrap().columns.iter().map(|col| col.ty.as_str()).collect();
assert_eq!(types, vec!["int", "str"]);
assert!(!c.normalize_types(), "第二次没有可改的");
}
+239
View File
@@ -0,0 +1,239 @@
use std::path::PathBuf;
use std::sync::Mutex;
use chusql_core_storage::config::{self, Config, DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE, Origin};
use chusql_core_storage::log::Level;
// 配置测试:默认值、文件覆盖、环境变量彻底无效、非法值报错。
/// set_var 改的是进程环境,用例之间要串行,免得互相污染
static ENV_LOCK: Mutex<()> = Mutex::new(());
/// 拿锁(前面的用例 panic 过也照样拿)
fn env_guard() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// 无文件时全默认
#[test]
fn defaults_without_file() {
let _guard = env_guard();
let loaded = config::resolve(None, None).unwrap();
assert_eq!(loaded.config, Config::default());
assert!(loaded.config_path.is_none());
assert!(loaded.origins.iter().all(|(_, _, o)| *o == Origin::Default));
assert_eq!(loaded.config.page_size, DEFAULT_PAGE_SIZE);
assert_eq!(loaded.config.btree_order, DEFAULT_BTREE_ORDER);
assert_eq!(loaded.config.data_dir, config::default_data_dir());
assert_eq!(loaded.config.log_level, Level::Info);
}
/// 只覆盖文件里写了的项
#[test]
fn file_overrides_only_written_keys() {
let _guard = env_guard();
let path = PathBuf::from("t.toml");
let loaded = config::resolve(Some("[page]\nsize = 8192\n"), Some(path.clone()))
.unwrap();
assert_eq!(loaded.config.page_size, 8192);
assert_eq!(loaded.config.btree_order, DEFAULT_BTREE_ORDER);
assert_eq!(loaded.config.data_dir, config::default_data_dir());
assert_eq!(loaded.config_path, Some(path.clone()));
let page = loaded.origins.iter().find(|(n, _, _)| *n == "page.size").unwrap();
assert_eq!(page.2, Origin::File(path));
let order = loaded.origins.iter().find(|(n, _, _)| *n == "btree.order").unwrap();
assert_eq!(order.2, Origin::Default);
}
/// 五项全写都生效
#[test]
fn file_all_keys() {
let _guard = env_guard();
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[buffer]\npool_size = 128\n[storage]\ndata_dir = \"mydata\"\n[log]\nlevel = \"debug\"\n";
let path = PathBuf::from("t.toml");
let loaded = config::resolve(Some(text), Some(path.clone())).unwrap();
assert_eq!(loaded.config.page_size, 8192);
assert_eq!(loaded.config.btree_order, 32);
assert_eq!(loaded.config.pool_size, 128);
assert_eq!(loaded.config.data_dir, PathBuf::from("mydata"));
assert_eq!(loaded.config.log_level, Level::Debug);
assert!(
loaded
.origins
.iter()
.all(|(_, _, o)| *o == Origin::File(path.clone()))
);
}
/// 设了 CHUSQL_* 环境变量也不生效
#[test]
fn env_vars_are_ignored_entirely() {
let _guard = env_guard();
unsafe {
std::env::set_var("CHUSQL_DATA_DIR", "from-env");
std::env::set_var("CHUSQL_PAGE_SIZE", "8192");
}
let text = "[storage]\ndata_dir = \"from-file\"\n";
let loaded = config::resolve(Some(text), Some(PathBuf::from("t.toml"))).unwrap();
assert_eq!(loaded.config.data_dir, PathBuf::from("from-file"));
assert_eq!(loaded.config.page_size, DEFAULT_PAGE_SIZE);
let dir = loaded
.origins
.iter()
.find(|(n, _, _)| *n == "storage.data_dir")
.unwrap();
assert_eq!(dir.2, Origin::File(PathBuf::from("t.toml")));
let builtin = config::resolve(None, None).unwrap();
assert_eq!(builtin.config.data_dir, config::default_data_dir());
assert_eq!(builtin.config.page_size, DEFAULT_PAGE_SIZE);
unsafe {
std::env::remove_var("CHUSQL_DATA_DIR");
std::env::remove_var("CHUSQL_PAGE_SIZE");
}
}
/// 固定配置路径在系统配置目录的 ChuSQL 下
#[test]
fn default_config_path_follows_system_convention() {
let _guard = env_guard();
let path = config::default_config_path();
assert_eq!(path.file_name().unwrap(), "chusql.toml");
assert_eq!(path.parent().unwrap().file_name().unwrap(), "ChuSQL");
assert!(path.ends_with(format!("ChuSQL{}chusql.toml", std::path::MAIN_SEPARATOR)), "got {}", path.display());
// 本机 APPDATA 存在时必须是 %APPDATA%\ChuSQL\chusql.toml
#[cfg(windows)]
if let Some(appdata) = std::env::var_os("APPDATA") {
assert_eq!(
path,
PathBuf::from(appdata).join("ChuSQL").join("chusql.toml")
);
}
}
/// 默认数据目录落地按系统惯例
#[test]
fn default_data_dir_follows_system_convention() {
let _guard = env_guard();
let dir = config::default_data_dir();
let expected_parent = if cfg!(windows) { "ChuSQL" } else { "chusql" };
assert_eq!(dir.file_name().unwrap(), "data");
assert_eq!(dir.parent().unwrap().file_name().unwrap(), expected_parent);
// 老的内置默认是仓库里的相对路径 ../localdata,现在不能再指回仓库
assert_ne!(dir, PathBuf::from("../localdata"));
#[cfg(windows)]
if let Some(local) = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty()) {
assert_eq!(dir, PathBuf::from(local).join("ChuSQL").join("data"));
} else if let Some(roaming) = std::env::var_os("APPDATA").filter(|v| !v.is_empty()) {
assert_eq!(dir, PathBuf::from(roaming).join("ChuSQL").join("data"));
}
#[cfg(not(windows))]
if let Some(xdg) = std::env::var_os("XDG_DATA_HOME").filter(|v| !v.is_empty()) {
assert_eq!(dir, PathBuf::from(xdg).join("chusql").join("data"));
} else if let Some(home) = std::env::var_os("HOME").filter(|v| !v.is_empty()) {
assert_eq!(
dir,
PathBuf::from(home)
.join(".local")
.join("share")
.join("chusql")
.join("data")
);
}
}
/// 显式给了一个不存在的路径:必须报错,不能静默回退默认
#[test]
fn explicit_missing_path_is_an_error() {
let _guard = env_guard();
let missing = std::env::temp_dir().join("chusql-no-such-config-9f21.toml");
assert!(!missing.is_file());
let err = config::load(Some(missing.to_str().unwrap())).unwrap_err();
assert!(
err.contains("config file not found") || err.contains("cannot read config file"),
"got {}",
err
);
assert!(err.contains("chusql-no-such-config-9f21.toml"), "got {}", err);
}
/// 不给 --config 且无配置文件时用内置默认
#[test]
fn load_without_explicit_path_uses_builtin_defaults() {
let _guard = env_guard();
let default = config::default_config_path();
let loaded = config::load(None).unwrap();
if default.is_file() {
assert_eq!(loaded.config_path, Some(default));
} else {
assert_eq!(loaded.config, Config::default());
assert_eq!(loaded.config.data_dir, config::default_data_dir());
assert!(loaded.origins.iter().all(|(_, _, o)| *o == Origin::Default));
assert!(loaded.config_path.is_none());
}
}
/// [server] 分区归别的层管;本层拼写错误要报错
#[test]
fn server_section_belongs_to_another_layer() {
let _guard = env_guard();
let db_server = "[server]\nhost = \"0.0.0.0\"\nport = 7778\nmax_rows = 10\n";
let loaded = config::resolve(Some(db_server), None).unwrap();
assert_eq!(loaded.config.data_dir, config::default_data_dir());
let typo = "[storage]\ndata_dirs = \"oops\"\n";
assert!(config::resolve(Some(typo), None).is_err());
}
/// 日志等级非法要报错
#[test]
fn rejects_bad_log_level() {
let _guard = env_guard();
let err = config::resolve(Some("[log]\nlevel = \"loud\"\n"), None).unwrap_err();
assert!(err.contains("log.level"), "got {}", err);
}
/// 页大小或 order 非法都要报错
#[test]
fn rejects_bad_numeric_limits() {
let _guard = env_guard();
for text in ["[page]\nsize = 3000\n", "[page]\nsize = 128\n"] {
let err = config::resolve(Some(text), None).unwrap_err();
assert!(err.contains("page.size"), "{}: {}", text, err);
}
let err = config::resolve(Some("[btree]\norder = 2\n"), None).unwrap_err();
assert!(err.contains("btree.order"), "got {}", err);
let err = config::resolve(Some("[btree]\norder = 300\n"), None).unwrap_err();
assert!(err.contains("does not fit"), "got {}", err);
}
/// 未知配置键要报错
#[test]
fn rejects_unknown_key() {
let _guard = env_guard();
let err = config::resolve(Some("[page]\nsizes = 4096\n"), None).unwrap_err();
assert!(err.contains("bad config"), "got {}", err);
}
/// 坏的页面尺寸只能来自文件
#[test]
fn rejects_bad_page_size_from_file() {
let _guard = env_guard();
unsafe { std::env::set_var("CHUSQL_PAGE_SIZE", "big") };
let err = config::resolve(Some("[page]\nsize = 3000\n"), None).unwrap_err();
assert!(err.contains("page.size"), "got {}", err);
unsafe { std::env::remove_var("CHUSQL_PAGE_SIZE") };
}
+306
View File
@@ -0,0 +1,306 @@
use chusql_core_storage::config::DEFAULT_PAGE_SIZE;
use chusql_core_storage::heap::HeapTable;
use chusql_core_storage::page::DEFAULT_POOL_SIZE;
use chusql_core_storage::protocol::Row;
use serde_json::json;
// 堆表测试:插查、跨页、大行、重开、索引重建。
/// 用键值对构造一行
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
m.insert((*k).to_string(), v.clone());
}
m
}
/// 索引文件被清空(强杀或截断)后打开,会按堆数据补齐
#[test]
fn empty_index_file_is_rebuilt_from_rows() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("t.db");
let index = dir.path().join("t.idx");
{
let mut t = HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
for i in 1..=5 {
t.insert_row(&row(&[("id", json!(i))])).unwrap();
}
t.flush().unwrap();
}
std::fs::write(&index, b"").unwrap();
let mut t = HeapTable::open_indexed(&data, &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
assert!(t.get_by_column_key("id", 3).unwrap().is_some(), "空索引文件要从堆数据重建");
assert!(t.get_by_column_key("id", 9).unwrap().is_none());
}
/// 字符串列能真正建索引:按值点查,NULL 不入索引
#[test]
fn string_index_looks_up_by_value() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("s.db");
let index = dir.path().join("s.idx");
let mut t = HeapTable::open(&data, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert_row(&row(&[("id", json!(1)), ("name", json!("alice"))])).unwrap();
t.insert_row(&row(&[("id", json!(2)), ("name", json!("bob"))])).unwrap();
t.insert_row(&row(&[("id", json!(3)), ("name", json!(null))])).unwrap();
t.build_index("name", &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
assert_eq!(t.get_by_string_key("name", "bob").unwrap().unwrap()["id"], json!(2));
assert!(t.get_by_string_key("name", "dave").unwrap().is_none());
assert!(t.get_by_string_key("name", "").unwrap().is_none(), "NULL 不入索引");
}
/// 允许重复值建索引:点查把重复的行都取回来
#[test]
fn duplicate_values_are_indexed_and_all_returned() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("d.db");
let index = dir.path().join("d.idx");
let mut t = HeapTable::open(&data, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert_row(&row(&[("id", json!(1)), ("name", json!("alice"))])).unwrap();
t.insert_row(&row(&[("id", json!(2)), ("name", json!("alice"))])).unwrap();
t.build_index("name", &index, DEFAULT_PAGE_SIZE, 4, DEFAULT_POOL_SIZE).unwrap();
let mut ids: Vec<i64> = t
.get_all_by_string_key("name", "alice")
.unwrap()
.iter()
.filter_map(|r| r["id"].as_i64())
.collect();
ids.sort_unstable();
assert_eq!(ids, vec![1, 2]);
assert!(t.get_all_by_string_key("name", "bob").unwrap().is_empty());
}
/// 500 行跨页顺序不变
#[test]
fn insert_many_rows_cross_pages() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
for i in 0..500 {
t.insert_row(&row(&[("id", json!(i))])).unwrap();
}
let rows = t.scan().unwrap();
assert_eq!(rows.len(), 500);
for (i, r) in rows.iter().enumerate() {
assert_eq!(r["id"], json!(i as i64));
}
}
/// 2KB 大行放得下
#[test]
fn large_row_still_fits() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let big = "x".repeat(2000);
t.insert_row(&row(&[("data", json!(big.clone()))])).unwrap();
let rows = t.scan().unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["data"], json!(big));
}
/// 超页的行要报错
#[test]
fn row_too_large_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let huge = "x".repeat(10_000);
let err = t.insert_row(&row(&[("data", json!(huge))]));
assert!(err.is_err());
}
/// 重开文件数据还在
#[test]
fn reopen_and_scan() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
{
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert_row(&row(&[("id", json!(1))])).unwrap();
t.insert_row(&row(&[("id", json!(2))])).unwrap();
}
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let rows = t.scan().unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["id"], json!(1));
assert_eq!(rows[1]["id"], json!(2));
}
/// 整表改写后索引重建
#[test]
fn replace_all_rebuilds_index() {
let dir = tempfile::tempdir().unwrap();
let mut t = HeapTable::open_indexed(
dir.path().join("t.db"),
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
t.insert_row(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
.unwrap();
t.insert_row(&row(&[("id", json!(2)), ("name", json!("Bob"))]))
.unwrap();
assert!(t.get_by_key(1).unwrap().is_some());
t.replace_all(&[
row(&[("id", json!(7)), ("name", json!("Zoe"))]),
row(&[("name", json!("NoId"))]),
])
.unwrap();
assert!(t.get_by_key(1).unwrap().is_none(), "old key must be invalidated");
assert!(t.get_by_key(2).unwrap().is_none());
let got = t.get_by_key(7).unwrap().expect("new row should be indexed by id");
assert_eq!(got["name"], json!("Zoe"));
assert_eq!(t.scan().unwrap().len(), 2);
}
/// 行级删除:行没了、索引条目没了、顺序不乱
#[test]
fn delete_by_keys_removes_rows() {
let dir = tempfile::tempdir().unwrap();
let mut t = HeapTable::open_indexed(
dir.path().join("t.db"),
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in 1..=5 {
t.insert_row(&row(&[("id", json!(i)), ("name", json!(format!("u{}", i)))]))
.unwrap();
}
let deleted = t.delete_by_keys(&[2, 4, 99]).unwrap();
assert_eq!(deleted.len(), 2, "only 2 and 4 were actually deleted");
let ids: Vec<i64> = t
.scan()
.unwrap()
.iter()
.map(|r| r["id"].as_i64().unwrap())
.collect();
assert_eq!(ids, vec![1, 3, 5], "remaining rows must keep their order");
assert!(t.get_by_key(2).unwrap().is_none(), "index entry should be deleted too");
assert!(t.get_by_key(4).unwrap().is_none());
assert!(t.get_by_key(3).unwrap().is_some());
t.insert_row(&row(&[("id", json!(2)), ("name", json!("u2b"))]))
.unwrap();
assert_eq!(t.get_by_key(2).unwrap().unwrap()["name"], json!("u2b"));
}
/// 批量插入
#[test]
fn insert_rows_batch() {
let dir = tempfile::tempdir().unwrap();
let mut t = HeapTable::open_indexed(
dir.path().join("t.db"),
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
let rows: Vec<Row> = (1..=20)
.map(|i| row(&[("id", json!(i)), ("name", json!(format!("u{}", i)))]))
.collect();
t.insert_rows(&rows).unwrap();
assert_eq!(t.scan().unwrap().len(), 20);
assert_eq!(t.get_by_key(20).unwrap().unwrap()["name"], json!("u20"));
let dup = vec![
row(&[("id", json!(100))]),
row(&[("id", json!(100))]),
];
assert!(t.insert_rows(&dup).is_err());
}
/// 第二列索引:建完插入/删除都跟着维护,值可重复
#[test]
fn secondary_index_is_maintained() {
let dir = tempfile::tempdir().unwrap();
let mut t = HeapTable::open_indexed(
dir.path().join("t.db"),
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
t.insert_rows(&[
row(&[("id", json!(1)), ("code", json!(7001))]),
row(&[("id", json!(2)), ("code", json!(7002))]),
])
.unwrap();
assert!(!t.has_index("code"));
t.build_index(
"code",
&dir.path().join("t.code.idx"),
DEFAULT_PAGE_SIZE,
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
assert!(t.has_index("code"));
assert_eq!(t.get_by_column_key("code", 7002).unwrap().unwrap()["id"], json!(2));
t.insert_row(&row(&[("id", json!(3)), ("code", json!(7003))]))
.unwrap();
assert_eq!(t.get_by_column_key("code", 7003).unwrap().unwrap()["id"], json!(3));
t.delete_by_keys(&[2]).unwrap();
assert!(t.get_by_column_key("code", 7002).unwrap().is_none());
t.insert_row(&row(&[("id", json!(4)), ("code", json!(7001))])).unwrap();
let mut dup_ids: Vec<i64> = t
.get_all_by_column_key("code", 7001)
.unwrap()
.iter()
.filter_map(|r| r["id"].as_i64())
.collect();
dup_ids.sort_unstable();
assert_eq!(dup_ids, vec![1, 4], "duplicate index values keep both rows");
let mut t2 = HeapTable::open(dir.path().join("d.db"), DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t2.insert_rows(&[
row(&[("id", json!(1)), ("age", json!(30))]),
row(&[("id", json!(2)), ("age", json!(30))]),
])
.unwrap();
t2.build_index(
"age",
&dir.path().join("d.age.idx"),
DEFAULT_PAGE_SIZE,
chusql_core_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
assert_eq!(
t2.get_all_by_column_key("age", 30).unwrap().len(),
2,
"duplicate values are all indexed"
);
}
+144
View File
@@ -0,0 +1,144 @@
use chusql_core_storage::config::DEFAULT_PAGE_SIZE;
use chusql_core_storage::page::{Page, PageFile};
// 页文件测试:读写、追加、覆写、页大小校验与页缓存。
/// 写一页再读回一致
#[test]
fn write_then_read_page() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut p = Page::new(0, DEFAULT_PAGE_SIZE);
p.data[0] = 42;
p.data[DEFAULT_PAGE_SIZE - 1] = 7;
pf.write_page(&p).unwrap();
let read = pf.read_page(0).unwrap();
assert_eq!(read.data[0], 42);
assert_eq!(read.data[DEFAULT_PAGE_SIZE - 1], 7);
assert_eq!(read.data.len(), DEFAULT_PAGE_SIZE);
}
/// 空文件读第 0 页报错
#[test]
fn read_out_of_range_returns_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
assert!(pf.read_page(0).is_err());
}
/// 追加让页数加一
#[test]
fn append_grows_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
assert_eq!(pf.num_pages().unwrap(), 0);
pf.append_page().unwrap();
assert_eq!(pf.num_pages().unwrap(), 1);
pf.append_page().unwrap();
pf.append_page().unwrap();
assert_eq!(pf.num_pages().unwrap(), 3);
}
/// 覆写同页读到新内容
#[test]
fn overwrite_page() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut a = Page::new(0, DEFAULT_PAGE_SIZE);
a.data[0] = 1;
pf.write_page(&a).unwrap();
let mut b = Page::new(0, DEFAULT_PAGE_SIZE);
b.data[0] = 99;
pf.write_page(&b).unwrap();
assert_eq!(pf.read_page(0).unwrap().data[0], 99);
assert_eq!(pf.num_pages().unwrap(), 1);
}
/// 512 字节页可用
#[test]
fn custom_page_size() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut pf = PageFile::open(&path, 512).unwrap();
pf.write_page(&Page::new(0, 512)).unwrap();
pf.flush().unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 512);
}
/// 长度不是整数倍要报错
#[test]
fn mismatched_file_length_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("odd.db");
std::fs::write(&path, vec![0u8; 100]).unwrap();
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
assert!(pf.num_pages().is_err());
}
/// 页大小不符拒绝写入
#[test]
fn write_wrong_sized_page_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
let mut pf = PageFile::open(&path, 512).unwrap();
assert!(pf.write_page(&Page::new(0, 1024)).is_err());
}
/// 第二次读命中缓存
#[test]
fn cache_hit_avoids_second_read() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
{
let mut pf = PageFile::open(&path, 4096).unwrap();
pf.write_page(&Page::new(0, 4096)).unwrap();
pf.flush().unwrap();
}
let mut pf = PageFile::open(&path, 4096).unwrap();
let _ = pf.read_page(0).unwrap();
let _ = pf.read_page(0).unwrap();
let _ = pf.read_page(0).unwrap();
assert_eq!(pf.hits(), 2);
assert_eq!(pf.misses(), 1);
assert!(pf.hit_rate() > 0.6);
}
/// 退出时脏页写回
#[test]
fn dirty_page_flushed_on_drop() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
{
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut p = Page::new(0, DEFAULT_PAGE_SIZE);
p.data[0] = 42;
pf.write_page(&p).unwrap();
}
let mut pf2 = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let p = pf2.read_page(0).unwrap();
assert_eq!(p.data[0], 42);
}
File diff suppressed because it is too large. Load diff
+101
View File
@@ -0,0 +1,101 @@
use chusql_core_storage::protocol::Row;
use chusql_core_storage::wal::{Wal, WalOp};
use serde_json::json;
// WAL 测试:追加、读取、清空、截断尾巴。
/// 用键值对构造一行
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
m.insert((*k).to_string(), v.clone());
}
m
}
/// 三种操作写完都能原样读回
#[test]
fn append_and_read_round_trips() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
let ops = [
WalOp::Insert {
table: "users".into(),
row: row(&[("id", json!(1)), ("name", json!("Alice"))]),
},
WalOp::ReplaceAll {
table: "users".into(),
rows: vec![
row(&[("id", json!(1)), ("name", json!("Alice"))]),
row(&[("id", json!(2)), ("name", json!("Bob"))]),
],
},
WalOp::DropColumn {
table: "users".into(),
column: "age".into(),
rows: vec![
row(&[("id", json!(1)), ("name", json!("Alice"))]),
row(&[("id", json!(2)), ("name", json!("Bob"))]),
],
},
];
for op in ops {
wal.clear().unwrap();
wal.append(&op).unwrap();
assert_eq!(wal.read().unwrap().unwrap(), op);
}
}
/// 文件不存在或为空都返回 None
#[test]
fn read_without_content_returns_none() {
let dir = tempfile::tempdir().unwrap();
let missing = Wal::new(dir.path().join("missing.log"));
assert!(missing.read().unwrap().is_none());
let path = dir.path().join("wal.log");
std::fs::write(&path, b"").unwrap();
let empty = Wal::new(&path);
assert!(empty.read().unwrap().is_none());
}
/// 清空后读不到
#[test]
fn clear_truncates() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(1))]),
})
.unwrap();
assert!(wal.read().unwrap().is_some());
wal.clear().unwrap();
assert!(wal.read().unwrap().is_none());
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
/// 尾巴截断被忽略
#[test]
fn truncated_tail_is_ignored() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
wal.append(&WalOp::Insert {
table: "users".into(),
row: row(&[("id", json!(1)), ("name", json!("Alice"))]),
})
.unwrap();
let full = std::fs::read(&path).unwrap();
std::fs::write(&path, &full[..full.len() - 5]).unwrap();
assert!(wal.read().unwrap().is_none());
}