修复错误,优化性能,实现类型系统,优化安装卸载过程

This commit is contained in:
LIm1tZz7 committed 2026-10-05 00:15:01 +08:00
1 parent fd5f320360
commit 48eec484b7
104 files changed
+4127 -845

No files matched your search

+15 -9
View File
@@ -3,8 +3,9 @@
module Main (main) where
import ChuSQL.CLI.Format (OutputFormat (..), formatName, parseFormat)
import ChuSQL.CLI.Password (readPasswordInput)
import ChuSQL.CLI.Script (stripTerminator)
import ChuSQL.CLI.Session (
import ChuSQL.Interface.Session (
Session,
authenticateSession,
isPlainIdentifier,
@@ -13,7 +14,6 @@ import ChuSQL.CLI.Session (
)
import ChuSQL.Interface.Config (ServerConfig (..), loadServerConfigAt, loadWebConfigAt, rootUserName)
import ChuSQL.Interface.Link (Client, closeClient, connectClient)
import ChuSQL.Interface.Settings (readSettingsFile)
import ChuSQL.Interface.TOML (resolveConfigPath)
import Control.Exception (finally)
import Control.Monad (unless)
@@ -27,7 +27,7 @@ import System.Console.Haskeline (defaultSettings, getPassword, runInputT)
import System.Directory (getHomeDirectory)
import System.Exit (exitFailure)
import System.FilePath ((</>))
import System.IO (hSetEncoding, stderr, stdout, utf8)
import System.IO (hFlush, hIsTerminalDevice, hPutStr, hSetEncoding, stderr, stdin, stdout, utf8)
-- chusql-cli:与 Web 管理端并列的前端,接到拥有存储的 server 上跑 SQL,账号与 Web 共用。
@@ -44,7 +44,7 @@ data Options = Options
optionsParser :: Parser Options
optionsParser =
Options
<$> optional (T.pack <$> strOption (long "user" <> short 'u' <> metavar "USER" <> help "Sign in as this account (default: the configured administrator)"))
<$> optional (T.pack <$> strOption (long "user" <> short 'u' <> metavar "USER" <> help "Sign in as this account (default: root)"))
<*> ( T.pack
<$> strOption
( long "database"
@@ -65,7 +65,7 @@ optionsParser =
)
<*> optional (T.pack <$> strOption (long "execute" <> short 'e' <> metavar "SQL" <> help "Run one statement and exit"))
<*> optional (strOption (long "history" <> metavar "FILE" <> help "SQL history file (default ~/.chusql_history)"))
<*> optional (strOption (long "config" <> metavar "FILE" <> help "Config file to read (default: the fixed chusql.toml under the system config directory)"))
<*> optional (strOption (long "config" <> metavar "FILE" <> help "Config file to read (default: the fixed settings.toml under the system config directory)"))
-- | 把 --format 的取值解析成格式
formatReader :: String -> Either String OutputFormat
@@ -89,9 +89,8 @@ main = do
options <- execParser parserInfo
configPath <- resolveConfigPath (optConfig options)
base <- loadWebConfigAt configPath
saved <- readSettingsFile configPath
config <- loadServerConfigAt configPath
let rootName = rootUserName base saved
let rootName = rootUserName base
userName = fromMaybe rootName (optUser options)
password <- promptPassword userName
connected <- connectClient (T.pack (scHost config)) (scPort config)
@@ -139,8 +138,15 @@ selectDatabase session wanted
-- | 交互式读取口令;直接回车表示空口令
promptPassword :: Text -> IO Text
promptPassword user = do
entered <- runInputT defaultSettings (getPassword (Just '*') (T.unpack user <> "'s password: "))
pure (maybe "" T.pack entered)
terminal <- hIsTerminalDevice stdin
if terminal
then do
entered <- runInputT defaultSettings (getPassword (Just '*') (T.unpack user <> "'s password: "))
pure (maybe "" T.pack entered)
else do
hPutStr stderr (T.unpack user <> "'s password: ")
hFlush stderr
readPasswordInput stdin
-- | 取历史文件路径,没给就用默认
resolveHistory :: Maybe FilePath -> IO (Maybe FilePath)
+7 -3
View File
@@ -7,8 +7,9 @@ module Repl
) where
import ChuSQL.CLI.Format (OutputFormat, formatName, renderResult, renderRows)
import ChuSQL.CLI.History (historySettings, rememberStatement)
import ChuSQL.CLI.Script (Meta (..), errorHint, metaHelp, parseMeta, takeStatement)
import ChuSQL.CLI.Session (Session, catalog, databases, roleViews, runStatement, sessionDatabase, switchDatabase)
import ChuSQL.Interface.Session (Session, catalog, databases, roleViews, runStatement, sessionDatabase, switchDatabase)
import ChuSQL.Core.Protocol (SchemaColumn (..), TableInfo (..))
import ChuSQL.Interface.Protocol (Grant (..), RoleView (..))
import Control.Monad (forM_, when)
@@ -25,7 +26,7 @@ import System.IO (hIsTerminalDevice, stderr, stdin)
-- | 交互循环,没有历史文件就不落盘
runRepl :: Session -> IORef OutputFormat -> Maybe FilePath -> IO ()
runRepl session formatRef historyPath =
runInputT (defaultSettings{historyFile = historyPath}) (loop session formatRef T.empty)
runInputT (historySettings historyPath) (loop session formatRef T.empty)
-- | 入口处直接报错(-e 与启动阶段用)
reportError :: Text -> IO ()
@@ -56,6 +57,7 @@ loop session formatRef buffer = do
then loop session formatRef buffer
else case (T.null buffer, parseMeta line) of
(True, Just meta) -> do
rememberStatement line
again <- runMeta session formatRef meta
when again (loop session formatRef T.empty)
_ -> do
@@ -81,7 +83,9 @@ 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
when (not (T.null statement)) (runOne session formatRef statement)
when (not (T.null statement)) $ do
rememberStatement (statement <> ";")
runOne session formatRef statement
drain session formatRef rest
-- | 执行一条语句并打印结果
+10 -4
View File
@@ -2,21 +2,25 @@ cabal-version: 2.4
name: chusql-cli
version: 1.0.0
synopsis: ChuSQL 命令行客户端:直连数据库服务,与 Web 管理端共用账号
description: ChuSQL 的命令行前端。自己不开存储、不链引擎,所有 SQL 与会话
都走 chusql-server 的 TCP 端点;账号表与权限服务与 Web 管理端完全共用。
synopsis: ChuSQL command-line client for the database server
description: Provides a command-line interface for SQL and database sessions
through the chusql-server TCP endpoint. Shares accounts and
permissions with the web interface while keeping query execution
and storage in the database server process.
build-type: Simple
library
hs-source-dirs: src
exposed-modules: ChuSQL.CLI.Format
ChuSQL.CLI.History
ChuSQL.CLI.Password
ChuSQL.CLI.Script
ChuSQL.CLI.Session
build-depends: base >= 4.14 && < 5
, aeson
, bytestring
, chusql-core-model
, chusql-interface
, haskeline
, text
default-language: Haskell2010
ghc-options: -Wall
@@ -48,11 +52,13 @@ test-suite chusql-cli-test
main-is: Spec.hs
hs-source-dirs: test
build-depends: base
, directory
, aeson
, bytestring
, chusql-cli
, chusql-core-model
, hspec
, haskeline
, text
default-language: Haskell2010
ghc-options: -Wall
+21
View File
@@ -0,0 +1,21 @@
{-# LANGUAGE OverloadedStrings #-}
module ChuSQL.CLI.History (historySettings, rememberStatement) where
import Control.Monad (unless)
import Control.Monad.IO.Class (MonadIO)
import qualified Data.Text as T
import System.Console.Haskeline (InputT, Settings (..), defaultSettings, modifyHistory)
import System.Console.Haskeline.History (addHistory)
-- 关闭逐行历史,筛选完整语句后记录安全输入。
-- | 配置仅手动追加的输入历史
historySettings :: MonadIO m => Maybe FilePath -> Settings m
historySettings path = defaultSettings {historyFile = path, autoAddHistory = False}
-- | 排除含口令标记的完整语句
rememberStatement :: MonadIO m => T.Text -> InputT m ()
rememberStatement statement =
unless (T.null (T.strip statement) || any (`T.isInfixOf` T.toCaseFold statement) ["password", "identified"])
(modifyHistory (addHistory (T.unpack statement)))
+14
View File
@@ -0,0 +1,14 @@
module ChuSQL.CLI.Password (readPasswordInput) where
import Data.Text (Text)
import qualified Data.Text.IO as T
import System.IO (Handle, hIsEOF, hSetNewlineMode, universalNewlineMode)
-- 管道口令输入:识别 LF 与 CRLF 行尾,保留口令内容。
-- | 从句柄读取一行口令
readPasswordInput :: Handle -> IO Text
readPasswordInput handle = do
hSetNewlineMode handle universalNewlineMode
empty <- hIsEOF handle
if empty then pure mempty else T.hGetLine handle
-5
View File
@@ -1,5 +0,0 @@
module ChuSQL.CLI.Session (module ChuSQL.Interface.Session) where
import ChuSQL.Interface.Session
-- 命令行会话:转出前端共享的会话接口,一切请求走 server。
+1
View File
@@ -8,5 +8,6 @@ packages:
- ../chusql-core/engine
- ../chusql-server
- ../chusql-web
- ../chusql-bootstrap
- ../chusql-core/model
- ../chusql-interface
+78 -1
View File
@@ -3,6 +3,8 @@
module Main (main) where
import ChuSQL.CLI.Format (OutputFormat (..), parseFormat, renderCsv, renderJson, renderTable)
import ChuSQL.CLI.History (historySettings, rememberStatement)
import ChuSQL.CLI.Password (readPasswordInput)
import ChuSQL.CLI.Script (Meta (..), errorHint, parseMeta, statementComplete, stripTerminator, takeStatement)
import ChuSQL.Core.Model (Value (..))
import ChuSQL.Core.Protocol (QueryResult (..), queryResultJson)
@@ -11,9 +13,15 @@ import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Control.Exception (bracket, finally)
import Control.Monad.IO.Class (liftIO)
import System.Directory (getTemporaryDirectory, removeFile)
import System.IO (Handle, SeekMode (AbsoluteSeek), hClose, hPutStr, hSeek, hSetNewlineMode, noNewlineTranslation, openTempFile)
import Test.Hspec
import System.Console.Haskeline (getHistory, getInputLine, runInputT, runInputTBehavior, useFileHandle)
import System.Console.Haskeline.History (historyLines)
-- chusql-cli 测试:只覆盖本前端的纯逻辑(输出格式、语句缓冲与元命令)。
-- chusql-cli 测试:输出格式、语句缓冲、元命令与口令输入。
-- | 测试入口
main :: IO ()
@@ -24,6 +32,75 @@ spec :: Spec
spec = do
formatSpec
scriptSpec
passwordSpec
historySpec
-- | 历史过滤与持久化的用例
historySpec :: Spec
historySpec = describe "CLI: SQL history" $ do
it "does not automatically record unfinished input lines" $
withPasswordInput "CREATE USER alice IDENTIFIED BY\n'secret'\n" (\handle -> do
entries <- runInputTBehavior (useFileHandle handle) (historySettings Nothing) $ do
getInputLine "" >>= liftExpectation "CREATE USER alice IDENTIFIED BY"
getInputLine "" >>= liftExpectation "'secret'"
historyLines <$> getHistory
entries `shouldBe` [])
it "keeps ordinary statements and excludes multiline or mixed-case passwords" $ do
entries <- runInputT (historySettings Nothing) $ do
rememberStatement "SELECT 1;"
rememberStatement "CREATE USER alice\nWITH PaSsWoRd\n'secret';"
rememberStatement "ALTER USER alice PASSWORD 'new-secret';"
rememberStatement "CREATE USER alice\nIdEnTiFiEd /* credential */ BY\n'secret';"
rememberStatement "ALTER ROLE analyst IDENTIFIED BY 'new-secret';"
rememberStatement "SELECT 'password';"
rememberStatement " "
historyLines <$> getHistory
entries `shouldBe` ["SELECT 1;"]
it "persists only safe statements from a batch" $ do
directory <- getTemporaryDirectory
bracket (openTempFile directory "csql-history-test")
(\(path, _) -> removeFile path)
(\(path, handle) -> do
hClose handle
runInputT (historySettings (Just path)) $
rememberBatch "SELECT 1; CREATE USER alice IDENTIFIED BY 'secret'; SELECT 2;"
entries <- runInputT (historySettings (Just path)) (historyLines <$> getHistory)
entries `shouldBe` ["SELECT 2;", "SELECT 1;"])
where
-- | 核对实际读取的输入行
liftExpectation expected actual = liftIO (actual `shouldBe` Just expected)
-- | 按完整语句筛选批量输入
rememberBatch input = case takeStatement input of
Nothing -> pure ()
Just (statement, rest) -> do
rememberStatement (statement <> ";")
rememberBatch rest
-- | 在临时句柄中写入口令输入
withPasswordInput :: String -> (Handle -> IO a) -> IO a
withPasswordInput input action = do
directory <- getTemporaryDirectory
bracket (openTempFile directory "csql-password-test")
(\(path, handle) -> hClose handle `finally` removeFile path)
(\(_, handle) -> do
hSetNewlineMode handle noNewlineTranslation
hPutStr handle input
hSeek handle AbsoluteSeek 0
action handle)
-- | 管道口令的行尾与内容用例
passwordSpec :: Spec
passwordSpec = describe "CLI: password input" $ do
it "reads LF, CRLF and unterminated passwords identically" $ do
mapM_ (\input -> withPasswordInput input (\handle -> readPasswordInput handle `shouldReturn` "secret"))
["secret\n", "secret\r\n", "secret"]
it "preserves password spaces and internal carriage returns" $
withPasswordInput " a\rb \r\n" (\handle -> readPasswordInput handle `shouldReturn` " a\rb ")
it "accepts an empty password and consumes only one line" $
withPasswordInput "\r\nsecond\n" (\handle -> do
readPasswordInput handle `shouldReturn` ""
readPasswordInput handle `shouldReturn` "second"
readPasswordInput handle `shouldReturn` "")
-- | 输出格式的用例
formatSpec :: Spec