{-| 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))