mariadb-haskell-0.1.0.0: validation/hs/Validation/Reliability.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Validation.Reliability (cases) where
import Control.Concurrent
import Control.Exception hiding (assert)
import Control.Monad
import qualified Data.Text as T
import Database.MySQL.Base
import qualified Database.MySQL.TLS as TLS
import qualified System.IO.Streams as S
import System.Timeout
import Validation.Common
import qualified Validation.Stage3 as Stage3
import qualified Validation.Stage4 as Stage4
import qualified Validation.Stage6 as Stage6
cases :: [Case]
cases =
[ Case "transaction.commit" "transactions" $ withConn $ \c -> do
exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"
withTransaction c (exec c "INSERT INTO mb_tx VALUES(7)")
equal "commit persisted" 7 =<< scalarInt c "SELECT n FROM mb_tx"
, Case "transaction.rollback" "transactions" $ withConn $ \c -> do
exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"
result <- try (withTransaction c (exec c "INSERT INTO mb_tx VALUES(7)" >> throwIO (userError "rollback-marker"))) :: IO (Either SomeException ())
case result of Left _ -> pure (); Right () -> throwIO (userError "transaction did not raise")
equal "rollback removed insert" 0 =<< scalarInt c "SELECT COUNT(*) FROM mb_tx"
healthy c
, Case "transaction.savepoint" "transactions" $ withConn $ \c -> do
exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"
withTransaction c $ do
exec c "INSERT INTO mb_tx VALUES(7)"
exec c "SAVEPOINT before_second"
exec c "INSERT INTO mb_tx VALUES(8)"
exec c "ROLLBACK TO SAVEPOINT before_second"
equal "savepoint rollback" [[7]] . map (map number) =<< rows c "SELECT n FROM mb_tx"
, Case "transaction.partial-result-exception" "transactions" $ withConn $ \c -> do
exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"
result <- try (withTransaction c $ do
exec c "INSERT INTO mb_tx VALUES(7)"
(_,stream) <- query_ c "SELECT seq FROM seq_1_to_1000"
void (S.read stream)
throwIO (userError "original-application-exception")) :: IO (Either SomeException ())
case result of
Left e -> assert ("original-application-exception" `T.isInfixOf` T.pack (displayException e))
("original error replaced by " ++ displayException e)
Right () -> throwIO (userError "expected application exception")
equal "rollback completed" 0 =<< scalarInt c "SELECT COUNT(*) FROM mb_tx"
, Case "transaction.nested-preserves-outer" "transactions" $ withConn $ \c -> do
exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"
void (try (withTransaction c $ do
exec c "INSERT INTO mb_tx VALUES(7)"
withTransaction c (exec c "INSERT INTO mb_tx VALUES(8)")
throwIO (userError "outer-rollback")) :: IO (Either SomeException ()))
equal "nested call must not implicitly commit outer work" 0 =<< scalarInt c "SELECT COUNT(*) FROM mb_tx"
, Case "transaction.read-committed-no-dirty-read" "transactions" $ withConn $ \a -> withConn $ \b -> do
exec a "DROP TABLE IF EXISTS mb_isolation"
exec a "CREATE TABLE mb_isolation(n INT) ENGINE=InnoDB"
exec b "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
exec a "BEGIN"
exec a "INSERT INTO mb_isolation VALUES(7)"
equal "uncommitted row invisible" 0 =<< scalarInt b "SELECT COUNT(*) FROM mb_isolation"
exec a "COMMIT"
equal "committed row visible" 1 =<< scalarInt b "SELECT COUNT(*) FROM mb_isolation"
exec a "DROP TABLE mb_isolation"
, Case "transaction.lock-timeout-and-reuse" "transactions" $ withConn $ \a -> withConn $ \b -> do
exec a "DROP TABLE IF EXISTS mb_lock"
exec a "CREATE TABLE mb_lock(id INT PRIMARY KEY,n INT) ENGINE=InnoDB"
exec a "INSERT INTO mb_lock VALUES(1,7)"
exec a "BEGIN"; exec a "UPDATE mb_lock SET n=8 WHERE id=1"
exec b "SET innodb_lock_wait_timeout=1"
expectERR [1205] (exec b "UPDATE mb_lock SET n=9 WHERE id=1")
exec a "ROLLBACK"
healthy b
exec a "DROP TABLE mb_lock"
, Case "transaction.deadlock-and-reuse" "transactions" $ withConn $ \a -> withConn $ \b -> do
exec a "DROP TABLE IF EXISTS mb_deadlock"
exec a "CREATE TABLE mb_deadlock(id INT PRIMARY KEY,n INT) ENGINE=InnoDB"
exec a "INSERT INTO mb_deadlock VALUES(1,7),(2,8)"
exec a "BEGIN"; exec b "BEGIN"
exec a "UPDATE mb_deadlock SET n=n+1 WHERE id=1"
exec b "UPDATE mb_deadlock SET n=n+1 WHERE id=2"
done <- newEmptyMVar
_ <- forkIO (try (exec a "UPDATE mb_deadlock SET n=n+1 WHERE id=2") >>= putMVar done)
threadDelay 50000
other <- try (exec b "UPDATE mb_deadlock SET n=n+1 WHERE id=1") :: IO (Either SomeException ())
first <- takeMVar done :: IO (Either SomeException ())
let isDeadlock (Left e)=case fromException e of Just (ERRException err)->errCode err==1213; Nothing->False
isDeadlock _=False
assert (isDeadlock first || isDeadlock other) "no deadlock error was returned"
exec a "ROLLBACK"; exec b "ROLLBACK"
healthy a; healthy b
exec a "DROP TABLE mb_deadlock"
, Case "transaction.ddl-implicit-commit" "transactions" $ withConn $ \c -> do
exec c "DROP TABLE IF EXISTS mb_ddl_commit,mb_ddl_created"
exec c "CREATE TABLE mb_ddl_commit(n INT) ENGINE=InnoDB"
exec c "BEGIN"; exec c "INSERT INTO mb_ddl_commit VALUES(7)"
exec c "CREATE TABLE mb_ddl_created(n INT)"
exec c "ROLLBACK"
equal "MariaDB DDL commits preceding work" 1 =<< scalarInt c "SELECT COUNT(*) FROM mb_ddl_commit"
exec c "DROP TABLE mb_ddl_commit,mb_ddl_created"
, Case "reliability.reset-unread-result" "reliability" $ withConn $ \c -> do
st <- prepareStmt c "SELECT seq FROM seq_1_to_1000"
(_,stream) <- queryStmt c st []
void (S.read stream)
result <- try (resetStmt c st) :: IO (Either SomeException ())
case result of
Left e | Just UnconsumedResultSet <- fromException e -> S.skipToEof stream
Left e -> throwIO e
Right () -> pure ()
healthy c
, Case "reliability.timeout-before-metadata" "reliability" $ withConn $ \c -> do
result <- timeout 20000 (rows c "SELECT SLEEP(0.2)")
equal "query timed out" Nothing result
-- Contract correction approved 9 November 2026: cancellation while the
-- reply is incomplete retires the wire. Reusing it could consume this
-- command's late packets as another command's result. The original
-- baseline retains its historical reuse expectation and failure.
equal "cancelled transport retired" Broken =<< connectionStatus c
rejected <- try (ping c)
case rejected of
Left (ConnectionUnavailable Broken) -> pure ()
Left err -> throwIO err
Right _ -> fail "cancelled connection admitted another command"
withConn healthy
, Case "reliability.server-kill-query" "reliability" $ withConn $ \c -> withConn $ \control -> do
connectionId <- scalarInt c "SELECT CONNECTION_ID()"
(_,stream) <- query_ c "SELECT REPEAT('x',20000),SLEEP(0.01) FROM seq_1_to_10000"
void (S.read stream)
exec control ("KILL QUERY " ++ show connectionId)
expectERR [1317] (S.skipToEof stream)
healthy c
, Case "reliability.peer-disconnect" "reliability" $ withConn $ \c -> withConn $ \control -> do
connectionId <- scalarInt c "SELECT CONNECTION_ID()"
exec control ("KILL CONNECTION " ++ show connectionId)
expectRejection (healthy c)
, Case "reliability.unread-result-guard" "reliability" $ withConn $ \c -> do
(_,stream) <- query_ c "SELECT seq FROM seq_1_to_1000"
result <- try (ping c)
case result of
Left UnconsumedResultSet -> pure ()
Right _ -> throwIO (userError "command allowed while rows remain unread")
S.skipToEof stream
healthy c
, Case "reliability.tls-cancellation-propagates" "reliability" $ withTLS cancellation
, Case "reliability.tcp-cancellation-propagates" "reliability" $ withConn cancellation
, Case "connection.tls-untrusted-ca" "connection" $ do
ci <- config
params <- TLS.makeClientParams TLS.SystemCAStore
expectRejection (withConnect (TLS.connect ci (params,"localhost")) healthy)
, Case "connection.localhost-address-fallback" "connection" $ do
ci <- config
withConnect (connect ci {ciHost="localhost"}) healthy
, Case "types.non-utf8-results" "types" $ withConn $ \c -> do
exec c "SET character_set_results=latin1"
equal "latin1 result" "é" . textOf =<< scalar c "SELECT CONVERT(0xE9 USING latin1)"
, Case "capability.binary-bulk" "capabilities" $ Stage3.bulkFeature
, Case "capability.metadata-cache" "capabilities" $ Stage3.cacheFeature
, Case "capability.extended-metadata" "capabilities" $ Stage3.extendedFeature
, Case "capability.session-tracking" "capabilities" $ Stage4.trackingFeature
, Case "capability.reset-connection" "capabilities" $ Stage4.resetFeature
, Case "capability.progress-reporting" "capabilities" Stage6.progressFeature
, Case "capability.cursor-fetch" "capabilities" Stage6.cursorFeature
, Case "capability.long-data" "capabilities" Stage6.longDataFeature
, Case "capability.compression" "capabilities" Stage6.compressionFeature
]
where
cancellation c = do
done <- newEmptyMVar
thread <- forkIO (try (rows c "SELECT SLEEP(5)") >>= putMVar done)
threadDelay 100000
throwTo thread ThreadKilled
result <- takeMVar done :: IO (Either SomeException [[MySQLValue]])
case result of
Left e | Just ThreadKilled <- fromException e -> pure ()
Left e -> throwIO (userError ("cancellation replaced by " ++ displayException e))
Right _ -> throwIO (userError "cancellation was swallowed")