packages feed

mysql-haskell 0.2.0.0 → 0.3.0.0

raw patch · 13 files changed

+342/−221 lines, 13 filesdep +HsOpenSSLdep −tlsdep −transformersdep ~tcp-streamsPVP ok

version bump matches the API change (PVP)

Dependencies added: HsOpenSSL

Dependencies removed: tls, transformers

Dependency ranges changed: tcp-streams

API changes (from Hackage documentation)

- Database.MySQL.Base: [ciTLSInfo] :: ConnectInfo -> Maybe (ClientParams, String)
- Database.MySQL.Protocol.Auth: [greetingLang] :: Greeting -> !Word8
- Database.MySQL.Protocol.Auth: [greetingTid] :: Greeting -> !Word32
- Database.MySQL.Protocol.Command: getCommand :: Get Command
- Database.MySQL.Protocol.Command: instance Data.Binary.Class.Binary Database.MySQL.Protocol.Command.Command
+ Database.MySQL.OpenSSL: connect :: ConnectInfo -> SSLContext -> IO MySQLConn
+ Database.MySQL.OpenSSL: connectDetail :: ConnectInfo -> SSLContext -> IO (Greeting, MySQLConn)
+ Database.MySQL.Protocol.Auth: SSLRequest :: !Word32 -> !Word32 -> !Word8 -> SSLRequest
+ Database.MySQL.Protocol.Auth: [greetingAuthPlugin] :: Greeting -> !ByteString
+ Database.MySQL.Protocol.Auth: [greetingCharset] :: Greeting -> !Word8
+ Database.MySQL.Protocol.Auth: [greetingConnId] :: Greeting -> !Word32
+ Database.MySQL.Protocol.Auth: [sslReqCaps] :: SSLRequest -> !Word32
+ Database.MySQL.Protocol.Auth: [sslReqCharset] :: SSLRequest -> !Word8
+ Database.MySQL.Protocol.Auth: [sslReqMaxPacket] :: SSLRequest -> !Word32
+ Database.MySQL.Protocol.Auth: clientCap :: Word32
+ Database.MySQL.Protocol.Auth: clientCharset :: Word8
+ Database.MySQL.Protocol.Auth: clientMaxPacketSize :: Word32
+ Database.MySQL.Protocol.Auth: data SSLRequest
+ Database.MySQL.Protocol.Auth: getSSLRequest :: Get SSLRequest
+ Database.MySQL.Protocol.Auth: instance Data.Binary.Class.Binary Database.MySQL.Protocol.Auth.SSLRequest
+ Database.MySQL.Protocol.Auth: instance GHC.Classes.Eq Database.MySQL.Protocol.Auth.SSLRequest
+ Database.MySQL.Protocol.Auth: instance GHC.Show.Show Database.MySQL.Protocol.Auth.SSLRequest
+ Database.MySQL.Protocol.Auth: putSSLRequest :: SSLRequest -> Put
+ Database.MySQL.Protocol.Auth: sslRequest :: SSLRequest
+ Database.MySQL.Protocol.Auth: supportTLS :: Word32 -> Bool
+ Database.MySQL.Protocol.MySQLValue: feedLenEncBytes :: FieldType -> (t -> b) -> (ByteString -> Maybe t) -> Get b
- Database.MySQL.Base: ConnectInfo :: HostName -> PortNumber -> ByteString -> ByteString -> ByteString -> Maybe (ClientParams, String) -> ConnectInfo
+ Database.MySQL.Base: ConnectInfo :: HostName -> PortNumber -> ByteString -> ByteString -> ByteString -> ConnectInfo
- Database.MySQL.Protocol.Auth: Greeting :: !Word8 -> !ByteString -> !Word32 -> !ByteString -> !Word16 -> !Word8 -> !Word16 -> !ByteString -> Greeting
+ Database.MySQL.Protocol.Auth: Greeting :: !Word8 -> !ByteString -> !Word32 -> !ByteString -> !Word32 -> !Word8 -> !Word16 -> !ByteString -> !ByteString -> Greeting
- Database.MySQL.Protocol.Auth: [greetingCaps] :: Greeting -> !Word16
+ Database.MySQL.Protocol.Auth: [greetingCaps] :: Greeting -> !Word32

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # Revision history for mysql-haskell +## 0.3.0.0  -- 2016-8-19++* Fix tls connection, change TLS implementation to HsOpenSSL, add benchmarks.+* Fix a bug in 'putLenEncInt' which cause sending large field fail.+* Various optimizations.+ ## 0.2.0.0  -- 2016-8-19  * Fix OK packet decoder.
Database/MySQL/BinLogProtocol/BinLogValue.hs view
@@ -321,3 +321,4 @@             rest <- nullpos' `seq` ppos' `seq` go fs nullmap nullpos' pmap' ppos'             return (rest `seq` (r : rest))         else ppos' `seq` go fs nullmap nullpos pmap' ppos'+
Database/MySQL/Connection.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE CPP #-}- {-| Module      : Database.MySQL.Connection Description : Connection managment@@ -19,7 +17,6 @@                                                   throwIO) import           Control.Monad import qualified Crypto.Hash                     as Crypto-import qualified Data.Binary                     as Binary import qualified Data.Binary.Put                 as Binary import           Data.Bits import qualified Data.ByteArray                  as BA@@ -29,20 +26,16 @@ import qualified Data.ByteString.Unsafe          as B import           Data.IORef                      (IORef, newIORef, readIORef,                                                   writeIORef)-import qualified Data.TLSSetting                 as TLS import           Data.Typeable-import           Data.Word import           Database.MySQL.Protocol.Auth import           Database.MySQL.Protocol.Command import           Database.MySQL.Protocol.Packet import           Network.Socket                  (HostName, PortNumber) import qualified Network.Socket                  as N-import qualified Network.TLS                     as TLS import           System.IO.Streams               (InputStream, OutputStream) import qualified System.IO.Streams               as Stream import qualified System.IO.Streams.Binary        as Binary import qualified System.IO.Streams.TCP           as TCP-import qualified System.IO.Streams.TLS           as TLS  -------------------------------------------------------------------------------- @@ -63,14 +56,12 @@     , ciDatabase :: ByteString     , ciUser     :: ByteString     , ciPassword :: ByteString-    , ciTLSInfo  :: Maybe (TLS.ClientParams, String) -- ^ If 'TLS.ClientParams' and subject name are provided,-                                                     -- TLS connection will be used.-    } deriving Show+    }  -- | A simple 'ConnectInfo' targeting localhost with @user=root@ and empty password. -- defaultConnectInfo :: ConnectInfo-defaultConnectInfo = ConnectInfo "127.0.0.1" 3306 "" "root" "" Nothing+defaultConnectInfo = ConnectInfo "127.0.0.1" 3306 "" "root" ""  -------------------------------------------------------------------------------- @@ -89,47 +80,29 @@ -- | Establish a MySQL connection with 'Greeting' back, so you can find server's version .etc. -- connectDetail :: ConnectInfo -> IO (Greeting, MySQLConn)-connectDetail ci@(ConnectInfo host port _ _ _ tls) =-    case tls of-        Nothing ->-            bracketOnError (TCP.connectWithBufferSize host port bUFSIZE)-               (\(_, _, sock) -> N.close sock) $ \ (is, os, sock) -> do-                    is' <- decodeInputStream is-                    os' <- Binary.encodeOutputStream os-                    p <- readPacket is'-                    greet <- decodeFromPacket p-                    let auth = mkAuth ci greet-                    Stream.write (Just (encodeToPacket 1 auth)) os'-                    q <- readPacket is'-                    if isOK q-                    then do-                        consumed <- newIORef True-                        let conn = (MySQLConn is' os' (N.close sock) consumed)-                        return (greet, conn)-                    else Stream.write Nothing os' >> decodeFromPacket q >>= throwIO . ERRException-        Just (cp, sname) ->-            bracketOnError (TLS.connect cp (Just sname) host port)-               (\(_, _, ctx) -> TLS.close ctx) $ \ (is, os, ctx) -> do-                    is' <- decodeInputStream is-                    os' <- Binary.encodeOutputStream os-                    p <- readPacket is'-                    greet <- decodeFromPacket p-                    let auth = mkAuth ci greet-                    Stream.write (Just (encodeToPacket 1 auth)) os'-                    q <- readPacket is'-                    if isOK q-                    then do-                        consumed <- newIORef True-                        let conn = (MySQLConn is' os' (TLS.close ctx) consumed)-                        return (greet, conn)-                    else Stream.write Nothing os' >> decodeFromPacket q >>= throwIO . ERRException-  where-    mkAuth :: ConnectInfo -> Greeting -> Auth-    mkAuth (ConnectInfo _ _ db user pass _) greet =-        let salt = greetingSalt1 greet `B.append` greetingSalt2 greet-            scambleBuf = scramble salt pass-        in Auth clientCap clientMaxPacketSize clientCharset user scambleBuf db+connectDetail ci@(ConnectInfo host port _ _ _) =+    bracketOnError (TCP.connectWithBufferSize host port bUFSIZE)+       (\(_, _, sock) -> N.close sock) $ \ (is, os, sock) -> do+            is' <- decodeInputStream is+            os' <- Binary.encodeOutputStream os+            p <- readPacket is'+            greet <- decodeFromPacket p+            let auth = mkAuth ci greet+            Stream.write (Just (encodeToPacket 1 auth)) os'+            q <- readPacket is'+            if isOK q+            then do+                consumed <- newIORef True+                let conn = MySQLConn is' os' (N.close sock) consumed+                return (greet, conn)+            else Stream.write Nothing os' >> decodeFromPacket q >>= throwIO . ERRException +mkAuth :: ConnectInfo -> Greeting -> Auth+mkAuth (ConnectInfo _ _ db user pass) greet =+    let salt = greetingSalt1 greet `B.append` greetingSalt2 greet+        scambleBuf = scramble salt pass+    in Auth clientCap clientMaxPacketSize clientCharset user scambleBuf db+  where     scramble :: ByteString -> ByteString -> ByteString     scramble salt pass         | B.null pass = B.empty@@ -140,17 +113,17 @@     sha1 :: ByteString -> ByteString     sha1 = BA.convert . (Crypto.hash :: ByteString -> Crypto.Digest Crypto.SHA1) -    -- | A specialized 'decodeInputStream' here for speed-    decodeInputStream :: InputStream ByteString -> IO (InputStream Packet)-    decodeInputStream is = Stream.makeInputStream $ do-        bs <- Stream.readExactly 4 is-        let len =  fromIntegral (bs `B.unsafeIndex` 0)-               .|. fromIntegral (bs `B.unsafeIndex` 1) `shiftL` 8-               .|. fromIntegral (bs `B.unsafeIndex` 2) `shiftL` 16-            seqN = bs `B.unsafeIndex` 3-        body <- loopRead [] len is-        return . Just $ Packet len seqN body-+-- | A specialized 'decodeInputStream' here for speed+decodeInputStream :: InputStream ByteString -> IO (InputStream Packet)+decodeInputStream is = Stream.makeInputStream $ do+    bs <- Stream.readExactly 4 is+    let len =  fromIntegral (bs `B.unsafeIndex` 0)+           .|. fromIntegral (bs `B.unsafeIndex` 1) `shiftL` 8+           .|. fromIntegral (bs `B.unsafeIndex` 2) `shiftL` 16+        seqN = bs `B.unsafeIndex` 3+    body <- loopRead [] len is+    return . Just $ Packet len seqN body+  where     loopRead acc 0 _  = return $! L.fromChunks (reverse acc)     loopRead acc k is = do         bs <- Stream.read is@@ -214,7 +187,7 @@ {-# INLINE readPacket #-}  writeCommand :: Command -> OutputStream Packet -> IO ()-writeCommand a os = let bs = Binary.runPut (Binary.put a) in+writeCommand a os = let bs = Binary.runPut (putCommand a) in     go (fromIntegral (L.length bs)) 0 bs os   where     go len seqN bs os' = do@@ -238,50 +211,6 @@ writeIORef' :: IORef a -> a -> IO () writeIORef' ref x = x `seq` writeIORef ref x {-# INLINE writeIORef' #-}------------------------------------------------------------------------------------- default Capability Flags--#define CLIENT_LONG_PASSWORD                  0x00000001-#define CLIENT_FOUND_ROWS                     0x00000002-#define CLIENT_LONG_FLAG                      0x00000004-#define CLIENT_CONNECT_WITH_DB                0x00000008-#define CLIENT_NO_SCHEMA                      0x00000010-#define CLIENT_COMPRESS                       0x00000020-#define CLIENT_ODBC                           0x00000040-#define CLIENT_LOCAL_FILES                    0x00000080-#define CLIENT_IGNORE_SPACE                   0x00000100-#define CLIENT_PROTOCOL_41                    0x00000200-#define CLIENT_INTERACTIVE                    0x00000400-#define CLIENT_SSL                            0x00000800-#define CLIENT_IGNORE_SIGPIPE                 0x00001000-#define CLIENT_TRANSACTIONS                   0x00002000-#define CLIENT_RESERVED                       0x00004000-#define CLIENT_SECURE_CONNECTION              0x00008000-#define CLIENT_MULTI_STATEMENTS               0x00010000-#define CLIENT_MULTI_RESULTS                  0x00020000-#define CLIENT_PS_MULTI_RESULTS               0x00040000-#define CLIENT_PLUGIN_AUTH                    0x00080000-#define CLIENT_CONNECT_ATTRS                  0x00100000-#define CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 0x00200000--clientCap :: Word32-clientCap =  CLIENT_LONG_PASSWORD-                .|. CLIENT_LONG_FLAG-                .|. CLIENT_CONNECT_WITH_DB-                .|. CLIENT_IGNORE_SPACE-                .|. CLIENT_PROTOCOL_41-                .|. CLIENT_TRANSACTIONS-                .|. CLIENT_MULTI_STATEMENTS-                .|. CLIENT_SECURE_CONNECTION--clientMaxPacketSize :: Word32-clientMaxPacketSize = 0x00ffffff :: Word32---- | Always use @utf8_general_ci@ when connecting mysql server,--- since this will simplify string decoding.-clientCharset :: Word8-clientCharset = 0x21 :: Word8  -------------------------------------------------------------------------------- -- Exceptions
+ Database/MySQL/OpenSSL.hs view
@@ -0,0 +1,60 @@+{-|+Module      : Database.MySQL.Connection+Description : Connection managment+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : drkoster@qq.com+Stability   : experimental+Portability : PORTABLE++This module provides secure MySQL connection using 'HsOpenSSL' package.++-}++module Database.MySQL.OpenSSL where++import           Control.Exception              (bracketOnError, throwIO)+import           Control.Monad+import           Data.IORef                     (newIORef)+import           Database.MySQL.Connection      hiding (connect, connectDetail)+import           Database.MySQL.Protocol.Auth+import           Database.MySQL.Protocol.Packet+import qualified Network.Socket                 as N+import qualified OpenSSL                        as SSL+import qualified OpenSSL.Session                as Session+import qualified System.IO.Streams              as Stream+import qualified System.IO.Streams.Binary       as Binary+import qualified System.IO.Streams.OpenSSL      as SSL+import qualified System.IO.Streams.TCP          as TCP++--------------------------------------------------------------------------------++connect :: ConnectInfo -> Session.SSLContext -> IO MySQLConn+connect c cp = fmap snd (connectDetail c cp)++connectDetail :: ConnectInfo -> Session.SSLContext -> IO (Greeting, MySQLConn)+connectDetail ci@(ConnectInfo host port _ _ _) ctx =+    bracketOnError (TCP.connectWithBufferSize host port bUFSIZE)+       (\(_, _, sock) -> N.close sock) $ \ (is, os, sock) -> do+            is' <- decodeInputStream is+            os' <- Binary.encodeOutputStream os+            p <- readPacket is'+            greet <- decodeFromPacket p+            if supportTLS (greetingCaps greet)+            then SSL.withOpenSSL $ do+                Stream.write (Just (encodeToPacket 1 sslRequest)) os'+                bracketOnError (Session.connection ctx sock) SSL.close $ \ ssl -> do+                    Session.connect ssl+                    (sslIs, sslOs) <- SSL.sslToStreams ssl+                    sslIs' <- decodeInputStream sslIs+                    sslOs' <- Binary.encodeOutputStream sslOs+                    let auth = mkAuth ci greet+                    Stream.write (Just (encodeToPacket 2 auth)) sslOs'+                    q <- readPacket sslIs'+                    if isOK q+                    then do+                        consumed <- newIORef True+                        let conn = MySQLConn sslIs' sslOs' (SSL.close ssl) consumed+                        return (greet, conn)+                    else Stream.write Nothing sslOs' >> decodeFromPacket q >>= throwIO . ERRException+            else error "Database.MySQL.OpenSSL: server doesn't support TLS connection"
Database/MySQL/Protocol/Auth.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# OPTIONS_GHC -funbox-strict-fields #-}  {-|@@ -22,48 +23,90 @@ import           Data.Binary.Put import qualified Data.ByteString                as B import           Data.ByteString.Char8          as BC+import           Data.Bits import           Database.MySQL.Protocol.Packet  -------------------------------------------------------------------------------- -- Authentications +#define CLIENT_LONG_PASSWORD                  0x00000001+#define CLIENT_FOUND_ROWS                     0x00000002+#define CLIENT_LONG_FLAG                      0x00000004+#define CLIENT_CONNECT_WITH_DB                0x00000008+#define CLIENT_NO_SCHEMA                      0x00000010+#define CLIENT_COMPRESS                       0x00000020+#define CLIENT_ODBC                           0x00000040+#define CLIENT_LOCAL_FILES                    0x00000080+#define CLIENT_IGNORE_SPACE                   0x00000100+#define CLIENT_PROTOCOL_41                    0x00000200+#define CLIENT_INTERACTIVE                    0x00000400+#define CLIENT_SSL                            0x00000800+#define CLIENT_IGNORE_SIGPIPE                 0x00001000+#define CLIENT_TRANSACTIONS                   0x00002000+#define CLIENT_RESERVED                       0x00004000+#define CLIENT_SECURE_CONNECTION              0x00008000+#define CLIENT_MULTI_STATEMENTS               0x00010000+#define CLIENT_MULTI_RESULTS                  0x00020000+#define CLIENT_PS_MULTI_RESULTS               0x00040000+#define CLIENT_PLUGIN_AUTH                    0x00080000+#define CLIENT_CONNECT_ATTRS                  0x00100000+#define CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 0x00200000+ data Greeting = Greeting     { greetingProtocol :: !Word8     , greetingVersion  :: !B.ByteString-    , greetingTid      :: !Word32+    , greetingConnId   :: !Word32     , greetingSalt1    :: !B.ByteString-    , greetingCaps     :: !Word16-    , greetingLang     :: !Word8+    , greetingCaps     :: !Word32+    , greetingCharset  :: !Word8     , greetingStatus   :: !Word16     , greetingSalt2    :: !B.ByteString+    , greetingAuthPlugin :: !B.ByteString     } deriving (Show, Eq)  putGreeting :: Greeting -> Put-putGreeting (Greeting p v t s1 c l st s2) = do-    putWord8 p-    putByteString v+putGreeting (Greeting pv sv cid salt1 cap charset st salt2 authPlugin) = do+    putWord8 pv+    putByteString sv     putWord8 0x00-    putWord32le t-    putByteString s1-    putWord16le c-    putWord8 l+    putWord32le cid+    putByteString salt1+    let capL = fromIntegral cap .|. 0xFF+        capH = fromIntegral (cap `shiftR` 16) .|. 0xFF+    putWord16le capL+    putWord8 charset     putWord16le st-    replicateM_ 13 (putWord8 0x00)-    putByteString s2+    putWord16le capH+    putWord8 (fromIntegral $ B.length salt2)+    replicateM_ 10 (putWord8 0x00)+    when (cap .&. CLIENT_SECURE_CONNECTION /= 0)+        (putByteString salt2)+    when (cap .&. CLIENT_PLUGIN_AUTH /= 0)+        (putByteString authPlugin)  getGreeting :: Get Greeting-getGreeting = Greeting-    <$> getWord8-    <*> getByteStringNul-    <*> getWord32le-    <*> getByteStringNul-    <*> getWord16le-    <*> getWord8-    <*> getWord16le-    <*  skip 13-    <*> getByteStringNul-    <*  getByteStringNul+getGreeting = do+    pv <- getWord8+    sv <- getByteStringNul+    cid <- getWord32le+    salt1 <- getByteString 8+    skip 1  -- 0x00+    capL <- getWord16le+    charset <- getWord8+    status <- getWord16le+    capH <- getWord16le+    let cap = fromIntegral capH `shiftL` 16 .|. fromIntegral capL+    authPluginLen <- getWord8+    skip 10 -- 10 * 0x00+    salt2 <- if (cap .&. CLIENT_SECURE_CONNECTION) == 0+        then pure B.empty+        else let len = max 13 (authPluginLen - 8) in getByteString (fromIntegral len)+    authPlugin <- if (cap .&. CLIENT_PLUGIN_AUTH) == 0+        then pure B.empty+        else getByteStringNul +    return (Greeting pv sv cid salt1 cap charset status salt2 authPlugin)+ instance Binary Greeting where     get = getGreeting     put = putGreeting@@ -101,3 +144,50 @@ instance Binary Auth where     get = getAuth     put = putAuth++data SSLRequest = SSLRequest+    { sslReqCaps      :: !Word32+    , sslReqMaxPacket :: !Word32+    , sslReqCharset   :: !Word8+    } deriving (Show, Eq)++getSSLRequest :: Get SSLRequest+getSSLRequest = SSLRequest <$> getWord32le <*> getWord32le <*> getWord8 <* skip 23++putSSLRequest :: SSLRequest -> Put+putSSLRequest (SSLRequest cap m c) = do+    putWord32le cap+    putWord32le m+    putWord8 c+    replicateM_ 23 (putWord8 0x00)++instance Binary SSLRequest where+    get = getSSLRequest+    put = putSSLRequest++--------------------------------------------------------------------------------+-- default Capability Flags++clientCap :: Word32+clientCap =  CLIENT_LONG_PASSWORD+                .|. CLIENT_LONG_FLAG+                .|. CLIENT_CONNECT_WITH_DB+                .|. CLIENT_IGNORE_SPACE+                .|. CLIENT_PROTOCOL_41+                .|. CLIENT_TRANSACTIONS+                .|. CLIENT_MULTI_STATEMENTS+                .|. CLIENT_SECURE_CONNECTION++clientMaxPacketSize :: Word32+clientMaxPacketSize = 0x00ffffff :: Word32++-- | Always use @utf8_general_ci@ when connecting mysql server,+-- since this will simplify string decoding.+clientCharset :: Word8+clientCharset = 0x21 :: Word8++supportTLS :: Word32 -> Bool+supportTLS x = (x .&. CLIENT_SSL) /= 0++sslRequest :: SSLRequest+sslRequest = SSLRequest (clientCap .|. CLIENT_SSL) clientMaxPacketSize clientCharset
Database/MySQL/Protocol/Command.hs view
@@ -21,7 +21,7 @@ import           Data.Binary.Get import           Data.Binary.Put import           Data.ByteString                    (ByteString)-import qualified          Data.ByteString.Lazy               as L+import qualified Data.ByteString.Lazy               as L import           Database.MySQL.Protocol.MySQLValue import           Database.MySQL.Protocol.Packet @@ -48,25 +48,6 @@     | COM_UNSUPPORTED    deriving (Show, Eq) -getCommand :: Get Command-getCommand = do-    cmdId <- getWord8-    case cmdId of-        0x01  -> pure COM_QUIT-        0x02  -> COM_INIT_DB <$> getRemainingByteString-        0x03  -> COM_QUERY   <$> getRemainingLazyByteString-        0x0E  -> pure COM_PING-        0x12  -> COM_BINLOG_DUMP-                    <$> getWord32le <*> getWord16le <*> getWord32le <*> getRemainingByteString-        0x15  -> COM_REGISTER_SLAVE-                    <$> getWord32le <*> getLenEncBytes <*> getLenEncBytes <*> getLenEncBytes-                    <*> getWord16le <*> getWord32le <*> getWord32le-        0x16  -> COM_STMT_PREPARE <$> getRemainingLazyByteString-        0x17  -> fail "Database.MySQL.Protocol.Command: decode COM_STMT_EXECUTE need column number"-        0x19  -> COM_STMT_CLOSE <$> getWord32le-        0x1A  -> COM_STMT_RESET <$> getWord32le-        _     -> pure COM_UNSUPPORTED- putCommand :: Command -> Put putCommand COM_QUIT              = putWord8 0x01 putCommand (COM_INIT_DB db)      = putWord8 0x02 >> putByteString db@@ -103,10 +84,6 @@ putCommand (COM_STMT_RESET stid) = putWord8 0x1A >> putWord32le stid putCommand _                     = fail "unsupported command" -instance Binary Command where-    get = getCommand-    put = putCommand- -------------------------------------------------------------------------------- --  Prepared statment related @@ -120,10 +97,10 @@  getStmtPrepareOK :: Get StmtPrepareOK getStmtPrepareOK = do-    _ <- getWord8 -- OK byte+    skip 1 -- OK byte     stmtid <- getWord32le     cc <- fromIntegral <$> getWord16le     pc <- fromIntegral <$> getWord16le-    _ <- getWord8 -- reserved+    skip 1 -- reserved     wc <- fromIntegral <$> getWord16le     return (StmtPrepareOK stmtid cc pc wc)
Database/MySQL/Protocol/MySQLValue.hs view
@@ -152,9 +152,7 @@         || t == mySQLTypeNewDate      = feedLenEncBytes t MySQLDate dateParser     | t == mySQLTypeTime         || t == mySQLTypeTime2        = feedLenEncBytes t id $ \ bs ->-                                            if B.null bs-                                            then pure MySQLNull-                                            else if bs `BC.index` 0 == '-'+                                            if bs `B.unsafeIndex` 0 == 45  -- '-'                                                  then MySQLTime 1 <$> timeParser (B.unsafeDrop 1 bs)                                                  else MySQLTime 0 <$> timeParser bs @@ -169,9 +167,7 @@         || t == mySQLTypeVarString         || t == mySQLTypeString       = (if isText then MySQLText . T.decodeUtf8 else MySQLBytes) <$> getLenEncBytes -    | t == mySQLTypeBit               = do len <- getLenEncInt-                                           if len == 0 then pure MySQLNull-                                                       else MySQLBit <$> getBits len+    | t == mySQLTypeBit               = MySQLBit <$> (getBits =<< getLenEncInt)      | otherwise                         = fail $ "Database.MySQL.Protocol.MySQLValue: missing text decoder for " ++ show t   where@@ -192,16 +188,16 @@         (ss, _) <- LexFrac.readDecimal (B.unsafeTail rest')         return (TimeOfDay hh mm ss) -    feedLenEncBytes typ con parser = do-        bs <- getLenEncBytes-        if B.null bs-        then return MySQLNull-        else case parser bs of-            Just v -> return (con v)-            Nothing -> fail $ "Database.MySQL.Protocol.MySQLValue: parsing " ++ show typ ++ " failed, \-                              \input: " ++ BC.unpack bs-    {-# INLINE feedLenEncBytes #-} +feedLenEncBytes :: FieldType -> (t -> b) -> (ByteString -> Maybe t) -> Get b+feedLenEncBytes typ con parser = do+    bs <- getLenEncBytes+    case parser bs of+        Just v -> return (con v)+        Nothing -> fail $ "Database.MySQL.Protocol.MySQLValue: parsing " ++ show typ ++ " failed, \+                          \input: " ++ BC.unpack bs+{-# INLINE feedLenEncBytes #-}+ -------------------------------------------------------------------------------- -- | Text protocol encoder putTextField :: MySQLValue -> Put@@ -246,7 +242,7 @@ getTextRow fs = forM fs $ \ f -> do     p <- lookAhead getWord8     if p == 0xFB-    then getWord8 >> return MySQLNull+    then skip 1 >> return MySQLNull     else getTextField f {-# INLINE getTextRow #-} @@ -341,9 +337,7 @@         || t == mySQLTypeVarString         || t == mySQLTypeString       = if isText then MySQLText . T.decodeUtf8 <$> getLenEncBytes                                                   else MySQLBytes <$> getLenEncBytes-    | t == mySQLTypeBit               = do len <- getLenEncInt-                                           if len == 0 then pure MySQLNull-                                                       else MySQLBit <$> getBits len+    | t == mySQLTypeBit               = MySQLBit <$> (getBits =<< getLenEncInt)     | otherwise                       = fail $ "Database.MySQL.Protocol.MySQLValue:\                                                \ missing binary decoder for " ++ show t   where@@ -363,13 +357,6 @@         ms <- fromIntegral <$> getWord32le :: Get Int         pure $! (realToFrac s + realToFrac ms / 1000000 :: Pico) -    feedLenEncBytes typ con parser = do-        bs <- getLenEncBytes-        case parser bs of-            Just v -> return (con v)-            Nothing -> fail $ "Database.MySQL.Protocol.MySQLValue: \-                              \parsing " ++ show typ ++ " failed, input: " ++ BC.unpack bs-    {-# INLINE feedLenEncBytes #-}  -- | convert a bit sequence to a Word64 --@@ -436,6 +423,7 @@                     putWord16le (fromIntegral yyyy)                     putWord8 (fromIntegral mm)                     putWord8 (fromIntegral dd)+{-# INLINE putBinaryDay #-}  putBinaryTime' :: TimeOfDay -> Put putBinaryTime' (TimeOfDay hh mm ss) = do let s = floor ss@@ -444,6 +432,8 @@                                          putWord8 (fromIntegral mm)                                          putWord8 s                                          putWord32le ms+{-# INLINE putBinaryTime' #-}+ putBinaryTime :: TimeOfDay -> Put putBinaryTime (TimeOfDay hh mm ss) = do let s = floor ss                                             ms = floor $ (ss - realToFrac s) * 1000000@@ -453,6 +443,7 @@                                         putWord8 (fromIntegral mm)                                         putWord8 s                                         putWord32le ms+{-# INLINE putBinaryTime #-}  -------------------------------------------------------------------------------- -- | Binary row decoder@@ -461,7 +452,7 @@ -- getBinaryRow :: [ColumnDef] -> Int -> Get [MySQLValue] getBinaryRow fields flen = do-    _ <- getWord8           -- 0x00+    skip 1           -- 0x00     let maplen = (flen + 7 + 2) `shiftR` 3     nullmap <- getByteString maplen     go fields nullmap 0@@ -482,6 +473,7 @@     {-# INLINE isNull #-} {-# INLINE getBinaryRow #-} +-------------------------------------------------------------------------------- -- | Use 'ByteString' to present a bitmap. -- -- When used for represent bits values, the underlining 'ByteString' follows:
Database/MySQL/Protocol/Packet.hs view
@@ -205,17 +205,12 @@  putLenEncBytes :: ByteString -> Put putLenEncBytes c = do-        let l = B.length c-        putLenEncInt l-        putByteString c+    putLenEncInt (B.length c)+    putByteString c {-# INLINE putLenEncBytes #-}  getLenEncBytes :: Get ByteString-getLenEncBytes = do-    b <- lookAhead getWord8-    if b == 0xfb-    then getWord8 >> return B.empty-    else getLenEncInt >>= getByteString+getLenEncBytes = getLenEncInt >>= getByteString {-# INLINE getLenEncBytes #-}  -- | length encoded int@@ -224,19 +219,19 @@ getLenEncInt = getWord8 >>= word2Len   where     word2Len l-         | l <  0xfb  = return (fromIntegral l)-         | l == 0xfc  = fromIntegral <$> getWord16le-         | l == 0xfd  = fromIntegral <$> getWord24le-         | l == 0xfe  = fromIntegral <$> getWord64le+         | l <  0xFB  = pure (fromIntegral l)+         | l == 0xFC  = fromIntegral <$> getWord16le+         | l == 0xFD  = fromIntegral <$> getWord24le+         | l == 0xFE  = fromIntegral <$> getWord64le          | otherwise = fail $ "invalid length val " ++ show l {-# INLINE getLenEncInt #-}  putLenEncInt:: Int -> Put putLenEncInt x-         | x <  251      = putWord8    (fromIntegral x)-         | x < 65536     = putWord16le (fromIntegral x)-         | x < 16777216  = putWord24le (fromIntegral x)-         | otherwise     = putWord64le (fromIntegral x)+         | x <  251      = putWord8 (fromIntegral x)+         | x < 65536     = putWord8 0xFC >> putWord16le (fromIntegral x)+         | x < 16777216  = putWord8 0xFD >> putWord24le (fromIntegral x)+         | otherwise     = putWord8 0xFE >> putWord64le (fromIntegral x) {-# INLINE putLenEncInt #-}  putWord24le :: Word32 -> Put
README.md view
@@ -11,9 +11,9 @@ Is it fast? ---------- -In short, it's about 2 times slower than pure c/c++, but 5 times faster than old FFI bindings(mysql by Bryan O'Sullivan).+In short, `select`(decode) is about 2 times slower than pure c/c++, `insert` (encode) is about 1.5 times slower than pure c/c++, there're many factors involved(tls, prepared statment, batch using multiple statement): -![result figure](https://github.com/winterland1989/mysql-haskell/blob/master/benchmark/result.png?raw=true)+<img src="https://github.com/winterland1989/mysql-haskell/blob/master/benchmark/result.png?raw=true" width="100%">  Above figures showed the time to: @@ -78,12 +78,14 @@  Please reference `.travis.yml` if you have problems with setting up test environment. -Enter benchmark directory and run `./bench.sh` to benchmark 1) c++ version 2) mysql-haskell 3) FFI version mysql, you may need to modify `bench.sh`(change the include path) to get c++ version compiled, and you may need to adjust rts options `-N` to get best results, with `-N10` on my company's 24-core machine, binary protocol performs almost identical to c version!+Enter benchmark directory and run `./bench.sh` to benchmark 1) c++ version 2) mysql-haskell 3) FFI version mysql, you may need to: -Guide------++ modify `bench.sh`(change the include path) to get c++ version compiled.++ modify `mysql-haskell-bench.cabal`(change the openssl's lib path) to get haskell version compiled.++ setup MySQL's TLS support, modify `MySQLHaskellOpenSSL.hs` to change the CA file's path.++ adjust rts options `-N` to get best results. -Run `cabal haddock` and you will get pretty decent document.+With `-N10` on my company's 24-core machine, binary protocol performs almost identical to c version!  Reference ---------
mysql-haskell.cabal view
@@ -1,5 +1,5 @@ name:                mysql-haskell-version:             0.2.0.0+version:             0.3.0.0 synopsis:            pure haskell MySQL driver description:         pure haskell MySQL driver license:             BSD3@@ -18,8 +18,13 @@   type:     git   location: git://github.com/winterland1989/mysql-haskell.git +flag openssl+  description:  Enable openssl support via @HsOpenSSL@+  default:      True+ library     exposed-modules:    Database.MySQL.Base+                    -- ,   Database.MySQL.TLS                            ,   Database.MySQL.Protocol.Auth                     ,   Database.MySQL.Protocol.Command                     ,   Database.MySQL.Protocol.ColumnDef@@ -30,16 +35,17 @@                     ,   Database.MySQL.BinLogProtocol.BinLogEvent                     ,   Database.MySQL.BinLogProtocol.BinLogValue                     ,   Database.MySQL.BinLogProtocol.BinLogMeta+    if flag(openssl)+        exposed-modules: Database.MySQL.OpenSSL       other-modules:      Database.MySQL.Connection                     ,   Database.MySQL.Query-    build-depends:      base          >= 4.7 && <5-                    ,   transformers  >= 0.3 && < 0.6+    build-depends:      base          >= 4.7 && < 5                     ,   monad-loops   == 0.4.*                     ,   network       >= 2.3 && < 3.0                     ,   io-streams    >= 1.2 && < 2.0-                    ,   tcp-streams   == 0.3.*+                    ,   tcp-streams   == 0.4.*                     ,   wire-streams  >= 0.0.2 && < 0.1                     ,   binary        >= 0.8.4                     ,   bytestring    >= 0.10.2.0@@ -51,7 +57,19 @@                     ,   bytestring-lexing == 0.5.*                     ,   blaze-textual     == 0.2.*                     ,   word24            == 1.*-                    ,   tls               >= 1.3.5 && <1.4+                    -- ,   tls           >=1.3.5 && < 1.4++    if flag(openssl)+        build-depends:      HsOpenSSL      >=0.10.3 && <0.12+        if os(mingw32) || os(windows)+            extra-libraries: eay32, ssl32+        else+            if os(osx)+                extra-libraries: crypto+                extra-lib-dirs: /usr/local/lib+                include-dirs: /usr/local/include+          else+                extra-libraries: crypto      default-language:    Haskell2010     default-extensions:     DeriveDataTypeable
test/BinaryRow.hs view
@@ -11,6 +11,8 @@ import           Database.MySQL.Base import qualified System.IO.Streams   as Stream import           Test.Tasty.HUnit+import qualified Data.Text as T+import qualified Data.ByteString as B  tests :: MySQLConn -> Assertion tests c = do@@ -85,7 +87,7 @@         ]      Stream.skipToEof is-+--------------------------------------------------------------------------------     let bitV = 43744 -- 0b1010101011100000      execute_ c "UPDATE test SET \@@ -155,6 +157,7 @@         , MySQLText "foo,bar"]      Stream.skipToEof is+--------------------------------------------------------------------------------     updStmt <- prepareStmt c             "UPDATE test SET \             \__bit        = ?     ,\@@ -258,7 +261,7 @@         ]      Stream.skipToEof is-+--------------------------------------------------------------------------------     execute_ c "UPDATE test SET \         \__mediumInt  = null         ,\         \__double     = null         ,\@@ -301,7 +304,7 @@         ]      Stream.skipToEof is-+--------------------------------------------------------------------------------     updStmt1 <- prepareStmt c "UPDATE test SET \         \__decimal  = ?         ,\         \__date     = ?         ,\@@ -343,7 +346,7 @@         , MySQLText "foo"         , MySQLText "foo,bar"         ]-+--------------------------------------------------------------------------------     Stream.skipToEof is     execute_ c "UPDATE test SET \         \__time       = '199:59:59'     ,\@@ -359,17 +362,51 @@             ]      Stream.skipToEof is-+--------------------------------------------------------------------------------     updStmt2 <- prepareStmt c "UPDATE test SET \                 \__time       = ?     ,\                 \__year       = ?  WHERE __id=0" -    (_, is) <- queryStmt c selStmt2 [ MySQLTime 0 (TimeOfDay 199 59 59), MySQLYear 0]-    Just v <- Stream.read is+    executeStmt c updStmt2 [ MySQLTime 0 (TimeOfDay 00 00 00), MySQLYear 2055] +    (_, is) <- queryStmt c selStmt2 []+    Just v <- Stream.read is     assertEqual "roundtrip binary protocol 2" v-            [ MySQLTime 0 (TimeOfDay 199 59 59)-            , MySQLYear 0+            [ MySQLTime 0 (TimeOfDay 00 00 00)+            , MySQLYear 2055             ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    execute_ c "UPDATE test SET \+        \__text       = ''     ,\+        \__blob       = ''  WHERE __id=0"++    selStmt3 <- prepareStmt c "SELECT __text, __blob FROM test"+    (_, is) <- queryStmt c selStmt3 []+    Just v <- Stream.read is++    assertEqual "decode binary protocol 3" v+            [ MySQLText ""+            , MySQLBytes ""+            ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    updStmt3 <- prepareStmt c "UPDATE test SET \+                \__text       = ?     ,\+                \__blob       = ?  WHERE __id=0"++    executeStmt c updStmt3+        [ MySQLText (T.replicate 100000 "xyz")+        , MySQLBytes (B.replicate 1000000 64)+        ]++    (_, is) <- queryStmt c selStmt3 []+    Just v <- Stream.read is+    assertEqual "roundtrip binary protocol 3" v+        [ MySQLText (T.replicate 100000 "xyz")+        , MySQLBytes (B.replicate 1000000 64)+        ]      Stream.skipToEof is
test/Main.hs view
@@ -62,8 +62,8 @@                 \__varbinary    VARBINARY(1024),\                 \__tinyblob     TINYBLOB,\                 \__tinytext     TINYTEXT,\-                \__blob         BLOB,\-                \__text         TEXT,\+                \__blob         BLOB(1000000),\+                \__text         TEXT(1000000),\                 \__enum         ENUM('foo', 'bar', 'qux'),\                 \__set          SET('foo', 'bar', 'qux')\                 \) CHARACTER SET utf8"
test/TextRow.hs view
@@ -355,6 +355,20 @@      Stream.skipToEof is +    execute_ c "UPDATE test SET \+        \__text       = ''     ,\+        \__blob       = ''  WHERE __id=0"++    (_, is) <- query_ c "SELECT __text, __blob FROM test"+    Just v <- Stream.read is++    assertEqual "decode text protocol 3" v+            [ MySQLText ""+            , MySQLBytes ""+            ]++    Stream.skipToEof is+     execute c "UPDATE test SET \         \__time       = ?     ,\         \__year       = ?  WHERE __id=0"