Глава 32 / 51
Том 5 · Расширения, эксплуатация и практика

Расширение SQLite: кратчайший путь к персистентности

272 символа статически вкомпилированы в хост; скрипту достаточно одного include — 2004 записи демо тому подтверждение. Глава раскрывает его контекст в XServer.

Обзор

Позиционирование: открытие тома 5. Семантику самого SQLite смотрите в официальной документации — здесь три вещи, специфичные для XServer: вариант вкомпиляции, режим многих worker и решение о дескрипторе поколения. Полный индекс API — в справочнике.

32.1 Введение

Серверным приложениям почти всегда нужна персистентность, а в философии одного файла «поставить ещё один сервис базы данных» ломает всё повествование. SQLite — природный ответ: встроенный, однофайловый, без эксплуатации — в паре с XServer «один exe + один каталог» действительно закрывает бизнес с состоянием. XServer делает его опциональным расширением (не берущие — не тащат балласт); вкомпилируется полный нативный API без второй обёртки.

32.2 Вкомпиляция и открытие

Путь открытия демо (реальный исходник)проверено на реальной машинеисточник: demo-single/modules/database.h
/* предусловие: вариант build.bat sqlite; в скрипте */
#include <sqlite3.h>   /* в VFS, существует только в варианте sqlite */

static sqlite3* G_DB;   /* состояние поколения: переоткрывается в каждом (см. 32.4) */

static void DB_Init(void)
{
    char sDBFile[512];
    snprintf(sDBFile, sizeof(sDBFile), "%s/db/main.db", xsAppPath());
    iResult = sqlite3_open_v2(sDBFile, &G_DB,
        SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE   /* чтение-запись, создать при отсутствии */
        | SQLITE_OPEN_FULLMUTEX,                     /* безопасность для многих worker */
        NULL);
    if ( iResult != SQLITE_OK ) {
        printf("[db] open failed: %s\n", sqlite3_errmsg(G_DB));
        G_DB = NULL;
        return;
    }
    (void)sqlite3_exec(G_DB,
        "CREATE TABLE IF NOT EXISTS item ("
        "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
        "  name TEXT NOT NULL, created INTEGER)",
        NULL, NULL, NULL);
}
static void DB_Unit(void) { if ( G_DB ) { sqlite3_close(G_DB); G_DB = NULL; } }

Что только что произошло: строка лога старта [demo] database opened — это ровно этот код; FULLMUTEX позволяет одному дескриптору обслуживать 12 worker конкурентно; каталог db/ лежит в каталоге приложения — файлы данных межпоколенческие от природы (обещание планировки из гл. 05 выполняется: при смене поколения дескриптор переоткрывается, данные целы).

32.3 Пять шагов оператора на практике

Параметризованный CRUD (паттерн item.h демо)проверено на реальной машинеисточник: demo-single/route_http/item.h (фрагмент паттерна)
/* вставка: индексы привязки с 1; TRANSIENT = SQLite копирует параметр */
static bool DB_InsertItem(const char* sName)
{
    sqlite3_stmt* p = NULL;
    bool bOK = sqlite3_prepare_v2(G_DB,
            "INSERT INTO item(name, created) VALUES(?1, ?2)",
            -1, &p, NULL) == SQLITE_OK;
    if ( bOK ) {
        sqlite3_bind_text(p, 1, sName, -1, SQLITE_TRANSIENT);
        sqlite3_bind_int64(p, 2, (int64)time(NULL));
        bOK = sqlite3_step(p) == SQLITE_DONE;
    }
    sqlite3_finalize(p);          /* каждому prepare — finalize */
    return bOK;
}

/* выборка: step двигает строку, column читает по индексу (столбцы с 0) */
while ( sqlite3_step(p) == SQLITE_ROW ) {
    int64 iId   = sqlite3_column_int64(p, 0);
    const unsigned char* sName = sqlite3_column_text(p, 1);
    /* ... сложить в массив xvalue → ReplyJSON ... */
}

Привязка параметров (плейсхолдеры ?N) — не стилевая прихоть, а граница безопасности: склейка SQL строками и есть инъекция. Демо везде идёт через привязку — потому routebench и смог безопасно залить 2000 записей bench-write.

32.4 Транзакции и многие worker

  • Пакетные записи обязаны группироваться: bench-write демо оборачивает массовую вставку транзакцией — тысяча записей из «fsync на каждую» становится «fsync на группу», разница в один-два порядка
  • Граница FULLMUTEX: мьютекс уровня дескриптора гарантирует «не упадёт», но не «быстро» — горячий путь записи всё равно стоит очередить на уровне приложения (одна очередь записи или раз баз по бизнесам)
  • Объекты операторов не переиспользуются между потоками: prepare-step-finalize замыкайте в одном колбэке; межзапросное переиспользование — через reset (не перешагивая полушаганный оператор)
  • Решение о поколениях (две позы гл. 28): демо выбирает «переоткрытие в каждом поколении» — нулевая цена и природная безопасность; глобальный синглтон-дескриптор подходит «переоткрытие дорого + перезагрузки часты», ценой собственного управления межпоколенческими ссылками
  • Резервирование: sqlite3_backup_* в экспортируемом составе — скрипты онлайн-горячего бэкапа пишутся напрямую

32.5 Контракт

  • Вариант: символы sqlite существуют только в варианте sqlite; отказ компиляции скрипта под default — ожидаемое поведение.
  • Безопасность: всегда привязка параметров; склейка SQL — открытая дверь.
  • Жизненный цикл: дескриптор — состояние поколения (переоткрытие) или явный глобальный синглтон; выберите одно и впишите в комментарий модуля.

Частые ошибки

На пути ошибки потерян finalize
Симптом
После долгого марафона SQLITE_BUSY / память медленно растёт.
Причина
Ветка, где prepare успешен, а step провален, делает прямой return — объект оператора не освобождён. Держите finalize на едином выходе (как в одновыходном стиле выше).
Индексы bind и column перепутаны
Симптом
Поля возвращаются вперемешку.
Причина
bind считает с 1, column с 0 — две «единицы-офф» SQLite. Пишите номера столбцов по порядку SELECT, не по догадке.

Упражнения

БазовыйЧетыре CRUD-эндпоинта

Дополните демо путём Update (PUT) для таблицы item.

Приёмка: полный цикл создания-чтения-обновления-удаления curl'ом; после рестарта данные на месте.
ПродвинутыйСравнение пропускной способности транзакций

Залейте данные построчно и тысячными транзакциями; сравните время.

Приёмка: вывод о разнице порядков (ожидание ≥ 10×); объяснение причины (слияние fsync).
ВызовБез потерь при смене поколения

Вставьте данные → горячая перезагрузка (дескриптор переоткрывается в поколении) → проверьте целостность данных и продолжение записи.

Приёмка: count в /api/list непрерывно растёт через перезагрузку; ошибок «таблицы нет».

Итоги

ТезисВывод
Вкомпиляцияbuild.bat sqlite; 272 символа; include <sqlite3.h> (VFS)
Открытиеopen_v2 + READWRITE|CREATE|FULLMUTEX; каталог db/ межпоколенческий
Безопасностьпривязка параметров — железное правило; bind с 1, column с 0
Производительностьпакетные записи в транзакции; горячая запись — очередь на уровне приложения
Поколенияпереоткрытие в каждом (умолчание) или глобальный синглтон (явное управление)
Что дальше по xrt