PRAGMA table_info Column Type Case Sensitivity Change After SQLite 3.36 to 3.44 Upgrade
Behavioral Shift in PRAGMA table_info Output for Column Types
The upgrade from SQLite 3.36 to 3.44 introduced a non-backward-compatible change in the behavior of the PRAGMA table_info
command. Specifically, the case sensitivity of column type names returned by this pragma shifted for standardized SQL data types. Prior to SQLite 3.37.0, the PRAGMA table_info
output returned column type names exactly as declared in the CREATE TABLE
statement. Starting with SQLite 3.37.0 (released November 27, 2021), standardized type names (e.g., INTEGER
, TEXT
) are converted to uppercase in the PRAGMA table_info
output, even if declared in lowercase or mixed case. This change was implemented to optimize storage efficiency but inadvertently broke unit tests and systems that relied on lowercase type names returned by the pragma.
The discrepancy arises from SQLite’s internal handling of standardized type names. When a column type matches a predefined SQL standard type (e.g., INTEGER
, REAL
, TEXT
, BLOB
), SQLite now normalizes the type name to uppercase in the PRAGMA table_info
result set. Non-standard or user-defined type names retain their original case. This optimization reduces storage overhead by replacing standardized type strings with internal integer identifiers, which are later resolved to uppercase strings during pragma execution. The change was not explicitly documented in SQLite’s release notes, leading to unexpected test failures during upgrades.
Technical Roots of the PRAGMA table_info Case Normalization
The root cause of this behavioral shift lies in SQLite’s internal type resolution logic, modified in commit d2da62a9 (November 2021). The commit introduced a space-saving optimization where standardized column type names are stored as integer identifiers instead of raw strings. During schema parsing, SQLite checks if a declared column type matches a known standard type (e.g., integer
, TEXT
, Real
). If a match is found, the type name is replaced with an internal eType
identifier, and the original string is discarded. When PRAGMA table_info
reconstructs the column information, it maps these internal identifiers back to their uppercase standard equivalents, overriding the original declaration’s case.
This optimization impacts backward compatibility because SQLite’s schema introspection pragmas no longer reflect the exact text of CREATE TABLE
statements for standardized types. For example:
CREATE TABLE t1 (a INteGEr, b varchar(10));
PRAGMA table_info(t1);
In SQLite ≤3.36.0, this returns INteGEr
and varchar(10)
for the column types. In SQLite ≥3.37.0, it returns INTEGER
(uppercase) and varchar(10)
(unchanged, as varchar
is non-standard). The change was validated against SQLite’s own test suite, which required updates to accommodate the new behavior. However, external projects relying on the original case-sensitive output were not explicitly warned, leading to undetected dependencies on the pre-3.37.0 behavior.
Mitigating Test Failures and Aligning with SQLite’s Type Handling
To resolve unit test failures and align code with SQLite’s updated behavior, follow these steps:
Audit All Uses of
PRAGMA table_info
and Schema Introspection
Identify every instance wherePRAGMA table_info
or similar introspection tools (e.g.,sqlite_schema
table queries) are used to validate column types. Replace case-sensitive string comparisons with case-insensitive checks or direct affinity matching. For example, instead of:assert col_type == "integer"
Use:
assert col_type.lower() == "integer"
Normalize Column Type Names in Application Logic
If the originalCREATE TABLE
statements use non-standard type names (e.g.,STR
instead ofTEXT
), refactor them to use SQL standard types. This ensures consistency with SQLite’s internal normalization. For legacy systems where schema changes are impractical, use a lookup table to map application-specific type names to their standardized equivalents during introspection.Leverage SQLite’s Type Affinity Directly
SQLite’s type affinity system determines how values are stored and coerced. Instead of relying on the string returned byPRAGMA table_info
, derive the affinity programmatically usingtypeof()
or by queryingsqlite3_column_type()
in the C API. For example:SELECT typeof(col1), typeof(col2) FROM t1 LIMIT 1;
This returns the storage class (e.g.,
integer
,text
), which is always lowercase and unaffected by the pragma’s case changes.Update Unit Tests to Reflect SQLite’s Guarantees
SQLite does not guarantee the preservation of type name case in pragmas. Rewrite tests to validate behavioral contracts (e.g., data insertion/retrieval, type coercion) rather than implementation details like type name casing. For example, test that inserting a string into anINTEGER
column triggers type affinity rules, rather than asserting the column’s type name is lowercase.Monitor SQLite’s Development and Release Notes Proactively
Subscribe to SQLite’s changelog and review commits affecting schema introspection. While SQLite prioritizes backward compatibility, optimizations impacting undocumented behaviors can occur. Use thesqlite3_libversion()
function orSELECT sqlite_version()
query to log the SQLite version in application diagnostics, aiding in post-mortem analysis of compatibility issues.Implement Version-Specific Workarounds If Necessary
For systems requiring compatibility across multiple SQLite versions, conditionally handle type name casing based on the detected SQLite version:import sqlite3 conn = sqlite3.connect(":memory:") sqlite_version = conn.execute("SELECT sqlite_version()").fetchone()[0] if sqlite_version >= "3.37.0": expected_type = "INTEGER" else: expected_type = "integer"
By aligning application logic with SQLite’s type affinity rules and avoiding dependencies on undocumented pragma behavior, systems can achieve robustness across SQLite versions. The key is to treat PRAGMA table_info
as a diagnostic tool rather than a contractual API, recognizing that its output may evolve to reflect internal optimizations.