All rules Rule SL027 · DS
SQLite · beta
warning
DS · Destructive changes
free in the CLI, --engine=sqlite

What does an unbounded DELETE cascade to in SQLite?

Unbounded DELETE that cascades to child tables

Warning: it works, but holds the single write lock or breaks the previous release.

What happens

A DELETE with no WHERE on a table that other tables reference with ON DELETE CASCADE empties those tables too, in the same statement, with foreign keys on, and does nothing to them with foreign keys off. The blast radius depends on a per-connection pragma, and the statement itself names only one table.

Why it is dangerous on a populated table

SQLite holds one write lock for the whole database, so anything that rebuilds a table blocks every writer for as long as the copy takes, and that time grows with the table.

Fires on

CREATE TABLE lines (id INTEGER PRIMARY KEY, order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE);
DELETE FROM orders;

The safe pattern

State the predicate and batch the delete; if the intent is to clear the parent and its children, delete the children explicitly first so the migration reads the way it behaves.

-- batched job, children first, explicit:
--    DELETE FROM lines WHERE order_id IN (SELECT id FROM orders WHERE status = 'closed' AND rowid BETWEEN ? AND ?);
--    DELETE FROM orders WHERE status = 'closed' AND rowid BETWEEN ? AND ?;

Fixtures

The rule ships with these files and the test suite runs them on every change: the first set must fire, the second must stay silent.

Fires (1)

cascade delete all
CREATE TABLE lines (id INTEGER PRIMARY KEY, order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE) STRICT;
CREATE INDEX lines_order_id ON lines (order_id);
DELETE FROM orders;

Stays silent (2)

no cascade
CREATE TABLE lines (id INTEGER PRIMARY KEY, order_id INTEGER REFERENCES orders(id)) STRICT;
CREATE INDEX lines_order_id ON lines (order_id);
DELETE FROM orders;
with where
CREATE TABLE lines (id INTEGER PRIMARY KEY, order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE) STRICT;
CREATE INDEX lines_order_id ON lines (order_id);
DELETE FROM orders WHERE status = 'closed' AND rowid BETWEEN 1 AND 1000;

How to check locally

Catch this before it ships

SQLite support is in beta: this rule runs locally in the free CLI, static only, and not in the hosted service yet. No install, nothing leaves your machine:

npx bolvrk check migration.sql --engine=sqlite