*** DRAFT ***
The Advantages Of Flexible Typing
Table Of Contents

1. Introduction

SQLite provides developers with the freedom to store content in any desired format, regardless of the declared datatype of the column. Some people find this feature troublesome. Some developers are shocked to discover that it is possible to insert text into a column marked INTEGER.

This article advocates in favor of the flexible type rules in SQLite.

2. About Flexible Typing

Details regarding the flexible type system of SQLite are found in the separate Datatypes In SQLite document. Here is a quick summary:

3. Cases Where Flexible Typing Is Useful

Some readers, upon first encountering flexible typing in SQLite, ask themselves "how could this ever be useful?" Here is an attempt to answer that question:

3.1. Attribute tables

Many applications, especially those that use SQLite as an application file format, need a place to store miscellaneous attributes such as thumbnail images (as BLOB values), short pieces of text (such as the user's name), as well as numeric, date, and JSON values. It is convenient to create a single table to handle this storage:

CREATE TABLE attribute(name TEXT PRIMARY KEY, value) WITHOUT ROWID;

Without flexible typing, such a table would need to be more complex, with separate columns for each possible type of data. Flexible typing of the "value" column makes the table conceptually simpler, more space-efficient, and easier to access and update.

In the Fossil version control system, each repository has a CONFIG table that is used to store all kinds of settings with every possible datatype. The user-specific configuration file for Fossil (the ~/.fossil file) is a separate SQLite database that contains a single attribute table holding the user-specific state across all repositories.

Some applications use an SQLite database as a pure key-value store. The database schema contains a single table that looks something like this:

CREATE TABLE storage(name TEXT PRIMARY KEY, value ANYTHING);

3.2. The "value" column output from the json_tree virtual tables

The json_tree and json_each table-valued functions that are built into SQLite both have a "value" column that can hold values of type INTEGER, REAL, or TEXT depending on the type of the corresponding JSON field. For example:

SELECT typeof(value) FROM json_each('{"a":1,"b":2.5,"c":"hello"}');

The query above returns three rows of one column with values "integer", "real", and "text", respectively.

3.3. Storage for dirty data

Analysts sometimes encounter CSV files where some columns contain a mixture of integer, real, and text data. CSV files that are obtained from Excel spreadsheet exports commonly have this trait, for example. When importing such "dirty data" into an SQL database, it is convenient to have flexibly typed columns to import into.

Dirty data is not restricted to CSV files coming out of Excel, of course. There are many data sources in which a single field might contain a mix of types. For example, a data column might contain the number of seconds since 1970 sometimes, or a text date string in other cases. It is desirable to clean up these inconsistent representations, but at the same time it is convenient to be able to store all the different representations in the same column of the intermediate database while the cleanup is underway.

3.4. Dynamic programming languages

SQLite began as a TCL extension that later escaped into the wild. TCL is a dynamic language in the sense that the programmer does not need to be aware of datatypes. Under the hood, TCL keeps careful track of the datatype of every value, but to the developer and user of a TCL program, everything looks like a string. Flexible typing is a natural fit for use with dynamic programming languages like TCL and others, since with a dynamic programming language, you can not always predict in advance what datatype a variable will hold. So when you need to store the value of that variable into the database, having a database that supports flexible typing makes storage much easier.

3.5. Data typename cross-compatibility

Every SQL database engine seems to have its own unique set of supported datatype names:

The fact that SQLite will accept any of these names as a valid typename, and let you store any kind of content into the column, increases the chances that a script written to run on some other SQL database engine will also work in SQLite.

The desire for interoperability with other systems was a key reason why the original design SQLite supports flexible typing.

3.6. Expand and/or change the meaning of columns without migration

Software appliations typical grow and expand over time. Sometimes datatypes need to evolve to cope. For example, a project might start out storing unix timestamps in seconds since 1970, but later on realize that they need millisecond precision rather than just second precision. A database with flexible typing, like SQLite, accommodates this enhancement without any modifications whatsoever. New records can be inserted into the timestamp field as a floating point value (even though the column definition says "INT") and that will just work. No application code changes needed. Legacy builds of the software continue to operate without change. The older versions of the software presumably extract the timestamp using sqlite3_column_int64() which converts the value to integer, and so while they older code does not see the milliseconds at the end, it still gets the integer timestamp it expects. Meanwhile, newer versions of the software can use sqlite3_column_double() to get the full precision of the timestamp. Older database entries stored as integers get converted into floating point as they are extracted. So you have full backwards and forwards compatibility of the data and increased capabilities in newer versions of the application. With flexible typing, this "just works".

Another example is when SQLite was enhanced to support JSONB (a binary encoding of JSON). JSONB is stored as a BLOB whereas JSON is stored as TEXT. JSONB is about twice as fast. Applications can transition toward using JSONB without have to do a migration or a full database file rewrite and upgrade. Older rows that still contain text JSON continue to work fine. Newer values are inserted as JSONB BLOBs for improved performance and reduced storage size. You get steady improvement in performance without the disruption of a datatype migration. With flexible typing, this "just works".

See Appendix A below for a real-world example of when the author of this article used an overly rigid data type in a schema design, which caused problems years later when the application needed to evolve.

3.7. The SQL Language is evolving toward flexible typing

Decades ago, SQL implementations only had fixed-length types, for example: "CHAR(20)". The text value had to be exactly 40 bytes in length. (Shorter values would get padded with spaces automatically.) Later, it became popular to add support for "VARCHAR(20)" and similar, which dispensed with the space padding, but still set an upper bound on the size of the text. Most modern SQL implementations support "TEXT" or similar for text values of unbounded size.

The trend is toward more flexible data types. Through experience, developers have found that (for example) some people have long names that won't necessarily fit in a VARCHAR(20) and so it makes the application more resilient to just make the name field TEXT. SQLite just happens to be ahead of the curve.

4. Perceived Disadvantages of Flexible Typing (With Rebuttals)

The following perceived disadvantages of flexible typing were gleaned and compiled from countless posts on Hacker News and Reddit and similar forums where developers discuss these sorts of things. If you can think of other reasons why flexible typing is a bad idea, please contact the SQLite developers or leave a post on the SQLite Forum so that your idea can be added to the list.

4.1. We've never done it that way before

Many skeptics of flexible typing simply express shock and disbelief, without offering any rationale for why they think flexible typing is a bad idea. Without supporting arguments, one must assume their reason for not liking flexible typing is that it is different from what they are used to.

Presumably, many developers who are aghast at SQLite's flexible typing feel this way because they have just never encountered anything like it before. All prior exposure to databases and especially SQL databases has involved rigid typing, and the readers mental model of SQL includes rigid typing as a fundamental feature. Flexible typing upsets their world-view.

Yes, flexible typing is a new way of thinking about data in an SQL database. But new is not necessary bad. Sometimes, and I think especially in the case of flexible typing, innovation leads to improvement.

4.2. Rigid type enforcement helps prevent application bugs

It has become a point of doctrine among many programmers that the best way to prevent application bugs is strict type enforcement. But I find no evidence in support of this.

To be sure, strict type enforcement does help prevent some kinds of bugs in lower-level languages like C and C++ that present a model that is close to machine hardware. But this does not seem to be the case for higher-abstraction languages in which all data is passed around in a "Value" superclass of some kind which is subclassed for the various lower-level data types. When everything is a Value object, specific datatypes cease to be important.

This technical note is authored by the original author of SQLite. I have been writing TCL programs for 27 years. TCL has no type enforcement whatsoever. The "Value" class in TCL (called Tcl_Obj) can hold many different datatypes, but it presents the content to the program and to the application user as a string. And I've had a lot of bugs in those TCL programs over the years. But I do not recall a single instance where the bugs might have been caught by a rigid type system. I have also written a lot of C code over a span of 35 years, not the least of which is SQLite itself. I have found the type system in C to be very helpful at finding and preventing problems. For the Fossil Version Control System, which is written in C, I have even implemented supplemental static analysis programs that scan the Fossil source code prior to compilation, looking for problems that compilers miss. This works well for compiled programs.

The SQL language model is a higher-level abstraction than C/C++. In SQLite, every data item is stored in memory as an "sqlite3_value" object. There are subclasses of this object for strings, integers, floating-point numbers, blobs, and other representations. Everything is passed around inside the SQL language implemented by SQLite as "sqlite3_value" objects so the underlying datatype does not really matter. I have never found rigid type enforcement to be helpful in languages like TCL and SQLite that have a single "Value" superclass used to represent any data element. Fossil makes extensive use of SQLite in its implementation. There have been many bugs in Fossil over its 14-years history, but I cannot recall a single bug that might have been prevented by rigid type enforcement in the SQLite. Some C-language bugs might have been caught by better type enforcement (which is why I wrote the supplemental source code scanners), but no SQL bugs.

Based on decades of experience, I reject the thesis that rigid type enforcement helps prevent application bugs. I will accept and believe a slightly modified thesis: Rigid type enforcement helps to prevent applications bugs in languages that lack a single top-level "Value" superclass. But SQLite does have the single "sqlite3_value" superclass, so that proverb does not apply.

4.3. Rigid type enforcement prevents data pollution

Some people contend that if you have rigorous constraints on the schema, and especially strict enforcement of column datatypes, this will help prevent incorrect data from being added to the database. This is not true. It is true that type enforcement might help prevent egregiously incorrect data from getting into the system. But type enforcement is no help in prevent subtly incorrect data from being recorded.

So, for example, rigid type enforcement can successfully prevent the customer name (text) from being inserted into the integer Customer.creditScore column. On the other hand, if that mistake occurs, it is very easy to spot the problem and find all affected rows. But type enforcement is no help in preventing a bug where the customer family name and given name are reversed, since both are text fields.

By suppressing easy-to-detect errors and passing through only the hard-to-detect errors, rigid type enforcement can actually make it more difficult to find and fix bugs. Data errors tend to cluster. If you have 20 different data sources, most of the data errors will usually come from just 2 or 3 of those sources. The presence of egregious errors (such as text in an integer column) is a convenient early warning signal that something is amiss. The source of the problem can be tracked quickly and extra scrutiny applied to the source of the egregious errors, thus hopefully also fixing the subtle errors too. When egregious errors are suppressed, you lose an important signal that helps you to detect and fix the subtle errors.

Data errors are inevitable. They will happen regardless of how much type checking is done. Rigid type enforcement can catch only a small subset of those cases - the most obvious cases. It does nothing to help find and fix the more subtle cases. And, by suppressing the signal of which data sources are problematic, it can sometimes make the subtle errors more difficult to locate.

4.4. Other SQL database engines don't work this way

Because SQLite is less restrictive and allows you to do more things, SQL scripts that work on other database engines will also usually work on SQLite, but scripts written initially for SQLite might not work on more restrictive database engines. This can cause problems when developers use SQLite for prototyping and testing and then migrate their application to a more restrictive SQL engine for deployment. If the application was (unintentionally) taking advantage of the flexible typing available in SQLite, then it will fail when migrated.

People use this problem to argue that SQLite should be more restrictive about datatypes. But you could just as easily turn that argument around and say that other database engines should be more flexible with regard to datatypes. The application was working correctly under SQLite, prior to being migrated, after all. If rigid type enforcement is really all that useful, why did it break an application that was previously working?

5. If You Insist On Rigid Type Enforcement...

As of SQLite version 3.37.0 (2021-11-27), SQLite supports this development style using STRICT tables.

If you find a real-world case where STRICT tables prevented or would have prevented a bug in an application, please post a message to the SQLite Forum so that we can add your story to this document.

(Update 2026-07-12): After nearly 5 years, nobody has yet shown me a single case where rigid type enforcement prevented an application bug. I have read many strident and emotional appeals in support of rigid type enforcement, but have seen no actual examples or real-world data. I have been confronted with a lot of doctrine, but no actual evidence.

6. Embrace Freedom

If flexible typing in an SQL database is a new concept to you, I encourage you to give it a try. It probably will not cause you any problems and it might make your program simpler and easier to write and maintain. I think that even if you are skeptical at first, if you will just give flexible typing a try, you will eventually come to realize that it is a better approach and will start encouraging other database vendors to support at least an ANY datatype if not complete SQLite-style type flexibility.

Most of the time, flexible typing does not matter because a column stores a single well-defined type. But occasionally you will run across situations where having a flexible type system makes the solution to your problem cleaner and easier.

7. Appendix A

Here is an example of where the lead developer of SQLite, and the author of this document, used strict constraints on a schema design which caused problems 10 years later.

The source code for SQLite is stored in a version control system called Fossil, which is Git-like, but stores its content in an SQLite database rather than a bespoke "packfiles". Like Git, Fossil started out using SHA1 hashes to identify content files. This worked fine for years. Then in 2017, the "SHAattered" attack was revealed, showing that SHA1 was no longer secure. In response, Fossil was enhanced to support SHA3-256 hashes in addition to SHA1 hashes. Content that had already been in repositories for years continued to be identified by the original SHA1 hash, but new check-ins got a longer and more secure SHA3-256 hash.

Unfortunately, in the original schema design, the hash was stored in a field that had a CHECK constraint that said "length(uuid)==40". That CHECK constraint prevented the 64-character SHA3-256 hash from being stored. So some gnarly migration code to change the CHECK constraint had to be run every time a repository database is opened. And that code had to be run every time a repository database was opened (which means every time the repository was touched) since the Fossil program does not know if the particular repository being opened has ever been migrated before. (The automatic migration was discontinued in 2022 under they theory that five years was long enough to update every legacy repository.)

This migration was due to an explicit CHECK constraint, not a datatype issue, so it is not 100% applicable datatype flexibility. However, the lesson still applies: Just because you think you know the bounds on a field in a database in 2007 does not mean that you won't need to expand the scope of that field in 2017, so keeping things flexible can improve resilience.

The author of this article thought he was using "good engineering practice" back in 2007 when it put in the "CHECK( length(uuid)==40 )" constraint in the Fossil repository schema. However, that CHECK constraint ended up causing more problems than it solved. Rigid type enforcement can be similar. It seems like a "safe" thing to do early on, but years later can end up causing more grief than it prevents.

SQLite enables you to impose rigid and unnecessary constraints on your data store if that is what you want to do. But, unlike other SQL implementations, SQLite does not force you to do so, and does not do so as its default behavior. This is a feature, not a bug.

7.1. It could have been much worse!

When SHAttered appeared, it took a few weeks to enhance Fossil so that it could support both SHA1 and SHA3-265 in the same repository. That upgrade required a schema migration, and so was more difficult than it needed to be, but it all worked out in the end. The situation at Git was far worse. Git does not use SQL and does not have a "schema" in the same sense as Fossil. The use of SHA1 is baked into the bespoke, binary Git file format. A new and incompatible file format needed to be designed. This took literally years, and to this day it is still not possible to host both SHA1 and SHA2 hashes in the same repository. Nine years after SHAttered, most Git repositories continue to use the older and less secure SHA1 hash algorithm.

The problems created for Git by SHAttered are rooted in the rigid and inflexible way in which Git stores hashes. If there had been some datatype flexibility in the original design of the Git file format, then it probably would have been able to adapt and deal with the SHAttered disruption much faster and with more grace.

Having wiggle room in the design of a file format is important for long-term viability in a software application. The datatype flexibility of SQLite provides that wiggle room. I implore the reader to heed that lesson.

This page was last updated on 2026-07-12 10:22:59Z

*** DRAFT ***