use vibesql_catalog::ReferentialAction;
use vibesql_storage::{DeferredFkViolation, DeferredFkViolationKind};
use vibesql_types::SqlValue;
use crate::errors::ExecutorError;
pub fn check_no_child_references(
db: &mut vibesql_storage::Database,
parent_table_name: &str,
parent_row: &vibesql_storage::Row,
) -> Result<(), ExecutorError> {
if !db.foreign_keys_enabled() {
return Ok(());
}
let _parent_schema = db
.catalog
.get_table(parent_table_name)
.ok_or_else(|| ExecutorError::TableNotFound(parent_table_name.to_string()))?;
let has_any_fks = db.catalog.list_tables().iter().any(|table_name| {
db.catalog
.get_table(table_name)
.map(|schema| !schema.foreign_keys.is_empty())
.unwrap_or(false)
});
if !has_any_fks {
return Ok(());
}
let session_defer = db.defer_foreign_keys();
let in_txn = db.in_transaction();
let snapshot = crate::mvcc::read_snapshot(db);
let mut actions_to_perform: Vec<(
String,
vibesql_catalog::ForeignKeyConstraint,
usize,
ReferentialAction,
Vec<SqlValue>, // parent_key_values for THIS FK
)> = Vec::new();
for table_name in db.catalog.list_tables() {
let child_schema = db.catalog.get_table(&table_name).unwrap();
if child_schema.foreign_keys.is_empty() {
continue;
}
for (fk_idx, fk) in child_schema.foreign_keys.iter().enumerate() {
if !fk.parent_table.eq_ignore_ascii_case(parent_table_name) {
continue;
}
let parent_indices = crate::foreign_key_check::resolved_parent_indices_for_fk(db, fk);
if parent_indices.is_empty()
|| parent_indices.iter().any(|&idx| idx >= parent_row.values.len())
{
continue;
}
let fk_parent_key_values: Vec<SqlValue> =
parent_indices.iter().map(|&idx| parent_row.values[idx].clone()).collect();
if fk_parent_key_values.iter().any(|v| matches!(v, SqlValue::Null)) {
continue;
}
let parent_collations = crate::foreign_key_check::parent_collations_for_fk(db, fk);
let child_table = db.get_table(&table_name).unwrap();
let has_references = child_table.scan_visible(&snapshot).any(|(_, child_row)| {
let child_fk_values: Vec<SqlValue> =
fk.column_indices.iter().map(|&idx| child_row.values[idx].clone()).collect();
if child_fk_values.iter().any(|v| matches!(v, SqlValue::Null)) {
return false;
}
child_fk_values.iter().zip(&fk_parent_key_values).enumerate().all(
|(i, (cv, pv))| {
crate::foreign_key_check::fk_values_equal(
cv,
pv,
parent_collations.get(i).and_then(|c| c.as_deref()),
)
},
)
});
if has_references {
actions_to_perform.push((
table_name.clone(),
fk.clone(),
fk_idx,
fk.on_delete.clone(),
fk_parent_key_values,
));
}
}
}
for (child_table_name, fk, fk_idx, action, parent_key_values) in actions_to_perform {
match action {
ReferentialAction::Restrict => {
let should_defer = in_txn && session_defer;
if should_defer {
queue_orphaned_children(db, &child_table_name, &fk, fk_idx, &parent_key_values);
} else {
return Err(ExecutorError::ConstraintViolation(format!(
"FOREIGN KEY constraint violation: cannot delete or update a parent row when a foreign key constraint exists. The conflict occurred in table \'{}\', constraint \'{}\'.",
child_table_name,
fk.name.as_deref().unwrap_or(""),
)));
}
}
ReferentialAction::NoAction => {
let should_defer = in_txn && (fk.initially_deferred || session_defer);
if should_defer {
queue_orphaned_children(db, &child_table_name, &fk, fk_idx, &parent_key_values);
} else {
return Err(ExecutorError::ConstraintViolation(format!(
"FOREIGN KEY constraint violation: cannot delete or update a parent row when a foreign key constraint exists. The conflict occurred in table \'{}\', constraint \'{}\'.",
child_table_name,
fk.name.as_deref().unwrap_or(""),
)));
}
}
ReferentialAction::Cascade => {
cascade_delete(
db,
&child_table_name,
&fk,
fk_idx,
&parent_key_values,
in_txn,
session_defer,
)?;
}
ReferentialAction::SetNull => {
set_null(
db,
&child_table_name,
&fk,
fk_idx,
&parent_key_values,
in_txn,
session_defer,
)?;
}
ReferentialAction::SetDefault => {
set_default(
db,
&child_table_name,
&fk,
fk_idx,
&parent_key_values,
in_txn,
session_defer,
)?;
}
}
}
Ok(())
}
fn queue_orphaned_children(
db: &mut vibesql_storage::Database,
child_table_name: &str,
fk: &vibesql_catalog::ForeignKeyConstraint,
fk_idx: usize,
parent_key_values: &[SqlValue],
) {
let parent_collations = crate::foreign_key_check::parent_collations_for_fk(db, fk);
let snapshot = crate::mvcc::read_snapshot(db);
let orphaned: Vec<Vec<SqlValue>> = {
let child_table = match db.get_table(child_table_name) {
Some(t) => t,
None => return,
};
child_table
.scan_visible(&snapshot)
.filter_map(|(_, child_row)| {
let child_fk_values: Vec<SqlValue> =
fk.column_indices.iter().map(|&idx| child_row.values[idx].clone()).collect();
if child_fk_values.iter().any(|v| matches!(v, SqlValue::Null)) {
return None;
}
let matches = child_fk_values.iter().zip(parent_key_values).enumerate().all(
|(i, (cv, pv))| {
crate::foreign_key_check::fk_values_equal(
cv,
pv,
parent_collations.get(i).and_then(|c| c.as_deref()),
)
},
);
if matches {
Some(child_row.values.to_vec())
} else {
None
}
})
.collect()
};
for row_values in orphaned {
db.queue_deferred_fk_violation(DeferredFkViolation {
child_table: child_table_name.to_string(),
fk_index: fk_idx,
child_row: row_values,
kind: DeferredFkViolationKind::ChildInsertOrUpdate,
});
}
}
#[allow(clippy::too_many_arguments)]
fn cascade_delete(
db: &mut vibesql_storage::Database,
child_table_name: &str,
fk: &vibesql_catalog::ForeignKeyConstraint,
fk_idx: usize,
parent_key_values: &[SqlValue],
in_txn: bool,
session_defer: bool,
) -> Result<(), ExecutorError> {
let parent_collations = crate::foreign_key_check::parent_collations_for_fk(db, fk);
let snapshot = crate::mvcc::read_snapshot(db);
let child_table = db.get_table(child_table_name).unwrap();
let mut rows_to_delete: Vec<vibesql_storage::Row> = Vec::new();
for (_, child_row) in child_table.scan_visible(&snapshot) {
let child_fk_values: Vec<SqlValue> =
fk.column_indices.iter().map(|&idx| child_row.values[idx].clone()).collect();
if child_fk_values.iter().any(|v| matches!(v, SqlValue::Null)) {
continue;
}
let matches = child_fk_values.iter().zip(parent_key_values).enumerate().all(
|(i, (cv, pv))| {
crate::foreign_key_check::fk_values_equal(
cv,
pv,
parent_collations.get(i).and_then(|c| c.as_deref()),
)
},
);
if matches {
rows_to_delete.push(child_row.clone());
}
}
let has_child_delete_triggers = db
.catalog
.get_triggers_for_table(child_table_name, Some(vibesql_ast::TriggerEvent::Delete))
.next()
.is_some();
for child_row in &rows_to_delete {
if has_child_delete_triggers {
let outcome = crate::TriggerFirer::execute_before_triggers(
db,
child_table_name,
vibesql_ast::TriggerEvent::Delete,
Some(child_row),
None,
)?;
if outcome == crate::TriggerOutcome::SkipRow {
let should_defer = in_txn && (fk.initially_deferred || session_defer);
if should_defer {
db.queue_deferred_fk_violation(DeferredFkViolation {
child_table: child_table_name.to_string(),
fk_index: fk_idx,
child_row: child_row.values.to_vec(),
kind: DeferredFkViolationKind::ChildInsertOrUpdate,
});
} else {
return Err(ExecutorError::ConstraintViolation(
"FOREIGN KEY constraint failed".to_string(),
));
}
continue;
}
}
check_no_child_references(db, child_table_name, child_row)?;
let child_table_mut = db.get_table_mut(child_table_name).unwrap();
let _ = child_table_mut.delete_where(|row| row == child_row);
if has_child_delete_triggers {
let _after = crate::TriggerFirer::execute_after_triggers(
db,
child_table_name,
vibesql_ast::TriggerEvent::Delete,
Some(child_row),
None,
)?;
}
}
db.rebuild_indexes(child_table_name);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn set_null(
db: &mut vibesql_storage::Database,
child_table_name: &str,
fk: &vibesql_catalog::ForeignKeyConstraint,
fk_idx: usize,
parent_key_values: &[SqlValue],
in_txn: bool,
session_defer: bool,
) -> Result<(), ExecutorError> {
let parent_collations = crate::foreign_key_check::parent_collations_for_fk(db, fk);
let snapshot = crate::mvcc::read_snapshot(db);
let child_table = db.get_table(child_table_name).unwrap();
let mut rows_to_update: Vec<(usize, vibesql_storage::Row, vibesql_storage::Row)> = Vec::new();
for (idx, child_row) in child_table.scan_visible(&snapshot) {
let child_fk_values: Vec<SqlValue> =
fk.column_indices.iter().map(|&idx| child_row.values[idx].clone()).collect();
if child_fk_values.iter().any(|v| matches!(v, SqlValue::Null)) {
continue;
}
let matches = child_fk_values.iter().zip(parent_key_values).enumerate().all(
|(i, (cv, pv))| {
crate::foreign_key_check::fk_values_equal(
cv,
pv,
parent_collations.get(i).and_then(|c| c.as_deref()),
)
},
);
if matches {
let old_row = child_row.clone();
let mut updated_row = old_row.clone();
for &fk_col_idx in &fk.column_indices {
updated_row.values[fk_col_idx] = SqlValue::Null;
}
rows_to_update.push((idx, old_row, updated_row));
}
}
apply_cascade_child_updates(
db,
child_table_name,
fk,
fk_idx,
rows_to_update,
in_txn,
session_defer,
)
}
#[allow(clippy::too_many_arguments)]
fn set_default(
db: &mut vibesql_storage::Database,
child_table_name: &str,
fk: &vibesql_catalog::ForeignKeyConstraint,
fk_idx: usize,
parent_key_values: &[SqlValue],
in_txn: bool,
session_defer: bool,
) -> Result<(), ExecutorError> {
let child_schema = db.catalog.get_table(child_table_name).unwrap().clone();
let mut default_values: Vec<SqlValue> = Vec::new();
for &fk_col_idx in &fk.column_indices {
let column = &child_schema.columns[fk_col_idx];
let default_value = if let Some(default_expr) = &column.default_value {
match default_expr {
vibesql_ast::Expression::NextValue { sequence_name } => {
let seq = db.catalog.get_sequence_mut(sequence_name).map_err(|e| {
ExecutorError::UnsupportedExpression(format!("Sequence error: {:?}", e))
})?;
let next_val = seq.next_value().map_err(|e| {
ExecutorError::ConstraintViolation(format!("Sequence error: {}", e))
})?;
SqlValue::Integer(next_val)
}
_ => crate::insert::defaults::evaluate_default_expression(default_expr)?,
}
} else {
SqlValue::Null
};
default_values.push(default_value);
}
let parent_collations = crate::foreign_key_check::parent_collations_for_fk(db, fk);
let snapshot = crate::mvcc::read_snapshot(db);
let child_table = db.get_table(child_table_name).unwrap();
let mut rows_to_update: Vec<(usize, vibesql_storage::Row, vibesql_storage::Row)> = Vec::new();
for (idx, child_row) in child_table.scan_visible(&snapshot) {
let child_fk_values: Vec<SqlValue> =
fk.column_indices.iter().map(|&idx| child_row.values[idx].clone()).collect();
if child_fk_values.iter().any(|v| matches!(v, SqlValue::Null)) {
continue;
}
let matches = child_fk_values.iter().zip(parent_key_values).enumerate().all(
|(i, (cv, pv))| {
crate::foreign_key_check::fk_values_equal(
cv,
pv,
parent_collations.get(i).and_then(|c| c.as_deref()),
)
},
);
if matches {
let old_row = child_row.clone();
let mut updated_row = old_row.clone();
for (i, &fk_col_idx) in fk.column_indices.iter().enumerate() {
updated_row.values[fk_col_idx] = default_values[i].clone();
}
rows_to_update.push((idx, old_row, updated_row));
}
}
apply_cascade_child_updates(
db,
child_table_name,
fk,
fk_idx,
rows_to_update,
in_txn,
session_defer,
)
}
#[allow(clippy::too_many_arguments)]
fn apply_cascade_child_updates(
db: &mut vibesql_storage::Database,
child_table_name: &str,
fk: &vibesql_catalog::ForeignKeyConstraint,
fk_idx: usize,
mut rows_to_update: Vec<(usize, vibesql_storage::Row, vibesql_storage::Row)>,
in_txn: bool,
session_defer: bool,
) -> Result<(), ExecutorError> {
let txn_id = db.transaction_id();
for (_, _old_row, new_row) in rows_to_update.iter_mut() {
vibesql_storage::stamp_xmin_for_write(new_row, txn_id);
new_row.xmax = None;
}
let has_child_update_triggers = db
.catalog
.get_triggers_for_table(child_table_name, Some(vibesql_ast::TriggerEvent::Update(None)))
.next()
.is_some();
for (idx, old_row, new_row) in rows_to_update {
if has_child_update_triggers {
let outcome = crate::TriggerFirer::execute_before_triggers(
db,
child_table_name,
vibesql_ast::TriggerEvent::Update(None),
Some(&old_row),
Some(&new_row),
)?;
if outcome == crate::TriggerOutcome::SkipRow {
let should_defer = in_txn && (fk.initially_deferred || session_defer);
if should_defer {
db.queue_deferred_fk_violation(DeferredFkViolation {
child_table: child_table_name.to_string(),
fk_index: fk_idx,
child_row: old_row.values.to_vec(),
kind: DeferredFkViolationKind::ChildInsertOrUpdate,
});
} else {
return Err(ExecutorError::ConstraintViolation(
"FOREIGN KEY constraint failed".to_string(),
));
}
continue;
}
}
let child_table_mut = db.get_table_mut(child_table_name).unwrap();
child_table_mut
.update_row(idx, new_row.clone())
.map_err(|e| ExecutorError::StorageError(e.to_string()))?;
if has_child_update_triggers {
let _after = crate::TriggerFirer::execute_after_triggers(
db,
child_table_name,
vibesql_ast::TriggerEvent::Update(None),
Some(&old_row),
Some(&new_row),
)?;
}
}
db.rebuild_indexes(child_table_name);
Ok(())
}