SQLite: база данных без сервера — 9 примеров, PHP и мастерская
Настоящая SQLite в браузере и PHP: девять практических задач, связанные таблицы, транзакции, индексы, JSON-импорт и скачивание базы. Исходники и запуск на Windows и Linux.

SQLite на практике: девять настоящих SQL-сценариев в браузере, PHP PDO на Windows и Linux, транзакции, индексы, импорт и резервные копии. Меняйте данные, скачивайте базу и переносите проверенные решения в свои инструменты.
Что такое SQLite: база данных внутри приложения
SQLite — полноценная реляционная база, которая выполняет SQL внутри вашего приложения и обычно хранит данные в одном файле. Отдельный сервер базы, его пользователь и пароль для подключения не нужны. Программа открывает файл, создаёт таблицы, записывает строки и получает результат запроса. При этом остаются привычные возможности: связи, индексы, ограничения и транзакции.
Это удобно почувствовать на маленькой CRM. Контакт хранится отдельно от его заказов, суммы можно сгруппировать по городам, удаление связанного клиента отклоняется, а резерв товара меняет остаток и журнал одной операцией. Здесь SQL решает понятные задачи приложения, а не существует в виде набора отвлечённых команд.
В статье — девять работающих задач. На странице SQL выполняет настоящая SQLite, собранная в WebAssembly через sql.js. В исходниках есть второй вариант: нативный PHP PDO_SQLITE с теми же таблицами и учебными данными. Он сохраняет файл на диске, проверяет конкурентную запись и создаёт согласованную резервную копию. Можно сначала изучить поведение в браузере, затем повторить операции на своём компьютере или Linux-сервере.
Где применять SQLite: каталог, локальная CRM, отчёты и служебные инструменты
Подходящая задача — локальный каталог, небольшое внутреннее приложение, реестр задач, настольная утилита или импортёр данных. Например, скрипт раз в день получает поставку, проверяет записи, сохраняет их в таблицы и формирует отчёт. У такого инструмента появляется надёжное состояние между запусками без отдельной инфраструктуры базы данных.
Другой вариант — переносимый набор данных. Приложение может подготовить файл SQLite со справочником и отдать его другому инструменту для чтения. Получатель получает таблицы, типы и индексы, а не набор несвязанных CSV. Возможность переноса файла полезна и для тестов: каждый запуск создаёт собственную базу с известным исходным состоянием.
Решение выбирают по характеру работы, а не только по числу посетителей. Частое чтение и короткие записи сильно отличаются от множества одновременных длинных транзакций. Оцените расположение приложения, интенсивность записи, размер данных, требования к резервированию и обслуживание. Для начала работы с файловым каталогом есть подробная глава нашего PHP-мануала: создание каталога и чтение новым подключением.
SQLite, PostgreSQL, JSON-файл и IndexedDB: что выбрать
JSON-файл удобен для небольшого конфигурационного документа. Когда нужны выборки, связи и согласованное изменение нескольких записей, ручная перезапись JSON усложняется. SQLite предоставляет эти механизмы готовыми. Но сам файл не заменяет резервирование, права доступа, миграции схемы и наблюдение за ошибками приложения.
| Задача | Что рассмотреть |
|---|---|
| Один процесс или приложение, локальные данные, SQL | SQLite |
| Много одновременно пишущих сервисов на разных машинах | PostgreSQL или MySQL |
| Несколько простых настроек | JSON или другой конфигурационный формат |
| Долговременное хранилище веб-приложения в браузере | IndexedDB, например через Dexie.js |
| SQL-демонстрация и переносимый файл в браузере | sql.js с явно спроектированным сохранением |
В мастерской Dexie.js мы уже разбирали IndexedDB. Здесь другая модель: SQL, таблицы и SQLite-файл. Наша sql.js-база находится в оперативной памяти вкладки; IndexedDB автоматически не используется. Для настоящего offline-приложения потребуется отдельно продумать хранение выгрузки, версию схемы, синхронизацию и конфликты.
Версии, требования и разница между PDO_SQLITE и SQLite3
В проекте зафиксированы sql.js 1.15.2 и Bootstrap 5.3.8. Вызов SELECT sqlite_version() в используемой WASM-сборке возвращает 3.54.0. PHP-вариант рассчитан на PHP 8.2 или новее, расширение pdo_sqlite и SQLite не ниже 3.37, поскольку схема использует STRICT. Номер SQLite внутри PHP зависит от установленной сборки: на проверенном Windows-окружении это PHP 8.3.33 и SQLite 3.53.2.
PDO_SQLITE и SQLite3 — два PHP-интерфейса к SQLite. PDO удобен привычными prepare, bindValue, fetch и общим устройством доступа к базам. SQLite3 предоставляет собственные классы SQLite3Stmt/SQLite3Result и, например, метод backup. Наличие одного расширения не доказывает наличие другого. Наш проект использует обычный new PDO и не требует нового класса Pdo\Sqlite.
В мануале на сайте отдельно разобраны установка и требования SQLite3 и современный класс Pdo\Sqlite. Обращайте внимание на указанные там версии PHP: специализированный класс и новые методы имеют собственные требования. Установить расширение PHP через Composer нельзя; различие зависимостей объясняется в практическом руководстве по Composer.
SELECT sqlite_version();
PRAGMA foreign_keys;
PRAGMA journal_mode;
Мини-мастерская: девять задач с настоящим результатом
Выберите задачу, измените параметры и посмотрите таблицу результата. Созданный контакт сразу доступен в других сценах, резерв уменьшает общий остаток, а индекс меняет фактический план запроса. Под результатом показаны SQL и отдельные значения параметров. Редактор произвольного SQL намеренно не предлагается: все действия имеют понятное назначение и ограничены небольшим учебным набором.
Начните с фильтра оплаченных заказов от 10 000 рублей: исходная база вернёт три строки. Затем сравните LEFT JOIN с INNER JOIN, выполните успешный резерв и вызовите ошибку после UPDATE. Остаток после ошибочной попытки должен остаться прежним. Так удобно проверять именно поведение инструмента, а не доверять картинке с готовыми цифрами.
Данные живут только в памяти этой вкладки и исчезают после перезагрузки. Сервер не выполняет запросы посетителя и не получает введённые значения. Ссылки скачивания появляются после подготовки файлов и сохраняются на странице; изменение базы скрывает устаревшую выгрузку. На телефоне широкая таблица прокручивается внутри собственного блока, а мастерскую можно открыть отдельно.
Исходники и учебные данные: что именно вы скачиваете
Архив содержит готовую статическую мастерскую, HTML/CSS/JavaScript, локальные WASM и Bootstrap, нативный PHP-класс, CLI-команды, общие SQL/JSON-данные, тесты и инструкции Windows/Linux. Собственный код доступен по MIT, лицензии библиотек вложены отдельно. Зависимости npm закреплены lock-файлом; node_modules и рабочие базы в архив не включаются.
В исходном наборе пять контактов, девять заказов, складской остаток 100 и пустой журнал резервов. У одного клиента нет заказов — это полезный случай для LEFT JOIN. Есть оплаченные, новые и отменённые заказы; четыре оплаченных дают 62 500 рублей. Такие данные позволяют увидеть различие запросов без загрузки большого искусственного массива.
src/model.js # разрешённые SQL-операции браузера
src/app.js # формы, таблицы, скачивание
fixtures/schema.sql # общая схема
fixtures/seed.json # общий набор данных
php/Database.php # реальные операции PDO
tools/console.php # команды для файла runtime/app.sqlite
tests/ # WASM и PHP проверки
public/ # готовые статические файлы
В PHP рабочий каталог runtime расположен рядом с public, а не внутри него. Браузерная база и PHP-файл независимы: изменения в мастерской не появятся автоматически на сервере. Общий контракт задают схема, исходные данные и проверяемый смысл запросов, а не скрытая синхронизация.
Пример 1. Схема: таблицы, типы, ключи и денежные суммы
Сначала откройте таблицы contacts и orders. Контакт хранит имя, email и город; заказ ссылается на contact_id. Это избавляет от копирования имени клиента в каждую строку заказа. INTEGER PRIMARY KEY задаёт идентификатор, UNIQUE запрещает повторный email, CHECK ограничивает допустимые значения. STRICT проверяет соответствие поддерживаемым типам после допустимых преобразований.
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL
REFERENCES contacts(id) ON DELETE RESTRICT,
amount_cents INTEGER NOT NULL
CHECK(amount_cents BETWEEN 0 AND 100000000),
status TEXT NOT NULL
CHECK(status IN ('new','paid','cancelled'))
) STRICT;
Деньги записаны целым числом копеек: 12 000 рублей становятся 1 200 000. В интерфейсе значение форматируется обратно в рубли. Это позволяет складывать эти суммы без ошибки двоичного представления дроби. Для курсов валют, налогов и произвольной точности нужны явные правила округления; одно слово INTEGER не проектирует финансовую модель за вас.
Поля и внешние ключи мастерская читает через PRAGMA, а текст CREATE TABLE — из sqlite_schema. Если SQL пока незнаком, пройдите раздел создание таблицы с объяснением каждого слова в PHP-мануале. Там можно начать с совсем маленькой таблицы и постепенно перейти к связанным данным.
Пример 2. CRUD: создать, прочитать, изменить и удалить контакт
Добавьте контакт с учебным email, затем измените город и сохраните. Новая строка появляется в SQLite и получает ID. В таблице можно выбрать запись для редактирования или удалить её. При попытке удалить исходного клиента с заказами срабатывает FOREIGN KEY: база не оставляет заказ с несуществующим владельцем.
INSERT INTO contacts(name,email,city)
VALUES(:name,:email,:city);
UPDATE contacts
SET name=:name,email=:email,city=:city
WHERE id=:id;
DELETE FROM contacts WHERE id=:id;
WHERE в UPDATE и DELETE определяет, какие строки изменятся. Если убрать условие, операция затронет всю таблицу. В пользовательском приложении идентификатор также проверяется на право доступа: подготовленный запрос защищает передачу значения, но не доказывает, что текущему пользователю разрешено редактировать выбранного клиента.
Форма проверяет обязательные поля, длину, допустимый город и формат email; база повторно охраняет свои ограничения. Имя с апострофом остаётся текстом, а результат выводится через textContent. Для удаления заказа в реальной CRM чаще нужна отдельная бизнес-операция: отмена, архивирование или явно согласованная каскадная политика. Подробнее об изменениях и условиях — UPDATE, DELETE и WHERE в мануале.
Пример 3. Подготовленный фильтр: статус и минимальная сумма
В этой сцене статус и сумма меняются независимо. SQL остаётся одинаковым, значения передаются через bind. При статусе paid и пороге 10 000 рублей исходные данные возвращают заказы 103, 107 и 101 по убыванию суммы. Порог 20 000 оставляет одну строку, а 30 000 — пустой результат. Отсутствие подходящих строк является нормальным исходом запроса.
SELECT id,contact_id,amount_cents,status
FROM orders
WHERE status=:status AND amount_cents>=:minimum
ORDER BY amount_cents DESC,id;
-- :status = paid
-- :minimum = 1000000, то есть 10 000 рублей
Подготовка отделяет SQL от пользовательских данных. Она не превращает любую строку в безопасное имя таблицы или направление сортировки: такие части запроса выбирают из заранее разрешённых вариантов. В проекте статусы ограничены тремя значениями, сумма — целым числом в заданном диапазоне. Это одновременно понятный интерфейс и ограничение работы базы.
На PHP значения суммы и идентификаторов связываются с PDO::PARAM_INT, строки — с PDO::PARAM_STR. Не нужно вручную добавлять кавычки или экранировать апострофы перед bindValue. Пошаговый разбор есть в разделе передачи пользовательских данных через параметры.
Пример 4. INNER JOIN и LEFT JOIN: показать клиента без заказов
Переключите соединение таблиц и сравните число строк. INNER JOIN возвращает девять строк заказов с данными клиентов. LEFT JOIN добавляет Олега Мирного, у которого пока нет заказа: получается десять строк. Поля правой таблицы у такого клиента равны NULL. Это полезно для отчёта о клиентах, которые ещё ничего не приобрели.
SELECT c.name,c.city,o.id AS order_id,
o.amount_cents,o.status
FROM contacts AS c
LEFT JOIN orders AS o ON o.contact_id=c.id
ORDER BY c.id,o.id;
NULL означает отсутствие соответствующего значения, а не ноль и не пустую строку. Если заменить его нулём слишком рано, можно спутать отсутствие заказа с настоящим заказом на нулевую сумму. В интерфейсе мастерской NULL показан явно, чтобы связь между SQL и результатом оставалась заметной.
Обращайте внимание на размещение условий. WHERE o.status=’paid’ после LEFT JOIN исключит строки без заказа, потому что их статус неизвестен. Если требуется сохранить всех клиентов и присоединить только оплаченные заказы, условие по статусу переносится в ON. Это типичная причина неожиданного исчезновения строк в отчётах и административных таблицах.
Пример 5. GROUP BY, SUM и HAVING: сводка продаж по городам
Сводка сначала выбирает заказы нужного статуса, затем соединяет их с клиентами и группирует по городу. COUNT показывает число заказов, SUM — общий оборот, AVG — средний чек. На исходных оплаченных заказах Казань получает 33 000 рублей, Москва — 29 500. Среднее округлено до копейки; интерфейс форматирует его в рублях.
SELECT c.city,COUNT(*) AS order_count,
SUM(o.amount_cents) AS total_cents,
ROUND(AVG(o.amount_cents)) AS average_cents
FROM orders o JOIN contacts c ON c.id=o.contact_id
WHERE o.status=:status
GROUP BY c.city
HAVING COUNT(*)>=:minimum
ORDER BY total_cents DESC,c.city;
WHERE применяется к исходным строкам, HAVING — к уже созданным группам. Поставьте минимальное число заказов три: обе исходные группы исчезнут, поскольку в каждой только по два оплаченных заказа. Это хороший опыт для понимания разницы: сумма не сломалась, просто группы не прошли заданное условие.
Подобная выборка питает KPI-карточки, отчёт менеджера или выгрузку в Excel. Для дальнейшего развития есть отчёт с собственными правилами SQLite в PHP-мануале и мастерская PhpSpreadsheet. Сначала закрепите состав строк и единицы измерения, затем оформляйте диаграмму.
Пример 6. Транзакция: резерв товара и полный откат
Резерв состоит из двух изменений: уменьшить остаток и добавить запись в журнал. Они должны происходить вместе. Нажмите «Зарезервировать» с количеством 10: остаток станет 90, в журнале появится одна строка. Затем выберите «Вызвать ошибку и откат»: код выбросит исключение после UPDATE, а ROLLBACK вернёт прежнее состояние.
BEGIN IMMEDIATE;
UPDATE stock SET quantity=quantity-:quantity WHERE id=1;
INSERT INTO reservations(quantity) VALUES(:quantity);
COMMIT;
-- При ошибке вместо COMMIT выполняется ROLLBACK.
Перед изменением проверяется доступный остаток. В файловом PHP-варианте BEGIN IMMEDIATE получает право записи до чтения, поэтому второй писатель не сможет незаметно изменить остаток между проверкой и UPDATE. Если остатка недостаточно, не записывается ни списание, ни журнал. Ошибка здесь является частью нормального поведения, которое важно проверить заранее.
Внутри короткой транзакции не стоит выполнять HTTP-запрос, отправлять письмо или ожидать пользователя: это удлиняет блокировку. Для конкурентных операций и повторов прочитайте режимы DEFERRED, IMMEDIATE и EXCLUSIVE и резерв с настоящим конфликтом двух участников в нашем мануале.
Пример 7. UNIQUE, CHECK и FOREIGN KEY: база отклоняет ошибку
Выберите одно из трёх нарушений. UNIQUE пытается добавить уже существующий email. CHECK отклоняет отрицательную сумму заказа. FOREIGN KEY не позволяет создать заказ для клиента, которого нет в таблице contacts. В интерфейсе показаны настоящее сообщение SQLite и количество строк до и после попытки.
INSERT INTO orders(contact_id,amount_cents,status)
VALUES(1,-100,'new');
-- CHECK constraint failed
INSERT INTO orders(contact_id,amount_cents,status)
VALUES(999999,100,'new');
-- FOREIGN KEY constraint failed
Внешние ключи включаются командой PRAGMA foreign_keys=ON для каждого подключения. Недостаточно один раз написать REFERENCES в CREATE TABLE и забыть о настройке соединения. В проекте этот режим включён и в sql.js, и в PDO, а тесты специально пытаются нарушить связь.
Проверка формы нужна для понятного объяснения, ограничение базы — для целостности при любом пути записи. Между предварительным SELECT и INSERT другой процесс может успеть записать такой же email, поэтому предварительная проверка не заменяет UNIQUE. В публичном приложении переводите ожидаемые ошибки в понятный ответ, а техническую причину сохраняйте в серверном журнале.
Пример 8. Индекс и EXPLAIN QUERY PLAN: понять выборку
Сцена выполняет EXPLAIN QUERY PLAN для фильтра по статусу и сумме. Сначала собственного индекса нет; затем кнопка создаёт индекс по двум полям. В detail появляется его имя, а результат обычной выборки остаётся прежним. Это настоящий план SQLite, а не заранее подписанная картинка.
CREATE INDEX idx_orders_status_amount
ON orders(status,amount_cents);
EXPLAIN QUERY PLAN
SELECT id,contact_id,amount_cents,status
FROM orders
WHERE status=:status AND amount_cents>=:minimum
ORDER BY amount_cents DESC,id;
На девяти строках сравнение миллисекунд почти ничего не доказывает: шум измерения и накладные расходы могут быть больше полезной работы. Здесь задача — увидеть SCAN или SEARCH, выбранный индекс и возможную временную сортировку. Формат текста плана зависит от версии SQLite; не стройте бизнес-логику на точном совпадении всей строки detail.
Индекс ускоряет подходящие чтения, но занимает место и обслуживается при записи. Порядок его колонок связан с условиями реальных запросов. Проверять решение нужно на представительных данных и нагрузке. Дополнительный самостоятельный пример есть в главе выборки трёх заказов и проверки собственного индекса.
Пример 9. Атомарный JSON-импорт и скачивание базы
Учебный JSON содержит два новых контакта. Нажмите «Проверить и добавить»: пакет проходит проверку структуры и записывается одной транзакцией. Повторный запуск отклоняется из-за существующих email. Если первая строка новая, а вторая содержит дубликат, первая тоже не сохраняется: операция либо выполнена целиком, либо отменена.
[
{"name":"Мария Учебная","email":"maria@example.test","city":"Москва"},
{"name":"Денис Учебный","email":"denis@example.test","city":"Казань"}
]
Разрешены только name, email и city, до 200 контактов и до 120 000 символов JSON. Общий лимит базы также 200 контактов. Эти ограничения защищают удобство учебного интерфейса; они не являются универсальной политикой импорта для вашего проекта. В рабочем сервисе дополнительно нужны права доступа, протокол ошибок и понятное правило обработки повторов.
SQLite-выгрузка содержит все четыре таблицы и созданный индекс. JSON-выгрузка — только контакты без ID, пригодные для добавления в новую пустую схему. Она не восстанавливает заказы и не заменяет существующие строки. Полный файл можно открыть SQLite-инструментом отдельно; загрузки чужой базы в этой мастерской нет. Связанный импорт подробно разобран в главе приёма поставки целиком.
PHP PDO: открыть файл и выполнить настоящий запрос
В PHP тот же SQL работает через нативный драйвер. Файловый путь передаётся в DSN sqlite:, ошибки превращаются в исключения, результаты возвращаются ассоциативными массивами. Базу следует размещать вне публичного каталога сайта: иначе неверная конфигурация веб-сервера может позволить скачать все данные одним файлом.
$db = new PDO('sqlite:' . __DIR__ . '/../runtime/app.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$db->exec('PRAGMA foreign_keys=ON');
$db->exec('PRAGMA busy_timeout=2000');
$stmt = $db->prepare(
'SELECT id,amount_cents FROM orders WHERE status=:status'
);
$stmt->bindValue(':status', 'paid', PDO::PARAM_STR);
$stmt->execute();
$rows = $stmt->fetchAll();
$stmt->closeCursor();
Фрагмент предполагает, что файл уже инициализирован схемой. Полный CLI сначала выполняет init и отказывается перезаписывать существующую базу. Команды чтения также не создают случайно новую пустую базу при опечатке в пути: они требуют существующий файл. Это небольшая, но полезная защита от незаметной работы не с тем хранилищем.
Выбор между файлом, памятью и временной базой объяснён в разделе DSN нашего PHP-мануала. Для альтернативного расширения используйте полный справочник классов и методов SQLite3.
Как выполняется PHP-транзакция и почему важен тип параметра
Класс Database связывает целые числа с PDO::PARAM_INT, а строки с PDO::PARAM_STR. Это особенно полезно для сумм, лимитов и сравнения агрегатов. Выполнение запроса, чтение результата и закрытие курсора находятся рядом: завершённый statement не должен случайно удерживать ненужное состояние при следующей операции.
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare(
'UPDATE stock SET quantity=quantity-:qty WHERE id=1'
);
$stmt->bindValue(':qty', 10, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
// Здесь выполняются остальные связанные записи.
$db->exec('COMMIT');
} catch (Throwable $error) {
$db->exec('ROLLBACK');
throw $error;
}
Это фрагмент механики транзакции; полный reserve в архиве дополнительно проверяет допустимость количества и остаток, затем записывает журнал. Если BEGIN IMMEDIATE уже отклонён из-за занятого писателя, транзакция ещё не началась. Поэтому начало расположено перед try, а ROLLBACK вызывается только после успешного BEGIN и последующей ошибки.
При ручном BEGIN в совместимом PDO-варианте проект использует также явные SQL COMMIT и ROLLBACK. В современных версиях PHP доступны дополнительные средства Pdo\Sqlite; не смешивайте примеры разных API без проверки версии. Логику отмены отдельно раскрывает раздел обработки неудачной транзакции.
Windows: запуск мастерской и PHP-команд
Распакуйте архив, откройте PowerShell в sqlite-project и проверьте Node.js 24. npm ci устанавливает закреплённые зависимости, сборка копирует WASM и CSS в public, тесты запускают настоящее ядро SQLite. npm start открывает статический сервер только на 127.0.0.1:8175; мастерская доступна по HTTP. Для остановки нажмите Ctrl+C.
node --version
npm ci
npm run build
npm test
npm start
PHP запускается отдельно. Проверьте php –ini и список модулей: нужен именно pdo_sqlite у выбранного php.exe. Если расширение отключено, включите его в соответствующем ini и убедитесь, что extension_dir указывает на совместимые модули. Другой PHP, используемый веб-сервером, может читать другой конфигурационный файл.
php --version
php --ini
php -m | Select-String 'PDO|pdo_sqlite'
php tests/run.php
php tools/console.php init
php tools/console.php report paid 10000
php tools/console.php reserve 10
php tools/console.php reserve 10 --fail
php tools/console.php info
Вызов с –fail специально завершается с кодом 1, демонстрируя откат. Следующая команда info показывает сохранённый остаток. Открытие HTML двойным щелчком через file:// не заменяет HTTP: браузеру нужно корректно загрузить WASM и общие fixtures. Публиковать Node-сервер разработки наружу не требуется.
Linux: установить расширение и получить полезный результат
На Debian 12 или Ubuntu 24.04 PHP CLI и SQLite-расширение можно установить пакетами. Для другого дистрибутива уточните названия и доступные версии. Важно совпадение расширения с используемым PHP, а не просто наличие похожего пакета. После установки проверьте версию PHP, ini и фактическую доступность pdo_sqlite.
sudo apt update
sudo apt install php-cli php-sqlite3 unzip
php --version
php --ini
php -m | grep -E 'PDO|pdo_sqlite'
php -r 'echo (new PDO("sqlite::memory:"))->query("SELECT sqlite_version()")->fetchColumn(), PHP_EOL;'
Из каталога проекта запустите PHP-тесты, затем создайте рабочую базу и получите отчёт. umask 077 ограничивает доступ к новым файлам текущего процесса. Это удобно для отдельного пользователя приложения; реальные права должны учитывать, кто будет читать и изменять базу, например пользователь PHP-FPM или фонового задания.
php tests/run.php
umask 077
php tools/console.php init
php tools/console.php report paid 10000
php tools/console.php aggregate paid 1
php tools/console.php import fixtures/contacts-import.json
php tools/console.php index on
php tools/console.php check
php tools/console.php backup
Повторный импорт того же файла ожидаемо отклоняется по UNIQUE. Состояние базы сохраняется между запусками CLI. Таким способом можно построить небольшой ежедневный импортёр или отчёт без веб-интерфейса; для удобных аргументов, прогресса и кодов завершения полезна мастерская Symfony Console.
VPS: каталоги, права и публикация статической мастерской
Для размещения на сервере разделите публичные файлы и рабочие данные. Например, приложение находится в /srv/aen-sqlite, веб-сервер обслуживает только public, а runtime остаётся соседним приватным каталогом. Не копируйте весь проект в общедоступный document root. PHP CLI может запускаться от отдельного непривилегированного пользователя.
/srv/aen-sqlite/
├── public/ # доступен по HTTPS
├── php/ # класс приложения
├── fixtures/ # учебная схема и данные
└── runtime/ # закрытый каталог
├── app.sqlite
└── backups/
Процесс записи должен иметь доступ к каталогу базы: SQLite создаёт рядом служебные файлы -wal и -shm. Ошибку прав не нужно исправлять chmod 777. Настройте владельца и минимально необходимые разрешения под реального пользователя процесса. Для простого CLI достаточно приватного runtime; при интеграции PHP-FPM согласуйте владельца или группу.
Статическая часть работает через Nginx или Caddy. WASM должен отдаваться как application/wasm, JavaScript — с корректным JavaScript MIME. Пример location включён в архив. Настройку HTTPS и публикации приложений разбирают материалы про Nginx и Caddy. PHP endpoint для посетителей наш проект не создаёт.
WAL, занятая база и единственный писатель
WAL позволяет читателям и писателю работать одновременно, сохраняя согласованное представление данных. Он не превращает SQLite в систему с неограниченным количеством параллельных писателей: право записи одновременно получает один участник. Короткие транзакции важнее обещания «включить один PRAGMA и ускорить всё».
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=2000;
PRAGMA foreign_keys=ON;
busy_timeout задаётся в миллисекундах и ограничивает ожидание подходящей блокировки. Это не общий таймер выполнения SELECT и не гарантия, что любая конфликтующая операция будет ждать ровно столько. При SQLITE_BUSY приложение решает, можно ли повторить операцию целиком. Для создания заказа полезен ключ идемпотентности, чтобы повтор не создал второй заказ после уже успешной фиксации.
Долго открытая читающая транзакция может продолжать видеть прежний снимок. Это не обязательно потеря UPDATE. В PHP-мануале есть отдельные опыты согласованного снимка WAL и ограничения ожидания. WAL требует подходящей локальной файловой системы; общий файл на сетевом диске между несколькими серверами — плохая замена серверной СУБД.
Резервная копия: VACUUM INTO вместо копирования живого файла
В WAL подтверждённые изменения могут находиться в отдельном журнале. Простое копирование одного app.sqlite работающего приложения поэтому не является надёжной резервной копией. Не удаляйте вручную -wal и -shm и не пытайтесь собрать снимок случайным порядком копирования файлов.
$statement = $db->prepare('VACUUM INTO :destination');
$statement->bindValue(
':destination',
'/srv/aen-sqlite/runtime/backups/new-snapshot.sqlite',
PDO::PARAM_STR
);
$statement->execute();
$statement->closeCursor();
VACUUM INTO создаёт согласованный снимок в новом файле. В проекте имя формирует доверенная CLI-команда из UTC-времени и случайного суффикса; путь не приходит из HTTP-параметра. Старые копии не перезаписываются. Операция требует свободного места, завершённых statements и отсутствия открытой транзакции на этом соединении.
Альтернатива — Backup API. В справочнике SQLite3 на нашем сайте разобран SQLite3::backup, включая направление копирования и особенности назначения. Рабочий регламент добавляет расписание, ротацию и хранение отдельно от исходного сервера. Полезность копии подтверждает восстановление в другой путь и проверка данных. Наш PHP-тест действительно открывает отдельный снимок и сверяет состояние; рабочую базу он не заменяет.
Миграции схемы и границы роста приложения
Создание таблиц при первом запуске и изменение существующей базы — разные операции. Когда у контакта появляется новый атрибут, подготовьте миграцию с известным номером, проверкой текущей схемы и тестом на копии реальных данных. Не используйте ошибку произвольного SELECT как доказательство того, что колонка отсутствует: причиной может быть другое повреждение запроса или доступа.
PRAGMA user_version;
PRAGMA table_info(contacts);
-- Миграция выполняется приложением после проверки версии:
ALTER TABLE contacts ADD COLUMN note TEXT NOT NULL DEFAULT '';
PRAGMA user_version=1;
Это пример направления миграции, а не команда для неподготовленной рабочей базы. В мастерской схема фиксирована, поэтому кнопка сброса создаёт известный учебный набор заново. Настоящее приложение должно сохранять пользовательские данные, иметь резервную копию перед изменением и проверять совместимость старой и новой версии кода.
По мере роста добавляйте пагинацию, представительные нагрузочные тесты, наблюдение за временем транзакций и размером WAL. Перенос на PostgreSQL не сводится к смене DSN: различаются SQL-диалект, типы и конкурентное поведение. Обдумать переход заранее полезно, но небольшому локальному инструменту отдельный сервер базы может ещё долго не понадобиться.
Ошибки, пустой результат и безопасный вывод данных
Разделяйте три ситуации: запрос успешно вернул строки, успешно вернул пустой набор или завершился ошибкой. Пользователю фильтра нужен текст «ничего не найдено», а при конфликте email — объяснение, какое правило нарушено. Техническую ошибку подключения не следует маскировать таким же пустым массивом: приложение потеряет важный сигнал.
try {
$rows = $database->filter('paid', 10000);
// [] — корректный результат: подходящих заказов нет.
} catch (PDOException $error) {
error_log($error->getMessage());
// В HTTP-контроллере верните понятную общую ошибку,
// не раскрывая посетителю путь к файлу и SQL.
}
В браузере введённые имена отображаются как текст. В PHP HTML-странице для этого применяют htmlspecialchars с корректной кодировкой. Подготовленный SQL решает передачу данных в базу, а экранирование HTML — вывод в браузер; одна мера не заменяет другую. Для API возвращайте JSON через json_encode, а не склейку строки вручную.
При выгрузке из Windows учитывайте кодировку: Windows PowerShell 5.1 может записывать перенаправленный вывод как UTF-16. Для UTF-8-файла без BOM используйте явную запись, показанную ниже. Это предотвращает непонятную ошибку JSON при последующем импорте.
$json = php tools/console.php export-json
if ($LASTEXITCODE -ne 0) { throw 'Экспорт не выполнен' }
$path = Join-Path (Get-Location) 'contacts.json'
[IO.File]::WriteAllText($path, ($json -join [Environment]::NewLine), [Text.UTF8Encoding]::new($false))
Что проверяют тесты и как обновлять зависимости
Node-тесты запускают настоящий SQLite WASM: проверяют версию, исходные данные, CRUD, подготовленные фильтры, JOIN, агрегаты, транзакции и ограничения. Отдельно проверяются индекс, атомарный импорт, лимит контактов и экспорт бинарной базы с повторным открытием. SQL-файл должен начинаться настоящим заголовком SQLite, а восстановленная база — проходить integrity_check.
PHP-тесты выполняют аналогичные операции через PDO. Дополнительно создают временную файловую базу, открывают два подключения, получают реальный конфликт писателей и проверяют успешную запись после освобождения блокировки. Резервная копия создаётся VACUUM INTO и читается как независимый файл. Тест удаляет только собственный уникальный временный каталог.
npm ci
npm run build
npm test
php tests/run.php
Эти проверки подтверждают логику учебного проекта, но не являются нагрузочным испытанием вашего VPS. При обновлении библиотеки пересоберите public, повторите тесты и проверьте формы, выгрузки и мобильное управление. Версии SQLite в разных PHP-сборках могут отличаться; сравнивайте смысл результата и целостность, а не обязательное совпадение всех байтов файлов.
Как превратить примеры в свой сервис
Начните с одного законченного сценария: например, каталог услуг с импортом и отчётом. Оставьте схему и слой доступа к данным отдельно от HTML. Контроллер получает запрос, проверяет аутентификацию, права и CSRF, проверяет входные поля и вызывает понятный метод приложения. Представление показывает результат и ожидаемые ошибки.
Для серверного приложения источником истины служит серверная база, а не скачанный посетителем файл. Клиентские проверки можно обойти, поэтому цены, права доступа и остатки подтверждаются на сервере. sql.js в статье нужен для настоящего интерактивного обучения и локальных инструментов; он не выдаёт пользователю полномочия управлять общей CRM.
Сначала закрепите правила: какие связи нельзя удалять, что считается повтором импорта, как округляются суммы и как операция повторяется после сбоя. Затем добавляйте интерфейс, экспорт и фоновые задачи. Когда контракт данных устойчив, таблицу можно заменить карточками, диаграммой или Excel-отчётом без переписывания всей логики хранения. Подробный путь от HTTP-формы к SQLite есть в разделе границ ответственности PHP-приложения.
Источники и продолжение в PHP-мануале
Для продолжения на нашем сайте начните с SQL и PDO с нуля, затем откройте локальный каталог и связанный импорт, конкуренцию, снимки и планы запросов и полный справочник SQLite3. Это развёрнутые материалы с пояснениями, исходниками и самостоятельными задачами, а не только указатели на функции.
Первичные технические источники: релизы sql.js, API sql.js, синтаксис SQL SQLite, STRICT-таблицы, PRAGMA foreign_keys, режим WAL, EXPLAIN QUERY PLAN, VACUUM INTO и PDO_SQLITE в PHP Manual.
Версии проекта закреплены в package-lock.json; команды и полная схема находятся в архиве. При переносе в рабочий сервис сверяйте требования установленного PHP, права на каталог, правила доступа и результат восстановления копии. Удобнее начать с небольшого законченного инструмента и измеренных требований, затем развивать его по реальной задаче.


