How to check foreign key

Read the declared constraint with PRAGMA foreign_key_list(orders), then read whether anything enforces it with PRAGMA foreign_keys. In the sqlite3 shell that second pragma answers 0, so an order pointing at customer 999 is accepted. PRAGMA foreign_key_check then lists the orphan the schema promised to prevent.

Why check this

A foreign key in a schema file is two separate things, and testers routinely check only the first. One is the declaration: this column points at that table. The other is enforcement: some component refuses a write that breaks the declaration. SQLite ships them apart. The declaration lives in the file; enforcement is a per-connection switch that the library leaves off, and each client decides for itself whether to turn it on.

Run this after any change to the schema, after a restore, and on every service that opens its own connection. A driver, an ORM, a migration runner and a support engineer with a shell are four connections, and each decides for itself.

The failure it prevents is a table full of rows pointing at records deleted months ago. Nothing errors. Reports under-count because the join drops those rows, the delete path never fired its cascade, and the migration that adds the real constraint fails on production data the day it runs.

Prerequisites

CREATE TABLE customer (
  id      INTEGER PRIMARY KEY,
  email   TEXT UNIQUE,
  country TEXT NOT NULL
);
CREATE TABLE orders (
  id          INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL REFERENCES customer(id),
  total_cents INTEGER NOT NULL
);
INSERT INTO customer (id, email, country) VALUES
  (1, 'ana@example.com', 'UA'),
  (2, 'bo@example.com',  'PL');
INSERT INTO orders (id, customer_id, total_cents) VALUES
  (10, 1, 2500),
  (11, 2, 900);

One more file, fk-default.mjs, for step 5.

import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('shop.db');
console.log('foreign_keys =', db.prepare('PRAGMA foreign_keys').get().foreign_keys);
console.log('sqlite_version =', db.prepare('SELECT sqlite_version() AS v').get().v);
db.close();

Steps

  1. Step 1.

    Read what the schema declares for the child table.

    sqlite3 shop.db ".mode box" "PRAGMA foreign_key_list(orders);"
    
    ┌────┬─────┬──────────┬─────────────┬────┬───────────┬───────────┬───────┐
    │ id │ seq │  table   │    from     │ to │ on_update │ on_delete │ match │
    ├────┼─────┼──────────┼─────────────┼────┼───────────┼───────────┼───────┤
    │ 0  │ 0   │ customer │ customer_id │ id │ NO ACTION │ NO ACTION │ NONE  │
    └────┴─────┴──────────┴─────────────┴────┴───────────┴───────────┴───────┘

    One key: orders.customer_id points at customer.id. on_delete is NO ACTION, so nothing cascades. An empty result means the table declares no keys, whatever the diagram shows.

  2. Step 2.

    Ask whether this connection enforces that declaration, then write a row that breaks it.

    sqlite3 shop.db ".mode box" "PRAGMA foreign_keys; INSERT INTO orders (id, customer_id, total_cents) VALUES (12, 999, 4200); SELECT changes() AS rows_inserted;"
    
    ┌──────────────┐
    │ foreign_keys │
    ├──────────────┤
    │ 0            │
    └──────────────┘
    ┌───────────────┐
    │ rows_inserted │
    ├───────────────┤
    │ 1             │
    └───────────────┘

    The pragma returns 0 and the insert returns 1. There is no customer 999, no error, and no warning. This is the most important line on the page: the constraint is declared and this client is enforcing none of it.

  3. Step 3.

    Find the rows that got in. This pragma works whatever the enforcement setting is.

    sqlite3 shop.db ".mode box" "PRAGMA foreign_key_check;"
    
    ┌────────┬───────┬──────────┬──────┐
    │ table  │ rowid │  parent  │ fkid │
    ├────────┼───────┼──────────┼──────┤
    │ orders │ 12    │ customer │ 0    │
    └────────┴───────┴──────────┴──────┘

    One row per violation: the child table, the rowid of the offending row, the parent table, and the index of the key in the foreign_key_list output above. No rows means no orphans.

  4. Step 4.

    Turn enforcement on and run the same insert again.

    sqlite3 shop.db "PRAGMA foreign_keys = ON; INSERT INTO orders (id, customer_id, total_cents) VALUES (13, 999, 4200);"
    
    Error: stepping, FOREIGN KEY constraint failed (19)

    The same statement, the same file, one pragma apart. The message names no table and no column, unlike the unique constraint error, so log the statement with it. The orphan from step 2 is still there: switching the pragma on does not re-examine stored rows.

  5. Step 5.

    Open the same file from a different client and read the pragma again.

    node --no-warnings fk-default.mjs
    
    foreign_keys = 1
    sqlite_version = 3.51.3

    Node's DatabaseSync turns the pragma on when it connects, and it links a newer SQLite than the shell on PATH. The same file therefore enforces foreign keys for the application and not for the engineer debugging it in a terminal. The default belongs to the client, not to the database.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | foreign_key_list returns no rows | The table declares no foreign key | The relationship exists only in the diagram. Add it in a migration. | | PRAGMA foreign_keys returns 0 | This connection enforces nothing | Set it to ON at connect, in every client, and assert it in a health check. | | PRAGMA foreign_keys returns 1 | This connection enforces | Confirm the setting is applied at connect, not once by hand. | | foreign_key_check returns rows | Orphans are already stored | Fix or delete them before the migration that adds enforcement. | | FOREIGN KEY constraint failed (19) | Enforcement is live and refused a write | Expected. Assert on it in the test for the delete path. | | on_delete is NO ACTION | No cascade, no SET NULL | The application owns the delete order. Test it. |

Common mistakes

Sign: A schema review confirms the foreign keys and orphan rows keep appearing in production.Cause: The review read the declaration. The SQLite library leaves PRAGMA foreign_keys at 0, so the declaration is documentation until a client sets it. The insert in step 2 above put customer_id 999 into a table whose schema forbids it, through the sqlite3 shell, with no error.
Sign: The application rejects the bad write and a psql-style shell session accepts it.Cause: The pragma is per connection, not per database, and it cannot be stored in the file. node:sqlite turns it on when it connects; the sqlite3 shell leaves it at 0. Both read the same file. The two clients here also link different SQLite versions, 3.51.3 and 3.50.6.
Sign: Enforcement is switched on in a release and nothing changes for the existing bad rows.Cause: Turning the pragma on affects future statements only. Rows already stored stay exactly where they are, and queries keep dropping them silently in joins. PRAGMA foreign_key_check is the only thing that looks backwards, and it has to be run on purpose.
Sign: A test sets the pragma inside a transaction and it has no effect.Cause: PRAGMA foreign_keys is a no-op while a transaction is open, and it reports no error when it is ignored. Set it immediately after connecting, before any BEGIN, and read it back to confirm.

What to check next

FAQ

How do I check a foreign key in SQL?

Read the declaration with PRAGMA foreign_key_list(<table>) in SQLite, or information_schema.referential_constraints in PostgreSQL and MySQL. Then check enforcement separately. In SQLite that is PRAGMA foreign_keys, and it answers per connection.

Why does SQLite default foreign keys to off?

Backwards compatibility. Enforcement arrived in version 3.6.19, and switching it on by default would have broken databases written before it existed. Clients override it: the shell here reported 0 and node:sqlite reported 1 against one file.

Does PRAGMA foreign_key_check need enforcement switched on?

No. It scans the stored rows against the declared keys whatever the pragma says, which is why it is the right tool on a restored backup or before a migration. It returns one row per violation and nothing when the data is clean.

Where should the pragma be set?

In the connection setup of every client, as the first statement after connecting. Setting it in one service leaves the others writing orphans into the same file, and setting it inside a transaction does nothing at all.

Verified

Verified by Maks Vernysqlite3 3.50.6, Android SDK platform-tools buildnode 22.23.2node:sqlite SQLite 3.51.3

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate9 minpublished updated Maks Verny