Files
ChuBlog/chusql-server/src/ChuSQL/Server/Backend.hs
T

426 lines
17 KiB
Haskell
Raw Normal View History

2026-09-25 17:41:51 +08:00
{-# LANGUAGE OverloadedStrings #-}
module ChuSQL.Server.Backend (
2026-09-25 17:41:51 +08:00
StatementResult (..),
Backend (..),
columnsFromStatement,
2026-10-01 20:48:38 +08:00
currentStamp,
exprTables,
internalTable,
grantsTable,
2026-09-25 17:41:51 +08:00
ipcBackend,
memoryBackend,
2026-10-01 20:48:38 +08:00
membersTable,
2026-10-03 19:35:30 +08:00
optionsTable,
2026-10-01 20:48:38 +08:00
rolesTable,
statementNeedsDatabase,
statementTables,
systemDatabaseName,
2026-09-25 17:41:51 +08:00
tableInfoOf,
2026-10-01 20:48:38 +08:00
tableRefsOf,
2026-09-25 17:41:51 +08:00
) where
import ChuSQL.Core.Engine (runStatement, runStatementM)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Semantic (prepare)
2026-10-03 16:49:23 +08:00
import ChuSQL.Core.Engine.Storage (MonadStorage (schema, snapshot))
import ChuSQL.Core.Engine.Storage.IPC (
2026-10-01 20:48:38 +08:00
Account (..),
2026-10-03 16:49:23 +08:00
Env,
2026-09-25 17:41:51 +08:00
IPCStorage (..),
Request (..),
Response (..),
SchemaColumn (..),
TableInfo (..),
2026-10-03 16:49:23 +08:00
envForDatabase,
sendRawRequest,
2026-09-25 17:41:51 +08:00
sendRequest,
)
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseStatement)
2026-10-03 16:49:23 +08:00
import ChuSQL.Core.Protocol (TxnOp (..))
import Control.Concurrent.MVar (MVar, modifyMVar, readMVar)
2026-09-25 17:41:51 +08:00
import Data.List (nub)
2026-10-01 20:48:38 +08:00
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
2026-09-25 17:41:51 +08:00
-- 后端抽象:跑语句、取数据字典与账号,分 IPC(真存储)和内存(测试)两套实现。
2026-09-25 17:41:51 +08:00
data StatementResult = StatementResult
{ srColumns :: [String]
, srRows :: [Row]
}
data Backend = Backend
{ beStatement :: String -> IO (Either String StatementResult)
, beCatalog :: IO (Either String [TableInfo])
, bePing :: IO Bool
2026-10-01 20:48:38 +08:00
, beAccounts :: Request -> IO (Either String [Account])
, beWithDatabase :: String -> Backend
, beDatabases :: IO (Either String [String])
-- | 原样转发一条存储请求
, beStorage :: A.Value -> IO (Either String A.Value)
2026-10-03 16:49:23 +08:00
-- | 取一份整库快照,当事务的起点
, beSnapshot :: IO (Either String Database)
-- | 事务提交:把一批写操作交给存储层应用
, beApplyTransaction :: [TxnOp] -> IO (Either String ())
2026-09-25 17:41:51 +08:00
}
-- | 按给定列名取列,固定列顺序
columnsFromStatement :: Database -> Statement -> [String]
2026-10-01 20:48:38 +08:00
columnsFromStatement db stmt = case prepare db stmt of
Right (Select cols _ _ _ _ _) -> cols
Right (SelectExpr items _ _ _ _ _) -> map fst items
2026-09-25 17:41:51 +08:00
_ -> []
-- | 走本地存储链路的后端
2026-09-25 17:41:51 +08:00
ipcBackend :: IO Backend
ipcBackend = do
2026-10-01 20:48:38 +08:00
let backend current = Backend
2026-10-03 16:49:23 +08:00
{ beStatement = \sql -> runIpc current sql
, beCatalog = do
2026-10-01 20:48:38 +08:00
response <- sendRequest (ReqInDatabase current ReqListCatalog)
pure $ case response of
RespCatalog infos -> Right infos
RespError err -> Left err
_ -> Left "unexpected catalog response"
2026-10-03 16:49:23 +08:00
, bePing = do
2026-09-25 17:41:51 +08:00
resp <- sendRequest ReqPing
pure (case resp of RespPong -> True; _ -> False)
2026-10-03 16:49:23 +08:00
, beAccounts = \req -> do
2026-10-01 20:48:38 +08:00
response <- sendRequest req
pure $ case response of
RespAccounts accounts -> Right accounts
RespError err -> Left err
_ -> Left "unexpected account response"
, beWithDatabase = backend
2026-10-03 16:49:23 +08:00
, beDatabases = do
2026-10-01 20:48:38 +08:00
response <- sendRequest (ReqDatabase "list_databases" "")
pure $ case response of
RespTables names -> Right names
RespError err -> Left err
_ -> Left "unexpected database response"
2026-10-03 16:49:23 +08:00
, 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"
2026-09-25 17:41:51 +08:00
}
2026-10-01 20:48:38 +08:00
-- 没有默认库:没选库时 current 是空串,任何请求都得先选库
pure (backend "")
2026-09-25 17:41:51 +08:00
where
2026-10-03 16:49:23 +08:00
-- | 会话的存储环境:库名跟这条连接走
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
2026-10-03 16:49:23 +08:00
-- | 走存储链路跑一条语句:表名不再补当前库前缀
-- 引擎侧的数据字典按当前库过滤之后键是裸表名,补前缀会让查表落空;
-- 库名由 database 字段随请求带走,存储层自己按它分发。
2026-10-01 20:48:38 +08:00
runIpc :: String -> String -> IO (Either String StatementResult)
2026-10-03 16:49:23 +08:00
runIpc current sql = case parseStatement sql of
2026-09-25 17:41:51 +08:00
Left e -> pure (Left e)
Right stmt -> do
2026-10-03 16:49:23 +08:00
let env = sessionEnv current
result <- runIPCStorageIn (runStatementM stmt) env
2026-09-25 17:41:51 +08:00
case result of
Left e -> pure (Left e)
Right rows -> do
cols <- case rows of
[] -> do
2026-10-03 16:49:23 +08:00
db <- runIPCStorageIn (schema :: IPCStorage Database) env
2026-09-25 17:41:51 +08:00
pure (columnsFromStatement db stmt)
(r : _) -> pure (map fst r)
pure (Right (StatementResult cols rows))
-- | 测试用内存后端,状态是一个 Database
2026-10-01 20:48:38 +08:00
memoryBackend :: String -> MVar Database -> Backend
memoryBackend = memoryBackendWith False
-- | 构造内存后端,Bool 决定普通库还是系统库视角
2026-10-01 20:48:38 +08:00
memoryBackendWith :: Bool -> String -> MVar Database -> Backend
memoryBackendWith trusted name ref =
2026-09-25 17:41:51 +08:00
Backend
2026-10-01 20:48:38 +08:00
{ 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
2026-09-25 17:41:51 +08:00
Left e -> pure (db, Left e)
2026-10-01 20:48:38 +08:00
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))
2026-09-25 17:41:51 +08:00
, beCatalog = do
db <- readMVar ref
2026-10-01 20:48:38 +08:00
pure (Right (map tableInfoOf (filter (not . internalTable . fst) db)))
2026-09-25 17:41:51 +08:00
, bePing = pure True
2026-10-01 20:48:38 +08:00
, 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)
2026-10-01 20:48:38 +08:00
, beWithDatabase = withDatabase
, beDatabases = pure (Right (nub [name, systemDatabaseName]))
, beStorage = \_ -> pure (Left "storage is not available in this backend")
2026-10-03 16:49:23 +08:00
, 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 ())
2026-09-25 17:41:51 +08:00
}
where
-- | 按视角过滤库里的表
2026-10-03 17:16:26 +08:00
scoped = if trusted then filter (not . blocked . fst) else visible
-- | 该视角下要隐藏的表
2026-10-03 17:16:26 +08:00
hidden = if trusted then blocked else internalTable
-- | 取结果列名,空结果回落到解析
2026-09-25 17:41:51 +08:00
colsOf db stmt rows = case rows of
[] -> columnsFromStatement db stmt
(r : _) -> map fst r
-- | 按目标库名挑视角
2026-10-01 20:48:38 +08:00
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)
-- | 把后端的语句与字典都改成同一条错误
2026-10-01 20:48:38 +08:00
unavailableDatabase :: String -> Backend -> Backend
unavailableDatabase message backend = backend
{ beStatement = \_ -> pure (Left message)
, beCatalog = pure (Left message)
2026-10-03 16:49:23 +08:00
, beSnapshot = pure (Left message)
, beApplyTransaction = \_ -> pure (Left message)
2026-10-01 20:48:38 +08:00
}
-- | 未选库时的视角:只放行不碰表的语句
2026-10-01 20:48:38 +08:00
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")
2026-10-03 16:49:23 +08:00
, beSnapshot = pure (Left "no database selected")
, beApplyTransaction = \_ -> pure (Left "no database selected")
2026-10-01 20:48:38 +08:00
}
2026-10-03 16:49:23 +08:00
-- | 把一批事务写操作应用到内存库:先按 id 删,再按追加写
applyTransactionMemory :: [TxnOp] -> Database -> Either String Database
applyTransactionMemory ops db = foldl step (Right db) ops
2026-10-01 20:48:38 +08:00
where
2026-10-03 16:49:23 +08:00
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)
2026-10-01 20:48:38 +08:00
2026-10-03 16:49:23 +08:00
-- | 换掉一张表,其余保持原顺序
put entry = map (\one@(name, _) -> if name == fst entry then entry else one)
2026-10-01 20:48:38 +08:00
2026-10-03 16:49:23 +08:00
-- | 这一行要不要删:按行里的 id 列比
doomed keys r = case lookup "id" r of
Just (VInt k) -> k `elem` keys
_ -> False
2026-10-01 20:48:38 +08:00
-- | 语句里出现的表
2026-10-01 20:48:38 +08:00
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]
2026-10-03 19:49:57 +08:00
tableRefsOf (FromSubquery _ stmt) = statementTables stmt
tableRefsOf (FromJoin _ left right cond) = tableRefsOf left ++ tableRefsOf right ++ exprTables cond
2026-10-01 20:48:38 +08:00
-- | 表达式里出现的表(子查询是唯一的来路)
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
2026-10-03 20:23:18 +08:00
GtE a b -> both a b
LtE a b -> both a b
NotEq a b -> both a b
2026-10-01 20:48:38 +08:00
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
-- | 两侧都取表名
2026-10-01 20:48:38 +08:00
both a b = exprTables a ++ exprTables b
2026-09-25 17:41:51 +08:00
-- | 一张表的线上信息(内存实现:没有索引,统计现算)
tableInfoOf :: (String, Table) -> TableInfo
tableInfoOf (name, tbl) =
TableInfo
{ tiTable = name
2026-10-01 20:48:38 +08:00
, tiColumns = [wireColumn c ty | (c, ty) <- tableCols tbl]
2026-09-25 17:41:51 +08:00
, tiRows = length (tableRows tbl)
, tiIndexes = []
, tiStats = [(c, distinctOf c, False) | (c, _) <- tableCols tbl]
}
where
-- | 一列的不同值个数
2026-09-25 17:41:51 +08:00
distinctOf c = length (nub [v | r <- tableRows tbl, Just v <- [lookup c r]])
-- | 本地列类型转线上字符串(和 IPC 那边同一套写法)
wireType :: Column -> String
2026-10-01 20:48:38 +08:00
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
}
-- | 系统库名,账号、角色与授权表住在这里
2026-10-01 20:48:38 +08:00
systemDatabaseName :: String
systemDatabaseName = "system"
usersTable :: String
usersTable = "__system_users"
2026-10-01 20:48:38 +08:00
2026-10-03 17:16:26 +08:00
-- | 角色与授权表的名字,在系统库里但服务自己要读写
2026-10-01 20:48:38 +08:00
rolesTable :: String
2026-10-03 17:16:26 +08:00
rolesTable = "__system_roles"
2026-10-01 20:48:38 +08:00
grantsTable :: String
2026-10-03 17:16:26 +08:00
grantsTable = "__system_grants"
2026-10-01 20:48:38 +08:00
2026-10-03 19:35:30 +08:00
-- | 带 grant option 的授权单独一张表
optionsTable :: String
optionsTable = "__system_grant_options"
2026-10-01 20:48:38 +08:00
membersTable :: String
2026-10-03 17:16:26 +08:00
membersTable = "__system_members"
2026-10-01 20:48:38 +08:00
internalNames :: [String]
2026-10-03 19:35:30 +08:00
internalNames = [rolesTable, grantsTable, optionsTable, membersTable]
2026-10-01 20:48:38 +08:00
-- | 名字是否带内部表前缀
2026-10-01 20:48:38 +08:00
reserved :: String -> Bool
reserved = T.isPrefixOf "__system_" . T.toLower . T.pack
2026-10-01 20:48:38 +08:00
2026-10-03 17:16:26 +08:00
-- | 是不是服务自己要经请求通道读写的表
privilegeTable :: String -> Bool
privilegeTable name = T.unpack (T.toLower (T.pack name)) `elem` internalNames
-- | 是不是服务自己的内部表
2026-10-01 20:48:38 +08:00
internalTable :: String -> Bool
2026-10-03 17:16:26 +08:00
internalTable name = reserved name || privilegeTable name
-- | 外部语句能不能碰这张表
blocked :: String -> Bool
blocked name = reserved name && not (privilegeTable name)
2026-10-01 20:48:38 +08:00
-- | 去掉内部表
2026-10-01 20:48:38 +08:00
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
-- | 在内存账号表上执行一条账号请求
2026-10-01 20:48:38 +08:00
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
-- | 把一行解回账号
2026-10-01 20:48:38 +08:00
decodeAccount row = case lookup "account" row of
Just (VStr encoded) -> A.eitherDecode (BL.pack encoded)
_ -> Left "invalid account record"