packages feed

mysql-haskell 1.1.8 → 1.2.0

raw patch · 15 files changed

+467/−68 lines, 15 filesdep ~basedep ~bytestringdep ~io-streamsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base, bytestring, io-streams, text, time, vector

API changes (from Hackage documentation)

+ Database.MySQL.Connection: AuthException :: String -> AuthException
+ Database.MySQL.Connection: completeAuth :: InputStream Packet -> (Packet -> IO ()) -> ByteString -> Packet -> (Word8 -> ByteString -> (Packet -> IO ()) -> InputStream Packet -> IO ()) -> IO ()
+ Database.MySQL.Connection: data AuthException
+ Database.MySQL.Connection: instance GHC.Exception.Type.Exception Database.MySQL.Connection.AuthException
+ Database.MySQL.Connection: instance GHC.Show.Show Database.MySQL.Connection.AuthException
+ Database.MySQL.Connection: plainFullAuth :: Word8 -> ByteString -> (Packet -> IO ()) -> InputStream Packet -> IO ()
+ Database.MySQL.Connection: scrambleForPlugin :: ByteString -> ByteString -> ByteString -> ByteString
+ Database.MySQL.Connection: scrambleSHA1 :: ByteString -> ByteString -> ByteString
+ Database.MySQL.Connection: scrambleSHA256 :: ByteString -> ByteString -> ByteString
+ Database.MySQL.Protocol.Auth: [authPlugin] :: Auth -> !ByteString
+ Database.MySQL.Protocol.Packet: isAuthMoreData :: Packet -> Bool
+ Database.MySQL.Protocol.Packet: isAuthSwitch :: Packet -> Bool
- Database.MySQL.Protocol.Auth: Auth :: !Word32 -> !Word32 -> !Word8 -> !ByteString -> !ByteString -> !ByteString -> Auth
+ Database.MySQL.Protocol.Auth: Auth :: !Word32 -> !Word32 -> !Word8 -> !ByteString -> !ByteString -> !ByteString -> !ByteString -> Auth

Files

ChangeLog.md view
@@ -1,6 +1,22 @@ # Revision history for mysql-haskell -## 1.1.8 +## 1.2.0 -- 2026.03.07++ Add support for  caching_sha2_password authentiation.++## 1.1.9 -- 2026.03.07++ Fix binary protocol error 1210 on modern MySQL/MariaDB:+  `MYSQL_TYPE_BIT` and `MYSQL_TYPE_YEAR` are not valid parameter types+  for `COM_STMT_EXECUTE`. Send `MySQLBit` as `MYSQL_TYPE_LONGLONG`+  (unsigned, little-endian) and `MySQLYear` as `MYSQL_TYPE_SHORT`+  (unsigned, 2-byte integer) instead.++ Fix `MySQLYear` binary encoding: was sending a length-encoded string+  which caused data misalignment for all subsequent parameters.++ Fix version detection for MariaDB 10+/11+ (fractional seconds support).++ Fix CI: enable binary logging and grant binlog privileges for+  integration tests on MariaDB.++ Add roundtrip integration tests for `MySQLBit` and `MySQLYear`.++## 1.1.8 + bump constraints  ## 1.1.7 -- 2025.08.23 
mysql-haskell.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.4 name:               mysql-haskell-version:            1.1.8+version:            1.2.0 synopsis:           pure haskell MySQL driver description:        pure haskell MySQL driver. license:            BSD-3-Clause@@ -123,20 +123,13 @@   other-modules:     Aeson     AesonBP-    BinaryRow-    BinaryRowNew-    BinLog-    BinLogNew-    ExecuteMany     JSON-    MysqlTests     QC.ByteString     QC.Combinator     QC.Common     Orphans+    Sha256Scramble     TCPStreams-    TextRow-    TextRowNew     Word24    hs-source-dirs:     test@@ -154,9 +147,9 @@     quickcheck-instances < 1.0,     scientific >=0.3.0,     tasty >=0.11 && <2.0,+    tasty-expected-failure < 1.0,     tasty-hunit < 1.0,     tasty-quickcheck >=0.8 && < 1.0,-    tasty-expected-failure  < 1.0,     text,     time,     unordered-containers < 1.0,@@ -167,6 +160,42 @@     OverloadedStrings    ghc-options:        -threaded ++test-suite integration+  type:               exitcode-stdio-1.0+  main-is:            Integration.hs+  other-modules:+    BinaryRow+    BinaryRowNew+    BinLog+    BinLogNew+    CachingSha2+    ExecuteMany+    MysqlTests+    RoundtripBit+    RoundtripYear+    SelectOne+    TextRow+    TextRowNew++  hs-source-dirs:     test+  build-depends:+    base,+    bytestring >=0.10,+    io-streams,+    mysql-haskell,+    tasty >=0.11 && <2.0,+    tasty-hunit < 1.0,+    text,+    time,+    vector++  default-extensions:+    MultiWayIf+    OverloadedStrings++  ghc-options:        -threaded+  default-language:   Haskell2010  benchmark binary-parsers-bench   import: common-options
src/Database/MySQL/Connection.hs view
@@ -14,7 +14,9 @@  -} -module Database.MySQL.Connection where+module Database.MySQL.Connection+    ( module Database.MySQL.Connection+    ) where  import           Control.Exception               (Exception, bracketOnError,                                                   throwIO, catch, SomeException)@@ -124,18 +126,16 @@         let auth = mkAuth db user pass charset greet         write c $ encodeToPacket 1 auth         q <- readPacket is'-        if isOK q-        then do-            consumed <- newIORef True-            let waitNotMandatoryOK = catch-                    (void (waitCommandReply is'))           -- server will either reply an OK packet-                    ((\ _ -> return ()) :: SomeException -> IO ())   -- or directy close the connection-                conn = MySQLConn is'-                    (write c)-                    (writeCommand COM_QUIT (write c) >> waitNotMandatoryOK >> TCP.close c)-                    consumed-            return (greet, conn)-        else TCP.close c >> decodeFromPacket q >>= throwIO . ERRException+        completeAuth is' (write c) pass q plainFullAuth+        consumed <- newIORef True+        let waitNotMandatoryOK = catch+                (void (waitCommandReply is'))           -- server will either reply an OK packet+                ((\ _ -> return ()) :: SomeException -> IO ())   -- or directy close the connection+            conn = MySQLConn is'+                (write c)+                (writeCommand COM_QUIT (write c) >> waitNotMandatoryOK >> TCP.close c)+                consumed+        return (greet, conn)      connectWithBufferSize h p bs = TCP.connectSocket h p >>= TCP.socketToConnection bs     write c a = TCP.send c $ Binary.runPut . Binary.put $ a@@ -143,18 +143,90 @@ mkAuth :: ByteString -> ByteString -> ByteString -> Word8 -> Greeting -> Auth mkAuth db user pass charset greet =     let salt = greetingSalt1 greet `B.append` greetingSalt2 greet-        scambleBuf = scramble salt pass-    in Auth clientCap clientMaxPacketSize charset user scambleBuf db-  where-    scramble :: ByteString -> ByteString -> ByteString-    scramble salt pass'-        | B.null pass' = B.empty-        | otherwise   = B.pack (B.zipWith xor sha1pass withSalt)-        where sha1pass = sha1 pass'-              withSalt = sha1 (salt `B.append` sha1 sha1pass)+        plugin = greetingAuthPlugin greet+        scambleBuf = scrambleForPlugin plugin salt pass+    in Auth clientCap clientMaxPacketSize charset user scambleBuf db plugin -    sha1 :: ByteString -> ByteString-    sha1 = BA.convert . (Crypto.hash :: ByteString -> Crypto.Digest Crypto.SHA1)+-- | Dispatch scramble based on the authentication plugin name.+scrambleForPlugin :: ByteString -> ByteString -> ByteString -> ByteString+scrambleForPlugin plugin salt pass+    | plugin == "caching_sha2_password" = scrambleSHA256 salt pass+    | otherwise                         = scrambleSHA1 salt pass++-- | SHA1-based scramble for @mysql_native_password@.+scrambleSHA1 :: ByteString -> ByteString -> ByteString+scrambleSHA1 salt pass+    | B.null pass = B.empty+    | otherwise   = B.pack (B.zipWith xor sha1pass withSalt)+    where sha1pass = sha1 pass+          withSalt = sha1 (salt `B.append` sha1 sha1pass)+          sha1 :: ByteString -> ByteString+          sha1 = BA.convert . (Crypto.hash :: ByteString -> Crypto.Digest Crypto.SHA1)++-- | SHA256-based scramble for @caching_sha2_password@.+-- XOR(SHA256(password), SHA256(SHA256(SHA256(password)) + nonce))+scrambleSHA256 :: ByteString -> ByteString -> ByteString+scrambleSHA256 salt pass+    | B.null pass = B.empty+    | otherwise   = B.pack (B.zipWith xor sha256pass withSalt)+    where sha256pass = sha256 pass+          withSalt   = sha256 (sha256 sha256pass `B.append` salt)+          sha256 :: ByteString -> ByteString+          sha256 = BA.convert . (Crypto.hash :: ByteString -> Crypto.Digest Crypto.SHA256)++-- | Handle multi-step authentication after sending the initial auth response.+--+-- This handles OK, ERR, AuthMoreData (0x01), and AuthSwitchRequest (0xFE).+-- The @fullAuth@ callback is invoked when the server requests full authentication+-- (e.g., cleartext password over TLS).+completeAuth :: InputStream Packet       -- ^ packet input stream+             -> (Packet -> IO ())        -- ^ packet writer+             -> ByteString               -- ^ password+             -> Packet                   -- ^ the first response packet from server+             -> (Word8 -> ByteString -> (Packet -> IO ()) -> InputStream Packet -> IO ())+                                         -- ^ full auth callback (seqN, password, writer, input)+             -> IO ()+completeAuth is writePacket pass p fullAuth+    | isOK p  = return ()+    | isERR p = decodeFromPacket p >>= throwIO . ERRException+    | isAuthMoreData p = do+        let body = L.toStrict (pBody p)+        case B.index body 1 of+            0x03 -> do  -- fast auth success, read the final OK+                ok <- readPacket is+                if isOK ok+                    then return ()+                    else decodeFromPacket ok >>= throwIO . ERRException+            0x04 -> do  -- full auth required+                fullAuth (pSeqN p + 1) pass writePacket is+            _    -> throwIO (UnexpectedPacket p)+    | isAuthSwitch p = do+        -- Parse AuthSwitchRequest: 0xFE, plugin name (NUL), salt+        let body = L.toStrict (pBody p)+            rest = B.drop 1 body  -- skip 0xFE+            (newPlugin, rest') = B.break (== 0) rest+            newSalt = B.drop 1 rest'  -- skip NUL; trailing NUL may or may not be present+            -- Remove trailing NUL from salt if present+            newSalt' = if not (B.null newSalt) && B.last newSalt == 0+                       then B.init newSalt+                       else newSalt+            scrambled = scrambleForPlugin newPlugin newSalt' pass+            seqN = pSeqN p + 1+            responseBody = L.fromStrict scrambled+            responsePacket = Packet (fromIntegral (B.length scrambled)) seqN responseBody+        writePacket responsePacket+        q <- readPacket is+        completeAuth is writePacket pass q fullAuth+    | otherwise = throwIO (UnexpectedPacket p)++-- | Full auth handler for plain TCP connections: throws an error because+-- caching_sha2_password full authentication requires a secure connection.+plainFullAuth :: Word8 -> ByteString -> (Packet -> IO ()) -> InputStream Packet -> IO ()+plainFullAuth _ _ _ _ =+    throwIO $ AuthException "caching_sha2_password full authentication requires a TLS connection. Use Database.MySQL.TLS to connect, or ensure the password verifier is cached (fast auth path)."++data AuthException = AuthException String deriving (Typeable, Show)+instance Exception AuthException  -- | A specialized 'decodeInputStream' here for speed decodeInputStream :: InputStream ByteString -> IO (InputStream Packet)
src/Database/MySQL/Protocol/Auth.hs view
@@ -122,6 +122,7 @@     , authName      :: !ByteString     , authPassword  :: !ByteString     , authSchema    :: !ByteString+    , authPlugin    :: !ByteString     } deriving (Show, Eq)  getAuth :: Get Auth@@ -131,10 +132,10 @@     c <- getWord8     skipN 23     n <- getByteStringNul-    return $ Auth a m c n B.empty B.empty+    return $ Auth a m c n B.empty B.empty B.empty  putAuth :: Auth -> Put-putAuth (Auth cap m c n p s) = do+putAuth (Auth cap m c n p s plugin) = do     putWord32le cap     putWord32le m     putWord8 c@@ -144,6 +145,8 @@     putByteString p     putByteString s     putWord8 0x00+    putByteString plugin+    putWord8 0x00  instance Binary Auth where     get = getAuth@@ -182,6 +185,7 @@                 .|. CLIENT_MULTI_STATEMENTS                 .|. CLIENT_MULTI_RESULTS                 .|. CLIENT_SECURE_CONNECTION+                .|. CLIENT_PLUGIN_AUTH  clientMaxPacketSize :: Word32 clientMaxPacketSize = 0x00ffffff :: Word32
src/Database/MySQL/Protocol/MySQLValue.hs view
@@ -130,14 +130,14 @@ putParamMySQLType (MySQLInt64        _)  = putFieldType mySQLTypeLongLong >> putWord8 0x00 putParamMySQLType (MySQLFloat        _)  = putFieldType mySQLTypeFloat    >> putWord8 0x00 putParamMySQLType (MySQLDouble       _)  = putFieldType mySQLTypeDouble   >> putWord8 0x00-putParamMySQLType (MySQLYear         _)  = putFieldType mySQLTypeYear     >> putWord8 0x80+putParamMySQLType (MySQLYear         _)  = putFieldType mySQLTypeShort    >> putWord8 0x80 putParamMySQLType (MySQLDateTime     _)  = putFieldType mySQLTypeDateTime >> putWord8 0x00 putParamMySQLType (MySQLTimeStamp    _)  = putFieldType mySQLTypeTimestamp>> putWord8 0x00 putParamMySQLType (MySQLDate         _)  = putFieldType mySQLTypeDate     >> putWord8 0x00 putParamMySQLType (MySQLTime       _ _)  = putFieldType mySQLTypeTime     >> putWord8 0x00 putParamMySQLType (MySQLBytes        _)  = putFieldType mySQLTypeBlob     >> putWord8 0x00 putParamMySQLType (MySQLGeometry     _)  = putFieldType mySQLTypeGeometry >> putWord8 0x00-putParamMySQLType (MySQLBit          _)  = putFieldType mySQLTypeBit      >> putWord8 0x00+putParamMySQLType (MySQLBit          _)  = putFieldType mySQLTypeLongLong >> putWord8 0x80 putParamMySQLType (MySQLText         _)  = putFieldType mySQLTypeString   >> putWord8 0x00 putParamMySQLType MySQLNull              = putFieldType mySQLTypeNull     >> putWord8 0x00 @@ -424,9 +424,7 @@ putBinaryField (MySQLInt64      n) = putInt64le n putBinaryField (MySQLFloat      x) = putFloatle x putBinaryField (MySQLDouble     x) = putDoublele x-putBinaryField (MySQLYear       n) = putLenEncBytes . L.toStrict . BB.toLazyByteString $-                                        Textual.integral n  -- this's really weird, it's not documented anywhere-                                                            -- we must encode year into string in binary mode!+putBinaryField (MySQLYear       n) = putWord16le n putBinaryField (MySQLTimeStamp (LocalTime date time)) = do putWord8 11    -- always put full                                                            putBinaryDay date                                                            putBinaryTime' time@@ -440,8 +438,7 @@                                         putBinaryTime t putBinaryField (MySQLGeometry bs)  = putLenEncBytes bs putBinaryField (MySQLBytes  bs)    = putLenEncBytes bs-putBinaryField (MySQLBit    word)  = do putWord8 8     -- always put full-                                        putWord64be word+putBinaryField (MySQLBit    word)  = putWord64le word putBinaryField (MySQLText    t)    = putLenEncBytes (T.encodeUtf8 t) putBinaryField MySQLNull           = return () 
src/Database/MySQL/Protocol/Packet.hs view
@@ -71,6 +71,18 @@ isEOF p = L.index (pBody p) 0 == 0xFE {-# INLINE isEOF #-} +-- | Is this an AuthMoreData packet? (first byte 0x01)+-- Used during authentication handshake for caching_sha2_password.+isAuthMoreData :: Packet -> Bool+isAuthMoreData p = L.index (pBody p) 0 == 0x01+{-# INLINE isAuthMoreData #-}++-- | Is this an AuthSwitchRequest packet? (first byte 0xFE)+-- Same marker as EOF but used in authentication context.+isAuthSwitch :: Packet -> Bool+isAuthSwitch p = L.index (pBody p) 0 == 0xFE+{-# INLINE isAuthSwitch #-}+ -- | Is there more packet to be read? -- --  https://dev.mysql.com/doc/internals/en/status-flags.html
src/Database/MySQL/TLS.hs view
@@ -17,15 +17,22 @@     , module Data.TLSSetting     ) where -import           Control.Exception              (bracketOnError, throwIO)+import           Control.Exception              (bracketOnError, throwIO, catch, SomeException)+import           Control.Monad                  (void) import qualified Data.Binary                    as Binary import qualified Data.Binary.Put                as Binary+import           Data.ByteString                (ByteString)+import qualified Data.ByteString                as B+import qualified Data.ByteString.Lazy           as L+import           Data.Word                      (Word8) import qualified Data.Connection                as Conn import           Data.IORef                     (newIORef) import           Data.TLSSetting import           Database.MySQL.Connection      hiding (connect, connectDetail) import           Database.MySQL.Protocol.Auth+import           Database.MySQL.Protocol.Command import           Database.MySQL.Protocol.Packet+import           System.IO.Streams               (InputStream) import qualified Network.TLS                    as TLS import qualified System.IO.Streams.TCP          as TCP import qualified Data.Connection                as TCP@@ -63,13 +70,29 @@                     let auth = mkAuth db user pass charset greet                     write tc (encodeToPacket 2 auth)                     q <- readPacket tlsIs'-                    if isOK q-                    then do-                        consumed <- newIORef True-                        let conn = MySQLConn tlsIs' (write tc) (TCP.close tc) consumed-                        return (greet, conn)-                    else TCP.close c >> decodeFromPacket q >>= throwIO . ERRException+                    completeAuth tlsIs' (write tc) pass q tlsFullAuth+                    consumed <- newIORef True+                    let waitNotMandatoryOK = catch+                            (void (waitCommandReply tlsIs'))+                            ((\ _ -> return ()) :: SomeException -> IO ())+                        conn = MySQLConn tlsIs' (write tc)+                            (writeCommand COM_QUIT (write tc) >> waitNotMandatoryOK >> TCP.close tc)+                            consumed+                    return (greet, conn)             else error "Database.MySQL.TLS: server doesn't support TLS connection"   where     connectWithBufferSize h p bs = TCP.connectSocket h p >>= TCP.socketToConnection bs     write c a = TCP.send c $ Binary.runPut . Binary.put $ a++-- | Full auth handler for TLS connections: sends the cleartext password+-- as a NUL-terminated packet, which MySQL accepts over encrypted connections.+tlsFullAuth :: Word8 -> ByteString -> (Packet -> IO ()) -> InputStream Packet -> IO ()+tlsFullAuth seqN pass writePacket is = do+    let payload = pass `B.append` "\0"+        body = L.fromStrict payload+        pkt = Packet (fromIntegral (B.length payload)) seqN body+    writePacket pkt+    q <- readPacket is+    if isOK q+        then return ()+        else decodeFromPacket q >>= throwIO . ERRException
+ test/CachingSha2.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE ScopedTypeVariables #-}++module CachingSha2 (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++-- | These tests exercise two different authentication paths in 'completeAuth'.+-- Both tests connect and run @SELECT 1@, but the server-side auth protocol+-- differs based on which MySQL user is used. The users are created in+-- @nix/ci.nix@ (integrated-checks-mysql80) with different auth plugins:+--+-- * @testMySQLHaskellSha2@ — created with @caching_sha2_password@ (the MySQL 8.0+--   default). The client sends a SHA256 scramble, the server responds with+--   AuthMoreData (0x01, byte 2 = 0x03) indicating fast auth success.+--   The CI script pre-caches the verifier via a unix socket login so the+--   fast path is guaranteed.+--+-- * @testMySQLHaskellNative@ — created with @mysql_native_password@. The server+--   advertises @caching_sha2_password@ in its Greeting, so the client initially+--   sends a SHA256 scramble. The server then responds with AuthSwitchRequest+--   (0xFE) telling the client to re-authenticate using @mysql_native_password@+--   with a new salt. The client re-scrambles with SHA1 and sends the response.+tests :: TestTree+tests = testGroup "caching_sha2_password"+    [ testCaseSteps "SHA256 fast auth" $ \step -> do+        step "connecting as testMySQLHaskellSha2 (caching_sha2_password)..."+        (_, c) <- connectDetail defaultConnectInfo+            { ciUser = "testMySQLHaskellSha2"+            , ciPassword = "testPassword123"+            , ciDatabase = "testMySQLHaskell"+            }++        step "executing SELECT 1..."+        (_, is) <- query_ c "SELECT 1"+        Just row <- Stream.read is+        assertBool "SELECT 1 returns 1" (row == [MySQLInt32 1] || row == [MySQLInt64 1])+        Stream.skipToEof is++        close c++    , testCaseSteps "AuthSwitchRequest handling (mysql_native_password on sha2 server)" $ \step -> do+        step "connecting as testMySQLHaskellNative (mysql_native_password)..."+        (_, c) <- connectDetail defaultConnectInfo+            { ciUser = "testMySQLHaskellNative"+            , ciPassword = "nativePass123"+            , ciDatabase = "testMySQLHaskell"+            }++        step "executing SELECT 1..."+        (_, is) <- query_ c "SELECT 1"+        Just row <- Stream.read is+        assertBool "SELECT 1 returns 1" (row == [MySQLInt32 1] || row == [MySQLInt64 1])+        Stream.skipToEof is++        close c+    ]
+ test/Integration.hs view
@@ -0,0 +1,30 @@+module Main (main) where++import qualified Data.ByteString as B+import           Database.MySQL.Base+import           Test.Tasty (defaultMain, testGroup)+import qualified CachingSha2+import qualified MysqlTests+import qualified RoundtripBit+import qualified RoundtripYear+import qualified SelectOne++main :: IO ()+main = do+    -- Connect briefly to detect the server version, so we can conditionally+    -- include tests that only apply to certain servers (e.g. caching_sha2 on MySQL 8.0+).+    (greet, c) <- connectDetail defaultConnectInfo+        {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}+    close c+    let ver = greetingVersion greet+        isMySql80 = "8." `B.isPrefixOf` ver+                 || "9." `B.isPrefixOf` ver+    defaultMain $ testGroup "mysql-integration" $+        [ SelectOne.tests+        , RoundtripBit.tests+        , RoundtripYear.tests+        , MysqlTests.tests+        ]+        -- caching_sha2_password is MySQL 8.0+ only (MariaDB does not support it).+        -- The sha2 test users are created by the nix CI config for the MySQL 8.0 VM.+        ++ [ CachingSha2.tests | isMySql80 ]
test/Main.hs view
@@ -4,7 +4,7 @@ import qualified QC.Combinator as Combinator import Test.Tasty (defaultMain, testGroup) import qualified JSON-import qualified MysqlTests+import qualified Sha256Scramble import qualified Word24 import qualified TCPStreams @@ -17,15 +17,11 @@       , testGroup "combinator" Combinator.tests       , testGroup "JSON" jsonTests       ],-      testGroup "mysql" [-          -- TODO figure out how to run the tests that need a mysql-          -- db-          -- MysqlTests.tests-      ],       testGroup "word24"          Word24.tests       , testGroup "tcp-streams"          [            TCPStreams.tests          ]+      , Sha256Scramble.tests       ]
test/MysqlTests.hs view
@@ -27,9 +27,11 @@     (greet, c) <- connectDetail defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}      let ver = greetingVersion greet-        isNew = "5.6" `B.isPrefixOf` ver-                || "5.7" `B.isPrefixOf` ver  -- from MySQL 5.6.4 and up-                                             -- TIME, DATETIME, and TIMESTAMP support fractional seconds+        isOld = "5.0" `B.isPrefixOf` ver+                || "5.1" `B.isPrefixOf` ver+                || "5.5" `B.isPrefixOf` ver+        isNew = not isOld  -- MySQL 5.6+ and MariaDB 10+ support fractional seconds+                           -- in TIME, DATETIME, and TIMESTAMP columns       execute_ c "DROP TABLE IF EXISTS test"@@ -118,23 +120,24 @@      close c -    (greet, c) <- connectDetail defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}-    execute_ c "SET PASSWORD = PASSWORD('123456abcdefg???')"+    step "testing password change"+    (_, c) <- connectDetail defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}+    -- ALTER USER works on both MariaDB and MySQL 8.0 (SET PASSWORD = PASSWORD('...') was removed in MySQL 8.0)+    execute_ c "ALTER USER 'testMySQLHaskell'@'localhost' IDENTIFIED BY '123456abcdefg???'"     close c -    let loginFailMsg = "ERRException (ERR {errCode = 1045, errState = \"28000\", \-            \errMsg = \"Access denied for user 'testMySQLHaskell'@'localhost' (using password: YES)\"})"--    (greet, c) <- connectDetail+    (_, c) <- connectDetail         defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell", ciPassword = "123456abcdefg???"}-    execute_ c "SET PASSWORD = PASSWORD('')"+    execute_ c "ALTER USER 'testMySQLHaskell'@'localhost' IDENTIFIED BY ''"     close c      catch         (void $ connectDetail                     defaultConnectInfo                         {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell", ciPassword = "wrongPassWord"})-        (\ (e :: ERRException) -> assertEqual "wrong password should fail to login" (show e) loginFailMsg)+        (\ (e :: ERRException) -> do+            let ERRException err = e+            assertEqual "wrong password should fail with error 1045" 1045 (errCode err))    where     resetTestTable c = do
+ test/RoundtripBit.hs view
@@ -0,0 +1,46 @@+module RoundtripBit (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = testCaseSteps "roundtrip MySQLBit" $ \step -> do+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }++    execute_ c "CREATE TEMPORARY TABLE test_bit (__id INT, __val BIT(16))"+    execute_ c "INSERT INTO test_bit VALUES (1, NULL)"++    step "roundtrip MySQLBit via binary protocol"+    updStmt <- prepareStmt c "UPDATE test_bit SET __val = ? WHERE __id = 1"+    selStmt <- prepareStmt c "SELECT __val FROM test_bit WHERE __id = 1"++    let bitVal = 43744 -- 0b1010101011100000+    executeStmt c updStmt [MySQLBit bitVal]++    (_, is) <- queryStmt c selStmt []+    Just [v] <- Stream.read is+    Stream.skipToEof is+    assertEqual "MySQLBit roundtrips through BIT(16)" (MySQLBit bitVal) v++    step "roundtrip MySQLBit zero"+    executeStmt c updStmt [MySQLBit 0]++    (_, is2) <- queryStmt c selStmt []+    Just [v2] <- Stream.read is2+    Stream.skipToEof is2+    assertEqual "MySQLBit 0 roundtrips" (MySQLBit 0) v2++    step "roundtrip MySQLBit max for BIT(16)"+    executeStmt c updStmt [MySQLBit 65535]++    (_, is3) <- queryStmt c selStmt []+    Just [v3] <- Stream.read is3+    Stream.skipToEof is3+    assertEqual "MySQLBit 65535 roundtrips" (MySQLBit 65535) v3++    close c
+ test/RoundtripYear.hs view
@@ -0,0 +1,53 @@+module RoundtripYear (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = testCaseSteps "roundtrip MySQLYear" $ \step -> do+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }++    execute_ c "CREATE TEMPORARY TABLE test_year (__id INT, __val YEAR(4))"+    execute_ c "INSERT INTO test_year VALUES (1, NULL)"++    step "roundtrip MySQLYear via binary protocol"+    updStmt <- prepareStmt c "UPDATE test_year SET __val = ? WHERE __id = 1"+    selStmt <- prepareStmt c "SELECT __val FROM test_year WHERE __id = 1"++    executeStmt c updStmt [MySQLYear 1999]++    (_, is) <- queryStmt c selStmt []+    Just [v] <- Stream.read is+    Stream.skipToEof is+    assertEqual "MySQLYear 1999 roundtrips" (MySQLYear 1999) v++    step "roundtrip MySQLYear min (1901)"+    executeStmt c updStmt [MySQLYear 1901]++    (_, is2) <- queryStmt c selStmt []+    Just [v2] <- Stream.read is2+    Stream.skipToEof is2+    assertEqual "MySQLYear 1901 roundtrips" (MySQLYear 1901) v2++    step "roundtrip MySQLYear max (2155)"+    executeStmt c updStmt [MySQLYear 2155]++    (_, is3) <- queryStmt c selStmt []+    Just [v3] <- Stream.read is3+    Stream.skipToEof is3+    assertEqual "MySQLYear 2155 roundtrips" (MySQLYear 2155) v3++    step "roundtrip MySQLYear zero"+    executeStmt c updStmt [MySQLYear 0]++    (_, is4) <- queryStmt c selStmt []+    Just [v4] <- Stream.read is4+    Stream.skipToEof is4+    assertEqual "MySQLYear 0 roundtrips" (MySQLYear 0) v4++    close c
+ test/SelectOne.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE ScopedTypeVariables #-}++module SelectOne (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = testCaseSteps "select 1" $ \step -> do+    step "connecting..."+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }++    step "executing SELECT 1..."+    (_, is) <- query_ c "SELECT 1"+    Just row <- Stream.read is+    -- MySQL 8.0 returns MySQLInt64 for integer literals, older versions return MySQLInt32+    assertBool "SELECT 1 returns 1" (row == [MySQLInt32 1] || row == [MySQLInt64 1])+    Stream.skipToEof is++    close c
+ test/Sha256Scramble.hs view
@@ -0,0 +1,34 @@+module Sha256Scramble (tests) where++import qualified Data.ByteString as B+import           Database.MySQL.Connection (scrambleSHA256)+import           Test.Tasty+import           Test.Tasty.HUnit++tests :: TestTree+tests = testGroup "SHA256 Scramble"+    [ testCase "empty password returns empty" $ do+        let result = scrambleSHA256 "some_salt" ""+        assertEqual "empty password" B.empty result++    , testCase "non-empty password returns 32 bytes" $ do+        let result = scrambleSHA256 "12345678901234567890" "password"+        assertEqual "scramble length" 32 (B.length result)++    , testCase "different salts produce different scrambles" $ do+        let r1 = scrambleSHA256 "salt1_______________" "password"+            r2 = scrambleSHA256 "salt2_______________" "password"+        assertBool "different salts should produce different results" (r1 /= r2)++    , testCase "different passwords produce different scrambles" $ do+        let salt = "12345678901234567890"+            r1 = scrambleSHA256 salt "password1"+            r2 = scrambleSHA256 salt "password2"+        assertBool "different passwords should produce different results" (r1 /= r2)++    , testCase "scramble is deterministic" $ do+        let salt = "12345678901234567890"+            r1 = scrambleSHA256 salt "testPassword123"+            r2 = scrambleSHA256 salt "testPassword123"+        assertEqual "same inputs produce same output" r1 r2+    ]