2026-10-01 20:48:38 +08:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
|
|
|
|
|
module Repl
|
|
|
|
|
( runRepl
|
|
|
|
|
, runStatementOnce
|
|
|
|
|
, reportError
|
|
|
|
|
) where
|
|
|
|
|
|
|
|
|
|
import ChuSQL.CLI.Format (OutputFormat, formatName, renderResult, renderRows)
|
2026-10-05 00:15:01 +08:00
|
|
|
import ChuSQL.CLI.History (historySettings, rememberStatement)
|
2026-10-01 20:48:38 +08:00
|
|
|
import ChuSQL.CLI.Script (Meta (..), errorHint, metaHelp, parseMeta, takeStatement)
|
2026-10-05 00:15:01 +08:00
|
|
|
import ChuSQL.Interface.Session (Session, catalog, databases, roleViews, runStatement, sessionDatabase, switchDatabase)
|
2026-10-02 14:53:02 +08:00
|
|
|
import ChuSQL.Core.Protocol (SchemaColumn (..), TableInfo (..))
|
|
|
|
|
import ChuSQL.Interface.Protocol (Grant (..), RoleView (..))
|
2026-10-01 20:48:38 +08:00
|
|
|
import Control.Monad (forM_, when)
|
|
|
|
|
import Control.Monad.IO.Class (liftIO)
|
|
|
|
|
import Data.IORef (IORef, readIORef, writeIORef)
|
|
|
|
|
import Data.Text (Text)
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import qualified Data.Text.IO as TIO
|
|
|
|
|
import System.Console.Haskeline
|
|
|
|
|
import System.IO (hIsTerminalDevice, stderr, stdin)
|
|
|
|
|
|
|
|
|
|
-- 交互层:提示符、多行输入、历史、元命令与错误提示。
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 交互循环,没有历史文件就不落盘
|
2026-10-01 20:48:38 +08:00
|
|
|
runRepl :: Session -> IORef OutputFormat -> Maybe FilePath -> IO ()
|
|
|
|
|
runRepl session formatRef historyPath =
|
2026-10-05 00:15:01 +08:00
|
|
|
runInputT (historySettings historyPath) (loop session formatRef T.empty)
|
2026-10-01 20:48:38 +08:00
|
|
|
|
|
|
|
|
-- | 入口处直接报错(-e 与启动阶段用)
|
|
|
|
|
reportError :: Text -> IO ()
|
|
|
|
|
reportError message = do
|
|
|
|
|
TIO.hPutStrLn stderr ("chusql: error: " <> message)
|
|
|
|
|
forM_ (errorHint message) (\hint -> TIO.hPutStrLn stderr ("hint: " <> hint))
|
|
|
|
|
|
|
|
|
|
-- | 跑一条语句并按格式打印;成功与否当退出码
|
|
|
|
|
runStatementOnce :: Session -> OutputFormat -> Text -> IO Bool
|
|
|
|
|
runStatementOnce session format sql = do
|
|
|
|
|
result <- runStatement session sql
|
|
|
|
|
case result of
|
|
|
|
|
Left message -> reportError message >> pure False
|
|
|
|
|
Right answer -> TIO.putStrLn (renderResult format answer) >> pure True
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 读一行输入,分派到元命令或语句缓冲
|
2026-10-01 20:48:38 +08:00
|
|
|
loop :: Session -> IORef OutputFormat -> Text -> InputT IO ()
|
|
|
|
|
loop session formatRef buffer = do
|
|
|
|
|
prompt <- liftIO (promptFor session buffer)
|
|
|
|
|
entered <- getInputLine (T.unpack prompt)
|
|
|
|
|
case entered of
|
|
|
|
|
Nothing -> do
|
|
|
|
|
interactive <- liftIO (hIsTerminalDevice stdin)
|
|
|
|
|
when interactive (outputStrLn "")
|
|
|
|
|
Just raw -> do
|
|
|
|
|
let line = T.strip (T.pack raw)
|
|
|
|
|
if T.null line
|
|
|
|
|
then loop session formatRef buffer
|
|
|
|
|
else case (T.null buffer, parseMeta line) of
|
|
|
|
|
(True, Just meta) -> do
|
2026-10-05 00:15:01 +08:00
|
|
|
rememberStatement line
|
2026-10-01 20:48:38 +08:00
|
|
|
again <- runMeta session formatRef meta
|
|
|
|
|
when again (loop session formatRef T.empty)
|
|
|
|
|
_ -> do
|
|
|
|
|
let extended = if T.null buffer then line else buffer <> "\n" <> line
|
|
|
|
|
remaining <- drain session formatRef extended
|
|
|
|
|
loop session formatRef remaining
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 生成提示符;续行与非终端输入另有样式
|
2026-10-01 20:48:38 +08:00
|
|
|
promptFor :: Session -> Text -> IO Text
|
|
|
|
|
promptFor session buffer = do
|
|
|
|
|
interactive <- hIsTerminalDevice stdin
|
|
|
|
|
if not interactive
|
|
|
|
|
then pure ""
|
|
|
|
|
else
|
|
|
|
|
if not (T.null (T.strip buffer))
|
|
|
|
|
then pure " -> "
|
|
|
|
|
else do
|
|
|
|
|
current <- sessionDatabase session
|
|
|
|
|
pure (if T.null current then "chusql> " else "chusql [" <> current <> "]> ")
|
|
|
|
|
|
|
|
|
|
-- | 把攒下来的内容里所有完整语句依次执行,返回没写完的残余
|
|
|
|
|
drain :: Session -> IORef OutputFormat -> Text -> InputT IO Text
|
|
|
|
|
drain session formatRef buffer = case takeStatement buffer of
|
|
|
|
|
Nothing -> pure (T.strip buffer)
|
|
|
|
|
Just (statement, rest) -> do
|
2026-10-05 00:15:01 +08:00
|
|
|
when (not (T.null statement)) $ do
|
|
|
|
|
rememberStatement (statement <> ";")
|
|
|
|
|
runOne session formatRef statement
|
2026-10-01 20:48:38 +08:00
|
|
|
drain session formatRef rest
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 执行一条语句并打印结果
|
2026-10-01 20:48:38 +08:00
|
|
|
runOne :: Session -> IORef OutputFormat -> Text -> InputT IO ()
|
|
|
|
|
runOne session formatRef statement = do
|
|
|
|
|
result <- liftIO (runStatement session statement)
|
|
|
|
|
case result of
|
|
|
|
|
Left message -> liftIO (reportError message)
|
|
|
|
|
Right answer -> do
|
|
|
|
|
format <- liftIO (readIORef formatRef)
|
|
|
|
|
outputStrLn (T.unpack (renderResult format answer))
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 执行元命令,返回是否继续循环
|
2026-10-01 20:48:38 +08:00
|
|
|
runMeta :: Session -> IORef OutputFormat -> Meta -> InputT IO Bool
|
|
|
|
|
runMeta session formatRef meta = case meta of
|
|
|
|
|
MetaQuit -> pure False
|
|
|
|
|
MetaHelp -> outputStrLn (T.unpack metaHelp) >> pure True
|
|
|
|
|
MetaDatabases -> listDatabases session >> pure True
|
|
|
|
|
MetaTables -> listTables session >> pure True
|
|
|
|
|
MetaDescribe name -> describeTable session name >> pure True
|
|
|
|
|
MetaRoles -> listRoles session >> pure True
|
|
|
|
|
MetaConnect name -> do
|
|
|
|
|
switched <- liftIO (switchDatabase session name)
|
|
|
|
|
case switched of
|
|
|
|
|
Left message -> liftIO (reportError message)
|
|
|
|
|
Right () -> outputStrLn ("database is now " <> T.unpack name)
|
|
|
|
|
pure True
|
|
|
|
|
MetaSetFormat format -> do
|
|
|
|
|
liftIO (writeIORef formatRef format)
|
|
|
|
|
outputStrLn ("output format is now " <> T.unpack (formatName format))
|
|
|
|
|
pure True
|
|
|
|
|
MetaUnknown name -> outputStrLn ("unknown command: \\" <> T.unpack name <> " (try \\?)") >> pure True
|
|
|
|
|
|
|
|
|
|
-- | \l:库清单
|
|
|
|
|
listDatabases :: Session -> InputT IO ()
|
|
|
|
|
listDatabases session = do
|
|
|
|
|
answer <- liftIO (databases session)
|
|
|
|
|
case answer of
|
|
|
|
|
Left message -> liftIO (reportError message)
|
|
|
|
|
Right names -> outputStrLn (T.unpack (renderRows ["database"] (map (: []) names)))
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | \dt:表清单(含 server 补的账号表)
|
2026-10-01 20:48:38 +08:00
|
|
|
listTables :: Session -> InputT IO ()
|
|
|
|
|
listTables session = do
|
|
|
|
|
answer <- liftIO (catalog session)
|
|
|
|
|
case answer of
|
|
|
|
|
Left message -> liftIO (reportError message)
|
2026-10-02 14:53:02 +08:00
|
|
|
Right tables -> outputStrLn (T.unpack (renderRows headers (map tableRow tables)))
|
2026-10-01 20:48:38 +08:00
|
|
|
where
|
|
|
|
|
headers = ["table", "columns", "rows", "indexes"]
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 把表信息排成一行
|
2026-10-01 20:48:38 +08:00
|
|
|
tableRow info =
|
|
|
|
|
[ T.pack (tiTable info)
|
|
|
|
|
, tshow (length (tiColumns info))
|
|
|
|
|
, tshow (tiRows info)
|
|
|
|
|
, if null (tiIndexes info) then "-" else T.intercalate ", " (map T.pack (tiIndexes info))
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
-- | \d <table>:列结构
|
|
|
|
|
describeTable :: Session -> Text -> InputT IO ()
|
|
|
|
|
describeTable session name = do
|
|
|
|
|
answer <- liftIO (catalog session)
|
|
|
|
|
case answer of
|
|
|
|
|
Left message -> liftIO (reportError message)
|
2026-10-02 14:53:02 +08:00
|
|
|
Right tables ->
|
|
|
|
|
case filter (matches name) tables of
|
2026-10-01 20:48:38 +08:00
|
|
|
[] -> liftIO (reportError ("unknown table: " <> name))
|
|
|
|
|
(info : _) -> outputStrLn (T.unpack (renderRows headers (map columnRow (tiColumns info))))
|
|
|
|
|
where
|
|
|
|
|
headers = ["column", "type", "primary", "unique", "nullable", "auto"]
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 表名忽略大小写比较
|
2026-10-01 20:48:38 +08:00
|
|
|
matches wanted info = T.toLower (T.pack (tiTable info)) == T.toLower wanted
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 把列信息排成一行
|
2026-10-01 20:48:38 +08:00
|
|
|
columnRow column =
|
|
|
|
|
[ T.pack (scName column)
|
|
|
|
|
, T.pack (scType column)
|
|
|
|
|
, yesNo (scPrimaryKey column)
|
|
|
|
|
, yesNo (scUnique column)
|
|
|
|
|
, yesNo (scNullable column)
|
|
|
|
|
, yesNo (scAutoIncrement column)
|
|
|
|
|
]
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 布尔值转成 yes/no
|
2026-10-01 20:48:38 +08:00
|
|
|
yesNo True = "yes"
|
|
|
|
|
yesNo False = "no"
|
|
|
|
|
|
|
|
|
|
-- | \dr:角色清单(授权对象与成员)
|
|
|
|
|
listRoles :: Session -> InputT IO ()
|
|
|
|
|
listRoles session = do
|
|
|
|
|
answer <- liftIO (roleViews session)
|
|
|
|
|
case answer of
|
|
|
|
|
Left message -> liftIO (reportError message)
|
|
|
|
|
Right roles -> outputStrLn (T.unpack (renderRows ["role", "grants", "members"] (map roleRow roles)))
|
|
|
|
|
where
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 把角色信息排成一行
|
2026-10-01 20:48:38 +08:00
|
|
|
roleRow view =
|
|
|
|
|
[ roleName view
|
|
|
|
|
, if null (roleGrants view) then "-" else T.intercalate ", " (map grantText (roleGrants view))
|
|
|
|
|
, if null (roleMembers view) then "-" else T.intercalate ", " (roleMembers view)
|
|
|
|
|
]
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 拼一条授权说明
|
2026-10-03 19:35:30 +08:00
|
|
|
grantText grant = grantPrivilege grant <> " on " <> grantObject grant <> optionText grant
|
|
|
|
|
-- | 带转授权时补一段说明
|
|
|
|
|
optionText grant = if grantable grant then " with grant option" else ""
|
2026-10-01 20:48:38 +08:00
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 用 show 转成 Text
|
2026-10-01 20:48:38 +08:00
|
|
|
tshow :: Show a => a -> Text
|
|
|
|
|
tshow = T.pack . show
|