/freer-persistent

freer-effects wrapper for persistent queries

Primary LanguageHaskellBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

freer-persistent

freer-effects wrapper for persistent queries


freer-persistent uses free monads and extensible effects to turn persistent queries into easily testable effects.

It defines a new effect called Db that corresponds to a subset of the queries performable by persistent. When you use only this subset, you'll be able to mock the database to write 100% pure tests. If you need to use a more advanced query, that's ok too, but that'll require the SQL effect. This effect cannot be mocked out, and that'll be reflected in the type system, so you know which of your functions are purely testable and which ones need test databases to test.

Here's an example:

import Control.Monad.Freer.Sql

getUsers :: (Member Db r) => Eff r '[Entity User]
getUsers = do
  selectList [] [Desc UserAge]

If we want to run this effect against a DB:

runM . {otherEffectRunners} . runSql {connectionPool} runDb $ getUsers

Running the SQL effect interprets to IO.

Since selectList is one of the simple persistent queries supported, we can also interpret getUsers against a mock db:

import Control.Monad.Freer.Fresh

emptyDb = makeDb []

dbWithUsers = makeDb [ MkTable [ (1, User "ben" 26)
                               , (2, User "alice" 34)
                               ]
                     ]

emptyDbResults = runFresh' 3 . mockDb emptyDb $ getUsers
-- (resultDb, [])

dbWithUsersResults = runFresh' 3 . mockDb dbWithUsers $ getUsers
-- (resultDb, [Entity 2 (User "alice" 34), Entity 1 (User "ben" 26)])

Current supported queries are:

  • insert
  • insertMany
  • insertUnique
  • get
  • selectFirst
  • selectList
  • update

Control.Monad.Freer.Sql exports the other queries from Database.Persistent as well to make it easy to use a combination of simple and more complicated queries:

celebrateBirthday :: (Member Db r, Member SQL r) => Eff r () 
celebrateBirthday = do
  benIds <- fmap entityKey <$> selectList [UserName ==. "ben"] []
  sql $ updateWhere [UserId <-. benIds] [UserAge +=. 1]

sql wraps the query directly from persistent. Notice how we end up with two effects here, one for the mockable db interactions and one for everything else.

To run:

runM . {otherEffectRunners} . runSql {connectionPool} . runDb $ celebrateBirthday

If you wanted to (though I don't have a good reason why you would of the top of my head, hmu if you got something), you could mock the Db effect and still run the leftover SQL effects.

runM . {otherEffectRunners} . runSql {connectionPool} . runFresh' 0 . mockDb emptyDb $ celebrateBirthday