Files
ChuBlog/chusql-server/src/ChuSQL/Server/Backend.hs
T
2026-10-03 16:49:23 +08:00

410 lines
17 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
module ChuSQL.Server.Backend (
StatementResult (..),
Backend (..),
columnsFromStatement,
currentStamp,
exprTables,
internalTable,
grantsTable,
ipcBackend,
memoryBackend,
membersTable,
rolesTable,
statementNeedsDatabase,
statementTables,
systemDatabaseName,
tableInfoOf,
tableRefsOf,
) where
import ChuSQL.Core.Engine (runStatement, runStatementM)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Semantic (prepare)
import ChuSQL.Core.Engine.Storage (MonadStorage (schema, snapshot))
import ChuSQL.Core.Engine.Storage.IPC (
Account (..),
Env,
IPCStorage (..),
Request (..),
Response (..),
SchemaColumn (..),
TableInfo (..),
envForDatabase,
sendRawRequest,
sendRequest,
)
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseStatement)
import ChuSQL.Core.Protocol (TxnOp (..))
import Control.Concurrent.MVar (MVar, modifyMVar, readMVar)
import Data.List (nub)
import Data.Text (Text)
import Data.Time (getCurrentTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.Text as T
-- 后端抽象:跑语句、取数据字典与账号,分 IPC(真存储)和内存(测试)两套实现。
data StatementResult = StatementResult
{ srColumns :: [String]
, srRows :: [Row]
}
data Backend = Backend
{ beStatement :: String -> IO (Either String StatementResult)
, beCatalog :: IO (Either String [TableInfo])
, bePing :: IO Bool
, beAccounts :: Request -> IO (Either String [Account])
, beWithDatabase :: String -> Backend
, beDatabases :: IO (Either String [String])
-- | 原样转发一条存储请求
, beStorage :: A.Value -> IO (Either String A.Value)
-- | 取一份整库快照,当事务的起点
, beSnapshot :: IO (Either String Database)
-- | 事务提交:把一批写操作交给存储层应用
, beApplyTransaction :: [TxnOp] -> IO (Either String ())
}
-- | 按给定列名取列,固定列顺序
columnsFromStatement :: Database -> Statement -> [String]
columnsFromStatement db stmt = case prepare db stmt of
Right (Select cols _ _ _ _ _) -> cols
Right (SelectExpr items _ _ _ _ _) -> map fst items
_ -> []
-- | 走本地存储链路的后端
ipcBackend :: IO Backend
ipcBackend = do
let backend current = Backend
{ beStatement = \sql -> runIpc current sql
, beCatalog = do
response <- sendRequest (ReqInDatabase current ReqListCatalog)
pure $ case response of
RespCatalog infos -> Right infos
RespError err -> Left err
_ -> Left "unexpected catalog response"
, bePing = do
resp <- sendRequest ReqPing
pure (case resp of RespPong -> True; _ -> False)
, beAccounts = \req -> do
response <- sendRequest req
pure $ case response of
RespAccounts accounts -> Right accounts
RespError err -> Left err
_ -> Left "unexpected account response"
, beWithDatabase = backend
, beDatabases = do
response <- sendRequest (ReqDatabase "list_databases" "")
pure $ case response of
RespTables names -> Right names
RespError err -> Left err
_ -> Left "unexpected database response"
, beStorage = \payload -> runRaw payload
, beSnapshot = Right <$> runIPCStorageIn (snapshot :: IPCStorage Database) (sessionEnv current)
, beApplyTransaction = \ops -> do
response <- sendRequest (ReqInDatabase current (ReqApplyTransaction ops))
pure $ case response of
RespOk -> Right ()
RespError err -> Left err
_ -> Left "unexpected transaction response"
}
-- 没有默认库:没选库时 current 是空串,任何请求都得先选库
pure (backend "")
where
-- | 会话的存储环境:库名跟这条连接走
sessionEnv :: String -> Env
sessionEnv current = envForDatabase (if null current then Nothing else Just current)
-- | 转发一条存储请求并解析响应
runRaw :: A.Value -> IO (Either String A.Value)
runRaw payload = do
response <- sendRawRequest (BL.toStrict (A.encode payload))
pure $ case response of
Left err -> Left err
Right raw -> case A.eitherDecodeStrict raw of
Left err -> Left ("storage response is not valid JSON: " ++ err)
Right value -> Right value
-- | 走存储链路跑一条语句:表名不再补当前库前缀
-- 引擎侧的数据字典按当前库过滤之后键是裸表名,补前缀会让查表落空;
-- 库名由 database 字段随请求带走,存储层自己按它分发。
runIpc :: String -> String -> IO (Either String StatementResult)
runIpc current sql = case parseStatement sql of
Left e -> pure (Left e)
Right stmt -> do
let env = sessionEnv current
result <- runIPCStorageIn (runStatementM stmt) env
case result of
Left e -> pure (Left e)
Right rows -> do
cols <- case rows of
[] -> do
db <- runIPCStorageIn (schema :: IPCStorage Database) env
pure (columnsFromStatement db stmt)
(r : _) -> pure (map fst r)
pure (Right (StatementResult cols rows))
-- | 测试用内存后端,状态是一个 Database
memoryBackend :: String -> MVar Database -> Backend
memoryBackend = memoryBackendWith False
-- | 构造内存后端,Bool 决定普通库还是系统库视角
memoryBackendWith :: Bool -> String -> MVar Database -> Backend
memoryBackendWith trusted name ref =
Backend
{ beStatement = \sql -> case parseStatement sql of
Left e -> pure (Left e)
-- 内存夹具也有"库"的概念:USE 只能切到它有的两个名字(自己 + system),
-- 与 beDatabases 口径一致;引擎的内存存储不实现库管理,所以拦在这里。
Right (UseDatabase target)
| target == name || target == systemDatabaseName -> pure (Right (StatementResult [] []))
| otherwise -> pure (Left ("unknown database: " ++ target))
Right stmt -> modifyMVar ref $ \db -> case runStatement (scoped db) stmt of
Left e -> pure (db, Left e)
Right (db', rows)
| any (hidden . fst) db' -> pure (db, Left "reserved system table")
| otherwise -> pure (filter (hidden . fst) db ++ db', Right (StatementResult (colsOf (scoped db) stmt rows) rows))
, beCatalog = do
db <- readMVar ref
pure (Right (map tableInfoOf (filter (not . internalTable . fst) db)))
, bePing = pure True
, beAccounts = \req -> do
stamp <- currentStamp
modifyMVar ref $ \db -> case memoryAccounts db stamp req of
Left err -> pure (db, Left err)
Right accounts -> pure ((usersTable, Table usersTable [("account", TStr)]
[[("account", VStr (BL.unpack (A.encode a))) ] | a <- accounts] Nothing) : filter ((/= usersTable) . fst) db, Right accounts)
, beWithDatabase = withDatabase
, beDatabases = pure (Right (nub [name, systemDatabaseName]))
, beStorage = \_ -> pure (Left "storage is not available in this backend")
, beSnapshot = Right . scoped <$> readMVar ref
, beApplyTransaction = \ops -> modifyMVar ref $ \db -> case applyTransactionMemory ops db of
Left err -> pure (db, Left err)
Right db' -> pure (db', Right ())
}
where
-- | 按视角过滤库里的表
scoped = if trusted then filter (not . reserved . fst) else visible
-- | 该视角下要隐藏的表
hidden = if trusted then reserved else internalTable
-- | 取结果列名,空结果回落到解析
colsOf db stmt rows = case rows of
[] -> columnsFromStatement db stmt
(r : _) -> map fst r
-- | 按目标库名挑视角
withDatabase target
| target == systemDatabaseName = memoryBackendWith True name ref
| target == name = memoryBackendWith False name ref
| null target = noDatabaseBackend (memoryBackendWith False name ref)
| otherwise = unavailableDatabase ("unknown database: " ++ target) (memoryBackendWith False name ref)
-- | 把后端的语句与字典都改成同一条错误
unavailableDatabase :: String -> Backend -> Backend
unavailableDatabase message backend = backend
{ beStatement = \_ -> pure (Left message)
, beCatalog = pure (Left message)
, beSnapshot = pure (Left message)
, beApplyTransaction = \_ -> pure (Left message)
}
-- | 未选库时的视角:只放行不碰表的语句
noDatabaseBackend :: Backend -> Backend
noDatabaseBackend backend = backend
{ beStatement = \sql -> case parseStatement sql of
Left e -> pure (Left e)
Right stmt
| statementNeedsDatabase stmt -> pure (Left "no database selected")
| otherwise -> beStatement backend sql
, beCatalog = pure (Left "no database selected")
, beSnapshot = pure (Left "no database selected")
, beApplyTransaction = \_ -> pure (Left "no database selected")
}
-- | 把一批事务写操作应用到内存库:先按 id 删,再按追加写
applyTransactionMemory :: [TxnOp] -> Database -> Either String Database
applyTransactionMemory ops db = foldl step (Right db) ops
where
step acc op = do
current <- acc
case op of
TxnDelete name keys -> do
table <- lookupTable current name
pure (put (name, table {tableRows = [r | r <- tableRows table, not (doomed keys r)]}) current)
TxnUpsert name rows -> do
table <- lookupTable current name
pure (put (name, table {tableRows = tableRows table ++ rows}) current)
TxnReplace name rows -> do
table <- lookupTable current name
pure (put (name, table {tableRows = rows}) current)
-- | 换掉一张表,其余保持原顺序
put entry = map (\one@(name, _) -> if name == fst entry then entry else one)
-- | 这一行要不要删:按行里的 id 列比
doomed keys r = case lookup "id" r of
Just (VInt k) -> k `elem` keys
_ -> False
-- | 语句里出现的表
statementTables :: Statement -> [Text]
statementTables stmt = nub (case stmt of
Select{} ->
tableRefsOf (selectFrom stmt)
++ concatMap exprTables (maybe [] (: []) (selectWhere stmt))
SelectExpr{} ->
tableRefsOf (selectFrom stmt)
++ concatMap exprTables (maybe [] (: []) (selectWhere stmt) ++ map snd (selectItems stmt))
Insert table _ rows -> T.pack table : concatMap exprTables (concat rows)
Update table assigns cond -> T.pack table : concatMap exprTables (map snd assigns ++ maybe [] (: []) cond)
Delete table cond -> T.pack table : concatMap exprTables (maybe [] (: []) cond)
CreateTable table _ -> [T.pack table]
DropTable table -> [T.pack table]
CreateIndex table _ -> [T.pack table]
DropIndex table _ -> [T.pack table]
DropColumn table _ -> [T.pack table]
AddColumn table _ -> [T.pack table]
RenameColumn table _ _ -> [T.pack table]
AlterColumnType table _ _ -> [T.pack table]
AlterColumnDefault table _ _ -> [T.pack table]
AlterColumnNull table _ _ -> [T.pack table]
_ -> [])
-- | 语句需不需要一个当前库:出现的表里有没写库名的就必须要
statementNeedsDatabase :: Statement -> Bool
statementNeedsDatabase stmt = any (not . T.any (== '.')) (statementTables stmt)
-- | FROM 子句里的所有表
tableRefsOf :: FromClause -> [Text]
tableRefsOf FromUnit = []
tableRefsOf (FromTable _ name) = [T.pack name]
tableRefsOf (FromJoin _ left _ name cond) = tableRefsOf left ++ [T.pack name] ++ exprTables cond
-- | 表达式里出现的表(子查询是唯一的来路)
exprTables :: Expr -> [Text]
exprTables expr = case expr of
ScalarSub sub -> statementTables (subqueryStatement sub)
ExistsSub sub _ -> statementTables (subqueryStatement sub)
InSub value sub _ -> exprTables value ++ statementTables (subqueryStatement sub)
InList value items _ -> concatMap exprTables (value : items)
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
Div 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
Neg a -> exprTables a
IsNull a -> exprTables a
IsNotNull a -> exprTables a
CountOf a -> exprTables a
SumOf a -> exprTables a
AvgOf a -> exprTables a
MinOf a -> exprTables a
MaxOf a -> exprTables a
_ -> []
where
-- | 两侧都取表名
both a b = exprTables a ++ exprTables b
-- | 一张表的线上信息(内存实现:没有索引,统计现算)
tableInfoOf :: (String, Table) -> TableInfo
tableInfoOf (name, tbl) =
TableInfo
{ tiTable = name
, tiColumns = [wireColumn c ty | (c, ty) <- tableCols tbl]
, tiRows = length (tableRows tbl)
, tiIndexes = []
, tiStats = [(c, distinctOf c, False) | (c, _) <- tableCols tbl]
}
where
-- | 一列的不同值个数
distinctOf c = length (nub [v | r <- tableRows tbl, Just v <- [lookup c r]])
-- | 本地列类型转线上字符串(和 IPC 那边同一套写法)
wireType :: Column -> String
wireType = typeName . columnType
-- | 本地列转线上 schema
wireColumn :: String -> Column -> SchemaColumn
wireColumn name col =
SchemaColumn
{ scName = name
, scType = wireType col
, scNullable = columnNullable col
, scDefault = columnDefault col
, scAutoIncrement = columnAutoIncrement col
, scPrimaryKey = columnPrimaryKey col
, scUnique = columnUnique col
, scCheck = columnCheck col
}
-- | 系统库名,账号、角色与授权表住在这里
systemDatabaseName :: String
systemDatabaseName = "system"
usersTable :: String
usersTable = "__system_users"
-- | 角色与授权表的名字,不带内部前缀
rolesTable :: String
rolesTable = "sys_roles"
grantsTable :: String
grantsTable = "sys_grants"
membersTable :: String
membersTable = "sys_members"
internalNames :: [String]
internalNames = [rolesTable, grantsTable, membersTable]
-- | 名字是否带内部表前缀
reserved :: String -> Bool
reserved = T.isPrefixOf "__system_" . T.toLower . T.pack
-- | 是不是服务自己的内部表
internalTable :: String -> Bool
internalTable name = reserved name || T.unpack (T.toLower (T.pack name)) `elem` internalNames
-- | 去掉内部表
visible :: Database -> Database
visible = filter (not . internalTable . fst)
-- | 当前 UTC 时间,写法与引擎的 timestamp 一致
currentStamp :: IO T.Text
currentStamp = T.pack . formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" <$> getCurrentTime
-- | 在内存账号表上执行一条账号请求
memoryAccounts :: Database -> T.Text -> Request -> Either String [Account]
memoryAccounts db stamp req = do
accounts <- case lookup usersTable db of
Nothing -> Right []
Just table -> mapM decodeAccount (tableRows table)
case req of
ReqAccountsList -> Right accounts
ReqAccountCreate u h
| any ((== T.toLower u) . accountUser) accounts -> Left "account already exists"
| otherwise -> Right (accounts ++ [Account (1 + maximum (0 : map accountId accounts)) (T.toLower u) h 1 stamp Nothing])
ReqAccountReset u h -> case filter ((== T.toLower u) . accountUser) accounts of
[_] -> Right [if accountUser x == T.toLower u then x{accountHash = h, accountRevision = accountRevision x + 1} else x | x <- accounts]
_ -> Left "unknown account"
ReqAccountLogin u at -> case filter ((== T.toLower u) . accountUser) accounts of
[_] -> Right [if accountUser x == T.toLower u then x{accountLastLoginAt = at} else x | x <- accounts]
_ -> Left "unknown account"
ReqAccountDrop u -> case filter ((== T.toLower u) . accountUser) accounts of
[] -> Left "unknown account"
_ -> Right [x | x <- accounts, accountUser x /= T.toLower u]
_ -> Left "invalid account request"
where
-- | 把一行解回账号
decodeAccount row = case lookup "account" row of
Just (VStr encoded) -> A.eitherDecode (BL.pack encoded)
_ -> Left "invalid account record"