mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-03 23:34:24 +01:00
Implement proper handling of deferred foreign keys
This commit is contained in:
@@ -583,6 +583,7 @@ impl Database {
|
||||
busy_timeout: RwLock::new(Duration::new(0, 0)),
|
||||
is_mvcc_bootstrap_connection: AtomicBool::new(is_mvcc_bootstrap_connection),
|
||||
fk_pragma: AtomicBool::new(false),
|
||||
fk_deferred_violations: AtomicIsize::new(0),
|
||||
});
|
||||
self.n_connections
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
@@ -1102,6 +1103,7 @@ pub struct Connection {
|
||||
is_mvcc_bootstrap_connection: AtomicBool,
|
||||
/// Whether pragma foreign_keys=ON for this connection
|
||||
fk_pragma: AtomicBool,
|
||||
fk_deferred_violations: AtomicIsize,
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
@@ -1540,7 +1542,6 @@ impl Connection {
|
||||
pub fn foreign_keys_enabled(&self) -> bool {
|
||||
self.fk_pragma.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub(crate) fn clear_deferred_foreign_key_violations(&self) -> isize {
|
||||
self.fk_deferred_violations.swap(0, Ordering::Release)
|
||||
}
|
||||
|
||||
272
core/schema.rs
272
core/schema.rs
@@ -849,12 +849,17 @@ impl Schema {
|
||||
/// Compute all resolved FKs *referencing* `table_name` (arg: `table_name` is the parent).
|
||||
/// Each item contains the child table, normalized columns/positions, and the parent lookup
|
||||
/// strategy (rowid vs. UNIQUE index or PK).
|
||||
pub fn resolved_fks_referencing(&self, table_name: &str) -> Vec<ResolvedFkRef> {
|
||||
pub fn resolved_fks_referencing(&self, table_name: &str) -> Result<Vec<ResolvedFkRef>> {
|
||||
let fk_mismatch_err = |child: &str, parent: &str| -> crate::LimboError {
|
||||
crate::LimboError::Constraint(format!(
|
||||
"foreign key mismatch - \"{child}\" referencing \"{parent}\""
|
||||
))
|
||||
};
|
||||
let target = normalize_ident(table_name);
|
||||
let mut out = Vec::with_capacity(4); // arbitrary estimate
|
||||
let parent_tbl = self
|
||||
.get_btree_table(&target)
|
||||
.expect("parent table must exist");
|
||||
.ok_or_else(|| fk_mismatch_err("<unknown>", &target))?;
|
||||
|
||||
// Precompute helper to find parent unique index, if it's not the rowid
|
||||
let find_parent_unique = |cols: &Vec<String>| -> Option<Arc<Index>> {
|
||||
@@ -875,78 +880,82 @@ impl Schema {
|
||||
let Some(child) = t.btree() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for fk in &child.foreign_keys {
|
||||
if fk.parent_table != target {
|
||||
if !fk.parent_table.eq_ignore_ascii_case(&target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve + normalize columns
|
||||
if fk.child_columns.is_empty() {
|
||||
// SQLite requires an explicit child column list unless the table has a single-column PK that
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
}
|
||||
let child_cols: Vec<String> = fk.child_columns.clone();
|
||||
let mut child_pos = Vec::with_capacity(child_cols.len());
|
||||
|
||||
// If no explicit parent columns were given, they were validated in add_btree_table()
|
||||
// to match the parent's PK. We resolve them the same way here.
|
||||
for cname in &child_cols {
|
||||
let (i, _) = child
|
||||
.get_column(cname)
|
||||
.ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))?;
|
||||
child_pos.push(i);
|
||||
}
|
||||
let parent_cols: Vec<String> = if fk.parent_columns.is_empty() {
|
||||
parent_tbl
|
||||
.primary_key_columns
|
||||
.iter()
|
||||
.map(|(col, _)| col)
|
||||
.cloned()
|
||||
.collect()
|
||||
if !parent_tbl.primary_key_columns.is_empty() {
|
||||
parent_tbl
|
||||
.primary_key_columns
|
||||
.iter()
|
||||
.map(|(col, _)| col)
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
}
|
||||
} else {
|
||||
fk.parent_columns.clone()
|
||||
};
|
||||
|
||||
// Child positions
|
||||
let child_pos: Vec<usize> = child_cols
|
||||
.iter()
|
||||
.map(|cname| {
|
||||
child
|
||||
.get_column(cname)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or_else(|| panic!("child col {}.{} missing", child.name, cname))
|
||||
})
|
||||
.collect();
|
||||
// Same length required
|
||||
if parent_cols.len() != child_cols.len() {
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
}
|
||||
|
||||
let parent_pos: Vec<usize> = parent_cols
|
||||
.iter()
|
||||
.map(|cname| {
|
||||
// Allow "rowid" sentinel; return 0 but it won't be used when parent_uses_rowid == true
|
||||
parent_tbl
|
||||
.get_column(cname)
|
||||
.map(|(i, _)| i)
|
||||
.or_else(|| {
|
||||
if cname.eq_ignore_ascii_case("rowid") {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!("parent col {}.{cname} missing", parent_tbl.name)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut parent_pos = Vec::with_capacity(parent_cols.len());
|
||||
for pc in &parent_cols {
|
||||
let pos = parent_tbl.get_column(pc).map(|(i, _)| i).or_else(|| {
|
||||
ROWID_STRS
|
||||
.iter()
|
||||
.any(|s| pc.eq_ignore_ascii_case(s))
|
||||
.then_some(0)
|
||||
});
|
||||
let Some(p) = pos else {
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
};
|
||||
parent_pos.push(p);
|
||||
}
|
||||
|
||||
// Detect parent rowid usage (single-column and rowid/alias)
|
||||
let parent_uses_rowid = parent_cols.len() == 1 && {
|
||||
let c = parent_cols[0].as_str();
|
||||
c.eq_ignore_ascii_case("rowid")
|
||||
|| parent_tbl.columns.iter().any(|col| {
|
||||
col.is_rowid_alias
|
||||
&& col
|
||||
.name
|
||||
// Determine if parent key is ROWID/alias
|
||||
let parent_uses_rowid = parent_tbl.primary_key_columns.len().eq(&1) && {
|
||||
if parent_tbl.primary_key_columns.len() == 1 {
|
||||
let pk_name = &parent_tbl.primary_key_columns[0].0;
|
||||
// rowid or alias INTEGER PRIMARY KEY; either is ok implicitly
|
||||
parent_tbl.columns.iter().any(|c| {
|
||||
c.is_rowid_alias
|
||||
&& c.name
|
||||
.as_deref()
|
||||
.is_some_and(|n| n.eq_ignore_ascii_case(c))
|
||||
})
|
||||
.is_some_and(|n| n.eq_ignore_ascii_case(pk_name))
|
||||
}) || ROWID_STRS.iter().any(|&r| r.eq_ignore_ascii_case(pk_name))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// If not rowid, there must be a non-partial UNIQUE exactly on parent_cols
|
||||
let parent_unique_index = if parent_uses_rowid {
|
||||
None
|
||||
} else {
|
||||
find_parent_unique(&parent_cols)
|
||||
.ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))?
|
||||
.into()
|
||||
};
|
||||
|
||||
fk.validate()?;
|
||||
out.push(ResolvedFkRef {
|
||||
child_table: Arc::clone(&child),
|
||||
fk: Arc::clone(fk),
|
||||
@@ -959,80 +968,80 @@ impl Schema {
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Compute all resolved FKs *declared by* `child_table`
|
||||
pub fn resolved_fks_for_child(&self, child_table: &str) -> Vec<ResolvedFkRef> {
|
||||
let child_name = normalize_ident(child_table);
|
||||
let Some(child) = self.get_btree_table(&child_name) else {
|
||||
return vec![];
|
||||
pub fn resolved_fks_for_child(&self, child_table: &str) -> crate::Result<Vec<ResolvedFkRef>> {
|
||||
let fk_mismatch_err = |child: &str, parent: &str| -> crate::LimboError {
|
||||
crate::LimboError::Constraint(format!(
|
||||
"foreign key mismatch - \"{child}\" referencing \"{parent}\""
|
||||
))
|
||||
};
|
||||
|
||||
// Helper to find the UNIQUE/index on the parent that matches the resolved parent cols
|
||||
let find_parent_unique =
|
||||
|parent_tbl: &BTreeTable, cols: &Vec<String>| -> Option<Arc<Index>> {
|
||||
self.get_indices(&parent_tbl.name)
|
||||
.find(|idx| {
|
||||
idx.unique
|
||||
&& idx.columns.len() == cols.len()
|
||||
&& idx
|
||||
.columns
|
||||
.iter()
|
||||
.zip(cols.iter())
|
||||
.all(|(ic, pc)| ic.name.eq_ignore_ascii_case(pc))
|
||||
})
|
||||
.cloned()
|
||||
};
|
||||
let child_name = normalize_ident(child_table);
|
||||
let child = self
|
||||
.get_btree_table(&child_name)
|
||||
.ok_or_else(|| fk_mismatch_err(&child_name, "<unknown>"))?;
|
||||
|
||||
let mut out = Vec::with_capacity(child.foreign_keys.len());
|
||||
for fk in &child.foreign_keys {
|
||||
let parent_name = &fk.parent_table;
|
||||
let Some(parent_tbl) = self.get_btree_table(parent_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Normalize columns
|
||||
let child_cols: Vec<String> = fk
|
||||
.child_columns
|
||||
.iter()
|
||||
.map(|s| normalize_ident(s))
|
||||
.collect();
|
||||
for fk in &child.foreign_keys {
|
||||
let parent_name = normalize_ident(&fk.parent_table);
|
||||
let parent_tbl = self
|
||||
.get_btree_table(&parent_name)
|
||||
.ok_or_else(|| fk_mismatch_err(&child.name, &parent_name))?;
|
||||
|
||||
let child_cols: Vec<String> = fk.child_columns.clone();
|
||||
if child_cols.is_empty() {
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
}
|
||||
|
||||
// Child positions exist
|
||||
let mut child_pos = Vec::with_capacity(child_cols.len());
|
||||
for cname in &child_cols {
|
||||
let (i, _) = child
|
||||
.get_column(cname)
|
||||
.ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))?;
|
||||
child_pos.push(i);
|
||||
}
|
||||
|
||||
let parent_cols: Vec<String> = if fk.parent_columns.is_empty() {
|
||||
if !parent_tbl.primary_key_columns.is_empty() {
|
||||
parent_tbl
|
||||
.primary_key_columns
|
||||
.iter()
|
||||
.map(|(n, _)| normalize_ident(n))
|
||||
.map(|(col, _)| col)
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
vec!["rowid".to_string()]
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
}
|
||||
} else {
|
||||
fk.parent_columns
|
||||
.iter()
|
||||
.map(|s| normalize_ident(s))
|
||||
.collect()
|
||||
fk.parent_columns.clone()
|
||||
};
|
||||
|
||||
let child_pos: Vec<usize> = child_cols
|
||||
.iter()
|
||||
.map(|c| child.get_column(c).expect("child col missing").0)
|
||||
.collect();
|
||||
let parent_pos: Vec<usize> = parent_cols
|
||||
.iter()
|
||||
.map(|c| {
|
||||
parent_tbl
|
||||
.get_column(c)
|
||||
.map(|(i, _)| i)
|
||||
.or_else(|| c.eq_ignore_ascii_case("rowid").then_some(0))
|
||||
.expect("parent col missing")
|
||||
})
|
||||
.collect();
|
||||
if parent_cols.len() != child_cols.len() {
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
}
|
||||
|
||||
let parent_uses_rowid = parent_cols.len() == 1 && {
|
||||
// Parent positions exist, or rowid sentinel
|
||||
let mut parent_pos = Vec::with_capacity(parent_cols.len());
|
||||
for pc in &parent_cols {
|
||||
let pos = parent_tbl.get_column(pc).map(|(i, _)| i).or_else(|| {
|
||||
ROWID_STRS
|
||||
.iter()
|
||||
.any(|&r| r.eq_ignore_ascii_case(pc))
|
||||
.then_some(0)
|
||||
});
|
||||
let Some(p) = pos else {
|
||||
return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
|
||||
};
|
||||
parent_pos.push(p);
|
||||
}
|
||||
|
||||
let parent_uses_rowid = parent_cols.len().eq(&1) && {
|
||||
let c = parent_cols[0].as_str();
|
||||
c.eq_ignore_ascii_case("rowid")
|
||||
ROWID_STRS.iter().any(|&r| r.eq_ignore_ascii_case(c))
|
||||
|| parent_tbl.columns.iter().any(|col| {
|
||||
col.is_rowid_alias
|
||||
&& col
|
||||
@@ -1042,12 +1051,27 @@ impl Schema {
|
||||
})
|
||||
};
|
||||
|
||||
// Must be PK or a non-partial UNIQUE on exactly those columns.
|
||||
let parent_unique_index = if parent_uses_rowid {
|
||||
None
|
||||
} else {
|
||||
find_parent_unique(&parent_tbl, &parent_cols)
|
||||
self.get_indices(&parent_tbl.name)
|
||||
.find(|idx| {
|
||||
idx.unique
|
||||
&& idx.where_clause.is_none()
|
||||
&& idx.columns.len() == parent_cols.len()
|
||||
&& idx
|
||||
.columns
|
||||
.iter()
|
||||
.zip(parent_cols.iter())
|
||||
.all(|(ic, pc)| ic.name.eq_ignore_ascii_case(pc))
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))?
|
||||
.into()
|
||||
};
|
||||
|
||||
fk.validate()?;
|
||||
out.push(ResolvedFkRef {
|
||||
child_table: Arc::clone(&child),
|
||||
fk: Arc::clone(fk),
|
||||
@@ -1059,7 +1083,8 @@ impl Schema {
|
||||
parent_unique_index,
|
||||
});
|
||||
}
|
||||
out
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Returns if any table declares a FOREIGN KEY whose parent is `table_name`.
|
||||
@@ -1534,7 +1559,7 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
|
||||
constraints,
|
||||
} in columns
|
||||
{
|
||||
let name = normalize_ident(col_name.as_str());
|
||||
let name = col_name.as_str().to_string();
|
||||
// Regular sqlite tables have an integer rowid that uniquely identifies a row.
|
||||
// Even if you create a table with a column e.g. 'id INT PRIMARY KEY', there will still
|
||||
// be a separate hidden rowid, and the 'id' column will have a separate index built for it.
|
||||
@@ -1673,7 +1698,7 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
|
||||
}
|
||||
|
||||
cols.push(Column {
|
||||
name: Some(name),
|
||||
name: Some(normalize_ident(&name)),
|
||||
ty,
|
||||
ty_str,
|
||||
primary_key,
|
||||
@@ -1809,6 +1834,29 @@ pub struct ForeignKey {
|
||||
/// DEFERRABLE INITIALLY DEFERRED
|
||||
pub deferred: bool,
|
||||
}
|
||||
impl ForeignKey {
|
||||
fn validate(&self) -> Result<()> {
|
||||
// TODO: remove this when actions are implemented
|
||||
if !(matches!(self.on_update, RefAct::NoAction)
|
||||
&& matches!(self.on_delete, RefAct::NoAction))
|
||||
{
|
||||
crate::bail_parse_error!(
|
||||
"foreign key actions other than NO ACTION are not implemented"
|
||||
);
|
||||
}
|
||||
if self
|
||||
.parent_columns
|
||||
.iter()
|
||||
.any(|c| ROWID_STRS.iter().any(|&r| r.eq_ignore_ascii_case(c)))
|
||||
{
|
||||
return Err(crate::LimboError::Constraint(format!(
|
||||
"foreign key mismatch referencing \"{}\"",
|
||||
self.parent_table
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A single resolved foreign key where `parent_table == target`.
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -371,6 +371,7 @@ mod tests {
|
||||
hidden: false,
|
||||
}],
|
||||
unique_sets: vec![],
|
||||
foreign_keys: vec![],
|
||||
})),
|
||||
});
|
||||
|
||||
@@ -413,6 +414,7 @@ mod tests {
|
||||
hidden: false,
|
||||
}],
|
||||
unique_sets: vec![],
|
||||
foreign_keys: vec![],
|
||||
})),
|
||||
});
|
||||
// Right table t2(id=2)
|
||||
@@ -446,6 +448,7 @@ mod tests {
|
||||
hidden: false,
|
||||
}],
|
||||
unique_sets: vec![],
|
||||
foreign_keys: vec![],
|
||||
})),
|
||||
});
|
||||
table_references
|
||||
@@ -486,6 +489,7 @@ mod tests {
|
||||
hidden: false,
|
||||
}],
|
||||
unique_sets: vec![],
|
||||
foreign_keys: vec![],
|
||||
})),
|
||||
});
|
||||
table_references
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1025
core/translate/fkeys.rs
Normal file
1025
core/translate/fkeys.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ use turso_parser::ast::{
|
||||
use crate::error::{
|
||||
SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, SQLITE_CONSTRAINT_UNIQUE,
|
||||
};
|
||||
use crate::schema::{self, Affinity, Index, Table};
|
||||
use crate::schema::{self, Affinity, BTreeTable, Index, ResolvedFkRef, Table};
|
||||
use crate::translate::emitter::{
|
||||
emit_cdc_insns, emit_cdc_patch_record, prepare_cdc_if_necessary, OperationMode,
|
||||
};
|
||||
@@ -16,6 +16,10 @@ use crate::translate::expr::{
|
||||
bind_and_rewrite_expr, emit_returning_results, process_returning_clause, walk_expr_mut,
|
||||
BindingBehavior, ReturningValueRegisters, WalkControl,
|
||||
};
|
||||
use crate::translate::fkeys::{
|
||||
build_index_affinity_string, emit_fk_scope_if_needed, emit_fk_violation, index_probe,
|
||||
open_read_index, open_read_table,
|
||||
};
|
||||
use crate::translate::plan::TableReferences;
|
||||
use crate::translate::planner::ROWID_STRS;
|
||||
use crate::translate::upsert::{
|
||||
@@ -134,11 +138,6 @@ pub fn translate_insert(
|
||||
if !btree_table.has_rowid {
|
||||
crate::bail_parse_error!("INSERT into WITHOUT ROWID table is not supported");
|
||||
}
|
||||
let has_child_fks = fk_enabled && !btree_table.foreign_keys.is_empty();
|
||||
let has_parent_fks = fk_enabled
|
||||
&& resolver
|
||||
.schema
|
||||
.any_resolved_fks_referencing(table_name.as_str());
|
||||
|
||||
let root_page = btree_table.root_page;
|
||||
|
||||
@@ -242,14 +241,11 @@ pub fn translate_insert(
|
||||
connection,
|
||||
)?;
|
||||
|
||||
if has_child_fks || has_parent_fks {
|
||||
program.emit_insn(Insn::FkCounter {
|
||||
increment_value: 1,
|
||||
check_abort: false,
|
||||
is_scope: true,
|
||||
});
|
||||
}
|
||||
|
||||
let has_fks = if fk_enabled {
|
||||
emit_fk_scope_if_needed(&mut program, resolver, table_name.as_str(), true)?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let mut yield_reg_opt = None;
|
||||
let mut temp_table_ctx = None;
|
||||
let (num_values, cursor_id) = match body {
|
||||
@@ -274,7 +270,6 @@ pub fn translate_insert(
|
||||
|
||||
let query_destination = QueryDestination::CoroutineYield {
|
||||
yield_reg,
|
||||
// keep implementation_start as halt_label (producer internals)
|
||||
coroutine_implementation_start: halt_label,
|
||||
};
|
||||
program.incr_nesting();
|
||||
@@ -1043,13 +1038,14 @@ pub fn translate_insert(
|
||||
}
|
||||
}
|
||||
}
|
||||
if has_child_fks || has_parent_fks {
|
||||
emit_fk_checks_for_insert(
|
||||
if has_fks {
|
||||
// Child-side check must run before Insert (may HALT or increment deferred counter)
|
||||
emit_fk_child_insert_checks(
|
||||
&mut program,
|
||||
resolver,
|
||||
&insertion,
|
||||
table_name.as_str(),
|
||||
!inserting_multiple_rows,
|
||||
&btree_table,
|
||||
insertion.first_col_register(),
|
||||
insertion.key_register(),
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -1061,6 +1057,11 @@ pub fn translate_insert(
|
||||
table_name: table_name.to_string(),
|
||||
});
|
||||
|
||||
if has_fks {
|
||||
// After the row is actually present, repair deferred counters for children referencing this NEW parent key.
|
||||
emit_parent_side_fk_decrement_on_insert(&mut program, resolver, &btree_table, &insertion)?;
|
||||
}
|
||||
|
||||
if let Some((seq_cursor_id, r_seq, r_seq_rowid, table_name_reg)) = autoincrement_meta {
|
||||
let no_update_needed_label = program.allocate_label();
|
||||
program.emit_insn(Insn::Le {
|
||||
@@ -1151,6 +1152,7 @@ pub fn translate_insert(
|
||||
&mut result_columns,
|
||||
cdc_table.as_ref().map(|c| c.0),
|
||||
row_done_label,
|
||||
connection,
|
||||
)?;
|
||||
} else {
|
||||
// UpsertDo::Nothing case
|
||||
@@ -1198,13 +1200,8 @@ pub fn translate_insert(
|
||||
}
|
||||
|
||||
program.preassign_label_to_next_insn(stmt_epilogue);
|
||||
if has_child_fks || has_parent_fks {
|
||||
// close FK scope and surface deferred violations
|
||||
program.emit_insn(Insn::FkCounter {
|
||||
increment_value: -1,
|
||||
check_abort: true,
|
||||
is_scope: true,
|
||||
});
|
||||
if has_fks {
|
||||
emit_fk_scope_if_needed(&mut program, resolver, table_name.as_str(), false)?;
|
||||
}
|
||||
|
||||
program.resolve_label(halt_label, program.offset());
|
||||
@@ -1900,38 +1897,29 @@ fn emit_update_sqlite_sequence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emit child->parent foreign key checks for an INSERT, for the current row
|
||||
fn emit_fk_checks_for_insert(
|
||||
/// Child-side FK checks for INSERT of a single row:
|
||||
/// For each outgoing FK on `child_tbl`, if the NEW tuple's FK columns are all non-NULL,
|
||||
/// verify that the referenced parent key exists.
|
||||
pub fn emit_fk_child_insert_checks(
|
||||
program: &mut ProgramBuilder,
|
||||
resolver: &Resolver,
|
||||
insertion: &Insertion,
|
||||
table_name: &str,
|
||||
single_row_insert: bool,
|
||||
) -> Result<()> {
|
||||
let after_all = program.allocate_label();
|
||||
program.emit_insn(Insn::FkIfZero {
|
||||
target_pc: after_all,
|
||||
if_zero: true,
|
||||
});
|
||||
child_tbl: &BTreeTable,
|
||||
new_start_reg: usize,
|
||||
new_rowid_reg: usize,
|
||||
) -> crate::Result<()> {
|
||||
for fk_ref in resolver.schema.resolved_fks_for_child(&child_tbl.name)? {
|
||||
let ncols = fk_ref.child_cols.len();
|
||||
let is_self_ref = fk_ref.fk.parent_table.eq_ignore_ascii_case(&child_tbl.name);
|
||||
|
||||
// Iterate child FKs declared on this table
|
||||
for fk_ref in resolver.schema.resolved_fks_for_child(table_name) {
|
||||
let parent_tbl = resolver
|
||||
.schema
|
||||
.get_btree_table(&fk_ref.fk.parent_table)
|
||||
.expect("parent table");
|
||||
let num_child_cols = fk_ref.child_cols.len();
|
||||
let is_self_single =
|
||||
table_name.eq_ignore_ascii_case(&fk_ref.fk.parent_table) && single_row_insert;
|
||||
// if any child FK value is NULL, this row doesn't reference the parent.
|
||||
// Short-circuit if any NEW component is NULL
|
||||
let fk_ok = program.allocate_label();
|
||||
for &pos_in_child in fk_ref.child_pos.iter() {
|
||||
// Map INSERT image register for that column
|
||||
let src = insertion
|
||||
.col_mappings
|
||||
.get(pos_in_child)
|
||||
.expect("col must be present")
|
||||
.register;
|
||||
for cname in &fk_ref.child_cols {
|
||||
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,
|
||||
@@ -1939,36 +1927,29 @@ fn emit_fk_checks_for_insert(
|
||||
}
|
||||
|
||||
if fk_ref.parent_uses_rowid {
|
||||
// Parent is rowid/alias: single-reg probe
|
||||
let pcur = program.alloc_cursor_id(CursorType::BTreeTable(parent_tbl.clone()));
|
||||
program.emit_insn(Insn::OpenRead {
|
||||
cursor_id: pcur,
|
||||
root_page: parent_tbl.root_page,
|
||||
db: 0,
|
||||
});
|
||||
let rowid_pos = 0; // guaranteed if parent_uses_rowid
|
||||
let src = insertion
|
||||
.get_col_mapping_by_name(fk_ref.child_cols[rowid_pos].as_str())
|
||||
.unwrap()
|
||||
.register;
|
||||
let violation = program.allocate_label();
|
||||
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 child col carries rowid
|
||||
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: src,
|
||||
src_reg: val_reg,
|
||||
dst_reg: tmp,
|
||||
extra_amount: 0,
|
||||
});
|
||||
// coerce to INT (parent rowid affinity)
|
||||
program.emit_insn(Insn::MustBeInt { reg: tmp });
|
||||
if is_self_single {
|
||||
program.emit_insn(Insn::Eq {
|
||||
lhs: tmp,
|
||||
rhs: insertion.key_register(),
|
||||
target_pc: fk_ok,
|
||||
flags: CmpInsFlags::default(),
|
||||
collation: None,
|
||||
});
|
||||
}
|
||||
|
||||
let violation = program.allocate_label();
|
||||
program.emit_insn(Insn::NotExists {
|
||||
cursor: pcur,
|
||||
rowid_reg: tmp,
|
||||
@@ -1980,102 +1961,296 @@ fn emit_fk_checks_for_insert(
|
||||
program.preassign_label_to_next_insn(violation);
|
||||
program.emit_insn(Insn::Close { cursor_id: pcur });
|
||||
|
||||
// Deferred vs immediate
|
||||
if fk_ref.fk.deferred {
|
||||
// Self-ref: count (don’t halt). Non-self: standard behavior.
|
||||
if is_self_ref {
|
||||
program.emit_insn(Insn::FkCounter {
|
||||
increment_value: 1,
|
||||
check_abort: false,
|
||||
is_scope: false,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Halt {
|
||||
err_code: crate::error::SQLITE_CONSTRAINT_FOREIGNKEY,
|
||||
description: "FOREIGN KEY constraint failed".to_string(),
|
||||
});
|
||||
emit_fk_violation(program, &fk_ref.fk)?;
|
||||
}
|
||||
} else if let Some(ix) = &fk_ref.parent_unique_index {
|
||||
if is_self_single {
|
||||
let skip_probe = program.allocate_label();
|
||||
for (i, &pos_in_child) in fk_ref.child_pos.iter().enumerate() {
|
||||
let child_reg = insertion.col_mappings.get(pos_in_child).unwrap().register;
|
||||
let parent_reg = insertion
|
||||
.get_col_mapping_by_name(fk_ref.parent_cols[i].as_str())
|
||||
.unwrap()
|
||||
.register;
|
||||
program.emit_insn(Insn::Ne {
|
||||
lhs: child_reg,
|
||||
rhs: parent_reg,
|
||||
target_pc: skip_probe, // any mismatch and we do the normal probe
|
||||
flags: CmpInsFlags::default().jump_if_null(),
|
||||
collation: None,
|
||||
} else {
|
||||
// Parent by unique index
|
||||
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 from child NEW values; apply parent index affinities
|
||||
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,
|
||||
});
|
||||
}
|
||||
// all matched, OK
|
||||
program.emit_insn(Insn::Goto { target_pc: fk_ok });
|
||||
program.preassign_label_to_next_insn(skip_probe);
|
||||
}
|
||||
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
|
||||
};
|
||||
index_probe(
|
||||
program,
|
||||
icur,
|
||||
probe,
|
||||
ncols,
|
||||
|_p| Ok(()),
|
||||
|p| {
|
||||
if is_self_ref {
|
||||
p.emit_insn(Insn::FkCounter {
|
||||
increment_value: 1,
|
||||
is_scope: false,
|
||||
});
|
||||
} else {
|
||||
emit_fk_violation(p, &fk_ref.fk)?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
program.emit_insn(Insn::Goto { target_pc: fk_ok });
|
||||
}
|
||||
|
||||
// Parent has a UNIQUE index exactly on parent_cols: use Found against that index
|
||||
let icur = program.alloc_cursor_id(CursorType::BTreeIndex(ix.clone()));
|
||||
program.emit_insn(Insn::OpenRead {
|
||||
cursor_id: icur,
|
||||
root_page: ix.root_page,
|
||||
db: 0,
|
||||
});
|
||||
program.preassign_label_to_next_insn(fk_ok);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Build probe (child values order == parent index order by construction)
|
||||
let probe_start = program.alloc_registers(num_child_cols);
|
||||
for (i, &pos_in_child) in fk_ref.child_pos.iter().enumerate() {
|
||||
let src = insertion.col_mappings.get(pos_in_child).unwrap().register;
|
||||
/// Build NEW parent key image in FK parent-column order into a contiguous register block.
|
||||
/// Handles 3 shapes:
|
||||
/// - parent_uses_rowid: single "rowid" component
|
||||
/// - explicit fk.parent_columns
|
||||
/// - fk.parent_columns empty => use parent's declared PK columns (order-preserving)
|
||||
fn build_parent_key_image_for_insert(
|
||||
program: &mut ProgramBuilder,
|
||||
parent_table: &BTreeTable,
|
||||
pref: &ResolvedFkRef,
|
||||
insertion: &Insertion,
|
||||
) -> crate::Result<(usize, usize)> {
|
||||
// Decide column list
|
||||
let parent_cols: Vec<String> = if pref.parent_uses_rowid {
|
||||
vec!["rowid".to_string()]
|
||||
} else if !pref.fk.parent_columns.is_empty() {
|
||||
pref.fk.parent_columns.clone()
|
||||
} else {
|
||||
// fall back to the declared PK of the parent table, in schema order
|
||||
parent_table
|
||||
.primary_key_columns
|
||||
.iter()
|
||||
.map(|(n, _)| n.clone())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let ncols = parent_cols.len();
|
||||
let start = program.alloc_registers(ncols);
|
||||
// Copy from the would-be parent insertion
|
||||
for (i, pname) in parent_cols.iter().enumerate() {
|
||||
let src = if pname.eq_ignore_ascii_case("rowid") {
|
||||
insertion.key_register()
|
||||
} else {
|
||||
// For rowid-alias parents, get_col_mapping_by_name will return the key mapping,
|
||||
// not the NULL placeholder in col_mappings.
|
||||
insertion
|
||||
.get_col_mapping_by_name(pname)
|
||||
.ok_or_else(|| {
|
||||
crate::LimboError::PlanningError(format!(
|
||||
"Column '{}' not present in INSERT image for parent {}",
|
||||
pname, parent_table.name
|
||||
))
|
||||
})?
|
||||
.register
|
||||
};
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: src,
|
||||
dst_reg: start + i,
|
||||
extra_amount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Apply affinities of the parent columns (or integer for rowid)
|
||||
let aff: String = if pref.parent_uses_rowid {
|
||||
"i".to_string()
|
||||
} else {
|
||||
parent_cols
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let (_, col) = parent_table.get_column(name).ok_or_else(|| {
|
||||
crate::LimboError::InternalError(format!("parent col {name} missing"))
|
||||
})?;
|
||||
Ok::<_, crate::LimboError>(col.affinity().aff_mask())
|
||||
})
|
||||
.collect::<Result<String, _>>()?
|
||||
};
|
||||
if let Some(count) = NonZeroUsize::new(ncols) {
|
||||
program.emit_insn(Insn::Affinity {
|
||||
start_reg: start,
|
||||
count,
|
||||
affinities: aff,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((start, ncols))
|
||||
}
|
||||
|
||||
/// Parent-side: when inserting into the parent, decrement the counter
|
||||
/// if any child rows reference the NEW parent key.
|
||||
/// We *always* do this for deferred FKs, and we *also* do it for
|
||||
/// self-referential FKs (even if immediate) because the insert can
|
||||
/// “repair” a prior child-insert count recorded earlier in the same statement.
|
||||
pub fn emit_parent_side_fk_decrement_on_insert(
|
||||
program: &mut ProgramBuilder,
|
||||
resolver: &Resolver,
|
||||
parent_table: &BTreeTable,
|
||||
insertion: &Insertion,
|
||||
) -> crate::Result<()> {
|
||||
for pref in resolver
|
||||
.schema
|
||||
.resolved_fks_referencing(&parent_table.name)?
|
||||
{
|
||||
let is_self_ref = pref
|
||||
.child_table
|
||||
.name
|
||||
.eq_ignore_ascii_case(&parent_table.name);
|
||||
// Skip only when it cannot repair anything: non-deferred and not self-ref.
|
||||
if !pref.fk.deferred && !is_self_ref {
|
||||
continue;
|
||||
}
|
||||
let (new_pk_start, n_cols) =
|
||||
build_parent_key_image_for_insert(program, parent_table, &pref, insertion)?;
|
||||
|
||||
let child_tbl = &pref.child_table;
|
||||
let child_cols = &pref.fk.child_columns;
|
||||
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);
|
||||
// Copy key into probe regs and apply child-index affinities
|
||||
let probe_start = program.alloc_registers(n_cols);
|
||||
for i in 0..n_cols {
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: src,
|
||||
src_reg: new_pk_start + i,
|
||||
dst_reg: probe_start + i,
|
||||
extra_amount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let aff: String = ix
|
||||
.columns
|
||||
.iter()
|
||||
.map(|c| parent_tbl.columns[c.pos_in_table].affinity().aff_mask())
|
||||
.collect();
|
||||
program.emit_insn(Insn::Affinity {
|
||||
start_reg: probe_start,
|
||||
count: std::num::NonZeroUsize::new(num_child_cols).unwrap(),
|
||||
affinities: aff,
|
||||
});
|
||||
if let Some(count) = NonZeroUsize::new(n_cols) {
|
||||
program.emit_insn(Insn::Affinity {
|
||||
start_reg: probe_start,
|
||||
count,
|
||||
affinities: build_index_affinity_string(ix, child_tbl),
|
||||
});
|
||||
}
|
||||
|
||||
let found = program.allocate_label();
|
||||
program.emit_insn(Insn::Found {
|
||||
cursor_id: icur,
|
||||
target_pc: found,
|
||||
record_reg: probe_start,
|
||||
num_regs: num_child_cols,
|
||||
num_regs: n_cols,
|
||||
});
|
||||
|
||||
// Not found: violation
|
||||
// Not found => nothing to decrement
|
||||
program.emit_insn(Insn::Close { cursor_id: icur });
|
||||
if fk_ref.fk.deferred {
|
||||
program.emit_insn(Insn::FkCounter {
|
||||
increment_value: 1,
|
||||
check_abort: false,
|
||||
is_scope: false,
|
||||
let skip = program.allocate_label();
|
||||
program.emit_insn(Insn::Goto { target_pc: skip });
|
||||
|
||||
// Found => guarded decrement
|
||||
program.resolve_label(found, program.offset());
|
||||
program.emit_insn(Insn::Close { cursor_id: icur });
|
||||
program.emit_insn(Insn::FkIfZero {
|
||||
is_scope: false,
|
||||
target_pc: skip,
|
||||
});
|
||||
program.emit_insn(Insn::FkCounter {
|
||||
increment_value: -1,
|
||||
is_scope: false,
|
||||
});
|
||||
program.resolve_label(skip, program.offset());
|
||||
} else {
|
||||
// fallback scan :(
|
||||
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();
|
||||
let next_row = program.allocate_label();
|
||||
program.resolve_label(loop_top, program.offset());
|
||||
|
||||
for (i, child_name) in child_cols.iter().enumerate() {
|
||||
let (pos, _) = child_tbl.get_column(child_name).ok_or_else(|| {
|
||||
crate::LimboError::InternalError(format!("child col {child_name} missing"))
|
||||
})?;
|
||||
let tmp = program.alloc_register();
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: ccur,
|
||||
column: pos,
|
||||
dest: tmp,
|
||||
default: None,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Halt {
|
||||
err_code: crate::error::SQLITE_CONSTRAINT_FOREIGNKEY,
|
||||
description: "FOREIGN KEY constraint failed".to_string(),
|
||||
|
||||
program.emit_insn(Insn::IsNull {
|
||||
reg: tmp,
|
||||
target_pc: next_row,
|
||||
});
|
||||
|
||||
let cont = program.allocate_label();
|
||||
program.emit_insn(Insn::Eq {
|
||||
lhs: tmp,
|
||||
rhs: new_pk_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.resolve_label(cont, program.offset());
|
||||
}
|
||||
program.emit_insn(Insn::Goto { target_pc: fk_ok });
|
||||
// Found OK
|
||||
program.preassign_label_to_next_insn(found);
|
||||
program.emit_insn(Insn::Close { cursor_id: icur });
|
||||
|
||||
// Matched one child row -> guarded decrement
|
||||
program.emit_insn(Insn::FkIfZero {
|
||||
is_scope: false,
|
||||
target_pc: next_row,
|
||||
});
|
||||
program.emit_insn(Insn::FkCounter {
|
||||
is_scope: false,
|
||||
increment_value: -1,
|
||||
});
|
||||
|
||||
program.resolve_label(next_row, program.offset());
|
||||
program.emit_insn(Insn::Next {
|
||||
cursor_id: ccur,
|
||||
pc_if_next: loop_top,
|
||||
});
|
||||
|
||||
program.resolve_label(done, program.offset());
|
||||
program.emit_insn(Insn::Close { cursor_id: ccur });
|
||||
}
|
||||
|
||||
program.preassign_label_to_next_insn(fk_ok);
|
||||
}
|
||||
|
||||
program.preassign_label_to_next_insn(after_all);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) mod delete;
|
||||
pub(crate) mod display;
|
||||
pub(crate) mod emitter;
|
||||
pub(crate) mod expr;
|
||||
pub(crate) mod fkeys;
|
||||
pub(crate) mod group_by;
|
||||
pub(crate) mod index;
|
||||
pub(crate) mod insert;
|
||||
|
||||
@@ -5,11 +5,9 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use turso_parser::ast::{self, Upsert};
|
||||
|
||||
use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY;
|
||||
use crate::translate::emitter::{
|
||||
emit_fk_child_existence_checks, emit_fk_parent_existence_checks,
|
||||
emit_fk_parent_pk_change_counters,
|
||||
};
|
||||
use crate::schema::ROWID_SENTINEL;
|
||||
use crate::translate::expr::{walk_expr, WalkControl};
|
||||
use crate::translate::fkeys::{emit_fk_child_update_counters, emit_parent_pk_change_checks};
|
||||
use crate::translate::insert::format_unique_violation_desc;
|
||||
use crate::translate::planner::ROWID_STRS;
|
||||
use crate::vdbe::insn::CmpInsFlags;
|
||||
@@ -471,176 +469,49 @@ pub fn emit_upsert(
|
||||
}
|
||||
|
||||
let (changed_cols, rowid_changed) = collect_changed_cols(table, set_pairs);
|
||||
let rowid_alias_idx = table.columns().iter().position(|c| c.is_rowid_alias);
|
||||
let has_direct_rowid_update = set_pairs
|
||||
.iter()
|
||||
.any(|(idx, _)| *idx == rowid_alias_idx.unwrap_or(ROWID_SENTINEL));
|
||||
let has_user_provided_rowid = if let Some(i) = rowid_alias_idx {
|
||||
set_pairs.iter().any(|(idx, _)| *idx == i) || has_direct_rowid_update
|
||||
} else {
|
||||
has_direct_rowid_update
|
||||
};
|
||||
|
||||
let rowid_set_clause_reg = if has_user_provided_rowid {
|
||||
Some(new_rowid_reg.unwrap_or(conflict_rowid_reg))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(bt) = table.btree() {
|
||||
if connection.foreign_keys_enabled() {
|
||||
let rowid_new_reg = new_rowid_reg.unwrap_or(conflict_rowid_reg);
|
||||
|
||||
// Child-side checks
|
||||
if resolver.schema.has_child_fks(bt.name.as_str()) {
|
||||
emit_fk_child_existence_checks(
|
||||
emit_fk_child_update_counters(
|
||||
program,
|
||||
resolver,
|
||||
&bt,
|
||||
table.get_name(),
|
||||
tbl_cursor_id,
|
||||
new_start,
|
||||
rowid_new_reg,
|
||||
&changed_cols,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Parent-side checks only if any incoming FK could care
|
||||
if resolver
|
||||
.schema
|
||||
.any_resolved_fks_referencing(table.get_name())
|
||||
{
|
||||
// if parent key can't change, skip
|
||||
let updated_parent_positions: HashSet<usize> =
|
||||
set_pairs.iter().map(|(i, _)| *i).collect();
|
||||
let incoming = resolver.schema.resolved_fks_referencing(table.get_name());
|
||||
let parent_key_may_change = incoming
|
||||
.iter()
|
||||
.any(|r| r.parent_key_may_change(&updated_parent_positions, &bt));
|
||||
|
||||
if parent_key_may_change {
|
||||
let skip_parent_fk = program.allocate_label();
|
||||
let pk_len = bt.primary_key_columns.len();
|
||||
|
||||
match pk_len {
|
||||
0 => {
|
||||
// implicit rowid
|
||||
program.emit_insn(Insn::Eq {
|
||||
lhs: rowid_new_reg,
|
||||
rhs: conflict_rowid_reg,
|
||||
target_pc: skip_parent_fk,
|
||||
flags: CmpInsFlags::default(),
|
||||
collation: program.curr_collation(),
|
||||
});
|
||||
emit_fk_parent_existence_checks(
|
||||
program,
|
||||
resolver,
|
||||
table.get_name(),
|
||||
tbl_cursor_id,
|
||||
conflict_rowid_reg,
|
||||
)?;
|
||||
program.preassign_label_to_next_insn(skip_parent_fk);
|
||||
}
|
||||
1 => {
|
||||
// single-col declared PK
|
||||
let (pk_name, _) = &bt.primary_key_columns[0];
|
||||
let (pos, col) = bt.get_column(pk_name).unwrap();
|
||||
|
||||
let old_reg = program.alloc_register();
|
||||
if col.is_rowid_alias {
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: tbl_cursor_id,
|
||||
dest: old_reg,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: tbl_cursor_id,
|
||||
column: pos,
|
||||
dest: old_reg,
|
||||
default: None,
|
||||
});
|
||||
}
|
||||
let new_reg = new_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: program.curr_collation(),
|
||||
});
|
||||
emit_fk_parent_existence_checks(
|
||||
program,
|
||||
resolver,
|
||||
table.get_name(),
|
||||
tbl_cursor_id,
|
||||
conflict_rowid_reg,
|
||||
)?;
|
||||
program.preassign_label_to_next_insn(skip);
|
||||
}
|
||||
_ => {
|
||||
// composite PK: build OLD/NEW vectors and do the 2-pass counter logic
|
||||
let old_pk_start = program.alloc_registers(pk_len);
|
||||
for (i, (pk_name, _)) in bt.primary_key_columns.iter().enumerate() {
|
||||
let (pos, col) = bt.get_column(pk_name).unwrap();
|
||||
if col.is_rowid_alias {
|
||||
// old rowid (UPSERT target) == conflict_rowid_reg
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: conflict_rowid_reg,
|
||||
dst_reg: old_pk_start + i,
|
||||
extra_amount: 0,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: tbl_cursor_id,
|
||||
column: pos,
|
||||
dest: old_pk_start + i,
|
||||
default: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let new_pk_start = program.alloc_registers(pk_len);
|
||||
for (i, (pk_name, _)) in bt.primary_key_columns.iter().enumerate() {
|
||||
let (pos, col) = bt.get_column(pk_name).unwrap();
|
||||
let src = if col.is_rowid_alias {
|
||||
rowid_new_reg
|
||||
} else {
|
||||
new_start + pos
|
||||
};
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: src,
|
||||
dst_reg: new_pk_start + i,
|
||||
extra_amount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Compare OLD vs NEW, if all equal then skip
|
||||
let skip = program.allocate_label();
|
||||
let changed = program.allocate_label();
|
||||
for i in 0..pk_len {
|
||||
if i == pk_len - 1 {
|
||||
program.emit_insn(Insn::Eq {
|
||||
lhs: old_pk_start + i,
|
||||
rhs: new_pk_start + i,
|
||||
target_pc: skip,
|
||||
flags: CmpInsFlags::default(),
|
||||
collation: program.curr_collation(),
|
||||
});
|
||||
program.emit_insn(Insn::Goto { target_pc: changed });
|
||||
} else {
|
||||
let next = program.allocate_label();
|
||||
program.emit_insn(Insn::Eq {
|
||||
lhs: old_pk_start + i,
|
||||
rhs: new_pk_start + i,
|
||||
target_pc: next,
|
||||
flags: CmpInsFlags::default(),
|
||||
collation: program.curr_collation(),
|
||||
});
|
||||
program.emit_insn(Insn::Goto { target_pc: changed });
|
||||
program.preassign_label_to_next_insn(next);
|
||||
}
|
||||
}
|
||||
|
||||
program.preassign_label_to_next_insn(changed);
|
||||
emit_fk_parent_pk_change_counters(
|
||||
program,
|
||||
&incoming,
|
||||
resolver,
|
||||
old_pk_start,
|
||||
new_pk_start,
|
||||
pk_len,
|
||||
)?;
|
||||
program.preassign_label_to_next_insn(skip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emit_parent_pk_change_checks(
|
||||
program,
|
||||
resolver,
|
||||
&bt,
|
||||
tbl_cursor_id,
|
||||
conflict_rowid_reg,
|
||||
new_start,
|
||||
new_rowid_reg.unwrap_or(conflict_rowid_reg),
|
||||
rowid_set_clause_reg,
|
||||
set_pairs,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -792,6 +792,9 @@ impl ProgramBuilder {
|
||||
Insn::NotFound { target_pc, .. } => {
|
||||
resolve(target_pc, "NotFound");
|
||||
}
|
||||
Insn::FkIfZero { target_pc, .. } => {
|
||||
resolve(target_pc, "FkIfZero");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2169,6 +2169,17 @@ pub fn halt(
|
||||
let auto_commit = program.connection.auto_commit.load(Ordering::SeqCst);
|
||||
tracing::trace!("halt(auto_commit={})", auto_commit);
|
||||
if auto_commit {
|
||||
if program.connection.foreign_keys_enabled()
|
||||
&& program
|
||||
.connection
|
||||
.fk_deferred_violations
|
||||
.swap(0, Ordering::AcqRel)
|
||||
> 0
|
||||
{
|
||||
return Err(LimboError::Constraint(
|
||||
"foreign key constraint failed".to_string(),
|
||||
));
|
||||
}
|
||||
program
|
||||
.commit_txn(pager.clone(), state, mv_store, false)
|
||||
.map(Into::into)
|
||||
@@ -2263,12 +2274,13 @@ pub fn op_transaction_inner(
|
||||
if write && conn.db.open_flags.get().contains(OpenFlags::ReadOnly) {
|
||||
return Err(LimboError::ReadOnly);
|
||||
}
|
||||
|
||||
// 1. We try to upgrade current version
|
||||
let current_state = conn.get_tx_state();
|
||||
let (new_transaction_state, updated) = if conn.is_nested_stmt.load(Ordering::SeqCst)
|
||||
let (new_transaction_state, updated, should_clear_deferred_violations) = if conn
|
||||
.is_nested_stmt
|
||||
.load(Ordering::SeqCst)
|
||||
{
|
||||
(current_state, false)
|
||||
(current_state, false, false)
|
||||
} else {
|
||||
match (current_state, write) {
|
||||
// pending state means that we tried beginning a tx and the method returned IO.
|
||||
@@ -2283,30 +2295,36 @@ pub fn op_transaction_inner(
|
||||
schema_did_change: false,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
(TransactionState::Write { schema_did_change }, true) => {
|
||||
(TransactionState::Write { schema_did_change }, false)
|
||||
(TransactionState::Write { schema_did_change }, false, false)
|
||||
}
|
||||
(TransactionState::Write { schema_did_change }, false) => {
|
||||
(TransactionState::Write { schema_did_change }, false)
|
||||
(TransactionState::Write { schema_did_change }, false, false)
|
||||
}
|
||||
(TransactionState::Read, true) => (
|
||||
TransactionState::Write {
|
||||
schema_did_change: false,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
),
|
||||
(TransactionState::Read, false) => (TransactionState::Read, false),
|
||||
(TransactionState::Read, false) => (TransactionState::Read, false, false),
|
||||
(TransactionState::None, true) => (
|
||||
TransactionState::Write {
|
||||
schema_did_change: false,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
),
|
||||
(TransactionState::None, false) => (TransactionState::Read, true),
|
||||
(TransactionState::None, false) => (TransactionState::Read, true, false),
|
||||
}
|
||||
};
|
||||
if should_clear_deferred_violations {
|
||||
conn.fk_deferred_violations.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
// 2. Start transaction if needed
|
||||
if let Some(mv_store) = &mv_store {
|
||||
@@ -2383,8 +2401,8 @@ pub fn op_transaction_inner(
|
||||
return Err(LimboError::Busy);
|
||||
}
|
||||
if let IOResult::IO(io) = begin_w_tx_res? {
|
||||
// set the transaction state to pending so we don't have to
|
||||
// end the read transaction.
|
||||
// set the transaction state to pending so we don't have to
|
||||
program
|
||||
.connection
|
||||
.set_tx_state(TransactionState::PendingUpgrade);
|
||||
@@ -2448,15 +2466,20 @@ pub fn op_auto_commit(
|
||||
},
|
||||
insn
|
||||
);
|
||||
let conn = program.connection.clone();
|
||||
if matches!(state.commit_state, CommitState::Committing) {
|
||||
return program
|
||||
.commit_txn(pager.clone(), state, mv_store, *rollback)
|
||||
.map(Into::into);
|
||||
}
|
||||
|
||||
let conn = program.connection.clone();
|
||||
if *auto_commit != conn.auto_commit.load(Ordering::SeqCst) {
|
||||
if *rollback {
|
||||
program // reset deferred fk violations on ROLLBACK
|
||||
.connection
|
||||
.fk_deferred_violations
|
||||
.store(0, Ordering::Release);
|
||||
|
||||
// TODO(pere): add rollback I/O logic once we implement rollback journal
|
||||
if let Some(mv_store) = mv_store {
|
||||
if let Some(tx_id) = conn.get_mv_tx_id() {
|
||||
@@ -2468,6 +2491,15 @@ pub fn op_auto_commit(
|
||||
conn.set_tx_state(TransactionState::None);
|
||||
conn.auto_commit.store(true, Ordering::SeqCst);
|
||||
} else {
|
||||
if conn.foreign_keys_enabled() {
|
||||
let violations = conn.fk_deferred_violations.swap(0, Ordering::AcqRel);
|
||||
if violations > 0 {
|
||||
// Fail the commit
|
||||
return Err(LimboError::Constraint(
|
||||
"FOREIGN KEY constraint failed".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
conn.auto_commit.store(*auto_commit, Ordering::SeqCst);
|
||||
}
|
||||
} else {
|
||||
@@ -2494,6 +2526,15 @@ pub fn op_auto_commit(
|
||||
));
|
||||
}
|
||||
}
|
||||
if conn.foreign_keys_enabled() {
|
||||
let violations = conn.fk_deferred_violations.swap(0, Ordering::AcqRel);
|
||||
if violations > 0 {
|
||||
// Fail the commit
|
||||
return Err(LimboError::Constraint(
|
||||
"FOREIGN KEY constraint failed".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
@@ -8289,35 +8330,18 @@ pub fn op_fk_counter(
|
||||
load_insn!(
|
||||
FkCounter {
|
||||
increment_value,
|
||||
check_abort,
|
||||
is_scope,
|
||||
},
|
||||
insn
|
||||
);
|
||||
if *is_scope {
|
||||
// Adjust FK scope depth
|
||||
state.fk_scope_counter = state.fk_scope_counter.saturating_add(*increment_value);
|
||||
|
||||
// raise if there were deferred violations in this statement.
|
||||
if *check_abort {
|
||||
if state.fk_scope_counter < 0 {
|
||||
return Err(LimboError::Constraint(
|
||||
"FOREIGN KEY constraint failed".into(),
|
||||
));
|
||||
}
|
||||
if state.fk_scope_counter == 0 && state.fk_deferred_violations > 0 {
|
||||
// Clear violations for safety, a new statement will re-open scope.
|
||||
state.fk_deferred_violations = 0;
|
||||
return Err(LimboError::Constraint(
|
||||
"FOREIGN KEY constraint failed".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Adjust deferred violations counter
|
||||
state.fk_deferred_violations = state
|
||||
// Transaction-level counter: add/subtract for deferred FKs.
|
||||
program
|
||||
.connection
|
||||
.fk_deferred_violations
|
||||
.saturating_add(*increment_value);
|
||||
.fetch_add(*increment_value, Ordering::AcqRel);
|
||||
}
|
||||
|
||||
state.pc += 1;
|
||||
@@ -8331,29 +8355,37 @@ pub fn op_fk_if_zero(
|
||||
_pager: &Arc<Pager>,
|
||||
_mv_store: Option<&Arc<MvStore>>,
|
||||
) -> Result<InsnFunctionStepResult> {
|
||||
load_insn!(FkIfZero { target_pc, if_zero }, insn);
|
||||
load_insn!(
|
||||
FkIfZero {
|
||||
is_scope,
|
||||
target_pc,
|
||||
},
|
||||
insn
|
||||
);
|
||||
let fk_enabled = program.connection.foreign_keys_enabled();
|
||||
|
||||
// Jump if any:
|
||||
// Foreign keys are disabled globally
|
||||
// p1 is true AND deferred constraint counter is zero
|
||||
// p1 is false AND deferred constraint counter is non-zero
|
||||
let scope_zero = state.fk_scope_counter == 0;
|
||||
|
||||
let should_jump = if !fk_enabled {
|
||||
true
|
||||
} else if *if_zero {
|
||||
scope_zero
|
||||
if !fk_enabled {
|
||||
state.pc = target_pc.as_offset_int();
|
||||
return Ok(InsnFunctionStepResult::Step);
|
||||
}
|
||||
let v = if !*is_scope {
|
||||
program
|
||||
.connection
|
||||
.fk_deferred_violations
|
||||
.load(Ordering::Acquire)
|
||||
} else {
|
||||
!scope_zero
|
||||
state.fk_scope_counter
|
||||
};
|
||||
|
||||
if should_jump {
|
||||
state.pc = target_pc.as_offset_int();
|
||||
state.pc = if v == 0 {
|
||||
target_pc.as_offset_int()
|
||||
} else {
|
||||
state.pc += 1;
|
||||
}
|
||||
|
||||
state.pc + 1
|
||||
};
|
||||
Ok(InsnFunctionStepResult::Step)
|
||||
}
|
||||
|
||||
|
||||
@@ -1804,19 +1804,19 @@ pub fn insn_to_row(
|
||||
0,
|
||||
String::new(),
|
||||
),
|
||||
Insn::FkCounter{check_abort, increment_value, is_scope } => (
|
||||
Insn::FkCounter{increment_value, is_scope } => (
|
||||
"FkCounter",
|
||||
*check_abort as i32,
|
||||
*increment_value as i32,
|
||||
*is_scope as i32,
|
||||
0,
|
||||
Value::build_text(""),
|
||||
0,
|
||||
String::new(),
|
||||
),
|
||||
Insn::FkIfZero{target_pc, if_zero } => (
|
||||
Insn::FkIfZero{target_pc, is_scope } => (
|
||||
"FkIfZero",
|
||||
target_pc.as_debug_int(),
|
||||
*if_zero as i32,
|
||||
*is_scope as i32,
|
||||
0,
|
||||
Value::build_text(""),
|
||||
0,
|
||||
|
||||
@@ -1173,7 +1173,6 @@ pub enum Insn {
|
||||
// If P1 is non-zero, the database constraint counter is incremented (deferred foreign key constraints).
|
||||
// Otherwise, if P1 is zero, the statement counter is incremented (immediate foreign key constraints).
|
||||
FkCounter {
|
||||
check_abort: bool,
|
||||
increment_value: isize,
|
||||
is_scope: bool,
|
||||
},
|
||||
@@ -1181,7 +1180,7 @@ pub enum Insn {
|
||||
// If P1 is non-zero, then the jump is taken if the database constraint-counter is zero (the one that counts deferred constraint violations).
|
||||
// If P1 is zero, the jump is taken if the statement constraint-counter is zero (immediate foreign key constraint violations).
|
||||
FkIfZero {
|
||||
if_zero: bool,
|
||||
is_scope: bool,
|
||||
target_pc: BranchOffset,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -832,7 +832,6 @@ impl Program {
|
||||
|
||||
// Reset state for next use
|
||||
program_state.view_delta_state = ViewDeltaCommitState::NotStarted;
|
||||
|
||||
if self.connection.get_tx_state() == TransactionState::None {
|
||||
// No need to do any work here if not in tx. Current MVCC logic doesn't work with this assumption,
|
||||
// hence the mv_store.is_none() check.
|
||||
@@ -915,6 +914,9 @@ impl Program {
|
||||
self.connection
|
||||
.set_changes(self.n_change.load(Ordering::SeqCst));
|
||||
}
|
||||
if connection.foreign_keys_enabled() {
|
||||
connection.clear_deferred_foreign_key_violations();
|
||||
}
|
||||
Ok(IOResult::Done(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,16 +124,12 @@ do_execsql_test_in_memory_any_error fk-composite-unique-missing {
|
||||
INSERT INTO child VALUES (2,'A','X'); -- no ('A','X') in parent
|
||||
}
|
||||
|
||||
# SQLite doesnt let you name a foreign key constraint 'rowid' explicitly...
|
||||
# well it does.. but it throws a parse error only when you try to insert into the table -_-
|
||||
# We will throw a parse error when you create the table instead, because that is
|
||||
# obviously the only sane thing to do
|
||||
do_execsql_test_in_memory_any_error fk-rowid-alias-parent {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(id INTEGER PRIMARY KEY, a TEXT);
|
||||
CREATE TABLE c(cid INTEGER PRIMARY KEY, rid REFERENCES t(rowid)); -- we error here
|
||||
CREATE TABLE c(cid INTEGER PRIMARY KEY, rid REFERENCES t(rowid));
|
||||
INSERT INTO t VALUES (100,'x');
|
||||
INSERT INTO c VALUES (1, 100); - sqlite errors here
|
||||
INSERT INTO c VALUES (1, 100);
|
||||
}
|
||||
|
||||
do_execsql_test_in_memory_any_error fk-rowid-alias-parent-missing {
|
||||
@@ -399,3 +395,712 @@ do_execsql_test_in_memory_any_error fk-self-multirow-one-bad {
|
||||
FOREIGN KEY(rid) REFERENCES t(id));
|
||||
INSERT INTO t(id,rid) VALUES (1,1),(3,99); -- 99 has no parent -> error
|
||||
}
|
||||
|
||||
# doesnt fail because tx is un-committed
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-commit-doesnt-fail-early {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 99); -- shouldnt fail because we are mid-tx
|
||||
} {}
|
||||
|
||||
# it should fail here because we actuall COMMIT
|
||||
do_execsql_test_in_memory_any_error fk-deferred-commit-fails {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 99);
|
||||
COMMIT;
|
||||
}
|
||||
|
||||
|
||||
# If we fix it before COMMIT, COMMIT succeeds
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-fix-before-commit-succeeds {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 99); -- temporary violation
|
||||
INSERT INTO p VALUES(99); -- fix parent
|
||||
COMMIT;
|
||||
SELECT * FROM p ORDER BY 1;
|
||||
} {99}
|
||||
|
||||
# ROLLBACK clears deferred state; a new tx can still fail if violation persists
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-rollback-clears {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 123);
|
||||
ROLLBACK;
|
||||
|
||||
-- Now start over and *fix* it, COMMIT should pass.
|
||||
BEGIN;
|
||||
INSERT INTO p VALUES(123);
|
||||
INSERT INTO c VALUES(1, 123);
|
||||
COMMIT;
|
||||
SELECT * FROM c ORDER BY 1;
|
||||
} {1|123}
|
||||
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-insert-parent-fixes-before-commit {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 50); -- violation
|
||||
INSERT INTO p VALUES(50); -- resolve
|
||||
COMMIT;
|
||||
SELECT * FROM c ORDER BY 1;
|
||||
} {1|50}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-fixes-child-before-commit {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 50); -- violation
|
||||
INSERT INTO p VALUES(32);
|
||||
UPDATE c SET pid=32 WHERE id=1; -- resolve child
|
||||
COMMIT;
|
||||
SELECT * FROM c ORDER BY 1;
|
||||
} {1|32}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-delete-fixes-child-before-commit {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 50); -- violation
|
||||
INSERT INTO p VALUES(32);
|
||||
DELETE FROM c WHERE id=1; -- resolve by deleting child
|
||||
COMMIT;
|
||||
SELECT * FROM c ORDER BY 1;
|
||||
} {}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-fixes-parent-before-commit {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 50); -- violation
|
||||
INSERT INTO p VALUES(32);
|
||||
UPDATE p SET id=50 WHERE id=32; -- resolve via parent
|
||||
COMMIT;
|
||||
SELECT * FROM c ORDER BY 1;
|
||||
} {1|50}
|
||||
|
||||
# Self-referential: row referencing itself should succeed
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-self-ref-succeeds {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES t(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO t VALUES(1, 1); -- self-match
|
||||
COMMIT;
|
||||
SELECT * FROM t ORDER BY 1;
|
||||
} {1|1}
|
||||
|
||||
# Two-step self-ref: insert invalid, then create parent before COMMIT
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-self-ref-late-parent {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES t(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO t VALUES(2, 3); -- currently invalid
|
||||
INSERT INTO t VALUES(3, 3); -- now parent exists
|
||||
COMMIT;
|
||||
SELECT * FROM t ORDER BY 1;
|
||||
} {2|3
|
||||
3|3}
|
||||
|
||||
|
||||
# counter must not be neutralized by later good statements
|
||||
do_execsql_test_in_memory_any_error fk-deferred-neutralize.1 {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE parent_comp(a INT NOT NULL, b INT NOT NULL, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child_deferred(id INTEGER PRIMARY KEY, pid INT,
|
||||
FOREIGN KEY(pid) REFERENCES parent(id));
|
||||
|
||||
CREATE TABLE child_comp_deferred(id INTEGER PRIMARY KEY, ca INT, cb INT,
|
||||
FOREIGN KEY(ca,cb) REFERENCES parent_comp(a,b));
|
||||
INSERT INTO parent_comp VALUES (4,-1);
|
||||
BEGIN;
|
||||
INSERT INTO child_deferred VALUES (1, 999);
|
||||
INSERT INTO child_comp_deferred VALUES (2, 4, -1);
|
||||
COMMIT;
|
||||
}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-upsert-late-parent {
|
||||
PRAGMA foreign_keys=ON;
|
||||
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 50); -- deferred violation
|
||||
INSERT INTO p VALUES(32); -- parent exists, but pid still 50
|
||||
INSERT INTO c(id,pid) VALUES(1,32)
|
||||
ON CONFLICT(id) DO UPDATE SET pid=excluded.pid; -- resolve child via UPSERT
|
||||
COMMIT;
|
||||
-- Expect: row is (1,32) and no violations remain
|
||||
SELECT * FROM c ORDER BY id;
|
||||
} {1|32}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-upsert-late-child {
|
||||
PRAGMA foreign_keys=ON;
|
||||
|
||||
CREATE TABLE p(
|
||||
id INTEGER PRIMARY KEY,
|
||||
u INT UNIQUE
|
||||
);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 50); -- deferred violation (no parent 50)
|
||||
INSERT INTO p VALUES(32, 7); -- parent row with u=7
|
||||
-- Trigger DO UPDATE via conflict on p.u, then change the PK id to 50,
|
||||
-- which satisfies the child reference.
|
||||
INSERT INTO p(id,u) VALUES(999,7)
|
||||
ON CONFLICT(u) DO UPDATE SET id=50;
|
||||
COMMIT;
|
||||
-- Expect: parent is now (50,7), child (1,50), no violations remain
|
||||
SELECT p.id, c.id FROM p join c on c.pid = p.id;
|
||||
} {50|1}
|
||||
|
||||
do_execsql_test_in_memory_any_error fk-deferred-insert-commit-fails {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INTEGER REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 99); -- no parent -> deferred violation
|
||||
COMMIT; -- must fail
|
||||
}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-insert-parent-fix-before-commit {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INTEGER REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 99); -- violation
|
||||
INSERT INTO p VALUES(99); -- fix by inserting parent
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c ORDER BY id;
|
||||
} {1|99}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-insert-multi-children-one-parent-fix {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 50);
|
||||
INSERT INTO c VALUES(2, 50); -- two violations pointing to same parent
|
||||
INSERT INTO p VALUES(50); -- one parent fixes both
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c ORDER BY id;
|
||||
} {1|50 2|50}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-insert-then-delete-child-fix {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 77); -- violation
|
||||
DELETE FROM c WHERE id=1; -- resolve by removing the child
|
||||
COMMIT;
|
||||
SELECT count(*) FROM c;
|
||||
} {0}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-insert-self-ref-succeeds {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES t(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
BEGIN;
|
||||
INSERT INTO t VALUES(1, 1); -- self-reference, legal at COMMIT
|
||||
COMMIT;
|
||||
SELECT id, pid FROM t;
|
||||
} {1|1}
|
||||
|
||||
do_execsql_test_in_memory_any_error fk-deferred-update-child-breaks-commit-fails {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(10);
|
||||
INSERT INTO c VALUES(1, 10); -- valid
|
||||
BEGIN;
|
||||
UPDATE c SET pid=99 WHERE id=1; -- create violation
|
||||
COMMIT; -- must fail
|
||||
}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-child-fix-before-commit {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(10);
|
||||
INSERT INTO c VALUES(1, 10);
|
||||
BEGIN;
|
||||
UPDATE c SET pid=99 WHERE id=1; -- violation
|
||||
UPDATE c SET pid=10 WHERE id=1; -- fix child back
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c;
|
||||
} {1|10}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-child-fix-by-inserting-parent {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(10);
|
||||
INSERT INTO c VALUES(1, 10);
|
||||
BEGIN;
|
||||
UPDATE c SET pid=50 WHERE id=1; -- violation
|
||||
INSERT INTO p VALUES(50); -- fix by adding parent
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c;
|
||||
} {1|50}
|
||||
|
||||
do_execsql_test_in_memory_any_error fk-deferred-update-parent-breaks-commit-fails {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(32);
|
||||
INSERT INTO c VALUES(1, 32); -- valid
|
||||
BEGIN;
|
||||
UPDATE p SET id=50 WHERE id=32; -- break child reference
|
||||
COMMIT; -- must fail (no fix)
|
||||
}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-parent-fix-by-updating-child {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(32);
|
||||
INSERT INTO c VALUES(1, 32);
|
||||
BEGIN;
|
||||
UPDATE p SET id=50 WHERE id=32; -- break
|
||||
UPDATE c SET pid=50 WHERE id=1; -- fix child to new parent key
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c;
|
||||
} {1|50}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-parent-fix-by-reverting-parent {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(32);
|
||||
INSERT INTO c VALUES(1, 32);
|
||||
BEGIN;
|
||||
UPDATE p SET id=50 WHERE id=32; -- break
|
||||
UPDATE p SET id=32 WHERE id=50; -- revert (fix)
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c;
|
||||
} {1|32}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-self-ref-id-change-and-fix {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES t(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
INSERT INTO t VALUES(1,1);
|
||||
BEGIN;
|
||||
UPDATE t SET id=2 WHERE id=1; -- break self-ref
|
||||
UPDATE t SET pid=2 WHERE id=2; -- fix to new self
|
||||
COMMIT;
|
||||
SELECT id, pid FROM t;
|
||||
} {2|2}
|
||||
|
||||
do_execsql_test_in_memory_any_error fk-deferred-delete-parent-commit-fails {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(10);
|
||||
INSERT INTO c VALUES(1, 10); -- valid
|
||||
BEGIN;
|
||||
DELETE FROM p WHERE id=10; -- break reference
|
||||
COMMIT; -- must fail
|
||||
}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-delete-parent-then-delete-child-fix {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(10);
|
||||
INSERT INTO c VALUES(1, 10);
|
||||
BEGIN;
|
||||
DELETE FROM p WHERE id=10; -- break
|
||||
DELETE FROM c WHERE id=1; -- fix by removing child
|
||||
COMMIT;
|
||||
SELECT count(*) FROM p, c; -- both empty
|
||||
} {0}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-delete-parent-then-reinsert-parent-fix {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(10);
|
||||
INSERT INTO c VALUES(1, 10);
|
||||
BEGIN;
|
||||
DELETE FROM p WHERE id=10; -- break
|
||||
INSERT INTO p VALUES(10); -- fix by re-creating parent
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c;
|
||||
} {1|10}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-delete-self-ref-row-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES t(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
INSERT INTO t VALUES(1,1); -- valid
|
||||
BEGIN;
|
||||
DELETE FROM t WHERE id=1; -- removes both child+parent (same row)
|
||||
COMMIT; -- should succeed
|
||||
SELECT count(*) FROM t;
|
||||
} {0}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-delete-parent-then-update-child-to-null-fix {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
INSERT INTO p VALUES(5);
|
||||
INSERT INTO c VALUES(1,5);
|
||||
BEGIN;
|
||||
DELETE FROM p WHERE id=5; -- break
|
||||
UPDATE c SET pid=NULL WHERE id=1; -- fix (NULL never violates)
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c;
|
||||
} {1|}
|
||||
|
||||
# AUTOCOMMIT: deferred FK still fails at end-of-statement
|
||||
do_execsql_test_in_memory_any_error fk-deferred-autocommit-insert-missing-parent {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child(id INTEGER PRIMARY KEY, pid INT REFERENCES parent(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO child VALUES(1, 3); -- no BEGIN; should fail at statement end
|
||||
}
|
||||
|
||||
# AUTOCOMMIT: self-referential insert is OK (parent is same row)
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-autocommit-selfref-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(id INTEGER PRIMARY KEY, pid INT REFERENCES t(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO t VALUES(1,1);
|
||||
SELECT * FROM t;
|
||||
} {1|1}
|
||||
|
||||
# AUTOCOMMIT: deleting a parent that has a child → fails at statement end
|
||||
do_execsql_test_in_memory_any_error fk-deferred-autocommit-delete-parent-fails {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(1);
|
||||
INSERT INTO c VALUES(10,1);
|
||||
DELETE FROM p WHERE id=1; -- no BEGIN; should fail at statement end
|
||||
}
|
||||
|
||||
# TX: delete a referenced parent then reinsert before COMMIT -> OK
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-tx-delete-parent-then-reinsert-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(5);
|
||||
INSERT INTO c VALUES(1,5);
|
||||
BEGIN;
|
||||
DELETE FROM p WHERE id=5; -- violation (deferred)
|
||||
INSERT INTO p VALUES(5); -- fix in same tx
|
||||
COMMIT;
|
||||
SELECT count(*) FROM p WHERE id=5;
|
||||
} {1}
|
||||
|
||||
# TX: multiple violating children, later insert parent, COMMIT -> OK
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-tx-multi-children-fixed-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1,99);
|
||||
INSERT INTO c VALUES(2,99);
|
||||
INSERT INTO p VALUES(99);
|
||||
COMMIT;
|
||||
SELECT id,pid FROM c ORDER BY id;
|
||||
} {1|99 2|99}
|
||||
|
||||
# one of several children left unfixed -> COMMIT fails
|
||||
do_execsql_test_in_memory_any_error fk-deferred-tx-multi-children-one-left-fails {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1,42);
|
||||
INSERT INTO c VALUES(2,42);
|
||||
INSERT INTO p VALUES(42);
|
||||
UPDATE c SET pid=777 WHERE id=2; -- reintroduce a bad reference
|
||||
COMMIT; -- should fail
|
||||
}
|
||||
|
||||
# composite PK parent, fix via parent UPDATE before COMMIT -> OK
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-composite-parent-update-fix {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent(a INT NOT NULL, b INT NOT NULL, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child(id INT PRIMARY KEY, ca INT, cb INT,
|
||||
FOREIGN KEY(ca,cb) REFERENCES parent(a,b) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO parent VALUES(1,1);
|
||||
BEGIN;
|
||||
INSERT INTO child VALUES(10, 7, 7); -- violation
|
||||
UPDATE parent SET a=7, b=7 WHERE a=1 AND b=1; -- fix composite PK
|
||||
COMMIT;
|
||||
SELECT id, ca, cb FROM child;
|
||||
} {10|7|7}
|
||||
|
||||
# TX: NULL in child FK -> never a violation
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-null-fk-never-violates {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, NULL); -- always OK
|
||||
COMMIT;
|
||||
SELECT id, pid FROM c;
|
||||
} {1|}
|
||||
|
||||
# TX: child UPDATE to NULL resolves before COMMIT
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-child-null-resolves {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 500); -- violation
|
||||
UPDATE c SET pid=NULL WHERE id=1; -- resolves
|
||||
COMMIT;
|
||||
SELECT * FROM c;
|
||||
} {1|}
|
||||
|
||||
# TX: delete violating child resolves before COMMIT
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-delete-child-resolves {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 777); -- violation
|
||||
DELETE FROM c WHERE id=1; -- resolves
|
||||
COMMIT;
|
||||
SELECT count(*) FROM c;
|
||||
} {0}
|
||||
|
||||
# TX: update parent PK to match child before COMMIT -> OK
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-update-parent-pk-resolves {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO p VALUES(10);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 20); -- violation
|
||||
UPDATE p SET id=20 WHERE id=10; -- resolve via parent
|
||||
COMMIT;
|
||||
SELECT * FROM c;
|
||||
} {1|20}
|
||||
|
||||
# Two-table cycle; both inserted before COMMIT -> OK
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-cycle-two-tables-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE a(id INT PRIMARY KEY, b_id INT, FOREIGN KEY(b_id) REFERENCES b(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
CREATE TABLE b(id INT PRIMARY KEY, a_id INT, FOREIGN KEY(a_id) REFERENCES a(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO a VALUES(1, 1); -- refers to b(1) (not yet present)
|
||||
INSERT INTO b VALUES(1, 1); -- refers to a(1)
|
||||
COMMIT;
|
||||
SELECT count(b.id), count(a.id) FROM a, b;
|
||||
} {1|1}
|
||||
|
||||
# Delete a row that self-references (child==parent) within a tx -> OK
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-selfref-delete-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE t(id INTEGER PRIMARY KEY, pid INT REFERENCES t(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
INSERT INTO t VALUES(1,1);
|
||||
BEGIN;
|
||||
DELETE FROM t WHERE id=1;
|
||||
COMMIT;
|
||||
SELECT count(*) FROM t;
|
||||
} {0}
|
||||
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-parentcomp-donothing-noconflict-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent (id INTEGER PRIMARY KEY, a INT, b INT);
|
||||
CREATE TABLE child_deferred (
|
||||
id INTEGER PRIMARY KEY, pid INT, x INT,
|
||||
FOREIGN KEY(pid) REFERENCES parent(id) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
CREATE TABLE parent_comp (a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child_comp_deferred (
|
||||
id INTEGER PRIMARY KEY, ca INT, cb INT, z INT,
|
||||
FOREIGN KEY (ca,cb) REFERENCES parent_comp(a,b) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
-- No conflict on (a,b); should insert 1 row, no FK noise
|
||||
INSERT INTO parent_comp VALUES (-1,-1,9) ON CONFLICT DO NOTHING;
|
||||
SELECT a,b,c FROM parent_comp ORDER BY a,b;
|
||||
} {-1|-1|9}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-parentcomp-donothing-conflict-noop {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent_comp (a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child_comp_deferred (
|
||||
id INTEGER PRIMARY KEY, ca INT, cb INT, z INT,
|
||||
FOREIGN KEY (ca,cb) REFERENCES parent_comp(a,b) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
INSERT INTO parent_comp VALUES (10,20,1);
|
||||
-- Conflicts with existing (10,20); must do nothing (no triggers, no FK scans that mutate counters)
|
||||
INSERT INTO parent_comp VALUES (10,20,999) ON CONFLICT DO NOTHING;
|
||||
SELECT a,b,c FROM parent_comp;
|
||||
} {10|20|1}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-parentcomp-donothing-unrelated-immediate-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent (id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE child_immediate (
|
||||
id INTEGER PRIMARY KEY, pid INT,
|
||||
FOREIGN KEY(pid) REFERENCES parent(id) -- IMMEDIATE
|
||||
);
|
||||
CREATE TABLE parent_comp (a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child_comp_deferred (
|
||||
id INTEGER PRIMARY KEY, ca INT, cb INT, z INT,
|
||||
FOREIGN KEY(ca,cb) REFERENCES parent_comp(a,b) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
INSERT INTO parent_comp VALUES (-1,-1,9) ON CONFLICT DO NOTHING;
|
||||
SELECT a,b,c FROM parent_comp;
|
||||
} {-1|-1|9}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-parentcomp-deferred-fix-inside-tx-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent_comp (a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child_comp_deferred (
|
||||
id INTEGER PRIMARY KEY, ca INT, cb INT,
|
||||
FOREIGN KEY(ca,cb) REFERENCES parent_comp(a,b) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
BEGIN;
|
||||
INSERT INTO child_comp_deferred VALUES (1, -5, -6); -- violation
|
||||
INSERT INTO parent_comp VALUES (-5, -6, 9); -- fix via parent insert
|
||||
COMMIT;
|
||||
SELECT id,ca,cb FROM child_comp_deferred;
|
||||
} {1|-5|-6}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-parentcomp-autocommit-unrelated-children-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent_comp (a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child_comp_deferred (
|
||||
id INTEGER PRIMARY KEY, ca INT, cb INT,
|
||||
FOREIGN KEY(ca,cb) REFERENCES parent_comp(a,b) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
INSERT INTO parent_comp VALUES (1,1,0);
|
||||
INSERT INTO child_comp_deferred VALUES (10,1,1); -- valid
|
||||
INSERT INTO parent_comp VALUES (2,2,0) ON CONFLICT DO NOTHING; -- unrelated insert; must not raise
|
||||
SELECT a,b,c FROM parent_comp ORDER BY a,b;
|
||||
} {1|1|0
|
||||
2|2|0}
|
||||
|
||||
# ROLLBACK must clear any deferred state; next statement must not trip.
|
||||
do_execsql_test_on_specific_db {:memory:} fk-rollback-clears-then-donothing-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
CREATE TABLE parent_comp(a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b));
|
||||
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 456); -- create deferred violation
|
||||
ROLLBACK; -- must clear counters
|
||||
|
||||
INSERT INTO parent_comp VALUES(-2,-2,0) ON CONFLICT DO NOTHING;
|
||||
SELECT a,b,c FROM parent_comp;
|
||||
} {-2|-2|0}
|
||||
|
||||
# DO NOTHING conflict path must touch no FK maintenance at all.
|
||||
do_execsql_test_on_specific_db {:memory:} fk-parentcomp-donothing-conflict-stays-quiet {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE parent_comp(a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b));
|
||||
CREATE TABLE child_comp_deferred(
|
||||
id INTEGER PRIMARY KEY, ca INT, cb INT,
|
||||
FOREIGN KEY(ca,cb) REFERENCES parent_comp(a,b) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
INSERT INTO parent_comp VALUES(10,20,1);
|
||||
-- This conflicts with (10,20) and must be a no-op; if counters move here, it’s a bug.
|
||||
INSERT INTO parent_comp VALUES(10,20,999) ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Prove DB is sane afterwards (no stray FK error)
|
||||
INSERT INTO parent_comp VALUES(11,22,3) ON CONFLICT DO NOTHING;
|
||||
SELECT a,b FROM parent_comp ORDER BY a,b;
|
||||
} {10|20
|
||||
11|22}
|
||||
|
||||
# Two-statement fix inside an explicit transaction (separate statements).
|
||||
#Insert child (violation), then insert parent in a new statement; commit must pass.
|
||||
do_execsql_test_on_specific_db {:memory:} fk-deferred-two-stmt-fix-inside-tx-ok {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(id INTEGER PRIMARY KEY);
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, pid INT REFERENCES p(id) DEFERRABLE INITIALLY DEFERRED);
|
||||
BEGIN;
|
||||
INSERT INTO c VALUES(1, 777); -- violation recorded in tx
|
||||
INSERT INTO p VALUES(777); -- next statement fixes it
|
||||
COMMIT;
|
||||
SELECT * FROM c;
|
||||
} {1|777}
|
||||
|
||||
do_execsql_test_on_specific_db {:memory:} fk-delete-composite-bounds {
|
||||
PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE p(a INT NOT NULL, b INT NOT NULL, v INT, PRIMARY KEY(a,b));
|
||||
CREATE TABLE c(id INTEGER PRIMARY KEY, x INT, y INT, w INT,
|
||||
FOREIGN KEY(x,y) REFERENCES p(a,b));
|
||||
|
||||
INSERT INTO p VALUES (5,1,0),(5,2,0),(5,4,0);
|
||||
INSERT INTO c VALUES (1,5,4,0); -- child references (5,4)
|
||||
|
||||
-- This should be a no-op (no row (5,3)), and MUST NOT error.
|
||||
DELETE FROM p WHERE a=5 AND b=3;
|
||||
} {}
|
||||
|
||||
@@ -2,7 +2,7 @@ pub mod grammar_generator;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rand::seq::{IndexedRandom, SliceRandom};
|
||||
use rand::seq::{IndexedRandom, IteratorRandom, SliceRandom};
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use rusqlite::{params, types::Value};
|
||||
@@ -645,19 +645,334 @@ mod tests {
|
||||
"Different results! limbo: {:?}, sqlite: {:?}, seed: {}, query: {}, table def: {}",
|
||||
limbo_rows, sqlite_rows, seed, query, table_defs[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn fk_deferred_constraints_fuzz() {
|
||||
let _ = env_logger::try_init();
|
||||
let (mut rng, seed) = rng_from_time();
|
||||
println!("fk_deferred_constraints_fuzz seed: {seed}");
|
||||
|
||||
const OUTER_ITERS: usize = 10;
|
||||
const INNER_ITERS: usize = 50;
|
||||
|
||||
for outer in 0..OUTER_ITERS {
|
||||
println!("fk_deferred_constraints_fuzz {}/{}", outer + 1, OUTER_ITERS);
|
||||
|
||||
let limbo_db = TempDatabase::new_empty(true);
|
||||
let sqlite_db = TempDatabase::new_empty(true);
|
||||
let limbo = limbo_db.connect_limbo();
|
||||
let sqlite = rusqlite::Connection::open(sqlite_db.path.clone()).unwrap();
|
||||
|
||||
let mut stmts: Vec<String> = Vec::new();
|
||||
let mut log_and_exec = |sql: &str| {
|
||||
stmts.push(sql.to_string());
|
||||
sql.to_string()
|
||||
};
|
||||
// Enable FKs
|
||||
let s = log_and_exec("PRAGMA foreign_keys=ON");
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Mix of immediate and deferred FK constraints
|
||||
let s = log_and_exec("CREATE TABLE parent(id INTEGER PRIMARY KEY, a INT, b INT)");
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Child with DEFERRABLE INITIALLY DEFERRED FK
|
||||
let s = log_and_exec(
|
||||
"CREATE TABLE child_deferred(id INTEGER PRIMARY KEY, pid INT, x INT, \
|
||||
FOREIGN KEY(pid) REFERENCES parent(id) DEFERRABLE INITIALLY DEFERRED)",
|
||||
);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Child with immediate FK (default)
|
||||
let s = log_and_exec(
|
||||
"CREATE TABLE child_immediate(id INTEGER PRIMARY KEY, pid INT, y INT, \
|
||||
FOREIGN KEY(pid) REFERENCES parent(id))",
|
||||
);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Composite key parent for deferred testing
|
||||
let s = log_and_exec(
|
||||
"CREATE TABLE parent_comp(a INT NOT NULL, b INT NOT NULL, c INT, PRIMARY KEY(a,b))",
|
||||
);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Child with composite deferred FK
|
||||
let s = log_and_exec(
|
||||
"CREATE TABLE child_comp_deferred(id INTEGER PRIMARY KEY, ca INT, cb INT, z INT, \
|
||||
FOREIGN KEY(ca,cb) REFERENCES parent_comp(a,b) DEFERRABLE INITIALLY DEFERRED)",
|
||||
);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Seed initial data
|
||||
let mut parent_ids = std::collections::HashSet::new();
|
||||
for _ in 0..rng.random_range(10..=25) {
|
||||
let id = rng.random_range(1..=50) as i64;
|
||||
if parent_ids.insert(id) {
|
||||
let a = rng.random_range(-5..=25);
|
||||
let b = rng.random_range(-5..=25);
|
||||
let stmt = log_and_exec(&format!("INSERT INTO parent VALUES ({id}, {a}, {b})"));
|
||||
limbo_exec_rows(&limbo_db, &limbo, &stmt);
|
||||
sqlite.execute(&stmt, params![]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Seed composite parent
|
||||
let mut comp_pairs = std::collections::HashSet::new();
|
||||
for _ in 0..rng.random_range(3..=10) {
|
||||
let a = rng.random_range(-3..=6) as i64;
|
||||
let b = rng.random_range(-3..=6) as i64;
|
||||
if comp_pairs.insert((a, b)) {
|
||||
let c = rng.random_range(0..=20);
|
||||
let stmt =
|
||||
log_and_exec(&format!("INSERT INTO parent_comp VALUES ({a}, {b}, {c})"));
|
||||
limbo_exec_rows(&limbo_db, &limbo, &stmt);
|
||||
sqlite.execute(&stmt, params![]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-based mutations with mix of deferred and immediate operations
|
||||
for tx_num in 0..INNER_ITERS {
|
||||
// Decide if we're in a transaction
|
||||
let mut in_tx = false;
|
||||
let use_transaction = rng.random_bool(0.7);
|
||||
|
||||
if use_transaction && !in_tx {
|
||||
in_tx = true;
|
||||
let s = log_and_exec("BEGIN");
|
||||
let sres = sqlite.execute(&s, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &s);
|
||||
match (&sres, &lres) {
|
||||
(Ok(_), Ok(_)) | (Err(_), Err(_)) => {}
|
||||
_ => {
|
||||
eprintln!("BEGIN mismatch");
|
||||
eprintln!("sqlite result: {sres:?}");
|
||||
eprintln!("limbo result: {lres:?}");
|
||||
let file = std::fs::File::create("fk_deferred.sql").unwrap();
|
||||
for stmt in stmts.iter() {
|
||||
writeln!(&file, "{stmt};").unwrap();
|
||||
}
|
||||
eprintln!("Wrote `tests/fk_deferred.sql` for debugging");
|
||||
eprintln!("turso path: {}", limbo_db.path.display());
|
||||
eprintln!("sqlite path: {}", sqlite_db.path.display());
|
||||
panic!("BEGIN mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let op = rng.random_range(0..12);
|
||||
let stmt = match op {
|
||||
// Insert into child_deferred (can violate temporarily in transaction)
|
||||
0 => {
|
||||
let id = rng.random_range(1000..=2000);
|
||||
let pid = if rng.random_bool(0.6) {
|
||||
*parent_ids.iter().choose(&mut rng).unwrap_or(&1)
|
||||
} else {
|
||||
// Non-existent parent - OK if deferred and fixed before commit
|
||||
rng.random_range(200..=300) as i64
|
||||
};
|
||||
let x = rng.random_range(-10..=10);
|
||||
format!("INSERT INTO child_deferred VALUES ({id}, {pid}, {x})")
|
||||
}
|
||||
// Insert into child_immediate (must satisfy FK immediately)
|
||||
1 => {
|
||||
let id = rng.random_range(3000..=4000);
|
||||
let pid = if rng.random_bool(0.8) {
|
||||
*parent_ids.iter().choose(&mut rng).unwrap_or(&1)
|
||||
} else {
|
||||
rng.random_range(200..=300) as i64
|
||||
};
|
||||
let y = rng.random_range(-10..=10);
|
||||
format!("INSERT INTO child_immediate VALUES ({id}, {pid}, {y})")
|
||||
}
|
||||
// Insert parent (may fix deferred violations)
|
||||
2 => {
|
||||
let id = rng.random_range(1..=300);
|
||||
let a = rng.random_range(-5..=25);
|
||||
let b = rng.random_range(-5..=25);
|
||||
parent_ids.insert(id as i64);
|
||||
format!("INSERT INTO parent VALUES ({id}, {a}, {b})")
|
||||
}
|
||||
// Delete parent (may cause violations)
|
||||
3 => {
|
||||
let id = if rng.random_bool(0.5) {
|
||||
*parent_ids.iter().choose(&mut rng).unwrap_or(&1)
|
||||
} else {
|
||||
rng.random_range(1..=300) as i64
|
||||
};
|
||||
format!("DELETE FROM parent WHERE id={id}")
|
||||
}
|
||||
// Update parent PK
|
||||
4 => {
|
||||
let old = rng.random_range(1..=300);
|
||||
let new = rng.random_range(1..=350);
|
||||
format!("UPDATE parent SET id={new} WHERE id={old}")
|
||||
}
|
||||
// Update child_deferred FK
|
||||
5 => {
|
||||
let id = rng.random_range(1000..=2000);
|
||||
let pid = if rng.random_bool(0.5) {
|
||||
*parent_ids.iter().choose(&mut rng).unwrap_or(&1)
|
||||
} else {
|
||||
rng.random_range(200..=400) as i64
|
||||
};
|
||||
format!("UPDATE child_deferred SET pid={pid} WHERE id={id}")
|
||||
}
|
||||
// Insert into composite deferred child
|
||||
6 => {
|
||||
let id = rng.random_range(5000..=6000);
|
||||
let (ca, cb) = if rng.random_bool(0.6) {
|
||||
*comp_pairs.iter().choose(&mut rng).unwrap_or(&(1, 1))
|
||||
} else {
|
||||
// Non-existent composite parent
|
||||
(
|
||||
rng.random_range(-5..=8) as i64,
|
||||
rng.random_range(-5..=8) as i64,
|
||||
)
|
||||
};
|
||||
let z = rng.random_range(0..=10);
|
||||
format!(
|
||||
"INSERT INTO child_comp_deferred VALUES ({id}, {ca}, {cb}, {z}) ON CONFLICT DO NOTHING"
|
||||
)
|
||||
}
|
||||
// Insert composite parent
|
||||
7 => {
|
||||
let a = rng.random_range(-5..=8) as i64;
|
||||
let b = rng.random_range(-5..=8) as i64;
|
||||
let c = rng.random_range(0..=20);
|
||||
comp_pairs.insert((a, b));
|
||||
format!("INSERT INTO parent_comp VALUES ({a}, {b}, {c})")
|
||||
}
|
||||
// UPSERT with deferred child
|
||||
8 => {
|
||||
let id = rng.random_range(1000..=2000);
|
||||
let pid = if rng.random_bool(0.5) {
|
||||
*parent_ids.iter().choose(&mut rng).unwrap_or(&1)
|
||||
} else {
|
||||
rng.random_range(200..=400) as i64
|
||||
};
|
||||
let x = rng.random_range(-10..=10);
|
||||
format!(
|
||||
"INSERT INTO child_deferred VALUES ({id}, {pid}, {x})
|
||||
ON CONFLICT(id) DO UPDATE SET pid=excluded.pid, x=excluded.x"
|
||||
)
|
||||
}
|
||||
// Delete from child_deferred
|
||||
9 => {
|
||||
let id = rng.random_range(1000..=2000);
|
||||
format!("DELETE FROM child_deferred WHERE id={id}")
|
||||
}
|
||||
// Self-referential deferred insert (create temp violation then fix)
|
||||
10 if use_transaction => {
|
||||
let id = rng.random_range(400..=500);
|
||||
let pid = id + 1; // References non-existent yet
|
||||
format!("INSERT INTO child_deferred VALUES ({id}, {pid}, 0)")
|
||||
}
|
||||
_ => {
|
||||
// Default: simple parent insert
|
||||
let id = rng.random_range(1..=300);
|
||||
format!("INSERT INTO parent VALUES ({id}, 0, 0)")
|
||||
}
|
||||
};
|
||||
|
||||
let stmt = log_and_exec(&stmt);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
|
||||
if !use_transaction && !in_tx {
|
||||
match (sres, lres) {
|
||||
(Ok(_), Ok(_)) | (Err(_), Err(_)) => {}
|
||||
(s, l) => {
|
||||
eprintln!("Non-tx mismatch: sqlite={s:?}, limbo={l:?}");
|
||||
eprintln!("Statement: {stmt}");
|
||||
eprintln!("Seed: {seed}, outer: {outer}, tx: {tx_num}");
|
||||
let mut file = std::fs::File::create("fk_deferred.sql").unwrap();
|
||||
for stmt in stmts.iter() {
|
||||
writeln!(file, "{stmt};").expect("write to file");
|
||||
}
|
||||
eprintln!("turso path: {}", limbo_db.path.display());
|
||||
eprintln!("sqlite path: {}", sqlite_db.path.display());
|
||||
panic!("Non-transactional operation mismatch, file written to 'tests/fk_deferred.sql'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if use_transaction && in_tx {
|
||||
// Randomly COMMIT or ROLLBACK
|
||||
let commit = rng.random_bool(0.7);
|
||||
let s = log_and_exec("COMMIT");
|
||||
|
||||
let sres = sqlite.execute(&s, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &s);
|
||||
|
||||
match (sres, lres) {
|
||||
(Ok(_), Ok(_)) => {}
|
||||
(Err(_), Err(_)) => {
|
||||
// Both failed - OK, deferred constraint violation at commit
|
||||
if commit && in_tx {
|
||||
in_tx = false;
|
||||
let s = if commit {
|
||||
log_and_exec("ROLLBACK")
|
||||
} else {
|
||||
log_and_exec("SELECT 1") // noop if we already rolled back
|
||||
};
|
||||
|
||||
let sres = sqlite.execute(&s, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &s);
|
||||
match (sres, lres) {
|
||||
(Ok(_), Ok(_)) => {}
|
||||
(s, l) => {
|
||||
eprintln!("Post-failed-commit cleanup mismatch: sqlite={s:?}, limbo={l:?}");
|
||||
let mut file =
|
||||
std::fs::File::create("fk_deferred.sql").unwrap();
|
||||
for stmt in stmts.iter() {
|
||||
writeln!(file, "{stmt};").expect("write to file");
|
||||
}
|
||||
eprintln!("turso path: {}", limbo_db.path.display());
|
||||
eprintln!("sqlite path: {}", sqlite_db.path.display());
|
||||
panic!("Post-failed-commit cleanup mismatch, file written to 'tests/fk_deferred.sql'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(s, l) => {
|
||||
eprintln!("\n=== COMMIT/ROLLBACK mismatch ===");
|
||||
eprintln!("Operation: {s:?}");
|
||||
eprintln!("sqlite={s:?}, limbo={l:?}");
|
||||
eprintln!("Seed: {seed}, outer: {outer}, tx: {tx_num}");
|
||||
eprintln!("--- Replay statements ({}) ---", stmts.len());
|
||||
let mut file = std::fs::File::create("fk_deferred.sql").unwrap();
|
||||
for stmt in stmts.iter() {
|
||||
writeln!(file, "{stmt};").expect("write to file");
|
||||
}
|
||||
eprintln!("Turso path: {}", limbo_db.path.display());
|
||||
eprintln!("Sqlite path: {}", sqlite_db.path.display());
|
||||
panic!(
|
||||
"outcome mismatch, .sql file written to `tests/fk_deferred.sql`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn fk_single_pk_mutation_fuzz() {
|
||||
let _ = env_logger::try_init();
|
||||
let (mut rng, seed) = rng_from_time();
|
||||
println!("fk_single_pk_mutation_fuzz seed: {seed}");
|
||||
|
||||
const OUTER_ITERS: usize = 50;
|
||||
const INNER_ITERS: usize = 200;
|
||||
const OUTER_ITERS: usize = 20;
|
||||
const INNER_ITERS: usize = 100;
|
||||
|
||||
for outer in 0..OUTER_ITERS {
|
||||
println!("fk_single_pk_mutation_fuzz {}/{}", outer + 1, OUTER_ITERS);
|
||||
@@ -679,7 +994,6 @@ mod tests {
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// DDL
|
||||
let s = log_and_exec("CREATE TABLE p(id INTEGER PRIMARY KEY, a INT, b INT)");
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
@@ -704,8 +1018,14 @@ mod tests {
|
||||
let a = rng.random_range(-5..=25);
|
||||
let b = rng.random_range(-5..=25);
|
||||
let stmt = log_and_exec(&format!("INSERT INTO p VALUES ({id}, {a}, {b})"));
|
||||
limbo_exec_rows(&limbo_db, &limbo, &stmt);
|
||||
sqlite.execute(&stmt, params![]).unwrap();
|
||||
let l_res = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
let s_res = sqlite.execute(&stmt, params![]);
|
||||
match (l_res, s_res) {
|
||||
(Ok(_), Ok(_)) | (Err(_), Err(_)) => {}
|
||||
_ => {
|
||||
panic!("Seeding parent insert mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seed child
|
||||
@@ -817,8 +1137,7 @@ mod tests {
|
||||
let a = rng.random_range(-5..=25);
|
||||
let b = rng.random_range(-5..=25);
|
||||
format!(
|
||||
"INSERT INTO p VALUES({pick}, {a}, {b}) \
|
||||
ON CONFLICT(id) DO UPDATE SET a=excluded.a, b=excluded.b"
|
||||
"INSERT INTO p VALUES({pick}, {a}, {b}) ON CONFLICT(id) DO UPDATE SET a=excluded.a, b=excluded.b"
|
||||
)
|
||||
} else {
|
||||
let a = rng.random_range(-5..=25);
|
||||
@@ -935,14 +1254,368 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // TODO: un-ignore when UNIQUE constraints are fixed
|
||||
pub fn fk_edgecases_fuzzing() {
|
||||
let _ = env_logger::try_init();
|
||||
let (mut rng, seed) = rng_from_time();
|
||||
println!("fk_edgecases_minifuzz seed: {seed}");
|
||||
|
||||
const OUTER_ITERS: usize = 20;
|
||||
const INNER_ITERS: usize = 100;
|
||||
|
||||
fn assert_parity(
|
||||
seed: u64,
|
||||
stmts: &[String],
|
||||
sqlite_res: rusqlite::Result<usize>,
|
||||
limbo_res: Result<Vec<Vec<rusqlite::types::Value>>, turso_core::LimboError>,
|
||||
last_stmt: &str,
|
||||
tag: &str,
|
||||
) {
|
||||
match (sqlite_res.is_ok(), limbo_res.is_ok()) {
|
||||
(true, true) | (false, false) => (),
|
||||
_ => {
|
||||
eprintln!("\n=== {tag} mismatch ===");
|
||||
eprintln!("seed: {seed}");
|
||||
eprintln!("sqlite: {sqlite_res:?}, limbo: {limbo_res:?}");
|
||||
eprintln!("stmt: {last_stmt}");
|
||||
eprintln!("--- replay statements ({}) ---", stmts.len());
|
||||
for (i, s) in stmts.iter().enumerate() {
|
||||
eprintln!("{:04}: {};", i + 1, s);
|
||||
}
|
||||
panic!("{tag}: engines disagree");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parent rowid, child textified integers -> MustBeInt coercion path
|
||||
for outer in 0..OUTER_ITERS {
|
||||
let limbo_db = TempDatabase::new_empty(true);
|
||||
let sqlite_db = TempDatabase::new_empty(true);
|
||||
let limbo = limbo_db.connect_limbo();
|
||||
let sqlite = rusqlite::Connection::open(sqlite_db.path.clone()).unwrap();
|
||||
|
||||
let mut stmts: Vec<String> = Vec::new();
|
||||
let log = |s: &str, stmts: &mut Vec<String>| {
|
||||
stmts.push(s.to_string());
|
||||
s.to_string()
|
||||
};
|
||||
|
||||
for s in [
|
||||
"PRAGMA foreign_keys=ON",
|
||||
"CREATE TABLE p(id INTEGER PRIMARY KEY, a INT)",
|
||||
"CREATE TABLE c(id INTEGER PRIMARY KEY, x INT, FOREIGN KEY(x) REFERENCES p(id))",
|
||||
] {
|
||||
let s = log(s, &mut stmts);
|
||||
let _ = limbo_exec_rows_fallible(&limbo_db, &limbo, &s);
|
||||
let _ = sqlite.execute(&s, params![]);
|
||||
}
|
||||
|
||||
// Seed a few parents
|
||||
for _ in 0..rng.random_range(2..=5) {
|
||||
let id = rng.random_range(1..=15);
|
||||
let a = rng.random_range(-5..=5);
|
||||
let s = log(&format!("INSERT INTO p VALUES({id},{a})"), &mut stmts);
|
||||
let _ = limbo_exec_rows_fallible(&limbo_db, &limbo, &s);
|
||||
let _ = sqlite.execute(&s, params![]);
|
||||
}
|
||||
|
||||
// try random child inserts with weird text-ints
|
||||
for i in 0..INNER_ITERS {
|
||||
let id = 1000 + i as i64;
|
||||
let raw = if rng.random_bool(0.7) {
|
||||
1 + rng.random_range(0..=15)
|
||||
} else {
|
||||
rng.random_range(100..=200) as i64
|
||||
};
|
||||
|
||||
// Randomly decorate the integer as text with spacing/zeros/plus
|
||||
let pad_left_zeros = rng.random_range(0..=2);
|
||||
let spaces_left = rng.random_range(0..=2);
|
||||
let spaces_right = rng.random_range(0..=2);
|
||||
let plus = if rng.random_bool(0.3) { "+" } else { "" };
|
||||
let txt_num = format!(
|
||||
"{plus}{:0width$}",
|
||||
raw,
|
||||
width = (1 + pad_left_zeros) as usize
|
||||
);
|
||||
let txt = format!(
|
||||
"'{}{}{}'",
|
||||
" ".repeat(spaces_left),
|
||||
txt_num,
|
||||
" ".repeat(spaces_right)
|
||||
);
|
||||
|
||||
let stmt = log(&format!("INSERT INTO c VALUES({id}, {txt})"), &mut stmts);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
assert_parity(seed, &stmts, sres, lres, &stmt, "A: rowid-coercion");
|
||||
}
|
||||
println!("A {}/{} ok", outer + 1, OUTER_ITERS);
|
||||
}
|
||||
|
||||
// slf-referential rowid FK
|
||||
for outer in 0..OUTER_ITERS {
|
||||
let limbo_db = TempDatabase::new_empty(true);
|
||||
let sqlite_db = TempDatabase::new_empty(true);
|
||||
let limbo = limbo_db.connect_limbo();
|
||||
let sqlite = rusqlite::Connection::open(sqlite_db.path.clone()).unwrap();
|
||||
|
||||
let mut stmts: Vec<String> = Vec::new();
|
||||
let log = |s: &str, stmts: &mut Vec<String>| {
|
||||
stmts.push(s.to_string());
|
||||
s.to_string()
|
||||
};
|
||||
|
||||
for s in [
|
||||
"PRAGMA foreign_keys=ON",
|
||||
"CREATE TABLE t(id INTEGER PRIMARY KEY, rid REFERENCES t(id))",
|
||||
] {
|
||||
let s = log(s, &mut stmts);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
}
|
||||
|
||||
// Self-match should succeed for many ids
|
||||
for _ in 0..INNER_ITERS {
|
||||
let id = rng.random_range(1..=500);
|
||||
let stmt = log(
|
||||
&format!("INSERT INTO t(id,rid) VALUES({id},{id})"),
|
||||
&mut stmts,
|
||||
);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
assert_parity(seed, &stmts, sres, lres, &stmt, "B1: self-row ok");
|
||||
}
|
||||
|
||||
// Mismatch (rid != id) should fail (unless the referenced id already exists).
|
||||
for _ in 0..rng.random_range(1..=10) {
|
||||
let id = rng.random_range(1..=20);
|
||||
let s = log(
|
||||
&format!("INSERT INTO t(id,rid) VALUES({id},{id})"),
|
||||
&mut stmts,
|
||||
);
|
||||
let s_res = sqlite.execute(&s, params![]);
|
||||
let turso_rs = limbo_exec_rows_fallible(&limbo_db, &limbo, &s);
|
||||
match (s_res.is_ok(), turso_rs.is_ok()) {
|
||||
(true, true) | (false, false) => {}
|
||||
_ => panic!("Seeding self-ref failed differently"),
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..INNER_ITERS {
|
||||
let id = rng.random_range(600..=900);
|
||||
let ref_ = rng.random_range(1..=25);
|
||||
let stmt = log(
|
||||
&format!("INSERT INTO t(id,rid) VALUES({id},{ref_})"),
|
||||
&mut stmts,
|
||||
);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
assert_parity(seed, &stmts, sres, lres, &stmt, "B2: self-row mismatch");
|
||||
}
|
||||
println!("B {}/{} ok", outer + 1, OUTER_ITERS);
|
||||
}
|
||||
|
||||
// self-referential UNIQUE(u,v) parent (fast-path for composite)
|
||||
for outer in 0..OUTER_ITERS {
|
||||
let limbo_db = TempDatabase::new_empty(true);
|
||||
let sqlite_db = TempDatabase::new_empty(true);
|
||||
let limbo = limbo_db.connect_limbo();
|
||||
let sqlite = rusqlite::Connection::open(sqlite_db.path.clone()).unwrap();
|
||||
|
||||
let mut stmts: Vec<String> = Vec::new();
|
||||
let log = |s: &str, stmts: &mut Vec<String>| {
|
||||
stmts.push(s.to_string());
|
||||
s.to_string()
|
||||
};
|
||||
|
||||
let s = log("PRAGMA foreign_keys=ON", &mut stmts);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Variant the schema a bit: TEXT/TEXT, NUMERIC/TEXT, etc.
|
||||
let decls = [
|
||||
("TEXT", "TEXT"),
|
||||
("TEXT", "NUMERIC"),
|
||||
("NUMERIC", "TEXT"),
|
||||
("TEXT", "BLOB"),
|
||||
];
|
||||
let (tu, tv) = decls[rng.random_range(0..decls.len())];
|
||||
|
||||
let s = log(
|
||||
&format!(
|
||||
"CREATE TABLE sr(u {tu}, v {tv}, cu {tu}, cv {tv}, UNIQUE(u,v), \
|
||||
FOREIGN KEY(cu,cv) REFERENCES sr(u,v))"
|
||||
),
|
||||
&mut stmts,
|
||||
);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
|
||||
// Self-matching composite rows should succeed
|
||||
for _ in 0..INNER_ITERS {
|
||||
// Random small tokens, possibly padded
|
||||
let u = format!("U{}", rng.random_range(0..50));
|
||||
let v = format!("V{}", rng.random_range(0..50));
|
||||
let mut cu = u.clone();
|
||||
let mut cv = v.clone();
|
||||
|
||||
// occasionally wrap child refs as blobs/text to stress coercion on parent index
|
||||
if rng.random_bool(0.2) {
|
||||
// child cv as hex blob of ascii v
|
||||
let hex: String = v.bytes().map(|b| format!("{b:02X}")).collect();
|
||||
cv = format!("x'{hex}'");
|
||||
} else {
|
||||
cu = format!("'{cu}'");
|
||||
cv = format!("'{cv}'");
|
||||
}
|
||||
|
||||
let stmt = log(
|
||||
&format!("INSERT INTO sr(u,v,cu,cv) VALUES('{u}','{v}',{cu},{cv})"),
|
||||
&mut stmts,
|
||||
);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
assert_parity(seed, &stmts, sres, lres, &stmt, "C1: self-UNIQUE ok");
|
||||
}
|
||||
|
||||
// Non-self-match likely fails unless earlier rows happen to satisfy (u,v)
|
||||
for _ in 0..INNER_ITERS {
|
||||
let u = format!("U{}", rng.random_range(60..100));
|
||||
let v = format!("V{}", rng.random_range(60..100));
|
||||
let cu = format!("'U{}'", rng.random_range(0..40));
|
||||
let cv = format!("'{}{}'", "V", rng.random_range(0..40));
|
||||
let stmt = log(
|
||||
&format!("INSERT INTO sr(u,v,cu,cv) VALUES('{u}','{v}',{cu},{cv})"),
|
||||
&mut stmts,
|
||||
);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
assert_parity(seed, &stmts, sres, lres, &stmt, "C2: self-UNIQUE mismatch");
|
||||
}
|
||||
println!("C {}/{} ok", outer + 1, OUTER_ITERS);
|
||||
}
|
||||
|
||||
// parent TEXT UNIQUE(u,v), child types differ; rely on parent-index affinities
|
||||
for outer in 0..OUTER_ITERS {
|
||||
let limbo_db = TempDatabase::new_empty(true);
|
||||
let sqlite_db = TempDatabase::new_empty(true);
|
||||
let limbo = limbo_db.connect_limbo();
|
||||
let sqlite = rusqlite::Connection::open(sqlite_db.path.clone()).unwrap();
|
||||
|
||||
let mut stmts: Vec<String> = Vec::new();
|
||||
let log = |s: &str, stmts: &mut Vec<String>| {
|
||||
stmts.push(s.to_string());
|
||||
s.to_string()
|
||||
};
|
||||
|
||||
for s in [
|
||||
"PRAGMA foreign_keys=ON",
|
||||
"CREATE TABLE parent(u TEXT, v TEXT, UNIQUE(u,v))",
|
||||
"CREATE TABLE child(id INTEGER PRIMARY KEY, cu INT, cv BLOB, \
|
||||
FOREIGN KEY(cu,cv) REFERENCES parent(u,v))",
|
||||
] {
|
||||
let s = log(s, &mut stmts);
|
||||
limbo_exec_rows(&limbo_db, &limbo, &s);
|
||||
sqlite.execute(&s, params![]).unwrap();
|
||||
}
|
||||
|
||||
for _ in 0..rng.random_range(3..=8) {
|
||||
let u_raw = rng.random_range(0..=9);
|
||||
let v_raw = rng.random_range(0..=9);
|
||||
let u = if rng.random_bool(0.4) {
|
||||
format!("+{u_raw}")
|
||||
} else {
|
||||
format!("{u_raw}")
|
||||
};
|
||||
let v = if rng.random_bool(0.5) {
|
||||
format!("{v_raw:02}",)
|
||||
} else {
|
||||
format!("{v_raw}")
|
||||
};
|
||||
let s = log(
|
||||
&format!("INSERT INTO parent VALUES('{u}','{v}')"),
|
||||
&mut stmts,
|
||||
);
|
||||
let l_res = limbo_exec_rows_fallible(&limbo_db, &limbo, &s);
|
||||
let s_res = sqlite.execute(&s, params![]);
|
||||
match (s_res, l_res) {
|
||||
(Ok(_), Ok(_)) | (Err(_), Err(_)) => {}
|
||||
(x, y) => {
|
||||
panic!("Parent seeding mismatch: sqlite {x:?}, limbo {y:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..INNER_ITERS {
|
||||
let id = i as i64 + 1;
|
||||
let u_txt = if rng.random_bool(0.7) {
|
||||
format!("+{}", rng.random_range(0..=9))
|
||||
} else {
|
||||
format!("{}", rng.random_range(0..=9))
|
||||
};
|
||||
let v_txt = if rng.random_bool(0.5) {
|
||||
format!("{:02}", rng.random_range(0..=9))
|
||||
} else {
|
||||
format!("{}", rng.random_range(0..=9))
|
||||
};
|
||||
|
||||
// produce child literals that *look different* but should match under TEXT affinity
|
||||
// cu uses integer-ish form of u; cv uses blob of ASCII v or quoted v randomly.
|
||||
let cu = if let Ok(u_int) = u_txt.trim().trim_start_matches('+').parse::<i64>() {
|
||||
if rng.random_bool(0.5) {
|
||||
format!("{u_int}",)
|
||||
} else {
|
||||
format!("'{u_txt}'")
|
||||
}
|
||||
} else {
|
||||
format!("'{u_txt}'")
|
||||
};
|
||||
let cv = if rng.random_bool(0.6) {
|
||||
let hex: String = v_txt
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect();
|
||||
format!("x'{hex}'")
|
||||
} else {
|
||||
format!("'{v_txt}'")
|
||||
};
|
||||
|
||||
let stmt = log(
|
||||
&format!("INSERT INTO child VALUES({id}, {cu}, {cv})"),
|
||||
&mut stmts,
|
||||
);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
assert_parity(seed, &stmts, sres, lres, &stmt, "D1: parent-index affinity");
|
||||
}
|
||||
|
||||
for i in 0..(INNER_ITERS / 3) {
|
||||
let id = 10_000 + i as i64;
|
||||
let cu = rng.random_range(0..=9);
|
||||
let miss = rng.random_range(10..=19);
|
||||
let stmt = log(
|
||||
&format!("INSERT INTO child VALUES({id}, {cu}, x'{miss:02X}')"),
|
||||
&mut stmts,
|
||||
);
|
||||
let sres = sqlite.execute(&stmt, params![]);
|
||||
let lres = limbo_exec_rows_fallible(&limbo_db, &limbo, &stmt);
|
||||
assert_parity(seed, &stmts, sres, lres, &stmt, "D2: parent-index negative");
|
||||
}
|
||||
println!("D {}/{} ok", outer + 1, OUTER_ITERS);
|
||||
}
|
||||
|
||||
println!("fk_edgecases_minifuzz complete (seed {seed})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn fk_composite_pk_mutation_fuzz() {
|
||||
let _ = env_logger::try_init();
|
||||
let (mut rng, seed) = rng_from_time();
|
||||
println!("fk_composite_pk_mutation_fuzz seed: {seed}");
|
||||
|
||||
const OUTER_ITERS: usize = 30;
|
||||
const INNER_ITERS: usize = 200;
|
||||
const OUTER_ITERS: usize = 10;
|
||||
const INNER_ITERS: usize = 100;
|
||||
|
||||
for outer in 0..OUTER_ITERS {
|
||||
println!(
|
||||
|
||||
Reference in New Issue
Block a user