packages feed

persistent-mtl-0.2.2.0: test/Integration.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Integration where

import Conduit (runConduit, (.|))
import qualified Conduit
import Control.Arrow ((&&&))
import qualified Data.Acquire as Acquire
import Data.Bifunctor (first)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Typeable (Typeable)
import Database.Persist.Sql
    ( Entity(..)
    , Migration
    , PersistField
    , PersistRecordBackend
    , PersistValue
    , Single(..)
    , SqlBackend
    , fromPersistValue
    , (=.)
    , (==.)
    )
#if MIN_VERSION_persistent(2,9,0)
import Database.Persist.Sql (IsolationLevel(..))
#endif
import Test.Tasty
import Test.Tasty.HUnit
import UnliftIO (MonadIO, MonadUnliftIO, liftIO)
import UnliftIO.Exception
    ( Exception
    , SomeException
    , StringException(..)
    , fromException
    , throwIO
    , throwString
    , try
    )
import UnliftIO.IORef (atomicModifyIORef, newIORef)

import Control.Monad.IO.Rerunnable (MonadRerunnableIO, rerunnableIO)
import Database.Persist.Monad
import Example
import TestUtils.DB (BackendType(..), allBackendTypes)
import TestUtils.Match (Match(..), (@?~))

tests :: TestTree
tests = testGroup "Integration tests" $
  map testsWithBackend allBackendTypes

testsWithBackend :: BackendType -> TestTree
testsWithBackend backendType = testGroup (show backendType)
  [ testWithTransaction backendType
  , testComposability backendType
  , testPersistentAPI backendType
  ]

testWithTransaction :: BackendType -> TestTree
testWithTransaction backendType = testGroup "withTransaction"
  [ testCase "it uses the same transaction" $ do
      -- without transactions, the INSERT shouldn't be rolled back
      runTestApp backendType $ do
        catchTestError $ insertAndFail $ person "Alice"
        result <- getPeopleNames
        liftIO $ result @?= ["Alice"]

      -- with transactions, the INSERT should be rolled back
      runTestApp backendType $ do
        catchTestError $ withTransaction $ insertAndFail $ person "Alice"
        result <- getPeopleNames
        liftIO $ result @?= []

  , testCase "retries transactions" $ do
      let retryIf e = case fromException e of
            Just (StringException "retry me" _) -> True
            _ -> False
          setRetry env = env { retryIf, retryLimit = 5 }

      counter <- newIORef (0 :: Int)

      result <- try @_ @SomeException $ runTestAppWith backendType setRetry $
        withTransaction $ rerunnableIO $ do
          x <- atomicModifyIORef counter $ \x -> (x + 1, x)
          if x > 2
            then return ()
            else throwString "retry me"

      case result of
        Right () -> return ()
        Left e -> error $ "Got unexpected error: " ++ show e

  , testCase "throws error when retry hits limit" $ do
      let setRetry env = env { retryIf = const True, retryLimit = 2 }

      result <- try @_ @TransactionError @() $ runTestAppWith backendType setRetry $
        withTransaction $ rerunnableIO $ throwString "retry me"

      result @?= Left RetryLimitExceeded
  ]

-- this should compile
testComposability :: BackendType -> TestTree
testComposability backendType = testCase "Operations can be composed" $ do
  let onlySql :: MonadSqlQuery m => m ()
      onlySql = do
        _ <- getPeople
        return ()

      sqlAndRerunnableIO :: (MonadSqlQuery m, MonadRerunnableIO m) => m ()
      sqlAndRerunnableIO = do
        _ <- getPeopleNames
        _ <- rerunnableIO $ newIORef True
        return ()

      onlyRerunnableIO :: MonadRerunnableIO m => m ()
      onlyRerunnableIO = do
        _ <- rerunnableIO $ newIORef True
        return ()

      arbitraryIO :: MonadIO m => m ()
      arbitraryIO = do
        _ <- liftIO $ newIORef True
        return ()

  -- everything should compose naturally by default
  runTestApp backendType $ do
    onlySql
    sqlAndRerunnableIO
    onlyRerunnableIO
    arbitraryIO

  -- in a transaction, you can compose everything except arbitrary IO
  runTestApp backendType $ withTransaction $ do
    onlySql
    sqlAndRerunnableIO
    onlyRerunnableIO
    -- uncomment this to get compile error
    -- arbitraryIO

testPersistentAPI :: BackendType -> TestTree
testPersistentAPI backendType = testGroup "Persistent API"
  [ testCase "get" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        mapM get [1, 2]
      map (fmap personName) result @?= [Just "Alice", Nothing]

  , testCase "getMany" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        getMany [1]
      personName <$> Map.lookup 1 result @?= Just "Alice"

  , testCase "getJust" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        getJust 1
      personName result @?= "Alice"

  , testCase "getJustEntity" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        getJustEntity 1
      getName result @?= "Alice"

  , testCase "getEntity" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        mapM getEntity [1, 2]
      map (fmap getName) result @?= [Just "Alice", Nothing]

  , testCase "belongsTo" $ do
      result <- runTestApp backendType $ do
        aliceKey <- insert $ person "Alice"
        let post1 = Post "Post #1" aliceKey (Just aliceKey)
            post2 = Post "Post #2" aliceKey Nothing
        insertMany_ [post1, post2]
        mapM (belongsTo postEditor) [post1, post2]
      map (fmap personName) result @?= [Just "Alice", Nothing]

  , testCase "belongsToJust" $ do
      result <- runTestApp backendType $ do
        aliceKey <- insert $ person "Alice"
        let post1 = Post "Post #1" aliceKey Nothing
        insert_ post1
        belongsToJust postAuthor post1
      personName result @?= "Alice"

  , testCase "insert" $ do
      result <- runTestApp backendType $ do
        aliceKey <- insert $ person "Alice"
        people <- getPeopleNames
        return (aliceKey, people)
      result @?= (1, ["Alice"])

  , testCase "insert_" $ do
      result <- runTestApp backendType $ do
        result <- insert_ $ person "Alice"
        people <- getPeopleNames
        return (result, people)
      result @?= ((), ["Alice"])

  , testCase "insertMany" $ do
      result <- runTestApp backendType $ do
        keys <- insertMany [person "Alice", person "Bob"]
        people <- getPeopleNames
        return (keys, people)
      result @?= ([1, 2], ["Alice", "Bob"])

  , testCase "insertMany_" $ do
      result <- runTestApp backendType $ do
        result <- insertMany_ [person "Alice", person "Bob"]
        people <- getPeopleNames
        return (result, people)
      result @?= ((), ["Alice", "Bob"])

  , testCase "insertEntityMany" $ do
      result <- runTestApp backendType $ do
        result <- insertEntityMany
          [ Entity 1 $ person "Alice"
          , Entity 2 $ person "Bob"
          ]
        people <- getPeopleNames
        return (result, people)
      result @?= ((), ["Alice", "Bob"])

  , testCase "insertKey" $ do
      result <- runTestApp backendType $ do
        result <- insertKey 1 $ person "Alice"
        people <- getPeopleNames
        return (result, people)
      result @?= ((), ["Alice"])

  , testCase "repsert" $ do
      result <- runTestApp backendType $ do
        let alice = person "Alice"
        insert_ alice
        repsert 1 $ alice { personAge = 100 }
        repsert 2 $ person "Bob"
        getPeople
      map nameAndAge result @?=
        [ ("Alice", 100)
        , ("Bob", 0)
        ]

  , testCase "repsertMany" $ do
      result <- runTestApp backendType $ do
        let alice = person "Alice"
-- https://github.com/yesodweb/persistent/issues/832
#if MIN_VERSION_persistent(2,9,0)
        insert_ alice
        repsertMany
          [ (1, alice { personAge = 100 })
          , (2, person "Bob")
          ]
#else
        repsertMany [(1, alice { personAge = 100 })]
        repsertMany [(2, person "Bob")]
#endif
        getPeople
      map nameAndAge result @?=
        [ ("Alice", 100)
        , ("Bob", 0)
        ]

  , testCase "replace" $ do
      result <- runTestApp backendType $ do
        let alice = person "Alice"
        insert_ alice
        replace 1 $ alice { personAge = 100 }
        getJust 1
      personAge result @?= 100

  , testCase "delete" $ do
      result <- runTestApp backendType $ do
        aliceKey <- insert $ person "Alice"
        delete aliceKey
        getPeople
      result @?= []

  , testCase "update" $ do
      result <- runTestApp backendType $ do
        key <- insert $ person "Alice"
        update key [PersonName =. "Alicia"]
        getPeopleNames
      result @?= ["Alicia"]

  , testCase "updateGet" $ do
      (updateResult, getResult) <- runTestApp backendType $ do
        key <- insert $ person "Alice"
        updateResult <- updateGet key [PersonName =. "Alicia"]
        getResult <- getJust key
        return (updateResult, getResult)
      updateResult @?= getResult

  , testCase "insertEntity" $ do
      (insertResult, getResult) <- runTestApp backendType $ do
        insertResult <- insertEntity $ person "Alice"
        getResult <- getJust $ entityKey insertResult
        return (insertResult, getResult)
      entityVal insertResult @?= getResult

  , testCase "insertRecord" $ do
      (insertResult, getResult) <- runTestApp backendType $ do
        insertResult <- insertRecord $ person "Alice"
        getResult <- getJust 1
        return (insertResult, getResult)
      insertResult @?= getResult

  , testCase "getBy" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        mapM getBy [UniqueName "Alice", UniqueName "Bob"]
      map (fmap getName) result @?= [Just "Alice", Nothing]

  , testCase "getByValue" $ do
      result <- runTestApp backendType $ do
        let alice = person "Alice"
        insert_ alice
        mapM getByValue [alice, person "Bob"]
      map (fmap getName) result @?= [Just "Alice", Nothing]

  , testCase "checkUnique" $ do
      result <- runTestApp backendType $ do
        let alice = person "Alice"
        insert_ alice
        mapM checkUnique
          [ alice
          , person "Bob"
          , (person "Alice"){ personAge = 100 }
          ]
      result @?= [Just (UniqueName "Alice"), Nothing, Just (UniqueName "Alice")]

#if MIN_VERSION_persistent(2,11,0)
  , testCase "checkUniqueUpdateable" $ do
      result <- runTestApp backendType $ do
        let alice = person "Alice"
        insert_ alice
        mapM checkUniqueUpdateable
          [ Entity 1 alice
          , Entity 2 $ person "Bob"
          , Entity 3 $ (person "Alice"){ personAge = 100 }
          ]
      result @?= [Nothing, Nothing, Just (UniqueName "Alice")]
#endif

  , testCase "deleteBy" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        deleteBy $ UniqueName "Alice"
        getPeople
      result @?= []

  , testCase "insertUnique" $ do
      (result1, result2, people) <- runTestApp backendType $ do
        result1 <- insertUnique $ person "Alice"
        result2 <- insertUnique $ person "Alice"
        people <- getPeopleNames
        return (result1, result2, people)
      result1 @?= Just 1
      result2 @?= Nothing
      people @?= ["Alice"]

  , testCase "upsert" $ do
      (result1, result2, people) <- runTestApp backendType $ do
        result1 <- upsert (person "Alice") [PersonAge =. 0]
        result2 <- upsert (person "Alice") [PersonAge =. 100]
        people <- getPeople
        return (result1, result2, people)
      entityKey result1 @?= entityKey result2
      nameAndAge (entityVal result1) @?= ("Alice", 0)
      nameAndAge (entityVal result2) @?= ("Alice", 100)
      map nameAndAge people @?= [("Alice", 100)]

  , testCase "upsertBy" $ do
      (result1, result2, people) <- runTestApp backendType $ do
        result1 <- upsertBy (UniqueName "Alice") (person "Alice") [PersonAge =. 0]
        result2 <- upsertBy (UniqueName "Alice") (person "Alice") [PersonAge =. 100]
        people <- getPeople
        return (result1, result2, people)
      entityKey result1 @?= entityKey result2
      nameAndAge (entityVal result1) @?= ("Alice", 0)
      nameAndAge (entityVal result2) @?= ("Alice", 100)
      map nameAndAge people @?= [("Alice", 100)]

  , testCase "putMany" $ do
      result <- runTestApp backendType $ do
        let alice = person "Alice"
        insert_ alice
        putMany
          [ alice { personAge = 100 }
          , person "Bob"
          ]
        getPeople
      map nameAndAge result @?=
        [ ("Alice", 100)
        , ("Bob", 0)
        ]

  , testCase "insertBy" $ do
      (result1, result2, people) <- runTestApp backendType $ do
        let alice = person "Alice"
        result1 <- insertBy alice
        result2 <- insertBy $ alice { personAge = 100 }
        people <- getPeople
        return (result1, result2, people)
      result1 @?= Right 1
      first (entityKey &&& getName) result2 @?= Left (1, "Alice")
      map nameAndAge people @?= [("Alice", 0)]

  , testCase "insertUniqueEntity" $ do
      (result1, result2, people) <- runTestApp backendType $ do
        let alice = person "Alice"
        result1 <- insertUniqueEntity alice
        result2 <- insertUniqueEntity $ alice { personAge = 100 }
        people <- getPeople
        return (result1, result2, people)
      (entityKey &&& getName) <$> result1 @?= Just (1, "Alice")
      result2 @?= Nothing
      map nameAndAge people @?= [("Alice", 0)]

  , testCase "replaceUnique" $ do
      (result1, result2, people) <- runTestApp backendType $ do
        let alice = person "Alice"
            bob = person "Bob"
        insertMany_ [alice, bob]
        result1 <- replaceUnique 1 $ alice { personName = "Bob" }
        result2 <- replaceUnique 2 $ bob { personAge = 100 }
        people <- getPeople
        return (result1, result2, people)
      result1 @?= Just (UniqueName "Bob")
      result2 @?= Nothing
      map nameAndAge people @?= [("Alice", 0), ("Bob", 100)]

  , testCase "onlyUnique" $ do
      result <- runTestApp backendType $ onlyUnique $ person "Alice"
      result @?= UniqueName "Alice"

  , testCase "selectSourceRes" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        acquire <- selectSourceRes [] []
        Acquire.with acquire $ \conduit ->
          runConduit $ conduit .| Conduit.mapC getName .| Conduit.sinkList
      result @?= ["Alice", "Bob"]

  , testCase "selectFirst" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        sequence
          [ selectFirst [PersonName ==. "Alice"] []
          , selectFirst [PersonName ==. "Bob"] []
          ]
      map (fmap getName) result @?= [Just "Alice", Nothing]

  , testCase "selectKeysRes" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        acquire <- selectKeysRes @_ @Person [] []
        Acquire.with acquire $ \conduit ->
          runConduit $ conduit .| Conduit.sinkList
      result @?= [1, 2]

  , testCase "count" $ do
      result <- runTestApp backendType $ do
        insertMany_ $ map (\p -> p{personAge = 100}) [person "Alice", person "Bob"]
        count [PersonAge ==. 100]
      result @?= 2

#if MIN_VERSION_persistent(2,11,0)
  , testCase "exists" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        exists [PersonName ==. "Alice"]
      result @?= True
#endif

  , testCase "selectSource" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        runConduit $ selectSource [] [] .| Conduit.mapC getName .| Conduit.sinkList
      result @?= ["Alice", "Bob"]

  , testCase "selectKeys" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        runConduit $ selectKeys @Person [] [] .| Conduit.sinkList
      result @?= [1, 2]

  , testCase "selectList" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        insert_ $ person "Bob"
        selectList [] []
      map getName result @?= ["Alice", "Bob"]

  , testCase "selectKeysList" $ do
      result <- runTestApp backendType $ do
        insert_ $ person "Alice"
        insert_ $ person "Bob"
        selectKeysList @Person [] []
      result @?= [1, 2]

  , testCase "updateWhere" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        updateWhere [PersonName ==. "Alice"] [PersonAge =. 100]
        getPeople
      map nameAndAge result @?= [("Alice", 100), ("Bob", 0)]

  , testCase "deleteWhere" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        deleteWhere [PersonName ==. "Alice"]
        getPeopleNames
      result @?= ["Bob"]

  , testCase "updateWhereCount" $ do
      (rowsUpdated, people) <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        rowsUpdated <- updateWhereCount [PersonName ==. "Alice"] [PersonAge =. 100]
        people <- getPeople
        return (rowsUpdated, people)
      rowsUpdated @?= 1
      map nameAndAge people @?= [("Alice", 100), ("Bob", 0)]

  , testCase "deleteWhereCount" $ do
      (rowsDeleted, names) <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        rowsDeleted <- deleteWhereCount [PersonName ==. "Alice"]
        names <- getPeopleNames
        return (rowsDeleted, names)
      rowsDeleted @?= 1
      names @?= ["Bob"]

#if !MIN_VERSION_persistent(2,13,0)
  , testCase "deleteCascade" $ do
      (people, posts) <- runTestApp backendType $ do
        aliceKey <- insert $ person "Alice"
        bobKey <- insert $ person "Bob"
        insertMany_
          [ post "Post #1" aliceKey
          , post "Post #2" bobKey
          ]
        deleteCascade aliceKey
        people <- getPeopleNames
        posts <- getPostTitles
        return (people, posts)
      people @?= ["Bob"]
      posts @?= ["Post #2"]

  , testCase "deleteCascadeWhere" $ do
      (people, posts) <- runTestApp backendType $ do
        aliceKey <- insert $ person "Alice"
        bobKey <- insert $ person "Bob"
        insertMany_
          [ post "Post #1" aliceKey
          , post "Post #2" bobKey
          ]
        deleteCascadeWhere [PersonName ==. "Alice"]
        people <- getPeopleNames
        posts <- getPostTitles
        return (people, posts)
      people @?= ["Bob"]
      posts @?= ["Post #2"]
#endif

  , testCase "parseMigration" $ do
      result <- runTestApp backendType $ do
        setupUnsafeMigration
        parseMigration migration

      let sql = case backendType of
            Sqlite ->
              [ Match
                  ( False
                  , Text.concat
                      [ "CREATE TEMP TABLE \"person_backup\"("
                      , "\"id\" INTEGER PRIMARY KEY,"
                      , "\"name\" VARCHAR NOT NULL,"
                      , "\"age\" INTEGER NOT NULL,"
                      , "CONSTRAINT \"unique_name\" UNIQUE (\"name\"))"
                      ]
                  )
              , Anything
              , Match (True, "DROP TABLE \"person\"")
              , Anything
              , Anything
              , Match (False, "DROP TABLE \"person_backup\"")
              ]
            Postgresql ->
              [ Match (True, "ALTER TABLE \"person\" DROP COLUMN \"foo\"")
              ]

      result @?~ Right @[Text] sql

  , testCase "parseMigration'" $ do
      let action :: (Migration -> TestApp a) -> IO a
          action f = runTestApp backendType $ do
            setupUnsafeMigration
            f migration

      result <- action parseMigration
      result' <- action parseMigration'
      Right result' @?= result

  , testCase "printMigration" $
      runTestApp backendType $ do
        setupUnsafeMigration
        printMigration migration

  , testCase "showMigration" $ do
      result <- runTestApp backendType $ do
        setupUnsafeMigration
        showMigration migration

      let sql = case backendType of
            Sqlite ->
              [ Match $ Text.concat
                  [ "CREATE TEMP TABLE \"person_backup\"("
                  , "\"id\" INTEGER PRIMARY KEY,"
                  , "\"name\" VARCHAR NOT NULL,"
                  , "\"age\" INTEGER NOT NULL,"
                  , "CONSTRAINT \"unique_name\" UNIQUE (\"name\"));"
                  ]
              , Anything
              , Match "DROP TABLE \"person\";"
              , Anything
              , Anything
              , Match "DROP TABLE \"person_backup\";"
              ]
            Postgresql ->
              [ Match "ALTER TABLE \"person\" DROP COLUMN \"foo\";"
              ]

      result @?~ sql

  , testCase "getMigration" $ do
      result <- runTestApp backendType $ do
        setupUnsafeMigration
        getMigration migration

      let sql = case backendType of
            Sqlite ->
              [ Match $ Text.concat
                  [ "CREATE TEMP TABLE \"person_backup\"("
                  , "\"id\" INTEGER PRIMARY KEY,"
                  , "\"name\" VARCHAR NOT NULL,"
                  , "\"age\" INTEGER NOT NULL,"
                  , "CONSTRAINT \"unique_name\" UNIQUE (\"name\"))"
                  ]
              , Anything
              , Match "DROP TABLE \"person\""
              , Anything
              , Anything
              , Match "DROP TABLE \"person_backup\""
              ]
            Postgresql ->
              [ Match "ALTER TABLE \"person\" DROP COLUMN \"foo\""
              ]

      result @?~ sql

  , testCase "runMigration" $ do
      result <- runTestApp backendType $ do
        setupSafeMigration
        runMigration migration
        getSchemaColumnNames backendType "person"
      assertNotIn "removed_column" result

#if MIN_VERSION_persistent(2,10,2)
  , testCase "runMigrationQuiet" $ do
      (withQuiet, cols) <- runTestApp backendType $ do
        setupSafeMigration
        sql <- runMigrationQuiet migration
        cols <- getSchemaColumnNames backendType "person"
        return (sql, cols)
      withSilent <- runTestApp backendType $ do
        setupSafeMigration
        runMigrationSilent migration
      assertNotIn "removed_column" cols
      withQuiet @?= withSilent
#endif

  , testCase "runMigrationSilent" $ do
      (sqlPlanned, sqlExecuted, cols) <- runTestApp backendType $ do
        setupSafeMigration
        sqlPlanned <- getMigration migration
        sqlExecuted <- runMigrationSilent migration
        cols <- getSchemaColumnNames backendType "person"
        return (sqlPlanned, sqlExecuted, cols)
      assertNotIn "removed_column" cols
      sqlExecuted @?= sqlPlanned

  , testCase "runMigrationUnsafe" $ do
      result <- runTestApp backendType $ do
        setupUnsafeMigration
        runMigrationUnsafe migration
        getSchemaColumnNames backendType "person"
      assertNotIn "removed_column" result

#if MIN_VERSION_persistent(2,10,2)
  , testCase "runMigrationUnsafeQuiet" $ do
      (sqlPlanned, sqlExecuted, cols) <- runTestApp backendType $ do
        setupUnsafeMigration
        sqlPlanned <- getMigration migration
        sqlExecuted <- runMigrationUnsafeQuiet migration
        cols <- getSchemaColumnNames backendType "person"
        return (sqlPlanned, sqlExecuted, cols)
      assertNotIn "removed_column" cols
      sqlExecuted @?= sqlPlanned
#endif

  , testCase "getFieldName" $ do
      result <- runTestApp backendType $
        getFieldName PersonName
      result @?= "\"name\""

  , testCase "getTableName" $ do
      result <- runTestApp backendType $
        getTableName $ person "Alice"
      result @?= "\"person\""

  , testCase "withRawQuery" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        withRawQuery "SELECT name FROM person" [] $
          Conduit.mapC (fromPersistValue' @Text . head) .| Conduit.sinkList

      result @?= ["Alice", "Bob"]

  , testCase "rawQueryRes" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        acquire <- rawQueryRes "SELECT name FROM person" []
        Acquire.with acquire $ \conduit ->
          runConduit $ conduit .| Conduit.mapC (fromPersistValue' @Text . head) .| Conduit.sinkList
      result @?= ["Alice", "Bob"]

  , testCase "rawQuery" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        runConduit $ rawQuery "SELECT name FROM person" [] .| Conduit.mapC (fromPersistValue' @Text . head) .| Conduit.sinkList
      result @?= ["Alice", "Bob"]

  , testCase "rawExecute" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        rawExecute "UPDATE person SET age = 100 WHERE name = 'Alice'" []
        getPeople
      map nameAndAge result @?= [("Alice", 100), ("Bob", 0)]

  , testCase "rawExecuteCount" $ do
      (rowsUpdated, people) <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        rowsUpdated <- rawExecuteCount "UPDATE person SET age = 100 WHERE name = 'Alice'" []
        people <- getPeople
        return (rowsUpdated, people)
      rowsUpdated @?= 1
      map nameAndAge people @?= [("Alice", 100), ("Bob", 0)]

  , testCase "rawSql" $ do
      result <- runTestApp backendType $ do
        insertMany_ [person "Alice", person "Bob"]
        rawSql @(Single String) "SELECT name FROM person" []
      map unSingle result @?= ["Alice", "Bob"]

  , testCase "transactionSave" $ do
      result1 <- runTestApp backendType $ do
        catchTestError $ withTransaction $ do
          insert_ $ person "Alice"
          insertAndFail $ person "Bob"
        getPeopleNames
      result1 @?= []

      result2 <- runTestApp backendType $ do
        catchTestError $ withTransaction $ do
          insert_ $ person "Alice"
          transactionSave
          insertAndFail $ person "Bob"
        getPeopleNames
      result2 @?= ["Alice"]

#if MIN_VERSION_persistent(2,9,0)
  , testCase "transactionSaveWithIsolation" $ do
      result1 <- runTestApp backendType $ do
        catchTestError $ withTransaction $ do
          insert_ $ person "Alice"
          insertAndFail $ person "Bob"
        getPeopleNames
      result1 @?= []

      result2 <- runTestApp backendType $ do
        catchTestError $ withTransaction $ do
          insert_ $ person "Alice"
          transactionSaveWithIsolation Serializable
          insertAndFail $ person "Bob"
        getPeopleNames
      result2 @?= ["Alice"]
#endif

  , testCase "transactionUndo" $ do
      result <- runTestApp backendType $ withTransaction $ do
        insert_ $ person "Alice"
        transactionUndo
        getPeopleNames
      result @?= []

#if MIN_VERSION_persistent(2,9,0)
  , testCase "transactionUndoWithIsolation" $ do
      result <- runTestApp backendType $ withTransaction $ do
        insert_ $ person "Alice"
        transactionUndoWithIsolation Serializable
        getPeopleNames
      result @?= []
#endif
  ]

{- Persistent helpers -}

fromPersistValue' :: PersistField a => PersistValue -> a
fromPersistValue' = either (error . Text.unpack) id . fromPersistValue

{- Meta SQL helpers -}

-- | Put the database in a state where running a migration is safe.
setupSafeMigration :: MonadSqlQuery m => m ()
setupSafeMigration = rawExecute "ALTER TABLE person ADD COLUMN removed_column VARCHAR" []

-- | Put the database in a state where running a migration is unsafe.
setupUnsafeMigration :: MonadSqlQuery m => m ()
setupUnsafeMigration = rawExecute "ALTER TABLE person ADD COLUMN foo VARCHAR" []

-- | Get the names of all columns in the given table.
getSchemaColumnNames :: MonadSqlQuery m => BackendType -> String -> m [String]
getSchemaColumnNames backendType tableName = map unSingle <$> rawSql sql []
  where
    sql = Text.pack $ case backendType of
      Sqlite -> "SELECT name FROM pragma_table_info('" ++ tableName ++ "')"
      Postgresql -> unlines
        [ "SELECT column_name FROM information_schema.columns"
        , "WHERE table_schema = 'public' AND table_name = '" ++ tableName ++ "'"
        ]

{- Test helpers -}

data TestError = TestError
  deriving (Show, Eq)

instance Exception TestError

catchTestError :: (MonadUnliftIO m, Eq a, Show a) => m a -> m ()
catchTestError m = do
  result <- try m
  liftIO $ result @?= Left TestError

insertAndFail ::
  ( MonadRerunnableIO m
  , MonadSqlQuery m
  , PersistRecordBackend record SqlBackend
  , Typeable record
  )
  => record -> m ()
insertAndFail record = do
  insert_ record
  rerunnableIO $ throwIO TestError

assertNotIn :: (Eq a, Show a) => a -> [a] -> Assertion
assertNotIn a as = as @?= filter (/= a) as