{-# LANGUAGE OverloadedStrings #-}
module Test.PgGen
( pgText
, pgDouble
, pgInt32
, pgIdentifier
, pgIdentifierWithPrefix
, pgUTCTime
, pgLocalTime
, pgDay
, pgJSON
)
where
import Data.Int (Int32)
import qualified Data.Text as T
import qualified Data.Time as Time
import qualified Hedgehog as HH
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
pgText :: HH.Range Int -> HH.Gen T.Text
pgText range =
Gen.text range $
Gen.filter (/= '\NUL') Gen.unicode
pgInt32 :: HH.Gen Int32
pgInt32 =
Gen.integral (Range.linearFrom 0 minBound maxBound)
{- |
Produces a double value that can be reliably round tripped through
PostgreSQL, which only allows 15 digits of decimal precision.
Generating doubles naively using 'Gen.double' produces large numbers with
more than 15 digits of precisio, so we use 'encodeFloat' to directly ensure
the precision of large numbers is within PostgreSQL's limit. Precision of
small numbers is enforced by rounding excess digits off.
@since 1.0.0.0
-}
pgDouble :: HH.Gen Double
pgDouble = do
let
-- We use 14 instead of 15 here to allow for the addinitional power
-- of 10 that may end up coming from the mantissa, based on the
-- value of 'maxMantissa' below
maxExpn =
truncate (logBase 2 (10 ^ (14 :: Int)) :: Double)
maxMantissa =
10
mantissa <- Gen.integral (Range.linearFrom 0 (-maxMantissa) maxMantissa)
expn <- Gen.integral (Range.linearFrom 0 (-maxExpn) maxExpn)
pure . roundExcessPrecisionAfterDecimal 15 $ encodeFloat mantissa expn
roundExcessPrecisionAfterDecimal :: Integer -> Double -> Double
roundExcessPrecisionAfterDecimal maxTotalPrecision double =
let
digitsBeforeDecimal =
decimalDigits (truncate double)
digitsAfterDecimal =
maxTotalPrecision - digitsBeforeDecimal
roundingFactor =
10.0 ^^ digitsAfterDecimal
roundDouble =
fromInteger . round
in
if digitsAfterDecimal > 0
then roundDouble (double * roundingFactor) / roundingFactor
else double
decimalDigits :: Integer -> Integer
decimalDigits n =
if abs n < 10
then 1
else 1 + decimalDigits (quot n 10)
pgIdentifier :: HH.Gen String
pgIdentifier =
Gen.string (Range.linear 1 63) $ Gen.element pgIdentifierChars
{- |
Relation names must be unique in PostgreSQL, so we sometimes generate
names with prefixes to avoid conflicts between different types of
relations such as tables and indexes. The min length value allows the
caller to length of the random strings that will be appended to prefix,
which case be useful to avoid conflicts.
@since 1.0.0.0
-}
pgIdentifierWithPrefix :: String -> Int -> HH.Gen String
pgIdentifierWithPrefix prefix minLength =
fmap (prefix <>)
. Gen.string (Range.linear minLength (63 - length prefix))
. Gen.element
$ pgIdentifierChars
{- |
A list of characters to include in identifiers when testing. Not all of these
are valid in unquoted identifiers -- this helps ensure that Orville is
properly quoting ids.
@since 1.0.0.0
-}
pgIdentifierChars :: [Char]
pgIdentifierChars =
['a' .. 'z']
<> ['A' .. 'Z']
<> ['0' .. '9']
<> "{}[]()<>!?:;_~^'%&"
pgUTCTime :: HH.Gen Time.UTCTime
pgUTCTime =
Time.UTCTime <$> pgDay <*> pgDiffTime
pgLocalTime :: HH.Gen Time.LocalTime
pgLocalTime =
Time.LocalTime <$> pgDay <*> pgTimeOfDay
pgDay :: HH.Gen Time.Day
pgDay = do
year <- Gen.integral (Range.linearFrom 2000 0 3000)
month <- Gen.integral (Range.constant 1 12)
day <- Gen.integral (Range.constant 1 (Time.gregorianMonthLength year month))
pure (Time.fromGregorian year month day)
pgTimeOfDay :: HH.Gen Time.TimeOfDay
pgTimeOfDay = fmap Time.timeToTimeOfDay pgDiffTime
pgJSON :: HH.Gen T.Text
pgJSON = do
let
alphaNumText :: HH.Range Int -> HH.Gen T.Text
alphaNumText range =
Gen.text range $ Gen.filter (/= '\NUL') Gen.alphaNum
jsonKey <- alphaNumText (Range.constant 0 1024)
jsonValue <- alphaNumText (Range.constant 0 1024)
pure $ "{\"" <> jsonKey <> "\": \"" <> jsonValue <> "\"}"
pgDiffTime :: HH.Gen Time.DiffTime
pgDiffTime =
Time.secondsToDiffTime <$> Gen.integral (Range.constant 0 85399)