Session Extension предлагает удобный способ фиксировать изменения в таблицах базы данных SQLite, упаковывая их в changeset (набор изменений) или patchset (набор патчей), а затем применяя те же изменения к другой базе данных с аналогичной схемой и совместимыми данными. Changeset также может быть инвертирован для отмены сессии.

Данный документ является введением в Session Extension. Подробности интерфейса описаны в отдельном документе Session Extension C-language Interface.
По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы
По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы
- Типичный сценарий использования
- Получение расширения session
- Ограничения расширения
- Что такое Changeset и Patchset в SQLite
- Конфликты при применении изменений
- Как создать changeset с помощью Session Extension
- Использование расширения Session
- Захват changeset: запись изменений через Session
- Применение changeset к базе данных SQLite
- Просмотр содержимого набора изменений
- Расширенная функциональность
- Типичные ошибки при работе с расширением Session
- Часто задаваемые вопросы по Session Extension
Типичный сценарий использования
Предположим, SQLite используется в качестве формата файла для дизайн-приложения. Два пользователя, Алиса и Боб, начинают работу с одним базовым проектом размером около гигабайта. Они работают весь день параллельно, каждый внося собственные изменения и правки в проект. В конце дня они хотят объединить свои изменения в единый общий проект.
Session Extension облегчает это, записывая все изменения в базах данных как Алисы, так и Боба и сохраняя эти изменения в файлы changeset или patchset. В конце дня Алиса может отправить свой changeset Бобу, и Боб может «применить» его к своей базе данных. В результате — при отсутствии конфликтов — база данных Боба будет содержать как его изменения, так и изменения Алисы.
Аналогично, Боб может отправить changeset своей работы Алисе, и она может применить его изменения к своей базе данных.
Иными словами, Session Extension предоставляет для файлов баз данных SQLite возможности, аналогичные утилите unix patch и системам контроля версий, таким как Fossil, Git или Mercurial.
Получение расширения session
С версии 3.13.0 (2016-05-18) Session Extension включено в стандартный дистрибутив исходного кода SQLite. Однако оно отключено по умолчанию. Чтобы активировать его, необходимо собрать SQLite с указанными ключами компиляции.
-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK
Или, если используется каноническая система сборки, передайте параметр --enable-session скрипту configure.
Ограничения расширения
До версии 3.17.0 Session Extension поддерживалось только для таблиц rowid и не применялось к таблицам WITHOUT ROWID. Начиная с версии 3.17.0, возможности расширения охватывают как таблицы rowid, так и WITHOUT ROWID, но для управления первичными ключами в таблицах WITHOUT ROWID потребуются специфические шаги.
Виртуальные таблицы не поддерживаются. Изменения виртуальных таблиц не фиксируются.
Session Extension работает только с таблицами, у которых объявлен PRIMARY KEY. PRIMARY KEY таблицы может быть INTEGER PRIMARY KEY (псевдоним rowid) или внешним PRIMARY KEY.
SQLite допускает хранение значений NULL в столбцах PRIMARY KEY. Однако расширение session игнорирует все такие строки. Никакие изменения, затрагивающие строки с одним или несколькими значениями NULL в столбцах PRIMARY KEY, не записываются модулем sessions. На моём опыте это чаще всего становится источником неожиданных пропусков при отладке.
Что такое Changeset и Patchset в SQLite
Session Extension сосредоточен на создании и управлении changesets. Changeset представляет собой данные, которые кодируют последовательность изменений в базе данных. Каждое изменение в changeset может быть одним из следующих:
INSERT. Изменение INSERT содержит одну строку для добавления в таблицу базы данных. Полезная нагрузка изменения INSERT состоит из значений каждого поля новой строки.
DELETE. Изменение DELETE представляет строку, идентифицированную по значениям первичного ключа, которую необходимо удалить из таблицы базы данных. Полезная нагрузка изменения DELETE состоит из значений всех полей удалённой строки.
UPDATE. Изменение UPDATE отражает модификацию одного или нескольких не-первичных полей строки таблицы, которая идентифицируется по ее первичным ключам. Полезная нагрузка UPDATE включает: значения первичного ключа для обнаружения измененной строки, новые значения актуальных полей, а также изначальные значения этих полей.
Изменение UPDATE не содержит никакой информации о полях, не являющихся PRIMARY KEY, которые не были изменены данным изменением. Изменение UPDATE не может задавать модификации полей PRIMARY KEY.
Один changeset может содержать изменения, относящиеся к более чем одной таблице базы данных. Для каждой таблицы, для которой changeset включает хотя бы одно изменение, он также кодирует следующие данные:
- имя таблицы базы данных,
- количество столбцов в таблице,
- какие из этих столбцов являются столбцами PRIMARY KEY.
Changesets могут применяться только к базам данных, содержащим таблицы, соответствующие трём вышеуказанным критериям, хранящимся в changeset.
Patchset похож на changeset. Он несколько компактнее changeset, но предоставляет более ограниченные возможности обнаружения и разрешения конфликтов. Различия между patchset и changeset состоят в следующем:
Исходные значения других полей не хранятся в составе patchset.
значений изменённых полей. Исходные значения изменённых полей не хранятся в составе patchset.
- Для изменения DELETE полезная нагрузка состоит только из полей PRIMARY KEY.
- Для изменения UPDATE полезная нагрузка состоит только из полей PRIMARY KEY и новых
Конфликты при применении изменений
Когда changeset или patchset применяется к базе данных, предпринимается попытка вставить новую строку для каждого изменения INSERT, удалить строку для каждого изменения DELETE и изменить строку для каждого изменения UPDATE. Если целевая база данных находится в том же состоянии, что и исходная база данных, на которой был записан changeset, это простая операция.
Однако если содержимое целевой базы данных не находится именно в этом состоянии, при применении changeset или patchset могут возникнуть конфликты.
При обработке изменения INSERT могут возникнуть следующие конфликты:
что указаны в изменении INSERT.
ограничение UNIQUE или CHECK, при вставке новой строки.
- Целевая база данных может уже содержать строку с теми же значениями PRIMARY KEY,
- Может быть нарушено какое-либо другое ограничение базы данных, например
При обработке изменения DELETE могут быть обнаружены следующие конфликты:
для удаления.
другие поля могут содержать значения, не совпадающие с теми, что хранятся в наборе изменений. Этот тип конфликта не обнаруживается при использовании набора патчей.
- Целевая база данных может не содержать строки с указанными значениями PRIMARY KEY
- Целевая база данных может содержать строку с указанными значениями PRIMARY KEY, но
для изменения.
текущие значения полей, которые будут изменены, могут не совпадать с исходными значениями, хранящимися в наборе изменений. Этот тип конфликта не обнаруживается при использовании набора патчей.
ограничение UNIQUE или CHECK, при обновлении строки.
- Целевая база данных может не содержать строки с указанными значениями PRIMARY KEY
- Целевая база данных может содержать строку с указанными значениями PRIMARY KEY, но
- Может быть нарушено какое-либо другое ограничение базы данных, например
В зависимости от типа конфликта приложение, использующее сессии, располагает рядом настраиваемых параметров для обработки конфликтов: от пропуска конфликтующего изменения и прерывания всего применения набора изменений до применения изменения несмотря на конфликт. Подробности см. в документации по API
Как создать changeset с помощью Session Extension
После того как объект сессии настроен, он начинает отслеживать изменения в своих настроенных таблицах. Однако он не записывает изменение целиком каждый раз, когда строка в базе данных изменяется. Вместо этого он записывает только поля PRIMARY KEY для каждой вставленной строки, а для любых обновлённых или удалённых строк — только PRIMARY KEY и все исходные значения строки.
Если строка изменяется более одного раза в рамках одной сессии, никакая новая информация не записывается.
Остальная информация, необходимая для создания набора изменений или набора патчей, считывается из файла базы данных при вызове sqlite3session_changeset() или sqlite3session_patchset(). В частности:
сессий проверяет, существует ли в таблице строка с совпадающим первичным ключом. Если да, в набор изменений добавляется изменение INSERT.
модуль сессий также проверяет наличие строки с совпадающим первичным ключом в таблице. Если такая строка найдена, но одно или несколько полей, не являющихся PRIMARY KEY, не совпадают с исходным записанным значением, в набор изменений добавляется UPDATE. Или, если строки с указанным первичным ключом вообще не существует, в набор изменений добавляется DELETE.
Если строка существует, но ни одно из полей, не являющихся PRIMARY KEY, не было изменено, никакое изменение в набор изменений не добавляется.
- Для каждого первичного ключа, записанного в результате операции INSERT, модуль
- Для каждого первичного ключа, записанного в результате операции UPDATE или DELETE,
Одно из следствий вышесказанного состоит в том, что если изменение было сделано, а затем отменено в рамках одной сессии (например, если строка была вставлена, а затем снова удалена), модуль сессий не сообщает ни о каком изменении вообще. Или если строка обновлялась несколько раз в рамках одной сессии, все обновления объединяются в одно обновление в любом блобе набора изменений или набора патчей.
Использование расширения Session
В этом разделе приведены примеры, демонстрирующие использование расширения session. Я рекомендую изучить их последовательно — от захвата changeset до его применения и просмотра содержимого.
Захват changeset: запись изменений через Session
Приведённый ниже пример кода демонстрирует шаги, необходимые для захвата набора изменений при выполнении SQL-команд. В кратком изложении:
Объект сессии (типа sqlite3_session) создаётся вызовом функции API sqlite3session_create(). Один объект сессии отслеживает изменения, вносимые в одну базу данных (то есть "main", "temp" или присоединённую базу данных) через один дескриптор базы данных sqlite3.
Объект сессии настраивается с набором таблиц для отслеживания изменений. По умолчанию объект сессии не отслеживает изменения ни в одной таблице базы данных. Прежде чем он начнёт это делать, его необходимо настроить. Существует три способа настроить набор таблиц для отслеживания изменений:
каждой таблицы,
вызвав sqlite3session_attach() с аргументом NULL,
каждую таблицу и который указывает модулю сессий, следует ли отслеживать изменения в этой таблице.
- явно указать таблицы, выполнив по одному вызову
sqlite3session_attach()для - указать, что все таблицы в базе данных должны отслеживаться на предмет изменений,
- настроить обратный вызов (callback), который будет вызываться при первой записи в
Приведённый ниже пример кода использует второй из перечисленных методов — он отслеживает изменения во всех таблицах базы данных.
Изменения вносятся в базу данных путём выполнения SQL-операторов. Объект сессии записывает эти изменения.
Блоб набора изменений извлекается из объекта сессии вызовом sqlite3session_changeset() (или, при использовании наборов патчей, вызовом функции
Объект сессии удаляется вызовом функции API sqlite3session_delete(). Удалять объект сессии после извлечения из него changeset или patchset не обязательно. Его можно оставить присоединённым к дескриптору базы данных, и он продолжит отслеживать изменения в настроенных таблицах, как и прежде.
Однако если sqlite3session_changeset() или sqlite3session_patchset() вызывается на объекте сессии второй раз, changeset или patchset будет содержать все изменения, произошедшие в соединении с момента создания сессии. Иными словами, объект сессии не сбрасывается и не обнуляется при вызове sqlite3session_changeset() или sqlite3session_patchset().
Применение changeset к базе данных SQLite
Применение набора изменений к базе данных проще, чем его захват. Как правило, достаточно одного вызова sqlite3changeset_apply(), как показано в приведённом ниже примере кода.
В случаях, когда это сложнее, трудности при применении набора изменений заключаются в разрешении конфликтов. Подробности см. в документации по API, ссылка на которую приведена выше.
Просмотр содержимого набора изменений
Пример кода ниже демонстрирует техники, используемые для итерации и извлечения данных, связанных со всеми изменениями в наборе изменений. Кратко:
API sqlite3changeset_start() вызывается для создания и инициализации итератора для перебора содержимого набора изменений. Изначально итератор не указывает ни на один элемент.
Первый вызов sqlite3changeset_next() на итераторе перемещает его так, чтобы он указывал на первое изменение в changeset (или на EOF, если changeset полностью пуст). sqlite3changeset_next() возвращает SQLITE_ROW, если перемещает итератор на допустимую запись, SQLITE_DONE, если перемещает итератор на EOF, или код ошибки SQLite, если возникает ошибка.
Если итератор указывает на допустимую запись, API sqlite3changeset_op() может использоваться для определения типа изменения (INSERT, UPDATE или DELETE), на которое указывает итератор. Кроме того, тот же API можно использовать для получения имени таблицы, к которой применяется изменение, а также ожидаемого количества столбцов и столбцов первичного ключа.
Если итератор указывает на допустимую запись INSERT или UPDATE, API sqlite3changeset_new() может использоваться для получения значений new.* в полезной нагрузке изменения.
Итератор удаляется с помощью вызова API sqlite3changeset_finalize(). Если в процессе итерации произошла ошибка, возвращается код ошибки SQLite (даже если тот же код ошибки уже был возвращён функцией sqlite3changeset_next()). Если же ошибок не возникло, возвращается SQLITE_OK.
Расширенная функциональность
Большинство приложений будут использовать только функциональность модуля сессий, описанную в предыдущем разделе. Однако для использования и манипулирования блобами changeset и patchset доступна следующая дополнительная функциональность.
Два или более changeset/patchset могут быть объединены с помощью интерфейсов sqlite3changeset_concat() или sqlite3_changegroup. На практике я нахожу эту возможность особенно полезной, когда нужно агрегировать изменения из нескольких независимых сессий перед их применением к целевой базе данных.
Changeset может быть «инвертирован» с помощью функции API sqlite3changeset_invert(). Инвертированный changeset отменяет изменения, внесённые оригиналом. Если changeset C+ является инверсией changeset C, то применение C, а затем C+ к базе данных должно оставить базу данных неизменной.
Типичные ошибки при работе с расширением Session
Работая с расширением session, я неоднократно сталкивался с одними и теми же проблемами, которые стоит учитывать заранее.
Отсутствие PRIMARY KEY в таблице. Session Extension полностью игнорирует таблицы без объявленного PRIMARY KEY. Если вы ожидаете, что изменения в такой таблице будут зафиксированы, — они не будут. Убедитесь, что все отслеживаемые таблицы имеют явный PRIMARY KEY.
NULL в столбцах PRIMARY KEY. Строки, у которых хотя бы один столбец PRIMARY KEY содержит NULL, не записываются модулем sessions. Это поведение не является ошибкой — оно задокументировано, — но легко упустить из виду при проектировании схемы.
Повторный вызов sqlite3session_changeset() без сброса сессии. Объект сессии не сбрасывается после первого вызова sqlite3session_changeset(). Повторный вызов вернёт все изменения с момента создания сессии, а не только те, что произошли после предыдущего вызова. Если нужна инкрементальная фиксация изменений, создавайте новый объект сессии после каждого извлечения.
Игнорирование конфликтов при применении changeset. Функция sqlite3changeset_apply() требует обработчика конфликтов. Если не реализовать его корректно, конфликтующие изменения могут быть молча пропущены или, напротив, вызвать прерывание всей операции применения.
Использование patchset там, где нужен changeset. Patchset компактнее, но не хранит исходные значения изменённых полей. Это означает, что ряд конфликтов при применении patchset просто не обнаруживается. Если надёжное обнаружение конфликтов критично — используйте changeset.
Часто задаваемые вопросы по Session Extension
Можно ли использовать Session Extension с виртуальными таблицами SQLite? Нет. Виртуальные таблицы не поддерживаются Session Extension, и изменения в них не фиксируются.
Что произойдёт, если одна строка была изменена несколько раз в рамках одной сессии? Все промежуточные изменения объединяются в одно итоговое изменение. Если строка была вставлена, а затем удалена в рамках одной сессии, Session Extension не сообщит ни о каком изменении вообще.
В чём принципиальная разница между changeset и patchset? Patchset компактнее, но не хранит исходные значения изменённых и удалённых полей. Из-за этого при применении patchset невозможно обнаружить ряд конфликтов, которые changeset обнаружил бы.
Как инвертировать changeset, чтобы отменить уже применённые правки? Используйте функцию API sqlite3changeset_invert(). Применение инвертированного changeset к базе данных, к которой уже был применён оригинальный changeset, вернёт базу данных в исходное состояние.
Как объединить несколько changesets в один? Используйте интерфейс sqlite3changeset_concat() или sqlite3_changegroup. Это позволяет агрегировать изменения из нескольких сессий перед их применением к целевой базе данных.



