Resolving "FOREIGN KEY constraint failed" with Cloudflare SQLite

tl;dr - You likely need PRAGMA defer_foreign_keys = on; and these docs.

While writing a SQL migration for a Durable Object I kept hitting this frustrating error:

FOREIGN KEY constraint failed: SQLITE_CONSTRAINT

I had something similar to the following:

CREATE TABLE A (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT
);

CREATE TABLE B (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  id2 INTEGER,
  book TEXT,
  FOREIGN KEY(id2) REFERENCES A(id)
);

I wanted to add a CHECK constraint to a column in table A which can be done by renaming and copying:

CREATE TABLE new_A (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL CHECK (type IN ('image', 'html', 'pdf'))
);

INSERT INTO new_A
SELECT
  id,
  name
FROM A;

DROP TABLE A;

ALTER TABLE new_A RENAME TO A;

Note that this invariant was already enforced by the app. This would not be safe to do in general.

However this won’t work, and you’ll get the FOREIGN KEY constraint failed error.

The SQLite docs will lead you to:

PRAGMA foreign_keys = false

-- Do the migration

PRAGMA foreign_keys = true

However this will not work either. Though frustratingly it will if you dig into the .wrangler directory, find the sqlite db file, and try applying it directly.

The solution is in the Cloudflare docs. The doc is for D1 but this applies to SQLite in Durable Objects as well.

D1’s foreign key enforcement is equivalent to SQLite’s PRAGMA foreign_keys = on directive. Because D1 runs every query inside an implicit transaction, user queries cannot change this during a query or migration.

D1 allows you to call PRAGMA defer_foreign_keys = on or off, which allows you to violate foreign key constraints temporarily (until the end of the current transaction).

We can now fix our migration!

PRAGMA defer_foreign_keys = on;

CREATE TABLE new_A (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL CHECK (type IN ('image', 'html', 'pdf'))
);

INSERT INTO new_A
SELECT
  id,
  name
FROM A;

DROP TABLE A;
ALTER TABLE new_A RENAME TO A;

PRAGMA defer_foreign_keys = off;