use std::collections::HashMap;
use grafeo_common::types::{EpochId, TransactionId};
use grafeo_common::utils::error::{Error, Result, TransactionError};
use crate::Session;
#[derive(Debug, Clone)]
pub struct CommitInfo {
pub txn_id: TransactionId,
pub start_epoch: EpochId,
pub nodes_written: u64,
pub edges_written: u64,
}
pub struct PreparedCommit<'a> {
session: &'a mut Session,
metadata: HashMap<String, String>,
info: CommitInfo,
finalized: bool,
}
impl<'a> PreparedCommit<'a> {
pub(crate) fn new(session: &'a mut Session) -> Result<Self> {
let transaction_id = session.current_transaction_id().ok_or_else(|| {
Error::Transaction(TransactionError::InvalidState(
"No active transaction to prepare".to_string(),
))
})?;
let start_epoch = session
.transaction_manager()
.start_epoch(transaction_id)
.unwrap_or(EpochId::new(0));
let (start_nodes, current_nodes) = session.node_count_delta();
let (start_edges, current_edges) = session.edge_count_delta();
let nodes_written = current_nodes.saturating_sub(start_nodes) as u64;
let edges_written = current_edges.saturating_sub(start_edges) as u64;
let info = CommitInfo {
txn_id: transaction_id,
start_epoch,
nodes_written,
edges_written,
};
Ok(Self {
session,
metadata: HashMap::new(),
info,
finalized: false,
})
}
#[must_use]
pub fn info(&self) -> &CommitInfo {
&self.info
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
#[must_use]
pub fn metadata(&self) -> &HashMap<String, String> {
&self.metadata
}
pub fn commit(mut self) -> Result<EpochId> {
self.finalized = true;
self.session.commit()?;
Ok(self.session.transaction_manager().current_epoch())
}
pub fn abort(mut self) -> Result<()> {
self.finalized = true;
self.session.rollback()
}
}
impl Drop for PreparedCommit<'_> {
fn drop(&mut self) {
if !self.finalized {
let _ = self.session.rollback();
}
}
}
#[cfg(test)]
mod tests {
use crate::GrafeoDB;
#[test]
fn test_prepared_commit_basic() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let prepared = session.prepare_commit().unwrap();
let info = prepared.info();
assert_eq!(info.edges_written, 0);
let epoch = prepared.commit().unwrap();
assert!(epoch.as_u64() > 0);
assert_eq!(db.node_count(), 1, "Node should be visible after commit");
}
#[test]
fn test_prepared_commit_with_edges() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
session.commit().unwrap();
assert_eq!(
db.node_count(),
2,
"Both nodes should be visible after first commit"
);
session.begin_transaction().unwrap();
session
.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
let prepared = session.prepare_commit().unwrap();
prepared.commit().unwrap();
assert_eq!(
db.node_count(),
2,
"Both nodes should be visible after commit"
);
let session2 = db.session();
let result = session2
.execute("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
.unwrap();
assert_eq!(result.row_count(), 1, "Edge should be visible after commit");
}
#[test]
fn test_prepared_commit_metadata() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let mut prepared = session.prepare_commit().unwrap();
prepared.set_metadata("audit_user", "admin");
prepared.set_metadata("source", "api");
assert_eq!(prepared.metadata().len(), 2);
assert_eq!(prepared.metadata().get("audit_user").unwrap(), "admin");
prepared.commit().unwrap();
}
#[test]
fn test_prepared_commit_abort() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let prepared = session.prepare_commit().unwrap();
prepared.abort().unwrap();
let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(result.rows.len(), 0);
}
#[test]
fn test_prepared_commit_drop_rollback() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
session.begin_transaction().unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
{
let _prepared = session.prepare_commit().unwrap();
}
let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
assert_eq!(result.rows.len(), 0);
}
#[test]
fn test_prepared_commit_no_transaction() {
let db = GrafeoDB::new_in_memory();
let mut session = db.session();
let result = session.prepare_commit();
assert!(result.is_err());
}
}