packages feed

mysql-haskell-1.5.0: test/ZeroDateRoundtrip.hs

-- | Dates a 'Day' cannot hold, read and written back through both protocols.
-- They used to come back clamped to a real date (0000-00-00 as 0000-01-01),
-- and that date was what got written back.
module ZeroDateRoundtrip (tests, typedDecoderTests) where

import           Control.Exception   (try)
import           Control.Monad       (forM_, void)
import qualified Data.ByteString.Lazy as L
import           Database.MySQL.Base
import qualified Database.MySQL.Decoder as Decode
import           QueryApi
import qualified System.IO.Streams   as Stream
import           Test.Tasty
import           Test.Tasty.HUnit
import           TestUser            (withTestConnection)

tests :: QueryApi -> TestTree
tests api = testCase ("zero, partial and invalid dates survive a round trip (" ++ apiName api ++ ")") $
    withTestConnection $ \c -> do
        createZeroDates c

        (_, textRows) <- apiQuery_ api (ColumnCount 3) c "SELECT d, dt, ts FROM zero_dates ORDER BY id"
        textValues <- Stream.toList textRows
        forM_ (zip [11 ..] textValues) $ \(newId, values) ->
            execute c "INSERT INTO zero_dates VALUES (?, ?, ?, ?)" (MySQLInt32 newId : values)

        select <- prepareStmt c "SELECT d, dt, ts FROM zero_dates WHERE id < 10 ORDER BY id"
        (_, binaryRows) <- apiQueryStmt api (ColumnCount 3) c select []
        binaryValues <- Stream.toList binaryRows
        insert <- prepareStmt c "INSERT INTO zero_dates VALUES (?, ?, ?, ?)"
        forM_ (zip [21 ..] binaryValues) $ \(newId, values) ->
            executeStmt c insert (MySQLInt32 newId : values)

        original <- storedAsText c "id < 10"
        assertEqual "values stored" 3 (length original)
        textCopies <- storedAsText c "id BETWEEN 11 AND 19"
        assertEqual "written back through the text protocol" original textCopies
        binaryCopies <- storedAsText c "id BETWEEN 21 AND 29"
        assertEqual "written back through the binary protocol" original binaryCopies

-- | 'Decode.day' and 'Decode.localTime' have no value for such a date, so they
-- name it in the error instead.
typedDecoderTests :: TestTree
typedDecoderTests = testCase "the typed date decoders refuse a date a Day cannot hold" $
    withTestConnection $ \c -> do
        createZeroDates c
        textDay <- try (queryRows_ (Decode.field Decode.day) c "SELECT d FROM zero_dates WHERE id = 2"
                        >>= Stream.toList . snd)
        assertEqual "text DATE" (Left (unrepresentable (RawDate 2024 0 0))) (void textDay)
        select <- prepareStmt c "SELECT dt FROM zero_dates WHERE id = 2"
        binaryTime <- try (queryStmtRows (Decode.field Decode.localTime) c select []
                           >>= Stream.toList . snd)
        assertEqual "binary DATETIME" (Left (unrepresentable (RawDate 2024 5 0))) (void binaryTime)

unrepresentable :: RawDate -> Decode.FieldError
unrepresentable = Decode.FieldError (Decode.ColumnNumber 0) . Decode.FieldUnrepresentableDate

createZeroDates :: MySQLConn -> IO ()
createZeroDates c = do
    void (execute_ c "SET SESSION sql_mode = 'ALLOW_INVALID_DATES'")
    void (execute_ c "CREATE TEMPORARY TABLE zero_dates \
                     \(id INT, d DATE, dt DATETIME, ts TIMESTAMP NULL DEFAULT NULL)")
    void (execute_ c "INSERT INTO zero_dates VALUES \
                     \(1, '0000-00-00', '0000-00-00 00:00:00', '0000-00-00 00:00:00'), \
                     \(2, '2024-00-00', '2024-05-00 13:14:15', NULL), \
                     \(3, '2024-02-30', '2024-02-30 01:02:03', NULL)")

-- | The dates as the server prints them, which no decoding can clamp.
storedAsText :: MySQLConn -> L.ByteString -> IO [[MySQLValue]]
storedAsText c condition = do
    (_, rows) <- query_ c (Query ("SELECT CAST(d AS CHAR), CAST(dt AS CHAR), CAST(ts AS CHAR) \
                                  \FROM zero_dates WHERE " <> condition <> " ORDER BY id"))
    Stream.toList rows