I have been trying to run a left join using Opaleye in a project but I'm not being able to make the code compile. I start with two "models" which represent tables that are associated:
First:
data ModelA' a b = Model { primA :: a, foreignA :: b }
type ModelA = ModelA' UUID UUID
type ModelAColumn = ModelA' (Column PGUuid) (Column (Nullable PGUuid))
$(makeAdaptorAndInstance "pModelA" ''ModelA')
table :: Table ModelAColumn ModelAColumn
table = Opaleye.table "model_a" $ pModelA (ModelA (tableColumn "uuid") (tableColumn "foreign"))
And also:
data ModelB' a b = Model { primB :: a, valB :: b }
type ModelB = ModelB' UUID String
type ModelBColumn = ModelB' (Column PGUuid) (Column PGText)
$(makeAdaptorAndInstance "pModelB" ''ModelB')
table :: Table ModelBColumn ModelBColumn
table = Opaleye.table "model_b" $ pModelB (ModelB (tableColumn "uuid") (tableColumn "val"))
As the types reflect, ModelA can have no ModelB associated.
I need a query to obtain pairs of (ModelA, Maybe ModelB) given by the left join between the tables on foreignA == primB. I was expecting it to look like:
doJoin :: Connection -> IO [(ModelA, Maybe ModelB)]
doJoin conn = runQuery conn query
where
query :: Query (ModelAColumn, Maybe ModelBColumn)
query = leftJoin (queryTable ModelA.table) (queryTable ModelB.table) (\(ma, mb) -> foreignA ma .== primB mb)
But this does not work. I've also tried multiple variants, in particular I replaced the type signature in query to explicitly state the nullability of the columns at the right:
query :: Query (ModelAColumn, (Column (Nullable PGUuid), Column (Nullable PGText))
But this fails with:
No instance for Data.Profunctor.Product.Default.Class.Default Opaleye.Internal.Join.NullMaker ModelBColumn (Column (Nullable PGUuid), Column (Nullable PGText).
How can I make this query in Opaleye?
There are a couple of misunderstandings here. I produced a full working version below.
Firstly, the return type of the leftJoin
is not
Query (ModelAColumn, Maybe ModelBColumn)
You have to do
type ModelBNullableColumn = ModelB' (Column (Nullable PGUuid))
(Column (Nullable PGText))
and then use
Query (ModelAColumn, ModelBNullableColumn)
Secondly, the return type of the runQuery
is not
IO [(ModelA, Maybe ModelB)]
You have to do
type ModelBMaybe = ModelB' (Maybe UUID) (Maybe String)
and use
IO [(ModelA, ModelBMaybe)]
The reason for these differences is that Nullable
and Maybe
must be applied directly to every column and value in the ModelBColumn
and ModelB
not to the values as a whole.
(There are also some weird syntax errors like
ModelA { tableColumn "uuid", tableColumn "foreign" }
which means your code has no hope of compiling. I fixed those, too.)
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Opaleye hiding (table)
import qualified Opaleye
import Data.Profunctor.Product.TH
import Database.PostgreSQL.Simple hiding (Query)
import Data.UUID
data ModelA' a b = ModelA { primA :: a, foreignA :: b }
type ModelA = ModelA' UUID (Maybe UUID)
type ModelAColumn = ModelA' (Column PGUuid) (Column (Nullable PGUuid))
$(makeAdaptorAndInstance "pModelA" ''ModelA')
modelAtable :: Table ModelAColumn ModelAColumn
modelAtable = Opaleye.table "model_a" $ pModelA ModelA { primA = tableColumn "uuid", foreignA = tableColumn "foreign" }
data ModelB' a b = ModelB { primB :: a, valB :: b }
type ModelB = ModelB' UUID String
type ModelBMaybe = ModelB' (Maybe UUID) (Maybe String)
type ModelBColumn = ModelB' (Column PGUuid) (Column PGText)
type ModelBNullableColumn = ModelB' (Column (Nullable PGUuid)) (Column (Nullable PGText))
$(makeAdaptorAndInstance "pModelB" ''ModelB')
modelBtable :: Table ModelBColumn ModelBColumn
modelBtable = Opaleye.table "model_b" $ pModelB ModelB { primB = tableColumn "uuid", valB = tableColumn "val" }
doJoin :: Connection -> IO [(ModelA, ModelBMaybe)]
doJoin conn = runQuery conn query
where
query :: Query (ModelAColumn, ModelBNullableColumn)
query = leftJoin (queryTable modelAtable) (queryTable modelBtable) (\(ma, mb) -> matchNullable (pgBool False) (.== primB mb) (foreignA ma))
main :: IO ()
main = return ()