use vibesql_ast::{
BeginStmt, CommitStmt, DurabilityHint, ReleaseSavepointStmt, RollbackStmt,
RollbackToSavepointStmt, SavepointStmt,
};
use vibesql_storage::{Database, TransactionDurability};
use crate::errors::ExecutorError;
fn convert_durability_hint(hint: &DurabilityHint) -> TransactionDurability {
match hint {
DurabilityHint::Default => TransactionDurability::Default,
DurabilityHint::Durable => TransactionDurability::ForceDurable,
DurabilityHint::Lazy => TransactionDurability::AllowLazy,
DurabilityHint::Volatile => TransactionDurability::ForceVolatile,
}
}
pub struct BeginTransactionExecutor;
impl BeginTransactionExecutor {
pub fn execute(stmt: &BeginStmt, db: &mut Database) -> Result<String, ExecutorError> {
let durability = convert_durability_hint(&stmt.durability);
db.begin_transaction_with_durability(durability).map_err(|e| {
ExecutorError::StorageError(format!("Failed to begin transaction: {}", e))
})?;
let msg = match stmt.durability {
DurabilityHint::Default => "Transaction started".to_string(),
DurabilityHint::Durable => "Transaction started (durability: DURABLE)".to_string(),
DurabilityHint::Lazy => "Transaction started (durability: LAZY)".to_string(),
DurabilityHint::Volatile => "Transaction started (durability: VOLATILE)".to_string(),
};
Ok(msg)
}
}
pub struct CommitExecutor;
impl CommitExecutor {
pub fn execute(_stmt: &CommitStmt, db: &mut Database) -> Result<String, ExecutorError> {
let pending = db.take_deferred_fk_violations();
let commit_snapshot = db.capture_commit_time_snapshot();
if let Some(err_msg) = check_deferred_fk_violations(db, &pending, &commit_snapshot) {
let _ = db.rollback_transaction();
return Err(ExecutorError::ConstraintViolation(err_msg));
}
db.commit_transaction().map_err(|e| {
ExecutorError::StorageError(format!("Failed to commit transaction: {}", e))
})?;
Ok("Transaction committed".to_string())
}
}
fn check_deferred_fk_violations(
db: &Database,
pending: &[vibesql_storage::DeferredFkViolation],
snapshot: &vibesql_storage::TxnSnapshot,
) -> Option<String> {
use crate::foreign_key_check::{
fk_values_equal, parent_collations_for_fk, resolved_parent_indices_for_fk,
};
for violation in pending {
let child_schema = match db.catalog.get_table(&violation.child_table) {
Some(s) => s,
None => {
continue;
}
};
let fk = match child_schema.foreign_keys.get(violation.fk_index) {
Some(fk) => fk,
None => continue, };
let child_table = match db.get_table(&violation.child_table) {
Some(t) => t,
None => continue,
};
let snapshot_fk_values: Vec<vibesql_types::SqlValue> = fk
.column_indices
.iter()
.map(|&idx| {
violation.child_row.get(idx).cloned().unwrap_or(vibesql_types::SqlValue::Null)
})
.collect();
if snapshot_fk_values.iter().any(|v| v.is_null()) {
continue;
}
let child_still_present = child_table.scan_visible(snapshot).any(|(_, child_row)| {
fk.column_indices.iter().enumerate().all(|(i, &col_idx)| {
match child_row.values.get(col_idx) {
Some(v) => v == &snapshot_fk_values[i],
None => false,
}
})
});
if !child_still_present {
continue;
}
let parent_table = match db.get_table(&fk.parent_table) {
Some(t) => t,
None => {
return Some(format!(
"FOREIGN KEY constraint failed: parent table '{}' missing at COMMIT",
fk.parent_table
));
}
};
let parent_collations = parent_collations_for_fk(db, fk);
let parent_indices = resolved_parent_indices_for_fk(db, fk);
let key_exists = {
#[cfg(feature = "mvcc_enabled")]
{
parent_table.scan_visible(snapshot).any(|(_, parent_row)| {
parent_indices.iter().zip(&snapshot_fk_values).enumerate().all(
|(i, (&parent_idx, fk_val))| match parent_row.get(parent_idx) {
Some(parent_val) => fk_values_equal(
fk_val,
parent_val,
parent_collations.get(i).and_then(|c| c.as_deref()),
),
None => false,
},
)
})
}
#[cfg(not(feature = "mvcc_enabled"))]
{
let _ = snapshot;
parent_table.scan().iter().any(|parent_row| {
parent_indices.iter().zip(&snapshot_fk_values).enumerate().all(
|(i, (&parent_idx, fk_val))| match parent_row.get(parent_idx) {
Some(parent_val) => fk_values_equal(
fk_val,
parent_val,
parent_collations.get(i).and_then(|c| c.as_deref()),
),
None => false,
},
)
})
}
};
if !key_exists {
return Some("FOREIGN KEY constraint failed".to_string());
}
}
None
}
pub fn live_deferred_fk_violation_count(db: &Database) -> usize {
use crate::foreign_key_check::{
fk_values_equal, parent_collations_for_fk, resolved_parent_indices_for_fk,
};
let pending = db.deferred_fk_violations();
if pending.is_empty() {
return 0;
}
let mut live = 0usize;
for violation in pending {
let child_schema = match db.catalog.get_table(&violation.child_table) {
Some(s) => s,
None => continue, };
let fk = match child_schema.foreign_keys.get(violation.fk_index) {
Some(fk) => fk,
None => continue, };
let child_table = match db.get_table(&violation.child_table) {
Some(t) => t,
None => continue,
};
let snapshot_fk_values: Vec<vibesql_types::SqlValue> = fk
.column_indices
.iter()
.map(|&idx| {
violation.child_row.get(idx).cloned().unwrap_or(vibesql_types::SqlValue::Null)
})
.collect();
if snapshot_fk_values.iter().any(|v| v.is_null()) {
continue;
}
let child_still_present = child_table.scan_live().any(|(_, child_row)| {
fk.column_indices.iter().enumerate().all(
|(i, &col_idx)| match child_row.values.get(col_idx) {
Some(v) => v == &snapshot_fk_values[i],
None => false,
},
)
});
if !child_still_present {
continue;
}
let parent_table = match db.get_table(&fk.parent_table) {
Some(t) => t,
None => {
live += 1;
continue;
}
};
let parent_collations = parent_collations_for_fk(db, fk);
let parent_indices = resolved_parent_indices_for_fk(db, fk);
let key_exists = parent_table.scan_live().any(|(_, parent_row)| {
parent_indices.iter().zip(&snapshot_fk_values).enumerate().all(
|(i, (&parent_idx, fk_val))| match parent_row.values.get(parent_idx) {
Some(parent_val) => fk_values_equal(
fk_val,
parent_val,
parent_collations.get(i).and_then(|c| c.as_deref()),
),
None => false,
},
)
});
if !key_exists {
live += 1;
}
}
live
}
pub struct RollbackExecutor;
impl RollbackExecutor {
pub fn execute(_stmt: &RollbackStmt, db: &mut Database) -> Result<String, ExecutorError> {
db.rollback_transaction().map_err(|e| {
ExecutorError::StorageError(format!("Failed to rollback transaction: {}", e))
})?;
Ok("Transaction rolled back".to_string())
}
}
pub struct SavepointExecutor;
impl SavepointExecutor {
pub fn execute(stmt: &SavepointStmt, db: &mut Database) -> Result<String, ExecutorError> {
db.create_savepoint(stmt.name.clone()).map_err(|e| {
ExecutorError::StorageError(format!("Failed to create savepoint: {}", e))
})?;
Ok(format!("Savepoint '{}' created", stmt.name))
}
}
pub struct RollbackToSavepointExecutor;
impl RollbackToSavepointExecutor {
pub fn execute(
stmt: &RollbackToSavepointStmt,
db: &mut Database,
) -> Result<String, ExecutorError> {
db.rollback_to_savepoint(stmt.name.clone()).map_err(|e| {
ExecutorError::StorageError(format!("Failed to rollback to savepoint: {}", e))
})?;
Ok(format!("Rolled back to savepoint '{}'", stmt.name))
}
}
pub struct ReleaseSavepointExecutor;
impl ReleaseSavepointExecutor {
pub fn execute(
stmt: &ReleaseSavepointStmt,
db: &mut Database,
) -> Result<String, ExecutorError> {
db.release_savepoint(stmt.name.clone()).map_err(|e| {
ExecutorError::StorageError(format!("Failed to release savepoint: {}", e))
})?;
Ok(format!("Savepoint '{}' released", stmt.name))
}
}