packages feed

persistent-mtl-0.5.2: test/IntegrationSpec.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module IntegrationSpec (spec) where

import Conduit (runConduit, (.|))
import qualified Conduit
import Control.Arrow ((&&&))
import Control.Monad (forM_)
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 qualified Database.Esqueleto.Experimental as E
import Database.Persist.Sql (
  Entity (..),
  IsolationLevel (..),
  Migration,
  PersistField,
  PersistRecordBackend,
  PersistValue,
  Single (..),
  SqlBackend,
  fromPersistValue,
  (=.),
  (==.),
 )
import Skeletest
import qualified Skeletest.Predicate as P
import UnliftIO (MonadIO, MonadUnliftIO, liftIO)
import UnliftIO.Exception (
  Exception,
  SomeException,
  StringException (..),
  fromException,
  throwIO,
  throwString,
  try,
 )
import UnliftIO.IORef (atomicModifyIORef, newIORef, readIORef, writeIORef)

import Control.Monad.IO.Rerunnable (MonadRerunnableIO, rerunnableIO)
import Database.Persist.Monad
import Database.Persist.Monad.Internal.PersistentShim (SafeToInsert)
import Example
import TestUtils.DB (BackendType (..), allBackendTypes)
import TestUtils.Esqueleto (esqueletoSelect)

spec :: Spec
spec = do
  forM_ allBackendTypes $ \backendType ->
    describe (show backendType) $ do
      describe "withTransaction" $ do
        it "uses the same transaction" $ do
          -- without transactions, the INSERT shouldn't be rolled back
          resultWithoutTransactions <-
            runTestApp backendType $ do
              catchTestError $ insertAndFail $ person "Alice"
              getPeopleNames
          resultWithoutTransactions `shouldBe` ["Alice"]

          -- with transactions, the INSERT should be rolled back
          resultWithTransactions <-
            runTestApp backendType $ do
              catchTestError $ withTransaction $ insertAndFail $ person "Alice"
              getPeopleNames
          resultWithTransactions `shouldBe` []

        it "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)

          (`shouldNotSatisfy` P.throws (P.anything @_ @SomeException)) $
            runTestAppWith backendType setRetry $
              withTransaction $
                rerunnableIO $ do
                  x <- atomicModifyIORef counter $ \x -> (x + 1, x)
                  if x > 2
                    then return ()
                    else throwString "retry me"

        it "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 `shouldBe` Left RetryLimitExceeded

        it "runs retryCallback" $ do
          callbackRef <- newIORef Nothing

          let setRetry env =
                env
                  { retryIf = const True
                  , retryLimit = 2
                  , retryCallback = writeIORef callbackRef . Just
                  }
          _ <-
            try @_ @TransactionError @() $
              runTestAppWith backendType setRetry . withTransaction $
                rerunnableIO (throwIO TestError)

          mError <- readIORef callbackRef
          (mError >>= fromException) `shouldBe` Just TestError

      describe "catchSqlTransaction" $ do
        let newCatchRef = do
              wasCaughtRef <- newIORef False
              let markCaught (_ :: SomeException) =
                    rerunnableIO $ writeIORef wasCaughtRef True
                  getWasCaught = readIORef wasCaughtRef
              pure (markCaught, getWasCaught)

        it "catches errors" $ do
          (markCaught, getWasCaught) <- newCatchRef
          runTestApp backendType . withTransaction $
            (`catchSqlTransaction` markCaught) $
              rerunnableIO (throwString "error")
          getWasCaught `shouldSatisfy` P.returns (P.eq True)

        it "does not catch retry errors" $ do
          let retryIf e = case fromException e of
                Just (StringException "retry me" _) -> True
                _ -> False
              setRetry env = env{retryIf, retryLimit = 2}

          (markCaught, getWasCaught) <- newCatchRef
          _ <-
            try @_ @SomeException $
              runTestAppWith backendType setRetry . withTransaction $
                (`catchSqlTransaction` markCaught) $
                  rerunnableIO (throwString "retry me")
          getWasCaught `shouldSatisfy` P.returns (P.eq False)

      -- should compile
      it "can compose operations" $ 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
          pure ()

      describe "Persistent API" $ do
        it "get" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            mapM get [1, 2]
          map (fmap personName) result `shouldBe` [Just "Alice", Nothing]

        it "getMany" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            getMany [1]
          personName <$> Map.lookup 1 result `shouldBe` Just "Alice"

        it "getJust" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            getJust 1
          personName result `shouldBe` "Alice"

        it "getJustEntity" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            getJustEntity 1
          getName result `shouldBe` "Alice"

        it "getEntity" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            mapM getEntity [1, 2]
          map (fmap getName) result `shouldBe` [Just "Alice", Nothing]

        it "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 `shouldBe` [Just "Alice", Nothing]

        it "belongsToJust" $ do
          result <- runTestApp backendType $ do
            aliceKey <- insert $ person "Alice"
            let post1 = Post "Post #1" aliceKey Nothing
            insert_ post1
            belongsToJust postAuthor post1
          personName result `shouldBe` "Alice"

        it "insert" $ do
          result <- runTestApp backendType $ do
            aliceKey <- insert $ person "Alice"
            people <- getPeopleNames
            return (aliceKey, people)
          result `shouldBe` (1, ["Alice"])

        it "insert_" $ do
          result <- runTestApp backendType $ do
            result <- insert_ $ person "Alice"
            people <- getPeopleNames
            return (result, people)
          result `shouldBe` ((), ["Alice"])

        it "insertMany" $ do
          result <- runTestApp backendType $ do
            keys <- insertMany [person "Alice", person "Bob"]
            people <- getPeopleNames
            return (keys, people)
          result `shouldBe` ([1, 2], ["Alice", "Bob"])

        it "insertMany_" $ do
          result <- runTestApp backendType $ do
            result <- insertMany_ [person "Alice", person "Bob"]
            people <- getPeopleNames
            return (result, people)
          result `shouldBe` ((), ["Alice", "Bob"])

        it "insertEntityMany" $ do
          result <- runTestApp backendType $ do
            result <-
              insertEntityMany
                [ Entity 1 $ person "Alice"
                , Entity 2 $ person "Bob"
                ]
            people <- getPeopleNames
            return (result, people)
          result `shouldBe` ((), ["Alice", "Bob"])

        it "insertKey" $ do
          result <- runTestApp backendType $ do
            result <- insertKey 1 $ person "Alice"
            people <- getPeopleNames
            return (result, people)
          result `shouldBe` ((), ["Alice"])

        it "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
            `shouldBe` [ ("Alice", 100)
                       , ("Bob", 0)
                       ]

        it "repsertMany" $ do
          result <- runTestApp backendType $ do
            let alice = person "Alice"
            -- https://github.com/yesodweb/persistent/issues/832
            insert_ alice
            repsertMany
              [ (1, alice{personAge = 100})
              , (2, person "Bob")
              ]
            getPeople
          map nameAndAge result
            `shouldBe` [ ("Alice", 100)
                       , ("Bob", 0)
                       ]

        it "replace" $ do
          result <- runTestApp backendType $ do
            let alice = person "Alice"
            insert_ alice
            replace 1 $ alice{personAge = 100}
            getJust 1
          personAge result `shouldBe` 100

        it "delete" $ do
          result <- runTestApp backendType $ do
            aliceKey <- insert $ person "Alice"
            delete aliceKey
            getPeople
          result `shouldBe` []

        it "update" $ do
          result <- runTestApp backendType $ do
            key <- insert $ person "Alice"
            update key [PersonName =. "Alicia"]
            getPeopleNames
          result `shouldBe` ["Alicia"]

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

        it "insertEntity" $ do
          (insertResult, getResult) <- runTestApp backendType $ do
            insertResult <- insertEntity $ person "Alice"
            getResult <- getJust $ entityKey insertResult
            return (insertResult, getResult)
          entityVal insertResult `shouldBe` getResult

        it "insertRecord" $ do
          (insertResult, getResult) <- runTestApp backendType $ do
            insertResult <- insertRecord $ person "Alice"
            getResult <- getJust 1
            return (insertResult, getResult)
          insertResult `shouldBe` getResult

        it "getBy" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            mapM getBy [UniqueName "Alice", UniqueName "Bob"]
          map (fmap getName) result `shouldBe` [Just "Alice", Nothing]

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

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

        it "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 `shouldBe` [Nothing, Nothing, Just (UniqueName "Alice")]

        it "deleteBy" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            deleteBy $ UniqueName "Alice"
            getPeople
          result `shouldBe` []

        it "insertUnique" $ do
          (result1, result2, people) <- runTestApp backendType $ do
            result1 <- insertUnique $ person "Alice"
            result2 <- insertUnique $ person "Alice"
            people <- getPeopleNames
            return (result1, result2, people)
          result1 `shouldBe` Just 1
          result2 `shouldBe` Nothing
          people `shouldBe` ["Alice"]

        it "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 `shouldBe` entityKey result2
          nameAndAge (entityVal result1) `shouldBe` ("Alice", 0)
          nameAndAge (entityVal result2) `shouldBe` ("Alice", 100)
          map nameAndAge people `shouldBe` [("Alice", 100)]

        it "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 `shouldBe` entityKey result2
          nameAndAge (entityVal result1) `shouldBe` ("Alice", 0)
          nameAndAge (entityVal result2) `shouldBe` ("Alice", 100)
          map nameAndAge people `shouldBe` [("Alice", 100)]

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

        it "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 `shouldBe` Right 1
          first (entityKey &&& getName) result2 `shouldBe` Left (1, "Alice")
          map nameAndAge people `shouldBe` [("Alice", 0)]

        it "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 `shouldBe` Just (1, "Alice")
          result2 `shouldBe` Nothing
          map nameAndAge people `shouldBe` [("Alice", 0)]

        it "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 `shouldBe` Just (UniqueName "Bob")
          result2 `shouldBe` Nothing
          map nameAndAge people `shouldBe` [("Alice", 0), ("Bob", 100)]

        it "onlyUnique" $ do
          result <- runTestApp backendType $ onlyUnique $ person "Alice"
          result `shouldBe` UniqueName "Alice"

        it "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 `shouldBe` ["Alice", "Bob"]

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

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

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

        it "exists" $ do
          result <- runTestApp backendType $ do
            insertMany_ [person "Alice", person "Bob"]
            exists [PersonName ==. "Alice"]
          result `shouldBe` True

        it "selectSource" $ do
          result <- runTestApp backendType $ do
            insertMany_ [person "Alice", person "Bob"]
            runConduit $ selectSource [] [] .| Conduit.mapC getName .| Conduit.sinkList
          result `shouldBe` ["Alice", "Bob"]

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

        it "selectList" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            insert_ $ person "Bob"
            selectList [] []
          map getName result `shouldBe` ["Alice", "Bob"]

        it "selectKeysList" $ do
          result <- runTestApp backendType $ do
            insert_ $ person "Alice"
            insert_ $ person "Bob"
            selectKeysList @Person [] []
          result `shouldBe` [1, 2]

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

        it "deleteWhere" $ do
          result <- runTestApp backendType $ do
            insertMany_ [person "Alice", person "Bob"]
            deleteWhere [PersonName ==. "Alice"]
            getPeopleNames
          result `shouldBe` ["Bob"]

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

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

        it "parseMigration" $ do
          result <- runTestApp backendType $ do
            setupUnsafeMigration
            parseMigration migration

          let sql = case backendType of
                Sqlite ->
                  [ P.eq
                      ( 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\"))"
                          ]
                      )
                  , P.anything
                  , P.eq (True, "DROP TABLE \"person\"")
                  , P.anything
                  , P.anything
                  , P.eq (False, "DROP TABLE \"person_backup\"")
                  ]
                Postgresql ->
                  [ P.eq (True, "ALTER TABLE \"person\" DROP COLUMN \"foo\"")
                  ]

          result `shouldSatisfy` P.right (P.list sql)

        it "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' `shouldBe` result

        it "printMigration" $
          runTestApp backendType $ do
            setupUnsafeMigration
            printMigration migration

        it "showMigration" $ do
          result <- runTestApp backendType $ do
            setupUnsafeMigration
            showMigration migration

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

          result `shouldSatisfy` P.list sql

        it "getMigration" $ do
          result <- runTestApp backendType $ do
            setupUnsafeMigration
            getMigration migration

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

          result `shouldSatisfy` P.list sql

        it "runMigration" $ do
          result <- runTestApp backendType $ do
            setupSafeMigration
            runMigration migration
            getSchemaColumnNames backendType "person"
          result `shouldNotSatisfy` P.elem "removed_column"

        it "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
          cols `shouldNotSatisfy` P.elem "removed_column"
          withQuiet `shouldBe` withSilent

        it "runMigrationSilent" $ do
          (sqlPlanned, sqlExecuted, cols) <- runTestApp backendType $ do
            setupSafeMigration
            sqlPlanned <- getMigration migration
            sqlExecuted <- runMigrationSilent migration
            cols <- getSchemaColumnNames backendType "person"
            return (sqlPlanned, sqlExecuted, cols)
          cols `shouldNotSatisfy` P.elem "removed_column"
          sqlExecuted `shouldBe` sqlPlanned

        it "runMigrationUnsafe" $ do
          result <- runTestApp backendType $ do
            setupUnsafeMigration
            runMigrationUnsafe migration
            getSchemaColumnNames backendType "person"
          result `shouldNotSatisfy` P.elem "removed_column"

        it "runMigrationUnsafeQuiet" $ do
          (sqlPlanned, sqlExecuted, cols) <- runTestApp backendType $ do
            setupUnsafeMigration
            sqlPlanned <- getMigration migration
            sqlExecuted <- runMigrationUnsafeQuiet migration
            cols <- getSchemaColumnNames backendType "person"
            return (sqlPlanned, sqlExecuted, cols)
          cols `shouldNotSatisfy` P.elem "removed_column"
          sqlExecuted `shouldBe` sqlPlanned

        it "getFieldName" $ do
          result <-
            runTestApp backendType $
              getFieldName PersonName
          result `shouldBe` "\"name\""

        it "getTableName" $ do
          result <-
            runTestApp backendType $
              getTableName $
                person "Alice"
          result `shouldBe` "\"person\""

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

          result `shouldBe` ["Alice", "Bob"]

        it "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 (getFirstPersistValue @Text) .| Conduit.sinkList
          result `shouldBe` ["Alice", "Bob"]

        it "rawQuery" $ do
          result <- runTestApp backendType $ do
            insertMany_ [person "Alice", person "Bob"]
            runConduit $ rawQuery "SELECT name FROM person" [] .| Conduit.mapC (getFirstPersistValue @Text) .| Conduit.sinkList
          result `shouldBe` ["Alice", "Bob"]

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

        it "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 `shouldBe` 1
          map nameAndAge people `shouldBe` [("Alice", 100), ("Bob", 0)]

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

        it "transactionSave" $ do
          result1 <- runTestApp backendType $ do
            catchTestError $ withTransaction $ do
              insert_ $ person "Alice"
              insertAndFail $ person "Bob"
            getPeopleNames
          result1 `shouldBe` []

          result2 <- runTestApp backendType $ do
            catchTestError $ withTransaction $ do
              insert_ $ person "Alice"
              transactionSave
              insertAndFail $ person "Bob"
            getPeopleNames
          result2 `shouldBe` ["Alice"]

        it "transactionSaveWithIsolation" $ do
          result1 <- runTestApp backendType $ do
            catchTestError $ withTransaction $ do
              insert_ $ person "Alice"
              insertAndFail $ person "Bob"
            getPeopleNames
          result1 `shouldBe` []

          result2 <- runTestApp backendType $ do
            catchTestError $ withTransaction $ do
              insert_ $ person "Alice"
              transactionSaveWithIsolation Serializable
              insertAndFail $ person "Bob"
            getPeopleNames
          result2 `shouldBe` ["Alice"]

        it "transactionUndo" $ do
          result <- runTestApp backendType $ withTransaction $ do
            insert_ $ person "Alice"
            transactionUndo
            getPeopleNames
          result `shouldBe` []

        it "transactionUndoWithIsolation" $ do
          result <- runTestApp backendType $ withTransaction $ do
            insert_ $ person "Alice"
            transactionUndoWithIsolation Serializable
            getPeopleNames
          result `shouldBe` []

      describe "Interop with third-party Persistent libraries" $ do
        it "unsafeLiftSql" $ do
          let alice = person "Alice"
          result <- runTestApp backendType $ do
            insert_ alice
            esqueletoSelect $
              E.from $
                E.table @Person
          result `shouldBe` [Entity 1 alice]

{- Persistent helpers -}

getFirstPersistValue :: (PersistField a) => [PersistValue] -> a
getFirstPersistValue = \case
  [] -> error "Unexpectedly got no values"
  v : _ -> fromPersistValueOrFail v

fromPersistValueOrFail :: (PersistField a) => PersistValue -> a
fromPersistValueOrFail = 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 `shouldBe` Left TestError

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