packages feed

beam-postgres-0.5.6.0: test/Database/Beam/Postgres/Test/Migrate.hs

{-# LANGUAGE LambdaCase #-}
module Database.Beam.Postgres.Test.Migrate where

import Database.Beam
import Database.Beam.Postgres
import Database.Beam.Postgres.Migrate
import Database.Beam.Postgres.PgCrypto (PgCrypto)
import Database.Beam.Postgres.Test
import Database.Beam.Migrate
import Database.Beam.Migrate.Simple

import Data.ByteString (ByteString)
import Data.Int (Int32)
import qualified Data.List.NonEmpty as NE
import Data.Text (Text)

import qualified Database.PostgreSQL.Simple as Pg

import Test.Tasty
import Test.Tasty.HUnit

tests :: IO ByteString -> TestTree
tests postgresConn =
    testGroup "Migration tests"
      [ charWidthVerification postgresConn "VARCHAR" varchar
      , charNoWidthVerification postgresConn "VARCHAR" varchar
      , charWidthVerification postgresConn "CHAR" char
      , charNoWidthVerification postgresConn "CHAR" char
      , extensionVerification postgresConn
      , createTableWithSchemaWorks postgresConn
      , dropSchemaWorks postgresConn
      , indexVerification postgresConn
      , uniqueIndexVerification postgresConn
      , foreignKeyVerification postgresConn
      , foreignKeyOnDeleteCascadeVerification postgresConn
      , foreignKeyActionsWork postgresConn
      ]

data CharT f
    = CharT { vcKey :: C f Text }
      deriving (Generic, Beamable)

instance Table CharT where
    data PrimaryKey CharT f = VcKey (C f Text)
      deriving (Generic, Beamable)

    primaryKey = VcKey . vcKey

data CharDb entity
    = CharDb
    { vcTbl :: entity (TableEntity CharT) }
    deriving (Generic, Database Postgres)

data CryptoDb entity
    = CryptoDb
    { cryptoExtension :: entity (PgExtensionEntity PgCrypto) }
    deriving (Generic, Database Postgres)

-- | Verifies that 'verifySchema' correctly checks the width of
-- @VARCHAR@ or @CHAR@ columns.
charWidthVerification :: IO ByteString -> String -> (Maybe Word -> DataType Postgres Text) -> TestTree
charWidthVerification pgConn tyName charTy =
    testCase ("verifySchema correctly checks width of " ++ tyName ++ "(n) columns (#274)") $ do
      withTestPostgres "db_char_width" pgConn $ \conn -> do
        runBeamPostgres conn $ do
          db <- executeMigration runNoReturn
                  (CharDb <$> createTable "char_test"
                                    (CharT (field "key" (charTy (Just 10)) notNull)))

          res <- verifySchema migrationBackend db

          case res of
            VerificationSucceeded -> return ()
            VerificationFailed failures -> fail ("Verification failed: " ++ show failures)

-- | Verifies that 'verifySchema' correctly checks the width of
-- @VARCHAR@ or @CHAR@ columns without any max width
charNoWidthVerification :: IO ByteString -> String -> (Maybe Word -> DataType Postgres Text) -> TestTree
charNoWidthVerification pgConn tyName charTy =
    testCase ("verifySchema correctly checks width of " ++ tyName ++ " columns (#274)") $ do
      withTestPostgres "db_char_no_width" pgConn $ \conn -> do
        runBeamPostgres conn $ do
          db <- executeMigration runNoReturn
                  (CharDb <$> createTable "char_test"
                                    (CharT (field "key" (charTy Nothing) notNull)))

          res <- verifySchema migrationBackend db

          case res of
            VerificationSucceeded -> return ()
            VerificationFailed failures -> fail ("Verification failed: " ++ show failures)

-- | Verifies that 'verifySchema' correctly checks enabled PgCrypto extension
extensionVerification :: IO ByteString -> TestTree
extensionVerification pgConn =
    testCase "verifySchema correctly checks enabled PgCrypto extension" $
      withTestPostgres "db_extension_pgcrypto" pgConn $ \conn ->
        runBeamPostgres conn $ do
          let migration = CryptoDb <$> pgCreateExtension
          dbBefore <- executeMigration (const $ return ()) migration
          resBefore <- verifySchema migrationBackend dbBefore
          case resBefore of
            VerificationSucceeded -> fail "Verification succeeded before migration when it should have failed"
            VerificationFailed _ -> return ()

          dbAfter <- executeMigration runNoReturn migration
          resAfter <- verifySchema migrationBackend dbAfter
          case resAfter of
            VerificationSucceeded -> return ()
            VerificationFailed failures -> fail ("Verification failed: " ++ show failures)


-- | Verifies that 'createTableWithSchema' correctly creates a table
-- with a schema.
createTableWithSchemaWorks :: IO ByteString -> TestTree
createTableWithSchemaWorks pgConn =
    testCase ("createTableWithSchema works correctly") $ do
      withTestPostgres "create_table_with_schema" pgConn $ \conn -> do
        res <- runBeamPostgres conn $ do
          db <- executeMigration runNoReturn $ do
                  internalSchema <- createDatabaseSchema "internal_schema"
                  (CharDb <$> createTableWithSchema (Just internalSchema) "char_test"
                                    (CharT (field "key" (varchar Nothing) notNull)))

          verifySchema migrationBackend db

        case res of
          VerificationSucceeded -> return ()
          VerificationFailed failures -> fail ("Verification failed: " ++ show failures)


-- | Verifies that creating a schema and dropping it works
dropSchemaWorks :: IO ByteString -> TestTree
dropSchemaWorks pgConn =
    testCase ("dropDatabaseSchema works correctly") $ do
      withTestPostgres "drop_schema" pgConn $ \conn -> do
        runBeamPostgres conn $ do
          db <- executeMigration runNoReturn $ do
                  internalSchema <- createDatabaseSchema "internal_schema"
                  willBeDroppedSchema <- createDatabaseSchema "will_be_dropped"
                  db <- (CharDb <$> createTableWithSchema (Just internalSchema) "char_test"
                                    (CharT (field "key" (varchar Nothing) notNull)))
                  dropDatabaseSchema willBeDroppedSchema
                  pure db

          verifySchema migrationBackend db >>= \case
            VerificationFailed failures -> fail ("Verification failed: " ++ show failures)
            VerificationSucceeded -> pure ()

-- Shared table type for index tests

newtype IdxT f = IdxT
  { _idx_value :: C f Int32
  } deriving (Generic, Beamable)

instance Table IdxT where
  newtype PrimaryKey IdxT f = IdxPk (C f Int32)
    deriving (Generic, Beamable)
  primaryKey = IdxPk . _idx_value

data IdxDb entity = IdxDb
  { _idx_tbl :: entity (TableEntity IdxT)
  } deriving (Generic, Database Postgres)

-- | Verifies that 'verifySchema' correctly detects a secondary index
indexVerification :: IO ByteString -> TestTree
indexVerification pgConn =
    testCase "verifySchema correctly detects a secondary index" $
      withTestPostgres "db_index" pgConn $ \conn -> do
        Pg.execute_ conn "CREATE TABLE idx_tbl (idx_value integer NOT NULL PRIMARY KEY)"
        Pg.execute_ conn "CREATE INDEX idx_tbl_value ON idx_tbl (idx_value)"
        let db :: CheckedDatabaseSettings Postgres IdxDb
            db = defaultMigratableDbSettings `withDbModification`
                  (dbModification @_ @Postgres)
                    { _idx_tbl = addTableIndex "idx_tbl_value" (defaultIndexOptions @PgCommandSyntax)
                                   (\t -> selectorColumnName _idx_value t NE.:| []) }
        runBeamPostgres conn (verifySchema migrationBackend db) >>= \case
          VerificationSucceeded -> return ()
          VerificationFailed failures -> fail ("Verification failed: " ++ show failures)

-- | Verifies that 'verifySchema' correctly detects a UNIQUE secondary index
uniqueIndexVerification :: IO ByteString -> TestTree
uniqueIndexVerification pgConn =
    testCase "verifySchema correctly detects a UNIQUE secondary index" $
      withTestPostgres "db_unique_index" pgConn $ \conn -> do
        Pg.execute_ conn "CREATE TABLE idx_tbl (idx_value integer NOT NULL PRIMARY KEY)"
        Pg.execute_ conn "CREATE UNIQUE INDEX idx_tbl_value_uniq ON idx_tbl (idx_value)"
        let idxOpts = setUniqueIndexOptions @PgCommandSyntax True
                    $ defaultIndexOptions @PgCommandSyntax
            db :: CheckedDatabaseSettings Postgres IdxDb
            db = defaultMigratableDbSettings `withDbModification`
                  (dbModification @_ @Postgres)
                    { _idx_tbl = addTableIndex "idx_tbl_value_uniq" idxOpts
                                   (\t -> selectorColumnName _idx_value t NE.:| []) }
        runBeamPostgres conn (verifySchema migrationBackend db) >>= \case
          VerificationSucceeded -> return ()
          VerificationFailed failures -> fail ("Verification failed: " ++ show failures)

-- Foreign key test tables

data FkParentT f = FkParentT
  { _fk_parent_id :: C f Int32
  } deriving (Generic, Beamable)

instance Table FkParentT where
  newtype PrimaryKey FkParentT f = FkParentPk (C f Int32)
    deriving (Generic, Beamable)
  primaryKey = FkParentPk . _fk_parent_id

data FkChildT f = FkChildT
  { _fk_child_id        :: C f Int32
  , _fk_child_parent_id :: PrimaryKey FkParentT f
  } deriving (Generic, Beamable)

instance Table FkChildT where
  newtype PrimaryKey FkChildT f = FkChildPk (C f Int32)
    deriving (Generic, Beamable)
  primaryKey = FkChildPk . _fk_child_id

data FkDb entity = FkDb
  { _fk_parent :: entity (TableEntity FkParentT)
  , _fk_child  :: entity (TableEntity FkChildT)
  } deriving (Generic, Database Postgres)

-- | Verifies that 'verifySchema' correctly detects a plain foreign key
foreignKeyVerification :: IO ByteString -> TestTree
foreignKeyVerification pgConn =
    testCase "verifySchema correctly detects a plain foreign key" $
      withTestPostgres "db_fk" pgConn $ \conn -> do
        Pg.execute_ conn "CREATE TABLE fk_parent (fk_parent_id integer NOT NULL PRIMARY KEY)"
        Pg.execute_ conn "CREATE TABLE fk_child  (fk_child_id integer NOT NULL PRIMARY KEY, \
                         \fk_child_parent_id integer NOT NULL, \
                         \FOREIGN KEY (fk_child_parent_id) REFERENCES fk_parent (fk_parent_id))"
        let db :: CheckedDatabaseSettings Postgres FkDb
            db = defaultMigratableDbSettings `withDbModification`
                  (dbModification @_ @Postgres)
                    { _fk_child =
                        addTableForeignKey (_fk_parent db)
                          (foreignKeyColumns _fk_child_parent_id)
                          primaryKeyColumns
                          ForeignKeyNoAction
                          ForeignKeyNoAction
                        <> modifyCheckedTable id
                             (FkChildT { _fk_child_id        = "fk_child_id"
                                       , _fk_child_parent_id = FkParentPk "fk_child_parent_id" }) }
        runBeamPostgres conn (verifySchema migrationBackend db) >>= \case
          VerificationSucceeded -> return ()
          VerificationFailed failures -> fail ("Verification failed: " ++ show failures)

-- | Verifies that foreign key actions are enforced at runtime.
foreignKeyActionsWork :: IO ByteString -> TestTree
foreignKeyActionsWork pgConn =
    testCase "cascading foreign key actions" $
      withTestPostgres "db_fk_actions" pgConn $ \conn -> do
        let db :: CheckedDatabaseSettings Postgres FkDb
            db = defaultMigratableDbSettings `withDbModification`
                  (dbModification @_ @Postgres)
                    { _fk_child =
                        addTableForeignKey (_fk_parent db)
                          (foreignKeyColumns _fk_child_parent_id)
                          primaryKeyColumns
                          ForeignKeyActionCascade
                          ForeignKeyActionCascade
                    }
            unc = unCheckDatabase db
        runBeamPostgres conn $ autoMigrate migrationBackend db

        -- Insert two parents and three children (two for parent 1, one for parent 2).
        runBeamPostgres conn $ do
          runInsert $ insert (_fk_parent unc) $ insertValues
            [ FkParentT 1, FkParentT 2 ]
          runInsert $ insert (_fk_child unc) $ insertValues
            [ FkChildT 1 (FkParentPk 1), FkChildT 2 (FkParentPk 1), FkChildT 3 (FkParentPk 2) ]

        -- ON UPDATE CASCADE: changing fk_parent_id 1 → 10 should cascade to child rows.
        runBeamPostgres conn $
          runUpdate $ update (_fk_parent unc)
            (\p -> _fk_parent_id p <-. val_ 10)
            (\p -> _fk_parent_id p ==. val_ 1)
        childrenOf10 <- runBeamPostgres conn $ runSelectReturningList $ select $
          filter_ (\c -> let FkParentPk pid = _fk_child_parent_id c in pid ==. val_ 10) $ all_ (_fk_child unc)
        assertEqual "two children should now reference updated parent id 10"
          2 (length childrenOf10)
        childrenOf1 <- runBeamPostgres conn $ runSelectReturningList $ select $
          filter_ (\c -> let FkParentPk pid = _fk_child_parent_id c in pid ==. val_ 1) $ all_ (_fk_child unc)
        assertEqual "no children should still reference old parent id 1"
          0 (length childrenOf1)

        -- ON DELETE CASCADE: deleting parent 2 should remove its child row.
        runBeamPostgres conn $
          runDelete $ delete (_fk_parent unc) (\p -> _fk_parent_id p ==. val_ 2)
        childrenOf2 <- runBeamPostgres conn $ runSelectReturningList $ select $
          filter_ (\c -> let FkParentPk pid = _fk_child_parent_id c in pid ==. val_ 2) $ all_ (_fk_child unc)
        assertEqual "child of deleted parent 2 should be removed"
          0 (length childrenOf2)
        allChildren <- runBeamPostgres conn $ runSelectReturningList $ select $
          all_ (_fk_child unc)
        assertEqual "only the two children of parent 10 should remain"
          2 (length allChildren)

-- | Verifies that 'verifySchema' correctly detects a foreign key with ON DELETE CASCADE
foreignKeyOnDeleteCascadeVerification :: IO ByteString -> TestTree
foreignKeyOnDeleteCascadeVerification pgConn =
    testCase "verifySchema correctly detects a foreign key with ON DELETE CASCADE" $
      withTestPostgres "db_fk_cascade" pgConn $ \conn -> do
        Pg.execute_ conn "CREATE TABLE fk_parent (fk_parent_id integer NOT NULL PRIMARY KEY)"
        Pg.execute_ conn "CREATE TABLE fk_child  (fk_child_id integer NOT NULL PRIMARY KEY, \
                         \fk_child_parent_id integer NOT NULL, \
                         \FOREIGN KEY (fk_child_parent_id) REFERENCES fk_parent (fk_parent_id) \
                         \ON DELETE CASCADE)"
        let db :: CheckedDatabaseSettings Postgres FkDb
            db = defaultMigratableDbSettings `withDbModification`
                  (dbModification @_ @Postgres)
                    { _fk_child =
                        addTableForeignKey (_fk_parent db)
                          (foreignKeyColumns _fk_child_parent_id)
                          primaryKeyColumns
                          ForeignKeyNoAction
                          ForeignKeyActionCascade
                        <> modifyCheckedTable id
                             (FkChildT { _fk_child_id        = "fk_child_id"
                                       , _fk_child_parent_id = FkParentPk "fk_child_parent_id" }) }
        runBeamPostgres conn (verifySchema migrationBackend db) >>= \case
          VerificationSucceeded -> return ()
          VerificationFailed failures -> fail ("Verification failed: " ++ show failures)