use nodedb_types::document::Document;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use tokio_postgres::Client;
use tokio_postgres::error::SqlState;
use tokio_postgres::types::Type;
use crate::sql_escape::quote_identifier;
use super::core::{NodeDbRemote, pg_error_detail};
use super::document_bind::{DocumentInsert, check_identity_field, document_insert};
const PUT_SAVEPOINT: &str = "nodedb_document_put";
enum PutScope {
Savepoint,
Transaction,
}
struct PutTarget<'a> {
collection: &'a str,
key: &'a str,
id: &'a str,
}
impl PutTarget<'_> {
fn error(&self, step: &str, e: &tokio_postgres::Error) -> NodeDbError {
NodeDbError::storage(format!(
"document_put '{}'/'{}' {step} failed: {}",
self.collection,
self.id,
pg_error_detail(e)
))
}
fn undo_error(
&self,
cause: &NodeDbError,
step: &str,
e: &tokio_postgres::Error,
) -> NodeDbError {
NodeDbError::storage(format!(
"document_put '{}'/'{}' failed: {cause}; the {step} that undoes it also failed: {}",
self.collection,
self.id,
pg_error_detail(e)
))
}
}
impl NodeDbRemote {
pub(super) async fn document_put_impl(
&self,
collection: &str,
doc: Document,
) -> NodeDbResult<()> {
let key = self.identity_column(collection).await?;
check_identity_field(collection, &doc, &key)?;
let insert = document_insert(collection, &key, &doc)?;
let target = PutTarget {
collection,
key: &key,
id: &doc.id,
};
let client = self.client.lock().await;
let scope = open_scope(&client, &target).await?;
let written = replace(&client, &target, &insert).await;
close_scope(&client, &target, scope, written).await
}
}
async fn open_scope(client: &Client, target: &PutTarget<'_>) -> NodeDbResult<PutScope> {
match client
.batch_execute(&format!("SAVEPOINT {PUT_SAVEPOINT}"))
.await
{
Ok(()) => Ok(PutScope::Savepoint),
Err(e) if e.code() == Some(&SqlState::NO_ACTIVE_SQL_TRANSACTION) => {
client
.batch_execute("BEGIN")
.await
.map_err(|e| target.error("begin", &e))?;
Ok(PutScope::Transaction)
}
Err(e) => Err(target.error("savepoint", &e)),
}
}
async fn replace(
client: &Client,
target: &PutTarget<'_>,
insert: &DocumentInsert,
) -> NodeDbResult<()> {
let delete_sql = format!(
"DELETE FROM {} WHERE {} = $1",
quote_identifier(target.collection),
quote_identifier(target.key)
);
let delete = client
.prepare_typed(&delete_sql, &[Type::TEXT])
.await
.map_err(|e| target.error("delete prepare", &e))?;
client
.execute(&delete, &[&target.id])
.await
.map_err(|e| target.error("delete", &e))?;
let statement = client
.prepare_typed(&insert.sql, &insert.types())
.await
.map_err(|e| target.error("insert prepare", &e))?;
client
.execute(&statement, &insert.values())
.await
.map_err(|e| target.error("insert", &e))?;
Ok(())
}
async fn close_scope(
client: &Client,
target: &PutTarget<'_>,
scope: PutScope,
written: NodeDbResult<()>,
) -> NodeDbResult<()> {
match (scope, written) {
(PutScope::Savepoint, Ok(())) => client
.batch_execute(&format!("RELEASE SAVEPOINT {PUT_SAVEPOINT}"))
.await
.map_err(|e| target.error("release savepoint", &e)),
(PutScope::Transaction, Ok(())) => client
.batch_execute("COMMIT")
.await
.map_err(|e| target.error("commit", &e)),
(PutScope::Savepoint, Err(cause)) => {
let undo =
format!("ROLLBACK TO SAVEPOINT {PUT_SAVEPOINT}; RELEASE SAVEPOINT {PUT_SAVEPOINT}");
match client.batch_execute(&undo).await {
Ok(()) => Err(cause),
Err(e) => Err(target.undo_error(&cause, "rollback to savepoint", &e)),
}
}
(PutScope::Transaction, Err(cause)) => match client.batch_execute("ROLLBACK").await {
Ok(()) => Err(cause),
Err(e) => Err(target.undo_error(&cause, "rollback", &e)),
},
}
}