Open nlinker opened 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"
I see, thank you. Would you mind if I send a PR with readme
file clarifying the rationale of not having IO
?
Sure
What about a MonadError
instance for bailing out? That would never be retried, so wouldn't have the same problems as MonadIO
.
Hmm, with MonadError
you have to pick the error type upfront, MonadThrow
would work I suppose.
Can condemn
be what you're looking for?
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.
Yes, it seems to be a subject worth discussing in a dedicated thread. Thanks for considering
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.
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.
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
.
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.
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.
@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.
@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".
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.
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:
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:So to implement the interpreter for
Transact
it is impossible to use justwithTransaction
, but also necessary the more fine grained APIbeginTransaction
/endTransaction
androllbackTransaction
andTransaction
needs the access toIO
.Thanks.