Discrepancies in Column Type Reporting with SQLite’s table_info Pragmas
Column Type Reporting Inconsistencies in Views: Declared Type vs. Type Affinity
Issue Overview: Discrepancies in PRAGMA table_info Output for Aliased vs. Cast Columns
When working with SQLite views that involve column aliasing and explicit type casting, developers may encounter inconsistencies in the reported column types via PRAGMA table_info. For example, consider a table t with a column g of a custom type GENDER. A view foo is created with two columns: g1 (an alias of g) and g2 (an explicit cast of g to GENDER). Running PRAGMA table_info(foo) yields conflicting results: g1 is reported as type GENDER, while g2 is reported as NUM. This contradicts the expectation that both columns should reflect the declared type GENDER.
This discrepancy arises from SQLite’s handling of declared types (the type names specified during column creation) versus type affinity (the internal storage class determined by SQLite). SQLite does not enforce strict type checking; instead, it assigns a type affinity to columns based on declared types. Type affinity governs how values are stored and manipulated but does not inherently validate or restrict data types. The PRAGMA table_info command returns declared types for base tables but behaves differently for views, especially when columns are derived from expressions like CAST.
In views, SQLite attempts to infer column metadata from the underlying query. For aliased columns that directly reference a base table column (e.g., g as g1), the declared type from the base table propagates to the view. However, for columns generated via expressions (e.g., CAST(g as GENDER) as g2), SQLite resolves the type affinity based on the cast’s target type. Since GENDER is not a native SQLite type, it defaults to NUMERIC affinity, which is abbreviated as NUM in PRAGMA table_info output. This mismatch between declared types and inferred affinities creates confusion when validating schemas programmatically, as seen in tools like validate_gpkg.py.
Possible Causes: SQLite’s Type Resolution Logic and Metadata Propagation
-
Type Affinity Inference for Non-Standard Types
SQLite recognizes five storage classes:NULL,INTEGER,REAL,TEXT, andBLOB. Type affinity is derived from declared types using pattern matching. For example, a declared type containing "INT" (case-insensitive) maps toINTEGERaffinity. Non-standard types likeGENDERdefault toNUMERICaffinity. When a column is cast to a non-standard type, SQLite uses this pattern-matching logic to assign affinity, which may not align with the declared type string. -
Metadata Handling in Views
Views in SQLite are virtual tables whose schema is dynamically derived fromSELECTstatements. For columns directly aliased from base tables (e.g.,g as g1), SQLite propagates the original declared type. For computed columns (e.g.,CAST(g as GENDER) as g2), SQLite infers the type affinity from the expression. TheCASToperation explicitly defines a target type, but sinceGENDERis non-standard, SQLite resolves it toNUMERICaffinity. ThePRAGMA table_infooutput reflects this inferred affinity rather than the declared type string. -
CAST Operator Behavior
TheCAST(expr AS type)operator in SQLite convertsexprto the storage class associated withtype. However, the operator does not preserve the declared type string unlesstypematches a recognized affinity. For example,CAST(g AS INTEGER)would reportINTEGERinPRAGMA table_info, butCAST(g AS GENDER)resolves toNUMERICaffinity, reported asNUM. This leads to inconsistencies when comparing aliased columns to cast columns.
Troubleshooting Steps, Solutions & Fixes: Aligning Expectations with SQLite’s Behavior
-
Understand SQLite’s Type Affinity System
Before relying onPRAGMA table_info, familiarize yourself with how SQLite assigns type affinities:- Declared types containing
INT→INTEGERaffinity. - Declared types containing
CHAR,CLOB, orTEXT→TEXTaffinity. - Declared types containing
BLOB→BLOBaffinity. - Declared types containing
REAL,FLOA, orDOUB→REALaffinity. - All other types →
NUMERICaffinity.
Non-standard types like
GENDERorLINESTRINGwill always resolve toNUMERICaffinity. Tools expecting specific type strings must account for this behavior. - Declared types containing
-
Use Standard Type Names for CAST Operations
To ensurePRAGMA table_inforeports consistent types for cast columns, use standard SQLite type names (e.g.,TEXT,INTEGER). For example:CREATE VIEW foo AS SELECT g as g1, CAST(g AS TEXT) as g2 -- Use standard type FROM t;This ensures
g2is reported asTEXTinPRAGMA table_info. If non-standard types are required, consider adding a comment or documentation to clarify the expected affinity. -
Validate Schemas Using Alternative Methods
ThePRAGMA table_infocommand is insufficient for validating non-standard types. Instead:- Query
sqlite_masterto retrieve the original view or table definition:SELECT sql FROM sqlite_master WHERE name = 'foo';This returns the original
CREATE VIEWstatement, preserving declared types. - Use application-level validation to compare column types against expected strings.
- Query
-
Modify GeoPackage Validation Logic
Tools likevalidate_gpkg.pythat enforce GeoPackage standards (which often require specific geometry types likeLINESTRING) must avoid relying solely onPRAGMA table_info. Instead:- Parse the original
CREATE TABLEorCREATE VIEWstatements fromsqlite_master. - Use regular expressions to extract declared types directly from the schema SQL.
- For views, analyze the underlying
SELECTstatements to infer types from aliases and casts.
- Parse the original
-
Leverage SQLite’s Extension Framework
For advanced use cases, develop a SQLite extension that overrides type affinity resolution. This allows custom logic for handling non-standard types. However, this approach requires deep integration with SQLite’s internals and is not portable across environments. -
Educate Stakeholders on SQLite’s Flexibility
SQLite’s type system prioritizes flexibility over strictness. Document this behavior for teams expecting rigid type enforcement. Emphasize thatPRAGMA table_inforeflects SQLite’s internal affinity resolution, not necessarily the original declared types.
By aligning validation workflows with SQLite’s type affinity rules and leveraging schema introspection beyond PRAGMA table_info, developers can mitigate inconsistencies and ensure robust compatibility with tools expecting strict type reporting.