use cratestack_core::CratestackError;
use crate::error::cratestack_error_from_sqlx;
use crate::sqlx;
use crate::transaction::Tx;
const NESTED_SAVEPOINT: &str = "cratestack_isolated_nested";
pub(crate) async fn nested_in_bound<F, T>(
bound: &super::BoundTx,
body: F,
) -> Result<T, CratestackError>
where
F: AsyncFnOnce(&mut Tx) -> Result<T, CratestackError>,
{
let mut guard = bound.lock()?;
let tx = guard.tx()?;
let mut open = OpenSavepoint {
bound,
finished: false,
};
let result = async {
savepoint_statement(tx, "SAVEPOINT").await?;
match body(&mut *tx).await {
Ok(value) => match savepoint_statement(tx, "RELEASE SAVEPOINT").await {
Ok(()) => Ok(value),
Err(error) => {
poison(bound, &error);
Err(error)
}
},
Err(error) => {
if let Err(rollback_error) = savepoint_statement(tx, "ROLLBACK TO SAVEPOINT").await
{
poison(bound, &rollback_error);
}
Err(error)
}
}
}
.await;
open.finished = true;
bound.observe(&result);
result
}
struct OpenSavepoint<'a> {
bound: &'a super::BoundTx,
finished: bool,
}
impl Drop for OpenSavepoint<'_> {
fn drop(&mut self) {
if !self.finished {
poison(
self.bound,
&CratestackError::Internal(
"db.transaction(..) was dropped before it finished".to_owned(),
),
);
}
}
}
async fn savepoint_statement(tx: &mut Tx, verb: &str) -> Result<(), CratestackError> {
sqlx::query(sqlx::AssertSqlSafe(format!("{verb} {NESTED_SAVEPOINT}")))
.execute(&mut ***tx)
.await
.map(|_| ())
.map_err(cratestack_error_from_sqlx)
}
fn poison(bound: &super::BoundTx, cause: &CratestackError) {
tracing::warn!(
target: "cratestack",
cratestack_error = cause.code(),
"db.transaction(..) inside an @isolation procedure left the transaction aborted or \
ended; the attempt will not be committed",
);
bound.poison(CratestackError::Internal(format!(
"db.transaction(..) inside an @isolation procedure could not close its savepoint ({}): \
a statement in it failed without the error being returned, or it ended the \
transaction; the procedure's work was rolled back",
cause.code(),
)));
}