post cover

sqlite-utils 4.0rc2:Claude Fable 用约 150 美元写出了一个开源库(2026-07-05)


本文为翻译/转载,原文使用 CC BY-NC-SA 4.0 协议发布。 原文作者:Simon Willison 原文标题:sqlite-utils 4.0rc2, mostly written by Claude Fable (for about $149.25) 原文链接:https://simonwillison.net/2026/Jul/5/sqlite-utils-fable/ 原文发布:2026-07-05 本博客不参与任何商业变现(含 ads / 付费 / affiliate),本译文遵循 CC BY-NC-SA 4.0 条款发布。

【译者按】

Simon Willison 是 Django 框架的联合创始人、Datasette 的作者,也是 AI 编程领域最坦诚的实践者之一。这篇文章之所以值得翻译,是因为它不像大多数 “AI 写代码” 的 demo 只展示成功路径——它完整记录了 AI agent 发现了一个极其严重的 bug(delete_where() 永不提交事务、导致数据静默丢失),并逐步修复的整个过程。它还涉及一个少有人谈但极其有用的实践:让一个 AI 模型去审查另一个 AI 模型写的代码。对中文圈正在大量使用 Claude Code、Cursor 等 AI 编程工具的开发者而言,这篇文章提供了一个真实、可复现的工作流参考——而不是又一个 PPT 上的人工智能神话。

【正文】

sqlite-utils 4.0rc2,主要由 Claude Fable 编写(约花费 $149.25)

几周前我写了关于 sqlite-utils 4.0rc1 发布的文章。由于我们 Claude Max 订阅中 Fable 的可用时间只剩下几天,我决定看看它能否帮我把一个稳定可靠的 4.0 版本推出来——我一直坚持遵守 SemVer 语义化版本规范,不希望不兼容的大版本改动太频繁。

我在 iPhone 上的 Claude Code for web 中输入了以下提示词:

「在发布稳定版 4.0 之前的最终审查——非常重要,一定要发现任何会导致未来破坏性变更的问题」

这是它为我生成的初始报告。Fable 发现了一些我自己都还没遇到过的 严重问题——其中 5 个被归类为”发布阻塞器”。下面是最严重的一个:

1. delete_where() 永不提交并污染数据库连接(数据丢失)

Table.delete_where()sqlite_utils/db.py:2948)通过裸 self.db.execute() 执行 DELETE,没有 atomic() 包装器——对比一下 Table.delete()db.py:2944),后者正确包装了。连接被保持在 in_transaction=True 状态,因此每次后续的 atomic() 调用都会走 savepoint 分支(db.py:430-440),并且也不会提交。

端到端复现:

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 现在为 True
db["t"].insert({"id": 50})
db["u"].insert({"a": 1})
db.close()
# 重新打开:rows 为 [0, 1, 2]——删除操作、第 50 行、AND 表 u 全部丢失!

这是一个非常糟糕的 bug!我很庆幸没有带着它发布,不过至少如果发布了,也可以在 4.0.1 补丁版本中修复——这不是那种会迫使你发布 5.0 的设计缺陷。

经过 37 轮提示、34 次提交、+1321/-190 行代码变更,跨越了 30 个独立文件,我们逐一处理了整套反馈,沿途还做了若干其他设计改进。

编码 agent 有一点很奇怪:像这样更复杂的任务实际上给了你更多时间去做其他事,因为 agent 有时需要 10-15 分钟来处理一个新任务。我在 Half Moon Bay 的 7 月 4 日国庆游行中出去享受时光,偶尔从手机上查看进度并给 Fable 发出下一步指令。

完整的细节在 PR这份共享的对话记录中。最终审查我切换到了笔记本,通过 GitHub 的 PR 界面进行。

最重大的变更:事务处理

最重要的变更是关于事务处理的——这是前一个 RC 版本的标志性新功能。新 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()

db.execute() 执行的原始 SQL 也是如此——写入语句在运行后立即提交。

你永远不需要调用 commit(),也不需要关闭数据库来持久化更改。只有两种情况需要你考虑事务:

  • 你想把多个写入操作分组在一起,让它们要么全部成功,要么全部失败——使用 db.atomic()
  • 你自己用 db.begin() 管理事务,在这种情况下,直到你提交时才会提交——库永远不会提交你打开的事务。

在审查 Fable 写的文档时——我发现先审查文档编辑是理解变更内容的绝佳方式——我注意到了一个细节:

db.atomic() 和自动的每个方法事务是为 Python 默认事务处理模式下的连接设计的。使用 Python 3.12+ 的 sqlite3.connect(..., autocommit=True)autocommit=False 选项创建的连接不被支持,因为在这些连接上 commit()rollback() 的行为不同。

我承认我之前没有想过 sqlite-utils 会如何应对 Python 3.12 新增的 autocommit 设置。结果”在这些连接上行为不同”几乎等于整个测试套件全部失败,所以我与模型一起工作确保这种差异不会破坏库的功能

GPT-5.5 的最终审查

我以前觉得让一个模型去审查另一个模型的工作有点荒谬——感觉像是一种奇怪的迷信。问题是它真的管用——我已经开始习惯性地让 Anthropic 最好的模型审查 OpenAI 的工作,反之亦然,因为这种做法足够频繁地产生了有价值的结果。

我在 Codex Desktop 和 GPT-5.5 xhigh 上输入了以下提示:

「审查自上次 RC 以来的变更。另外确认 changelog 是最新的。」

结果发现两个值得调查的问题:

发现

  • [P1] sqlite_utils/db.py:663db.query() 仅在调用 db.execute() 之后才拒绝非行返回语句,而 db.py:705 会先自动提交这些写入操作。所以 db.query("update ...") 会抛出 ValueError,但 update 已经提交了。对于一个文档中写明”只能用于返回行的 SQL”的方法来说,这是一个令人惊讶的副作用。

  • [P1] sqlite_utils/db.py:672:通过 db.query() 执行的 INSERT ... RETURNING 只有在返回的生成器被完全耗尽后才提交。db.query("insert ... returning ...") 如果不迭代,或者常见的 next(db.query(...)) 用法,会留下一个打开的事务,写入可能在关闭时被回滚。这与 docs/changelog.rst:15docs/python-api.rst:232 中声称的”无需迭代即生效”相矛盾。

我把这些问题粘贴到一个新的 Fable 会话中,它运行了一些实验来确认问题:

两个发现都被确认了。db.query() 首先调用 self.execute(),这会自动提交写入操作,然后才检查 cursor.description——所以 db.query("update ...") 在抛出 ValueError 之前就提交了 update。而 INSERT ... RETURNING 的提交位于返回的生成器末尾,所以除非你耗尽迭代器,否则它永远不会触发——next(db.query(...)) 或未迭代的调用会留下打开的事务,这与 changelog 和文档的承诺相矛盾。

这是修复的 PR,以及完整的 Claude Code 对话记录。审查这些代码帮助我对 SQLite 事务语义的边缘情况建立了更好的心智模型!

约 $149.25 的预估(非补贴)成本

我升级到了 Claude Max $200/月的套餐(之前是 $100/月),以在 7 月 7 日的”Fable 末日”——届时连 Claude Max 订阅用户也需要按完整 API 价格付费使用该模型——之前增加我的 Fable 配额。

我很好奇如果直接按用量付费,这次会花多少钱。起初我以为这个数据拿不到,因为我是远程用 Claude Code for web 运行的,后来我意识到可以在现有会话中运行 AgentsView 来获取成本估算!

我让它:

「运行 uvx agentsview --help,然后用这个工具计算本次会话的成本」

Claude 学会了如何使用 session list --include-children 命令,给出了以下结果:

对话模型成本
主会话claude-fable-5$141.02
API 表面扫描 agentclaude-fable-5$2.40
事务/原子性审查 agentclaude-fable-5$2.39
发布后 RC1 提交审查 agentclaude-fable-5$1.72
迁移审查 agentclaude-fable-5$1.40
提示计数 agentclaude-opus-4-8$0.32
总计$149.25

我很庆幸我在用那个订阅!我真的应该遵循自己的建议,更多地使用廉价模型的子 agent。

这是 claude.ai/settings/usage 目前显示的:

[图片:Fable 使用量显示距离 100% 的进度条]

我手头还有其他几个由 Fable 驱动的大项目在进行中,目标正好在涨价前达到那个 Fable 条的 100%。

sqlite-utils 4.0rc2 的完整发布说明

以下是 RC 的完整发布说明。我在每次变更落地时让 Fable 将它们添加到 changelog 的”Unreleased”部分,边添加边审查。这有一个很妙的副作用:changelog 的提交历史本身就是这次发布中每一个变更的简洁摘要。

过去我一直坚持手动写发布说明,但说实话,这些比我写的还好。发布说明是一个很好的例子,说明为什么我可以放心地把文字工作外包给 agent——因为它们需要的是枯燥、可预测、准确

破坏性变更:

  • 通过 db.execute() 执行的写入语句现在会自动提交,除非已有打开的事务,此时会加入该事务。之前它们会打开一个隐式事务,一直保持打开直到某事提交它——写入看起来在同一个连接上生效了,但连接关闭时会被静默回滚。依赖回滚未提交的 db.execute() 写入的代码应先用新的 db.begin() 方法打开显式事务。事务模型在”事务与保存更改”中有完整文档。

  • db.query() 现在在调用时立即执行其 SQL,而不是等到返回的生成器第一次迭代时才执行。行仍然在迭代期间懒加载。SQL 错误现在在调用处抛出,像 INSERT ... RETURNING 这样的语句会立即执行并提交,无需迭代其结果,并且传入一个不返回行的语句——以前是静默无操作——现在会抛出一个 ValueError,建议改用 db.execute()。以这种方式拒绝的语句在抛出错误前会被回滚,因此不会对数据库产生任何影响。

  • Python API 验证错误现在抛出 ValueError 而不是 AssertionError。以前无效参数——比如没有列的 create_table()、在不存在的表上调用 transform()、或同时传入 ignore=Truereplace=True——是通过裸 assert 语句拒绝的,当 Python 以 -O 标志运行时这些 assert 会被静默跳过。为这些情况捕获 AssertionError 的代码应改为捕获 ValueError

  • table.upsert()table.upsert_all() 现在在记录缺少任何主键列的值或主键值为 None 时会抛出 PrimaryKeyRequired。以前这样的记录——它们永远无法匹配现有行——会被静默插入为全新行,或者在插入已发生后触发令人困惑的 KeyError

  • db.enable_wal()db.disable_wal() 现在如果在事务打开时被调用,会抛出 sqlite_utils.db.TransactionError。以前它们会在更改日志模式时静默提交打开的事务,破坏了 db.atomic() 和用户管理事务的回滚保证。

  • View 类不再有 enable_fts() 方法。它之前只存在以抛出 NotImplementedError,因为视图不支持全文搜索——现在调用它会抛出 AttributeError,并且该方法不再出现在 API 参考中。sqlite-utils enable-fts 命令在指向视图时会显示一个清晰的错误。

  • 无操作的 -d/--detect-types 标志已从 insertupsert 命令中移除。自 4.0a1 起,类型检测已是 CSV/TSV 数据的默认行为,所以这个标志什么都不做——使用它的调用应该直接去掉该标志。--no-detect-types 仍然可用以禁用检测。

  • Database() 现在如果传入了使用 Python 3.12+ sqlite3.connect(..., autocommit=True)autocommit=False 选项创建的连接,会抛出 sqlite_utils.db.TransactionError。在这些连接上 commit()rollback() 的行为不同,以前会导致库所做的每一次写入在连接关闭时被静默丢弃。

其他所有变更:

  • 修复了 table.delete_where()table.optimize()table.rebuild_fts() 不提交更改、使连接保持在打开事务中的 bug。它们的工作——以及之后的任何写入——可能在连接关闭时被静默回滚。这三个方法现在都使用 db.atomic(),与其他写入方法一致。

  • sqlite-utils drop-table 命令现在拒绝删除视图,drop-view 拒绝删除表。以前如果名称匹配,每个命令都会静默删除错误类型的对象。两者现在都会退出并显示错误,建议使用正确的命令。

  • 由新的迁移系统应用的迁移现在在一个事务内运行,同时包含迁移已应用的记录。如果迁移引发异常,其更改被回滚并保持待处理状态,以便在修复错误后安全地重新应用。无法在事务内运行的迁移(如执行 VACUUM 的迁移)可以使用 @migrations(transactional=False) 选择退出——详见”迁移与事务”。

  • table.upsert()table.upsert_all() 现在能检测现有表的主键或复合主键,因此在已存在主键的表上执行 upsert 时不再需要 pk= 参数。

  • db.table(table_name).insert({}) 现在可用于向现有表中插入完全由默认值组成的行,使用 INSERT INTO ... DEFAULT VALUES。(#759)

  • sqlite-utils migrate 命令的改进:不匹配任何已知迁移的 --stop-before 值现在会报错而不再是静默忽略;--stop-before 现在能与仍使用旧版 sqlite_migrate.Migrations 类的迁移文件正确工作;--list 现在是只读操作,不再创建数据库文件或迁移追踪表。migrations.applied() 现在按应用顺序返回迁移。

  • 新增 db.begin()db.commit()db.rollback() 方法用于手动控制事务,作为 db.atomic() 上下文管理器的替代方案。

  • 新增文档:“事务与保存更改” 描述了事务如何工作以及更改何时提交;新增 “升级指南” 页面详细说明了不同主要版本之间迁移所需的变更。

【译者注】

  1. Claude Fable:Anthropic 推出的顶级编程 AI 模型,是 Claude Max 订阅($200/月)的专属模型。2026 年 7 月 7 日后,即使订阅用户也需要额外按 API 价格付费使用。

  2. AgentsView:Simon Willison 开发的工具,用于查看 Claude Code 会话的详细使用情况和成本明细。开源地址:https://github.com/simonw/agentsview

  3. 交叉审查实践:让 GPT-5.5 审查 Claude 写的代码(反之亦然)是一种在中文圈较少被讨论但非常实用的做法。它能利用不同模型的架构差异来发现各自的盲区,类似于软件开发中”不同团队做代码审查”的实践。

  4. $149.25 的成本含义:这是未经任何补贴的 API 按量计费成本,实际订阅用户通过 $200/月套餐获得了一定的费用摊销。对于个人项目来说,$150 完成一个开源库的全面审计和 30 个文件的改动,性价比相当惊人。

【延伸阅读】