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

Simon Willison's Blog 工具

摘要

sqlite-utils 4.0rc2 发布,很大程度上借助Claude Fable AI编写。该版本修复了事务处理中的关键错误,并包含了全面的文档。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/05 10:28

# sqlite-utils 4.0rc2,主要由 Claude Fable 编写(成本约 149.25 美元) 来源:https://simonwillison.net/2026/Jul/5/sqlite-utils-fable/ 2026 年 7 月 5 日 几周前我写了关于 `sqlite-utils` 4.0rc1 (https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/) 的发布文章。由于我们的 Max 订阅中 Claude Fable 仅剩下几天可用,我决定看看它能否帮助我完成一个自认为真正满意的 4.0 稳定版发布——因为我努力遵循语义化版本控制 (SemVer)(https://semver.org/),并且希望不兼容的主版本尽可能少出现。 我从这个提示开始,在 iPhone 上通过 Claude Code for web 操作: > `发布稳定版 4.0 前的最终审查——非常重要的是要发现任何最后一刻的问题,如果以后修复它们会构成破坏性变更` 以下是它为我的初始报告 (https://github.com/simonw/sqlite-utils/blob/0c369a447eeaf39084f0d14a45b3eeb7eacb631b/fable-review-4.0rc1.md)。其中存在一些我本人尚未遇到的**重大**问题——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() # 重新打开:行是 [0, 1, 2] —— 删除、第 50 行以及表 u 全部消失。`` 这是一个非常糟糕的 bug!很高兴我没有发布它,尽管至少它是一个可以在 4.0.1 补丁版本中修复的 bug,而不是一个会迫使发布 5.0 的设计缺陷。 经过 37 个提示、34 次提交和跨越 30 个独立文件的 +1,321 -190 代码变更,我们依次处理了整个反馈集,并沿途做了若干其他设计改进。 关于编码代理的一个奇怪之处是,像这样更困难的任务实际上提供了**更多**同时做其他事情的机会,因为代理有时需要 10-15 分钟来处理新任务。我出去享受了半月湾的国庆游行,偶尔从手机上查看进展并提示 Fable 执行下一步。 详细信息见 PR (https://github.com/simonw/sqlite-utils/pull/767) 和这个共享记录 (https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd)。对于最终审查,我切换到了笔记本电脑,通过 GitHub 的 PR 界面进行。 最重要的变更涉及事务处理,这是之前 RC (https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/#new-feature-db-atomic-transactions) 中的标志性新功能。新的 RC 现在包含关于新事务模型的全面文档 (https://sqlite-utils.datasette.io/en/latest/python-api.html#transactions-and-saving-your-changes),其开篇我将完整引用如下: > 本库中所有写入数据库的方法——`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()` (https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-transactions-execute) 执行的原始 SQL——写语句运行后即被提交。你永远不需要调用 `commit()`,也不需要关闭数据库来持久化更改。只有两种情况需要你考虑事务:1. 你想将多个写操作分组,使它们要么全部成功要么全部失败——使用 `db.atomic()` (https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-atomic)。2. 你通过 `db.begin()` 自己管理事务 (https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-transactions-manual),在这种情况下,在你提交之前什么都不会提交——库永远不会提交你打开的事务。 在审查 Fable 的文档时——我发现首先审查文档编辑是构建对变更理解的**绝佳**方式——我发现了这个细节 (https://github.com/simonw/sqlite-utils/blob/6c88067ab76b9597fb1c538c53164632526a2891/docs/python-api.rst?plain=1#L386): > `db.atomic()` 和自动的每方法事务是为 Python 默认事务处理模式下的连接设计的。使用 Python 3.12+ 的 `sqlite3.connect(..., autocommit=True)` 或 `autocommit=False` 选项创建的连接不受支持,因为 `commit()` 和 `rollback()` 在这些连接上的行为不同。 我承认之前没有考虑过 `sqlite-utils` 对较新的 autocommit 设置 (https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.autocommit)(Python 3.12 中新增)会如何反应。结果发现“在这些连接上行为不同”意味着几乎整个测试套件都会失败,所以我与模型合作确保这种差异 (https://github.com/simonw/sqlite-utils/commit/f7ff3e2027aefb9905ebb2e611e5bbb0a62382c5) 不会破坏库的工作方式。 #### 以及 GPT-5.5 的最终审查 我曾经认为让一个模型审查另一个模型的工作的想法有些荒谬——这感觉奇怪地迷信。问题是**它确实有效**——我已经习惯性地让 Anthropic 最好的模型审查 OpenAI 的工作,反之亦然,因为这样做已足够频繁地产生有趣的结果,值得做。 我提示 Codex Desktop 和 GPT-5.5 xhigh 如下: > `审查自上一个 RC 以来的变更。同时确认变更日志是最新的。` 这足以发现两个值得调查的问题: > **发现** - [P1] `sqlite_utils/db.py:663` (https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/sqlite_utils/db.py#L663) `db.query()` 现在仅在调用 `db.execute()` 后才拒绝非行语句,并且 `sqlite_utils/db.py:705` (https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/sqlite_utils/db.py#L705) 首先自动提交那些写操作。因此 `db.query("update ...")` 会引发 `ValueError`,但更新已经被提交了。对于一个文档描述为“只能用于返回行的 SQL”的方法,这是一个令人意外的副作用。 - [P1] `sqlite_utils/db.py:672` (https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/sqlite_utils/db.py#L672) 通过 `db.query()` 的 `INSERT ... RETURNING` 仅在返回的生成器完全耗尽后才提交。`db.query("insert ... returning ...")` 没有迭代,或者常见的 `next(db.query(...))` 用法,会导致事务保持打开状态,并且在关闭时写操作可能被回滚。这与 `docs/changelog.rst:15` (https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/docs/changelog.rst?plain=1#L15) 和 `docs/python-api.rst:232` (https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/docs/python-api.rst?plain=1#L232) 相矛盾,后者表示无需迭代即可生效。 我将这些粘贴到一个新的 Fable 会话中,它运行了一些实验来确认问题: > 两个发现均被确认。`db.query()` 首先调用 `self.execute()`,它会自动提交写操作,然后才检查 `cursor.description`——因此 `db.query("update ...")` 在引发 `ValueError` 之前已经提交了更新。而 `INSERT ... RETURNING` 的提交位于返回生成器的末尾,因此除非你耗尽迭代器,否则它永远不会触发——`next(db.query(...))` 或未迭代的调用会保持事务打开,违反了变更日志和文档的承诺。 这是修复的 PR (https://github.com/simonw/sqlite-utils/pull/768),以及完整的 Claude Code 记录 (https://claude.ai/code/session_012U3iRfJoTZ5vd22cBSF2nJ)。审查这段代码帮助我建立了 SQLite 事务语义边界情况的更好心智模型! #### 预估(未补贴)成本为 149.25 美元 我将 Claude Max 计划升级到每月 200 美元(之前是每月 100 美元),以增加我在剩余的 Fable 配额,直到 7 月 7 日的 Fable 末日 (https://www.anthropic.com/news/redeploying-fable-5),届时即使 Claude Max 订阅者也必须为这个模型支付完整的 API 费用。 我很好奇如果直接支付这些成本,我会花费多少。起初我认为这些数字我无法获取,因为我是通过 Claude Code for web 远程完成工作的,然后我意识到可以在现有会话中运行 AgentsView (https://www.agentsview.io/) 来获得成本估算! > `运行 "uvx agentsview --help",然后使用该工具计算本会话的成本` Claude 弄清楚了如何使用 `session list --include-children` 命令,并得出了以下结果: | 记录 | 模型 | 成本 | |------|------|------| | 主会话 | claude-fable-5 | $141.02 | | API 面扫描代理 | claude-fable-5 | $2.40 | | 事务/原子审查代理 | claude-fable-5 | $2.39 | | 后 rc1 提交审查代理 | claude-fable-5 | $1.72 | | 迁移审查代理 | claude-fable-5 | $1.40 | | 提示计数代理 | claude-opus-4-8 | $0.32 | | **总计** | | **$149.25** | 我很高兴自己在那份订阅上!我本当遵循自己的建议 (https://simonwillison.net/2026/Jul/3/judgement/),更多地依赖使用更便宜模型的子代理。 以下是我现在在 claude.ai/settings/usage (https://claude.ai/settings/usage) 上看到的内容: Claude 计划用量限制面板的截图:"Plan usage limits Max (20x)";"Current session" 显示 "Resets in 3 hr 52 min",进度条为 "7% used";"Weekly limits" 标题,带有 "Learn more about usage limits" 链接;"All models" 显示 "Resets Wed 12:00 PM",进度条为 "32% used";"Fable" 显示 "Resets Wed 12:00 PM",进度条为 "63% used"。 我现在同时在进行另外几个由 Fable 驱动的大型项目,目标是在 Fable 条达到 100% 时正好赶上价格上涨。 #### sqlite-utils 4.0rc2 的完整发布说明 以下是 RC 的完整发布说明 (https://sqlite-utils.datasette.io/en/latest/changelog.html#rc2-2026-07-04)。我让 Fable 随着每个变更的落地将它们添加到变更日志的“未发布”部分,并在过程中进行审查。这样做的一个巧妙副作用是,变更日志的提交历史 (https://github.com/simonw/sqlite-utils/commits/4.0rc2/docs/changelog.rst) 可以作为每个发布变更的简洁摘要。 过去我的策略是手动编写发布说明,但老实说,这些比我本人写的要好。发布说明是我乐意外包给代理来写的写作类型,因为它们需要乏味、可预测且准确。 > 破坏性变更: > - 使用 `db.execute()` 执行的写语句现在自动提交,除非已有事务打开,在这种情况下它们会加入该事务。以前它们会打开一个隐式事务,该事务保持打开直到某物提交它——写操作在同一连接上读取时似乎有效,但在连接关闭时被静默回滚。依赖于回滚未提交的 `db.execute()` 写操作的代码应使用新的 `db.begin()` 方法首先打开一个显式事务。事务模型在“事务与保存更改” (https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-transactions) 中完整记录。 > - `db.query()` 现在在其被调用时立即执行 SQL,而不是等到返回的生成器首次迭代。行仍然在迭代期间惰性获取。SQL 错误现在在调用点抛出,诸如 `INSERT ... RETURNING` 的语句立即执行并提交,无需迭代其结果,并且传递一个不返回行的语句——之前是静默无操作——现在会引发 `ValueError` 并建议使用 `db.execute()`。被这样拒绝的语句会在错误抛出前回滚,因此对数据库无影响。 > - Python API 验证错误现在引发 `ValueError` 而不是 `AssertionError`。以前无效参数——例如不带列的 `create_table()`、对不存在的表调用 `transform()`、或同时传递 `ignore=True` 和 `replace=True`——是使用裸 `assert` 语句拒绝的,这些语句在 Python 以 `-O` 标志运行时被静默跳过。针对这些情况捕获 `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` 命令在指向视图时会显示一个清晰的错误。 > - 从 `insert` 和 `upsert` 命令中移除了无操作的 `-d/--detect-types` 标志。类型检测自 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` 拒绝删除表。以前每个命令都会静默删除错误类型的对象(如果名称匹配)。现在两者都会退出并显示错误,建议使用正确的命令。 > - 由新的迁移系统 (https://sqlite-utils.datasette.io/en/latest/migrations.html#migrations) 应用的迁移现在在一个事务内运行,同时记录迁移已应用。如果迁移抛出异常,其更改被回滚并保持待定状态,因此可以在修复错误后安全地重新应用。不能在事务内运行的迁移会回滚,并显示一个建议用户手动运行的清晰错误消息。 > - 修复了 `db.query()` 在传递像 `INSERT ... RETURNING` 这样的写语句时,仅在调用者完全耗尽了返回的生成器后才会提交它的 bug。现在提交在调用时立即发生,与文档一致。

相似文章

sqlite-utils 4.0rc1

Simon Willison's Blog

sqlite-utils 4.0rc1 是 Python CLI 工具的一个发布候选版本,该工具简化了 SQLite 数据库操作。

sqlite-utils 4.0rc3

Simon Willison's Blog

sqlite-utils 4.0rc3 是一个候选发布版本,包含新功能,包括复合外键内省和不区分大小写的列匹配。

sqlite-utils 4.0rc1 新增迁移和嵌套事务

Simon Willison's Blog

sqlite-utils 4.0rc1 是一个候选发布版本,新增了内置数据库迁移(从 sqlite-migrate 移植而来)以及通过 db.atomic() 实现的嵌套事务,同时包含少量不向后兼容的更改。

SQLite 3.53.0

Simon Willison's Blog

SQLite 3.53.0 发布,带来重要的累积改进,包括 ALTER TABLE 约束修改、新的 JSON 函数(json_array_insert),以及通过新的查询结果格式化库实现的重大 CLI 模式增强。