packages feed

dbmigrations-mysql-2.0.0: test/BackendTest.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module BackendTest where

import Test.HUnit
import Control.Exception (Handler(..), catches)
import Control.Monad ( forM_ )

import Database.Schema.Migrations.Backend.MySQL
import Database.Schema.Migrations.Migration ( Migration(..), newMigration )
import Database.Schema.Migrations.Backend ( Backend(..) )

import qualified Database.MySQL.Simple as MySQL
import qualified Database.MySQL.Base as MySQL

data BackendConnection
  = MySQLConnection MySQL.Connection

migrationBackend :: BackendConnection -> Backend
migrationBackend (MySQLConnection c) = mysqlBackend c

commit :: BackendConnection -> IO ()
commit (MySQLConnection c) = MySQL.commit c

getTables :: BackendConnection -> IO [String]
getTables (MySQLConnection c) =
  fmap (map MySQL.fromOnly)
       (MySQL.query_ c "SHOW TABLES")

catchSql_ :: IO a -> IO a -> IO a
catchSql_ act handler =
  act `catches`
  [Handler (\(_ :: MySQL.FormatError) -> handler)
  ,Handler (\(_ :: MySQL.QueryError) -> handler)
  ,Handler (\(_ :: MySQL.MySQLError) -> handler)
  ,Handler (\(_ :: MySQL.ResultError) -> handler)]

withTransaction
  :: BackendConnection -> (BackendConnection -> IO a) -> IO a
withTransaction (MySQLConnection c) transaction =
  MySQL.withTransaction c
                        (transaction (MySQLConnection c))

tests :: BackendConnection -> IO ()
tests conn = do
  let acts = [ isBootstrappedFalseTest
             , bootstrapTest
             , isBootstrappedTrueTest
             -- applyMigrationFailure intentionally disabled, see below
             -- , applyMigrationFailure
             , applyMigrationSuccess
             , revertMigrationFailure
             , revertMigrationNothing
             , revertMigrationJust
             ]
  forM_ acts $ \act -> do
               commit conn
               act conn

bootstrapTest :: BackendConnection -> IO ()
bootstrapTest conn = do
  let backend = migrationBackend conn
  bs <- getBootstrapMigration backend
  applyMigration backend bs
  assertEqual "installed_migrations table exists" ["installed_migrations"] =<< getTables conn
  assertEqual "successfully bootstrapped" [mId bs] =<< getMigrations backend

isBootstrappedTrueTest :: BackendConnection -> IO ()
isBootstrappedTrueTest conn = do
  result <- isBootstrapped $ migrationBackend conn
  assertBool "Bootstrapped check" result

isBootstrappedFalseTest :: BackendConnection -> IO ()
isBootstrappedFalseTest conn = do
  result <- isBootstrapped $ migrationBackend conn
  assertBool "Bootstrapped check" $ not result

ignoreSqlExceptions :: IO a -> IO (Maybe a)
ignoreSqlExceptions act = (act >>= return . Just) `catchSql_`
                          (return Nothing)

applyMigrationSuccess :: BackendConnection -> IO ()
applyMigrationSuccess conn = do
    let backend = migrationBackend conn

    let m1 = (newMigration "validMigration") { mApply = "CREATE TABLE valid1 (a int); CREATE TABLE valid2 (a int);" }

    -- Apply the migrations, ignore exceptions
    withTransaction conn $ \conn' -> applyMigration (migrationBackend conn') m1

    -- Check that none of the migrations were installed
    assertEqual "Installed migrations" ["root", "validMigration"] =<< getMigrations backend
    assertEqual "Installed tables" ["installed_migrations", "valid1", "valid2"] =<< getTables conn

-- |Does a failure to apply a migration imply a transaction rollback?
--
-- !! This test is intentionally disabled because MySQL does not support
-- transactions with regard to DDL statements!!
-- (see MYSQL-AND-TRANSACTIONAL-DDL.TXT)
--
-- applyMigrationFailure :: BackendConnection -> IO ()
-- applyMigrationFailure conn = do
--     let backend = migrationBackend conn
--
--     let m1 = (newMigration "second") { mApply = "CREATE TABLE validButTemporary (a int)" }
--         m2 = (newMigration "third") { mApply = "INVALID SQL" }
--
--     -- Apply the migrations, ignore exceptions
--     ignoreSqlExceptions $ withTransaction conn $ \conn' -> do
--         let backend' = migrationBackend conn'
--         applyMigration backend' m1
--         applyMigration backend' m2
--
--     -- Check that none of the migrations were installed
--     assertEqual "Installed migrations" ["root"] =<< getMigrations backend
--     assertEqual "Installed tables" ["installed_migrations"] =<< getTables conn

revertMigrationFailure :: BackendConnection -> IO ()
revertMigrationFailure conn = do
    let backend = migrationBackend conn

    let m1 = (newMigration "second") { mApply = "CREATE TABLE validRMF (a int)"
                                     , mRevert = Just "DROP TABLE validRMF"}
        m2 = (newMigration "third") { mApply = "DO 0" -- MySQL noop
                                    , mRevert = Just "INVALID REVERT SQL"}

    applyMigration backend m1
    applyMigration backend m2

    installedBeforeRevert <- getMigrations backend

    commitBackend backend

    -- Revert the migrations, ignore exceptions; the revert will fail,
    -- but withTransaction will roll back.
    ignoreSqlExceptions $ withTransaction conn $ \conn' -> do
        let backend' = migrationBackend conn'
        revertMigration backend' m2
        revertMigration backend' m1

    -- Check that none of the migrations were reverted
    assertEqual "successfully roll back failed revert" installedBeforeRevert
        =<< getMigrations backend

revertMigrationNothing :: BackendConnection -> IO ()
revertMigrationNothing conn = do
    let backend = migrationBackend conn

    let m1 = (newMigration "second") { mApply = "DO 0" -- MySQL noop
                                     , mRevert = Nothing }

    applyMigration backend m1

    installedAfterApply <- getMigrations backend
    assertBool "Check that the migration was applied" $ "second" `elem` installedAfterApply

    -- Revert the migration, which should do nothing EXCEPT remove it
    -- from the installed list
    revertMigration backend m1

    installed <- getMigrations backend
    assertBool "Check that the migration was reverted" $ not $ "second" `elem` installed

revertMigrationJust :: BackendConnection -> IO ()
revertMigrationJust conn = do
    let name = "revertable"
        backend = migrationBackend conn

    let m1 = (newMigration name) { mApply = "CREATE TABLE the_test_table (a int)"
                                 , mRevert = Just "DROP TABLE the_test_table" }

    applyMigration backend m1

    installedAfterApply <- getMigrations backend
    assertBool "Check that the migration was applied" $ name `elem` installedAfterApply

    -- Revert the migration, which should do nothing EXCEPT remove it
    -- from the installed list
    revertMigration backend m1

    installed <- getMigrations backend
    assertBool "Check that the migration was reverted" $ not $ name `elem` installed