postgres-tmp-0.2.0: src/Database/PostgreSQL/Tmp.hs
{-| Create temporary postgresql databases.
The main usecase for this are tests where you don’t want to assume that a certain database exists.
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Database.PostgreSQL.Tmp
( defaultDB
, DBInfo(..)
, withTmpDB
, withTmpDB'
-- * Low level APIs
, createTmpDB
, dropTmpDB
, newRole
, dropRole
, newDB
, dropDB
) where
import Control.Applicative (pure)
import Control.Exception
import Data.ByteString (ByteString)
import Data.Coerce
import Data.Int
import Data.Monoid
import qualified Data.Text as T
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.Types
-- | Connection string for the @postgres@ database owned by the
-- @postgres@ user
defaultDB :: ByteString
defaultDB = "dbname='postgres' user='postgres'"
-- | The data necessary to connect to the temporary database
data DBInfo =
DBInfo {dbName :: T.Text
,roleName :: T.Text} deriving (Show,Read,Eq,Ord)
-- | Convenience wrapper for 'withTmpDB'' using 'defaultDB'
withTmpDB :: (DBInfo -> IO a) -> IO a
withTmpDB = withTmpDB' defaultDB
-- | Create a temporary database and a temporary role that the
-- callback can operate on. After the action has finished the database
-- and the role are destroyed.
--
-- This is a `bracket`-style wrapper around 'createTmpDB' and 'dropTmpDB'
--
-- This function assumes that the connection string points to a
-- database containing the tables called @pg_roles@ and @pg_database@
-- and that the user has the @CREATEDB@ and @CREATEROLE@ privileges.
withTmpDB' :: ByteString -> (DBInfo -> IO a) -> IO a
withTmpDB' conStr f = bracket (createTmpDB conStr) dropTmpDB (\(_,dbInfo) -> f dbInfo)
-- | Create a temporary database and a temporary role.
--
-- To destroy the database and the role use `dropTmpDB`.
--
-- This function assumes that the connection string points to a
-- database containing the tables called @pg_roles@ and @pg_database@
-- and that the user has the @CREATEDB@ and @CREATEROLE@ privileges.
createTmpDB :: ByteString -> IO (Connection, DBInfo)
createTmpDB conStr = do
conn <- connectPostgreSQL conStr
role <- newRole conn
db <- newDB conn role
pure (conn, DBInfo {dbName = db,roleName = role})
-- | Destroy the database and the role created by `createTmpDB`.
dropTmpDB :: (Connection, DBInfo) -> IO ()
dropTmpDB (conn, DBInfo db role) = do
_ <- dropDB conn db
_ <- dropRole conn role
close conn
-- | Create a new role that does not already exist and return its name.
--
-- The new role does not have a password and has the @CREATEDB@
-- privilege. The database that the connection points to is assumed to
-- contain a table called @pg_roles@ with a @rolname@ column.
newRole :: Connection -> IO T.Text
newRole conn =
do (roles :: [Only T.Text]) <- query_ conn "SELECT rolname FROM pg_roles"
let newName = freshName "tmp" (coerce roles)
_ <- execute conn "CREATE USER ? WITH CREATEDB" (Only (Identifier newName))
pure newName
-- | Drop the role.
dropRole :: Connection -> T.Text -> IO Int64
dropRole conn name = execute conn "DROP ROLE ?" (Only (Identifier name))
-- | Create a new database that is owned by the user.
newDB :: Connection -> T.Text -> IO T.Text
newDB conn role =
do (dbNames :: [Only T.Text]) <- query_ conn "SELECT datname FROM pg_database"
let newName = freshName "tmp" (coerce dbNames)
_ <- execute conn "CREATE DATABASE ? OWNER ?" (Identifier newName,Identifier role)
pure newName
-- | Drop the database.
dropDB :: Connection -> T.Text -> IO Int64
dropDB conn name =
execute conn "DROP DATABASE ?" (Only (Identifier name))
-- | Create a fresh name that is not in the list of already existing names.
--
-- The fresh name is generated by appending a number to the supplied
-- template.
freshName :: T.Text -> [T.Text] -> T.Text
freshName template existingNames = loop 0
-- We could use a Set here to speed up the lookup, however the
-- construction of that Set is linear as well so it would only pay off
-- if at least one of the lookups fails.
where loop :: Int -> T.Text
loop i =
if (template <> T.pack (show i)) `elem` existingNames
then loop (i + 1)
else (template <> T.pack (show i))