circuithub / rel8

Hey! Hey! Can u rel8?
https://rel8.readthedocs.io
Other
154 stars 39 forks source link

Return type for `Insert` and `Select` #224

Closed worm2fed closed 1 year ago

worm2fed commented 1 year ago

Hello.

I started to use rel8 not so long time ago and there is still a lot of new for me. Maybe I just didn't found an answer in ;documentation or maybe there is no such option for now.

But I have feeling that what I do should be in a bit different way. So, hope you can make it clear for me.

1) Often I want to insert a single row to table and I want my function to return that created row:

createTeamRoleQ :: TeamR -> TeamRoleName -> Sql.Transaction TeamRoleR

but by default what I can get is

createTeamRoleQ :: TeamR -> TeamRoleName -> Sql.Transaction [TeamRoleR]

so, what I usually do - is just applying listToMaybe to result like this:

createTeamRoleQ :: TeamR -> TeamRoleName -> Sql.Transaction (Maybe TeamRoleR)
createTeamRoleQ Team{tTeamPk} name =
  fmap listToMaybe . Sql.statement () . Sql.insert $
    Sql.Insert
      { into = teamRoleSchema
      , rows =
          Sql.values
            [ TeamRole
                { trTeamRolePk = Sql.nextPk teamRoleSchema
                , trTeamPk = Sql.lit tTeamPk
                , trName = Sql.lit name
                }
            ]
      , onConflict = Sql.Abort
      , returning = Sql.Projection id
      }

I need Maybe here, because createTeamRoleQ is part of transaction:

createTeam
  :: (WithDb m, WithError m)
  => AccountPk
  -> TeamScopePk
  -> TeamName
  -> m TeamR
createTeam aPk teamScopePk name =
  -- throw error on top level of computation
  throwOnNothingM Logger.Error (ApiError "can't get team in response")
    . Db.runTransaction
    $ runMaybeT
      ( do
          -- create team
          team <- MaybeT insertTeam
          -- create default team roles
          roles <- mapM (MaybeT . createTeamRoleQ team) defaultTeamRoleNames
          guard $ length defaultTeamRoleNames == length roles
          -- assign creator to created roles
          assignees <- mapM (MaybeT . assignToTeamRoleQ aPk) roles
          guard $ length defaultTeamRoleNames == length assignees
          pure team
      )
      -- rollback transaction in case on any step Nothing occurs
      >>= maybe (Sql.condemn >> pure Nothing) (pure . pure)
  where
    insertTeam :: Sql.Transaction (Maybe TeamR)
    insertTeam =
      fmap listToMaybe
        . Sql.statement ()
        . Sql.insert
        $ Sql.Insert
          { into = teamSchema
          , rows =
              Sql.values
                [ Team
                    { tTeamPk = Sql.nextPk teamSchema
                    , tTeamScopePk = Sql.lit teamScopePk
                    , tInitiatorPk = Sql.lit aPk
                    , tName = Sql.lit name
                    }
                ]
          , onConflict = Sql.Abort
          , returning = Sql.Projection id
          }

In this example I apply listToMaybe en each Insert statement to convert list to Maybe value, and in case on some step there is Nothing - I rollback whole transaction. In terms of types it seems correct, but in fact I want to get not even Maybe TeamRoleR but single TeamRoleR, this way I do not need to apply all this listToMaybe and there is no need in MaybeT.

I've tried to replace Sql.values with pure, but I still have list in result type. What do you think about it?

2) Second concern is about Select Yes, it's seems natural to get list on Select query, but there is some cases:

and then to execute it we use Sql.select again and receive list of Bool I solved it by defining another helper:

-- | Helper to check existence.
selectExists :: Query exprs -> Statement () Bool
selectExists = fmap Relude.and . Sql.select . Sql.exists
{-# INLINE selectExists #-}

Maybe I'm missing something but I want to understand whether I'm using your library in right way. Thanks.