大改,整理项目结构,完成rust ffi调用,写好tcp接口
This commit is contained in:
1 parent
df6d348995
commit
9758439b82
118 files changed
+8103
-6321
No files matched your search
@@ -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)
|
||||
@@ -0,0 +1,8 @@
|
||||
resolver: lts-24.59
|
||||
|
||||
extra-lib-dirs:
|
||||
- ../storage/target/release
|
||||
|
||||
packages:
|
||||
- .
|
||||
- ../model
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
resolver: lts-24.59
|
||||
|
||||
packages:
|
||||
- .
|
||||
@@ -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
|
||||
Generated
+294
@@ -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"
|
||||
@@ -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 不加样式
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
})
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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(), "第二次没有可改的");
|
||||
}
|
||||
@@ -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") };
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user