haskellari / postgresql-simple

Mid-level client library for accessing PostgreSQL from Haskell
Other
85 stars 43 forks source link

instance FromRow a => FromRow (Maybe a) #83

Open derrickbeining opened 2 years ago

derrickbeining commented 2 years ago

I'm kind of new to haskell, so forgive me if the answer to this is obvious, but I was attempting to do a left join in order to get rows :: [LeftEntity :. Maybe RightEntity] but then was informed by the compiler that there's no instance for instance FromRow a => FromRow (Maybe a). So I ended up having parse into an intermediate structure first like [LeftEntity :. IntermediateRightEntityResults] and then transform IntermediateRightEntityResult into Maybe RightEntity. I would really prefer to be able to parse directly into the Maybe, though. Is there reason there isn't an instance for FromRow (Maybe a)? And is there perhaps some other way to do what I want? Or is parsing into a temporary intermediate structure just how things are supposed to be done?

phadej commented 2 years ago

If you select only one column, use Only wrapper.

derrickbeining commented 2 years ago

The RightEntity that I'm trying to parse out of my left join query consists of more than one column, so I can't use the Only wrapper.

Does my question make sense? Here's some example code

data LeftEntity = LeftEntity
    { leId :: Id LeftEntity
    , leName :: Text
    , leAssociatedRightEntityId :: Id RightEntity
    }
    deriving (Show, Generic, ToRow, FromRow)

data RightEntity = RightEntity
    { reId :: Id RightEntity
    , reName :: Text
    , reDescription :: Text
    }
    deriving (Show, Generic, ToRow, FromRow)

getLeftEntityWithAssociatedRightEntity = do
    rows :: [ LeftEntity :. Maybe RightEntity ] <- runSelect [PGQ.sqlExp|
              select
                le.id,
                le.name,
                le.associated_right_entity_id,

                re.id,
                re.name,
                re.uuid

              from left_entity le
              left join right_entity re on re.id = le.associated_right_entity_id
        |]

  pure $
    flip fmap rows $ \(leftEntity :. mRightEntity) ->
      (leftEntity, mRightEntity)

This doesn't compile because there's no instance of FromRow a => FromRow (Maybe a)

So I have to change Maybe RightEntity to something that can collect all RightEntity fields (which are nullable in a left join) like

data RightEntityFields = RightEntityFields
    { reId :: Maybe (Id RightEntity)
    , reName :: Maybe Text
    , reDescription :: Maybe Text
    }
    deriving (Show, Generic, ToRow, FromRow)

And then make a function mkRightEntityFromFields :: RightEntityFields -> Maybe RightEntity and map the results of my query to that.

I'm wanting to know if there's a way to avoid all that boilerplate.

derrickbeining commented 2 years ago

I see there's something close to what I want but it's for tuples:

(FromField a, FromField b) => FromRow (Maybe (a, b))

Someone pointed out to me that I could try uncurrying the constructor I was wanting to parse into and map that over the Maybe (a, b, ...z) to avoid having to define an intermediate structure.

So I could do something like

getLeftEntitiesWithAssociatedRightEntities = do
    rows :: [ LeftEntity :. Maybe RightEntity ] <-
        List.map go <$> runSelect [PGQ.sqlExp|
              select
                le.id,
                le.name,
                le.associated_right_entity_id,

                re.id,
                re.name,
                re.uuid

              from left_entity le
              left join right_entity re on re.id = le.associated_right_entity_id
        |]

  pure $
    flip fmap rows $ \(leftEntity :. mRightEntity) ->
      (leftEntity, mRightEntity)

  where
    go  (left :. maybeRight) = left :. fmap (uncurry3 RightEntity)  maybeRight

That does seem to work and I like that much better than having to define an ad-hoc type to collect intermediate results. But I still wish I could just parse directly into Maybe RightEntity without having to jump through extra hoops. Is there a way to achieve that? Perhaps a new feature to add to the library?

phadej commented 2 years ago

There is nothing wrong in defining ad-hoc types for specific purposes. Types are cheap.

Now as I think of it, the FromRow a => Maybe (FromRow a) is impossible to define as FromRow class doesn't tell how many columns it will consume. RowParser consumes some columns, in LeftEntity :. Maybe RightEntity specific case you know that Maybe part should consume all the rest of columns. But in Maybe X .: Maybe Y that would be wrong.

phadej commented 2 years ago

I see there's something close to what I want but it's for tuples:

(FromField a, FromField b) => FromRow (Maybe (a, b))

That's different. It has FromFields as context, not FromRow, so column count is known.

cimmanon commented 2 years ago

If you enable FlexibleInstances, you can define a Maybe instance for your type. Still a bit of boilerplate, but at least you don't need an intermediate type.

instance FromRow (Maybe RightEntity) where
    fromRow = maybeRightEntity <$> field <*> field <*> field

maybeRightEntity :: Maybe (Id RightEntity) -> Maybe Text -> Maybe Text -> Maybe RightEntity
maybeRightEntity (Just a) (Just b) (Just c) = Just $ RightEntity a b c
maybeRightEntity = Nothing