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:663:db.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:15和docs/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 表面扫描 agent | claude-fable-5 | $2.40 |
| 事务/原子性审查 agent | claude-fable-5 | $2.39 |
| 发布后 RC1 提交审查 agent | claude-fable-5 | $1.72 |
| 迁移审查 agent | claude-fable-5 | $1.40 |
| 提示计数 agent | claude-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=True和replace=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标志已从insert和upsert命令中移除。自 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()上下文管理器的替代方案。 -
新增文档:“事务与保存更改” 描述了事务如何工作以及更改何时提交;新增 “升级指南” 页面详细说明了不同主要版本之间迁移所需的变更。
【译者注】
-
Claude Fable:Anthropic 推出的顶级编程 AI 模型,是 Claude Max 订阅($200/月)的专属模型。2026 年 7 月 7 日后,即使订阅用户也需要额外按 API 价格付费使用。
-
AgentsView:Simon Willison 开发的工具,用于查看 Claude Code 会话的详细使用情况和成本明细。开源地址:https://github.com/simonw/agentsview
-
交叉审查实践:让 GPT-5.5 审查 Claude 写的代码(反之亦然)是一种在中文圈较少被讨论但非常实用的做法。它能利用不同模型的架构差异来发现各自的盲区,类似于软件开发中”不同团队做代码审查”的实践。
-
$149.25 的成本含义:这是未经任何补贴的 API 按量计费成本,实际订阅用户通过 $200/月套餐获得了一定的费用摊销。对于个人项目来说,$150 完成一个开源库的全面审计和 30 个文件的改动,性价比相当惊人。