mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-27 20:04:23 +01:00
Fix self-referential FK relationships and validation of FKs
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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|}
|
||||
|
||||
Reference in New Issue
Block a user