атомарно заменить содержимое базы данных SQLite, желательно в SQL или python3 - PullRequest
0 голосов
/ 17 июня 2020

У меня есть сценарий python, который поддерживает параметр --edit-the-database для вызова редактора, предпочитаемого пользователем, для дампа базы данных SQLite сценария. Этот параметр предназначен для облегчения быстрого доступа к частям базы данных, к которым другие параметры сценария не предоставляют доступа, особенно во время разработки этого сценария.

После того, как сценарий сбросил содержимое базы данных, запустил Editor и убедился, что измененное содержимое по-прежнему актуально, затем необходимо заменить существующее содержимое базы данных.

Сначала он удаляет весь существующий контент, выполнив этот SQL (используя модуль sqlite python):

PRAGMA writable_schema = 1;
DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
PRAGMA writable_schema = 0;
VACUUM;

, а затем загружает новый контент с помощью модуля sqlite executescript() метод:

cursor.executescript(sql_slurped_from_user_modified_dump)

Проблема в том, что эти две операции (удаление существующего содержимого, загрузка нового содержимого) не выполняются атомарно: нажмите CTRL-C в неподходящий момент, и содержимое базы данных будет потеряно.

Если я попытаюсь выполнить эти два блока кода внутри транзакции, я получу сообщение об ошибке:

Error: cannot VACUUM from within a transaction

И если я сохраню транзакцию, но удалю VACUUM, тогда я получу ошибка:

Error: table first_table already exists

У меня есть уродливый обходной путь: перед вызовом редактора сценарий копирует файл дампа в безопасное место, пишет пользователю предупреждающее сообщение:

WARNING: if anything goes wrong then a backup of the database 
    can be found in /some/path

и, если сценарий продолжит работу и завершит загрузку нового содержимого, он удалит копию дампа. Но это довольно уродливо!

Я мог бы использовать DROP TABLE вместо DELETE FROM sqlite_master ..., но если я пытаюсь разрешить изменение базы данных таким образом, я разрешаю что сам список таблиц может меняться. Т.е. если пользователь добавляет это в дамп:

CREATE TABLE t3 (n INT);

, то жестко запрограммированный список DROP s, например:

BEGIN TRANSACTION
DROP TABLE t1;
DROP TABLE t2;
DROP INDEX ...
...
cursor.executescript(sql_slurped_from_user_modified_dump)
...
END TRANSACTION;

, не будет работать второй раз. round (потому что он не удаляет таблицу t3).

Я мог бы использовать операции filesystem-atomi c (например, что-то вроде: загрузить измененный дамп в новый файл базы данных; жесткая ссылка новый файл в старый), но для этого потребуется, чтобы сценарий закрыл соединение с базой данных и затем снова открыл его, что по причинам, выходящим за рамки этого вопроса, я бы предпочел не делать.

Есть ли у кого-нибудь есть ли лучшие идеи для атомарно замены всего содержимого базы данных, список таблиц которой непредсказуем?

1 Ответ

0 голосов
/ 18 июня 2020

На случай, если Google приведет вас сюда ...

Мне удалось выполнить первую половину задачи (удалить существующий контент внутри одной транзакции) с чем-то вроде этого псевдокода:

-- Make the order in which tables are dropped irrelevant. Unfortunately, this
-- cannot be done just around the table dropping because it doesn't work inside
-- transactions.
PRAGMA foreign_keys = 0;

BEGIN TRANSACTION;

indexes =  (SELECT name 
            FROM sqlite_master 
            WHERE type = 'index' AND 
                  name NOT LIKE 'sqlite_autoindex_%';)
triggers = (SELECT name 
            FROM sqlite_master 
            WHERE type = 'trigger';)
tables =   (SELECT name 
            FROM sqlite_master
            WHERE type = 'table';)

for thing in indexes+triggers+tables:
    DROP thing;

В этот момент я подумал, что вторая половина (загрузка нового содержимого в той же транзакции ) будет просто такой:

cursor.executescript(sql_slurped_from_user_modified_dump)

END TRANSACTION;
-- Reinstate foreign key constraints.
PRAGMA foreign_keys = 1;

К сожалению, нажатие CTRL- C в середине двух блоков приводит к пустой базе данных. Причина? cursor.executescript () выполняет немедленно COMMIT перед запускает предоставленный SQL. Это превращает приведенный выше код в две транзакции!

Это не первый раз, когда я попадаю в ловушку управления скрытыми транзакциями этого модуля, но на этот раз я был мотивирован попробовать Модуль apsw . Это переключение было удивительно простым. Вторая половина кода теперь выглядит так:

cursor.execute(sql_slurped_from_user_modified_dump)

END TRANSACTION;
-- Reinstate foreign key constraints.
PRAGMA foreign_keys = 1;

и работает отлично!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...