I have the following scenario:
- I want to delete a row
- There may be a number of rows in the same table that reference that row. If a row is deleted, all the other rows that reference is need to be updated with either a different value or null to avoid "dangling pointers" in the DB
- If either the delete or any of the updates go wrong for whatever reason, I need to rollback the DB to the original state
First and foremost, Postgres doesn't support nested transactions
My initial take was something the lines of this, using checkpoints
postgres.use { s =>
s.prepare(updateGenreParent).flatMap { ps =>
s.transaction.use { t =>
me.headOption match {
case None => throw UnknownGenreError
case Some(dg) =>
dg.parent match {
case None => ???
case Some(p) => // Node is middle, realign the tree
val savepoint = t.savepoint
myChildren
.traverse(c => {
for {
_ <- Logger[F].println("Updating parent for " + c.uuid + " With " + p)
sp <- savepoint
_ <- ps.execute(Some(GenreParentId(p.value)) ~ c.toplevel ~ c.uuid).recoverWith {
case e: Exception =>
Logger[F].println(e) *> t.rollback(sp)
}
} yield ()
})
.flatMap(r => {
s.prepare(deleteById).flatMap { ps2 =>
savepoint.flatMap(
sp =>
ps2.execute(id).as(id).recoverWith {
case e: Exception =>
Logger[F].println(e) *> t.rollback(sp) *> ApplicativeError[F, Throwable]
.raiseError(e)
}
)
}
})
}
}
}
}
}
}
I can confirm this updates and deletes as needed however I have no proof that the rollback does what it is supposed to. In the Skunk documentation, the basic rollback is demonstrated using constraints which isn't a great fit for this table or a delete query. I would somehow need to make postgres unreachable after the updates but before the delete.
In the Typelevel discord, it was suggested that having nested prepare/transaction statements is not a good idea, as it interleaves unrelated logic, which can cause some trouble. So essentially,
s.prepare(updateGenreParent).flatMap { ps =>
s.transaction.use { t =>
val savepoint = t.savepoint
.......
s.prepare(deleteById).flatMap { ps2 => .... restore(savepoint)... }
}
}
Is not recommended. Normally one would chain the transactions with flatMap, but that results in the delete query losing track of the original savepoint.
So my question is what is the right way of performing multiple rollbacks like this in Scala + Cats + Skunk ?
The only other option I can think of is keeping track of all the changes that have been made and creating a retry loop with something like Defer to manually update all the rows to their original state. This of course creates a small loop as now you have to worry about those transactions failing and so on.
EDIT: Postgres logs from the query
2023-05-17 23:05:10.535 EDT [46120] LOG: statement: RESET ALL
2023-05-17 23:05:10.716 EDT [46120] LOG: statement: BEGIN
2023-05-17 23:05:10.799 EDT [46120] LOG: statement: SAVEPOINT savepoint_6
2023-05-17 23:05:10.882 EDT [46120] LOG: execute statement_5/portal_7:
UPDATE genres
SET parent_id=$1, top_level=$2
WHERE uuid=$3
2023-05-17 23:05:10.882 EDT [46120] DETAIL: parameters: $1 = NULL, $2 = NULL, $3 = 'b82fb0c0-a8d4-434e-849b-f026ff198713'
2023-05-17 23:05:10.947 EDT [46120] LOG: execute statement_5/portal_9:
UPDATE genres
SET parent_id=$1, top_level=$2
WHERE uuid=$3
2023-05-17 23:05:10.947 EDT [46120] DETAIL: parameters: $1 = NULL, $2 = NULL, $3 = 'b82fb0c0-a8d4-434e-849b-f026ff198713'
2023-05-17 23:05:10.977 EDT [46120] LOG: execute statement_5/portal_11:
UPDATE genres
SET parent_id=$1, top_level=$2
WHERE uuid=$3
2023-05-17 23:05:10.977 EDT [46120] DETAIL: parameters: $1 = 'b82fb0c0-a8d4-434e-849b-f026ff198713', $2 = 'b82fb0c0-a8d4-434e-849b-f026ff198713', $3 = 'e9bce582-d8e8-485c-a01d-04cee2cd26ca'
2023-05-17 23:05:11.082 EDT [46120] LOG: execute statement_12/portal_14: DELETE FROM genres WHERE uuid = $1
2023-05-17 23:05:11.082 EDT [46120] DETAIL: parameters: $1 = '467005e0-84fd-4729-a5cc-6fe1cd75787b'
2023-05-17 23:05:11.098 EDT [46120] LOG: statement: COMMIT
2023-05-17 23:05:11.130 EDT [46120] LOG: statement: UNLISTEN *
2023-05-17 23:05:11.146 EDT [46120] LOG: statement: RESET ALL