Ошибки SQLite — это просто способ что-то вам сообщить
Сообщения об ошибках SQLite короткие и порой загадочные, но за ними стоит вполне ограниченный набор реальных проблем. На практике почти всё, с чем вы столкнётесь в продакшене, укладывается в пять категорий: блокировки, права доступа, повреждение базы, расхождение схемы и нарушение ограничений. В этой статье разберём каждую — что её вызывает, что она на самом деле значит и как её починить.
К текстовым сообщениям прилагаются числовые коды (а расширенные коды — ещё точнее). В логах вы увидите обе формы:
Error: database is locked -- код 5 (SQLITE_BUSY)
Error: unable to open database -- код 14 (SQLITE_CANTOPEN)
Error: attempt to write a readonly -- код 8 (SQLITE_READONLY)
Error: database disk image is -- код 11 (SQLITE_CORRUPT)
Если знаешь числовой код, искать причину гораздо проще — по запросу SQLITE_BUSY найдёшь куда больше полезного, чем по обычной английской фразе.
database is locked (SQLITE_BUSY)
Самая частая ошибка SQLite в любом приложении, где запись идёт из нескольких мест. SQLite сериализует запись: блокировку на запись в каждый момент держит только одно соединение. Если второй писатель не успевает получить блокировку за время busy timeout — получаешь именно эту ошибку.
Три способа решения, от самого действенного к менее:
WAL-режим сам по себе закрывает проблему блокировок для большинства сценариев. А busy_timeout — это страховка на случай реальной конкуренции при записи. Но дело не только в настройках: пройдитесь по коду. Если транзакция остаётся открытой, пока программа уходит в сетевой I/O, блокировка будет висеть всё это время. Держите транзакции короткими и делайте COMMIT (или ROLLBACK) сразу, как только работа закончена.
unable to open database file (SQLITE_CANTOPEN) — sqlite unable to open database file
SQLite попытался открыть файл, а ОС ответила отказом. В 95% случаев виноват путь к файлу или его директория:
-- Что нужно проверить:
-- 1. Существует ли путь? ls -l /path/to/db.sqlite
-- 2. Существует ли родительский каталог? SQLite создаёт файл,
-- но не каталог, в котором он находится.
-- 3. Имеет ли пользователь, запускающий ваш процесс, права на чтение и запись
-- в каталоге (а не только в файле)?
-- 4. Смонтирован ли том, не переполнен ли он и не доступен ли только для чтения?
Тонкий момент: SQLite создаёт рядом с базой служебные файлы (-journal, -wal, -shm). Если сам файл доступен на запись, а каталог — нет, открытие проходит, а запись падает. Всегда давайте права на запись именно каталогу.
attempt to write a readonly database (SQLITE_READONLY)
Близкий родственник предыдущей ошибки sqlite. Файл открылся нормально, но запись не проходит. Причины по частоте:
- У пользователя ОС нет прав на запись в файл или каталог с ним.
- Соединение открыто в режиме «только чтение» (
SQLITE_OPEN_READONLYилиmode=roв URI). - Том примонтирован read-only (типичная история с bind-mount в Docker и некоторыми облачными ФС).
- База лежит на сетевой файловой системе, которая не поддерживает нужные SQLite блокировки.
Поправьте права доступа или перемонтируйте том. Если вы работаете в Docker, убедитесь, что bind mount не помечен как :ro и что директория принадлежит пользователю контейнера.
database disk image is malformed (SQLITE_CORRUPT)
Байты файла больше не соответствуют формату SQLite. Реальные причины почти всегда связаны с окружением: процесс был убит посреди записи на ФС без нормального fsync, базу копировали при активном писателе, отказало железо или файл синхронизировался через Dropbox/iCloud.
Для начала убедимся, что повреждение действительно есть:
Если integrity_check вернул ok, с самой базой всё в порядке — ошибка прилетела откуда-то ещё (чаще всего виновато подвисшее соединение). А вот если на выходе целый список проблем, придётся восстанавливать данные.
Самый чистый способ восстановления — команда .recover в CLI: она вытаскивает всё, что ещё можно прочитать, и переливает в новую базу:
sqlite3 corrupt.db ".recover" | sqlite3 recovered.db
sqlite3 recovered.db "PRAGMA integrity_check;"
Если у вас есть свежий бэкап — восстанавливайтесь из него, так быстрее и без двусмысленности в духе «вроде бы всё вытащили». О том, как правильно копировать живую базу, читайте на странице про резервное копирование и восстановление (подсказка: точно не через cp).
Ошибки no such table и no such column
Эти сообщения говорят ровно о том, о чём говорят, но причина обычно сводится к двум вариантам: вы подключились не к той базе, что думаете, либо не отработала миграция.
Проверьте строку подключения в вашем приложении: относительные пути разрешаются относительно текущей рабочей директории, а она отличается у терминала, IDE и продакшен-процесса. База в памяти (:memory:) каждый раз создаётся с нуля — на этом часто спотыкаются те, кто рассчитывает на сохранение данных.
С кавычками в идентификаторах та же история. Без кавычек имена нечувствительны к регистру, но "User" и "user" — это уже два разных идентификатора. Если таблица была создана с именем в кавычках, обращаться к ней придётся тоже только в кавычках.
Нарушения ограничений (constraint failed)
SQLite отклоняет запись, если она нарушает какое-либо ограничение. В тексте ошибки указано, какое именно:
За каждой такой ошибкой скрывается отдельный код: SQLITE_CONSTRAINT_UNIQUE, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_NOTNULL. Чинить почти всегда нужно на уровне приложения — либо валидируйте данные до записи, либо осознанно обрабатывайте дубликаты через INSERT ... ON CONFLICT.
Отдельного разговора заслуживает FOREIGN KEY constraint failed: в SQLite внешние ключи по умолчанию отключены. Если их не включить, некорректные ссылки молча запишутся в базу, а вылезет это позже — когда вы наконец включите проверку. Задавайте этот pragma на каждом подключении:
cannot start a transaction within a transaction
Эта ошибка возникает, когда вы вызываете BEGIN, а транзакция уже открыта. SQLite не поддерживает вложенные транзакции, зато умеет работать с вложенными точками сохранения (savepoints) — а это, по сути, даёт тот же результат:
Если транзакциями управляет ваша ORM или фреймворк, скорее всего, вы запустили транзакцию дважды. Проверьте, включён ли autocommit, и не отдаёт ли пул соединений уже занятое соединение с открытой транзакцией.
disk I/O error (SQLITE_IOERR)
ОС отклонила операцию чтения или записи. Кончилось место на диске, моргнула сетевая ФС или кто-то удалил файл прямо из-под SQLite. Первым делом смотрим df -h. Вторым — где вообще лежит база: если это что-то ненадёжное вроде NFS или папки, синкающейся в облако, жди беды. SQLite рассчитывает на локальную POSIX-файловую систему с рабочим fsync. Если перенести базу некуда — смиритесь с тем, что риск повреждения данных выше.
syntax error near "..."
Парсер SQLite честно говорит, какой токен его смутил. Но чинить чаще всего нужно строки на три выше: пропущенная запятая, незакавыченный идентификатор, совпавший с ключевым словом, или строка с одинарными кавычками, которые забыли экранировать ('it''s', а не 'it's').
Используйте параметризованные запросы (плейсхолдеры ?) для пользовательского ввода вместо склейки SQL через конкатенацию строк — так вы разом избавитесь от целого класса синтаксических ошибок и SQL-инъекций.
Чек-лист для диагностики
Когда что-то падает в проде, такая последовательность шагов закрывает большинство случаев меньше чем за минуту:
Пять прагм — пять ответов. Если к ним добавить код ошибки из упавшего запроса, сразу поймёте, к какой категории относится проблема и какую страницу документации открывать дальше.
Подводим итог курса
Вот и весь маршрут. Мы прошли путь от CREATE TABLE через джойны, индексы, транзакции, режим WAL, бэкапы — и закончили разбором ошибок sqlite, с которыми сталкиваешься в боевых условиях. Закономерности везде одни и те же: короткие транзакции, включённые внешние ключи, WAL-режим, регулярные бэкапы и здоровое уважение к PRAGMA integrity_check. Держитесь этих привычек — и SQLite будет тихо и незаметно работать у вас годами.
Часто задаваемые вопросы
Почему SQLite выдаёт «database is locked»?
Другое соединение держит блокировку на запись, а ваше не дождалось своей очереди. Лечится тремя вещами: включить WAL-режим через PRAGMA journal_mode=WAL, чтобы читатели не блокировали писателей; увеличить таймаут — PRAGMA busy_timeout = 5000; и не забывать вовремя коммитить транзакции, а не держать их открытыми.
Как починить «attempt to write a readonly database» в SQLite?
В 99% случаев это проблема прав файловой системы, а не самого SQLite. У пользователя ОС, под которым запущен процесс, должны быть права на запись и в сам файл БД, и в каталог с ним — потому что SQLite создаёт рядом служебные файлы -journal или -wal. Проверьте владельца, права доступа и не смонтирован ли раздел в read-only.
Что означает «database disk image is malformed»?
SQLite прочитал из файла байты, которые не укладываются в ожидаемый формат — обычно это последствия повреждения: процесс убили во время записи, диск сбоит, или файл скопировали, пока БД была открыта. Сначала подтвердите диагноз через PRAGMA integrity_check, потом вытаскивайте уцелевшие данные командой .recover в CLI и сливайте их в новую базу. Если есть свежий бэкап — быстрее восстановиться из него.
Откуда берётся «no such table» или «no such column»?
Либо вы подключились не к тому файлу БД, который думаете, либо не прокатилась миграция. Посмотрите PRAGMA database_list — там видно, какой файл реально открыт, и .schema имя_таблицы, чтобы увидеть настоящие колонки. Ещё частая причина — опечатки и регистр в именах: для имён без кавычек SQLite регистронезависимый, а для закавыченных — уже нет.