事务模型

This commit is contained in:
LIm1tZz7 committed 2026-10-03 16:49:23 +08:00
1 parent 274b7fcdcc
commit 045ccc2dc0
20 files changed
+1519 -294

No files matched your search

@@ -134,6 +134,9 @@ runStatementUncheckedM _ q@CreateDatabase{} = runStatementM q
runStatementUncheckedM _ q@DropDatabase{} = runStatementM q
runStatementUncheckedM _ q@UseDatabase{} = runStatementM q
runStatementUncheckedM _ ShowDatabases = runStatementM ShowDatabases
runStatementUncheckedM _ BeginTransaction = pure (Left "BEGIN is executed by the session")
runStatementUncheckedM _ CommitTransaction = pure (Left "COMMIT is executed by the session")
runStatementUncheckedM _ RollbackTransaction = pure (Left "ROLLBACK is executed by the session")
-- | 只有需要读全表才能判断约束时才扫表
existingRows :: (MonadStorage m) => String -> Table -> m (Either String [Row])
@@ -51,6 +51,8 @@ errorRules =
, ErrorRule "storage" StorageError "query_error"
, ErrorRule "storage library call failed" StorageError "query_error"
, ErrorRule "unsupported protocol" ProtocolError "query_error"
, ErrorRule "executed by the session" ProtocolError "query_error"
, ErrorRule "transaction" ProtocolError "query_error"
]
-- | 命中的第一条规则
@@ -38,16 +38,16 @@ scopeAt place scope c
| "ambiguous column:" `isInfixOf` err -> Left err
| otherwise -> columnAt place (scopeOuter scope) c
-- | 收集 FROM 能提供的列,含外层作用域
-- | 收集 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))
(key, t) <- resolveTable db tbl
Right (prefixColumns (Just (deriveQualifier mAlias key)) (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)
(key, t) <- resolveTable db tbl
let env = lcols ++ prefixColumns (Just (deriveQualifier mAlias key)) (tableCols t)
checkBool db "ON" (Scope env outer) cond
Right env
@@ -433,6 +433,10 @@ checkResolvedWith db outer q = case q of
DropDatabase name -> checkDatabaseName name
UseDatabase name -> checkDatabaseName name
ShowDatabases -> Right ()
-- | 事务控制由会话层执行,语义层不做列检查
BeginTransaction -> Right ()
CommitTransaction -> Right ()
RollbackTransaction -> Right ()
where
tableCols' = map fst . tableCols
@@ -650,21 +654,21 @@ resolveStatement db q = case q of
Select{} -> resolveSelect db q
SelectExpr{} -> resolveSelect db q
Update tbl assigns mWhere -> do
t <- lookupTable db tbl
(key, t) <- resolveTable 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)
pure (Update key assigns' w)
Delete tbl mWhere -> do
t <- lookupTable db tbl
(key, t) <- resolveTable db tbl
w <- mapM (resolveExpr db (tableCols t) [] (nameIn (tableCols t) "WHERE")) mWhere
pure (Delete tbl w)
pure (Delete key w)
Insert tbl cols rows -> do
t <- lookupTable db tbl
(key, t) <- resolveTable db tbl
let env = tableCols t
rows' <- mapM (mapM (resolveExpr db env [] (nameIn env "INSERT"))) rows
pure (Insert tbl cols rows')
pure (Insert key cols rows')
other -> Right other
-- | 查一列的物理名
@@ -685,9 +689,9 @@ 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
FromTable Nothing tbl -> unqualify (Just (deriveQualifier Nothing tbl)) c
_ -> c
-- | 查一列并换算成物理名
name place c = physical . fst <$> scopedColumnAt place env outer c
@@ -707,18 +711,23 @@ resolveQuery db outer q items = do
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
FromTable mAlias tbl -> do
(key, _) <- resolveTable db tbl
Right (FromTable mAlias key)
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 里的子查询也能引用外层列
-- | 连接两侧取表名末段作别名;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 _ (FromTable alias tbl) = do
(key, _) <- resolveTable db tbl
Right (FromTable (Just (deriveQualifier alias key)) key)
resolveFrom db outer fromC@(FromJoin kind left alias tbl cond) = do
env <- checkFrom db outer fromC
l <- resolveFrom db outer left
(key, _) <- resolveTable db tbl
c <- resolveExpr db env outer (fmap fst . columnAt "ON" env) cond
pure (FromJoin kind l (Just (fromMaybe tbl alias)) tbl c)
pure (FromJoin kind l (Just (deriveQualifier alias key)) key c)
@@ -3,6 +3,10 @@
module ChuSQL.Core.Engine.Storage.IPC (
IPCStorage (..),
Env (..),
envForDatabase,
defaultEnv,
runIPCStorage,
Request (..),
Response (..),
Account (..),
@@ -41,21 +45,38 @@ import System.IO.Unsafe (unsafePerformIO)
-- IPC 存储实现:把 MonadStorage 的操作变成发给存储库的 JSON 请求。
newtype IPCStorage a = IPCStorage
{ runIPCStorage :: IO a
-- | 一条会话的存储环境:当前库名随会话走,不共享
data Env = Env
{ envDatabase :: Maybe String
}
newtype IPCStorage a = IPCStorage
{ runIPCStorageIn :: Env -> IO a
}
-- | 按库名造一个会话环境
envForDatabase :: Maybe String -> Env
envForDatabase = Env
-- | 进程默认环境:读全局库名,给测试与 CLI 用
defaultEnv :: IO Env
defaultEnv = Env <$> readMVar databaseRef
-- | 用进程默认环境跑(兼容既有调用点)
runIPCStorage :: IPCStorage a -> IO a
runIPCStorage action = defaultEnv >>= runIPCStorageIn action
instance Functor IPCStorage where
fmap f (IPCStorage m) = IPCStorage (fmap f m)
fmap f (IPCStorage m) = IPCStorage (fmap f . m)
instance Applicative IPCStorage where
pure = IPCStorage . pure
IPCStorage mf <*> IPCStorage ma = IPCStorage (mf <*> ma)
pure = IPCStorage . const . pure
IPCStorage mf <*> IPCStorage ma = IPCStorage $ \env -> mf env <*> ma env
instance Monad IPCStorage where
IPCStorage m >>= k = IPCStorage $ do
a <- m
runIPCStorage (k a)
IPCStorage m >>= k = IPCStorage $ \env -> do
a <- m env
runIPCStorageIn (k a) env
-- | 一条到存储库的链路:管请求与关闭
data StorageLink = StorageLink
@@ -147,10 +168,9 @@ 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)
ask :: Env -> String -> Request -> (Response -> Maybe a) -> IO (Either String a)
ask env what req recognize = do
resp <- sendRequest (stampDatabase (envDatabase env) req)
pure $ case resp of
RespError e -> Left e
other -> case recognize other of
@@ -163,99 +183,100 @@ okOnly RespOk = Just ()
okOnly _ = Nothing
-- | 发 Scan
doScan :: String -> IO (Either String [Row])
doScan t = ask "scan" (ReqScan t) $ \resp -> case resp of
doScan :: Env -> String -> IO (Either String [Row])
doScan env t = ask env "scan" (ReqScan t) $ \resp -> case resp of
RespRows rows -> Just rows
_ -> Nothing
-- | 发带投影的 Scan
doScanColumns :: String -> [String] -> IO (Either String [Row])
doScanColumns t cols = ask "scan" (ReqScanColumns t cols) $ \resp -> case resp of
doScanColumns :: Env -> String -> [String] -> IO (Either String [Row])
doScanColumns env t cols = ask env "scan" (ReqScanColumns t cols) $ \resp -> case resp of
RespRows rows -> Just [[(c, v) | c <- cols, Just v <- [lookup c row]] | row <- rows]
_ -> Nothing
-- | 发分片 Scan:只取第 shard 片
doScanShard :: String -> Maybe [String] -> Int -> Int -> IO (Either String [Row])
doScanShard t cols shard shards = ask "scan_shard" (ReqScanShard t cols shard shards) $ \resp -> case resp of
doScanShard :: Env -> String -> Maybe [String] -> Int -> Int -> IO (Either String [Row])
doScanShard env t cols shard shards = ask env "scan_shard" (ReqScanShard t cols shard shards) $ \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
doInsert :: Env -> String -> Row -> IO (Either String ())
doInsert env t r = ask env "insert" (ReqInsert t r) okOnly
-- | 发 InsertBatch:一批行一次请求
doInsertMany :: String -> [Row] -> IO (Either String ())
doInsertMany t rs = ask "insert_batch" (ReqInsertBatch t rs) okOnly
doInsertMany :: Env -> String -> [Row] -> IO (Either String ())
doInsertMany env t rs = ask env "insert_batch" (ReqInsertBatch t rs) okOnly
-- | 发 DeleteKeys
doDeleteKeys :: String -> [Int] -> IO (Either String ())
doDeleteKeys t ks = ask "delete_keys" (ReqDeleteKeys t ks) okOnly
doDeleteKeys :: Env -> String -> [Int] -> IO (Either String ())
doDeleteKeys env t ks = ask env "delete_keys" (ReqDeleteKeys t ks) okOnly
-- | 发 ReplaceAll
doReplaceAll :: String -> [Row] -> IO (Either String ())
doReplaceAll t rs = ask "replace_all" (ReqReplaceAll t rs) okOnly
doReplaceAll :: Env -> String -> [Row] -> IO (Either String ())
doReplaceAll env t rs = ask env "replace_all" (ReqReplaceAll t rs) okOnly
-- | 发 ListTables
doListTables :: IO (Either String [String])
doListTables = ask "list_tables" ReqListTables $ \resp -> case resp of
doListTables :: Env -> IO (Either String [String])
doListTables env = ask env "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
doListCatalog :: Env -> IO (Either String [TableInfo])
doListCatalog env = ask env "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
doLookupByColumn :: Env -> String -> String -> Value -> IO (Either String IndexResult)
doLookupByColumn env t c k = ask env "lookup_by_index" (ReqLookupByColumn t c k) $ \resp -> case resp of
RespRows rows -> Just (IndexRows rows)
RespNoIndex -> Just NoIndex
_ -> Nothing
-- | 范围扫描:没有可用索引就回 Nothing,让上层退回扫描
doScanRange ::
Env ->
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
doScanRange env t c lo hi = ask env "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
doDescribeTable :: Env -> String -> IO (Either String TableInfo)
doDescribeTable env t = ask env "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
doCreateIndex :: Env -> String -> String -> IO (Either String ())
doCreateIndex env t c = ask env "create_index" (ReqCreateIndex t c) okOnly
-- | 去掉某一列的索引
doDropIndex :: String -> String -> IO (Either String ())
doDropIndex t c = ask "drop_index" (ReqDropIndex t c) okOnly
doDropIndex :: Env -> String -> String -> IO (Either String ())
doDropIndex env t c = ask env "drop_index" (ReqDropIndex t c) okOnly
-- | 删一列(列定义、这一列上的索引、每行里的那一格一起没)
doDropColumn :: String -> String -> IO (Either String ())
doDropColumn t c = ask "drop_column" (ReqDropColumn t c) okOnly
doDropColumn :: Env -> String -> String -> IO (Either String ())
doDropColumn env t c = ask env "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
doReplaceSchema :: Env -> String -> [SchemaColumn] -> [Row] -> IO (Either String ())
doReplaceSchema env t cols rs = ask env "replace_schema" (ReqReplaceSchema t cols rs) okOnly
-- | 发 CreateTable
doCreateTable :: String -> [SchemaColumn] -> IO (Either String ())
doCreateTable t cols = ask "create_table" (ReqCreateTable t cols) okOnly
doCreateTable :: Env -> String -> [SchemaColumn] -> IO (Either String ())
doCreateTable env t cols = ask env "create_table" (ReqCreateTable t cols) okOnly
-- | 发 DropTable
doDropTable :: String -> IO (Either String ())
doDropTable t = ask "drop_table" (ReqDropTable t) okOnly
doDropTable :: Env -> String -> IO (Either String ())
doDropTable env t = ask env "drop_table" (ReqDropTable t) okOnly
-- | 线上 schema 转本地列
schemaToColumns :: [SchemaColumn] -> [(String, Column)]
@@ -292,92 +313,91 @@ toWire (n, c) =
instance MonadStorage IPCStorage where
-- \| 建一个库
createDatabase name = IPCStorage (ask "create_database" (ReqDatabase "create_database" name) okOnly)
createDatabase name = IPCStorage $ \env -> ask env "create_database" (ReqDatabase "create_database" name) okOnly
-- \| 删一个库
dropDatabase name = IPCStorage (ask "drop_database" (ReqDatabase "drop_database" name) okOnly)
dropDatabase name = IPCStorage $ \env -> ask env "drop_database" (ReqDatabase "drop_database" name) okOnly
-- \| 切当前库
useDatabase name = IPCStorage (ask "use_database" (ReqDatabase "use_database" name) okOnly)
useDatabase name = IPCStorage $ \env -> ask env "use_database" (ReqDatabase "use_database" name) okOnly
-- \| 列出现有库
listDatabases = IPCStorage $ ask "list_databases" (ReqDatabase "list_databases" "") $ \resp -> case resp of
listDatabases = IPCStorage $ \env -> ask env "list_databases" (ReqDatabase "list_databases" "") $ \resp -> case resp of
RespTables names -> Just names
_ -> Nothing
-- \| 发 Scan
scan t = IPCStorage (doScan t)
scan t = IPCStorage (\env -> doScan env t)
-- \| 只取指定列
scanColumns t cols = IPCStorage (doScanColumns t cols)
scanColumns t cols = IPCStorage (\env -> doScanColumns env t cols)
-- \| 并行分片数:按 RTS 能力数,封顶在 parallelShardLimit
parallelShards = IPCStorage $ do
parallelShards = IPCStorage $ \_ -> do
caps <- getNumCapabilities
pure (max 1 (min parallelShardLimit caps))
-- \| 分片并行扫,再按分片序拼接
scanShards width t cols
| width <= 1 = IPCStorage (maybe (doScan t) (doScanColumns t) cols)
| otherwise = IPCStorage $ do
| width <= 1 = IPCStorage (\env -> maybe (doScan env t) (doScanColumns env t) cols)
| otherwise = IPCStorage $ \env -> do
pool <- workerPool width
parts <- poolRun pool [doScanShard t cols shard width | shard <- [0 .. width - 1]]
parts <- poolRun pool [doScanShard env t cols shard width | shard <- [0 .. width - 1]]
pure (fmap concat (sequence parts))
-- \| 发 Insert(索引由存储层自己维护)
insert t r = IPCStorage (doInsert t r)
insert t r = IPCStorage (\env -> doInsert env t r)
-- \| 一批行一次请求:N 行只落一次盘
insertMany t rs = IPCStorage (doInsertMany t rs)
insertMany t rs = IPCStorage (\env -> doInsertMany env t rs)
-- \| 按 id 批量删行
deleteKeys t ks = IPCStorage (doDeleteKeys t ks)
deleteKeys t ks = IPCStorage (\env -> doDeleteKeys env t ks)
-- \| 发 ReplaceAll
replaceAll t rs = IPCStorage (doReplaceAll t rs)
replaceAll t rs = IPCStorage (\env -> doReplaceAll env t rs)
-- \| 按某一列的索引取一行(没有索引就回 NoIndex)
lookupByColumn t c k = IPCStorage (doLookupByColumn t c k)
lookupByColumn t c k = IPCStorage (\env -> doLookupByColumn env t c k)
-- \| 范围扫描(没有索引就回 Nothing)
scanRange t c lo hi = IPCStorage (doScanRange t c lo hi)
scanRange t c lo hi = IPCStorage (\env -> doScanRange env t c lo hi)
-- \| 发 CreateTable
createTable name cols = IPCStorage (doCreateTable name (map toWire cols))
createTable name cols = IPCStorage (\env -> doCreateTable env name (map toWire cols))
-- \| ALTER:整表换列定义与全部行
replaceSchema name cols rows = IPCStorage (doReplaceSchema name (map toWire cols) rows)
replaceSchema name cols rows = IPCStorage (\env -> doReplaceSchema env name (map toWire cols) rows)
-- \| 发 DropTable
dropTable name = IPCStorage (doDropTable name)
dropTable name = IPCStorage (\env -> doDropTable env name)
-- \| 建索引
createIndex t c = IPCStorage (doCreateIndex t c)
createIndex t c = IPCStorage (\env -> doCreateIndex env t c)
-- \| 删索引
dropIndex t c = IPCStorage (doDropIndex t c)
dropIndex t c = IPCStorage (\env -> doDropIndex env t c)
-- \| 删一列
dropColumn t c = IPCStorage (doDropColumn t c)
dropColumn t c = IPCStorage (\env -> doDropColumn env t c)
-- \| 列表 + 逐表扫描拼库
snapshot = IPCStorage $ do
result <- doListCatalog
snapshot = IPCStorage $ \env -> do
result <- doListCatalog env
case result of
Left _ -> pure []
Right xs -> mapM loadEntry xs
Right xs -> mapM (loadEntry env) xs
where
loadEntry info = do
rows <- doScan (tiTable info)
loadEntry env info = do
rows <- doScan env (tiTable info)
pure
( tiTable info
, Table (tiTable info) (schemaToColumns (tiColumns info)) (either (const []) id rows) (Just (metaOf info))
)
-- \| 只问数据字典要结构,设了当前库只留它的表
schema = IPCStorage $ do
current <- getDatabaseName
result <- ask "all_catalogs" ReqAllCatalog $ \resp -> case resp of
schema = IPCStorage $ \env -> do
result <- ask env "all_catalogs" ReqAllCatalog $ \resp -> case resp of
RespCatalog infos -> Just infos
_ -> Nothing
pure $ case result of
Left _ -> []
Right xs -> concatMap (visibleTables current) xs
Right xs -> concatMap (visibleTables (envDatabase env)) xs
-- | 线上表信息转引擎侧统计
metaOf :: TableInfo -> TableMeta
@@ -53,6 +53,12 @@ data Statement
| DropDatabase String
| UseDatabase String
| ShowDatabases
-- | 开事务:BEGIN / START TRANSACTION
| BeginTransaction
-- | 提交事务
| CommitTransaction
-- | 回滚事务
| RollbackTransaction
deriving (Show, Eq)
-- | 连接类型:内连接与左外连接
@@ -613,6 +613,14 @@ updateStatement = do
mWhere <- optional (keyword "where" *> expr)
return (Update tbl assigns mWhere)
-- | 事务控制语句
transactionStatement :: Parser Statement
transactionStatement =
try (BeginTransaction <$ (keyword "start" *> keyword "transaction"))
<|> (BeginTransaction <$ (keyword "begin" *> optional (keyword "transaction")))
<|> (CommitTransaction <$ (keyword "commit" *> optional (keyword "transaction")))
<|> (RollbackTransaction <$ (keyword "rollback" *> optional (keyword "transaction")))
-- | 解析总入口
parseStatement :: String -> Either String Statement
parseStatement input =
@@ -622,7 +630,8 @@ parseStatement input =
where
-- | 按关键字分派到各语句解析器
statementP =
selectStatement
transactionStatement
<|> selectStatement
<|> try (CreateDatabase <$> (keyword "create" *> keyword "database" *> identifier))
<|> try (DropDatabase <$> (keyword "drop" *> keyword "database" *> identifier))
<|> (UseDatabase <$> (keyword "use" *> identifier))
+75 -2
View File
@@ -15,7 +15,7 @@ import ChuSQL.Core.Engine.Parallel (poolRun, workerPool)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Semantic (prepare)
import ChuSQL.Core.Engine.Storage (IndexResult (..), MonadStorage (..))
import ChuSQL.Core.Engine.Storage.IPC (IPCStorage (runIPCStorage), closeConnection, doListTables, getDatabaseName, localStorageLink, setDatabaseName, setStorageLink)
import ChuSQL.Core.Engine.Storage.IPC (IPCStorage (..), closeConnection, defaultEnv, doListTables, envForDatabase, getDatabaseName, localStorageLink, runIPCStorage, setDatabaseName, setStorageLink)
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser
import Control.Concurrent (threadDelay)
@@ -113,6 +113,40 @@ main = hspec $ do
rowsOf (parseStatement "SELECT sales.users.name FROM sales.users WHERE sales.users.id = 1" >>= runStatement db)
`shouldBe` Right [[("name", VStr "Alice")]]
it "derives the column prefix when the session schema is scoped to one database" $ do
let scoped = [("users", users)]
rowsOf (parseStatement "SELECT sales.users.name FROM sales.users WHERE sales.users.id = 1" >>= runStatement scoped)
`shouldBe` Right [[("name", VStr "Alice")]]
rowsOf (parseStatement "SELECT users.name FROM users WHERE users.id = 1" >>= runStatement scoped)
`shouldBe` Right [[("name", VStr "Alice")]]
it "derives the database prefix for a bare table name" $ do
let catalog = [("sales.users", users)]
rowsOf (parseStatement "SELECT name FROM users WHERE users.id = 1" >>= runStatement catalog)
`shouldBe` Right [[("name", VStr "Alice")]]
rowsOf (parseStatement "SELECT users.name FROM sales.users WHERE users.id = 1" >>= runStatement catalog)
`shouldBe` Right [[("name", VStr "Alice")]]
it "keeps derived prefixes short in joins" $ do
let catalog = [("sales.users", users), ("sales.orders", orders)]
fmap
(map (map snd))
(parseStatement "SELECT users.name, orders.product FROM sales.users JOIN sales.orders ON users.id = orders.user_id" >>= rowsOf . runStatement catalog)
`shouldBe` Right [[VStr "Alice", VStr "Book"], [VStr "Alice", VStr "Cup"], [VStr "Bob", VStr "Pen"]]
it "writes through the derived table key" $ do
let scoped = [("users", users)]
case parseStatement "UPDATE sales.users SET age = 26 WHERE id = 1" >>= runStatement scoped of
Left err -> expectationFailure err
Right (db', _) ->
rowsOf (runStatement db' (makeSelect ["age"] "users" (Just (Eq (Col "id") (LitInt 1)))))
`shouldBe` Right [[("age", VInt 26)]]
it "rejects an ambiguous derived table name" $ do
let catalog = [("sales.users", users), ("other.users", users)]
(parseStatement "SELECT name FROM users" >>= rowsOf . runStatement catalog)
`shouldSatisfy` either (isInfixOf "ambiguous table") (const False)
it "parses database lifecycle statements" $ do
map (fmap show . parseStatement) ["CREATE DATABASE sales", "DROP DATABASE sales", "USE sales", "SHOW DATABASES"]
`shouldBe` map Right ["CreateDatabase \"sales\"", "DropDatabase \"sales\"", "UseDatabase \"sales\"", "ShowDatabases"]
@@ -242,6 +276,33 @@ main = hspec $ do
parseStatement "SELECT name FROM"
`shouldSatisfy` isLeft
describe "ChuSQL.Core.Engine (事务)" $ do
it "parses transaction control statements" $ do
parseStatement "BEGIN" `shouldBe` Right BeginTransaction
parseStatement "BEGIN TRANSACTION" `shouldBe` Right BeginTransaction
parseStatement "START TRANSACTION" `shouldBe` Right BeginTransaction
parseStatement "begin transaction" `shouldBe` Right BeginTransaction
parseStatement "COMMIT" `shouldBe` Right CommitTransaction
parseStatement "COMMIT TRANSACTION" `shouldBe` Right CommitTransaction
parseStatement "ROLLBACK" `shouldBe` Right RollbackTransaction
parseStatement "rollback transaction" `shouldBe` Right RollbackTransaction
it "parses the bare words only as whole words" $ do
parseStatement "BEGINNER" `shouldSatisfy` isLeft
parseStatement "COMMITTED" `shouldSatisfy` isLeft
it "reports transaction control as session work" $ do
mapM_
( \sql ->
(runStatement [("users", users)] =<< parseStatement sql)
`shouldSatisfy` either (isInfixOf "executed by the session") (const False)
)
["BEGIN", "COMMIT", "ROLLBACK"]
it "puts transaction errors under the protocol category" $ do
errorCode "BEGIN is executed by the session" `shouldBe` "query_error"
errorCategory "BEGIN is executed by the session" `shouldBe` ProtocolError
describe "ChuSQL.Core.Engine.Syntax.Parser (JOIN)" $ do
it "parses a simple JOIN without aliases" $ do
parseStatement "SELECT name FROM users JOIN orders ON id = user_id"
@@ -1613,7 +1674,19 @@ main = hspec $ do
withTestSession $ \srv -> withServerEnv srv $ do
runIPCStorage (insert "t1" [("id", VInt 1)]) `shouldReturn` Right ()
runIPCStorage (insert "t2" [("id", VInt 2)]) `shouldReturn` Right ()
doListTables `shouldReturn` Right ["t1", "t2"]
env <- defaultEnv
doListTables env `shouldReturn` Right ["t1", "t2"]
it "two sessions keep their own database" $ do
withTestSession $ \_ -> do
let alpha = envForDatabase (Just "alpha")
beta = envForDatabase (Just "beta")
runIPCStorageIn (createDatabase "alpha") alpha `shouldReturn` Right ()
runIPCStorageIn (createDatabase "beta") beta `shouldReturn` Right ()
runIPCStorageIn (insert "iso" [("id", VInt 1)]) alpha `shouldReturn` Right ()
runIPCStorageIn (insert "iso" [("id", VInt 2)]) beta `shouldReturn` Right ()
runIPCStorageIn (scan "iso") alpha `shouldReturn` Right [[("id", VInt 1)]]
runIPCStorageIn (scan "iso") beta `shouldReturn` Right [[("id", VInt 2)]]
it "rows survive a restart on the same data directory" $ do
withTestSession $ \srv -> do
+31 -2
View File
@@ -15,6 +15,9 @@ module ChuSQL.Core.Model
, colNames
, colType
, lookupTable
, resolveTable
, lastTablePart
, deriveQualifier
, allColumns
, qualify
, unqualify
@@ -470,6 +473,32 @@ 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)
-- | 取表名引用的最后一段
lastTablePart :: String -> String
lastTablePart = reverse . takeWhile (/= '.') . reverse
-- | 推导列前缀:有别名用别名,否则取表名末段
deriveQualifier :: Maybe String -> String -> String
deriveQualifier (Just a) _ = a
deriveQualifier Nothing t = lastTablePart t
-- | 按名查表并返回 schema 键:精确、前缀、唯一后缀
resolveTable :: Database -> String -> Either String (String, Table)
resolveTable db name = case lookup name db of
Just table -> Right (name, table)
Nothing
| '.' `elem` name, Just table <- lookup bare db -> Right (bare, table)
| [(key, table)] <- suffixed -> Right (key, table)
| otherwise -> Left (miss suffixed)
where
-- | 引用里去掉库前缀的那一段
bare = lastTablePart name
-- | schema 里以这个表名收尾的键:`sales.users` 能推出裸表名引用
suffixed = [(key, table) | (key, table) <- db, ('.' : name) `isSuffixOf` key]
-- | 一个候选都没有就是查无此表,多于一个就是有歧义
miss [] = "unknown table: " ++ name
miss _ = "ambiguous table: " ++ name
-- | 通配列名
allColumns :: String
allColumns = "*"
@@ -491,5 +520,5 @@ resolveColumn name env = case [(k, v) | (k, v) <- env, matches k] of
[] -> Left ("unknown column: " ++ name)
_ -> Left ("ambiguous column: " ++ name)
where
-- | 裸名或带后缀的名称
matches k = k == name || ('.' : name) `isSuffixOf` k
-- | 裸名、补充了库前缀的写法,或 schema 键自带库前缀的写法
matches k = k == name || ('.' : name) `isSuffixOf` k || ('.' : k) `isSuffixOf` name
@@ -3,6 +3,7 @@
module ChuSQL.Core.Protocol (
SchemaColumn (..),
Request (..),
TxnOp (..),
Response (..),
TableInfo (..),
Account (..),
@@ -96,6 +97,35 @@ data Request
| ReqDropColumn String String
| ReqReplaceSchema String [SchemaColumn] [Row]
| ReqListCatalog
| ReqApplyTransaction [TxnOp]
-- | 事务提交里的一条写操作
data TxnOp
= TxnUpsert String [Row]
| TxnDelete String [Int]
| TxnReplace String [Row]
deriving (Show, Eq)
-- | 事务操作编码成 JSON
instance ToJSON TxnOp where
toJSON (TxnUpsert t rs) =
object
[ "op" .= ("upsert" :: T.Text)
, "table" .= t
, "rows" .= map rowToJSON rs
]
toJSON (TxnDelete t ks) =
object
[ "op" .= ("delete" :: T.Text)
, "table" .= t
, "ids" .= ks
]
toJSON (TxnReplace t rs) =
object
[ "op" .= ("replace" :: T.Text)
, "table" .= t
, "rows" .= map rowToJSON rs
]
-- | 请求编码成 JSON
instance ToJSON Request where
@@ -201,6 +231,11 @@ instance ToJSON Request where
, "rows" .= map rowToJSON rs
]
toJSON ReqListCatalog = object ["method" .= ("list_catalog" :: T.Text)]
toJSON (ReqApplyTransaction ops) =
object
[ "method" .= ("apply_transaction" :: T.Text)
, "ops" .= ops
]
-- | 一张表的梗概:列、行数、索引与列统计
data TableInfo = TableInfo
+23 -2
View File
@@ -171,6 +171,22 @@ pub enum Request {
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
},
/// 事务提交:把一批写操作当成一个请求原子应用
ApplyTransaction {
ops: Vec<StorageOp>,
},
}
/// 事务提交里的一条写操作
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum StorageOp {
/// 追加写入的行(提交端已先删掉要覆盖的行)
Upsert { table: String, rows: Vec<Row> },
/// 按行 id 删除
Delete { table: String, ids: Vec<i64> },
/// 整表替换(没有整数 id 的表只能这么提交)
Replace { table: String, rows: Vec<Row> },
}
impl Request {
@@ -202,7 +218,8 @@ impl Request {
| Self::ReplaceSchema { table, .. } => Some(table),
Self::Ping | Self::ListTables | Self::ListCatalog | Self::AccountsList
| Self::AccountCreate { .. } | Self::AccountReset { .. } | Self::AccountLogin { .. }
| Self::AccountDrop { .. } | Self::BootstrapSystem { .. } | Self::SystemStatus => None,
| Self::AccountDrop { .. } | Self::BootstrapSystem { .. } | Self::SystemStatus
| Self::ApplyTransaction { .. } => None,
}
}
}
@@ -237,7 +254,11 @@ pub enum Response {
NoIndex,
Ok,
/// 系统目录状态
System { initialized: bool },
System {
initialized: bool,
#[serde(default)]
last_lsn: u64,
},
Error { message: String },
Catalog { schemas: Vec<TableSchemaWire> },
}
+233 -59
View File
@@ -8,9 +8,10 @@ use crate::config::{self, Config, Loaded};
use crate::heap::{scalar_int, HeapTable};
use crate::log;
use crate::protocol::{
reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, SchemaColumn, TableSchemaWire, USERS_TABLE,
reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, SchemaColumn, StorageOp,
TableSchemaWire, USERS_TABLE,
};
use crate::wal::{Wal, WalOp};
use crate::wal::{committed_ops, Wal, WalOp};
use crate::{log_debug, log_error, log_info, log_warn};
// 进程内存储运行时:Haskell 侧经 ffi.rs 调进来。
@@ -196,6 +197,7 @@ impl Server {
/// 把一条 WAL 操作落到磁盘
fn apply_op(&self, op: &WalOp) -> std::io::Result<()> {
match op {
WalOp::Commit => Ok(()),
WalOp::HideColumn { table, column } => {
let mut c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
c.remove_column(table, column)?;
@@ -311,52 +313,119 @@ impl Server {
if self.recovery_required.load(Ordering::Acquire) {
return Err(std::io::Error::other("storage recovery required"));
}
{
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
let validate = |table: &str, rows: &[Row]| -> std::io::Result<()> {
let Some(schema) = c.describe(table) else { return Ok(()); };
if let Some(name) = rows.iter().flat_map(|r| r.keys()).find(|k| schema.dropped_columns.contains(*k)) {
return Err(std::io::Error::other(format!("unknown column: {name}")));
}
Ok(())
};
match op {
WalOp::Insert { table, row } => validate(table, std::slice::from_ref(row))?,
WalOp::InsertBatch { table, rows } | WalOp::ReplaceAll { table, rows } => validate(table, rows)?,
WalOp::HideColumn { table, column }
if !c.describe(table).is_some_and(|s| s.columns.iter().any(|col| &col.name == column)) =>
{
return Err(std::io::Error::other(format!("unknown column: {column}")));
}
_ => {}
self.validate_op(op)?;
self.group_via_wal(std::slice::from_ref(op))
}
/// 校验一条操作:命中被删的列就拒绝
fn validate_op(&self, op: &WalOp) -> std::io::Result<()> {
let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
let validate = |table: &str, rows: &[Row]| -> std::io::Result<()> {
let Some(schema) = c.describe(table) else { return Ok(()); };
if let Some(name) = rows.iter().flat_map(|r| r.keys()).find(|k| schema.dropped_columns.contains(*k)) {
return Err(std::io::Error::other(format!("unknown column: {name}")));
}
Ok(())
};
match op {
WalOp::Insert { table, row } => validate(table, std::slice::from_ref(row))?,
WalOp::InsertBatch { table, rows } | WalOp::ReplaceAll { table, rows } => validate(table, rows)?,
WalOp::HideColumn { table, column }
if !c.describe(table).is_some_and(|s| s.columns.iter().any(|col| &col.name == column)) =>
{
return Err(std::io::Error::other(format!("unknown column: {column}")));
}
_ => {}
}
Ok(())
}
/// 事务提交:一组写操作加一个提交标记,一次落盘
///
/// 组里每条都过 `validate_op`,跟单条写一致;调用方 `dispatch` 已经持有
/// `gate.write`,因此整批对外原子可见。崩在提交标记之前,这组记录在恢复
/// 时整组丢弃;崩在标记之后,逐条按「是否已生效」补写。
fn apply_transaction(&self, ops: &[StorageOp]) -> std::io::Result<()> {
let _guard = self.write_lock.lock().unwrap();
if self.recovery_required.load(Ordering::Acquire) {
return Err(std::io::Error::other("storage recovery required"));
}
let mut wal_ops = Vec::with_capacity(ops.len());
for op in ops {
let table = match op {
StorageOp::Upsert { table, .. }
| StorageOp::Delete { table, .. }
| StorageOp::Replace { table, .. } => table,
};
if reserved_table(table) {
return Err(std::io::Error::other("reserved system table"));
}
if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(std::io::Error::other("invalid table name"));
}
let wal_op = match op {
StorageOp::Upsert { table, rows } => WalOp::InsertBatch {
table: table.clone(),
rows: rows.clone(),
},
StorageOp::Delete { table, ids } => WalOp::DeleteKeys {
table: table.clone(),
keys: ids.clone(),
},
StorageOp::Replace { table, rows } => WalOp::ReplaceAll {
table: table.clone(),
rows: rows.clone(),
},
};
self.validate_op(&wal_op)?;
wal_ops.push(wal_op);
}
self.group_via_wal(&wal_ops)
}
/// 一组操作加提交标记落盘,成功后清空日志
fn group_via_wal(&self, ops: &[WalOp]) -> std::io::Result<()> {
std::fs::create_dir_all(&self.cfg.data_dir)?;
let wal = &self.wal;
wal.truncate()?;
wal.append(op)?;
let result = self.apply_op(op);
if result.is_err() && matches!(op, WalOp::HideColumn { .. }) {
self.recovery_required.store(true, Ordering::Release);
return result;
let commit_lsn = wal.append_group(ops)?;
for op in ops {
let result = self.apply_op(op);
if result.is_err() && matches!(op, WalOp::HideColumn { .. }) {
self.recovery_required.store(true, Ordering::Release);
return result;
}
result?;
}
if result.is_ok() {
for op in ops {
self.flush_op_tables(op)?;
}
self.flush_all_tables()?;
wal.clear()?;
result
log_debug!(core, "WAL committed group at LSN {}", commit_lsn);
Ok(())
}
/// 把打开的表都 fsync 一遍
fn flush_all_tables(&self) -> std::io::Result<()> {
let handles: Vec<Arc<Mutex<HeapTable>>> = self
.tables
.lock()
.map_err(|_| std::io::Error::other("table lock poisoned"))?
.values()
.cloned()
.collect();
for handle in handles {
handle.lock().map_err(|_| std::io::Error::other("heap lock poisoned"))?.flush()?;
}
Ok(())
}
/// 把涉及的表写回磁盘
fn flush_op_tables(&self, op: &WalOp) -> std::io::Result<()> {
let table = match op {
WalOp::HideColumn { .. } => return Ok(()),
WalOp::Commit => return Ok(()),
WalOp::Accounts { .. } => USERS_TABLE,
WalOp::Insert { table, .. }
| WalOp::InsertBatch { table, .. }
@@ -369,21 +438,97 @@ impl Server {
self.with_table(table, |t| t.flush())
}
/// 启动时重放残留 WAL
/// 重放残留 WAL:已提交组重放,未提交组丢弃
fn recover_wal(&self) -> std::io::Result<()> {
std::fs::create_dir_all(&self.cfg.data_dir)?;
let wal = &self.wal;
match wal.read()? {
None => Ok(()),
Some(op) => {
log_warn!(core, "replaying incomplete WAL entry");
self.apply_op(&op)?;
self.flush_op_tables(&op)?;
wal.clear()?;
log_info!(core, "WAL replay complete");
Ok(())
let scan = wal.read_all()?;
if scan.records.is_empty() {
if scan.good_len > 0 {
wal.truncate_to(scan.good_len)?;
}
return Ok(());
}
let records = committed_ops(&scan.records);
if records.is_empty() {
log_warn!(core, "discarding uncommitted WAL records");
wal.clear()?;
return Ok(());
}
let max_lsn = scan
.records
.iter()
.rev()
.find(|(_, op)| *op == WalOp::Commit)
.map(|(lsn, _)| *lsn)
.or_else(|| records.last().map(|(lsn, _)| *lsn))
.unwrap_or(0);
log_warn!(core, "replaying {} committed WAL records", records.len());
for (_, op) in &records {
self.replay_op(op)?;
self.flush_op_tables(op)?;
}
self.flush_all_tables()?;
wal.write_checkpoint(max_lsn)?;
wal.clear()?;
log_info!(core, "WAL replay complete");
Ok(())
}
/// 重放一条操作:数据已经生效的不再重复写
fn replay_op(&self, op: &WalOp) -> std::io::Result<()> {
if self.op_already_applied(op)? {
log_debug!(core, "WAL record already applied, skipping");
return Ok(());
}
self.apply_op(op)
}
/// 这条操作对应的数据是不是已经落盘了
fn op_already_applied(&self, op: &WalOp) -> std::io::Result<bool> {
match op {
WalOp::Insert { table, row } => self.applied_rows(table, std::slice::from_ref(row)),
WalOp::InsertBatch { table, rows } => self.applied_rows(table, rows),
WalOp::ReplaceAll { table, rows } => {
if self.catalog.lock().unwrap().describe(table).is_none() {
return Ok(false);
}
let stored = self.with_table(table, |t| t.scan())?;
Ok(stored == *rows)
}
WalOp::DeleteKeys { table, keys } => {
if keys.is_empty() {
return Ok(true);
}
if self.catalog.lock().unwrap().describe(table).is_none() {
return Ok(false);
}
let rows = self.with_table(table, |t| t.scan())?;
let present = rows.iter().any(|row| {
row.get("id")
.and_then(scalar_int)
.is_some_and(|id| keys.contains(&id))
});
Ok(!present)
}
WalOp::HideColumn { table, column } => {
let c = self.catalog.lock().unwrap();
Ok(c.describe(table).is_some_and(|s| !s.columns.iter().any(|col| &col.name == column)))
}
_ => Ok(false),
}
}
/// 这些行是不是都已经在表里
fn applied_rows(&self, table: &str, rows: &[Row]) -> std::io::Result<bool> {
if rows.is_empty() {
return Ok(true);
}
if self.catalog.lock().unwrap().describe(table).is_none() {
return Ok(false);
}
let stored = self.with_table(table, |t| t.scan())?;
Ok(rows.iter().all(|row| stored.iter().any(|one| one == row)))
}
/// 启动时重建列统计
@@ -607,6 +752,11 @@ impl Server {
.unwrap_or(false)
}
/// 最近一次落盘的检查点 LSN
fn last_checkpoint(&self) -> u64 {
self.wal.read_checkpoint().ok().flatten().unwrap_or(0)
}
/// 读账号表;表没登记或数据文件缺失都算错
fn load_accounts(&self) -> std::io::Result<Vec<Account>> {
use std::io::Error;
@@ -774,16 +924,29 @@ impl Server {
}
Request::BootstrapSystem { user, password_hash } => {
match self.bootstrap_system(user.as_deref(), password_hash.as_deref()) {
Ok(_) => Response::System { initialized: true },
Ok(_) => Response::System { initialized: true, last_lsn: self.last_checkpoint() },
Err(e) => Response::Error { message: e.to_string() },
}
}
Request::SystemStatus => Response::System { initialized: self.system_initialized() },
Request::SystemStatus => Response::System { initialized: self.system_initialized(), last_lsn: self.last_checkpoint() },
Request::Ping => {
log_debug!(request, "ping");
Response::Pong
}
Request::ApplyTransaction { ops } => {
log_debug!(request, "apply_transaction ops={}", ops.len());
match self.apply_transaction(&ops) {
Ok(()) => Response::Ok,
Err(e) => {
log_warn!(request, "apply_transaction: {}", e);
Response::Error {
message: format!("apply_transaction: {e}"),
}
}
}
}
Request::Scan { table, columns } => {
log_debug!(request, "scan table={}", table);
@@ -1347,8 +1510,8 @@ fn reserved_database(name: &str) -> bool {
/// 每个库拥有独立的目录、字典、句柄和 WAL。
struct Databases {
root: Config,
system: Server,
named: Mutex<HashMap<String, Server>>,
system: Arc<Server>,
named: Mutex<HashMap<String, Arc<Server>>>,
}
impl Databases {
@@ -1356,7 +1519,7 @@ impl Databases {
fn new(cfg: Config) -> std::io::Result<Self> {
// 系统库按需创建:目录、字典与系统表缺失时自动补齐。
// 其余库只在 data/databases/<name>/ 下已存在时才装载,启动不会凭空建库。
let system = Self::open(&cfg, SYSTEM_DATABASE)?;
let system = Arc::new(Self::open(&cfg, SYSTEM_DATABASE)?);
let root = cfg.data_dir.join("databases");
std::fs::create_dir_all(&root)?;
let canonical_root = std::fs::canonicalize(&root)?;
@@ -1379,7 +1542,7 @@ impl Databases {
server.rebuild_stats()?;
server.migrate_data()?;
server.compact_tables()?;
named.insert(name, server);
named.insert(name, Arc::new(server));
}
}
Ok(Self { root: cfg, system, named: Mutex::new(named) })
@@ -1413,9 +1576,13 @@ impl Databases {
let mut named = self.named.lock().map_err(|_| Error::other("database lock poisoned"))?;
match method.as_str() {
"all_catalogs" => {
let servers: Vec<(String, Arc<Server>)> =
std::iter::once((SYSTEM_DATABASE.to_string(), Arc::clone(&self.system)))
.chain(named.iter().map(|(name, server)| (name.clone(), Arc::clone(server))))
.collect();
drop(named);
let mut schemas = Vec::new();
for (database, server) in std::iter::once((SYSTEM_DATABASE, &self.system))
.chain(named.iter().map(|(name, server)| (name.as_str(), server))) {
for (database, server) in servers {
match server.handle_request(Request::ListCatalog) {
Response::Catalog { schemas: entries } => {
for mut entry in entries {
@@ -1442,23 +1609,30 @@ impl Databases {
std::fs::create_dir(&cfg.data_dir)?;
let server = Server::new(cfg)?;
server.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?.save(server.catalog_path())?;
named.insert(name, server);
named.insert(name, Arc::new(server));
return Ok(Response::Ok);
}
"drop_database" => {
if name == SYSTEM_DATABASE { return Err(Error::other("cannot drop the system database")); }
let server = named.get(&name).ok_or_else(|| Error::other("unknown database"))?;
let holder = named.remove(&name).ok_or_else(|| Error::other("unknown database"))?;
let server = match Arc::try_unwrap(holder) {
Ok(server) => server,
Err(holder) => {
named.insert(name, holder);
return Err(Error::other("database is in use"));
}
};
let root = std::fs::canonicalize(self.root.data_dir.join("databases"))?;
let target = std::fs::canonicalize(&server.cfg.data_dir)?;
if target.parent() != Some(root.as_path()) || !target.join("catalog.json").is_file() {
return Err(Error::other("invalid database directory"));
}
let config = server.cfg.clone();
drop(named.remove(&name));
drop(server);
let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(Error::other)?.as_nanos();
let tombstone = root.join(format!(".dropped-{name}-{stamp}"));
if let Err(error) = std::fs::rename(&target, &tombstone) {
named.insert(name, Server::new(config)?);
named.insert(name, Arc::new(Server::new(config)?));
return Err(error);
}
std::fs::remove_dir_all(&tombstone).map_err(|e| Error::other(format!("database dropped; directory cleanup failed: {e}")))?;
@@ -1482,10 +1656,10 @@ impl Databases {
| Request::AccountReset { .. } | Request::AccountLogin { .. }
| Request::AccountDrop { .. } | Request::BootstrapSystem { .. }
| Request::SystemStatus | Request::Ping);
let server = if global { &self.system }
else if selected.is_empty() { return Err(Error::other("no database selected")); }
else if selected == SYSTEM_DATABASE { &self.system }
else { named.get(&selected).ok_or_else(|| Error::other("unknown database"))? };
if !global && selected.is_empty() { return Err(Error::other("no database selected")); }
let server = if global || selected == SYSTEM_DATABASE { Arc::clone(&self.system) }
else { Arc::clone(named.get(&selected).ok_or_else(|| Error::other("unknown database"))?) };
drop(named);
Ok(server.handle_request(request))
}
}
+148 -28
View File
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::protocol::{Account, Row, SchemaColumn, USERS_TABLE};
// 预写日志:先记操作并 fsync,再改数据;重启重放残留。
// 预写日志:先记操作并 fsync,再改数据;重启按 LSN 重放已提交的组。
const OP_INSERT: u8 = 1;
const OP_REPLACE_ALL: u8 = 2;
@@ -18,6 +18,7 @@ const OP_ACCOUNTS: u8 = 6;
const OP_REPLACE_SCHEMA: u8 = 7;
const OP_HIDE_COLUMN: u8 = 8;
const OP_COMPACT: u8 = 9;
const OP_COMMIT: u8 = 10;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WalOp {
@@ -50,6 +51,8 @@ pub enum WalOp {
columns: Vec<SchemaColumn>,
rows: Vec<Row>,
},
/// 一组写操作的提交标记;没有标记的组算未提交
Commit,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -67,6 +70,13 @@ struct ReplaceSchemaPayload {
pub struct Wal {
path: PathBuf,
handle: Mutex<Option<File>>,
next_lsn: Mutex<u64>,
}
/// 一次扫描的结果:记录与完好字节长度
pub struct WalScan {
pub records: Vec<(u64, WalOp)>,
pub good_len: usize,
}
impl Wal {
@@ -75,6 +85,7 @@ impl Wal {
Wal {
path: path.as_ref().to_path_buf(),
handle: Mutex::new(None),
next_lsn: Mutex::new(0),
}
}
@@ -83,6 +94,11 @@ impl Wal {
&self.path
}
/// 检查点文件路径
pub fn checkpoint_path(&self) -> PathBuf {
self.path.with_extension("checkpoint")
}
/// 拿常驻句柄(没有就开一个)
fn with_handle<R>(&self, f: impl FnOnce(&mut File) -> io::Result<R>) -> io::Result<R> {
let mut slot = self.handle.lock().unwrap();
@@ -99,6 +115,15 @@ impl Wal {
f(slot.as_mut().expect("just opened"))
}
/// 把一段记录写到文件尾
fn write_at_end(&self, bytes: &[u8]) -> io::Result<()> {
self.with_handle(|f| {
f.seek(SeekFrom::End(0))?;
f.write_all(bytes)?;
f.sync_data()
})
}
/// 截断到空并落盘
pub fn clear(&self) -> io::Result<()> {
self.with_handle(|f| {
@@ -112,34 +137,136 @@ impl Wal {
self.with_handle(truncate)
}
/// 追加一条并 fsync
pub fn append(&self, op: &WalOp) -> io::Result<()> {
let bytes = encode(op)?;
/// 截到指定长度(去掉撕裂的尾巴)
pub fn truncate_to(&self, len: usize) -> io::Result<()> {
self.with_handle(|f| {
f.write_all(&bytes)?;
f.set_len(len as u64)?;
f.seek(SeekFrom::Start(0))?;
f.sync_data()
})
}
/// 读当前 WAL;空则 None
/// 追加一条并 fsync
pub fn append(&self, op: &WalOp) -> io::Result<()> {
let bytes = self.encode_next(op)?;
self.write_at_end(&bytes)
}
/// 追加一组操作与提交标记,返回提交 LSN
pub fn append_group(&self, ops: &[WalOp]) -> io::Result<u64> {
let mut frames = Vec::new();
for op in ops {
frames.extend_from_slice(&self.encode_next(op)?);
}
let commit_lsn = self.take_lsn()?;
frames.extend_from_slice(&encode(&WalOp::Commit, commit_lsn)?);
self.write_at_end(&frames)?;
Ok(commit_lsn)
}
/// 读当前 WAL 的第一条操作;空则 None
pub fn read(&self) -> io::Result<Option<WalOp>> {
let scan = self.read_all()?;
Ok(scan.records.into_iter().find_map(|(_, op)| {
if op == WalOp::Commit {
None
} else {
Some(op)
}
}))
}
/// 读全部记录,撕裂或损坏的尾巴按长度截断
pub fn read_all(&self) -> io::Result<WalScan> {
if !self.path.exists() {
return Ok(None);
return Ok(WalScan {
records: Vec::new(),
good_len: 0,
});
}
let mut f = File::open(&self.path)?;
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
if buf.is_empty() {
let mut records = Vec::new();
let mut offset = 0usize;
while offset + 4 <= buf.len() {
let body_len =
u32::from_le_bytes([buf[offset], buf[offset + 1], buf[offset + 2], buf[offset + 3]])
as usize;
let end = offset + 4 + body_len;
if end > buf.len() {
break;
}
match decode_body(&buf[offset + 4..end]) {
Ok(record) => {
records.push(record);
offset = end;
}
Err(_) => break,
}
}
Ok(WalScan {
records,
good_len: offset,
})
}
/// 记录检查点:到此为止的 LSN 都已落盘
pub fn write_checkpoint(&self, lsn: u64) -> io::Result<()> {
let mut f = File::create(self.checkpoint_path())?;
f.write_all(&lsn.to_le_bytes())?;
f.sync_all()
}
/// 读检查点 LSN
pub fn read_checkpoint(&self) -> io::Result<Option<u64>> {
let path = self.checkpoint_path();
if !path.exists() {
return Ok(None);
}
match decode(&buf) {
Ok(opt) => Ok(opt),
Err(e) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("corrupt WAL: {}", e),
)),
let bytes = std::fs::read(&path)?;
if bytes.len() < 8 {
return Ok(None);
}
let mut raw = [0u8; 8];
raw.copy_from_slice(&bytes[..8]);
Ok(Some(u64::from_le_bytes(raw)))
}
/// 取下一条 LSN
fn take_lsn(&self) -> io::Result<u64> {
let mut slot = self.next_lsn.lock().unwrap();
if *slot == 0 {
let last = self.read_all()?.records.last().map(|(lsn, _)| *lsn).unwrap_or(0);
*slot = last + 1;
}
let lsn = *slot;
*slot += 1;
Ok(lsn)
}
/// 编一条带新 LSN 的记录
fn encode_next(&self, op: &WalOp) -> io::Result<Vec<u8>> {
let lsn = self.take_lsn()?;
encode(op, lsn)
}
}
/// 已提交的记录:提交标记前的整组;无标记的单条按旧格式算已提交
pub fn committed_ops(records: &[(u64, WalOp)]) -> Vec<(u64, WalOp)> {
let mut out = Vec::new();
let mut pending: Vec<(u64, WalOp)> = Vec::new();
for (lsn, op) in records {
if *op == WalOp::Commit {
out.append(&mut pending);
} else {
pending.push((*lsn, op.clone()));
}
}
if pending.len() == 1 {
out.append(&mut pending);
}
out
}
/// 清空文件并把读写位置归零
@@ -150,7 +277,7 @@ fn truncate(f: &mut File) -> io::Result<()> {
}
/// 把操作编成字节
fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
fn encode(op: &WalOp, lsn: u64) -> 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)?),
@@ -192,11 +319,11 @@ fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
.map_err(io::Error::other)?;
(OP_REPLACE_SCHEMA, table.clone(), p)
}
WalOp::Commit => (OP_COMMIT, String::new(), Vec::new()),
};
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);
@@ -210,20 +337,12 @@ fn encode(op: &WalOp) -> io::Result<Vec<u8>> {
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];
/// 从一个帧体解出 LSN 与操作
fn decode_body(body: &[u8]) -> Result<(u64, WalOp), String> {
if body.len() < 8 + 1 + 2 {
return Err("body too short".into());
}
let _lsn = u64::from_le_bytes([
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];
@@ -236,6 +355,7 @@ fn decode(buf: &[u8]) -> Result<Option<WalOp>, String> {
let payload = &body[11 + table_len..];
let op = match op_type {
OP_COMMIT => WalOp::Commit,
OP_HIDE_COLUMN => {
let column = serde_json::from_slice(payload)
.map_err(|e| format!("bad hide_column payload: {e}"))?;
@@ -292,5 +412,5 @@ fn decode(buf: &[u8]) -> Result<Option<WalOp>, String> {
}
other => return Err(format!("unknown op type: {}", other)),
};
Ok(Some(op))
Ok((lsn, op))
}
+186
View File
@@ -216,6 +216,67 @@ fn concurrent_shard_reads_exclude_writers() -> Result<(), Box<dyn std::error::Er
Ok(())
}
/// 跨库并发互不串数据,目录查询不被写饿死
#[test]
fn concurrent_requests_across_databases_stay_isolated() -> Result<(), Box<dyn std::error::Error>> {
let (_srv, _data) = start_server();
let mut c = connect()?;
let request = |c: &mut Conn, value: serde_json::Value| -> Result<serde_json::Value, serde_json::Error> {
serde_json::from_str(&send(c, &value.to_string()))
};
let databases = ["alpha", "beta"];
for database in databases {
assert_eq!(request(&mut c, serde_json::json!({"method":"create_database","database":database}))?["status"], "ok");
assert_eq!(
request(&mut c, serde_json::json!({"method":"create_table","database":database,"table":"items","columns":[{"name":"id","ty":"int"}]}))?["status"],
"ok"
);
}
let storage = CURRENT.with(|slot| slot.borrow().as_ref().cloned().expect("storage opened"));
let written = 40;
std::thread::scope(|scope| {
let writers: Vec<_> = databases
.iter()
.map(|database| {
let storage = Arc::clone(&storage);
let database = database.to_string();
scope.spawn(move || {
for i in 1..=written {
let line = format!(
r#"{{"method":"insert","database":"{database}","table":"items","row":{{"id":{i}}}}}"#
);
let value: serde_json::Value = serde_json::from_str(&storage.request_line(&line)).expect("json");
assert_eq!(value["status"], "ok", "{database}: {value}");
}
database
})
})
.collect();
let catalog = {
let storage = Arc::clone(&storage);
scope.spawn(move || {
for _ in 0..40 {
for line in [r#"{"method":"list_databases"}"#, r#"{"method":"all_catalogs"}"#] {
let value: serde_json::Value = serde_json::from_str(&storage.request_line(line)).expect("json");
assert_ne!(value["status"], "error", "{line}: {value}");
}
}
})
};
for writer in writers {
writer.join().expect("writer thread");
}
catalog.join().expect("catalog thread");
});
for database in databases {
let rows = request(&mut c, serde_json::json!({"method":"scan","database":database,"table":"items"}))?["rows"].clone();
assert_eq!(rows.as_array().expect("rows").len(), written, "{database} 行数不对");
}
Ok(())
}
/// 删列不改堆字节,重启后不再暴露该列
#[test]
fn drop_column_keeps_heap_bytes_and_hides_values_after_restart() -> Result<(), Box<dyn std::error::Error>> {
@@ -744,6 +805,12 @@ fn account_failure_preserves_wal_and_blocks_writes() -> Result<(), Box<dyn std::
let mut c = connect()?;
let recovered = send(&mut c, r#"{"method":"accounts_list"}"#);
assert!(recovered.contains("hash-one"), "{recovered}");
let status: serde_json::Value =
serde_json::from_str(&send(&mut c, r#"{"method":"system_status"}"#))?;
assert!(
status["last_lsn"].as_u64().unwrap_or(0) >= 1,
"重放后检查点要推进: {status}"
);
Ok(())
}
@@ -1015,6 +1082,125 @@ fn delete_keys_removes_row_and_index_entry() {
assert!(r.contains(r#""status":"ok""#), "id 2 should be insertable again: {}", r);
}
/// 事务提交:一批写操作在同一个请求里落地
#[test]
fn apply_transaction_lands_upserts_and_deletes_together() {
let (_srv, _data) = start_server();
let mut c = connect().unwrap();
for (id, name) in [(1, "Alice"), (2, "Bob")] {
let req = format!(
r#"{{"method":"insert","table":"txn_t","row":{{"id":{},"name":"{}"}}}}"#,
id, name
);
send(&mut c, &req);
}
let batch = r#"{"method":"apply_transaction","ops":[
{"op":"delete","table":"txn_t","ids":[1]},
{"op":"upsert","table":"txn_t","rows":[{"id":3,"name":"Carol"}]}
]}"#;
let r = send(&mut c, batch);
assert!(r.contains(r#""status":"ok""#), "batch: {}", r);
let r = send(&mut c, r#"{"method":"scan","table":"txn_t"}"#);
assert!(!r.contains("Alice"), "deleted row should be gone: {}", r);
assert!(r.contains("Bob") && r.contains("Carol"), "kept and added rows: {}", r);
for line in [
r#"{"method":"apply_transaction","ops":[{"op":"upsert","table":"__system_users","rows":[{"id":1}]}]}"#,
r#"{"method":"apply_transaction","ops":[{"op":"delete","table":"../evil","ids":[1]}]}"#,
] {
let r = send(&mut c, line);
assert!(r.contains(r#""status":"error""#), "bad op rejected: {} -> {}", line, r);
}
// 没有整数 id 的表用整表替换提交
let r = send(&mut c, r#"{"method":"insert","table":"txn_no_id","row":{"name":"old"}}"#);
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
let replace = r#"{"method":"apply_transaction","ops":[{"op":"replace","table":"txn_no_id","rows":[{"name":"only"}]}]}"#;
let r = send(&mut c, replace);
assert!(r.contains(r#""status":"ok""#), "replace: {}", r);
let r = send(&mut c, r#"{"method":"scan","table":"txn_no_id"}"#);
assert!(r.contains("only") && !r.contains("old"), "replaced rows: {}", r);
}
/// 标记已落盘、数据未改的崩溃现场:重启重放并推进检查点
#[test]
fn committed_group_replays_after_restart() -> Result<(), Box<dyn std::error::Error>> {
use chusql_core_storage::wal::{Wal, WalOp};
let (server, data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"insert","table":"items","row":{"id":1,"secret":"old"}}))?;
drop(c);
drop(server);
let mut added = std::collections::HashMap::new();
added.insert("id".to_string(), serde_json::json!(2));
added.insert("secret".to_string(), serde_json::json!("new"));
let group = vec![
WalOp::InsertBatch {
table: "items".into(),
rows: vec![added.clone()],
},
WalOp::DeleteKeys {
table: "items".into(),
keys: vec![1],
},
];
let wal = Wal::new(data.path().join("databases/main/wal.log"));
let commit_lsn = wal.append_group(&group)?;
drop(wal);
let _server = start_server_in(data.path());
let mut c = connect()?;
let rows: serde_json::Value =
serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"items"}"#))?;
assert_eq!(rows["rows"], serde_json::json!([{"id":2,"secret":"new"}]));
assert_eq!(std::fs::metadata(data.path().join("databases/main/wal.log"))?.len(), 0);
let checkpoint = std::fs::read(data.path().join("databases/main/wal.checkpoint"))?;
assert_eq!(checkpoint.len(), 8, "检查点要落盘");
assert_eq!(
u64::from_le_bytes(checkpoint[..8].try_into()?),
commit_lsn,
"检查点记到提交 LSN"
);
Ok(())
}
/// 没有提交标记的记录组在重启时整组丢掉
#[test]
fn uncommitted_group_is_discarded_on_restart() -> Result<(), Box<dyn std::error::Error>> {
use chusql_core_storage::wal::{Wal, WalOp};
let (server, data) = start_server();
let mut c = connect()?;
request_ok(&mut c, serde_json::json!({"method":"insert","table":"items","row":{"id":1,"secret":"old"}}))?;
drop(c);
drop(server);
let mut lost = std::collections::HashMap::new();
lost.insert("id".to_string(), serde_json::json!(2));
lost.insert("secret".to_string(), serde_json::json!("lost"));
let wal = Wal::new(data.path().join("databases/main/wal.log"));
wal.append(&WalOp::InsertBatch {
table: "items".into(),
rows: vec![lost.clone()],
})?;
wal.append(&WalOp::InsertBatch {
table: "items".into(),
rows: vec![lost],
})?;
drop(wal);
let _server = start_server_in(data.path());
let mut c = connect()?;
let rows: serde_json::Value =
serde_json::from_str(&send(&mut c, r#"{"method":"scan","table":"items"}"#))?;
assert_eq!(rows["rows"], serde_json::json!([{"id":1,"secret":"old"}]));
assert_eq!(std::fs::metadata(data.path().join("databases/main/wal.log"))?.len(), 0);
Ok(())
}
/// 给第二列建索引后能按那一列查
#[test]
fn create_index_on_secondary_column() {
+121 -2
View File
@@ -1,8 +1,8 @@
use chusql_core_storage::protocol::Row;
use chusql_core_storage::wal::{Wal, WalOp};
use chusql_core_storage::wal::{committed_ops, Wal, WalOp};
use serde_json::json;
// WAL 测试:追加、读取、清空、截断尾巴。
// WAL 测试:追加、读取、清空、截断尾巴、LSN 与提交标记。
/// 用键值对构造一行
@@ -99,3 +99,122 @@ fn truncated_tail_is_ignored() {
assert!(wal.read().unwrap().is_none());
}
/// 每条记录带递增的 LSN,重开也从文件里的最大号往下走
#[test]
fn every_record_gets_an_increasing_lsn() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
for id in 1..=3 {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(id))]),
})
.unwrap();
}
let scan = wal.read_all().unwrap();
let lsns: Vec<u64> = scan.records.iter().map(|(lsn, _)| *lsn).collect();
assert_eq!(lsns, vec![1, 2, 3]);
assert_eq!(scan.good_len as u64, std::fs::metadata(&path).unwrap().len());
let reopened = Wal::new(&path);
reopened
.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(4))]),
})
.unwrap();
assert_eq!(reopened.read_all().unwrap().records.last().unwrap().0, 4);
}
/// 一组操作写完带提交标记,标记的 LSN 也在记录里
#[test]
fn append_group_writes_a_commit_marker() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
let group = [
WalOp::InsertBatch {
table: "t".into(),
rows: vec![row(&[("id", json!(2))])],
},
WalOp::DeleteKeys {
table: "t".into(),
keys: vec![1],
},
];
let commit_lsn = wal.append_group(&group).unwrap();
let scan = wal.read_all().unwrap();
assert_eq!(scan.records.len(), 3);
assert_eq!(scan.records.last().unwrap().1, WalOp::Commit);
assert_eq!(scan.records.last().unwrap().0, commit_lsn);
assert_eq!(wal.read().unwrap().unwrap(), group[0]);
assert_eq!(
committed_ops(&scan.records),
vec![(1, group[0].clone()), (2, group[1].clone())]
);
}
/// 没有提交标记的记录组算未提交,旧格式的单条仍算已提交
#[test]
fn records_without_a_commit_marker_stay_uncommitted() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
for id in 1..=2 {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(id))]),
})
.unwrap();
}
assert!(committed_ops(&wal.read_all().unwrap().records).is_empty());
wal.clear().unwrap();
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(9))]),
})
.unwrap();
assert_eq!(committed_ops(&wal.read_all().unwrap().records).len(), 1);
}
/// 撕成两半的尾巴能读出完好长度并截掉
#[test]
fn trailing_torn_frame_can_be_truncated() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wal.log");
let wal = Wal::new(&path);
for id in 1..=2 {
wal.append(&WalOp::Insert {
table: "t".into(),
row: row(&[("id", json!(id))]),
})
.unwrap();
}
let full = std::fs::read(&path).unwrap();
std::fs::write(&path, &full[..full.len() - 3]).unwrap();
let scan = wal.read_all().unwrap();
assert_eq!(scan.records.len(), 1);
assert!(scan.good_len < full.len() - 3);
wal.truncate_to(scan.good_len).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), scan.good_len as u64);
assert_eq!(wal.read_all().unwrap().records.len(), 1);
}
/// 检查点写读一致
#[test]
fn checkpoint_round_trips() {
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));
assert_eq!(wal.read_checkpoint().unwrap(), None);
wal.write_checkpoint(7).unwrap();
assert_eq!(wal.read_checkpoint().unwrap(), Some(7));
}