Add helper to pragma to parse enabled opts and fix schema parsing for foreign key constraints

This commit is contained in:
PThorpe92
2025-09-30 20:12:39 -04:00
parent 23248d9001
commit fa23cedbbe
6 changed files with 137 additions and 279 deletions

View File

@@ -1100,6 +1100,7 @@ pub struct Connection {
busy_timeout: RwLock<std::time::Duration>,
/// Whether this is an internal connection used for MVCC bootstrap
is_mvcc_bootstrap_connection: AtomicBool,
/// Whether pragma foreign_keys=ON for this connection
fk_pragma: AtomicBool,
}

View File

@@ -846,28 +846,18 @@ impl Schema {
Ok(())
}
pub fn incoming_fks_to(&self, table_name: &str) -> Vec<ResolvedFkRef> {
/// 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> {
let target = normalize_ident(table_name);
let mut out = vec![];
let mut out = Vec::with_capacity(4); // arbitrary estimate
let parent_tbl = self
.get_btree_table(&target)
.expect("incoming_fks_to: parent table must exist");
.expect("parent table must exist");
// Precompute helper to find parent unique index, if it's not the rowid
let find_parent_unique = |cols: &Vec<String>| -> Option<Arc<Index>> {
// If matches PK exactly, we don't need a secondary index probe
let matches_pk = !parent_tbl.primary_key_columns.is_empty()
&& parent_tbl.primary_key_columns.len() == cols.len()
&& parent_tbl
.primary_key_columns
.iter()
.zip(cols.iter())
.all(|((n, _ord), c)| n.eq_ignore_ascii_case(c));
if matches_pk {
return None;
}
self.get_indices(&parent_tbl.name)
.find(|idx| {
idx.unique
@@ -887,16 +877,12 @@ impl Schema {
};
for fk in &child.foreign_keys {
if normalize_ident(&fk.parent_table) != target {
if fk.parent_table != target {
continue;
}
// Resolve + normalize columns
let child_cols: Vec<String> = fk
.child_columns
.iter()
.map(|c| normalize_ident(c))
.collect();
let child_cols: Vec<String> = fk.child_columns.clone();
// 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.
@@ -904,25 +890,21 @@ impl Schema {
parent_tbl
.primary_key_columns
.iter()
.map(|(n, _)| normalize_ident(n))
.map(|(col, _)| col)
.cloned()
.collect()
} else {
fk.parent_columns
.iter()
.map(|c| normalize_ident(c))
.collect()
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!(
"incoming_fks_to: child col {}.{} missing",
child.name, cname
)
})
child
.get_column(cname)
.map(|(i, _)| i)
.unwrap_or_else(|| panic!("child col {}.{} missing", child.name, cname))
})
.collect();
@@ -941,10 +923,7 @@ impl Schema {
}
})
.unwrap_or_else(|| {
panic!(
"incoming_fks_to: parent col {}.{cname} missing",
parent_tbl.name
)
panic!("parent col {}.{cname} missing", parent_tbl.name)
})
})
.collect();
@@ -983,7 +962,8 @@ impl Schema {
out
}
pub fn outgoing_fks_of(&self, child_table: &str) -> Vec<ResolvedFkRef> {
/// 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![];
@@ -992,16 +972,6 @@ impl Schema {
// 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>> {
let matches_pk = !parent_tbl.primary_key_columns.is_empty()
&& parent_tbl.primary_key_columns.len() == cols.len()
&& parent_tbl
.primary_key_columns
.iter()
.zip(cols.iter())
.all(|((n, _), c)| n.eq_ignore_ascii_case(c));
if matches_pk {
return None;
}
self.get_indices(&parent_tbl.name)
.find(|idx| {
idx.unique
@@ -1015,14 +985,14 @@ impl Schema {
.cloned()
};
let mut out = Vec::new();
let mut out = Vec::with_capacity(child.foreign_keys.len());
for fk in &child.foreign_keys {
let parent_name = normalize_ident(&fk.parent_table);
let Some(parent_tbl) = self.get_btree_table(&parent_name) else {
let parent_name = &fk.parent_table;
let Some(parent_tbl) = self.get_btree_table(parent_name) else {
continue;
};
// Normalize columns (same rules you used in validation)
// Normalize columns
let child_cols: Vec<String> = fk
.child_columns
.iter()
@@ -1045,7 +1015,6 @@ impl Schema {
.collect()
};
// Positions
let child_pos: Vec<usize> = child_cols
.iter()
.map(|c| child.get_column(c).expect("child col missing").0)
@@ -1061,7 +1030,6 @@ impl Schema {
})
.collect();
// Parent uses rowid?
let parent_uses_rowid = parent_cols.len() == 1 && {
let c = parent_cols[0].as_str();
c.eq_ignore_ascii_case("rowid")
@@ -1094,7 +1062,8 @@ impl Schema {
out
}
pub fn any_incoming_fk_to(&self, table_name: &str) -> bool {
/// Returns if any table declares a FOREIGN KEY whose parent is `table_name`.
pub fn any_resolved_fks_referencing(&self, table_name: &str) -> bool {
self.tables.values().any(|t| {
let Some(bt) = t.btree() else {
return false;
@@ -1105,36 +1074,12 @@ impl Schema {
})
}
/// Returns if this table declares any outgoing FKs (is a child of some parent)
/// Returns true if `table_name` declares any FOREIGN KEYs
pub fn has_child_fks(&self, table_name: &str) -> bool {
self.get_table(table_name)
.and_then(|t| t.btree())
.is_some_and(|t| !t.foreign_keys.is_empty())
}
/// Return the *declared* (unresolved) FKs for a table. Callers that need
/// positions/rowid/unique info should use `incoming_fks_to` instead.
pub fn get_fks_for_table(&self, table_name: &str) -> Vec<Arc<ForeignKey>> {
self.get_table(table_name)
.and_then(|t| t.btree())
.map(|t| t.foreign_keys.clone())
.unwrap_or_default()
}
/// Return pairs of (child_table_name, FK) for FKs that reference `parent_table`
pub fn get_referencing_fks(&self, parent_table: &str) -> Vec<(String, Arc<ForeignKey>)> {
let mut refs = Vec::new();
for table in self.tables.values() {
if let Table::BTree(btree) = table.deref() {
for fk in &btree.foreign_keys {
if fk.parent_table == parent_table {
refs.push((btree.name.as_str().to_string(), fk.clone()));
}
}
}
}
refs
}
}
impl Clone for Schema {
@@ -1524,7 +1469,6 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
.iter()
.map(|ic| normalize_ident(ic.col_name.as_str()))
.collect();
// derive parent columns: explicit or default to parent PK
let parent_table = normalize_ident(clause.tbl_name.as_str());
let parent_columns: Vec<String> = clause
@@ -1533,8 +1477,8 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
.map(|ic| normalize_ident(ic.col_name.as_str()))
.collect();
// arity check
if child_columns.len() != parent_columns.len() {
// Only check arity if parent columns were explicitly listed
if !parent_columns.is_empty() && child_columns.len() != parent_columns.len() {
crate::bail_parse_error!(
"foreign key on \"{}\" has {} child column(s) but {} parent column(s)",
tbl_name,
@@ -1568,17 +1512,6 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
}
})
.unwrap_or(RefAct::NoAction),
on_insert: clause
.args
.iter()
.find_map(|a| {
if let ast::RefArg::OnInsert(x) = a {
Some(*x)
} else {
None
}
})
.unwrap_or(RefAct::NoAction),
on_update: clause
.args
.iter()
@@ -1601,7 +1534,7 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
constraints,
} in columns
{
let name = col_name.as_str().to_string();
let name = normalize_ident(col_name.as_str());
// 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.
@@ -1684,11 +1617,11 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
defer_clause,
} => {
let fk = ForeignKey {
parent_table: clause.tbl_name.to_string(),
parent_table: normalize_ident(clause.tbl_name.as_str()),
parent_columns: clause
.columns
.iter()
.map(|c| c.col_name.as_str().to_string())
.map(|c| normalize_ident(c.col_name.as_str()))
.collect(),
on_delete: clause
.args
@@ -1701,17 +1634,6 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
}
})
.unwrap_or(RefAct::NoAction),
on_insert: clause
.args
.iter()
.find_map(|arg| {
if let ast::RefArg::OnInsert(act) = arg {
Some(*act)
} else {
None
}
})
.unwrap_or(RefAct::NoAction),
on_update: clause
.args
.iter()
@@ -1724,7 +1646,16 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
})
.unwrap_or(RefAct::NoAction),
child_columns: vec![name.clone()],
deferred: defer_clause.is_some(),
deferred: match defer_clause {
Some(d) => {
d.deferrable
&& matches!(
d.init_deferred,
Some(InitDeferredPred::InitiallyDeferred)
)
}
None => false,
},
};
foreign_keys.push(Arc::new(fk));
}
@@ -1742,7 +1673,7 @@ pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> R
}
cols.push(Column {
name: Some(normalize_ident(&name)),
name: Some(name),
ty,
ty_str,
primary_key,
@@ -1875,7 +1806,6 @@ pub struct ForeignKey {
pub parent_columns: Vec<String>,
pub on_delete: RefAct,
pub on_update: RefAct,
pub on_insert: RefAct,
/// DEFERRABLE INITIALLY DEFERRED
pub deferred: bool,
}

View File

@@ -440,7 +440,7 @@ fn emit_program_for_delete(
.unwrap()
.table
.get_name();
resolver.schema.any_incoming_fk_to(table_name)
resolver.schema.any_resolved_fks_referencing(table_name)
};
// Open FK scope for the whole statement
if has_parent_fks {
@@ -542,7 +542,10 @@ fn emit_delete_insns(
if connection.foreign_keys_enabled()
&& unsafe { &*table_reference }.btree().is_some()
&& t_ctx.resolver.schema.any_incoming_fk_to(table_name)
&& t_ctx
.resolver
.schema
.any_resolved_fks_referencing(table_name)
{
emit_fk_parent_existence_checks(
program,
@@ -1047,7 +1050,7 @@ pub fn emit_fk_parent_existence_checks(
.get_btree_table(parent_table_name)
.ok_or_else(|| crate::LimboError::InternalError("parent not btree".into()))?;
for fk_ref in resolver.schema.incoming_fks_to(parent_table_name) {
for fk_ref in resolver.schema.resolved_fks_referencing(parent_table_name) {
// Resolve parent key columns
let parent_cols: Vec<String> = if fk_ref.fk.parent_columns.is_empty() {
parent_bt
@@ -1295,8 +1298,8 @@ fn emit_program_for_update(
.unwrap()
.table
.get_name();
let has_child_fks = fk_enabled && !resolver.schema.get_fks_for_table(table_name).is_empty();
let has_parent_fks = fk_enabled && resolver.schema.any_incoming_fk_to(table_name);
let has_child_fks = fk_enabled && resolver.schema.has_child_fks(table_name);
let has_parent_fks = fk_enabled && resolver.schema.any_resolved_fks_referencing(table_name);
// statement-level FK scope open
if has_child_fks || has_parent_fks {
program.emit_insn(Insn::FkCounter {
@@ -1695,12 +1698,16 @@ fn emit_update_insns(
// We only need to do work if the referenced key (the parent key) might change.
// we detect that by comparing OLD vs NEW primary key representation
// then run parent FK checks only when it actually changes.
if t_ctx.resolver.schema.any_incoming_fk_to(table_name) {
if t_ctx
.resolver
.schema
.any_resolved_fks_referencing(table_name)
{
let updated_parent_positions: HashSet<usize> =
plan.set_clauses.iter().map(|(i, _)| *i).collect();
// If no incoming FKs parent key can be affected by these updates, skip the whole parent-FK block.
let incoming = t_ctx.resolver.schema.incoming_fks_to(table_name);
let incoming = t_ctx.resolver.schema.resolved_fks_referencing(table_name);
let parent_tbl = &table_btree;
let maybe_affects_parent_key = incoming
.iter()
@@ -2338,7 +2345,7 @@ pub fn emit_fk_child_existence_checks(
if_zero: true,
});
for fk_ref in resolver.schema.outgoing_fks_of(table_name) {
for fk_ref in resolver.schema.resolved_fks_for_child(table_name) {
// Skip when the child key is untouched (including rowid-alias special case)
if !fk_ref.child_key_changed(updated_cols, table) {
continue;

View File

@@ -83,13 +83,6 @@ pub fn translate_insert(
);
}
let table_name = &tbl_name.name;
let fk_enabled = connection.foreign_keys_enabled();
let has_child_fks = fk_enabled
&& !resolver
.schema
.get_fks_for_table(table_name.as_str())
.is_empty();
let has_parent_fks = fk_enabled && resolver.schema.any_incoming_fk_to(table_name.as_str());
// Check if this is a system table that should be protected from direct writes
if crate::schema::is_system_table(table_name.as_str()) {
@@ -100,6 +93,7 @@ pub fn translate_insert(
Some(table) => table,
None => crate::bail_parse_error!("no such table: {}", table_name),
};
let fk_enabled = connection.foreign_keys_enabled();
// Check if this is a materialized view
if resolver.schema.is_materialized_view(table_name.as_str()) {
@@ -140,6 +134,7 @@ 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 root_page = btree_table.root_page;
@@ -243,7 +238,7 @@ pub fn translate_insert(
connection,
)?;
if has_child_fks || has_parent_fks {
if has_child_fks {
program.emit_insn(Insn::FkCounter {
increment_value: 1,
check_abort: false,
@@ -1044,7 +1039,7 @@ pub fn translate_insert(
}
}
}
if has_child_fks || has_parent_fks {
if has_child_fks {
emit_fk_checks_for_insert(&mut program, resolver, &insertion, table_name.as_str())?;
}
@@ -1909,87 +1904,56 @@ fn emit_fk_checks_for_insert(
});
// Iterate child FKs declared on this table
for fk in resolver.schema.get_fks_for_table(table_name) {
let fk_ok = program.allocate_label();
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();
// If any child column is NULL, skip this FK
for child_col in &fk.child_columns {
let mapping = insertion
.get_col_mapping_by_name(child_col)
.ok_or_else(|| {
crate::LimboError::InternalError(format!("FK column {child_col} not found"))
})?;
let src = if mapping.column.is_rowid_alias {
insertion.key_register()
} else {
mapping.register
};
// if any child FK value is NULL, this row doesn't reference the parent.
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;
program.emit_insn(Insn::IsNull {
reg: src,
target_pc: fk_ok,
});
}
// Parent lookup: rowid path or unique-index path
let parent_tbl = resolver.schema.get_table(&fk.parent_table).ok_or_else(|| {
crate::LimboError::InternalError(format!("Parent table {} not found", fk.parent_table))
})?;
let uses_rowid = {
// If single parent column equals rowid or aliases rowid
fk.parent_columns.len() == 1 && {
let parent_col = fk.parent_columns[0].as_str();
parent_col.eq_ignore_ascii_case("rowid")
|| parent_tbl.columns().iter().any(|c| {
c.is_rowid_alias
&& c.name
.as_ref()
.is_some_and(|n| n.eq_ignore_ascii_case(parent_col))
})
}
};
if uses_rowid {
// Simple rowid probe on parent table
let parent_bt = parent_tbl.btree().ok_or_else(|| {
crate::LimboError::InternalError("Parent table is not a BTree".into())
})?;
let pcur = program.alloc_cursor_id(CursorType::BTreeTable(parent_bt.clone()));
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_bt.root_page,
root_page: parent_tbl.root_page,
db: 0,
});
// Child value register
let cm = insertion
.get_col_mapping_by_name(&fk.child_columns[0])
.ok_or_else(|| {
crate::LimboError::InternalError("FK child column not found".into())
})?;
let val_reg = if cm.column.is_rowid_alias {
insertion.key_register()
} else {
cm.register
};
let only = 0; // n == 1 guaranteed if parent_uses_rowid
let src = insertion
.col_mappings
.get(fk_ref.child_pos[only])
.unwrap()
.register;
let violation = program.allocate_label();
// NotExists: jump to violation if missing in parent
program.emit_insn(Insn::NotExists {
cursor: pcur,
rowid_reg: val_reg,
rowid_reg: src,
target_pc: violation,
});
// OK
program.emit_insn(Insn::Close { cursor_id: pcur });
program.emit_insn(Insn::Goto { target_pc: fk_ok });
// Violation
program.preassign_label_to_next_insn(violation);
program.emit_insn(Insn::Close { cursor_id: pcur });
// Deferred vs immediate
if fk.deferred {
if fk_ref.fk.deferred {
program.emit_insn(Insn::FkCounter {
increment_value: 1,
check_abort: false,
@@ -2001,67 +1965,48 @@ fn emit_fk_checks_for_insert(
description: "FOREIGN KEY constraint failed".to_string(),
});
}
} else {
// Multi-column (or non-rowid) parent, we have to match a UNIQUE index with
// the exact column set and order
let parent_idx = resolver
.schema
.get_indices(&fk.parent_table)
.find(|idx| {
idx.unique
&& idx.columns.len() == fk.parent_columns.len()
&& idx
.columns
.iter()
.zip(fk.parent_columns.iter())
.all(|(ic, pc)| ic.name.eq_ignore_ascii_case(pc))
})
.ok_or_else(|| {
crate::LimboError::InternalError(format!(
"No UNIQUE index on parent {}({:?}) for FK",
fk.parent_table, fk.parent_columns
))
})?;
let icur = program.alloc_cursor_id(CursorType::BTreeIndex(parent_idx.clone()));
} else if let Some(ix) = &fk_ref.parent_unique_index {
// 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: parent_idx.root_page,
root_page: ix.root_page,
db: 0,
});
// Build packed search key registers from the *child* values
let n = fk.child_columns.len();
let start = program.alloc_registers(n);
for (i, child_col) in fk.child_columns.iter().enumerate() {
let cm = insertion
.get_col_mapping_by_name(child_col)
.ok_or_else(|| {
crate::LimboError::InternalError(format!("Column {child_col} not found"))
})?;
let src = if cm.column.is_rowid_alias {
insertion.key_register()
} else {
cm.register
};
// 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;
program.emit_insn(Insn::Copy {
src_reg: src,
dst_reg: 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,
});
let found = program.allocate_label();
program.emit_insn(Insn::Found {
cursor_id: icur,
target_pc: found,
record_reg: start,
num_regs: n,
record_reg: probe_start,
num_regs: num_child_cols,
});
// Violation path
// Not found: violation
program.emit_insn(Insn::Close { cursor_id: icur });
if fk.deferred {
if fk_ref.fk.deferred {
program.emit_insn(Insn::FkCounter {
increment_value: 1,
check_abort: false,
@@ -2074,16 +2019,14 @@ fn emit_fk_checks_for_insert(
});
}
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 });
}
// Done with this FK
program.preassign_label_to_next_insn(fk_ok);
}
program.resolve_label(after_all, program.offset());
program.preassign_label_to_next_insn(after_all);
Ok(())
}

View File

@@ -95,6 +95,20 @@ fn update_pragma(
connection: Arc<crate::Connection>,
mut program: ProgramBuilder,
) -> crate::Result<(ProgramBuilder, TransactionMode)> {
let parse_pragma_enabled = |expr: &ast::Expr| -> bool {
if let Expr::Literal(Literal::Numeric(n)) = expr {
return !matches!(n.as_str(), "0");
};
let name_bytes = match expr {
Expr::Literal(Literal::Keyword(name)) => name.as_bytes(),
Expr::Name(name) | Expr::Id(name) => name.as_str().as_bytes(),
_ => "".as_bytes(),
};
match_ignore_ascii_case!(match name_bytes {
b"ON" | b"TRUE" | b"YES" | b"1" => true,
_ => false,
})
};
match pragma {
PragmaName::ApplicationId => {
let data = parse_signed_number(&value)?;
@@ -343,38 +357,15 @@ fn update_pragma(
}
PragmaName::Synchronous => {
use crate::SyncMode;
let mode = match value {
Expr::Name(name) => {
let name_bytes = name.as_str().as_bytes();
match_ignore_ascii_case!(match name_bytes {
b"OFF" | b"FALSE" | b"NO" | b"0" => SyncMode::Off,
_ => SyncMode::Full,
})
}
Expr::Literal(Literal::Numeric(n)) => match n.as_str() {
"0" => SyncMode::Off,
_ => SyncMode::Full,
},
_ => SyncMode::Full,
let mode = match parse_pragma_enabled(&value) {
true => SyncMode::Full,
false => SyncMode::Off,
};
connection.set_sync_mode(mode);
Ok((program, TransactionMode::None))
}
PragmaName::DataSyncRetry => {
let retry_enabled = match value {
Expr::Name(name) => {
let name_bytes = name.as_str().as_bytes();
match_ignore_ascii_case!(match name_bytes {
b"ON" | b"TRUE" | b"YES" | b"1" => true,
_ => false,
})
}
Expr::Literal(Literal::Numeric(n)) => !matches!(n.as_str(), "0"),
_ => false,
};
let retry_enabled = parse_pragma_enabled(&value);
connection.set_data_sync_retry(retry_enabled);
Ok((program, TransactionMode::None))
}
@@ -388,24 +379,7 @@ fn update_pragma(
Ok((program, TransactionMode::None))
}
PragmaName::ForeignKeys => {
let enabled = match value {
Expr::Name(name) | Expr::Id(name) => {
let name_bytes = name.as_str().as_bytes();
match_ignore_ascii_case!(match name_bytes {
b"ON" | b"TRUE" | b"YES" | b"1" => true,
_ => false,
})
}
Expr::Literal(Literal::Keyword(name) | Literal::String(name)) => {
let name_bytes = name.as_bytes();
match_ignore_ascii_case!(match name_bytes {
b"ON" | b"TRUE" | b"YES" | b"1" => true,
_ => false,
})
}
Expr::Literal(Literal::Numeric(n)) => !matches!(n.as_str(), "0"),
_ => false,
};
let enabled = parse_pragma_enabled(&value);
connection.set_foreign_keys_enabled(enabled);
Ok((program, TransactionMode::None))
}

View File

@@ -490,11 +490,14 @@ pub fn emit_upsert(
}
// Parent-side checks only if any incoming FK could care
if resolver.schema.any_incoming_fk_to(table.get_name()) {
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.incoming_fks_to(table.get_name());
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));