mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-06 00:34:23 +01:00
Add helper to pragma to parse enabled opts and fix schema parsing for foreign key constraints
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
152
core/schema.rs
152
core/schema.rs
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 FK’s 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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user