Python sqlite3 executescript:脚本后来报错,为什么 with 前面写入的记录仍然留下

前天 3阅读

导入程序先执行一条INSERT,再把几条SQL交给executescript。外面已经套with db,脚本最后也报错,查询却发现前两条记录都在。不能只看缩进判断事务范围:执行入口本身可能先提交已有事务。

本例在Linux、CPython 3.12.14、SQLite 3.53.1实跑。要求Python 3.12或更新版本,显式设置autocommit为LEGACY_TRANSACTION_CONTROL、isolation_level为DEFERRED,结论限定这个模式。保存为demo.py,运行python3 demo.py;数据库只在内存中。两轮仅更换后两句SQL的执行入口,故意查询不存在的列制造失败。

Python sqlite3 executescript:脚本后来报错,为什么 with 前面写入的记录仍然留下

AI模型生成概念插图:前面的记录已经越过提交关口,后方停止标记不能把它们自动带回;不是数据库控制台截图。

完整程序与本地结果

完整可运行程序

import sqlite3

def scenario(use_script):
    db = sqlite3.connect(
        ":memory:",
        autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL,
        isolation_level="DEFERRED",
    )
    try:
        db.execute("CREATE TABLE item(id INTEGER PRIMARY KEY)")
        try:
            with db:
                db.execute("INSERT INTO item VALUES(1)")
                assert db.in_transaction
                if use_script:
                    db.executescript("""
                        INSERT INTO item VALUES(2);
                        SELECT missing_column FROM item;
                    """)
                else:
                    db.execute("INSERT INTO item VALUES(2)")
                    db.execute("SELECT missing_column FROM item")
        except sqlite3.OperationalError:
            pass
        else:
            raise AssertionError("the deliberate query must fail")
        rows = db.execute("SELECT id FROM item ORDER BY id").fetchall()
        assert rows == ([(1,), (2,)] if use_script else [])
        assert not db.in_transaction
        label = "executescript" if use_script else "execute calls"
        print(label, "rows after error:", rows)
        print(label, "transaction open:", db.in_transaction)
    finally:
        db.close()

scenario(True)
scenario(False)

本次实际输出(以下为结果,不是程序)

executescript rows after error: [(1,), (2,)]
executescript transaction open: False
execute calls rows after error: []
execute calls transaction open: False

第一笔在脚本开始之前就已提交

executescript一轮最后留下[(1,), (2,)]。在指定LEGACY模式下,如果调用时存在待决事务,executescript会先执行隐式COMMIT。编号1因此在脚本开始前已提交,后来报错不能让已经结束的事务重新出现。

这个入口不会为整份脚本自动补BEGIN和COMMIT。本例脚本没有BEGIN,编号2的独立语句也完成提交,然后缺失列查询失败。异常在with外捕获,管理器确实有机会处理失败,但此时已没有包含这两笔改动的活动事务,输出的False与此相符。

逐条execute保留了共同的事务

第二轮普通INSERT在指定模式下处于同一个事务,查询失败后,with退出回滚两次写入,最后得到空列表。这里不是把execute描述成永远安全,而是在展示不同入口的事务行为必须与调用方的边界设计一致。

即便给脚本内部添加BEGIN,也只能管理脚本中新开始的事务,不能把先前已提交的编号1拉回来。如果前置操作与脚本必须原子完成,应在测试库中重新设计完整边界,例如统一执行方式与事务管理,再用中途失败验证所有预期改动是否一起回滚。

记录模式、调用位置与最终数据

排查时检查实际autocommit设置、调用前的in_transaction,以及脚本自身是否含有事务语句。错误出现在哪一条也要写清楚,不能凭最后一次异常推断前面全都没有生效。本文用两笔固定写入证明,调用前后都可能留下已提交数据。

executescript接收整段SQL字符串,本例全部为固定教学内容。业务值仍应使用支持参数绑定的入口,不能为了拼脚本直接拼接外部文本。内存实验只验证可见结果,没有模拟断电、锁竞争或迁移设备故障;其他事务模式也应独立重跑,不能删除参数后沿用这里的结论。

参考资料

资料核验日期:2026年10月2日。以上输出对应文中固定输入和明确运行版本,本地执行退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。