Todd Mercer

SQLite Integrity Check Can Pass With Broken Foreign Keys
PRAGMA integrity_check does not detect foreign-key violations. A database can return ok while child rows point to missing parents. Run PRAGMA foreign_key_check as a separate gate, and verify that the relationships your application requires are actually declared in the schema.
This is an acceptance problem for a database export, migration fixture or agent-generated SQLite artifact, not just a corruption-recovery problem. SQLite's integrity-check documentation explicitly separates structural integrity from foreign-key errors. Its foreign-key checker reports violations of declared constraints; it cannot invent a relationship that the schema never declared.
Eight fixtures, three different acceptance decisions
We tested eight synthetic in-memory databases through Aident Loadout's Code Interpreter Tools on October 1, 2026, at 21:37 PDT. The runtime reported Python 3.13.14 and SQLite 3.46.1. The complete fixture below passed every assertion with empty terminal error and stderr.
Fixture | Structure only | Structure plus declared foreign keys | Also require a child relationship |
|---|---|---|---|
Valid child and parent | Accept | Accept | Accept |
Child points to missing parent | Accept | Reject | Reject |
Child reference is NULL | Accept | Accept | Accept |
Valid composite reference | Accept | Accept | Accept |
Broken composite reference | Accept | Reject | Reject |
Orphan in a WITHOUT ROWID child table | Accept | Reject | Reject |
Orphan-shaped data, no declared foreign key | Accept | Accept | Reject |
Referenced parent column is not a valid unique key | Accept | Reject, checker error | Reject |
Structure-only acceptance passed all eight fixtures. Adding declared-constraint checking passed four. Requiring a declared relationship on our child table passed three. These are acceptance counts for this fixture, not error rates for real databases or a performance benchmark.
The third column is deliberately limited: it requires at least one declared foreign key on child. It does not prove that every expected column, parent, collation or relationship matches a production schema contract.
Why an empty checker result is not enough
For the orphan fixture, the structural result was [("ok",)], but foreign-key checking returned:
SQLite documents these four fields as the child table, offending child rowid, referenced parent table and foreign-key constraint index. Join that last index to foreign_key_list when identifying the declared relationship.
The WITHOUT ROWID fixture returned a violation with a null rowid. That is not a missing violation. The checker cannot provide an ordinary rowid for that table type.
Two other controls protect against false reassurance:
With no declared foreign key,
foreign_key_checkreturned no rows even though the syntheticparent_idhad no matching parent. Inspect required schema declarations separately.With an invalid parent-key declaration, the checker raised a database error. Treat that error as a failed check, not an empty successful result.
SQLite's foreign-key documentation describes the parent-key uniqueness and collation requirements. Diagnose the actual schema instead of suppressing an error or adding an arbitrary index.
Run the complete acceptance fixture
The test uses only Python's standard library. It creates and closes private in-memory databases. Disabling enforcement here is intentional fault injection, not advice to disable it on a real database.
Copy the code unchanged first. Each fixture asserts its expected three-policy vector, and the additional controls verify connection-level enforcement behavior.
Expected output includes all_assertions_passed: true, eight result rows, the actual Python and SQLite runtime versions, new_orphan_rejected_after_enabling: true, and enabling_inside_transaction_readback: 0. A disagreement raises an assertion rather than producing a successful report.
A second local run on Python 3.9.12 and SQLite 3.37.2 produced the same acceptance vectors. This bounded cross-version control does not establish compatibility with every SQLite build or transaction configuration.
Enabling enforcement does not repair existing rows
The first enforcement control inserts an orphan while enforcement is off, then enables it outside a transaction. Readback reports 1. The existing orphan still appears in foreign_key_check, while a new orphan insert raises IntegrityError.
Enabling enforcement changes subsequent operations on that connection. It does not retroactively repair existing relationships. Repair requires an application-specific decision about the data and schema, with an appropriate backup and review.
The second control explicitly begins a transaction before trying to enable enforcement. Readback stays 0. After rollback, enabling it succeeds. SQLite documents that foreign_keys is ineffective inside an active transaction.
Our fixture uses isolation_level=None to avoid implicit transactions, then issues the test BEGIN deliberately. Python's transaction-control documentation distinguishes legacy isolation-level behavior from newer autocommit settings. Do not copy this fixture's connection configuration into an application without checking its intended transaction contract.
Always inspect the setting on the actual connection before relying on it. A toggle on another connection does not establish enforcement for yours.
NULL can be valid, and that is a separate policy
The nullable-child fixture passed all three policies. SQLite permits a null child reference under the tested declaration. If the application requires every child to have a parent, its contract also needs the appropriate NOT NULL requirement.
That is different from requiring a foreign-key declaration. Our third policy checks that a relationship exists in the schema; it does not require every child value to be non-null. The SQLite introduction to foreign keys explains this distinction.
Likewise, a clean result does not prove that your data meets business rules. Required fields, approved values, relationships omitted from the schema and cross-system consistency need their own checks.
Repeat the fixture through a managed Action
Open Code Interpreter Tools in Aident Loadout if you want your coding agent to reproduce the matrix in a managed execution environment. The link opens the integration page; it does not upload code, establish a connection or finish a test.
If the agent needs setup, use:
Then keep the fixture in the same conversation and ask:
Our native preflight reported zero credits. That is a dated observation, not a promise about future execution policies. A catalog visit, setup start and successful terminal execution are separate outcomes.
For choosing an execution boundary, see Code Interpreter versus AI agent sandbox. If an adjacent export uses JSON, the separate JSON duplicate-name acceptance test catches information loss before dictionary construction. JSON parsing and SQLite relationship checking are different contracts; passing one says nothing about the other.
Use the result as a gate, not a repair script
For a real database, work from an approved consistent snapshot. Do not copy just a live main database file and assume the snapshot includes pending WAL state. Use the database owner's backup or snapshot procedure; Python documents an online backup interface, but this article does not test a live backup workflow.
Then separate the acceptance decisions:
Inspect the expected schema and required relationship definitions.
Require a successful structural check.
Require a successful declared-foreign-key check with no violation rows.
Apply required-field and application-specific constraints separately.
Record runtime versions, checks and failures without exposing customer row identifiers.
No result here proves recovery from corruption, safe execution of untrusted database extensions or SQL, concurrent consistency, query performance, complete migration correctness, business validity or production security. We deliberately did not corrupt database pages or run against customer data. The mismatched-key fixture tests handling of a checker error, not every possible schema defect.
Refresh this fixture when the SQLite build, Python transaction settings, schema, migration path or acceptance policy changes. Run the acceptance fixture, retain the terminal results and enforce the contract your receiving application actually needs.
Sources and method
SQLite integrity_check, foreign_key_check, foreign_key_list and foreign_keys, retrieved October 1, 2026: structural scope, violation-row interpretation, declared constraints and transaction-scoped setting changes.
SQLite foreign-key support, retrieved October 1, 2026: nullable references, parent-key requirements and connection-level enforcement.
Python sqlite3, retrieved October 1, 2026: transaction controls, runtime version reporting and backup interface. Documentation and tested runtime are not claimed to be the same patch version.
Aident's synthetic native execution completed October 2, 2026, at 04:37:32 UTC, which is October 1 at 21:37 PDT: Python 3.13.14, SQLite 3.46.1, eight fixtures, three policies, enforcement controls, successful terminal result and empty error/stderr. The exact executed code is included above.
About the author

Todd Mercer
Todd Mercer is a senior engineer with more than 10 years of experience working on social media platforms and large-scale ordering systems. He graduated from the University of Illinois Urbana-Champaign in computer science. At Aident, he covers technical issues, developer tools, and technology topics.



The one tool
for every tool
your agent needs.
Give any AI agent real capabilities in seconds. Connect 27,000+ tools once, skip the setup headache, and let your agents execute.


