"PRAGMA foreign_keys = OFF" and "ON DELETE CASCADE"
(1) By Ulrich (ulrich.w) on 2024-02-07 07:14:03 [link] [source]
I wonder whether this is a bug or a feature:
I'm using DBI::SQLite to test the schema of my database (in SLES 12 SP5). One of the tests (that tests triggers) deliberately uses "PRAGMA foreign_keys = OFF" to check some deleting triggers while a "ON DELETE CASCADE" is present. Just to make sure the trigger deletes the rows, not the CASCADE.
With 3.7.17 that test worked, meaning the "ON DELETE CASCADE" was being ignored a s expected using PRAGMA foreign_keys = OFF".
However after upgrading to 3.9.2, the "ON DELETE CASCADE" is being obeyed even with "PRAGMA foreign_keys = OFF".
Now the simple question is: Is it a bug, or is it a feature that ON DELETE CASCADE is still being performed when PRAGMA foreign_keys = OFF?
(2) By Dan Kennedy (dan) on 2024-02-07 10:58:44 in reply to 1 [link] [source]
It sounds like a bug. Can you provide an SQL script to demonstrate the phenomenon? The following doesn't show the problem (returns a single row - ('x', 112)):
CREATE TABLE parent(a INTEGER PRIMARY KEY);
CREATE TABLE child(x, y REFERENCES parent ON DELETE CASCADE);
INSERT INTO parent VALUES(111);
INSERT INTO child VALUES('x', 111);
INSERT INTO parent VALUES(112);
INSERT INTO child VALUES('x', 112);
PRAGMA foreign_keys = ON;
DELETE FROM parent WHERE a=111;
PRAGMA foreign_keys = OFF;
DELETE FROM parent WHERE a=112;
SELECT * FROM child;
Thanks,
Dan.
(3) By Ulrich (ulrich.w) on 2024-02-08 08:53:57 in reply to 2 [source]
I cannot reproduce in 3.44.0, so I'd be interested when the bug was introduced, and when it was fixed. I don't have a minimal test script, but I have a test script: If cascade is ignored, the output will be ID|REF_CNT|NAME 1|2|C1 3|2|C3 but if CASCADE works, the output will be ID|REF_CNT|NAME 1|1|C1 The test script is: CREATE TABLE CREATORS ( ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, REF_CNT INTEGER NOT NULL DEFAULT 0, NAME VARCHAR(32) UNIQUE NOT NULL ); CREATE UNIQUE INDEX IDX_CREATORS_NAME ON CREATORS (NAME); CREATE TABLE KEY_DATA ( ID INTEGER PRIMARY KEY NOT NULL REFERENCES KEY_ID(ID) ON UPDATE CASCADE ON DELETE CASCADE, KEY_DATA VARCHAR(4096) NOT NULL, CREATOR_ID INTEGER NOT NULL REFERENCES CREATORS(ID) ON UPDATE CASCADE ON DELETE RESTRICT, CREATED DATETIME, MODIFIED DATETIME, LOCKED DATETIME); CREATE INDEX IDX_KEYDATA_CREATOR ON KEY_DATA (CREATOR_ID); CREATE TABLE KEY_TYPE ( ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, REF_CNT INTEGER NOT NULL DEFAULT 0, NAME VARCHAR(32) UNIQUE NOT NULL ); CREATE UNIQUE INDEX IDX_KEY_TYPE_NAME ON KEY_TYPE (NAME); CREATE TRIGGER TRG_KEY_TYPE_DEL AFTER UPDATE OF REF_CNT ON KEY_TYPE FOR EACH ROW WHEN NEW.REF_CNT = 0 BEGIN DELETE FROM KEY_TYPE WHERE ID = OLD.ID; END; CREATE TABLE KEY_ID ( ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, TYPE_ID INTEGER NOT NULL REFERENCES KEY_TYPE(ID) ON UPDATE RESTRICT ON DELETE RESTRICT, NAME VARCHAR(16) NOT NULL ); CREATE INDEX IDX_KEY_ID_TYPE_ID ON KEY_ID (TYPE_ID); CREATE UNIQUE INDEX IDX_KEY_ID_TYPE_INST ON KEY_ID (TYPE_ID, NAME); CREATE TRIGGER TRG_CREATORS_DEL AFTER UPDATE OF REF_CNT ON CREATORS FOR EACH ROW WHEN NEW.REF_CNT = 0 BEGIN DELETE FROM CREATORS WHERE ID = OLD.ID; END; CREATE TRIGGER TRG_CREATOR_ID_ADD AFTER INSERT ON KEY_DATA FOR EACH ROW WHEN NEW.CREATOR_ID = NEW.CREATOR_ID BEGIN UPDATE CREATORS SET REF_CNT = (SELECT COUNT(1) FROM KEY_DATA WHERE CREATOR_ID = NEW.CREATOR_ID) WHERE ID = NEW.CREATOR_ID; END; CREATE TRIGGER TRG_CREATOR_ID_UPD AFTER UPDATE OF CREATOR_ID ON KEY_DATA FOR EACH ROW WHEN NEW.CREATOR_ID != OLD.CREATOR_ID BEGIN UPDATE CREATORS SET REF_CNT = (SELECT COUNT(1) FROM KEY_DATA WHERE CREATOR_ID = OLD.CREATOR_ID) WHERE ID = OLD.CREATOR_ID; UPDATE CREATORS SET REF_CNT = (SELECT COUNT(1) FROM KEY_DATA WHERE CREATOR_ID = NEW.CREATOR_ID) WHERE ID = NEW.CREATOR_ID; END; CREATE TRIGGER TRG_CREATOR_ID_DEL AFTER DELETE ON KEY_DATA FOR EACH ROW WHEN OLD.CREATOR_ID = OLD.CREATOR_ID BEGIN UPDATE CREATORS SET REF_CNT = (SELECT COUNT(1) FROM KEY_DATA WHERE CREATOR_ID = OLD.CREATOR_ID) WHERE ID = OLD.CREATOR_ID; END; CREATE TRIGGER TRG_KEY_ID_ADD AFTER INSERT ON KEY_ID FOR EACH ROW BEGIN UPDATE KEY_TYPE SET REF_CNT = (SELECT COUNT(1) FROM KEY_ID WHERE TYPE_ID = NEW.TYPE_ID) WHERE ID = NEW.TYPE_ID; END; CREATE TRIGGER TRG_KEY_ID_UPD AFTER UPDATE OF TYPE_ID ON KEY_ID FOR EACH ROW BEGIN UPDATE KEY_TYPE SET REF_CNT = (SELECT COUNT(1) FROM KEY_ID WHERE TYPE_ID = OLD.TYPE_ID) WHERE ID = OLD.TYPE_ID; UPDATE KEY_TYPE SET REF_CNT = (SELECT COUNT(1) FROM KEY_ID WHERE TYPE_ID = NEW.TYPE_ID) WHERE ID = NEW.TYPE_ID; END; CREATE TRIGGER TRG_KEY_ID_DEL AFTER DELETE ON KEY_ID FOR EACH ROW BEGIN UPDATE KEY_TYPE SET REF_CNT = (SELECT COUNT(1) FROM KEY_ID WHERE TYPE_ID = OLD.TYPE_ID) WHERE ID = OLD.TYPE_ID; END; CREATE VIEW V_KEY_METADATA AS SELECT KEY_TYPE.NAME AS TYPE, KEY_ID.NAME, CREATORS.NAME AS CREATOR, CREATED, MODIFIED, LOCKED FROM KEY_TYPE INNER JOIN KEY_ID ON KEY_TYPE.ID = KEY_ID.TYPE_ID INNER JOIN KEY_DATA ON KEY_DATA.ID = KEY_ID.ID INNER JOIN CREATORS ON CREATORS.ID = CREATOR_ID; CREATE VIEW V_KEY_ID AS SELECT KEY_TYPE.NAME AS TYPE, KEY_ID.NAME, KEY_ID.ID FROM KEY_TYPE INNER JOIN KEY_ID ON KEY_TYPE.ID = KEY_ID.TYPE_ID; BEGIN TRANSACTION; PRAGMA foreign_keys = OFF; INSERT INTO KEY_TYPE (NAME) VALUES ('T1'), ('T2'), ('T3'); INSERT INTO CREATORS (NAME) VALUES ('C1'), ('C2'), ('C3'); .headers ON #SELECT REF_CNT, NAME FROM KEY_TYPE; #SELECT REF_CNT, NAME FROM CREATORS; INSERT INTO KEY_ID (TYPE_ID, NAME) VALUES (1, 'N1-1'), (2, 'N2-1'), (2, 'N2-2'), (3, 'N3-1'), (3, 'N3-2'), (3, 'N3-3'); #SELECT ID, REF_CNT, NAME FROM KEY_TYPE; INSERT INTO KEY_DATA (KEY_DATA, CREATOR_ID) VALUES ('C1-1', 1), ('C2-1', 2), ('C2-2', 2), ('C3-1', 3), ('C3-2', 3), ('C3-3', 3); #SELECT ID, REF_CNT, NAME FROM CREATORS; UPDATE KEY_DATA SET CREATOR_ID=1 WHERE ID=5; #SELECT ID, REF_CNT, NAME FROM CREATORS; UPDATE KEY_ID SET TYPE_ID=1 WHERE NAME='N2-2'; #SELECT ID, REF_CNT, NAME FROM KEY_TYPE; DELETE FROM KEY_ID WHERE TYPE_ID=3; #SELECT ID, REF_CNT, NAME FROM KEY_TYPE; DELETE FROM KEY_DATA WHERE CREATOR_ID=2; SELECT ID, REF_CNT, NAME FROM CREATORS; DELETE FROM KEY_DATA; DELETE FROM KEY_ID; COMMIT;
(4.1) By Dan Kennedy (dan) on 2024-02-08 11:10:26 edited from 4.0 in reply to 3 [link] [source]
I cannot reproduce in 3.44.0, so I'd be interested when the bug was introduced, and when it was fixed.
Suspect your 3.9.2 shell is built with foreign keys enabled by default. The script does have a "PRAGMA foreign_keys = off" statement, but it's inside a transaction. And you cannot enable or disable foreign keys inside a transaction. Last paragraph in this section:
https://sqlite.org/foreignkeys.html#fk_enable
Dan.
(5) By Ulrich (ulrich.w) on 2024-02-09 08:10:06 in reply to 4.1 [link] [source]
Thanks, two questions: 1) So it will work when done outside the transaction (and undone after it)? 2) Does "at the beginning" qualify as "in the middle" for "It is not possible to enable or disable foreign key constraints in the middle of a multi-statement transaction (when SQLite is not in autocommit mode)." I would agree that it's very complex to handle ONs and OFFs within one transaction, but right at the start shouldn't really be a problem IMHO.
(6) By Dan Kennedy (dan) on 2024-02-09 10:44:26 in reply to 5 [link] [source]
Yes and yes.
Dan.