Watch at Ease看见世界,读懂生活。

科技 · 实用指南

SQLite RETURNING 取过一行仍无法提交?结束每个写入游标

拿到 ID 不一定代表语句执行完毕。重放写入或丢弃结果前,先比较单行、多行和两个游标的行为。

八种 RETURNING 读取方式与提交结果示意
原创实测示意图:CPython3.12.14/SQLite3.53.1 的八组临时文件结果,使用明确的旧式事务控制。 Watch at Ease

INSERT ... RETURNING 后 COMMIT 报“SQL statements in progress”时,先检查自己的未完成写入结果,别直接认定是其他进程锁住数据库。收到一个 ID 与语句执行完毕是两件事。

八组临时文件测试说明 fetchone 何时有效、为何不能推广,以及关闭游标为什么能让写入提交,却丢失应用所需的返回 ID。

1. 记录驱动与事务控制方

记录 Python、sqlite3.sqlite_version、完整 SQL、所有仍存活的结果游标和事务设置。测试使用 Linux 上的 CPython3.12.14 与 SQLite3.53.1,明确设置 autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL 和 isolation_level="DEFERRED"。结果仅针对标准库 sqlite3,不代表 APSW 或 ORM。

保留完整异常和失败阶段。SQLite 允许有未完成 SELECT 时执行显式 COMMIT,但未完成写入可能阻止提交。检查本连接的 RETURNING 写入前,不要把所有 SQLITE_BUSY 都归为外部争用或先加大超时。也不要通过切换事务模式意外提交。

2. 单行成功不是通用规则

每组测试都创建新的文件和 t(x integer) 表。未读取的 INSERT INTO t VALUES(1) RETURNING x 阻止提交;在这个驱动中,用 fetchone() 取出唯一一行后可提交。但 INSERT INTO t VALUES(1),(2),(3) RETURNING x 只取一行后,提交仍然失败。

另两组三行样本分别用 fetchall()、反复 fetchmany(1) 直至空批次,均可提交,重新打开后保留全部三个值。继续前应保存每个所需 ID。不能假定返回顺序来配对输入:SQLite 不保证 RETURNING 顺序。分批读取限制的是 Python 列表大小,不能消除引擎已缓冲的 RETURNING 输出。

3. 结束所有写入游标,保留所需输出

第六组关闭未读取的三行游标后,提交成功,重新打开可见三个插入值,但应用没有收集到任何返回行。因此 Cursor.close() 不能代替回滚,也不能代替交付所需 ID。只有明确决定放弃这些输出时,才采用这种处理。

另外两组在同一连接保留两个 INSERT ... RETURNING 游标。读取完第一个后,第二个仍未完成,提交失败;两个都读完后提交成功,重新打开可见值 1 和 99。由事务控制组件保留游标引用,不要让辅助函数返回一个可见结果,却悄悄留下另一个写入结果。

4. 返回结果与提交分开验收

所有提交失败的测试在尝试后,in_transaction 仍为 true。测试随后关闭未完成游标并明确回滚,重新打开后未见插入行。空结果来自主动清理,不是提交异常自动撤销了写入。事务仍活跃时重放 INSERT,可能执行额外写入。

需要返回结果时,应先读取并验证相关输出、关闭游标,再由约定的事务控制方提交。从新连接核对持久化值,并保留准确行数的回归测试。出错时遵循应用回滚策略,不要因为拿到 ID 就报告成功。本次没有测试并发读者、触发器、大量输出或其他 autocommit 模式,应分别诊断这些情形。

读取返回行、结束游标与提交的区别示意
原创诊断示意图:输出交付、语句结束和事务提交分别验收;关闭游标没有撤销已执行的插入。 Watch at Ease