use crate::relational::catalog::{ForeignKey, MatchType, QualifiedName, ReferentialAction, Table};
use crate::relational::{Catalog, SqlValue, composite_key, ordered_key};
use crate::sql::error::{Result, SqlError};
use crate::sql::exec::{DeferredFkCheck, Exec};
use crate::sql::store::RowValues;
use crate::sql::trigger::TriggerOp;
use std::collections::{BTreeSet, VecDeque};
const MAX_RI_STEPS: usize = 1_000_000;
const MAX_CASCADE_DEPTH: u32 = 25;
pub(crate) enum RiWork {
Deleted {
table: QualifiedName,
row: RowValues,
cascade_depth: u32,
},
Updated {
table: QualifiedName,
old: RowValues,
new: RowValues,
},
}
struct PendingCheck {
parent: QualifiedName,
child: QualifiedName,
fk: ForeignKey,
key: String,
key_vals: Vec<SqlValue>,
}
pub(crate) fn fk_ripple(
catalog: &Catalog,
root: &QualifiedName,
include_children: bool,
) -> (Vec<QualifiedName>, Vec<QualifiedName>) {
let mut written: BTreeSet<QualifiedName> = BTreeSet::new();
if include_children {
let mut queue: VecDeque<QualifiedName> = VecDeque::from([root.clone()]);
while let Some(q) = queue.pop_front() {
for (child, _fk) in catalog.referencing_foreign_keys(&q) {
if child != *root && written.insert(child.clone()) {
queue.push_back(child);
}
}
}
}
let mut read: BTreeSet<QualifiedName> = BTreeSet::new();
for q in std::iter::once(root).chain(written.iter()) {
if let Some(table) = catalog.get_table(q) {
for fk in &table.foreign_keys {
if let Some(parent) =
catalog.resolve_table_name(Some(&fk.ref_schema), &fk.ref_table)
{
read.insert(parent);
}
}
}
}
(written.into_iter().collect(), read.into_iter().collect())
}
fn fk_values(fk: &ForeignKey, row: &RowValues) -> Vec<SqlValue> {
fk.columns
.iter()
.map(|c| row.get(c).cloned().unwrap_or(SqlValue::Null))
.collect()
}
fn ref_values(fk: &ForeignKey, row: &RowValues) -> Vec<SqlValue> {
fk.ref_columns
.iter()
.map(|c| row.get(c).cloned().unwrap_or(SqlValue::Null))
.collect()
}
fn display_vals(vals: &[SqlValue]) -> String {
vals.iter()
.map(|v| v.to_text().unwrap_or_else(|| "null".into()))
.collect::<Vec<_>>()
.join(", ")
}
impl Exec {
pub(crate) fn fk_check_child(
&self,
table: &Table,
new: &RowValues,
old: Option<&RowValues>,
) -> Result<()> {
for fk in &table.foreign_keys {
if let Some(old) = old
&& ordered_key(&fk_values(fk, old)) == ordered_key(&fk_values(fk, new))
{
continue;
}
self.fk_check_one(table, fk, new)?;
}
Ok(())
}
fn fk_check_one(&self, table: &Table, fk: &ForeignKey, row: &RowValues) -> Result<()> {
let vals = fk_values(fk, row);
let null_count = vals.iter().filter(|v| v.is_null()).count();
let table_q = table.qualified();
match fk.match_type {
MatchType::Simple => {
if null_count > 0 {
return Ok(());
}
}
MatchType::Full => {
if null_count == vals.len() {
return Ok(());
}
if null_count > 0 {
if self.fk_is_deferred(&table_q, fk) {
self.deferred_checks
.borrow_mut()
.push(DeferredFkCheck::MatchFullNullMix {
child: table_q,
fk: fk.clone(),
row_key: ordered_key(&vals),
});
return Ok(());
}
return Err(SqlError::ForeignKeyViolation {
table: table.name.clone(),
constraint: fk.name.clone(),
detail: "MATCH FULL does not allow mixing of null and nonnull key values."
.into(),
});
}
}
}
let key = composite_key(&vals).expect("null_count == 0 implies composite_key is Some");
let parent_q = self.fk_parent(fk)?;
if self.fk_parent_has_key(&parent_q, fk, &key)? {
return Ok(());
}
if self.fk_is_deferred(&table_q, fk) {
self.deferred_checks
.borrow_mut()
.push(DeferredFkCheck::Child {
child: table_q,
fk: fk.clone(),
key,
key_vals: vals,
});
return Ok(());
}
Err(SqlError::ForeignKeyViolation {
table: table.name.clone(),
constraint: fk.name.clone(),
detail: format!(
"Key ({})=({}) is not present in table \"{}\".",
fk.columns.join(", "),
display_vals(&vals),
fk.ref_table
),
})
}
fn fk_is_deferred(&self, table_q: &QualifiedName, fk: &ForeignKey) -> bool {
self.constraint_modes
.as_ref()
.map(|cm| cm.is_deferred(table_q, fk))
.unwrap_or(false)
}
fn fk_parent(&self, fk: &ForeignKey) -> Result<QualifiedName> {
self.catalog
.resolve_table_name(Some(&fk.ref_schema), &fk.ref_table)
.ok_or_else(|| SqlError::UndefinedTable(format!("{}.{}", fk.ref_schema, fk.ref_table)))
}
fn fk_parent_has_key(
&self,
parent_q: &QualifiedName,
fk: &ForeignKey,
key: &str,
) -> Result<bool> {
let loaded = self.fk_loaded(parent_q)?;
for idx in &loaded.indexes {
if idx.meta.columns == fk.ref_columns {
return Ok(!idx.data.get(key).is_empty());
}
}
Ok(loaded
.rows
.values()
.any(|r| composite_key(&ref_values(fk, r)).as_deref() == Some(key)))
}
fn fk_matching_children(
&self,
child_q: &QualifiedName,
fk: &ForeignKey,
key: &str,
) -> Result<Vec<(String, RowValues)>> {
let loaded = self.fk_loaded(child_q)?;
for idx in &loaded.indexes {
if idx.meta.columns == fk.columns {
return Ok(idx
.data
.get(key)
.iter()
.filter_map(|rid| loaded.rows.get(rid).map(|r| (rid.clone(), r.clone())))
.collect());
}
}
Ok(loaded
.rows
.iter()
.filter(|(_, r)| composite_key(&fk_values(fk, r)).as_deref() == Some(key))
.map(|(rid, r)| (rid.clone(), r.clone()))
.collect())
}
fn fk_child_row_shape_exists(
&self,
child_q: &QualifiedName,
fk: &ForeignKey,
row_key: &str,
) -> Result<bool> {
let loaded = self.fk_loaded(child_q)?;
Ok(loaded
.rows
.values()
.any(|r| ordered_key(&fk_values(fk, r)) == row_key))
}
fn fk_loaded(&self, q: &QualifiedName) -> Result<&crate::sql::store::LoadedTable> {
self.tables.get(q).ok_or_else(|| {
SqlError::Internal(format!(
"foreign-key table {} was not preloaded",
q.to_string_qualified()
))
})
}
pub(crate) fn fk_apply_referential_actions(&mut self, work: Vec<RiWork>) -> Result<()> {
let mut queue: VecDeque<RiWork> = work.into();
let mut checks: Vec<PendingCheck> = Vec::new();
let mut steps = 0usize;
while let Some(item) = queue.pop_front() {
steps += 1;
if steps > MAX_RI_STEPS {
return Err(SqlError::Internal(
"foreign-key referential actions did not terminate".into(),
));
}
match item {
RiWork::Deleted {
table,
row,
cascade_depth,
} => {
if cascade_depth > MAX_CASCADE_DEPTH {
return Err(SqlError::StatementTooComplex(
"FK CASCADE DELETE depth limit exceeded — too many levels of cascades \
(max 25)"
.into(),
));
}
self.ri_on_delete(&table, &row, cascade_depth, &mut queue, &mut checks)?
}
RiWork::Updated { table, old, new } => {
self.ri_on_update(&table, &old, &new, &mut queue, &mut checks)?
}
}
}
for c in checks {
if self.fk_parent_has_key(&c.parent, &c.fk, &c.key)? {
continue;
}
if !self
.fk_matching_children(&c.child, &c.fk, &c.key)?
.is_empty()
{
return Err(self.ri_referenced_error(&c.parent, &c.child, &c.fk, &c.key_vals));
}
}
Ok(())
}
pub(crate) fn fk_drain_deferred(&self, checks: Vec<DeferredFkCheck>) -> Result<()> {
for check in checks {
match check {
DeferredFkCheck::Child {
child,
fk,
key,
key_vals,
} => {
let parent_q = self.fk_parent(&fk)?;
if self.fk_parent_has_key(&parent_q, &fk, &key)? {
continue;
}
if self.fk_matching_children(&child, &fk, &key)?.is_empty() {
continue;
}
let table_name = self
.catalog
.get_table(&child)
.map(|t| t.name.clone())
.unwrap_or_else(|| child.name.clone());
return Err(SqlError::ForeignKeyViolation {
table: table_name,
constraint: fk.name.clone(),
detail: format!(
"Key ({})=({}) is not present in table \"{}\".",
fk.columns.join(", "),
display_vals(&key_vals),
fk.ref_table
),
});
}
DeferredFkCheck::Referenced {
parent,
child,
fk,
key,
key_vals,
} => {
if self.fk_parent_has_key(&parent, &fk, &key)? {
continue;
}
if !self.fk_matching_children(&child, &fk, &key)?.is_empty() {
return Err(self.ri_referenced_error(&parent, &child, &fk, &key_vals));
}
}
DeferredFkCheck::MatchFullNullMix { child, fk, row_key } => {
if !self.fk_child_row_shape_exists(&child, &fk, &row_key)? {
continue;
}
let table_name = self
.catalog
.get_table(&child)
.map(|t| t.name.clone())
.unwrap_or_else(|| child.name.clone());
return Err(SqlError::ForeignKeyViolation {
table: table_name,
constraint: fk.name.clone(),
detail: "MATCH FULL does not allow mixing of null and nonnull key values."
.into(),
});
}
}
}
Ok(())
}
fn ri_on_delete(
&mut self,
parent_q: &QualifiedName,
row: &RowValues,
cascade_depth: u32,
queue: &mut VecDeque<RiWork>,
checks: &mut Vec<PendingCheck>,
) -> Result<()> {
for (child_q, fk) in self.catalog.referencing_foreign_keys(parent_q) {
let key_vals = ref_values(&fk, row);
let Some(key) = composite_key(&key_vals) else {
continue;
};
match fk.on_delete {
ReferentialAction::NoAction => {
self.ri_queue_or_check(parent_q, &child_q, &fk, key, key_vals, checks);
}
ReferentialAction::Restrict => {
checks.push(PendingCheck {
parent: parent_q.clone(),
child: child_q.clone(),
fk: fk.clone(),
key,
key_vals,
});
}
ReferentialAction::Cascade => {
let child_table = self.catalog.get_table(&child_q).cloned();
let has_triggers = child_table
.as_ref()
.is_some_and(|t| t.triggers.iter().any(|trg| trg.enabled));
let children = self.fk_matching_children(&child_q, &fk, &key)?;
for (rid, child_row) in children {
if has_triggers && let Some(tbl) = &child_table {
let result = self.fire_before_row(
tbl,
TriggerOp::Delete,
Some(&child_row),
None,
None,
)?;
if result.is_none() {
continue;
}
}
self.ri_delete_child(&child_q, &rid)?;
queue.push_back(RiWork::Deleted {
table: child_q.clone(),
row: child_row.clone(),
cascade_depth: cascade_depth + 1,
});
if has_triggers && let Some(tbl) = &child_table {
self.fire_after_row(
tbl,
TriggerOp::Delete,
Some(&child_row),
None,
None,
)?;
}
}
}
ReferentialAction::SetNull => {
for (rid, child_row) in self.fk_matching_children(&child_q, &fk, &key)? {
let mut new = child_row.clone();
for c in &fk.columns {
new.insert(c.clone(), SqlValue::Null);
}
self.ri_update_child(&child_q, &rid, child_row, new, queue)?;
}
}
ReferentialAction::SetDefault => {
for (rid, child_row) in self.fk_matching_children(&child_q, &fk, &key)? {
let new = self.ri_defaults_for(&child_q, &fk, &child_row)?;
self.ri_update_child(&child_q, &rid, child_row, new, queue)?;
}
self.ri_queue_or_check(parent_q, &child_q, &fk, key, key_vals, checks);
}
}
}
Ok(())
}
fn ri_queue_or_check(
&self,
parent_q: &QualifiedName,
child_q: &QualifiedName,
fk: &ForeignKey,
key: String,
key_vals: Vec<SqlValue>,
checks: &mut Vec<PendingCheck>,
) {
if self.fk_is_deferred(child_q, fk) {
self.deferred_checks
.borrow_mut()
.push(DeferredFkCheck::Referenced {
parent: parent_q.clone(),
child: child_q.clone(),
fk: fk.clone(),
key,
key_vals,
});
} else {
checks.push(PendingCheck {
parent: parent_q.clone(),
child: child_q.clone(),
fk: fk.clone(),
key,
key_vals,
});
}
}
fn ri_on_update(
&mut self,
parent_q: &QualifiedName,
old: &RowValues,
new: &RowValues,
queue: &mut VecDeque<RiWork>,
checks: &mut Vec<PendingCheck>,
) -> Result<()> {
for (child_q, fk) in self.catalog.referencing_foreign_keys(parent_q) {
let old_vals = ref_values(&fk, old);
let Some(key) = composite_key(&old_vals) else {
continue;
};
let new_vals = ref_values(&fk, new);
if ordered_key(&old_vals) == ordered_key(&new_vals) {
continue;
}
match fk.on_update {
ReferentialAction::NoAction => {
self.ri_queue_or_check(parent_q, &child_q, &fk, key, old_vals, checks);
}
ReferentialAction::Restrict => {
checks.push(PendingCheck {
parent: parent_q.clone(),
child: child_q.clone(),
fk: fk.clone(),
key,
key_vals: old_vals,
});
}
ReferentialAction::Cascade => {
let child_meta = self.fk_loaded(&child_q)?.meta.clone();
for (rid, child_row) in self.fk_matching_children(&child_q, &fk, &key)? {
let mut newc = child_row.clone();
for (i, c) in fk.columns.iter().enumerate() {
let v = new_vals.get(i).cloned().unwrap_or(SqlValue::Null);
let v = if v.is_null() {
SqlValue::Null
} else {
crate::sql::dml::coerce_to_col(v, &child_meta, c)?
};
newc.insert(c.clone(), v);
}
self.ri_update_child(&child_q, &rid, child_row, newc, queue)?;
}
}
ReferentialAction::SetNull => {
for (rid, child_row) in self.fk_matching_children(&child_q, &fk, &key)? {
let mut newc = child_row.clone();
for c in &fk.columns {
newc.insert(c.clone(), SqlValue::Null);
}
self.ri_update_child(&child_q, &rid, child_row, newc, queue)?;
}
}
ReferentialAction::SetDefault => {
for (rid, child_row) in self.fk_matching_children(&child_q, &fk, &key)? {
let newc = self.ri_defaults_for(&child_q, &fk, &child_row)?;
self.ri_update_child(&child_q, &rid, child_row, newc, queue)?;
}
self.ri_queue_or_check(parent_q, &child_q, &fk, key, old_vals, checks);
}
}
}
Ok(())
}
fn ri_referenced_error(
&self,
parent_q: &QualifiedName,
child_q: &QualifiedName,
fk: &ForeignKey,
key_vals: &[SqlValue],
) -> SqlError {
SqlError::ForeignKeyViolationReferenced {
table: parent_q.name.clone(),
constraint: fk.name.clone(),
referencing: child_q.name.clone(),
detail: format!(
"Key ({})=({}) is still referenced from table \"{}\".",
fk.ref_columns.join(", "),
display_vals(key_vals),
child_q.name
),
}
}
fn ri_delete_child(&mut self, child_q: &QualifiedName, rid: &str) -> Result<()> {
let (collection, oid) = {
let loaded = self.fk_loaded(child_q)?;
(loaded.meta.storage_collection.clone(), loaded.meta.oid)
};
self.record_pending(
crate::sql::lock::LockObject::Row(oid, rid.to_string()),
crate::sql::lock::LockMode::ForUpdate,
crate::sql::lock::LockScope::Transaction,
);
let loaded = self.tables.get_mut(child_q).unwrap();
loaded.apply_delete(rid);
self.mutations
.lock()
.unwrap()
.push(crate::sql::store::Mutation::Delete {
collection,
row_id: rid.to_string(),
});
Ok(())
}
fn ri_update_child(
&mut self,
child_q: &QualifiedName,
rid: &str,
old: RowValues,
new: RowValues,
queue: &mut VecDeque<RiWork>,
) -> Result<()> {
let table = self.fk_loaded(child_q)?.meta.clone();
let all_cols: Vec<String> = table.columns.iter().map(|c| c.name.clone()).collect();
let row_key = |r: &RowValues| {
ordered_key(
&all_cols
.iter()
.map(|c| r.get(c).cloned().unwrap_or(SqlValue::Null))
.collect::<Vec<_>>(),
)
};
if row_key(&old) == row_key(&new) {
return Ok(());
}
for c in &table.columns {
if !c.nullable && new.get(&c.name).map(SqlValue::is_null).unwrap_or(true) {
return Err(SqlError::NotNullViolation {
column: c.name.clone(),
table: table.name.clone(),
});
}
}
self.check_constraints(&table, &new)?;
self.check_unique_for(child_q, &new, Some(rid))?;
self.fk_check_child(&table, &new, Some(&old))?;
self.record_pending(
crate::sql::lock::LockObject::Row(table.oid, rid.to_string()),
crate::sql::lock::LockMode::ForUpdate,
crate::sql::lock::LockScope::Transaction,
);
let collection = table.storage_collection.clone();
self.write_update(child_q, &collection, &table, rid, new.clone())?;
queue.push_back(RiWork::Updated {
table: child_q.clone(),
old,
new,
});
Ok(())
}
fn ri_defaults_for(
&mut self,
child_q: &QualifiedName,
fk: &ForeignKey,
row: &RowValues,
) -> Result<RowValues> {
let table = self.fk_loaded(child_q)?.meta.clone();
let mut new = row.clone();
for cname in &fk.columns {
let col = table
.column(cname)
.ok_or_else(|| SqlError::UndefinedColumn(cname.clone()))?;
let value = if let Some(seq) = &col.identity_sequence {
let n = self.catalog.next_sequence_value(&table.schema, seq)?;
self.catalog_dirty = true;
crate::sql::dml::coerce_to_col(SqlValue::Int8(n), &table, cname)?
} else if let Some(def) = &col.default {
let v = self.eval_default(def)?;
if v.is_null() {
SqlValue::Null
} else {
crate::sql::dml::coerce_to_col(v, &table, cname)?
}
} else {
SqlValue::Null
};
new.insert(cname.clone(), value);
}
Ok(new)
}
}