use vibesql_catalog::{ForeignKeyConstraint, TableSchema};
use vibesql_storage::{Database, DeferredFkViolation, DeferredFkViolationKind};
pub fn detect_fk_mismatch(
db: &Database,
child_table: &str,
fk: &ForeignKeyConstraint,
) -> Option<(String, String)> {
let parent = db.catalog.get_table(&fk.parent_table)?;
let resolved_indices = resolve_parent_indices(parent, fk);
if parent_has_matching_key(db, parent, &resolved_indices) {
None
} else {
Some((child_table.to_string(), fk.parent_table.clone()))
}
}
fn resolve_parent_indices(
parent: &TableSchema,
fk: &ForeignKeyConstraint,
) -> Vec<usize> {
let names_usable = !fk.parent_column_names.is_empty()
&& fk.parent_column_names.iter().any(|n| !n.is_empty());
if names_usable {
let by_name: Vec<Option<usize>> = fk
.parent_column_names
.iter()
.map(|n| if n.is_empty() { None } else { parent.get_column_index(n) })
.collect();
if by_name.iter().all(|opt| opt.is_some()) {
return by_name.into_iter().map(|opt| opt.unwrap()).collect();
}
}
if fk.parent_column_names.iter().all(|n| n.is_empty()) {
if let Some(pk_indices) = parent.get_primary_key_indices() {
if pk_indices.len() == fk.parent_column_indices.len() {
return pk_indices;
}
}
}
fk.parent_column_indices.clone()
}
fn parent_has_matching_key(
db: &Database,
parent: &TableSchema,
parent_indices: &[usize],
) -> bool {
if parent_indices.is_empty() {
return false;
}
if let Some(pk_indices) = parent.get_primary_key_indices() {
if column_set_eq(&pk_indices, parent_indices) {
return true;
}
}
for unique_idx_set in parent.get_unique_constraint_indices() {
if column_set_eq(&unique_idx_set, parent_indices) {
return true;
}
}
for index in db.catalog.get_table_indexes(&parent.name) {
if !index.is_unique || index.has_expression_columns() || index.is_partial() {
continue;
}
let mut index_col_indices: Vec<usize> = Vec::with_capacity(index.columns.len());
let mut all_resolved = true;
for col in &index.columns {
match col.column_name().and_then(|n| parent.get_column_index(n)) {
Some(idx) => index_col_indices.push(idx),
None => {
all_resolved = false;
break;
}
}
}
if !all_resolved {
continue;
}
if column_set_eq(&index_col_indices, parent_indices) {
return true;
}
}
false
}
fn column_set_eq(a: &[usize], b: &[usize]) -> bool {
if a.len() != b.len() {
return false;
}
let mut a_sorted: Vec<usize> = a.to_vec();
let mut b_sorted: Vec<usize> = b.to_vec();
a_sorted.sort_unstable();
b_sorted.sort_unstable();
a_sorted == b_sorted
}
pub fn fk_values_equal(
child: &vibesql_types::SqlValue,
parent: &vibesql_types::SqlValue,
parent_collation: Option<&str>,
) -> bool {
if child == parent {
return true;
}
if let (Some(c), Some(p)) = (sql_value_as_f64(child), sql_value_as_f64(parent)) {
if c == p {
return true;
}
}
if let (Some(c), Some(p)) = (sql_value_as_text(child), sql_value_as_text(parent)) {
match parent_collation.map(|s| s.to_ascii_lowercase()) {
Some(ref name) if name == "nocase" => return c.eq_ignore_ascii_case(p),
Some(ref name) if name == "rtrim" => {
return c.trim_end_matches(' ') == p.trim_end_matches(' ');
}
_ => {}
}
}
false
}
fn sql_value_as_f64(v: &vibesql_types::SqlValue) -> Option<f64> {
use vibesql_types::SqlValue::*;
match v {
Integer(i) => Some(*i as f64),
Smallint(i) => Some(*i as f64),
Bigint(i) => Some(*i as f64),
Unsigned(i) => Some(*i as f64),
Float(f) => Some(*f as f64),
Real(r) => Some(*r as f64),
Double(d) | Numeric(d) => Some(*d),
Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
Character(s) | Varchar(s) => s.trim().parse::<f64>().ok(),
_ => None,
}
}
fn sql_value_as_text(v: &vibesql_types::SqlValue) -> Option<&str> {
use vibesql_types::SqlValue::*;
match v {
Character(s) | Varchar(s) => Some(s.as_str()),
_ => None,
}
}
pub fn parent_collations_for_fk(
db: &Database,
fk: &ForeignKeyConstraint,
) -> Vec<Option<String>> {
if let Some(parent) = db.catalog.get_table(&fk.parent_table) {
let indices = resolve_parent_indices(parent, fk);
indices
.iter()
.map(|&idx| parent.columns.get(idx).and_then(|c| c.collation.clone()))
.collect()
} else {
vec![None; fk.parent_column_indices.len()]
}
}
pub fn resolved_parent_indices_for_fk(
db: &Database,
fk: &ForeignKeyConstraint,
) -> Vec<usize> {
if let Some(parent) = db.catalog.get_table(&fk.parent_table) {
resolve_parent_indices(parent, fk)
} else {
fk.parent_column_indices.clone()
}
}
#[derive(Debug)]
pub(crate) enum FkRowCheck {
Ok,
Deferred(DeferredFkViolation),
Violation,
}
pub(crate) fn check_fk_row_existence(
db: &Database,
table_name: &str,
fk: &ForeignKeyConstraint,
fk_idx: usize,
fk_values: &[vibesql_types::SqlValue],
full_row_values: &[vibesql_types::SqlValue],
batch_full_rows: &[Vec<vibesql_types::SqlValue>],
) -> Result<FkRowCheck, crate::errors::ExecutorError> {
let parent_table = db.get_table(&fk.parent_table).ok_or_else(|| {
crate::errors::ExecutorError::TableNotFound(fk.parent_table.clone())
})?;
let parent_collations = parent_collations_for_fk(db, fk);
let parent_indices = resolved_parent_indices_for_fk(db, fk);
let snapshot = crate::mvcc::read_snapshot(db);
let key_exists = {
#[cfg(feature = "mvcc_enabled")]
{
parent_table.scan_visible(&snapshot).any(|(_, parent_row)| {
parent_indices.iter().zip(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(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 Ok(FkRowCheck::Ok);
}
if fk.parent_table.eq_ignore_ascii_case(table_name) {
let row_matches = |candidate: &[vibesql_types::SqlValue]| -> bool {
parent_indices.iter().zip(fk_values).enumerate().all(
|(i, (&parent_idx, fk_val))| match candidate.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 row_matches(full_row_values) || batch_full_rows.iter().any(|r| row_matches(r)) {
return Ok(FkRowCheck::Ok);
}
}
let session_defer = db.defer_foreign_keys();
let in_txn = db.in_transaction();
let should_defer = in_txn && (fk.initially_deferred || session_defer);
if should_defer {
return Ok(FkRowCheck::Deferred(DeferredFkViolation {
child_table: table_name.to_string(),
fk_index: fk_idx,
child_row: full_row_values.to_vec(),
kind: DeferredFkViolationKind::ChildInsertOrUpdate,
}));
}
Ok(FkRowCheck::Violation)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn column_set_eq_same_order() {
assert!(column_set_eq(&[1, 2, 3], &[1, 2, 3]));
}
#[test]
fn column_set_eq_reordered() {
assert!(column_set_eq(&[3, 1, 2], &[1, 2, 3]));
}
#[test]
fn column_set_eq_different_lengths() {
assert!(!column_set_eq(&[1, 2], &[1, 2, 3]));
}
#[test]
fn column_set_eq_disjoint() {
assert!(!column_set_eq(&[1, 2], &[3, 4]));
}
#[test]
fn fk_values_equal_strict() {
let v = vibesql_types::SqlValue::Integer(42);
assert!(fk_values_equal(&v, &v, None));
}
#[test]
fn fk_values_equal_numeric_coercion() {
let child = vibesql_types::SqlValue::Integer(88);
let parent = vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("88"));
assert!(fk_values_equal(&child, &parent, None));
}
#[test]
fn fk_values_equal_nocase() {
let child = vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("Alpha"));
let parent = vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("ALPHA"));
assert!(fk_values_equal(&child, &parent, Some("NOCASE")));
assert!(!fk_values_equal(&child, &parent, None));
}
#[test]
fn fk_values_equal_rtrim() {
let child = vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("abc"));
let parent = vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("abc "));
assert!(fk_values_equal(&child, &parent, Some("rtrim")));
assert!(!fk_values_equal(&child, &parent, None));
}
#[test]
fn fk_values_equal_distinct() {
let child = vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("foo"));
let parent = vibesql_types::SqlValue::Varchar(arcstr::ArcStr::from("bar"));
assert!(!fk_values_equal(&child, &parent, None));
assert!(!fk_values_equal(&child, &parent, Some("nocase")));
}
use vibesql_catalog::{ColumnSchema, ReferentialAction};
use vibesql_storage::Row;
use vibesql_types::{DataType, SqlValue};
fn child_fk() -> ForeignKeyConstraint {
ForeignKeyConstraint {
name: Some("fk_c_pid".to_string()),
column_names: vec!["pid".to_string()],
column_indices: vec![1],
parent_table: "p".to_string(),
parent_column_names: vec!["id".to_string()],
parent_column_indices: vec![0],
on_delete: ReferentialAction::NoAction,
on_update: ReferentialAction::NoAction,
is_deferrable: false,
initially_deferred: false,
}
}
fn setup_parent_child(parent_rows: &[i64]) -> (Database, ForeignKeyConstraint) {
let mut db = Database::new();
db.set_foreign_keys_enabled(true);
let p = TableSchema::with_primary_key(
"p".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
vec!["id".to_string()],
);
db.create_table(p).unwrap();
let child_fk = child_fk();
let child_columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("pid".to_string(), DataType::Integer, true),
];
let mut c = TableSchema::with_primary_key(
"c".to_string(),
child_columns,
vec!["id".to_string()],
);
c.foreign_keys.push(child_fk.clone());
db.create_table(c).unwrap();
for id in parent_rows {
db.insert_row("p", Row::new(vec![SqlValue::Integer(*id)])).unwrap();
}
(db, child_fk)
}
#[test]
fn check_fk_row_existence_ok_parent_exists() {
let (db, fk) = setup_parent_child(&[1, 2, 3]);
let full_row = vec![SqlValue::Integer(10), SqlValue::Integer(2)];
let fk_values = vec![SqlValue::Integer(2)];
let outcome = check_fk_row_existence(&db, "c", &fk, 0, &fk_values, &full_row, &[]).unwrap();
assert!(matches!(outcome, FkRowCheck::Ok), "expected Ok, got {:?}", outcome);
}
#[test]
fn check_fk_row_existence_ok_self_row_self_check() {
let mut db = Database::new();
db.set_foreign_keys_enabled(true);
let fk = ForeignKeyConstraint {
name: Some("fk_t_self".to_string()),
column_names: vec!["parent".to_string()],
column_indices: vec![1],
parent_table: "t".to_string(),
parent_column_names: vec!["id".to_string()],
parent_column_indices: vec![0],
on_delete: ReferentialAction::NoAction,
on_update: ReferentialAction::NoAction,
is_deferrable: false,
initially_deferred: false,
};
let cols = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("parent".to_string(), DataType::Integer, true),
];
let mut t = TableSchema::with_primary_key("t".to_string(), cols, vec!["id".to_string()]);
t.foreign_keys.push(fk.clone());
db.create_table(t).unwrap();
let full_row = vec![SqlValue::Integer(5), SqlValue::Integer(5)];
let fk_values = vec![SqlValue::Integer(5)];
let outcome = check_fk_row_existence(&db, "t", &fk, 0, &fk_values, &full_row, &[]).unwrap();
assert!(
matches!(outcome, FkRowCheck::Ok),
"self-FK row-self check must accept, got {:?}",
outcome
);
}
#[test]
fn check_fk_row_existence_ok_self_fk_sibling_in_batch() {
let mut db = Database::new();
db.set_foreign_keys_enabled(true);
let fk = ForeignKeyConstraint {
name: Some("fk_t_self".to_string()),
column_names: vec!["parent".to_string()],
column_indices: vec![1],
parent_table: "t".to_string(),
parent_column_names: vec!["x".to_string()],
parent_column_indices: vec![0],
on_delete: ReferentialAction::NoAction,
on_update: ReferentialAction::NoAction,
is_deferrable: false,
initially_deferred: false,
};
let cols = vec![
ColumnSchema::new("x".to_string(), DataType::Integer, false),
ColumnSchema::new("parent".to_string(), DataType::Integer, true),
];
let mut t = TableSchema::with_primary_key("t".to_string(), cols, vec!["x".to_string()]);
t.foreign_keys.push(fk.clone());
db.create_table(t).unwrap();
let earlier_row = vec![SqlValue::Integer(1), SqlValue::Null];
let batch = vec![earlier_row];
let full_row = vec![SqlValue::Integer(2), SqlValue::Integer(1)];
let fk_values = vec![SqlValue::Integer(1)];
let outcome =
check_fk_row_existence(&db, "t", &fk, 0, &fk_values, &full_row, &batch).unwrap();
assert!(
matches!(outcome, FkRowCheck::Ok),
"multi-row self-FK sibling rescue must accept, got {:?}",
outcome
);
let outcome_no_batch =
check_fk_row_existence(&db, "t", &fk, 0, &fk_values, &full_row, &[]).unwrap();
assert!(
matches!(outcome_no_batch, FkRowCheck::Violation),
"without batch_full_rows the same row must violate, got {:?}",
outcome_no_batch
);
}
#[test]
fn check_fk_row_existence_violation_immediate() {
let (db, fk) = setup_parent_child(&[1, 2, 3]);
let full_row = vec![SqlValue::Integer(10), SqlValue::Integer(999)];
let fk_values = vec![SqlValue::Integer(999)];
let outcome = check_fk_row_existence(&db, "c", &fk, 0, &fk_values, &full_row, &[]).unwrap();
assert!(matches!(outcome, FkRowCheck::Violation), "expected Violation, got {:?}", outcome);
}
#[test]
fn check_fk_row_existence_violation_when_deferred_outside_transaction() {
let (db, mut fk) = setup_parent_child(&[1]);
fk.is_deferrable = true;
fk.initially_deferred = true;
let full_row = vec![SqlValue::Integer(10), SqlValue::Integer(999)];
let fk_values = vec![SqlValue::Integer(999)];
assert!(!db.in_transaction(), "test precondition: no transaction");
let outcome = check_fk_row_existence(&db, "c", &fk, 0, &fk_values, &full_row, &[]).unwrap();
assert!(
matches!(outcome, FkRowCheck::Violation),
"deferred-but-outside-txn must error immediately, got {:?}",
outcome
);
}
#[test]
fn check_fk_row_existence_deferred_initially_deferred_in_txn() {
let (mut db, mut fk) = setup_parent_child(&[1]);
fk.is_deferrable = true;
fk.initially_deferred = true;
db.begin_transaction().unwrap();
assert!(db.in_transaction());
let full_row = vec![SqlValue::Integer(10), SqlValue::Integer(999)];
let fk_values = vec![SqlValue::Integer(999)];
let outcome = check_fk_row_existence(&db, "c", &fk, 0, &fk_values, &full_row, &[]).unwrap();
match outcome {
FkRowCheck::Deferred(v) => {
assert_eq!(v.child_table, "c");
assert_eq!(v.fk_index, 0);
assert_eq!(v.child_row, full_row);
assert_eq!(v.kind, DeferredFkViolationKind::ChildInsertOrUpdate);
}
other => panic!("expected Deferred, got {:?}", other),
}
}
#[test]
fn check_fk_row_existence_deferred_via_session_pragma() {
let (mut db, fk) = setup_parent_child(&[1]);
db.set_defer_foreign_keys(true);
db.begin_transaction().unwrap();
assert!(db.in_transaction());
let full_row = vec![SqlValue::Integer(10), SqlValue::Integer(999)];
let fk_values = vec![SqlValue::Integer(999)];
let outcome = check_fk_row_existence(&db, "c", &fk, 0, &fk_values, &full_row, &[]).unwrap();
assert!(
matches!(outcome, FkRowCheck::Deferred(_)),
"session pragma must defer, got {:?}",
outcome
);
}
}