nikita-volkov / hasql-transaction

A composable abstraction over retriable transactions for Hasql
http://hackage.haskell.org/package/hasql-transaction
MIT License
12 stars 15 forks source link

Add an MonadIO instance to Transaction and open begin/rollback/commit functions #7

Open nlinker opened 7 years ago

nlinker commented 7 years ago

Transactions in a normal real-world applications could consist of not only database calls, but also be file system-related or network-related actions. Also, there could be the following code:

  user <- readFromDatabase id
  if user & type == Admin then rollbackTransaction
  else commitTransaction

It seems impossible to write the code given the current version of the library.

Another point is that the separating begin/rollback/commit are necessary, when we want to implement a DSL like this:

foo :: (MonadFree TestF m) => m ()
foo = do
  x <- get "k1"
  put "k2" x
  z <- transact $ do
    y <- get "k3"
    log y  -- something not related with DB inside the transaction
    when (y == "42")
      rollback
    put "k4" y
    return (42 :: Int)
  log $ pack $ show z

-- where types are defined like this
type Key = Text
type Val = Text

data TestF n where
  Log :: Text -> n -> TestF n
  Get :: Key -> (Val -> n) -> TestF n
  Put :: Key -> Val -> n -> TestF n
  Transact :: TestFree a -> (a -> n) -> TestF n
  Rollback :: n -> TestF n
  Commit :: n -> TestF n

type TestFree = Free TestF

instance Functor TestF where
  fmap f (Log msg n) = Log msg (f n)
  fmap f (Get k r) = Get k (f . r)
  fmap f (Put k v n) = Put k v (f n)
  fmap f (Transact block r) = Transact block (f . r)
  fmap f (Rollback n) = Rollback (f n)
  fmap f (Commit n) = Commit (f n)

makeFree ''TestF

So to implement the interpreter for Transact it is impossible to use just withTransaction, but also necessary the more fine grained API beginTransaction/endTransaction and rollbackTransaction and Transaction needs the access to IO.

Thanks.

nikita-volkov commented 7 years ago

This library provides an opinionated higher-level abstraction over transactions. One of its purposes is exactly to abstract over the explicit "begin/commit/rollback" lifecycle. IO is prohibited because the code in transaction can be executed multiple times when it is automatically retried in case of conflicts.

It seems like what you're looking for is simply a lower-level abstraction, or even no abstraction at all, because:

import qualified Hasql.Session as A

begin :: Session ()
begin = sql "begin"

commit :: Session ()
commit = sql "commit"

rollback :: Session ()
rollback = sql "rollback"
nlinker commented 7 years ago

I see, thank you. Would you mind if I send a PR with readme file clarifying the rationale of not having IO?

nikita-volkov commented 7 years ago

Sure

seagreen commented 5 years ago

What about a MonadError instance for bailing out? That would never be retried, so wouldn't have the same problems as MonadIO.

seagreen commented 5 years ago

Hmm, with MonadError you have to pick the error type upfront, MonadThrow would work I suppose.

nikita-volkov commented 5 years ago

Can condemn be what you're looking for?

seagreen commented 5 years ago

Not in this case. My goal is to be able to write error throwing functions like guard400 :: MonadThrow m => Bool -> m and use them with both the main app monad and within a Transaction.

However, I'm not sure this is a good idea, so it doesn't need to block closing this issue. I can experiment with it and make a new issue if it turns out to be helpful.

nikita-volkov commented 5 years ago

Yes, it seems to be a subject worth discussing in a dedicated thread. Thanks for considering

domenkozar commented 4 years ago

Conflicts can only happen when the whole monad has ran (and commit finally happens).

If they need to be retried the logic can submit exactly the same inputs to the backend without retrying the monad.

If those exact inputs can't be committed again (due to DB inconsistencies), it should fail.

In practice this would hold onto memory for inputs a bit longer, but allow much more interesting uses of transactions.

Profpatsch commented 3 years ago
import qualified Hasql.Session as A

begin :: Session ()
begin = sql "begin"

commit :: Session ()
commit = sql "commit"

rollback :: Session ()
rollback = sql "rollback"

I think adding these as functions to the library would be worthwhile, since they belong to the postgres translaction DSL. For now I’m gonna copy them to our codebase.

Profpatsch commented 3 years ago

Actually now that I think about it, the signature should rather be:

commit :: Transaction a
commit = …

rollback :: Transaction a
rollback = …

Because inside a transaction, after the commit or rollback, no further statement is executed.

I noticed that because I had:

do
  results <- select …
  as <- case results of
    [] -> rollback
    as -> pure as
  insert $ … as …

which will not typecheck because rollback :: Transaction () and pure as :: Transaction [A].

This is similar to how die :: String -> IO a returns any a.

Profpatsch commented 3 years ago

I think this will require a change in hasql though, because the closest is Decoders.noResult, which returns a (); this is reasonable for a general SQL statement, cause:

begin;
select …
commit;
insert …

there can be more statements after the transaction block end, so you can’t have it return any a by default.

Profpatsch commented 3 years ago

I wrote a function

rollback :: Text -> Transaction Error
rollback msg = do
  sql "rollback"
  newError msg

So I can propagate the rollback on the surface level:

do
  select …
  case results of
    [] -> Left <$> rollback "error message"
    res -> Right <$> insert … res …

So I get:

> run … 
WARNING:  there is no transaction in progress
Left (Error ["error message"])

The WARNING stems from postgres, which runs ROLLBACK and then also the COMMIT of the Transaction block. (see https://github.com/nikita-volkov/hasql-transaction/issues/13 for another example where this happened.)

I think we need to integrate ROLLBACK into the abstraction if we want to correctly support it here. Maybe even supporting an error message that can be checked for when running the transaction.

andremarianiello commented 2 years ago

@nikita-volkov What is the status of this issue? If it is indeed the case that Transaction cannot support IO, then I would like to make another transaction type, tentatively called TransactionIO that has a MonadIO instance but no builtin retry semantics. What do you think of this idea? It doesn't have to live in hasql-transaction, but I would like to provide a similar api.

nikita-volkov commented 2 years ago

@andremarianiello Sure! Go ahead. No plans to add MonadIO here. This package being opinionated and there possibly being other viable approaches is the reason why it is just an extension-library and not part of core "hasql".

andremarianiello commented 2 years ago

I have released https://hackage.haskell.org/package/hasql-transaction-io-0.1.0.0 which provides TransactionIO and CursorTransactionIO. These aim to be like Transaction and CursorTransaction with MonadIO instances and without retryability. I created it to support https://hackage.haskell.org/package/hasql-streams-core, but hopefully can be helpful for others as well.