Pareto Optimal Dev

Which Haskell database library manages complexity best in 2022?

Now that I’ve used Esqueleto professionally in anger, I would understand anyone who used it and then swore off static types forever. - @ttuegel

Seeing this quote got me thinking about writing this very node again since I:

  • frequently use Esqueleto
  • have used Beam in the past
  • have looked on longingly at rel8 (built on top of Opaleye)
  • Recently annoyed with complexity while trying to teach others to use soon to be default Esqueleto.Experimental

I had the thought on a mentally slow day where I was struggling to line up types that Esqueleto.Experimental didn’t seem much easier than Beam did and wondered if it could be more complex.

This is my exploration to see if using a complex (rather than simple) example if Esqueleto.Experimental is still the simplest.

Note that I’ll use Microsoft’s northwind database as an example since it’s pretty widely known. If you use NixOS you might be interested in Provisioning a sample database along with postgres in nixos.

Real-world example with some complexity

Typical complexity: Get most expensive product bought in a given order

Modifying a string, inner joins across a couple tables, and an aggregation.

select column_name, data_type, character_maximum_length, column_default, is_nullable
from INFORMATION_SCHEMA.COLUMNS where table_name = 'orders';
select column_name, data_type, character_maximum_length, column_default, is_nullable
from INFORMATION_SCHEMA.COLUMNS where table_name = 'order_details';
select upper(o.ship_name), max(p.unit_price) as expensivest
from orders o
inner join order_details od on od.order_id = o.order_id
inner join products p on p.product_id = od.product_id
group by o.ship_name
order by expensivest desc
limit 5

Esqueleto

define models and enable extensions

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}

module Models where

import           Database.Persist
import           Database.Persist.TH
import Data.Int

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Order sql=orders
    Id sql=order_id
    shipName String
    deriving Show
OrderDetail sql=order_details
    Id (Key OrderId) sql=order_id
    productId Int64
    deriving Show
Product sql=products
    ProductId sql=product_id
    unitPrice Double
|]
:l Models

now try simply querying a table

a few more extensions and imports apparently

:set -XOverloadedStrings
:set -XTypeApplications
:set -XGADTs
:set -XFlexibleContexts
:set +m
import Database.Persist.Postgresql
:t selectList @Orders [] []
import Control.Monad.Reader
import Control.Monad.Reader
import Control.Monad.Logger
import Database.Esqueleto.Experimental
import qualified Database.Esqueleto.Experimental as E

a runDB helper

runDB = runStdoutLoggingT . withPostgresqlConn "port=5435 dbname=northwind user=postgres" . runReaderT

and now the query with persistent

runDB $ selectList @Orders [] [LimitTo 1]

then the simple query with esqueleto

:{
runDB $ select $ do
        orders <- from $ table @Orders
        limit 1
        pure orders
:}

Now let’s write Typical complexity: Get most expensive product bought in a given order with Esqueleto

:t OrderDetail
:{
runDB $ select $ do
  (order :& orderDetail) <- do
    from $ table @Order
      `innerJoin` table @OrderDetail
      `on` (\(order :& orderDetail) ->
               orderDetail ^. OrderDetailId E.==. order ^. OrderId)
  limit 1
  pure (order, orderDetail)
  :}

TODO HERE I’m stuck because of a template haskell bug I think, posted issue here https://github.com/yesodweb/persistent/issues/1353

Beam (not building on nixos)

rel8

load the models

:l rel8models.hs

try simple query

tons of imports/extensions

:set +m
:set -XOverloadedStrings
Right conn <- acquire "user=postgres port=5435 dbname=northwind"
import Hasql.Transaction (statement)
import Hasql.Transaction.Sessions (transaction, Mode(Write), IsolationLevel(ReadCommitted))
import Hasql.Connection (acquire)
import Hasql.Session (run)
import Rel8

simple query

:{
(flip run conn) $ do
  transaction ReadCommitted Write $ do
      statement () $ do
        select $ limit 1 $ do
          each orderSchema
:}

query with a join

:{
(flip run conn) $ do
  transaction ReadCommitted Write $ do
      statement () $ do
        select $ limit 5 $ do
          order <- each orderSchema
          orderDetail <- each orderDetailsSchema
          products <- each productsSchema
          where_ $ orderId order ==. orderDetailsId orderDetail &&. productsId products ==.  orderProductId orderDetail
          return $ do
            maxPrice <- Rel8.max (productUnitPrice products)
            return (shipName order, maxPrice)
:}

Currently above incorrect, might want:

https://hackage.haskell.org/package/rel8-1.0.0.0/docs/Rel8.html#v:listAgg

also useful:

https://github.com/mitchellwrosen/cpe365-in-haskell/blob/f54662a1f5dfc81c530a2000946697325c28d2ce/Cpe365.hs

<interactive>:(763,9)-(770,45): error:
    • Couldn't match type ‘Aggregate (Expr Text, Int64)’
                     with ‘(Expr (Expr Text), Expr Int64)’
        arising from a use of ‘select’
    • In a stmt of a 'do' block:
        select
          $ limit 5
              $ do order <- each orderSchema
                   orderDetail <- each orderDetailsSchema
                   products <- each productsSchema
                   where_
                     $ orderId order ==. orderDetailsId orderDetail
                         &&. productsId products ==. orderProductId orderDetail
                   ....
      In the second argument of ‘($)’, namely
        ‘do select
              $ limit 5
                  $ do order <- each orderSchema
                       orderDetail <- each orderDetailsSchema
                       ....’
      In a stmt of a 'do' block:
        statement ()
          $ do select
                 $ limit 5
                     $ do order <- each orderSchema
                          orderDetail <- each orderDetailsSchema
                          ....

Now let’s write Typical complexity: Get most expensive product bought in a given order with rel8

:{
(flip run conn) $ do
  transaction ReadCommitted Write $ do
      statement () $ do
        select $ limit 1 $ do
          each orderSchema
:}

squeal (not on nixos)

Ok… I love that this is a deep embedding but it’s wayyy uglier than SQL and so damn noisy :/

re-review https://www.williamyaoh.com/posts/2019-12-14-typesafe-db-libraries.html