2026-09-25 17:41:51 +08:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
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
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
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))
|
2026-10-02 14:53:02 +08:00
|
|
|
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,
|
2026-10-02 14:53:02 +08:00
|
|
|
sendRawRequest,
|
2026-09-25 17:41:51 +08:00
|
|
|
sendRequest,
|
|
|
|
|
)
|
2026-10-02 14:53:02 +08:00
|
|
|
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
|
|
|
|
2026-10-02 14:53:02 +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])
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 原样转发一条存储请求
|
|
|
|
|
, 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-10-02 14:53:02 +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)
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 转发一条存储请求并解析响应
|
|
|
|
|
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))
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 测试用内存后端,状态是一个 Database
|
2026-10-01 20:48:38 +08:00
|
|
|
memoryBackend :: String -> MVar Database -> Backend
|
|
|
|
|
memoryBackend = memoryBackendWith False
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 构造内存后端,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)]
|
2026-10-03 02:17:46 +08:00
|
|
|
[[("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]))
|
2026-10-02 14:53:02 +08:00
|
|
|
, 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-02 14:53:02 +08:00
|
|
|
-- | 按视角过滤库里的表
|
2026-10-03 17:16:26 +08:00
|
|
|
scoped = if trusted then filter (not . blocked . fst) else visible
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 该视角下要隐藏的表
|
2026-10-03 17:16:26 +08:00
|
|
|
hidden = if trusted then blocked else internalTable
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 取结果列名,空结果回落到解析
|
2026-09-25 17:41:51 +08:00
|
|
|
colsOf db stmt rows = case rows of
|
|
|
|
|
[] -> columnsFromStatement db stmt
|
|
|
|
|
(r : _) -> map fst r
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 按目标库名挑视角
|
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-02 14:53:02 +08:00
|
|
|
-- | 把后端的语句与字典都改成同一条错误
|
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-02 14:53:02 +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-02 14:53:02 +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]
|
|
|
|
|
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
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 两侧都取表名
|
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-10-02 14:53:02 +08:00
|
|
|
-- | 一列的不同值个数
|
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-02 14:53:02 +08:00
|
|
|
-- | 系统库名,账号、角色与授权表住在这里
|
2026-10-01 20:48:38 +08:00
|
|
|
systemDatabaseName :: String
|
|
|
|
|
systemDatabaseName = "system"
|
|
|
|
|
|
|
|
|
|
usersTable :: String
|
2026-10-03 02:17:46 +08:00
|
|
|
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-02 14:53:02 +08:00
|
|
|
-- | 名字是否带内部表前缀
|
2026-10-01 20:48:38 +08:00
|
|
|
reserved :: String -> Bool
|
2026-10-03 02:17:46 +08:00
|
|
|
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-02 14:53:02 +08:00
|
|
|
-- | 是不是服务自己的内部表
|
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-02 14:53:02 +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-02 14:53:02 +08:00
|
|
|
-- | 在内存账号表上执行一条账号请求
|
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-02 14:53:02 +08:00
|
|
|
-- | 把一行解回账号
|
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"
|