postgresqlhaskellservantopaleye

Opaleye newtype


One of the fields in my datatype for a table in my PostgreSQL database is a newtype wrapping UUID called ItemId.

import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import Data.DateTime (DateTime)
import Data.UUID
import GHC.Generics
import qualified Opaleye as O
import Data.Text (pack, Text)

newtype ItemId = ItemId UUID
  deriving (Show, Eq, Generic)

toItemId :: UUID -> ItemId
toItemId = ItemId

fromItemId :: ItemId -> UUID
fromItemId (ItemId x) = x

data Item' id name desc num most
  = Item {
    _itemId          :: id,
    _itemName        :: name,
    _itemDesc        :: desc,
    _numTimesOrdered :: num,
    _mostRecentOrder :: most
 }

type ItemRead = Item' ItemId Text Text Int DateTime
type ItemWrite = Item' (Maybe ItemId) Text Text (Maybe Int) (Maybe DateTime)
type ItemColRead = Item' (O.Column O.PGUuid)
                         (O.Column O.PGText)
                         (O.Column O.PGText)
                         (O.Column O.PGInt4)
                         (O.Column O.PGTimestamptz)
type ItemColWrite = Item' (Maybe (O.Column O.PGUuid))
                          (O.Column O.PGText)
                          (O.Column O.PGText)
                          (Maybe (O.Column O.PGInt4))
                          (Maybe (O.Column O.PGTimestamptz))

$(makeAdaptorAndInstance "pItem" ''Item')

itemTable :: O.Table ItemColWrite ItemColRead
itemTable = O.Table "items" (pItem Item { _itemId          = O.optional "id"
                                        , _itemName        = O.required "name"
                                        , _itemDesc        = O.required "desc"
                                        , _numTimesOrdered = O.optional "numTimesOrdered"
                                        , _mostRecentOrder = O.optional "mostRecentOrder"
                                        })

itemToPG :: ItemWrite -> ItemColWrite
itemToPG = pItem Item { _itemId          = const Nothing
                      , _itemName        = O.pgStrictText
                      , _itemDesc        = O.pgStrictText
                      , _numTimesOrdered = const Nothing
                      , _mostRecentOrder = const Nothing
                      }

However, when I compile my project, GHC throws:

/home/gigavinyl/Projects/ordermage/src/Api/Item.hs:34:3: error:
    • No instance for (O.QueryRunnerColumnDefault O.PGUuid ItemId)
        arising from a use of ‘O.runInsertManyReturning’
    • In the second argument of ‘(<$>)’, namely
        ‘O.runInsertManyReturning con itemTable [itemToPG item] _itemId’
      In the second argument of ‘($)’, namely
        ‘listToMaybe
         <$> O.runInsertManyReturning con itemTable [itemToPG item] _itemId’
      In the expression:
        liftIO
        $ listToMaybe
          <$> O.runInsertManyReturning con itemTable [itemToPG item] _itemId

where src/Api/Item.hs is:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}

module Api.Item where

import Control.Monad.IO.Class (liftIO)
import Data.Maybe (listToMaybe)
import Database.PostgreSQL.Simple (Connection)
import Models.Item
import Queries.Item
import Servant
import qualified Opaleye as O

type ItemApi =
   Get '[JSON] [ItemRead]                                      :<|>
   Capture "itemId" ItemId     :> Get '[JSON] (Maybe ItemRead) :<|>
   ReqBody '[JSON] ItemWrite   :> Post '[JSON] (Maybe ItemId)

itemServer :: Connection -> Server ItemApi
itemServer con =
  getItems con    :<|>
  getItemById con :<|>
  postItem con

getItems :: Connection -> Handler [ItemRead]
getItems con = liftIO $ O.runQuery con itemsQuery

getItemById :: Connection -> ItemId -> Handler (Maybe ItemRead)
getItemById con itemID = liftIO $ listToMaybe <$> O.runQuery con (itemByIdQuery itemID)

postItem :: Connection -> ItemWrite -> Handler (Maybe ItemId)
postItem con item = liftIO $ listToMaybe <$>
  O.runInsertManyReturning con itemTable [itemToPG item] _itemId

I'm still fairly new to Haskell but the issue appears to be that Opaleye doesn't know how to convert ItemId into a PGUuid but I know it can convert UUID to PGUuid. How would I go about writing the instance to allow Opaleye to do this conversion?


Solution

  • the issue appears to be that Opaleye doesn't know how to convert ItemId into a PGUuid but I know it can convert UUID to PGUuid

    It's the other way round. It's trying to convert a Column PGUuid into an ItemId and it only knows how to convert it into a UUID. One approach is to add the instance yourself:

    instance O.QueryRunnerColumnDefault O.PGUuid ItemId where
      queryRunnerColumnDefault =
             O.queryRunnerColumn id ItemId queryRunnerColumnDefault
    

    Another approach would be to make ItemId polymorphic:

    newtype ItemId' a = ItemId a
    $(makeAdaptorAndInstance "pItemId" ''ItemId')
    

    and then you can use it on both the Haskell side and Opaleye side without having to write the extra instance.