packages feed

orville-postgresql-1.0.0.0: test/Test/TableDefinition.hs

module Test.TableDefinition
  ( tableDefinitionTests
  )
where

import qualified Control.Exception as E
import qualified Control.Monad.IO.Class as MIO
import qualified Data.ByteString.Char8 as B8
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.Set as Set
import qualified Data.Text as T
import Hedgehog ((===))
import qualified Hedgehog as HH

import qualified Orville.PostgreSQL as Orville
import qualified Orville.PostgreSQL.Execution.ReturningOption as ReturningOption
import qualified Orville.PostgreSQL.Execution.Select as Select
import qualified Orville.PostgreSQL.Raw.Connection as Conn
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
import qualified Orville.PostgreSQL.Schema.ConstraintDefinition as ConstraintDefinition
import qualified Orville.PostgreSQL.Schema.TableDefinition as TableDefinition

import qualified Test.Entities.Bar as Bar
import qualified Test.Entities.Foo as Foo
import qualified Test.Property as Property
import qualified Test.TestTable as TestTable

tableDefinitionTests :: Orville.ConnectionPool -> Property.Group
tableDefinitionTests pool =
  Property.group
    "TableDefinition"
    [ prop_roundTrip pool
    , prop_readOnlyFields pool
    , prop_primaryKey pool
    , prop_uniqueConstraint pool
    , prop_fieldConstraints
    ]

prop_roundTrip :: Property.NamedDBProperty
prop_roundTrip =
  Property.namedDBProperty "Creates a table that can round trip an entity through it" $ \pool -> do
    originalFoo <- HH.forAll Foo.generate

    let
      insertFoo =
        TableDefinition.mkInsertExpr
          ReturningOption.WithoutReturning
          Foo.table
          (originalFoo :| [])

      selectFoos =
        Select.selectTable Foo.table mempty

    foosFromDB <-
      MIO.liftIO . Orville.runOrville pool $ do
        Orville.withConnection $ \connection -> do
          MIO.liftIO $ TestTable.dropAndRecreateTableDef connection Foo.table
        Orville.executeVoid Orville.InsertQuery insertFoo
        Select.executeSelect selectFoos

    foosFromDB === [originalFoo]

prop_readOnlyFields :: Property.NamedDBProperty
prop_readOnlyFields =
  Property.namedDBProperty "Creates a table that can read from read only fields" $ \pool -> do
    originalBar <- HH.forAll Bar.generate

    let
      insertBar =
        TableDefinition.mkInsertExpr ReturningOption.WithoutReturning Bar.table (originalBar :| [])

      selectBars =
        Select.selectTable Bar.table mempty

    barsFromDB <-
      MIO.liftIO . Orville.runOrville pool $ do
        Orville.withConnection $ \connection -> do
          MIO.liftIO $ TestTable.dropAndRecreateTableDef connection Bar.table
        Orville.executeVoid Orville.InsertQuery insertBar
        Select.executeSelect selectBars

    fmap Bar.barName barsFromDB === [Bar.barName originalBar]

prop_primaryKey :: Property.NamedDBProperty
prop_primaryKey =
  Property.singletonNamedDBProperty "Creates a primary key that rejects duplicate records" $ \pool -> do
    originalFoo <- HH.forAll Foo.generate

    let
      conflictingFoo =
        originalFoo {Foo.fooName = T.reverse $ Foo.fooName originalFoo}

      insertFoos =
        TableDefinition.mkInsertExpr
          ReturningOption.WithoutReturning
          Foo.table
          (originalFoo :| [conflictingFoo])

    result <- MIO.liftIO . E.try . Conn.withPoolConnection pool $ \connection -> do
      TestTable.dropAndRecreateTableDef connection Foo.table
      RawSql.executeVoid connection insertFoos

    case result of
      Right () -> do
        HH.footnote "Expected 'executeVoid' to return failure, but it did not"
        HH.failure
      Left err ->
        Conn.sqlExecutionErrorSqlState err === Just (B8.pack "23505")

prop_uniqueConstraint :: Property.NamedDBProperty
prop_uniqueConstraint =
  Property.singletonNamedDBProperty "Creates a unique constraint that rejects duplicate records" $ \pool -> do
    originalFoo <- HH.forAll Foo.generate

    let
      fooTableWithUniqueNameConstraint =
        Orville.addTableConstraints
          [Orville.uniqueConstraint (Orville.fieldName Foo.fooNameField :| [])]
          Foo.table

      conflictingFoo =
        originalFoo {Foo.fooId = 1 + Foo.fooId originalFoo}

      insertFoos =
        TableDefinition.mkInsertExpr
          ReturningOption.WithoutReturning
          Foo.table
          (originalFoo :| [conflictingFoo])

    result <- MIO.liftIO . E.try . Conn.withPoolConnection pool $ \connection -> do
      TestTable.dropAndRecreateTableDef connection fooTableWithUniqueNameConstraint
      RawSql.executeVoid connection insertFoos

    case result of
      Right () -> do
        HH.footnote "Expected 'executeVoid' to return failure, but it did not"
        HH.failure
      Left err ->
        Conn.sqlExecutionErrorSqlState err === Just (B8.pack "23505")

prop_fieldConstraints :: Property.NamedProperty
prop_fieldConstraints =
  Property.singletonNamedProperty "Includes field constraints in table constraints" $ do
    let
      foreignTableId =
        Orville.unqualifiedNameToTableId "foreign_table"

      foreignFieldName =
        Orville.stringToFieldName "foreign_field"

      fieldWithoutConstraints =
        Orville.integerField "foo"

      fieldWithConstraints =
        Orville.addForeignKeyConstraint foreignTableId foreignFieldName
          . Orville.addUniqueConstraint
          $ fieldWithoutConstraints

      tableWithFieldConstraints =
        Orville.mkTableDefinitionWithoutKey
          "test_table"
          (Orville.marshallField id fieldWithConstraints)

      fieldName =
        Orville.fieldName fieldWithoutConstraints

      tableWithTableConstraints =
        Orville.addTableConstraints
          [ Orville.foreignKeyConstraint
              foreignTableId
              (Orville.foreignReference fieldName foreignFieldName :| [])
          , Orville.uniqueConstraint (fieldName :| [])
          ]
          $ Orville.mkTableDefinitionWithoutKey
            "test_table"
            (Orville.marshallField id fieldWithoutConstraints)

      tableConstraintKeys ::
        Orville.TableDefinition hasKey writeEntity readEntity ->
        Set.Set Orville.ConstraintMigrationKey
      tableConstraintKeys =
        ConstraintDefinition.tableConstraintKeys . Orville.tableConstraints

    tableConstraintKeys tableWithFieldConstraints === tableConstraintKeys tableWithTableConstraints