sqlite-utils 4.0rc2:一次由编码代理主导的发布候选修复
背景
sqlite-utils 4.0rc1 发布后,作者希望在进入 4.0 稳定版之前再做一次严格审查。由于 Claude Fable 在 Max 订阅中的可用时间即将结束,他尝试让 Claude Code for web 协助完成最终检查,重点寻找那些如果日后再修复就可能构成破坏性变更的问题。
最初的提示词是:
Final review before shipping a stable 4.0 release - very important to spot any last minute things that would be a breaking change if we fix them later
Claude Fable 随后生成了一份审查报告,其中列出了若干严重问题,包括 5 个被归类为“发布阻塞”的问题。
一个严重的事务提交缺陷
其中最严重的问题出现在 Table.delete_where():它通过裸 self.db.execute() 执行 DELETE,但没有使用 atomic() 包装。相比之下,Table.delete() 使用了正确的事务包装。
这会导致连接保持在 in_transaction=True 状态,后续每一次 atomic() 调用都会进入保存点分支,最终也不会真正提交。
示例问题流程如下:
db = sqlite_utils.Database("dw.db")
db["t"].insert_all([{"id": i} for i in range(3)], pk="id")
db["t"].delete_where("id = ?", [0]) # conn.in_transaction is now True
db["t"].insert({"id": 50})
db["u"].insert({"a": 1})
db.close()
# 重新打开后:rows 仍为 [0, 1, 2]
# delete、id=50 的插入、以及表 u 都消失了
这是一个可能导致数据丢失的严重 bug。虽然它可以在 4.0.1 中修复,不一定迫使项目进入 5.0,但在稳定版发布前发现仍然很关键。
37 次提示、34 个提交与 30 个文件改动
围绕这轮审查,作者与 Claude Fable 进行了 37 次提示交互,完成 34 个提交,涉及 30 个文件,代码变更规模为 +1,321 -190。
这轮修复不只处理了报告中的缺陷,也带来了一些设计层面的改进。作者提到,编码代理在执行较难任务时有时需要 10 到 15 分钟,因此反而让人可以并行处理其他事情;他在 Half Moon Bay 的 7 月 4 日游行期间,也通过手机不时检查进展并给出下一步提示。
事务模型成为重点变化
这次 RC 中最重要的变化集中在事务处理上。新版文档明确说明:库中所有会写入数据库的方法都会在自己的事务中运行,并在返回前提交。
这些方法包括:
insert()upsert()update()delete()delete_where()transform()create_table()create_index()enable_fts()- 以及其他写操作
示例:
db = Database("data.db")
db.table("news").insert({"headline": "Dog wins award"})
# 新行已经保存,无需调用 commit()
原始 SQL 通过 db.execute() 执行写语句时,也会在运行后立即提交。
文档强调,一般情况下用户不需要手动调用 commit(),也不需要依赖关闭数据库来持久化变更。只有两种情况需要显式考虑事务:
- 希望将多个写操作组合在一起,要么全部成功,要么全部失败:使用
db.atomic()。 - 使用
db.begin()自己管理事务:此时库不会提交用户主动开启的事务,直到用户自行提交。
Python 3.12 autocommit 带来的兼容问题
审阅文档时,作者注意到一个细节:db.atomic() 和自动按方法提交的事务模型,是为 Python 默认事务处理模式下的连接设计的。
Python 3.12 新增了 sqlite3.connect(..., autocommit=True) 和 autocommit=False 选项。由于这些连接上的 commit() 与 rollback() 行为不同,最初的实现不支持它们。
实际测试发现,这种差异会导致几乎整个测试套件失败。随后作者与模型继续修复,确保这些差异不会破坏库的行为。
用 GPT-5.5 再审查 Claude Fable 的工作
作者还使用 Codex Desktop 和 GPT-5.5 xhigh 对 Claude Fable 的改动做交叉审查,提示词是:
Review changes since the last RC. Also confirm that the changelog is up-to-date.
这次审查发现了两个值得调查的问题:
db.query()会先调用db.execute(),再判断语句是否返回行。因此db.query("update ...")会在抛出ValueError之前就已经提交更新。这与该方法“只能用于返回行的 SQL”的定位不一致。- 通过
db.query()执行INSERT ... RETURNING时,提交动作位于返回生成器的末尾。若用户不迭代结果,或只调用next(db.query(...)),事务可能保持打开,并在关闭连接时回滚。这与文档和变更日志中的描述相矛盾。
这些发现随后被提交给新的 Claude Fable 会话进行实验验证。实验确认两个问题都存在,并进一步修复。
作者认为,用一个模型审查另一个模型的工作虽然听起来有些“迷信”,但实践中确实能发现有价值的问题,因此他开始习惯让 Anthropic 和 OpenAI 的强模型互相审查对方产出的代码。
估算直接调用成本:149.25 美元
作者升级到了 Claude Max 每月 200 美元方案,以便在 Fable 计费调整前尽可能使用更多额度。他随后使用 AgentsView 在已有会话中估算,如果直接按 API 成本支付,这轮工作大约需要多少钱。
估算结果如下:
| 会话 | 模型 | 成本 |
|---|---|---|
| 主会话 | claude-fable-5 | 141.02 美元 |
| API surface sweep agent | claude-fable-5 | 2.40 美元 |
| Transactions/atomic review agent | claude-fable-5 | 2.39 美元 |
| Post-rc1 commits review agent | claude-fable-5 | 1.72 美元 |
| Migrations review agent | claude-fable-5 | 1.40 美元 |
| Prompt-counting agent | claude-opus-4-8 | 0.32 美元 |
| 合计 | 149.25 美元 |
作者也提到,自己本应更多使用成本较低的子代理模型。
sqlite-utils 4.0rc2 的主要变更
破坏性变更
- 使用
db.execute()执行写语句时,现在会自动提交,除非已有事务打开;此前它会开启隐式事务,并可能在连接关闭时静默回滚。 db.query()现在会在被调用时立即执行 SQL,而不是等到返回的生成器首次迭代时才执行。行仍然会在迭代时懒加载。传入不返回行的语句会抛出ValueError,并建议改用db.execute();被拒绝的语句会在报错前回滚。- Python API 的参数校验错误现在抛出
ValueError,不再使用AssertionError。此前裸assert在 Python 使用-O运行时会被跳过。 table.upsert()和table.upsert_all()在记录缺少主键值或主键为None时,会抛出PrimaryKeyRequired。db.enable_wal()和db.disable_wal()如果在事务打开时调用,会抛出sqlite_utils.db.TransactionError,避免切换 journal mode 时意外提交事务。View类不再包含只会抛出NotImplementedError的enable_fts()方法;对视图调用相关能力时会得到更清晰的错误。insert和upsert命令中无实际作用的-d/--detect-types参数被移除。CSV/TSV 类型检测自 4.0a1 起已是默认行为,仍可使用--no-detect-types禁用。Database()现在会拒绝传入 Python 3.12+ 中使用autocommit=True或autocommit=False创建的连接,并抛出TransactionError。
其他修复与改进
- 修复
table.delete_where()、table.optimize()和table.rebuild_fts()不提交变更的问题。这些方法现在都使用db.atomic()。 sqlite-utils drop-table现在拒绝删除视图,drop-view也拒绝删除表;此前如果名称匹配,可能静默删除错误类型的对象。- 新的迁移系统中,迁移操作与“已应用迁移”的记录会在同一事务中执行。若迁移抛出异常,其改动会回滚,迁移仍保持待执行状态。无法在事务中运行的迁移可通过
transactional=False选择退出。 table.upsert()和table.upsert_all()现在可以检测已有表的主键或复合主键,因此向已有主键表执行 upsert 时不再必须提供pk=参数。db.table(table_name).insert({})现在可用于向已有表插入一行全部由默认值组成的数据,底层使用INSERT INTO ... DEFAULT VALUES。sqlite-utils migrate命令改进:无法匹配任何迁移的--stop-before现在会报错;--stop-before可正确处理仍使用旧sqlite_migrate.Migrations类的迁移文件;--list现在是只读操作,不再创建数据库文件或迁移跟踪表。- 新增
db.begin()、db.commit()和db.rollback(),用于手动控制事务,作为db.atomic()上下文管理器之外的选择。 - 新增事务模型和升级指南相关文档。
值得关注的经验
这次发布候选版本展示了编码代理在复杂代码审查和发布准备中的实际价值:
- 它发现了人工尚未遇到的严重事务问题。
- 它帮助补齐了文档、变更日志和边界行为。
- 另一个模型的交叉审查继续发现了高优先级问题。
- 成本估算显示,这类深度代理协作若按 API 直接付费,费用并不低。
对维护基础库的开发者而言,尤其值得注意的是:事务语义、生成器惰性执行、自动提交、错误时回滚、Python 版本差异等边界问题,很适合作为发布前模型审查的重点清单。
