Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
haskell-gargantext
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
199
Issues
199
List
Board
Labels
Milestones
Merge Requests
12
Merge Requests
12
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
gargantext
haskell-gargantext
Commits
cc2fcadd
Commit
cc2fcadd
authored
Jun 23, 2025
by
Alfredo Di Napoli
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add and document catchDBTxError and handleDBTxError
parent
94e2cbf9
Pipeline
#7694
passed with stages
in 42 minutes and 51 seconds
Changes
2
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
63 additions
and
8 deletions
+63
-8
Transactional.hs
src/Gargantext/Database/Transactional.hs
+61
-5
Transactions.hs
test/Test/Database/Transactions.hs
+2
-3
No files found.
src/Gargantext/Database/Transactional.hs
View file @
cc2fcadd
...
@@ -33,8 +33,10 @@ module Gargantext.Database.Transactional (
...
@@ -33,8 +33,10 @@ module Gargantext.Database.Transactional (
,
mkOpaInsert
,
mkOpaInsert
,
mkOpaDelete
,
mkOpaDelete
-- * Throwing errors (which allows rollbacks)
-- * Throwing
and catching
errors (which allows rollbacks)
,
dbFail
,
dbFail
,
catchDBTxError
,
handleDBTxError
)
where
)
where
import
Control.Exception.Safe
qualified
as
Safe
import
Control.Exception.Safe
qualified
as
Safe
...
@@ -190,6 +192,12 @@ runDBTx (DBTx m) = do
...
@@ -190,6 +192,12 @@ runDBTx (DBTx m) = do
pool
<-
view
connPool
pool
<-
view
connPool
withResourceM
pool
$
\
conn
->
withResourceM
pool
$
\
conn
->
(
withTransactionM
conn
$
foldF
(
evalOp
conn
)
m
)
(
withTransactionM
conn
$
foldF
(
evalOp
conn
)
m
)
-- IMPORTANT: We are catching the exception (after 'withTransactionM' has run, so rollback already
-- happened) and we are rethrowing this via 'throwError', such that application code can catch this
-- via 'catchDBTxError'.
-- /NOTA BENE/: the parenthesis around 'withTransactionM' ARE NOT OPTIONAL! If we remove them, we
-- would be catching this exception from 'foldF', meaning that we wouldn't let 'withTransactionM'
-- handle it, resulting in ROLLBACK NOT HAPPENING!
`
Safe
.
catches
`
`
Safe
.
catches
`
[
Safe
.
Handler
$
\
(
RollbackRequested
err
)
->
throwError
err
]
[
Safe
.
Handler
$
\
(
RollbackRequested
err
)
->
throwError
err
]
...
@@ -200,7 +208,12 @@ runDBTx (DBTx m) = do
...
@@ -200,7 +208,12 @@ runDBTx (DBTx m) = do
runDBQuery
::
(
Show
err
,
Safe
.
Typeable
err
)
=>
DBReadOnly
err
r
a
->
DBTxCmd
err
a
runDBQuery
::
(
Show
err
,
Safe
.
Typeable
err
)
=>
DBReadOnly
err
r
a
->
DBTxCmd
err
a
runDBQuery
(
DBTx
m
)
=
do
runDBQuery
(
DBTx
m
)
=
do
pool
<-
view
connPool
pool
<-
view
connPool
withResourceM
pool
$
\
conn
->
withReadOnlyTransactionM
conn
$
foldF
(
evalOp
conn
)
m
withResourceM
pool
$
\
conn
->
(
withReadOnlyTransactionM
conn
$
foldF
(
evalOp
conn
)
m
)
-- IMPORTANT: Same proviso as for 'runDBTx'. Technically speaking we wouldn't need
-- to throw and catch things for a query, but we are doing so for consistency with 'runDBTx'.
`
Safe
.
catches
`
[
Safe
.
Handler
$
\
(
RollbackRequested
err
)
->
throwError
err
]
-- | The main evaluator, turns our pure operations into side-effects that run into the
-- | The main evaluator, turns our pure operations into side-effects that run into the
-- 'DBCmd'.
-- 'DBCmd'.
...
@@ -237,6 +250,49 @@ queryOne conn q v = do
...
@@ -237,6 +250,49 @@ queryOne conn q v = do
[ ]
->
Safe
.
throwIO
$
userError
"queryOne: no result returned. Check your SQL!"
[ ]
->
Safe
.
throwIO
$
userError
"queryOne: no result returned. Check your SQL!"
_
->
Safe
.
throwIO
$
userError
"queryOne: more than one result returned. Have you used the 'RETURNING' directive?"
_
->
Safe
.
throwIO
$
userError
"queryOne: more than one result returned. Have you used the 'RETURNING' directive?"
{-
Throwing and catching exceptions in a DBTx monad
================================================
It's /VERY/ important to understand the proper way to throw and catch exceptions in a DBTx monad,
as not doing so might lead to footguns.
We need to remember that when we are composing 'DBTx' operations, we are just writing a DSL which
won't get evaluated until we call either 'runDBTx' or 'runDBQuery', therefore if some parts of
our transaction throw an error, we wouldn't know until there.
There are two types of errors we might have, and it's important to be precise in terminology:
1. IO Exception: these are being thrown by the evaluators for SQL queries, i.e. we might have
IO errors being thrown by wrongly-formatted SQL queries or the Postgres DB dying on us for any reason;
These exceptions get caught by 'withTransactionM' which allows proper rollback behavior, but crucially
these kind of exceptions gets rethrown by 'withTransactionM' and must be caught via the classic
exception handlers in upstream code, but the crucial point is that even if we don't catch them, the
transaction has been rolled back successfully;
2. Domain-specific ERRORS (not exceptions, ERRORS!) being thrown within a transaction itself via things like
'nodeError' and friends. These are errors which can be thrown because our transaction code didn't go as
planned (look for the implementation of 'insertNodeWithHyperdata' for a concrete example). These errors
are translated into the evaluator as proper exception but then caught and rethrown via 'throwError', which
is crucial, because it means that them being thrown as an exception means 'withTransactionM' can rollback
as we expect to, but upstream application code can still handle these errors via 'catchError' and friends.
In order to facilitate the handling of this, we expose the 'catchDBTxError' and 'handleDBTxError', which are
just wrappers over 'catchError' -- this is what users should be using if they want to handle domain-specific errors.
But the crucial bit, and let's state this again, is that rollbacks will happen in both scenario, which is
what we want.
-}
catchDBTxError
::
DBTxCmd
err
a
->
(
err
->
DBTxCmd
err
a
)
->
DBTxCmd
err
a
catchDBTxError
=
catchError
handleDBTxError
::
(
err
->
DBTxCmd
err
a
)
->
DBTxCmd
err
a
->
DBTxCmd
err
a
handleDBTxError
=
flip
catchError
--
--
-- Smart constructors
-- Smart constructors
--
--
...
@@ -244,9 +300,6 @@ queryOne conn q v = do
...
@@ -244,9 +300,6 @@ queryOne conn q v = do
-- we are not exposing for information hiding purposes.
-- we are not exposing for information hiding purposes.
--
--
dbFail
::
err
->
DBTx
err
r
b
dbFail
=
DBTx
.
liftF
.
DBFail
mkPGQuery
::
(
PG
.
ToRow
q
,
PG
.
FromRow
a
)
mkPGQuery
::
(
PG
.
ToRow
q
,
PG
.
FromRow
a
)
=>
PG
.
Query
=>
PG
.
Query
->
q
->
q
...
@@ -279,3 +332,6 @@ mkOpaInsert a = DBTx $ liftF (OpaInsert a id)
...
@@ -279,3 +332,6 @@ mkOpaInsert a = DBTx $ liftF (OpaInsert a id)
mkOpaDelete
::
Delete
a
->
DBUpdate
err
a
mkOpaDelete
::
Delete
a
->
DBUpdate
err
a
mkOpaDelete
a
=
DBTx
$
liftF
(
OpaDelete
a
id
)
mkOpaDelete
a
=
DBTx
$
liftF
(
OpaDelete
a
id
)
dbFail
::
err
->
DBTx
err
r
b
dbFail
=
DBTx
.
liftF
.
DBFail
test/Test/Database/Transactions.hs
View file @
cc2fcadd
...
@@ -48,7 +48,6 @@ import Test.Tasty.HUnit hiding (assert)
...
@@ -48,7 +48,6 @@ import Test.Tasty.HUnit hiding (assert)
import
Text.RawString.QQ
import
Text.RawString.QQ
import
Gargantext.Database.Action.User
import
Gargantext.Database.Action.User
import
Gargantext.Database.Query.Table.Node.Error
import
Gargantext.Database.Query.Table.Node.Error
import
Control.Monad.Except
(
handleError
)
--
--
-- For these tests we do not want to test the normal GGTX database queries, but rather
-- For these tests we do not want to test the normal GGTX database queries, but rather
...
@@ -265,7 +264,7 @@ testRollback env = runTestDBTxMonad env $ do
...
@@ -265,7 +264,7 @@ testRollback env = runTestDBTxMonad env $ do
liftIO
$
counterValue
initialCounter
`
shouldBe
`
1
liftIO
$
counterValue
initialCounter
`
shouldBe
`
1
-- Let's do another transaction where at the very last instruction we
-- Let's do another transaction where at the very last instruction we
-- fail.
-- fail.
handleError
(
\
(
_
::
IOException
)
->
pure
()
)
$
runDBTx
$
do
handle
DBTx
Error
(
\
(
_
::
IOException
)
->
pure
()
)
$
runDBTx
$
do
_x'
<-
stepCounter
(
counterId
initialCounter
)
_x'
<-
stepCounter
(
counterId
initialCounter
)
dbFail
$
Prelude
.
userError
"urgh"
dbFail
$
Prelude
.
userError
"urgh"
...
@@ -320,7 +319,7 @@ testGGTXErrorRollback env = runTestMonadM @NodeError env $ do
...
@@ -320,7 +319,7 @@ testGGTXErrorRollback env = runTestMonadM @NodeError env $ do
void
$
(
runDBTx
$
do
void
$
(
runDBTx
$
do
void
$
updateUserEmail
(
insertedUr
{
userLight_email
=
"alfredo@bar.com"
})
void
$
updateUserEmail
(
insertedUr
{
userLight_email
=
"alfredo@bar.com"
})
nodeError
$
NoRootFound
-- it doesn't matter which exception
nodeError
$
NoRootFound
-- it doesn't matter which exception
)
`
catchError
`
\
(
_e
::
NodeError
)
->
pure
()
-- swallow it.
)
`
catch
DBTx
Error
`
\
(
_e
::
NodeError
)
->
pure
()
-- swallow it.
-- let's check that the email hasn't been changed.
-- let's check that the email hasn't been changed.
insertedUr'
<-
runDBQuery
$
getUserLightDB
(
UserName
"alfredo"
)
insertedUr'
<-
runDBQuery
$
getUserLightDB
(
UserName
"alfredo"
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment