Зачем нужны STRICT-таблицы
SQLite славится своим вольным отношением к типам данных. Объявляешь колонку как INTEGER, суёшь туда строку "hello" — а SQLite только пожмёт плечами и спокойно сохранит её. Такая гибкость была осознанным архитектурным решением ещё в 90-х, но разработчиков, привыкших к Postgres или MySQL, она сбивает с толку и удобно прячет баги.
Именно эту проблему и решают STRICT-таблицы, появившиеся в SQLite 3.37. Строгая типизация включается отдельно для каждой таблицы, и с этого момента типы колонок начинают означать ровно то, что в них написано.
Ключевое слово STRICT ставится после закрывающей скобки. Всё остальное выглядит как обычный CREATE TABLE. Разница проявляется в тот момент, когда вы пытаетесь записать в колонку значение неподходящего типа.
Что на самом деле проверяют STRICT-таблицы
В обычной таблице SQLite задействует механизм type affinity: он пытается привести значение к объявленному типу, а если не получается — сохраняет его как есть. В STRICT-таблице такое несоответствие сразу превращается в ошибку.
Попробуйте то же самое на обычной (не-STRICT) таблице — и третий INSERT спокойно пройдёт: SQLite без зазрения совести сохранит строку 'oops' в колонке, которую вы объявили как INTEGER. А через пару месяцев агрегирующий запрос выдаст какую-то дичь, и вы убьёте полдня на поиски причины. STRICT-таблицы заставляют ошибку всплыть прямо при вставке — там, где её ещё легко починить.
Вот какую ошибку вы увидите:
Ошибка выполнения: невозможно сохранить значение TEXT в столбец INTEGER accounts.balance
Чётко, сразу, не отвертишься.
Пять допустимых типов
В STRICT-таблицах разрешены только пять типов:
INTEGER— целые числа.REAL— числа с плавающей точкой.TEXT— строки.BLOB— сырые байты.ANY— любой тип, без приведения.
И всё. Привычные алиасы, которые SQLite обычно проглатывает без вопросов — VARCHAR(255), DOUBLE, BOOLEAN, DATETIME, INT, — внутри STRICT-таблицы вызовут ошибку:
Ошибка:
Parse error: unknown datatype for bad.name: "VARCHAR(255)"
Чтобы это починить, используйте одно из пяти канонических имён. VARCHAR(255) превращается в TEXT, DATETIME — тоже в TEXT (SQLite всё равно хранит даты как ISO-строки), а BOOLEAN становится INTEGER (со значениями 0 и 1).
Тип ANY как лазейка
ANY — единственный тип, который разрешает колонке в STRICT-таблице хранить разнородные значения. Удобно, например, для универсальной колонки value в таблице ключ-значение:
ANY ведёт себя в STRICT-таблицах по-особенному: значения сохраняются без того приведения типов, которое это же слово подразумевает в обычных таблицах. Строка '100' остаётся строкой, а целое число 100 — целым числом. Вызовы typeof() в запросе выше это наглядно подтверждают.
В обычной (не-STRICT) таблице колонка с аффинностью ANY привела бы строки, похожие на числа, к числовому типу. А STRICT сохраняет исходный тип ровно таким, какой он есть.
STRICT и PRIMARY KEY
Есть один тонкий момент: в обычной таблице объявление INTEGER PRIMARY KEY — это особый случай. Такая колонка становится псевдонимом для rowid и принимает только целые числа. Остальные варианты первичного ключа ведут себя свободнее.
В STRICT-таблице тип колонки проверяется строго, независимо от того, является ли она первичным ключом:
Второй INSERT падает с ошибкой. В обычной таблице (без STRICT) число 42 молча улеглось бы в колонку TEXT первичного ключа. А здесь SQLite честно скажет, что так делать нельзя.
Совмещение STRICT и обычных таблиц
Режим STRICT включается на уровне таблицы, а не базы. В одном и том же файле спокойно уживаются строгая таблица users и расслабленная events. Внешние ключи между ними работают ровно так же, как и без STRICT.
В таблице events нет ни STRICT, ни объявленного типа для payload, поэтому туда можно класть что угодно. Иногда полезно — но как поведение по умолчанию это рискованно. Оставляйте «бестиповое» хранение для случаев, когда вам действительно нужна колонка-«мешок всего подряд».
Когда использовать STRICT
Для новых схем ответ — «практически всегда». Цена смешная: одно ключевое слово на таблицу плюс необходимость помнить пять канонических имён типов. А выгода в том, что баги, которые обычно тихо накапливаются в данных, всплывают сразу — на том самом INSERT, который их породил.
STRICT можно не включать, если:
- Вы поддерживаете старую базу SQLite, где существующая схема завязана на нестрогую типизацию.
- Вы целитесь в SQLite старше версии 3.37 (октябрь 2021) — там этого ключевого слова просто нет.
- Вам реально нужно, чтобы колонка хранила значения разных типов. Но даже в этом случае лучше взять
STRICT-таблицу с колонкойANY, чем обычную таблицу: так всё остальное остаётся под контролем.
Короткий чек-лист для перевода обычной таблицы в STRICT:
- Замените
VARCHAR,CHAR,NVARCHARнаTEXT. - Замените
DOUBLE,FLOAT,NUMERICнаREAL. - Замените
BOOLEAN,BIT,TINYINTнаINTEGER. - Замените
DATETIME,TIMESTAMP,DATEнаTEXT(илиINTEGER, если храните unix-таймстампы). - Допишите
STRICTпосле закрывающей скобки.
Дальше: первичные ключи
STRICT-таблицы наводят порядок в том, как колонки хранят данные. Следующий логичный шаг — навести порядок в том, какая колонка идентифицирует строку. У первичных ключей в SQLite есть пара особенностей (особенно вокруг INTEGER PRIMARY KEY и rowid), которые стоит понимать до того, как вы начнёте проектировать реальную схему.
Часто задаваемые вопросы
Что такое STRICT-таблица в SQLite?
Это таблица, в которой объявленный тип колонки реально соблюдается: если колонка INTEGER, SQLite отклонит любое значение, кроме целого числа или NULL. Включается режим просто — добавляете ключевое слово STRICT сразу после закрывающей скобки в CREATE TABLE. Без него SQLite работает по принципу type affinity: где может — приведёт значение, где не может — сохранит как есть.
Какие типы можно использовать в STRICT-таблице?
Всего пять: INTEGER, REAL, TEXT, BLOB и ANY. Привычные алиасы вроде VARCHAR, DOUBLE, BOOLEAN или DATETIME, которые в обычных таблицах прокатывают, в STRICT-таблице вызовут ошибку. Колонка ANY — это запасной выход: принимает значение любого типа и не пытается его преобразовать.
Стоит ли использовать STRICT-таблицы для новых баз?
Для большинства новых схем — да. STRICT ловит баги, которые обычные таблицы молча проглатывают: случайную строку в колонке INTEGER, сериализованный список, попавший в REAL. Цена вопроса — одно лишнее ключевое слово на таблицу и отказ от экзотических имён типов. Доступно начиная с SQLite 3.37 (2021 год).