{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE OverloadedStrings #-} module ChuSQL.Storage.IPC ( IPCStorage (..), Request (..), Response (..), SchemaColumn (..), sendRequest, doListTables, doLookupByKey, doInsertKeyed, doDescribeTable, ) where import ChuSQL.Model import ChuSQL.Storage import Control.Exception (bracket) import Data.Aeson (FromJSON (..), ToJSON (..), eitherDecodeStrict, encode, object, withObject, (.:), (.=)) import Data.Aeson qualified as A import Data.Aeson.Key qualified as K import Data.Aeson.KeyMap qualified as KM import Data.Aeson.Types (Parser) import Data.ByteString.Char8 qualified as BSC import Data.ByteString.Lazy qualified as BL import Data.List (nub) import Data.Scientific (floatingOrInteger) import Data.Text qualified as T import System.Environment (lookupEnv) import System.IO -- IPC 存储实现:每个 MonadStorage 方法翻译成管道上的一条 JSON 请求。 -- | IPC 实现:通过命名管道连 Rust 存储进程。 newtype IPCStorage a = IPCStorage { runIPCStorage :: IO a } -- | Functor:把 fmap 转发给底下的 IO。 instance Functor IPCStorage where fmap f (IPCStorage m) = IPCStorage (fmap f m) -- | Applicative:把 pure / <*> 转发给底下的 IO。 instance Applicative IPCStorage where pure = IPCStorage . pure IPCStorage mf <*> IPCStorage ma = IPCStorage (mf <*> ma) -- | Monad:顺序执行,转发给底下的 IO。 instance Monad IPCStorage where IPCStorage m >>= k = IPCStorage $ do a <- m runIPCStorage (k a) -- | 管道路径;默认 \\.\pipe\chusql-storage,可用 CHUSQL_PIPE 覆盖。 pipePath :: IO String pipePath = do name <- maybe "chusql-storage" id <$> lookupEnv "CHUSQL_PIPE" pure ("\\\\.\\pipe\\" ++ name) -- | 打开管道、跑闭包、关闭。 withPipe :: (Handle -> IO a) -> IO a withPipe = bracket openPipe hClose where -- \| 二进制读写、不做换行翻译,跟 Rust 侧对齐。 openPipe = do path <- pipePath h <- openFile path ReadWriteMode hSetBuffering h LineBuffering hSetNewlineMode h noNewlineTranslation pure h -- | 发一条请求,读一条响应。 sendRequest :: Request -> IO Response sendRequest req = withPipe $ \h -> do BL.hPutStr h (encode req) BSC.hPutStr h "\n" hFlush h line <- BSC.hGetLine h let cleaned = BSC.dropWhileEnd (== '\r') line case eitherDecodeStrict cleaned of Left err -> pure (RespError ("decode: " ++ err)) Right r -> pure r -- * JSON 编解码 -- | 一列的 schema;对应 Rust 侧的 SchemaColumn。 data SchemaColumn = SchemaColumn { scName :: String , scType :: String } deriving (Show, Eq) -- | SchemaColumn 编码;Haskell 侧只发 DescribeTable,编码用不上,暂存实现。 instance ToJSON SchemaColumn where toJSON (SchemaColumn n t) = object ["name" .= n, "ty" .= t] -- | SchemaColumn 解码:从 {"name": "...", "ty": "int"|"str"|"bool"} 读回。 instance FromJSON SchemaColumn where parseJSON = withObject "SchemaColumn" $ \o -> do n <- o .: "name" t <- o .: "ty" pure (SchemaColumn n t) -- | 请求;对应 Rust 侧带 method tag 的 enum。 data Request = ReqPing | ReqScan String | ReqInsert String Row (Maybe Int) | ReqReplaceAll String [Row] | ReqListTables | ReqLookupByKey String Int | ReqDescribeTable String -- | 请求编码:method 字段 + 各自参数。 instance ToJSON Request where toJSON ReqPing = object ["method" .= ("ping" :: T.Text)] toJSON (ReqScan t) = object ["method" .= ("scan" :: T.Text), "table" .= t] toJSON (ReqInsert t r mk) = object $ [ "method" .= ("insert" :: T.Text) , "table" .= t , "row" .= rowToJSON r ] ++ case mk of Nothing -> [] Just k -> ["key" .= k] toJSON (ReqReplaceAll t rs) = object [ "method" .= ("replace_all" :: T.Text) , "table" .= t , "rows" .= map rowToJSON rs ] toJSON ReqListTables = object ["method" .= ("list_tables" :: T.Text)] toJSON (ReqLookupByKey t k) = object [ "method" .= ("lookup_by_index" :: T.Text) , "table" .= t , "key" .= k ] toJSON (ReqDescribeTable t) = object [ "method" .= ("describe_table" :: T.Text) , "table" .= t ] -- | 响应;对应 Rust 侧带 status tag 的 enum。 data Response = RespPong | RespRows [Row] | RespTables [String] | RespSchema [SchemaColumn] | RespOk | RespError String -- | 响应解码:按 status 字段分派。 instance FromJSON Response where parseJSON = withObject "Response" $ \o -> do status <- o .: "status" case status :: T.Text of "pong" -> pure RespPong "ok" -> pure RespOk "rows" -> RespRows <$> (o .: "rows" >>= mapM rowFromJSON) "tables" -> RespTables <$> o .: "tables" "schema" -> RespSchema <$> o .: "columns" "error" -> RespError <$> o .: "message" other -> fail ("unknown status: " ++ T.unpack other) -- | Value 编码成 JSON 的 number / string / bool。 valueToJSON :: Value -> A.Value valueToJSON (VInt n) = A.Number (fromIntegral n) valueToJSON (VStr s) = A.String (T.pack s) valueToJSON (VBool b) = A.Bool b -- | JSON 解回 Value;number 只接受整数。 valueFromJSON :: A.Value -> Parser Value valueFromJSON (A.Number n) = case floatingOrInteger n :: Either Double Integer of Right i -> pure (VInt (fromIntegral i)) Left _ -> fail "non-integer number" valueFromJSON (A.String s) = pure (VStr (T.unpack s)) valueFromJSON (A.Bool b) = pure (VBool b) valueFromJSON _ = fail "unsupported value type" -- | 一行编成 JSON object。 rowToJSON :: Row -> A.Value rowToJSON r = A.Object (KM.fromList [(K.fromString k, valueToJSON v) | (k, v) <- r]) -- | JSON object 解成一行。 rowFromJSON :: A.Value -> Parser Row rowFromJSON (A.Object o) = mapM toPair (KM.toList o) where -- \| 解一个键值对。 toPair (k, v) = do val <- valueFromJSON v pure (K.toString k, val) rowFromJSON _ = fail "row must be a JSON object" -- * 内部请求封装 -- | 发 Scan 并把结果取成 Either。 doScan :: String -> IO (Either String [Row]) doScan t = do resp <- sendRequest (ReqScan t) pure $ case resp of RespRows rows -> Right rows RespError e -> Left e _ -> Left "unexpected response to scan" -- | 发 Insert(不带索引键)。 doInsert :: String -> Row -> IO (Either String ()) doInsert t r = do resp <- sendRequest (ReqInsert t r Nothing) pure $ case resp of RespOk -> Right () RespError e -> Left e _ -> Left "unexpected response to insert" -- | 发 Insert(带索引键)。 doInsertKeyed :: String -> Row -> Int -> IO (Either String ()) doInsertKeyed t r k = do resp <- sendRequest (ReqInsert t r (Just k)) pure $ case resp of RespOk -> Right () RespError e -> Left e _ -> Left "unexpected response to insert" -- | 发 ReplaceAll。 doReplaceAll :: String -> [Row] -> IO (Either String ()) doReplaceAll t rs = do resp <- sendRequest (ReqReplaceAll t rs) pure $ case resp of RespOk -> Right () RespError e -> Left e _ -> Left "unexpected response to replace_all" -- | 发 ListTables。 doListTables :: IO (Either String [String]) doListTables = do resp <- sendRequest ReqListTables pure $ case resp of RespTables ts -> Right ts RespError e -> Left e _ -> Left "unexpected response to list_tables" -- | 发 LookupByIndex。 doLookupByKey :: String -> Int -> IO (Either String (Maybe Row)) doLookupByKey t k = do resp <- sendRequest (ReqLookupByKey t k) pure $ case resp of RespRows [] -> Right Nothing RespRows (r : _) -> Right (Just r) RespError e -> Left e _ -> Left "unexpected response to lookup_by_index" -- | 发 DescribeTable。 doDescribeTable :: String -> IO (Either String [SchemaColumn]) doDescribeTable t = do resp <- sendRequest (ReqDescribeTable t) pure $ case resp of RespSchema cols -> Right cols RespError e -> Left e _ -> Left "unexpected response to describe_table" -- | 把 Rust 的 schema 列转成 Model 的 Column;未知类型降级为 TStr。 schemaToColumns :: [SchemaColumn] -> [(String, Column)] schemaToColumns = map go where -- \| 按 ty 字符串映射成 Column 构造子。 go (SchemaColumn n "int") = (n, TInt) go (SchemaColumn n "str") = (n, TStr) go (SchemaColumn n "bool") = (n, TBool) go (SchemaColumn n _) = (n, TStr) -- | 从若干行推断列名与类型;catalog 拿不到时的兜底。 inferColumns :: [Row] -> [(String, Column)] inferColumns rows = [(n, inferType n) | n <- names] where -- \| 全部出现过的列名。 names = nub (concatMap (map fst) rows) -- \| 用第一个出现的值推断列类型;找不到就给 TStr。 inferType n = case [v | r <- rows, Just v <- [lookup n r]] of (VInt _ : _) -> TInt (VStr _ : _) -> TStr (VBool _ : _) -> TBool [] -> TStr -- | 先 describe 拿列,再 scan 拿行;describe 拿不到时退回扫行推断。 loadTable :: String -> IO Table loadTable t = do schemaR <- doDescribeTable t case schemaR of Right cols -> do rowsR <- doScan t case rowsR of Right rows -> pure (Table t (schemaToColumns cols) rows) Left _ -> pure (Table t (schemaToColumns cols) []) Left _ -> do rowsR <- doScan t case rowsR of Right rows -> pure (Table t (inferColumns rows) rows) Left _ -> pure (Table t [] []) -- * MonadStorage 实例 -- | IPC 实现:所有方法都走管道。 instance MonadStorage IPCStorage where -- \| 发 Scan。 scan t = IPCStorage (doScan t) -- \| 发 Insert,不带索引键。 insert t r = IPCStorage (doInsert t r) -- \| 发 ReplaceAll。 replaceAll t rs = IPCStorage (doReplaceAll t rs) -- \| 发 LookupByIndex。 lookupByKey t k = IPCStorage (doLookupByKey t k) -- \| ListTables + 逐表 (describe, scan),拼成 Database。 snapshot = IPCStorage $ do result <- doListTables case result of Left _ -> pure [] Right ts -> mapM loadDbEntry ts where -- \| 加载一个 (表名, 表) 对。 loadDbEntry t = do tbl <- loadTable t pure (t, tbl)