use crate::api_core::{CoreError, CoreResult, TransactionHandle};
use crate::embedded::result::QueryResult;
use crate::embedded::session::Session;
use linkrs_core::Value;
use linkrs_storage::StorageClient;
use linkrs_transaction::types::{SavepointId, SavepointInfo};
use linkrs_transaction::{DurabilityLevel, IsolationLevel, TransactionOptions};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct TransactionConfig {
pub timeout: Option<Duration>,
pub read_only: bool,
pub durability: DurabilityLevel,
pub isolation_level: IsolationLevel,
pub query_timeout: Option<Duration>,
pub statement_timeout: Option<Duration>,
pub idle_timeout: Option<Duration>,
}
impl Default for TransactionConfig {
fn default() -> Self {
Self {
timeout: None,
read_only: false,
durability: DurabilityLevel::Sync,
isolation_level: IsolationLevel::default(),
query_timeout: None,
statement_timeout: None,
idle_timeout: None,
}
}
}
impl TransactionConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn read_only(mut self) -> Self {
self.read_only = true;
self
}
pub fn with_durability(mut self, durability: DurabilityLevel) -> Self {
self.durability = durability;
self
}
pub fn with_isolation_level(mut self, level: IsolationLevel) -> Self {
self.isolation_level = level;
self
}
pub fn with_query_timeout(mut self, timeout: Duration) -> Self {
self.query_timeout = Some(timeout);
self
}
pub fn with_statement_timeout(mut self, timeout: Duration) -> Self {
self.statement_timeout = Some(timeout);
self
}
pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
self.idle_timeout = Some(timeout);
self
}
pub(crate) fn into_options(self) -> TransactionOptions {
TransactionOptions {
timeout: self.timeout,
read_only: self.read_only,
long_read: false,
durability: self.durability,
isolation_level: self.isolation_level,
query_timeout: self.query_timeout,
statement_timeout: self.statement_timeout,
idle_timeout: self.idle_timeout,
}
}
}
pub struct Transaction<'sess, S: StorageClient + Clone + Send + Sync + 'static> {
session: &'sess Session<S>,
txn_handle: TransactionHandle,
committed: bool,
rolled_back: bool,
}
impl<'sess, S: StorageClient + Clone + Send + Sync + 'static + linkrs_storage::UndoTarget>
Transaction<'sess, S>
{
pub(crate) fn new(session: &'sess Session<S>, txn_handle: TransactionHandle) -> Self {
Self {
session,
txn_handle,
committed: false,
rolled_back: false,
}
}
pub fn execute(&self, query: &str) -> CoreResult<QueryResult> {
self.check_active()?;
self.session.ensure_writable(query)?;
let txn_manager = self.session.txn_manager();
let (ctx, statement_start) = txn_manager.begin_statement(self.txn_handle.0)?;
let execution = txn_manager.create_execution(self.txn_handle.0, false)?;
let query_ctx = self
.session
.session_query_request()
.auto_commit(false)
.transaction_id(Some(self.txn_handle.0))
.build();
let result = {
let mut query_api = self.session.query_api_mut();
query_api.execute_with_execution(query, query_ctx, &execution)?
};
txn_manager.finish_statement(&ctx, statement_start)?;
Ok(QueryResult::from_core(result))
}
pub fn execute_with_params(
&self,
query: &str,
params: HashMap<String, Value>,
) -> CoreResult<QueryResult> {
self.check_active()?;
self.session.ensure_writable(query)?;
let txn_manager = self.session.txn_manager();
let (ctx, statement_start) = txn_manager.begin_statement(self.txn_handle.0)?;
let execution = txn_manager.create_execution(self.txn_handle.0, false)?;
let query_ctx = self
.session
.session_query_request()
.auto_commit(false)
.transaction_id(Some(self.txn_handle.0))
.parameters(Some(params))
.build();
let result = {
let mut query_api = self.session.query_api_mut();
query_api.execute_with_execution(query, query_ctx, &execution)?
};
txn_manager.finish_statement(&ctx, statement_start)?;
Ok(QueryResult::from_core(result))
}
pub fn commit(mut self) -> CoreResult<()> {
self.check_active()?;
let txn_manager = self.session.txn_manager();
let ctx = txn_manager.get_context(self.txn_handle.0)?;
ctx.check_timeouts()
.map_err(|e| CoreError::TransactionFailed(format!("Transaction timeout: {}", e)))?;
txn_manager
.commit_transaction(self.txn_handle.0)
.map_err(|e| {
use linkrs_transaction::TransactionErrorKind;
if e.kind() == TransactionErrorKind::CommitVetoed {
if let Err(abort_error) = txn_manager.abort_transaction(self.txn_handle.0) {
log::error!(
"Abort-after-veto failed for transaction {}: {}",
self.txn_handle.0,
abort_error
);
}
}
crate::api_core::error::CoreError::TransactionFailed(e.to_string())
})?;
self.committed = true;
Ok(())
}
pub fn rollback(mut self) -> CoreResult<()> {
self.check_active()?;
let txn_manager = self.session.txn_manager();
let ctx = txn_manager.get_context(self.txn_handle.0)?;
ctx.check_timeouts()
.map_err(|e| CoreError::TransactionFailed(format!("Transaction timeout: {}", e)))?;
txn_manager
.abort_transaction(self.txn_handle.0)
.map_err(|e| crate::api_core::error::CoreError::TransactionFailed(e.to_string()))?;
self.rolled_back = true;
Ok(())
}
pub fn create_savepoint(&self, name: Option<String>) -> CoreResult<SavepointId> {
self.check_active()?;
let txn_manager = self.session.txn_manager();
let staged_mark = {
let storage = self.session.storage_mut();
linkrs_storage::UndoTarget::staged_write_mark(&*storage, self.txn_handle.0)
};
txn_manager
.create_savepoint(self.txn_handle.0, name, staged_mark)
.map_err(|e| CoreError::TransactionFailed(e.to_string()))
}
pub fn rollback_to_savepoint(&self, savepoint_id: SavepointId) -> CoreResult<()> {
self.check_active()?;
let txn_manager = self.session.txn_manager();
let storage = self.session.storage_mut();
txn_manager
.rollback_to_savepoint(self.txn_handle.0, savepoint_id, &*storage)
.map_err(|e| CoreError::TransactionFailed(e.to_string()))
}
pub fn release_savepoint(&self, savepoint_id: SavepointId) -> CoreResult<()> {
self.check_active()?;
let txn_manager = self.session.txn_manager();
txn_manager
.release_savepoint(self.txn_handle.0, savepoint_id)
.map_err(|e| CoreError::TransactionFailed(e.to_string()))
}
pub fn find_savepoint(&self, name: &str) -> Option<SavepointId> {
if !self.is_active() {
return None;
}
let context = self
.session
.txn_manager()
.get_context(self.txn_handle.0)
.ok()?;
context.find_savepoint_by_name(name).map(|info| info.id)
}
pub fn list_savepoints(&self) -> Vec<SavepointInfo> {
if !self.is_active() {
return Vec::new();
}
let txn_manager = self.session.txn_manager();
txn_manager.get_active_savepoints(self.txn_handle.0)
}
pub fn info(&self) -> CoreResult<TransactionInfo> {
let txn_manager = self.session.txn_manager();
txn_manager
.get_transaction_info(self.txn_handle.0)
.map(|info| TransactionInfo {
id: info.id.0,
state: format!("{:?}", info.state),
is_read_only: info.is_read_only,
elapsed_ms: info.elapsed.as_millis() as u64,
savepoint_count: info.savepoint_count,
})
.ok_or_else(|| CoreError::TransactionFailed("Transaction not found".to_string()))
}
fn check_active(&self) -> CoreResult<()> {
if self.committed {
return Err(CoreError::TransactionFailed(
"Transaction has been committed, cannot perform operations".to_string(),
));
}
if self.rolled_back {
return Err(CoreError::TransactionFailed(
"Transaction has been rolled back, cannot perform operations".to_string(),
));
}
Ok(())
}
pub fn is_committed(&self) -> bool {
self.committed
}
pub fn is_rolled_back(&self) -> bool {
self.rolled_back
}
pub fn is_active(&self) -> bool {
!self.committed && !self.rolled_back
}
pub fn handle(&self) -> TransactionHandle {
self.txn_handle
}
pub fn id(&self) -> u64 {
self.txn_handle.id()
}
#[cfg(feature = "c_api")]
pub(crate) fn txn_handle(&self) -> TransactionHandle {
self.txn_handle
}
}
impl<'sess, S: StorageClient + Clone + Send + Sync + 'static> Drop for Transaction<'sess, S> {
fn drop(&mut self) {
if self.is_active() {
let _ = self
.session
.txn_manager()
.abort_transaction(self.txn_handle.0);
}
}
}
#[derive(Debug, Clone)]
pub struct TransactionInfo {
pub id: u64,
pub state: String,
pub is_read_only: bool,
pub elapsed_ms: u64,
pub savepoint_count: usize,
}
impl TransactionInfo {
pub fn id(&self) -> u64 {
self.id
}
pub fn state(&self) -> &str {
&self.state
}
pub fn is_read_only(&self) -> bool {
self.is_read_only
}
pub fn elapsed_ms(&self) -> u64 {
self.elapsed_ms
}
pub fn savepoint_count(&self) -> usize {
self.savepoint_count
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transaction_config_default() {
let config = TransactionConfig::default();
assert!(!config.read_only);
assert!(config.timeout.is_none());
}
#[test]
fn test_transaction_config_builder() {
let config = TransactionConfig::new()
.read_only()
.with_timeout(Duration::from_secs(60))
.with_durability(DurabilityLevel::None);
assert!(config.read_only);
assert_eq!(config.timeout, Some(Duration::from_secs(60)));
assert_eq!(config.durability, DurabilityLevel::None);
}
#[test]
fn test_transaction_config_into_options() {
let config = TransactionConfig::new()
.read_only()
.with_timeout(Duration::from_secs(30));
let options = config.into_options();
assert!(options.read_only);
assert_eq!(options.timeout, Some(Duration::from_secs(30)));
}
}