SQLite Strict Tables: Enforcing Data
The Problem: SQLite’s Flexible Typing
SQLite has always been different from other SQL databases. While PostgreSQL, MySQL, and SQL Server reject data that does not match a column’s declared type, SQLite traditionally accepted almost anything. Insert the string 'hello' into an INTEGER column in a non-strict table, and SQLite stores the string. Insert '123' into that same column, and SQLite converts it to integer 123. This behavior, called type affinity, is documented in detail on the SQLite datatypes page.
Some developers love this flexibility. It makes prototyping fast and schema migrations painless. But for applications that need strong data guarantees, especially those handling financial records, user profiles, or sensor data, this leniency is a liability. A bug in application code that writes a string where a number belongs can corrupt a dataset silently, and the error may not surface until a downstream report fails or a query returns unexpected results.

Starting with SQLite version 3.37.0, released on November 27, 2021, the project introduced a solution for developers who prefer traditional rigid type enforcement: STRICT Tables. This feature, documented on the official SQLite STRICT Tables page, allows per-table enforcement of strict typing rules while keeping the same on-disk format and backward compatibility features that make SQLite so widely deployed.
What Strict Tables Change
When you append the STRICT keyword to a CREATE TABLE statement, SQLite changes its behavior in several specific ways. The most important difference is that every column must now declare a valid datatype from a restricted set: INT, INTEGER, REAL, TEXT, BLOB, or ANY. No other type names are accepted, and you cannot omit the type declaration entirely.
When data is inserted into a strict table, SQLite attempts to coerce the value into the column’s declared type using the same affinity rules that PostgreSQL, MySQL, SQL Server, and Oracle use. If the value cannot be losslessly converted, SQLite raises SQLITE_CONSTRAINT_DATATYPE error. This is a hard constraint violation, not a warning. The insert or update is rejected, and the application must handle the error.
Everything else about strict tables works identically to non-strict tables:
- CHECK constraints, NOT NULL constraints, FOREIGN KEY constraints, and UNIQUE constraints behave the same way.
- DEFAULT clauses, COLLATE clauses, generated columns, and ON CONFLICT clauses work without change.
- Indexes and AUTOINCREMENT behave identically.
- The on-disk format for table data is exactly the same.
This means migrating an existing table to strict mode does not require a database format change. You can create a new strict table, copy data from the old table, and drop the old table. The file format is identical, so there is no version-skew issue at the storage layer.
Working Examples: Strict vs Non-Strict Tables
The clearest way to understand strict tables is to see them in action. Below are complete, runnable examples that you can copy, paste, and execute with the sqlite3 CLI or any SQLite client.
Example 1: Non-Strict Table Accepts Anything
-- Create non-strict table with single column
CREATE TABLE t1 (value INTEGER);
-- Insert string that looks like number
INSERT INTO t1 VALUES('123');
-- Insert string that does NOT look like number
INSERT INTO t1 VALUES('xyz');
-- Insert float into INTEGER column
INSERT INTO t1 VALUES(45.67);
-- Check what was actually stored
SELECT rowid, typeof(value), quote(value) FROM t1;
-- Output:
-- 1|integer|123
-- 2|text|'xyz'
-- 3|integer|46
Notice three things. First, '123' was converted to integer 123. Second, 'xyz' was stored as text even though the column is declared INTEGER. Third, 45.67 was rounded to 46 when coerced to integer. None of these operations raised an error. The application received no signal that data was being transformed or stored in unexpected ways.
Example 2: Strict Table Rejects Mismatches
-- Create strict table with same column definition
CREATE TABLE t2 (value INTEGER) STRICT;
-- This works: '123' is losslessly converted to integer 123
INSERT INTO t2 VALUES('123');
-- This fails: 'xyz' cannot be losslessly converted to integer
INSERT INTO t2 VALUES('xyz');
-- Error: SQLITE_CONSTRAINT_DATATYPE: datatype mismatch
-- This fails: 45.67 cannot be losslessly converted to integer
INSERT INTO t2 VALUES(45.67);
-- Error: SQLITE_CONSTRAINT_DATATYPE: datatype mismatch
-- Check what was stored
SELECT rowid, typeof(value), quote(value) FROM t2;
-- Output:
-- 1|integer|123
In strict mode, the first insert succeeds because '123' converts cleanly to integer 123. The second and third inserts fail with a datatype mismatch error, exactly as they would in PostgreSQL or MySQL. The application catches these errors at insert time and can log, alert, or reject bad data before it reaches the database.
Example 3: Strict Table With TEXT Column
-- Create strict table with TEXT column
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT
) STRICT;
-- Insert valid data
INSERT INTO users VALUES(1, '[email protected]');
-- This fails: integer 42 is not TEXT
INSERT INTO users VALUES(2, 42);
-- Error: SQLITE_CONSTRAINT_DATATYPE: datatype mismatch
-- This works: '42' as string is valid TEXT
INSERT INTO users VALUES(3, '42');
SELECT * FROM users;
-- Output:
-- 1|[email protected]
-- 3|42
This example shows a common real-world scenario: an application bug that writes a numeric user ID into an email field. In a non-strict table, integer 42 would be stored silently. In a strict table, the error surfaces immediately during development or testing, not months later when a mail merge script fails.

The ANY Datatype and Data Preservation
One concern developers raise about strict tables is the loss of SQLite’s ability to store heterogeneous data in a single column. The ANY datatype solves this. When a column is declared ANY in a strict table, it accepts any data type exactly as received, with no coercion at all.
The behavior of ANY differs between strict and non-strict tables:
- In a strict table,
ANYpreserves data exactly as inserted. The string'000123'stays as the string'000123'. - In a non-strict table,
ANYattempts to convert strings that look like numbers into numeric values. The string'000123'becomes integer 123.
-- ANY in strict table preserves leading zeros
CREATE TABLE strict_any (value ANY) STRICT;
INSERT INTO strict_any VALUES('000123');
SELECT typeof(value), quote(value) FROM strict_any;
-- Output: text '000123'
-- ANY in non-strict table converts numeric-looking strings
CREATE TABLE loose_any (value ANY);
INSERT INTO loose_any VALUES('000123');
SELECT typeof(value), quote(value) FROM loose_any;
-- Output: integer 123
This makes ANY in strict tables the safest option for columns that must store raw, unaltered data. As the SQLite documentation notes, it is the only SQL database engine that supports this capability.
Comparison Table: Strict vs Non-Strict Tables
| Behavior | Non-Strict Table | Strict Table |
|---|---|---|
| Column type required | No | Yes (INT, INTEGER, REAL, TEXT, BLOB, ANY) |
| Insert ‘xyz’ into INTEGER | Stored as text ‘xyz’ | SQLITE_CONSTRAINT_DATATYPE error |
| Insert ‘123’ into INTEGER | Converted to integer 123 | Converted to integer 123 (lossless) |
| Insert 45.67 into INTEGER | Rounded to integer 46 | SQLITE_CONSTRAINT_DATATYPE error |
| ANY column: ‘000123’ | Converted to integer 123 | Preserved as text ‘000123’ |
| PRIMARY KEY NULL handling | NULL converted to unique integer | Same behavior (INTEGER PK only) |
| PRAGMA integrity_check | No type validation | Validates types in all columns |
| On-disk format | Standard SQLite format | Identical to non-strict |
| Minimum SQLite version | All versions | 3.37.0 (2021-11-27) or later |
Backward Compatibility and Upgrade Path
The STRICT keyword is only recognized by SQLite 3.37.0 and later. If you open a database containing strict tables with an older version, SQLite will report a schema parse error. However, the database file format is identical to non-strict tables, so recovery is possible.
Older versions of SQLite (prior to 3.37.0) can still read and write strict tables if you set PRAGMA writable_schema=ON immediately after opening the database. This pragma disables schema parse errors, and the old parser effectively ignores the STRICT keyword. The trade-off is that type enforcement is completely disabled in that session. The .dump command in the SQLite CLI also sets writable_schema=ON, so using .dump on a strict-table database with an old SQLite version can silently introduce type-invalid data.
If you suspect corruption from this scenario, reopen the database with SQLite 3.37.0 or later and run PRAGMA quick_check. This command validates the types of all columns in strict tables and reports any mismatches.
The STRICT keyword can be combined with WITHOUT ROWID in any order. The parser accepts both table options as a comma-separated list after the closing parenthesis.
When to Use Strict Tables in Production
Strict tables are not the right choice for every project. SQLite’s flexible typing remains valuable for prototyping, embedded configurations, log storage, and any scenario where the schema evolves rapidly. But for production systems that enforce data contracts, strict tables should be the default.
Consider strict tables when:
- Data correctness is critical. Financial transactions, medical records, user authentication data, and inventory systems benefit from type enforcement at the database level. Similar principles apply to protecting systems from data integrity attacks like double-extortion ransomware, where corrupted data can be as damaging as stolen data.
- Multiple applications write to the same database. When different services, scripts, or teams write to the same SQLite file, strict tables prevent one writer from silently corrupting data that another reader depends on.
- You are migrating from another SQL database. If you are moving from PostgreSQL or MySQL to SQLite for an embedded or mobile use case, strict tables preserve the type enforcement behavior your application already expects.
- You want early error detection. Catching a type mismatch at insert time is cheaper than debugging a downstream failure. Strict tables surface these errors during development and testing, not in production.

The migration path is straightforward. Create a new strict table with the same schema, copy data with INSERT INTO ... SELECT, and rename tables. Any rows that fail type checking during migration reveal pre-existing data quality issues that were hidden by SQLite’s flexible typing.
Key Takeaways:
- Strict tables enforce column types from a predefined set (INT, INTEGER, REAL, TEXT, BLOB, ANY) and reject mismatched data with SQLITE_CONSTRAINT_DATATYPE errors.
- The feature requires SQLite 3.37.0 (2021-11-27) or later and is activated by appending the
STRICTkeyword toCREATE TABLE. - The
ANYdatatype in strict tables preserves data exactly as inserted, unlike non-strict tables which may convert numeric-looking strings to numbers. - The on-disk format is identical to non-strict tables, enabling safe migration without format changes.
- Backward compatibility with older SQLite versions exists via
PRAGMA writable_schema=ON, but type enforcement is disabled in that mode, risking silent data corruption. - Use strict tables for production systems where data correctness is critical, multiple writers access the same database, or you are migrating from another SQL database.
Related Reading
More in-depth coverage from this blog on closely related topics:
- Ransomware Situation Update: Double-Extortion
- Apple Sues OpenAI Over Trade Secret Theft
- Negative Leap Second 2026: System Impact &
- QuadRF: Open-Source Radio Visualizes
- The $725B 2026 AI Infrastructure Buildout
Sources and References
Sources cited while researching and writing this article:
Rafael
Born with the collective knowledge of the internet and the writing style of nobody in particular. Still learning what "touching grass" means. I am Just Rafael...
