diff --git a/core/translate/insert.rs b/core/translate/insert.rs index 78c5dee5c..f6cfa4b88 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -23,7 +23,7 @@ use crate::translate::upsert::{ }; use crate::util::normalize_ident; use crate::vdbe::builder::ProgramBuilderOpts; -use crate::vdbe::insn::{IdxInsertFlags, InsertFlags, RegisterOrLiteral}; +use crate::vdbe::insn::{CmpInsFlags, IdxInsertFlags, InsertFlags, RegisterOrLiteral}; use crate::vdbe::BranchOffset; use crate::{ schema::{Column, Schema}, @@ -135,6 +135,10 @@ pub fn translate_insert( 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; @@ -238,7 +242,7 @@ pub fn translate_insert( connection, )?; - if has_child_fks { + if has_child_fks || has_parent_fks { program.emit_insn(Insn::FkCounter { increment_value: 1, check_abort: false, @@ -1039,8 +1043,14 @@ pub fn translate_insert( } } } - if has_child_fks { - emit_fk_checks_for_insert(&mut program, resolver, &insertion, table_name.as_str())?; + if has_child_fks || has_parent_fks { + emit_fk_checks_for_insert( + &mut program, + resolver, + &insertion, + table_name.as_str(), + !inserting_multiple_rows, + )?; } program.emit_insn(Insn::Insert { @@ -1188,7 +1198,7 @@ pub fn translate_insert( } program.preassign_label_to_next_insn(stmt_epilogue); - if has_child_fks { + if has_child_fks || has_parent_fks { // close FK scope and surface deferred violations program.emit_insn(Insn::FkCounter { increment_value: -1, @@ -1896,6 +1906,7 @@ fn emit_fk_checks_for_insert( resolver: &Resolver, insertion: &Insertion, table_name: &str, + single_row_insert: bool, ) -> Result<()> { let after_all = program.allocate_label(); program.emit_insn(Insn::FkIfZero { @@ -1910,7 +1921,8 @@ fn emit_fk_checks_for_insert( .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. let fk_ok = program.allocate_label(); for &pos_in_child in fk_ref.child_pos.iter() { @@ -1934,16 +1946,32 @@ fn emit_fk_checks_for_insert( root_page: parent_tbl.root_page, db: 0, }); - let only = 0; // n == 1 guaranteed if parent_uses_rowid + let rowid_pos = 0; // guaranteed if parent_uses_rowid let src = insertion - .col_mappings - .get(fk_ref.child_pos[only]) + .get_col_mapping_by_name(fk_ref.child_cols[rowid_pos].as_str()) .unwrap() .register; let violation = program.allocate_label(); + let tmp = program.alloc_register(); + program.emit_insn(Insn::Copy { + src_reg: src, + 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, + }); + } program.emit_insn(Insn::NotExists { cursor: pcur, - rowid_reg: src, + rowid_reg: tmp, target_pc: violation, }); program.emit_insn(Insn::Close { cursor_id: pcur }); @@ -1966,6 +1994,27 @@ fn emit_fk_checks_for_insert( }); } } 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, + }); + } + // all matched, OK + program.emit_insn(Insn::Goto { target_pc: fk_ok }); + program.preassign_label_to_next_insn(skip_probe); + } + // 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 { diff --git a/testing/foreign_keys.test b/testing/foreign_keys.test index 7db9b876c..a88ca55fe 100644 --- a/testing/foreign_keys.test +++ b/testing/foreign_keys.test @@ -124,14 +124,17 @@ do_execsql_test_in_memory_any_error fk-composite-unique-missing { INSERT INTO child VALUES (2,'A','X'); -- no ('A','X') in parent } -do_execsql_test_on_specific_db {:memory:} fk-rowid-alias-parent-ok { +# 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)); + CREATE TABLE c(cid INTEGER PRIMARY KEY, rid REFERENCES t(rowid)); -- we error here INSERT INTO t VALUES (100,'x'); - INSERT INTO c VALUES (1, 100); - SELECT cid, rid FROM c; -} {1|100} + INSERT INTO c VALUES (1, 100); - sqlite errors here +} do_execsql_test_in_memory_any_error fk-rowid-alias-parent-missing { PRAGMA foreign_keys=ON; @@ -192,3 +195,139 @@ do_execsql_test_in_memory_any_error fk-composite-pk-delete-violate { -- Deleting the referenced tuple should fail DELETE FROM p WHERE a=2 AND b=3; } + +# Parent columns omitted: should default to parent's declared PRIMARY KEY (composite) +do_execsql_test_on_specific_db {:memory:} fk-default-parent-pk-composite-ok { + PRAGMA foreign_keys=ON; + CREATE TABLE p( + a INT NOT NULL, + b INT NOT NULL, + PRIMARY KEY(a,b) + ); + -- Parent columns omitted in REFERENCES p + CREATE TABLE c( + id INT PRIMARY KEY, + x INT, y INT, + FOREIGN KEY(x,y) REFERENCES p + ); + INSERT INTO p VALUES (1,1), (1,2); + INSERT INTO c VALUES (10,1,1), (11,1,2), (12,NULL,2); -- NULL in child allowed + SELECT id,x,y FROM c ORDER BY id; +} {10|1|1 +11|1|2 +12||2} + +do_execsql_test_in_memory_any_error fk-default-parent-pk-composite-missing { + PRAGMA foreign_keys=ON; + CREATE TABLE p(a INT NOT NULL, b INT NOT NULL, PRIMARY KEY(a,b)); + CREATE TABLE c(id INT PRIMARY KEY, x INT, y INT, + FOREIGN KEY(x,y) REFERENCES p); -- omit parent cols + INSERT INTO p VALUES (1,1); + INSERT INTO c VALUES (20,1,2); -- (1,2) missing in parent +} + +# Parent has no explicitly declared PK, so we throw parse error when referencing bare table +do_execsql_test_in_memory_any_error fk-default-parent-rowid-no-parent-pk { + PRAGMA foreign_keys=ON; + CREATE TABLE p_no_pk(v TEXT); + CREATE TABLE c_rowid(id INT PRIMARY KEY, + r REFERENCES p_no_pk); + INSERT INTO p_no_pk(v) VALUES ('a'), ('b'); + INSERT INTO c_rowid VALUES (1, 1); +} + +do_execsql_test_on_specific_db {:memory:} fk-parent-omit-cols-parent-has-pk { + PRAGMA foreign_keys=ON; + CREATE TABLE p_pk(id INTEGER PRIMARY KEY, v TEXT); + CREATE TABLE c_ok(id INT PRIMARY KEY, r REFERENCES p_pk); -- binds to p_pk(id) + INSERT INTO p_pk VALUES (1,'a'),(2,'b'); + INSERT INTO c_ok VALUES (10,1); + INSERT INTO c_ok VALUES (11,2); + SELECT id, r FROM c_ok ORDER BY id; +} {10|1 11|2} + + +# Self-reference (same table) with INTEGER PRIMARY KEY: single-row insert should pass +do_execsql_test_on_specific_db {:memory:} fk-self-ipk-single-ok { + PRAGMA foreign_keys=ON; + CREATE TABLE t( + id INTEGER PRIMARY KEY, + rid REFERENCES t(id) -- child->parent in same table + ); + INSERT INTO t(id,rid) VALUES(5,5); -- self-reference, single-row + SELECT id, rid FROM t; +} {5|5} + +# Self-reference with mismatched value: should fail immediately (no counter semantics used) +do_execsql_test_in_memory_any_error fk-self-ipk-single-mismatch { + PRAGMA foreign_keys=ON; + CREATE TABLE t( + id INTEGER PRIMARY KEY, + rid REFERENCES t(id) + ); + INSERT INTO t(id,rid) VALUES(5,4); -- rid!=id -> FK violation +} + +# Self-reference on composite PRIMARY KEY: single-row insert should pass +do_execsql_test_on_specific_db {:memory:} fk-self-composite-single-ok { + PRAGMA foreign_keys=ON; + CREATE TABLE t( + a INT NOT NULL, + b INT NOT NULL, + x INT, + y INT, + PRIMARY KEY(a,b), + FOREIGN KEY(x,y) REFERENCES t(a,b) + ); + INSERT INTO t(a,b,x,y) VALUES(1,2,1,2); -- self-reference matches PK + SELECT a,b,x,y FROM t; +} {1|2|1|2} + +# Rowid parent path: text '10' must be coerced to integer (MustBeInt) and succeed +do_execsql_test_on_specific_db {:memory:} fk-rowid-mustbeint-coercion-ok { + PRAGMA foreign_keys=ON; + CREATE TABLE p(id INTEGER PRIMARY KEY); + CREATE TABLE c(cid INTEGER PRIMARY KEY, pid REFERENCES p(id)); + INSERT INTO p(id) VALUES(10); + INSERT INTO c VALUES(1, '10'); -- text -> int via MustBeInt; should match + SELECT pid FROM c; +} {10} + +# Rowid parent path: non-numeric text cannot be coerced -> violation +do_execsql_test_in_memory_any_error fk-rowid-mustbeint-coercion-fail { + PRAGMA foreign_keys=ON; + CREATE TABLE p(id INTEGER PRIMARY KEY); + CREATE TABLE c(cid INTEGER PRIMARY KEY, pid REFERENCES p(id)); + INSERT INTO p(id) VALUES(10); + INSERT INTO c VALUES(2, 'abc'); -- MustBeInt fails to match any parent row +} + +# Parent match via UNIQUE index (non-rowid), success path +do_execsql_test_on_specific_db {:memory:} fk-parent-unique-index-ok { + PRAGMA foreign_keys=ON; + CREATE TABLE parent(u TEXT, v TEXT, pad INT, UNIQUE(u,v)); + CREATE TABLE child(id INT PRIMARY KEY, cu TEXT, cv TEXT, + FOREIGN KEY(cu,cv) REFERENCES parent(u,v)); + INSERT INTO parent VALUES ('A','B',0),('A','C',0); + INSERT INTO child VALUES (1,'A','B'); + SELECT id, cu, cv FROM child ORDER BY id; +} {1|A|B} + +# Parent UNIQUE index path: missing key -> immediate violation +do_execsql_test_in_memory_any_error fk-parent-unique-index-missing { + PRAGMA foreign_keys=ON; + CREATE TABLE parent(u TEXT, v TEXT, pad INT, UNIQUE(u,v)); + CREATE TABLE child(id INT PRIMARY KEY, cu TEXT, cv TEXT, + FOREIGN KEY(cu,cv) REFERENCES parent(u,v)); + INSERT INTO parent VALUES ('A','B',0); + INSERT INTO child VALUES (2,'A','X'); -- no ('A','X') in parent +} + +# NULL in child short-circuits FK check +do_execsql_test_on_specific_db {:memory:} fk-child-null-shortcircuit { + PRAGMA foreign_keys=ON; + CREATE TABLE p(id INTEGER PRIMARY KEY); + CREATE TABLE c(id INTEGER PRIMARY KEY, pid REFERENCES p(id)); + INSERT INTO c VALUES (1, NULL); -- NULL child is allowed + SELECT id, pid FROM c; +} {1|}