Files
turso/core/translate/fkeys.rs
2025-10-07 16:45:23 -04:00

992 lines
32 KiB
Rust

use turso_parser::ast::Expr;
use super::ProgramBuilder;
use crate::{
schema::{BTreeTable, ForeignKey, Index, ResolvedFkRef, ROWID_SENTINEL},
translate::{emitter::Resolver, planner::ROWID_STRS},
vdbe::{
builder::CursorType,
insn::{CmpInsFlags, Insn},
BranchOffset,
},
Result,
};
use std::{collections::HashSet, num::NonZeroUsize, sync::Arc};
#[inline]
pub fn emit_guarded_fk_decrement(program: &mut ProgramBuilder, label: BranchOffset) {
program.emit_insn(Insn::FkIfZero {
is_scope: false,
target_pc: label,
});
program.emit_insn(Insn::FkCounter {
increment_value: -1,
is_scope: false,
});
}
/// Open a read cursor on an index and return its cursor id.
#[inline]
pub fn open_read_index(program: &mut ProgramBuilder, idx: &Arc<Index>) -> usize {
let icur = program.alloc_cursor_id(CursorType::BTreeIndex(idx.clone()));
program.emit_insn(Insn::OpenRead {
cursor_id: icur,
root_page: idx.root_page,
db: 0,
});
icur
}
/// Open a read cursor on a table and return its cursor id.
#[inline]
pub fn open_read_table(program: &mut ProgramBuilder, tbl: &Arc<BTreeTable>) -> usize {
let tcur = program.alloc_cursor_id(CursorType::BTreeTable(tbl.clone()));
program.emit_insn(Insn::OpenRead {
cursor_id: tcur,
root_page: tbl.root_page,
db: 0,
});
tcur
}
/// Copy `len` registers starting at `src_start` to a fresh block and apply index affinities.
/// Returns the destination start register.
#[inline]
fn copy_with_affinity(
program: &mut ProgramBuilder,
src_start: usize,
len: usize,
idx: &Index,
aff_from_tbl: &BTreeTable,
) -> usize {
let dst = program.alloc_registers(len);
for i in 0..len {
program.emit_insn(Insn::Copy {
src_reg: src_start + i,
dst_reg: dst + i,
extra_amount: 0,
});
}
if let Some(count) = NonZeroUsize::new(len) {
program.emit_insn(Insn::Affinity {
start_reg: dst,
count,
affinities: build_index_affinity_string(idx, aff_from_tbl),
});
}
dst
}
/// Issue an index probe using `Found`/`NotFound` and route to `on_found`/`on_not_found`.
pub fn index_probe<F, G>(
program: &mut ProgramBuilder,
icur: usize,
record_reg: usize,
num_regs: usize,
mut on_found: F,
mut on_not_found: G,
) -> Result<()>
where
F: FnMut(&mut ProgramBuilder) -> Result<()>,
G: FnMut(&mut ProgramBuilder) -> Result<()>,
{
let lbl_found = program.allocate_label();
let lbl_join = program.allocate_label();
program.emit_insn(Insn::Found {
cursor_id: icur,
target_pc: lbl_found,
record_reg,
num_regs,
});
// NOT FOUND path
on_not_found(program)?;
program.emit_insn(Insn::Goto {
target_pc: lbl_join,
});
// FOUND path
program.preassign_label_to_next_insn(lbl_found);
on_found(program)?;
// Join & close once
program.preassign_label_to_next_insn(lbl_join);
program.emit_insn(Insn::Close { cursor_id: icur });
Ok(())
}
/// Iterate a table and call `on_match` when all child columns equal the key at `parent_key_start`.
/// Skips rows where any FK column is NULL. If `self_exclude_rowid` is Some, the row with that rowid is skipped.
fn table_scan_match_any<F>(
program: &mut ProgramBuilder,
child_tbl: &Arc<BTreeTable>,
child_cols: &[String],
parent_key_start: usize,
self_exclude_rowid: Option<usize>,
mut on_match: F,
) -> Result<()>
where
F: FnMut(&mut ProgramBuilder) -> Result<()>,
{
let ccur = open_read_table(program, child_tbl);
let done = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: ccur,
pc_if_empty: done,
});
let loop_top = program.allocate_label();
program.preassign_label_to_next_insn(loop_top);
let next_row = program.allocate_label();
// Compare each FK column to parent key component.
for (i, cname) in child_cols.iter().enumerate() {
let (pos, _) = child_tbl.get_column(cname).ok_or_else(|| {
crate::LimboError::InternalError(format!("child col {cname} missing"))
})?;
let tmp = program.alloc_register();
program.emit_insn(Insn::Column {
cursor_id: ccur,
column: pos,
dest: tmp,
default: None,
});
program.emit_insn(Insn::IsNull {
reg: tmp,
target_pc: next_row,
});
let cont = program.allocate_label();
program.emit_insn(Insn::Eq {
lhs: tmp,
rhs: parent_key_start + i,
target_pc: cont,
flags: CmpInsFlags::default().jump_if_null(),
collation: Some(super::collate::CollationSeq::Binary),
});
program.emit_insn(Insn::Goto {
target_pc: next_row,
});
program.preassign_label_to_next_insn(cont);
}
//self-reference exclusion on rowid
if let Some(parent_rowid) = self_exclude_rowid {
let child_rowid = program.alloc_register();
let skip = program.allocate_label();
program.emit_insn(Insn::RowId {
cursor_id: ccur,
dest: child_rowid,
});
program.emit_insn(Insn::Eq {
lhs: child_rowid,
rhs: parent_rowid,
target_pc: skip,
flags: CmpInsFlags::default(),
collation: None,
});
on_match(program)?;
program.preassign_label_to_next_insn(skip);
} else {
on_match(program)?;
}
program.preassign_label_to_next_insn(next_row);
program.emit_insn(Insn::Next {
cursor_id: ccur,
pc_if_next: loop_top,
});
program.preassign_label_to_next_insn(done);
program.emit_insn(Insn::Close { cursor_id: ccur });
Ok(())
}
/// Build the index affinity mask string (one char per indexed column).
#[inline]
pub fn build_index_affinity_string(idx: &Index, table: &BTreeTable) -> String {
idx.columns
.iter()
.map(|ic| table.columns[ic.pos_in_table].affinity().aff_mask())
.collect()
}
/// For deferred FKs: increment the global counter; for immediate FKs: halt with FK error.
pub fn emit_fk_violation(program: &mut ProgramBuilder, fk: &ForeignKey) -> Result<()> {
if fk.deferred {
program.emit_insn(Insn::FkCounter {
increment_value: 1,
is_scope: false,
});
} else {
program.emit_insn(Insn::Halt {
err_code: crate::error::SQLITE_CONSTRAINT_FOREIGNKEY,
description: "FOREIGN KEY constraint failed".to_string(),
});
}
Ok(())
}
/// Stabilize the NEW row image for FK checks (UPDATE):
/// fill in unmodified PK columns from the current row so the NEW PK vector is complete.
pub fn stabilize_new_row_for_fk(
program: &mut ProgramBuilder,
table_btree: &BTreeTable,
set_clauses: &[(usize, Box<Expr>)],
cursor_id: usize,
start: usize,
rowid_new_reg: usize,
) -> Result<()> {
if table_btree.primary_key_columns.is_empty() {
return Ok(());
}
let set_cols: HashSet<usize> = set_clauses
.iter()
.filter_map(|(i, _)| if *i == ROWID_SENTINEL { None } else { Some(*i) })
.collect();
for (pk_name, _) in &table_btree.primary_key_columns {
let (pos, col) = table_btree
.get_column(pk_name)
.ok_or_else(|| crate::LimboError::InternalError(format!("pk col {pk_name} missing")))?;
if !set_cols.contains(&pos) {
if col.is_rowid_alias {
program.emit_insn(Insn::Copy {
src_reg: rowid_new_reg,
dst_reg: start + pos,
extra_amount: 0,
});
} else {
program.emit_insn(Insn::Column {
cursor_id,
column: pos,
dest: start + pos,
default: None,
});
}
}
}
Ok(())
}
/// Parent-side checks when the parent PK might change (UPDATE on parent):
/// Detect if any child references the OLD key (potential violation), and if any references the NEW key
/// (which cancels one potential violation). For composite PKs this builds OLD/NEW vectors first.
#[allow(clippy::too_many_arguments)]
pub fn emit_parent_pk_change_checks(
program: &mut ProgramBuilder,
resolver: &Resolver,
table_btree: &BTreeTable,
cursor_id: usize,
old_rowid_reg: usize,
start: usize,
rowid_new_reg: usize,
rowid_set_clause_reg: Option<usize>,
set_clauses: &[(usize, Box<Expr>)],
) -> Result<()> {
let updated_positions: HashSet<usize> = set_clauses.iter().map(|(i, _)| *i).collect();
let incoming = resolver
.schema
.resolved_fks_referencing(&table_btree.name)?;
let affects_pk = incoming
.iter()
.any(|r| r.parent_key_may_change(&updated_positions, table_btree));
if !affects_pk {
return Ok(());
}
match table_btree.primary_key_columns.len() {
0 => emit_rowid_pk_change_check(
program,
&incoming,
resolver,
old_rowid_reg,
rowid_set_clause_reg.unwrap_or(old_rowid_reg),
),
1 => emit_single_pk_change_check(
program,
&incoming,
resolver,
table_btree,
cursor_id,
start,
rowid_new_reg,
),
_ => emit_composite_pk_change_check(
program,
&incoming,
resolver,
table_btree,
cursor_id,
old_rowid_reg,
start,
rowid_new_reg,
),
}
}
/// Rowid-table parent PK change: compare rowid OLD vs NEW; if changed, run two-pass counters.
pub fn emit_rowid_pk_change_check(
program: &mut ProgramBuilder,
incoming: &[ResolvedFkRef],
resolver: &Resolver,
old_rowid_reg: usize,
new_rowid_reg: usize,
) -> Result<()> {
let skip = program.allocate_label();
program.emit_insn(Insn::Eq {
lhs: new_rowid_reg,
rhs: old_rowid_reg,
target_pc: skip,
flags: CmpInsFlags::default(),
collation: None,
});
let old_pk = program.alloc_register();
let new_pk = program.alloc_register();
program.emit_insn(Insn::Copy {
src_reg: old_rowid_reg,
dst_reg: old_pk,
extra_amount: 0,
});
program.emit_insn(Insn::Copy {
src_reg: new_rowid_reg,
dst_reg: new_pk,
extra_amount: 0,
});
emit_fk_parent_pk_change_counters(program, incoming, resolver, old_pk, new_pk, 1)?;
program.preassign_label_to_next_insn(skip);
Ok(())
}
/// Single-column PK parent change: load OLD and NEW; if changed, run two-pass counters.
pub fn emit_single_pk_change_check(
program: &mut ProgramBuilder,
incoming: &[ResolvedFkRef],
resolver: &Resolver,
table_btree: &BTreeTable,
cursor_id: usize,
start: usize,
rowid_new_reg: usize,
) -> Result<()> {
let (pk_name, _) = &table_btree.primary_key_columns[0];
let (pos, col) = table_btree.get_column(pk_name).unwrap();
let old_reg = program.alloc_register();
if col.is_rowid_alias {
program.emit_insn(Insn::RowId {
cursor_id,
dest: old_reg,
});
} else {
program.emit_insn(Insn::Column {
cursor_id,
column: pos,
dest: old_reg,
default: None,
});
}
let new_reg = if col.is_rowid_alias {
rowid_new_reg
} else {
start + pos
};
let skip = program.allocate_label();
program.emit_insn(Insn::Eq {
lhs: old_reg,
rhs: new_reg,
target_pc: skip,
flags: CmpInsFlags::default(),
collation: None,
});
let old_pk = program.alloc_register();
let new_pk = program.alloc_register();
program.emit_insn(Insn::Copy {
src_reg: old_reg,
dst_reg: old_pk,
extra_amount: 0,
});
program.emit_insn(Insn::Copy {
src_reg: new_reg,
dst_reg: new_pk,
extra_amount: 0,
});
emit_fk_parent_pk_change_counters(program, incoming, resolver, old_pk, new_pk, 1)?;
program.preassign_label_to_next_insn(skip);
Ok(())
}
/// Composite-PK parent change: build OLD/NEW vectors; if any component differs, run two-pass counters.
#[allow(clippy::too_many_arguments)]
pub fn emit_composite_pk_change_check(
program: &mut ProgramBuilder,
incoming: &[ResolvedFkRef],
resolver: &Resolver,
table_btree: &BTreeTable,
cursor_id: usize,
old_rowid_reg: usize,
start: usize,
rowid_new_reg: usize,
) -> Result<()> {
let pk_len = table_btree.primary_key_columns.len();
let old_pk = program.alloc_registers(pk_len);
for (i, (pk_name, _)) in table_btree.primary_key_columns.iter().enumerate() {
let (pos, col) = table_btree.get_column(pk_name).unwrap();
if col.is_rowid_alias {
program.emit_insn(Insn::Copy {
src_reg: old_rowid_reg,
dst_reg: old_pk + i,
extra_amount: 0,
});
} else {
program.emit_insn(Insn::Column {
cursor_id,
column: pos,
dest: old_pk + i,
default: None,
});
}
}
let new_pk = program.alloc_registers(pk_len);
for (i, (pk_name, _)) in table_btree.primary_key_columns.iter().enumerate() {
let (pos, col) = table_btree.get_column(pk_name).unwrap();
let src = if col.is_rowid_alias {
rowid_new_reg
} else {
start + pos
};
program.emit_insn(Insn::Copy {
src_reg: src,
dst_reg: new_pk + i,
extra_amount: 0,
});
}
let skip = program.allocate_label();
let changed = program.allocate_label();
for i in 0..pk_len {
let next = if i + 1 == pk_len {
None
} else {
Some(program.allocate_label())
};
program.emit_insn(Insn::Eq {
lhs: old_pk + i,
rhs: new_pk + i,
target_pc: next.unwrap_or(skip),
flags: CmpInsFlags::default(),
collation: None,
});
program.emit_insn(Insn::Goto { target_pc: changed });
if let Some(n) = next {
program.preassign_label_to_next_insn(n);
}
}
program.preassign_label_to_next_insn(changed);
emit_fk_parent_pk_change_counters(program, incoming, resolver, old_pk, new_pk, pk_len)?;
program.preassign_label_to_next_insn(skip);
Ok(())
}
/// Two-pass parent-side maintenance for UPDATE of a parent key:
/// 1. Probe child for OLD key, increment deferred counter if any references exist.
/// 2. Probe child for NEW key, guarded decrement cancels exactly one increment if present
pub fn emit_fk_parent_pk_change_counters(
program: &mut ProgramBuilder,
incoming: &[ResolvedFkRef],
resolver: &Resolver,
old_pk_start: usize,
new_pk_start: usize,
n_cols: usize,
) -> Result<()> {
for fk_ref in incoming {
emit_fk_parent_key_probe(
program,
resolver,
fk_ref,
old_pk_start,
n_cols,
ParentProbePass::Old,
)?;
emit_fk_parent_key_probe(
program,
resolver,
fk_ref,
new_pk_start,
n_cols,
ParentProbePass::New,
)?;
}
Ok(())
}
#[derive(Clone, Copy)]
enum ParentProbePass {
Old,
New,
}
/// Probe the child side for a given parent key
fn emit_fk_parent_key_probe(
program: &mut ProgramBuilder,
resolver: &Resolver,
fk_ref: &ResolvedFkRef,
parent_key_start: usize,
n_cols: usize,
pass: ParentProbePass,
) -> Result<()> {
let child_tbl = &fk_ref.child_table;
let child_cols = &fk_ref.fk.child_columns;
let is_deferred = fk_ref.fk.deferred;
let on_match = |p: &mut ProgramBuilder| -> Result<()> {
match (is_deferred, pass) {
// OLD key referenced by a child
(false, ParentProbePass::Old) => {
// Immediate FK: fail now.
emit_fk_violation(p, &fk_ref.fk)?; // HALT for immediate
}
(true, ParentProbePass::Old) => {
// Deferred FK: increment counter.
p.emit_insn(Insn::FkCounter {
increment_value: 1,
is_scope: false,
});
}
// NEW key referenced by a child (cancel one deferred violation)
(true, ParentProbePass::New) => {
// Guard to avoid underflow if OLD pass didn't increment.
let skip = p.allocate_label();
emit_guarded_fk_decrement(p, skip);
p.preassign_label_to_next_insn(skip);
}
// Immediate FK on NEW pass: nothing to cancel; do nothing.
(false, ParentProbePass::New) => {}
}
Ok(())
};
// Prefer exact child index on (child_cols...)
let idx = resolver.schema.get_indices(&child_tbl.name).find(|ix| {
ix.columns.len() == child_cols.len()
&& ix
.columns
.iter()
.zip(child_cols.iter())
.all(|(ic, cc)| ic.name.eq_ignore_ascii_case(cc))
});
if let Some(ix) = idx {
let icur = open_read_index(program, ix);
let probe = copy_with_affinity(program, parent_key_start, n_cols, ix, child_tbl);
// FOUND => on_match; NOT FOUND => no-op
index_probe(program, icur, probe, n_cols, on_match, |_p| Ok(()))?;
} else {
// Table scan fallback
table_scan_match_any(
program,
child_tbl,
child_cols,
parent_key_start,
None,
on_match,
)?;
}
Ok(())
}
/// Build a parent key vector (in FK parent-column order) into `dest_start`.
/// Handles rowid aliasing and explicit ROWID names; uses current row for non-rowid columns.
fn build_parent_key(
program: &mut ProgramBuilder,
parent_bt: &BTreeTable,
parent_cols: &[String],
parent_cursor_id: usize,
parent_rowid_reg: usize,
dest_start: usize,
) -> Result<()> {
for (i, pcol) in parent_cols.iter().enumerate() {
let src = if ROWID_STRS.iter().any(|s| pcol.eq_ignore_ascii_case(s)) {
parent_rowid_reg
} else {
let (pos, col) = parent_bt
.get_column(pcol)
.ok_or_else(|| crate::LimboError::InternalError(format!("col {pcol} missing")))?;
if col.is_rowid_alias {
parent_rowid_reg
} else {
program.emit_insn(Insn::Column {
cursor_id: parent_cursor_id,
column: pos,
dest: dest_start + i,
default: None,
});
continue;
}
};
program.emit_insn(Insn::Copy {
src_reg: src,
dst_reg: dest_start + i,
extra_amount: 0,
});
}
Ok(())
}
/// Child-side FK maintenance for UPDATE/UPSERT:
/// If any FK columns of this child row changed:
/// Pass 1 (OLD tuple): if OLD is non-NULL and parent is missing: decrement deferred counter (guarded).
/// Pass 2 (NEW tuple): if NEW is non-NULL and parent is missing: immediate error or deferred(+1).
#[allow(clippy::too_many_arguments)]
pub fn emit_fk_child_update_counters(
program: &mut ProgramBuilder,
resolver: &Resolver,
child_tbl: &BTreeTable,
child_table_name: &str,
child_cursor_id: usize,
new_start_reg: usize,
new_rowid_reg: usize,
updated_cols: &HashSet<usize>,
) -> crate::Result<()> {
// Helper: materialize OLD tuple for this FK; returns (start_reg, ncols) or None if any component is NULL.
let load_old_tuple =
|program: &mut ProgramBuilder, fk_cols: &[String]| -> Option<(usize, usize)> {
let n = fk_cols.len();
let start = program.alloc_registers(n);
let null_jmp = program.allocate_label();
for (k, cname) in fk_cols.iter().enumerate() {
let (pos, _col) = match child_tbl.get_column(cname) {
Some(v) => v,
None => {
return None;
}
};
program.emit_column_or_rowid(child_cursor_id, pos, start + k);
program.emit_insn(Insn::IsNull {
reg: start + k,
target_pc: null_jmp,
});
}
// No NULLs, proceed
let cont = program.allocate_label();
program.emit_insn(Insn::Goto { target_pc: cont });
// NULL encountered: invalidate tuple by jumping here
program.preassign_label_to_next_insn(null_jmp);
program.preassign_label_to_next_insn(cont);
Some((start, n))
};
for fk_ref in resolver.schema.resolved_fks_for_child(child_table_name)? {
// If the child-side FK columns did not change, there is nothing to do.
if !fk_ref.child_key_changed(updated_cols, child_tbl) {
continue;
}
let ncols = fk_ref.child_cols.len();
// Pass 1: OLD tuple handling only for deferred FKs
if fk_ref.fk.deferred {
if let Some((old_start, _)) = load_old_tuple(program, &fk_ref.child_cols) {
if fk_ref.parent_uses_rowid {
// Parent key is rowid: probe parent table by rowid
let parent_tbl = resolver
.schema
.get_btree_table(&fk_ref.fk.parent_table)
.expect("parent btree");
let pcur = open_read_table(program, &parent_tbl);
// first FK col is the rowid value
let rid = program.alloc_register();
program.emit_insn(Insn::Copy {
src_reg: old_start,
dst_reg: rid,
extra_amount: 0,
});
program.emit_insn(Insn::MustBeInt { reg: rid });
// If NOT exists => decrement
let miss = program.allocate_label();
program.emit_insn(Insn::NotExists {
cursor: pcur,
rowid_reg: rid,
target_pc: miss,
});
// found: close & continue
let join = program.allocate_label();
program.emit_insn(Insn::Close { cursor_id: pcur });
program.emit_insn(Insn::Goto { target_pc: join });
// missing: guarded decrement
program.preassign_label_to_next_insn(miss);
program.emit_insn(Insn::Close { cursor_id: pcur });
let skip = program.allocate_label();
emit_guarded_fk_decrement(program, skip);
program.preassign_label_to_next_insn(skip);
program.preassign_label_to_next_insn(join);
} else {
// Parent key is a unique index: use index probe and guarded decrement on NOT FOUND
let parent_tbl = resolver
.schema
.get_btree_table(&fk_ref.fk.parent_table)
.expect("parent btree");
let idx = fk_ref
.parent_unique_index
.as_ref()
.expect("parent unique index required");
let icur = open_read_index(program, idx);
// Copy OLD tuple and apply parent index affinities
let probe = copy_with_affinity(program, old_start, ncols, idx, &parent_tbl);
// Found: nothing; Not found: guarded decrement
index_probe(
program,
icur,
probe,
ncols,
|_p| Ok(()),
|p| {
let skip = p.allocate_label();
emit_guarded_fk_decrement(p, skip);
p.preassign_label_to_next_insn(skip);
Ok(())
},
)?;
}
}
}
// Pass 2: NEW tuple handling
let fk_ok = program.allocate_label();
for cname in &fk_ref.fk.child_columns {
let (i, col) = child_tbl.get_column(cname).unwrap();
let src = if col.is_rowid_alias {
new_rowid_reg
} else {
new_start_reg + i
};
program.emit_insn(Insn::IsNull {
reg: src,
target_pc: fk_ok,
});
}
if fk_ref.parent_uses_rowid {
let parent_tbl = resolver
.schema
.get_btree_table(&fk_ref.fk.parent_table)
.expect("parent btree");
let pcur = open_read_table(program, &parent_tbl);
// Take the first child column value from NEW image
let (i_child, col_child) = child_tbl.get_column(&fk_ref.child_cols[0]).unwrap();
let val_reg = if col_child.is_rowid_alias {
new_rowid_reg
} else {
new_start_reg + i_child
};
let tmp = program.alloc_register();
program.emit_insn(Insn::Copy {
src_reg: val_reg,
dst_reg: tmp,
extra_amount: 0,
});
program.emit_insn(Insn::MustBeInt { reg: tmp });
let violation = program.allocate_label();
program.emit_insn(Insn::NotExists {
cursor: pcur,
rowid_reg: tmp,
target_pc: violation,
});
// found: close and continue
program.emit_insn(Insn::Close { cursor_id: pcur });
program.emit_insn(Insn::Goto { target_pc: fk_ok });
// missing: violation (immediate HALT or deferred +1)
program.preassign_label_to_next_insn(violation);
program.emit_insn(Insn::Close { cursor_id: pcur });
emit_fk_violation(program, &fk_ref.fk)?;
} else {
let parent_tbl = resolver
.schema
.get_btree_table(&fk_ref.fk.parent_table)
.expect("parent btree");
let idx = fk_ref
.parent_unique_index
.as_ref()
.expect("parent unique index required");
let icur = open_read_index(program, idx);
// Build NEW probe (in FK child column order, aligns with parent index columns)
let probe = {
let start = program.alloc_registers(ncols);
for (k, cname) in fk_ref.child_cols.iter().enumerate() {
let (i, col) = child_tbl.get_column(cname).unwrap();
program.emit_insn(Insn::Copy {
src_reg: if col.is_rowid_alias {
new_rowid_reg
} else {
new_start_reg + i
},
dst_reg: start + k,
extra_amount: 0,
});
}
// Apply affinities of the parent index/table
if let Some(cnt) = NonZeroUsize::new(ncols) {
program.emit_insn(Insn::Affinity {
start_reg: start,
count: cnt,
affinities: build_index_affinity_string(idx, &parent_tbl),
});
}
start
};
// FOUND: ok; NOT FOUND: violation path
index_probe(
program,
icur,
probe,
ncols,
|_p| Ok(()),
|p| {
emit_fk_violation(p, &fk_ref.fk)?;
Ok(())
},
)?;
program.emit_insn(Insn::Goto { target_pc: fk_ok });
}
// Skip label for NEW tuple NULL short-circuit
program.preassign_label_to_next_insn(fk_ok);
}
Ok(())
}
/// Prevent deleting a parent row that is still referenced by any child.
/// For each incoming FK referencing `parent_table_name`:
/// 1. Build the parent key vector from the current parent row (FK parent-column order,
/// or the table's PK columns when the FK omits parent columns).
/// 2. Look for referencing child rows:
/// - Prefer an exact child index on (child_columns...). If found, probe the index.
/// - Otherwise scan the child table. For self-referential FKs, exclude the current rowid.
/// 3. If a referencing child exists:
/// - Immediate FK: HALT with SQLITE_CONSTRAINT_FOREIGNKEY
/// - Deferred FK: FkCounter +1
pub fn emit_fk_delete_parent_existence_checks(
program: &mut ProgramBuilder,
resolver: &Resolver,
parent_table_name: &str,
parent_cursor_id: usize,
parent_rowid_reg: usize,
) -> Result<()> {
let parent_bt = resolver
.schema
.get_btree_table(parent_table_name)
.ok_or_else(|| crate::LimboError::InternalError("parent not btree".into()))?;
for fk_ref in resolver
.schema
.resolved_fks_referencing(parent_table_name)?
{
let is_self_ref = fk_ref
.child_table
.name
.eq_ignore_ascii_case(parent_table_name);
// Build parent key in FK's parent-column order (or table PK columns if unspecified).
let parent_cols: Vec<String> = if fk_ref.fk.parent_columns.is_empty() {
parent_bt
.primary_key_columns
.iter()
.map(|(n, _)| n.clone())
.collect()
} else {
fk_ref.fk.parent_columns.clone()
};
let ncols = parent_cols.len();
let parent_key_start = program.alloc_registers(ncols);
build_parent_key(
program,
&parent_bt,
&parent_cols,
parent_cursor_id,
parent_rowid_reg,
parent_key_start,
)?;
// Try an exact child index on (child_columns...) if available and not self-ref
let child_cols = &fk_ref.fk.child_columns;
let child_idx = if !is_self_ref {
resolver
.schema
.get_indices(&fk_ref.child_table.name)
.find(|idx| {
idx.columns.len() == child_cols.len()
&& idx
.columns
.iter()
.zip(child_cols.iter())
.all(|(ic, cc)| ic.name.eq_ignore_ascii_case(cc))
})
} else {
None
};
if let Some(idx) = child_idx {
// Index probe: FOUND => violation; NOT FOUND => ok.
let icur = open_read_index(program, idx);
let probe =
copy_with_affinity(program, parent_key_start, ncols, idx, &fk_ref.child_table);
index_probe(
program,
icur,
probe,
ncols,
|p| {
emit_fk_violation(p, &fk_ref.fk)?;
Ok(())
},
|_p| Ok(()),
)?;
} else {
// Table scan fallback; for self-ref, exclude the same parent row by rowid.
table_scan_match_any(
program,
&fk_ref.child_table,
child_cols,
parent_key_start,
if is_self_ref {
Some(parent_rowid_reg)
} else {
None
},
|p| {
emit_fk_violation(p, &fk_ref.fk)?;
Ok(())
},
)?;
}
}
Ok(())
}