packages feed

duckdb-simple-0.1.2.2: test/Properties.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Properties (roundTripTests) where

import Control.Monad (forM)
import Data.Array (Array, elems, listArray)
import qualified Data.ByteString as BS
import Data.Fixed (Fixed (MkFixed))
import Data.Int (Int16, Int32, Int64, Int8)
import qualified Data.Map.Strict as Map
import Data.Proxy (Proxy (..))
import qualified Data.Text as Text
import Data.Time.Calendar (Day (..))
import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, picosecondsToDiffTime)
import Data.Time.LocalTime (LocalTime (..), TimeOfDay (..), timeOfDayToTime)
import Data.UUID (UUID)
import qualified Data.UUID as UUID
import Data.Word (Word16, Word32, Word64, Word8)
import Database.DuckDB.FFI (
    pattern DuckDBTypeBlob,
    pattern DuckDBTypeBoolean,
    pattern DuckDBTypeDouble,
    pattern DuckDBTypeInteger,
    pattern DuckDBTypeVarchar,
 )
import Database.DuckDB.Simple (Connection, close, open, query)
import Database.DuckDB.Simple.FromField (
    BigNum (..),
    BitString (..),
    FieldValue (..),
    FromField,
    LogicalTypeRep (..),
    StructField (..),
    StructValue (..),
    UnionMemberType (..),
    UnionValue (..)
 )
import Database.DuckDB.Simple.ToField (ToField)
import Database.DuckDB.Simple.Types (Null (..), Only (..))
import Numeric.Natural (Natural)
import Test.QuickCheck (
    Gen,
    Property,
    arbitrary,
    arbitrarySizedNatural,
    chooseInt,
    chooseInteger,
    counterexample,
    elements,
    frequency,
    forAllShrink,
    ioProperty,
    scale,
    shrink,
    shrinkIntegral,
    sized,
    suchThat,
    vectorOf,
    (===),
 )
import Test.Tasty (TestTree, testGroup, withResource)
import Test.Tasty.QuickCheck (testProperty)

data SomeRoundTrip
    = forall a.
      (Eq a, Show a, ToField a, FromField a) =>
      SomeRoundTrip String (Proxy a) (Gen a) (a -> [a])

roundTripTests :: TestTree
roundTripTests =
    withResource
        (open ":memory:")
        close
        \getConn ->
            testGroup
                "round-trip properties"
                (fmap (mkRoundTripTest getConn) specs)

mkRoundTripTest :: IO Connection -> SomeRoundTrip -> TestTree
mkRoundTripTest getConn (SomeRoundTrip label proxy gen shrinker) =
            testProperty label $
                forAllShrink gen shrinker (roundTripProperty getConn proxy)

roundTripProperty ::
    forall a.
    (Eq a, Show a, ToField a, FromField a) =>
    IO Connection ->
    Proxy a ->
    a ->
    Property
roundTripProperty getConn _ value =
    ioProperty do
        conn <- getConn
        rows <- query conn "SELECT ?" (Only value)
        case rows of
            [Only out] ->
                pure $
                    counterexample
                        ("expected " ++ show value ++ ", got " ++ show out)
                        (out === value)
            other ->
                fail $
                    "unexpected row count during round-trip: "
                        ++ show (length other)

specs :: [SomeRoundTrip]
specs =
    [ SomeRoundTrip "Null" (Proxy :: Proxy Null) (pure Null) shrinkNone
    , SomeRoundTrip "Bool" (Proxy :: Proxy Bool) (arbitrary :: Gen Bool) shrink
    , SomeRoundTrip "Int" (Proxy :: Proxy Int) (arbitrary :: Gen Int) shrink
    , SomeRoundTrip "Int8" (Proxy :: Proxy Int8) (arbitrary :: Gen Int8) shrink
    , SomeRoundTrip "Int16" (Proxy :: Proxy Int16) (arbitrary :: Gen Int16) shrink
    , SomeRoundTrip "Int32" (Proxy :: Proxy Int32) (arbitrary :: Gen Int32) shrink
    , SomeRoundTrip "Int64" (Proxy :: Proxy Int64) (arbitrary :: Gen Int64) shrink
    , SomeRoundTrip "Integer" (Proxy :: Proxy Integer) (arbitrary :: Gen Integer) shrink
    , SomeRoundTrip "BigNum" (Proxy :: Proxy BigNum) genBigNum shrinkBigNum
    , SomeRoundTrip "Natural" (Proxy :: Proxy Natural) genNatural shrinkNatural
    , SomeRoundTrip "Word" (Proxy :: Proxy Word) (arbitrary :: Gen Word) shrink
    , SomeRoundTrip "Word8" (Proxy :: Proxy Word8) (arbitrary :: Gen Word8) shrink
    , SomeRoundTrip "Word16" (Proxy :: Proxy Word16) (arbitrary :: Gen Word16) shrink
    , SomeRoundTrip "Word32" (Proxy :: Proxy Word32) (arbitrary :: Gen Word32) shrink
    , SomeRoundTrip "Word64" (Proxy :: Proxy Word64) (arbitrary :: Gen Word64) shrink
    , SomeRoundTrip "Float" (Proxy :: Proxy Float) genFiniteFloat shrink
    , SomeRoundTrip "Double" (Proxy :: Proxy Double) genFiniteDouble shrink
    , SomeRoundTrip "String" (Proxy :: Proxy String) genStringNoNul shrinkStringNoNul
    , SomeRoundTrip "Text" (Proxy :: Proxy Text.Text) genTextNoNul shrinkTextNoNul
    , SomeRoundTrip "ByteString" (Proxy :: Proxy BS.ByteString) genByteString shrinkByteString
    , SomeRoundTrip "BitString" (Proxy :: Proxy BitString) genBitString shrinkBitString
    , SomeRoundTrip "UUID" (Proxy :: Proxy UUID) genUUID shrinkNone
    , SomeRoundTrip "Day" (Proxy :: Proxy Day) genDay shrinkDay
    , SomeRoundTrip "TimeOfDay" (Proxy :: Proxy TimeOfDay) genTimeOfDay shrinkTimeOfDay
    , SomeRoundTrip "LocalTime" (Proxy :: Proxy LocalTime) genLocalTime shrinkLocalTime
    , SomeRoundTrip "UTCTime" (Proxy :: Proxy UTCTime) genUTCTime shrinkUTCTime
    , SomeRoundTrip "Maybe Int" (Proxy :: Proxy (Maybe Int)) genMaybeInt shrinkMaybeInt
    , SomeRoundTrip "Array Int Int" (Proxy :: Proxy (Array Int Int)) genIntArray shrinkIntArray
    , SomeRoundTrip "StructValue FieldValue" (Proxy :: Proxy (StructValue FieldValue)) genStructValue shrinkStructValue
    , SomeRoundTrip "UnionValue FieldValue" (Proxy :: Proxy (UnionValue FieldValue)) genUnionValue shrinkNone
    ]

shrinkNone :: a -> [a]
shrinkNone _ = []

genNatural :: Gen Natural
genNatural = fromInteger <$> arbitrarySizedNatural

shrinkNatural :: Natural -> [Natural]
shrinkNatural n = fromInteger <$> shrinkIntegral (toInteger n)

genBigNum :: Gen BigNum
genBigNum = BigNum <$> scale (\s -> s * 8) (arbitrary :: Gen Integer)

shrinkBigNum :: BigNum -> [BigNum]
shrinkBigNum (BigNum n) = BigNum <$> shrinkIntegral n

genFiniteFloat :: Gen Float
genFiniteFloat =
    (arbitrary :: Gen Float)
        `suchThat` \x -> not (isNaN x || isInfinite x)

genFiniteDouble :: Gen Double
genFiniteDouble =
    (arbitrary :: Gen Double)
        `suchThat` \x -> not (isNaN x || isInfinite x)

genStringNoNul :: Gen String
genStringNoNul =
    sized \n -> do
        len <- chooseInt (0, max 0 (min n 32))
        vectorOf len genCharNoNul

shrinkStringNoNul :: String -> [String]
shrinkStringNoNul =
    filter (all (/= '\0')) . shrink

genCharNoNul :: Gen Char
genCharNoNul =
    (arbitrary :: Gen Char) `suchThat` (/= '\0')

genTextNoNul :: Gen Text.Text
genTextNoNul = Text.pack <$> genStringNoNul

shrinkTextNoNul :: Text.Text -> [Text.Text]
shrinkTextNoNul txt = Text.pack <$> shrinkStringNoNul (Text.unpack txt)

genByteString :: Gen BS.ByteString
genByteString =
    sized \n -> do
        len <- chooseInt (0, max 0 (min n 64))
        BS.pack <$> vectorOf len (arbitrary :: Gen Word8)

shrinkByteString :: BS.ByteString -> [BS.ByteString]
shrinkByteString bs =
    [ BS.pack xs
    | xs <- shrink (BS.unpack bs)
    , length xs <= 64
    ]

genBitString :: Gen BitString
genBitString =
    sized \n -> do
        len <- chooseInt (1, max 1 (min 64 (n * 2)))
        bytes <- vectorOf len (arbitrary :: Gen Word8)
        pure (BitString 0 (BS.pack bytes))

shrinkBitString :: BitString -> [BitString]
shrinkBitString (BitString _ raw) =
    [ BitString 0 (BS.pack xs)
    | xs <- shrink (BS.unpack raw)
    , length xs <= 64
    , not (null xs)
    ]

genUUID :: Gen UUID
genUUID =
    UUID.fromWords
        <$> (arbitrary :: Gen Word32)
        <*> (arbitrary :: Gen Word32)
        <*> (arbitrary :: Gen Word32)
        <*> (arbitrary :: Gen Word32)

genMaybeInt :: Gen (Maybe Int)
genMaybeInt =
    sized \n ->
        let bound = max 1 (min 1000 (n * 10))
         in frequency
                [ (1, pure Nothing)
                , (3, Just <$> chooseInt (-bound, bound))
                ]

shrinkMaybeInt :: Maybe Int -> [Maybe Int]
shrinkMaybeInt Nothing = []
shrinkMaybeInt (Just x) = Nothing : [Just y | y <- shrink x]

genDay :: Gen Day
genDay = ModifiedJulianDay <$> (arbitrary :: Gen Integer)

shrinkDay :: Day -> [Day]
shrinkDay (ModifiedJulianDay d) = ModifiedJulianDay <$> shrink d

genTimeOfDay :: Gen TimeOfDay
genTimeOfDay = do
    micros <- chooseInteger (0, 86400 * 1000000 - 1)
    pure (microsecondsToTimeOfDay micros)

shrinkTimeOfDay :: TimeOfDay -> [TimeOfDay]
shrinkTimeOfDay tod =
    [ microsecondsToTimeOfDay us
    | us <- shrinkIntegral (timeOfDayToMicroseconds tod)
    , us >= 0
    ]

genLocalTime :: Gen LocalTime
genLocalTime = LocalTime <$> genDay <*> genTimeOfDay

shrinkLocalTime :: LocalTime -> [LocalTime]
shrinkLocalTime LocalTime{localDay, localTimeOfDay} =
    [ LocalTime day' localTimeOfDay
    | day' <- shrinkDay localDay
    ]
        ++ [ LocalTime localDay time'
           | time' <- shrinkTimeOfDay localTimeOfDay
           ]

genUTCTime :: Gen UTCTime
genUTCTime = do
    day <- genDay
    micros <- chooseInteger (0, 86400 * 1000000 - 1)
    pure (UTCTime day (picosecondsToDiffTime (micros * 1000000)))

shrinkUTCTime :: UTCTime -> [UTCTime]
shrinkUTCTime (UTCTime day diff) =
    [UTCTime day' diff | day' <- shrinkDay day]
        ++ [UTCTime day diff' | diff' <- shrinkDiff diff]
  where
    shrinkDiff dt =
        [ picosecondsToDiffTime ps
        | ps <- shrinkIntegral (diffTimeToPicoseconds dt)
        , ps >= 0
        , ps `mod` 1000000 == 0
        ]

genIntArray :: Gen (Array Int Int)
genIntArray =
    sized \n -> do
        size <- chooseInt (0, max 0 (min n 8))
        values <- vectorOf size (arbitrary :: Gen Int)
        pure $
            if size == 0
                then listArray (1, 0) []
                else listArray (0, size - 1) values

shrinkIntArray :: Array Int Int -> [Array Int Int]
shrinkIntArray arr =
    let values = elems arr
     in [ listArray (0, length xs - 1) xs
        | xs <- shrink values
        , not (null xs)
        ]
            ++ if null values then [] else [listArray (1, 0) []]

genStructValue :: Gen (StructValue FieldValue)
genStructValue = do
    count <- chooseInt (0, 3)
    if count == 0
        then pure emptyStruct
        else do
            types <- vectorOf count genSimpleType
            values <- forM types simpleTypeValue
            let names = [Text.pack ("field" ++ show i) | i <- [0 .. count - 1]]
                fieldArr =
                    listArray
                        (0, count - 1)
                        [ StructField{structFieldName = name, structFieldValue = val}
                        | (name, val) <- zip names values
                        ]
                typeArr =
                    listArray
                        (0, count - 1)
                        [ StructField{structFieldName = name, structFieldValue = simpleTypeRep ty}
                        | (name, ty) <- zip names types
                        ]
                indexMap = Map.fromList (zip names [0 ..])
            pure
                StructValue
                    { structValueFields = fieldArr
                    , structValueTypes = typeArr
                    , structValueIndex = indexMap
                    }

shrinkStructValue :: StructValue FieldValue -> [StructValue FieldValue]
shrinkStructValue StructValue{structValueFields}
    | null (elems structValueFields) = []
    | otherwise = [emptyStruct]

genUnionValue :: Gen (UnionValue FieldValue)
genUnionValue = do
    memberCount <- chooseInt (1, 4)
    types <- vectorOf memberCount genSimpleType
    let names = [Text.pack ("alt" ++ show i) | i <- [0 .. memberCount - 1]]
        members =
            listArray
                (0, memberCount - 1)
                [ UnionMemberType{unionMemberName = name, unionMemberType = simpleTypeRep ty}
                | (name, ty) <- zip names types
                ]
    tag <- chooseInt (0, memberCount - 1)
    payload <- simpleTypeValue (types !! tag)
    pure
        UnionValue
            { unionValueIndex = fromIntegral tag
            , unionValueLabel = names !! tag
            , unionValuePayload = payload
            , unionValueMembers = members
            }

emptyStruct :: StructValue FieldValue
emptyStruct =
    StructValue
        { structValueFields = listArray (1, 0) []
        , structValueTypes = listArray (1, 0) []
        , structValueIndex = Map.empty
        }

data SimpleType
    = SimpleBool
    | SimpleInt32
    | SimpleDouble
    | SimpleText
    | SimpleBlob
    deriving (Eq, Show)

genSimpleType :: Gen SimpleType
genSimpleType =
    elements [SimpleBool, SimpleInt32, SimpleDouble, SimpleText, SimpleBlob]

simpleTypeRep :: SimpleType -> LogicalTypeRep
simpleTypeRep = \case
    SimpleBool -> LogicalTypeScalar DuckDBTypeBoolean
    SimpleInt32 -> LogicalTypeScalar DuckDBTypeInteger
    SimpleDouble -> LogicalTypeScalar DuckDBTypeDouble
    SimpleText -> LogicalTypeScalar DuckDBTypeVarchar
    SimpleBlob -> LogicalTypeScalar DuckDBTypeBlob

simpleTypeValue :: SimpleType -> Gen FieldValue
simpleTypeValue = \case
    SimpleBool -> FieldBool <$> (arbitrary :: Gen Bool)
    SimpleInt32 -> FieldInt32 <$> (arbitrary :: Gen Int32)
    SimpleDouble -> FieldDouble <$> genFiniteDouble
    SimpleText -> FieldText <$> genTextNoNul
    SimpleBlob -> FieldBlob <$> genByteString

timeOfDayToMicroseconds :: TimeOfDay -> Integer
timeOfDayToMicroseconds tod =
    diffTimeToPicoseconds (timeOfDayToTime tod) `div` 1000000

microsecondsToTimeOfDay :: Integer -> TimeOfDay
microsecondsToTimeOfDay micros =
    let (hours, remHour) = micros `divMod` (3600 * 1000000)
        (minutes, remMinute) = remHour `divMod` (60 * 1000000)
        (seconds, fracMicros) = remMinute `divMod` 1000000
        pico = seconds * 1000000000000 + fracMicros * 1000000
     in TimeOfDay (fromInteger hours) (fromInteger minutes) (MkFixed pico)