Easy database access with Haskell and postgresql-simple

Avi Press | August 25, 2017

Working with a database can be a challenging task for newcomers to Haskell. For me, it became a non trivial hurdle on the road to productivity with the language. With libraries like persistent, unfamiliar Template Haskell combined with the complex types involved ramped the learning curve making it hard to get much done.

I recently found myself needing to write a script to import some data from a data dump file into postgresql (It was a dump of artists from musicbrainz if you're interested). This time, I gave `postgrseql-simple` a shot and it was surprisingly easy to use! My script ended up being small and quick to write. If you find yourself just needing a simple and straightforward way to talk to a database, it's a great choice. I'll give a brief intro to what I did here, and maybe it can help others get the ball rolling for their own projects.

Mapping a data type to a database table and getting a connection is easy:

module Main where

import           Control.Monad.IO.Class
import           Data.String.Utils
import           Database.PostgreSQL.Simple
import           Database.PostgreSQL.Simple.FromRow
import           Database.PostgreSQL.Simple.ToRow
import           Database.PostgreSQL.Simple.ToField

-- The data type we want as a table in the database. 
-- We define our data models as regular data 
-- records, no template haskell needed!
data Artist = Artist
  { musicbrainzId :: String
  , name :: String
  }

-- Define a mapping from rows to be unmarshaled 
-- into your data type. Just use one literal 
-- `<*> field` for each field in your record
instance FromRow Artist where
    fromRow = Artist <$> field <*> field


-- ... And the same for marshaling your datatype into a db row
instance ToRow Artist where
  toRow a = [toField (musicbrainzId a), toField (name a)]

We can easily grab a connection like so:

main = do
  conn <- connect defaultConnectInfo { connectUser = "my_username"
                                      , connectDatabase = "my_dbname"
                                      }
  -- do some stuff

Now we're ready to go! Lets write a function that can insert an artist record. We can now write:

insertArtist :: Connection -> Artist -> IO Int64
insertArtist conn artist =
  execute conn
    "insert into artist (musicbrainz_id, name) values (?, ?)" artist

Some things I like about this library:

  • Our DB facing methods only need the IO monad, so it's less likely you'll need to reach for monad transformers to use this productively.
  • The database connection is explicitly passed around rather than obfuscated behind a more complicated type.

The trade-off in type safety is well worth the ease of use if, like me, you're not an expert Haskell developer. To tie everything together and make things a little more usable, lets also make our insert function print its progress in the entire import:

main = do
  conn <- connect defaultConnectInfo { connectUser = "my_username"
                                      , connectDatabase = "my_dbname"
                                      }
  let artists = ... -- some unrelated file parsing
  mapM_ (uncurry $ insertAndPrintCount conn) $ zip [1 ..] artists
  close conn
  return ()

insertAndPrintCount :: Connection -> Int -> Artist -> IO Int64
insertAndPrintCount  conn count artist = do
  insertedId <- execute conn
    "insert into artist (musicbrainz_id, name) values (?, ?)" artist
  printf "total inserted: %d\n" count
  return insertedId