#!sqlite3 # # 2026-10-21 # # The author disclaims copyright to this source code. In place of # a legal notice, here is a blessing: # # May you do good and not evil. # May you find forgiveness for yourself and forgive others. # May you share freely, never taking more than you give. # #*********************************************************************** # # Test cases for foreign keys where the comparison between parent and # child keys does not naturally use the affinity of the parent key. # In that case an extra IN term is added to the comparison, but only if # the child key is indexed by a suitable index - one whose left-most # column is the child key, that is not a partial index, and that uses # the same collation sequence as the parent key. See the # fkChildKeyIsIndexed() routine in fkey.c. # # Each test case below creates a TEXT parent key and a BLOB (untyped) # child key, plus some set of indexes on the child table. It then checks: # # * Whether or not an index on the child table is used # (EXPLAIN QUERY PLAN). # # * Whether or not the IN term is present in the trigger program # that implements the ON DELETE CASCADE action on table c. The IN # term is the only source of an OP_Cast opcode in that program. # # * That the correct child rows are deleted, whichever plan is used, # and that the no-action foreign key on table n is enforced. # # To run these tests: # # ./sqlite3 test/fkey09.sql # # .mode list #------------------------------------------------------------------------- # No indexes on the child tables at all. # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 100.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 0 .testcase 100.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 100.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # An index on a column other than the child key. The left-most # column of the index is not the child key (line 558 false). # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX cz ON c(z); CREATE INDEX nz ON n(z); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 110.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 0 .testcase 110.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 110.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # The child key is a column of the index, but not the left-most. # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX czk ON c(z, k); CREATE INDEX nzk ON n(z, k); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 120.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 0 .testcase 120.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 120.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # A partial index on the child key (line 559 false). # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ckp ON c(k) WHERE z>0; CREATE INDEX nkp ON n(k) WHERE z>0; INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 130.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 0 .testcase 130.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 130.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # An index on the child key with the wrong collation sequence # (line 560 false). # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ckn ON c(k COLLATE nocase); CREATE INDEX nkn ON n(k COLLATE nocase); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 140.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 0 .testcase 140.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 140.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # A suitable index on the child key (all three conditions true). # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ck ON c(k); CREATE INDEX nk ON n(k); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 150.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 1 .testcase 150.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 150.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # A suitable index, with an explicit BINARY collation in a different # case. Collation sequence names are compared without regard to case. # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ck ON c(k COLLATE BiNaRy); CREATE INDEX nk ON n(k COLLATE BiNaRy); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 160.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 1 .testcase 160.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 160.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # Several unsuitable indexes, for each of the three reasons, followed # by a suitable one. The loop in fkChildKeyIsIndexed() must skip each # unsuitable index before it finds the suitable one. # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ck ON c(k); CREATE INDEX nk ON n(k); CREATE INDEX cz ON c(z); CREATE INDEX nz ON n(z); CREATE INDEX czk ON c(z, k); CREATE INDEX nzk ON n(z, k); CREATE INDEX ckp ON c(k) WHERE z>0; CREATE INDEX nkp ON n(k) WHERE z>0; CREATE INDEX ckn ON c(k COLLATE nocase); CREATE INDEX nkn ON n(k COLLATE nocase); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 170.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 1 .testcase 170.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 170.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # Several unsuitable indexes, for each of the three reasons, and no # suitable one. # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX cz ON c(z); CREATE INDEX nz ON n(z); CREATE INDEX czk ON c(z, k); CREATE INDEX nzk ON n(z, k); CREATE INDEX ckp ON c(k) WHERE z>0; CREATE INDEX nkp ON n(k) WHERE z>0; CREATE INDEX ckn ON c(k COLLATE nocase); CREATE INDEX nkn ON n(k COLLATE nocase); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 180.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 0 .testcase 180.3 DELETE FROM p WHERE code='1001'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 180.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # A parent key with NOCASE collation. An index on the child key with # BINARY collation is not suitable (line 560 false). # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT COLLATE NOCASE UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ck ON c(k); CREATE INDEX nk ON n(k); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 200.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 0 .testcase 200.3 DELETE FROM p WHERE code='ABC'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 200.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # A parent key with NOCASE collation and an index on the child key # with the same collation, named in a different case (line 560 true). # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT COLLATE NOCASE UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ckn ON c(k COLLATE nocase); CREATE INDEX nkn ON n(k COLLATE nocase); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (6, 'abc '); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 210.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 1 .testcase 210.3 DELETE FROM p WHERE code='ABC'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 210.6 PRAGMA foreign_key_check; .check '' #------------------------------------------------------------------------- # A parent key with RTRIM collation. Both an unsuitable index (NOCASE) # and a suitable one (RTRIM). # .open :memory: PRAGMA foreign_keys=ON; CREATE TABLE p(id INTEGER PRIMARY KEY, code TEXT COLLATE RTRIM UNIQUE); CREATE TABLE c(id INTEGER PRIMARY KEY, z, k REFERENCES p(code) ON DELETE CASCADE ); CREATE TABLE n(id INTEGER PRIMARY KEY, z, k REFERENCES p(code)); CREATE INDEX ckn ON c(k COLLATE nocase); CREATE INDEX nkn ON n(k COLLATE nocase); CREATE INDEX ckr ON c(k COLLATE rtrim); CREATE INDEX nkr ON n(k COLLATE rtrim); INSERT INTO p VALUES(1, '1001'), (2, '01001'), (3, 'abc'), (4, '1001.0'), (5, 'ABC'); INSERT INTO c(id, z, k) VALUES (1, 1, 1001), (2, 2, '1001'), (3, 3, '01001'), (4, 4, 1001.0), (5, 5, 'abc'), (6, 6, 'ABC'), (7, 7, 'abc '); .testcase 220.1 EXPLAIN QUERY PLAN DELETE FROM p WHERE id=1; .check <0 FROM bytecode('DELETE FROM p WHERE id=1') WHERE opcode='Cast' AND subprog IS NOT NULL; .check 1 .testcase 220.3 DELETE FROM p WHERE code='abc'; SELECT id FROM c ORDER BY id; .check <'1001.0'; SELECT count(*) FROM p; .check 1 .testcase 220.6 PRAGMA foreign_key_check; .check ''