mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-18 17:14:20 +01:00
992 lines
32 KiB
Rust
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(())
|
|
}
|