packages feed

mysql-haskell-1.3.2: test/MultipleResults.hs

-- | Statements whose reply holds several results: multi-statement strings and
-- CALLs. Whatever a function cannot hand back must still be read off the
-- connection, or the next query receives the previous one's leftovers.
module MultipleResults (tests) where

import           Control.Exception (try)
import           Data.Int          (Int64)
import           Database.MySQL.Base
import qualified System.IO.Streams as Stream
import           System.Timeout    (timeout)
import           Test.Tasty
import           Test.Tasty.HUnit

tests :: TestTree
tests = testGroup "statements with several results"
    [ testCase "query_ with two INSERTs" $ withTable $ \c -> do
        (_, rows) <- query_ c
            "INSERT INTO several_results VALUES (1); INSERT INTO several_results VALUES (2)"
        Stream.skipToEof rows
        assertRowCount c 2
    , testCase "execute_ with two INSERTs" $ withTable $ \c -> do
        _ <- execute_ c
            "INSERT INTO several_results VALUES (1); INSERT INTO several_results VALUES (2)"
        assertRowCount c 2
    , testCase "execute_ with a SELECT" $ withTable $ \c ->
        assertExtraResultSets c (execute_ c "SELECT COUNT(*) FROM several_results")
    , testCase "executeStmt with a SELECT" $ withTable $ \c -> do
        stmt <- prepareStmt c "SELECT COUNT(*) FROM several_results"
        assertExtraResultSets c (executeStmt c stmt [])
    , testCase "execute_ with a CALL returning one result set" $ withTable $ \c -> do
        _ <- execute_ c "DROP PROCEDURE IF EXISTS one_result_set"
        _ <- execute_ c "CREATE PROCEDURE one_result_set() SELECT COUNT(*) FROM several_results"
        assertExtraResultSets c (execute_ c "CALL one_result_set()")
    , testCase "query_ with a CALL returning one result set" $ withTable $ \c -> do
        _ <- execute_ c "DROP PROCEDURE IF EXISTS one_result_set"
        _ <- execute_ c "CREATE PROCEDURE one_result_set() SELECT COUNT(*) FROM several_results"
        (_, rows) <- query_ c "CALL one_result_set()"
        counts <- Stream.toList rows
        assertEqual "rows of the procedure's SELECT" [[MySQLInt64 0]] counts
        assertRowCount c 0
    , testCase "query_ with an INSERT before a SELECT" $ withTable $ \c -> do
        (_, rows) <- query_ c
            "INSERT INTO several_results VALUES (1); SELECT COUNT(*) FROM several_results"
        counts <- Stream.toList rows
        assertEqual "rows of the SELECT" [[MySQLInt64 1]] counts
        assertRowCount c 1
    , testCase "query_ with two SELECTs" $ withTable $ \c -> do
        (_, rows) <- query_ c
            "SELECT COUNT(*) FROM several_results; SELECT COUNT(*) FROM several_results"
        outcome <- try (Stream.toList rows)
        case outcome of
            Left ExtraResultSets -> pure ()
            Right _ -> assertFailure "the second SELECT's result set went unreported"
        again <- Stream.read rows
        assertEqual "reading the finished rows again" Nothing again
        assertRowCount c 0
    , testCase "query_ with a failing statement after a SELECT" $ withTable $ \c -> do
        (_, rows) <- query_ c
            "SELECT COUNT(*) FROM several_results; SELECT * FROM no_such_table"
        assertERRException c (Stream.toList rows)
    , testCase "execute_ with a failing statement after a SELECT" $ withTable $ \c ->
        assertERRException c
            (execute_ c "SELECT COUNT(*) FROM several_results; SELECT * FROM no_such_table")
    ]

-- | Generous for a few statements on an empty temporary table, and short enough
-- that a hang fails the suite instead of stalling CI.
replyTimeLimitMicroseconds :: Int
replyTimeLimitMicroseconds = 10000000

-- | Runs a case on a fresh connection with an empty temporary table.
withTable :: (MySQLConn -> Assertion) -> Assertion
withTable body = do
    (_, c) <- connectDetail defaultConnectInfo
        { ciUser = "testMySQLHaskell"
        , ciDatabase = "testMySQLHaskell"
        }
    _ <- execute_ c "CREATE TEMPORARY TABLE several_results (__id INT)"
    finished <- timeout replyTimeLimitMicroseconds (body c)
    case finished of
        Nothing -> assertFailure "blocked for 10 s waiting for a reply"
        Just () -> close c

-- | The statement must raise 'ExtraResultSets' and leave the connection in step.
assertExtraResultSets :: MySQLConn -> IO OK -> Assertion
assertExtraResultSets c runStatement = do
    outcome <- try runStatement
    case outcome of
        Left ExtraResultSets -> pure ()
        Right _ -> assertFailure "the result set went unreported"
    assertRowCount c 0

-- | The failing statement's error must come from this call, not the next one.
assertERRException :: MySQLConn -> IO a -> Assertion
assertERRException c runStatement = do
    outcome <- try runStatement
    case outcome of
        Left (ERRException _) -> pure ()
        Right _ -> assertFailure "the failing statement's error went unreported"
    assertRowCount c 0

-- | The connection must be back in step: a fresh query gets its own rows.
assertRowCount :: MySQLConn -> Int64 -> Assertion
assertRowCount c expected = do
    (_, rows) <- query_ c "SELECT COUNT(*) FROM several_results"
    counts <- Stream.toList rows
    assertEqual "rows of the next query" [[MySQLInt64 expected]] counts