sqlite-utils 4.0rc2,主要由 Claude Fable 编写(花费约 149.25 美元)
sqlite-utils 4.0rc2, mostly written by Claude Fable (for about $149.25)
sqlite-utils 4.0rc2 发布,由 Claude Fable 辅助完成最终审查与修复。Fable 发现 `delete_where()` 存在数据丢失 bug,因未使用 `atomic()` 包装导致事务未提交。经 37 个 prompt、34 次提交、+1,321/-190 行变更后修复。GPT-5.5 进一步审查发现 `db.query()` 在非行语句前自动提交写入及 `INSERT ... RETURNING` 未迭代时不提交的问题。总成本 $149.25。新版本引入自动提交事务模型,并新增 `db.begin()`、`db.commit()`、`db.rollback()` 方法。
几周前我写了关于 sqlite-utils 4.0rc1 发布的文章。由于我们的 Max 订阅中 Claude Fable 的使用权限仅剩几天,我决定看看它能否帮助我发布一个真正让我放心的 4.0 稳定版——因为我尽量遵循 SemVer,并且希望不兼容的主版本越少越好。我在 iPhone 上通过 Claude Code for web 以这个 prompt 开始:
在发布稳定版 4.0 之前的最终审查——非常重要,要发现任何如果以后修复会变成 breaking change 的最后一刻问题。
这是它为我生成的初始报告。其中有一些我自己尚未遇到的重大问题——Fable 将其中的 5 个归类为"发布阻塞器"。以下是其中最严重的一个:
delete_where()从不提交并污染连接(数据丢失)Table.delete_where()(sqlite_utils/db.py:2948) 通过裸的self.db.execute()执行 DELETE,没有atomic()包装——对比db.py:2944的Table.delete(),后者正确包装了。连接保持in_transaction=True,因此后续每次atomic()调用都会走 savepoint 分支 (db.py:430-440),也从不提交。端到端复现:
这是一个非常严重的 bug!很高兴我没有发布这个版本,尽管至少它是一个可以在 4.0.1 补丁版本中修复的 bug,而不是一个会迫使升级到 5.0 的设计缺陷。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]——delete、row 50 以及表 u 全部丢失。
经过 37 个 prompt、34 次提交以及跨越 30 个独立文件的 +1,321/-190 代码变更,我们依次处理了全部反馈,并在此过程中做了若干其他设计改进。编码 agent 的一个奇怪之处在于,像这样更困难的任务实际上提供了更多同时做其他事情的机会,因为 agent 有时需要 10-15 分钟来处理新任务。我出去享受了半月湾的国庆游行,偶尔用手机查看进度并给 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 提供了以下 prompt:
审查自上次 RC 以来的变更。同时确认 changelog 是最新的。
这足以发现两个值得调查的问题:
发现 [P1]
sqlite_utils/db.py:663db.query()现在仅在调用db.execute()之后才拒绝非行语句,而sqlite_utils/db.py:705首先自动提交这些写入。因此db.query("update ...")会引发ValueError,但 update 已经提交。对于一个文档说明为"只能用于返回行的 SQL"的方法来说,这是一个令人惊讶的副作用。 [P1]sqlite_utils/db.py:672通过db.query()的INSERT ... RETURNING仅在返回的 generator 被完全耗尽后才提交。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 的提交位于返回的 generator 末尾,因此除非你耗尽迭代器,否则它永远不会触发——next(db.query(...)) 或未迭代的调用会使事务保持打开状态,这与 changelog 和文档的承诺相矛盾。
这是修复的 PR,以及完整的 Claude Code 对话记录。审查这段代码帮助我更好地建立了 SQLite 事务语义边界情况的心智模型!
成本
估计(无补贴)成本为 $149.25,我将 Claude Max 升级到了 $200/月计划(之前是 $100/月),以在截至 7 月 7 日 Fablepocalypse 的剩余时间内增加我的 Fable 配额——届时即使 Claude Max 订阅者也将不得不为模型支付完整的 API 成本。我很好奇如果我直接支付这些费用会花多少钱。起初我以为这些数字我无法获取,因为我使用 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 |
| Prompt 计数 agent | claude-opus-4-8 | $0.32 |
| 总计 | $149.25 |
我很高兴我订阅了这个计划!我真的应该遵循自己的建议,更多地依赖使用更便宜模型的子 agent。这是 claude.ai/settings/usage 目前显示给我的:
我目前还有几个其他主要的 Fable 驱动项目正在进行中,目标是在价格上涨前将 Fable 条用满到 100%。
sqlite-utils 4.0rc2 的完整发布说明
以下是该 RC 的完整发布说明。我让 Fable 在每次变更落地时将其添加到 changelog 的"Unreleased"部分,并随进展进行审查。这有一个很好的副作用:changelog 的提交历史充当了发布中每个变更的简洁摘要。过去我坚持手动编写发布说明,但老实说,这些比我自己的写得更好。发布说明是一个很好的例子,说明我乐意将其外包给 agent,因为它们需要枯燥、可预测且准确。
Breaking changes:
- 通过
db.execute()执行的写入语句现在会自动提交,除非已有打开的事务,在这种情况下它们会加入该事务。以前它们会打开一个隐式事务,该事务保持打开直到某些东西提交它——写入在同一连接上读取时看似有效,但在连接关闭时会被静默回滚。依赖回滚未提交的db.execute()写入的代码应使用新的db.begin()方法先打开一个显式事务。事务模型在"事务与保存更改"中有完整文档。 db.query()现在在调用时立即执行其 SQL,而不是等到返回的 generator 首次迭代。行仍然在迭代期间惰性获取。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拒绝删除表。以前如果名称匹配,每个命令都会静默删除错误类型的对象。两者现在都会退出并显示错误,建议使用正确的命令。- 由新迁移系统应用的迁移现在在事务内运行,同时记录迁移已应用。如果迁移引发异常,其更改会回滚并保持待处理状态,因此在错误修复后可以安全地重新应用。无法在事务内运行的迁移,例如执行
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()上下文管理器的替代方案。 - 新文档:"事务与保存更改"描述了事务如何工作以及更改何时提交;新的"升级"页面详细说明了在主要版本之间迁移所需的更改。
标签: projects, sqlite, sqlite-utils, annotated-release-notes, anthropic, claude, coding-agents, claude-code, agentic-engineering, gpt, claude-mythos-fable