python-3.x - 原子替换 SQLite 数据库的内容,最好用 SQL 或 python3
问题描述
我有一个 python 脚本,它支持--edit-the-database
在脚本的 SQLite 数据库的转储上调用用户首选编辑器的选项。此选项旨在促进对脚本的其他选项不提供访问权限的数据库部分的快速访问,特别是在开发此脚本期间。
一旦脚本转储数据库的内容,启动编辑器并验证修改的内容仍然有效,那么它需要替换现有的数据库内容。
首先,它通过执行此 SQL(使用 python 的 sqlite 模块)删除所有现有内容:
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
:
BEGIN TRANSACTION
DROP TABLE t1;
DROP TABLE t2;
DROP INDEX ...
...
cursor.executescript(sql_slurped_from_user_modified_dump)
...
END TRANSACTION;
不会第二次工作(因为它不会删除表 t3)。
我可以使用文件系统原子操作(例如:将修改后的转储加载到新的数据库文件中;将新文件硬链接到旧文件),但这需要脚本关闭其数据库连接并在之后重新打开它,这是出于某种原因超出这个问题的范围,我宁愿不做。
有没有人有更好的想法来原子地替换表列表不可预测的数据库的全部内容?
解决方案
万一谷歌把你带到这里......
我设法用这样的伪代码完成了任务的前半部分(删除单个事务中的现有内容):
-- 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()在运行提供的 SQL之前立即 COMMIT
执行。这将上面的代码变成了两个事务!
这不是我第一次被这个模块的隐藏事务管理所吸引,但这次我有动力尝试使用apsw模块。这个转换非常简单。代码的后半部分现在如下所示:
cursor.execute(sql_slurped_from_user_modified_dump)
END TRANSACTION;
-- Reinstate foreign key constraints.
PRAGMA foreign_keys = 1;
它完美无缺!
推荐阅读
- javascript - 在 JSON 中附加元素并且不要覆盖
- java - 如何使用 Jenkins 的 groovy 自动安装 Maven 和 Java JDK8?
- multithreading - 使用由`fork`创建的多个C线程的回调函数时,Rust Mutex不起作用
- sql - 递归地从同一个表的父子表中获取最后一条记录
- c++ - 如何获取 ERRORLOG.txt 中最深的调用堆栈条目?
- python - 方程组的解返回空集
- c++ - 从较大的数字类型转换为较小的数字类型时有哪些潜在的陷阱?
- javascript - 如何取消选择嵌套的 JSON 对象?
- laravel - Laravel - 预订系统的数据库设计
- groovy - 未定义变量时获取空响应数据