orville-postgresql-1.0.0.0: test/Test/FieldDefinition.hs
module Test.FieldDefinition
( fieldDefinitionTests
)
where
import qualified Control.Exception as E
import qualified Data.ByteString.Char8 as B8
import qualified Data.Maybe as Maybe
import qualified Data.String as String
import qualified Data.Text as T
import qualified Data.UUID as UUID
import Hedgehog ((===))
import qualified Hedgehog as HH
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import qualified Orville.PostgreSQL as Orville
import qualified Orville.PostgreSQL.Execution as Execution
import qualified Orville.PostgreSQL.Expr as Expr
import qualified Orville.PostgreSQL.Marshall as Marshall
import qualified Orville.PostgreSQL.Raw.Connection as Conn
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue
import Test.Expr.TestSchema (sqlRowsToText)
import qualified Test.PgGen as PgGen
import qualified Test.Property as Property
fieldDefinitionTests :: Orville.ConnectionPool -> Property.Group
fieldDefinitionTests pool =
Property.group "FieldDefinition" $
integerField pool
<> bigIntegerField pool
<> doubleField pool
<> booleanField pool
<> unboundedTextField pool
<> boundedTextField pool
<> fixedTextField pool
<> textSearchVectorField pool
<> uuidField pool
<> dateField pool
<> utcTimestampField pool
<> localTimestampField pool
<> jsonbField pool
integerField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
integerField pool =
testFieldProperties pool "integerField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.integerField "foo"
, roundTripDefaultValueTests = [RoundTripDefaultTest Marshall.integerDefault]
, roundTripGen = PgGen.pgInt32
}
bigIntegerField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
bigIntegerField pool =
testFieldProperties pool "bigIntegerField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.bigIntegerField "foo"
, roundTripDefaultValueTests = [RoundTripDefaultTest Marshall.bigIntegerDefault]
, roundTripGen = Gen.integral (Range.linearFrom 0 minBound maxBound)
}
doubleField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
doubleField pool =
testFieldProperties pool "doubleField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.doubleField "foo"
, roundTripDefaultValueTests = [RoundTripDefaultTest Marshall.doubleDefault]
, roundTripGen = PgGen.pgDouble
}
booleanField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
booleanField pool =
testFieldProperties pool "booleanField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.booleanField "foo"
, roundTripDefaultValueTests = [RoundTripDefaultTest Marshall.booleanDefault]
, roundTripGen = Gen.bool
}
unboundedTextField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
unboundedTextField pool =
testFieldProperties pool "unboundedTextField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.unboundedTextField "foo"
, roundTripDefaultValueTests = [RoundTripDefaultTest Marshall.textDefault]
, roundTripGen = PgGen.pgText (Range.constant 0 1024)
}
boundedTextField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
boundedTextField pool =
testFieldProperties pool "boundedTextField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.boundedTextField "foo" 4
, roundTripDefaultValueTests = [RoundTripDefaultTest Marshall.textDefault]
, roundTripGen = PgGen.pgText (Range.constant 0 4)
}
fixedTextField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
fixedTextField pool =
testFieldProperties pool "fixedTextField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.fixedTextField "foo" 4
, roundTripDefaultValueTests = [RoundTripDefaultTest Marshall.textDefault]
, roundTripGen = PgGen.pgText (Range.constant 4 4)
}
textSearchVectorField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
textSearchVectorField pool =
testFieldProperties pool "textSearchVectorField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.textSearchVectorField "foo"
, roundTripDefaultValueTests = []
, roundTripGen = tsVectorGen
}
uuidField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
uuidField pool =
testFieldProperties pool "uuidField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.uuidField "foo"
, roundTripDefaultValueTests = []
, roundTripGen = uuidGen
}
dateField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
dateField pool =
testFieldProperties pool "dateField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.dateField "foo"
, roundTripDefaultValueTests =
[ RoundTripDefaultTest Marshall.dateDefault
, InsertOnlyDefaultTest Marshall.currentDateDefault
]
, roundTripGen = PgGen.pgDay
}
utcTimestampField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
utcTimestampField pool =
testFieldProperties pool "utcTimestampField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.utcTimestampField "foo"
, roundTripDefaultValueTests =
[ RoundTripDefaultTest Marshall.utcTimestampDefault
, InsertOnlyDefaultTest Marshall.currentUTCTimestampDefault
]
, roundTripGen = PgGen.pgUTCTime
}
localTimestampField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
localTimestampField pool =
testFieldProperties pool "localTimestampField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.localTimestampField "foo"
, roundTripDefaultValueTests =
[ RoundTripDefaultTest Marshall.localTimestampDefault
, InsertOnlyDefaultTest Marshall.currentLocalTimestampDefault
]
, roundTripGen = PgGen.pgLocalTime
}
jsonbField :: Orville.ConnectionPool -> [(HH.PropertyName, HH.Property)]
jsonbField pool =
testFieldProperties pool "jsonbField" $
FieldDefinitionTest
{ roundTripFieldDef = Marshall.jsonbField "foo"
, roundTripDefaultValueTests = []
, roundTripGen = PgGen.pgJSON
}
testFieldProperties ::
(Show a, Eq a) =>
Orville.ConnectionPool ->
String ->
FieldDefinitionTest a ->
[(HH.PropertyName, HH.Property)]
testFieldProperties pool fieldDefName roundTripTest =
( ( String.fromString (fieldDefName <> " - can round trip values (not null)")
, HH.property $ runRoundTripTest pool roundTripTest
)
: ( String.fromString (fieldDefName <> " - can round trip values (nullable)")
, HH.property $ runNullableRoundTripTest pool roundTripTest
)
: ( String.fromString (fieldDefName <> " - cannot insert null values into a not null field")
, Property.singletonProperty $ runNullCounterExampleTest pool roundTripTest
)
: map (testDefaultValueProperties pool fieldDefName roundTripTest) (roundTripDefaultValueTests roundTripTest)
)
testDefaultValueProperties ::
(Show a, Eq a) =>
Orville.ConnectionPool ->
String ->
FieldDefinitionTest a ->
DefaultValueTest a ->
(HH.PropertyName, HH.Property)
testDefaultValueProperties pool fieldDefName roundTripTest defaultValueTest =
case defaultValueTest of
RoundTripDefaultTest mkDefaultValue ->
( String.fromString (fieldDefName <> " - can round trip a value inserted via a column default")
, Property.singletonProperty $
runDefaultValueFieldDefinitionTest pool roundTripTest mkDefaultValue
)
InsertOnlyDefaultTest defaultValue ->
( String.fromString (fieldDefName <> " - can insert an insert-only default value")
, Property.singletonProperty $
runDefaultValueInsertOnlyTest pool roundTripTest defaultValue
)
-- This generator generates alphanumeric values currently because of syntax
-- issues with random characters being generated. There is a story to built
-- a better Haskell representation of TextSearchVector, which presumably will
-- help fix this.
tsVectorGen :: HH.Gen T.Text
tsVectorGen = do
text <- Gen.text (Range.linear 1 1024) Gen.alphaNum
pure $ T.concat [T.pack "'", text, T.pack "'"]
uuidGen :: HH.Gen UUID.UUID
uuidGen =
UUID.fromWords
<$> Gen.word32 Range.linearBounded
<*> Gen.word32 Range.linearBounded
<*> Gen.word32 Range.linearBounded
<*> Gen.word32 Range.linearBounded
data FieldDefinitionTest a = FieldDefinitionTest
{ roundTripFieldDef :: Marshall.FieldDefinition Marshall.NotNull a
, roundTripDefaultValueTests :: [DefaultValueTest a]
, roundTripGen :: HH.Gen a
}
data DefaultValueTest a
= RoundTripDefaultTest (a -> Marshall.DefaultValue a)
| InsertOnlyDefaultTest (Marshall.DefaultValue a)
runRoundTripTest :: (Show a, Eq a) => Orville.ConnectionPool -> FieldDefinitionTest a -> HH.PropertyT IO ()
runRoundTripTest pool testCase = do
let
fieldDef = roundTripFieldDef testCase
value <- HH.forAll (roundTripGen testCase)
rows <- HH.evalIO . Conn.withPoolConnection pool $ \connection -> do
dropAndRecreateTestTable fieldDef connection
RawSql.executeVoid connection $
Expr.insertExpr
testTable
Nothing
(Expr.insertSqlValues [[Marshall.fieldValueToSqlValue fieldDef value]])
Nothing
result <-
RawSql.execute connection $
Expr.queryExpr
(Expr.selectClause $ Expr.selectExpr Nothing)
(Expr.selectColumns [Marshall.fieldColumnName fieldDef])
(Just $ Expr.tableExpr (Expr.referencesTable testTable) Nothing Nothing Nothing Nothing Nothing)
Execution.readRows result
let
roundTripResult =
case rows of
[[(_, sqlValue)]] ->
Marshall.fieldValueFromSqlValue fieldDef sqlValue
_ ->
Left ("Expected one row with one value in results, but got: " ++ show (sqlRowsToText rows))
roundTripResult === Right value
runNullableRoundTripTest :: (Show a, Eq a) => Orville.ConnectionPool -> FieldDefinitionTest a -> HH.PropertyT IO ()
runNullableRoundTripTest pool testCase = do
let
fieldDef = Marshall.nullableField (roundTripFieldDef testCase)
value <-
HH.forAll $
Gen.frequency
[ (1, pure Nothing)
, (3, Just <$> roundTripGen testCase)
]
HH.cover 1 (String.fromString "Nothing") (Maybe.isNothing value)
HH.cover 20 (String.fromString "Just") (Maybe.isJust value)
rows <- HH.evalIO . Conn.withPoolConnection pool $ \connection -> do
dropAndRecreateTestTable fieldDef connection
RawSql.executeVoid connection $
Expr.insertExpr
testTable
Nothing
(Expr.insertSqlValues [[Marshall.fieldValueToSqlValue fieldDef value]])
Nothing
result <-
RawSql.execute connection $
Expr.queryExpr
(Expr.selectClause $ Expr.selectExpr Nothing)
(Expr.selectColumns [Marshall.fieldColumnName fieldDef])
(Just $ Expr.tableExpr (Expr.referencesTable testTable) Nothing Nothing Nothing Nothing Nothing)
Execution.readRows result
let
roundTripResult =
case rows of
[[(_, sqlValue)]] ->
Marshall.fieldValueFromSqlValue fieldDef sqlValue
_ ->
Left ("Expected one row with one value in results, but got: " ++ show (sqlRowsToText rows))
roundTripResult === Right value
runNullCounterExampleTest :: Orville.ConnectionPool -> FieldDefinitionTest a -> HH.PropertyT IO ()
runNullCounterExampleTest pool testCase = do
result <- HH.evalIO . Conn.withPoolConnection pool $ \connection -> do
let
fieldDef = roundTripFieldDef testCase
E.try $ do
dropAndRecreateTestTable fieldDef connection
RawSql.executeVoid connection $
Expr.insertExpr
testTable
Nothing
(Expr.insertSqlValues [[SqlValue.sqlNull]])
Nothing
case result of
Left err ->
Conn.sqlExecutionErrorSqlState err === Just (B8.pack "23502")
Right _ -> do
HH.footnote "Expected insert query to fail, but it did not"
HH.failure
runDefaultValueFieldDefinitionTest ::
(Show a, Eq a) =>
Orville.ConnectionPool ->
FieldDefinitionTest a ->
(a -> Marshall.DefaultValue a) ->
HH.PropertyT IO ()
runDefaultValueFieldDefinitionTest pool testCase mkDefaultValue = do
value <- HH.forAll (roundTripGen testCase)
let
defaultValue =
mkDefaultValue value
fieldDef =
Marshall.setDefaultValue defaultValue $ roundTripFieldDef testCase
rows <- HH.evalIO . Conn.withPoolConnection pool $ \connection -> do
dropAndRecreateTestTable fieldDef connection
RawSql.executeVoid connection $
Expr.insertExpr
testTable
Nothing
(RawSql.unsafeSqlExpression "VALUES(DEFAULT)")
Nothing
result <-
RawSql.execute connection $
Expr.queryExpr
(Expr.selectClause $ Expr.selectExpr Nothing)
(Expr.selectColumns [Marshall.fieldColumnName fieldDef])
(Just $ Expr.tableExpr (Expr.referencesTable testTable) Nothing Nothing Nothing Nothing Nothing)
Execution.readRows result
let
roundTripResult =
case rows of
[[(_, sqlValue)]] ->
Marshall.fieldValueFromSqlValue fieldDef sqlValue
_ ->
Left ("Expected one row with one value in results, but got: " ++ show (sqlRowsToText rows))
roundTripResult === Right value
runDefaultValueInsertOnlyTest ::
Orville.ConnectionPool ->
FieldDefinitionTest a ->
Marshall.DefaultValue a ->
HH.PropertyT IO ()
runDefaultValueInsertOnlyTest pool testCase defaultValue =
HH.evalIO . Conn.withPoolConnection pool $ \connection -> do
let
fieldDef =
Marshall.setDefaultValue defaultValue $ roundTripFieldDef testCase
dropAndRecreateTestTable fieldDef connection
RawSql.executeVoid connection $
Expr.insertExpr
testTable
Nothing
(RawSql.unsafeSqlExpression "VALUES(DEFAULT)")
Nothing
testTable :: Expr.Qualified Expr.TableName
testTable =
Expr.qualifyTable Nothing (Expr.tableName "field_definition_test")
dropAndRecreateTestTable :: Marshall.FieldDefinition nullability a -> Orville.Connection -> IO ()
dropAndRecreateTestTable fieldDef connection = do
RawSql.executeVoid connection (RawSql.fromString "DROP TABLE IF EXISTS " <> RawSql.toRawSql testTable)
RawSql.executeVoid connection $
Expr.createTableExpr testTable [Marshall.fieldColumnDefinition fieldDef] Nothing []