SQLite accepted an invalid child row even though the table definition contained a foreign key. The difference was not the schema. It was the connection.
I ran a small in-memory experiment with Python 3.14.7 and SQLite 3.53.1. Two databases created the same parent and child tables, with child.parent_id REFERENCES parent(id). Each then attempted to insert a child pointing at parent 404, which did not exist.
db.execute("PRAGMA foreign_keys").fetchone()[0] # read the setting
db.execute("PRAGMA foreign_keys = ON")
db.execute(
"INSERT INTO child (id, parent_id) VALUES (1, 404)"
)
The recorded results were sharply different:
- With enforcement left alone, the connection reported
0before and after the check. The insert committed, and the row(1, 404)was present. - After enabling enforcement, the setting changed from
0to1. The same insert raisedFOREIGN KEY constraint failed, and the child table stayed empty.
A final check used two simultaneous connections. Enabling the setting on the first produced 1 there and 0 on the second. The setting was connection-local; it was not a property that the schema declaration silently imposed on every connection.
That is the practical trap. A migration can leave a perfectly readable REFERENCES clause while an application connection still accepts orphan rows. A process or connection pool therefore needs to establish the setting for each connection and read it back as part of initialization. Checking one successful connection is not enough to prove that all of them enforce the same rule.
This was a constructed in-memory demonstration, not a product test. It exercised one Python binding and SQLite build; it did not model a pool, existing orphan rows, concurrency, or another driver. It does establish the narrower result shown here: in this setup, foreign-key enforcement was runtime connection state. If SQLite is carrying relational data, treat that state as something to initialize and verify, not something to infer from the table definition.