Fix self-referential FK relationships and validation of FKs

This commit is contained in:
PThorpe92
2025-10-01 11:09:18 -04:00
parent fa23cedbbe
commit 99ae96c5f6
2 changed files with 203 additions and 15 deletions

View File

@@ -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 {

View File

@@ -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|}