sqlite-utils 4.0rc2、ほぼClaude Fable製(約149.25ドルで)
原文は Simon Willison により に公開されました。 このブログを購読する
数週間前にsqlite-utils 4.0rc1のリリースについて書きました。Claude FableをMaxプランで使えるのもあと数日ということで、本当に納得できる形で4.0の安定版まで持っていけないか、試してみることにしました。私はSemVerを守るようにしており、互換性を壊すメジャーバージョンアップはできるだけ少なくしたいと考えているからです。
iPhone上の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
こちらが作ってくれた最初のレポートです。私自身まだ遭遇していなかった重大な問題がいくつかあり、そのうち5件はFableが「リリースブロッカー」に分類したものでした。中でも最悪だったのがこれです:
1.
delete_where()がコミットされずコネクションを汚染する(データ損失)
Table.delete_where()(sqlite_utils/db.py:2948)はDELETEをatomic()ラッパーなしの素のself.db.execute()で実行しています——正しくラップしているdb.py:2944のTable.delete()と比較してください。コネクションはin_transaction=Trueのまま残るため、その後のあらゆるatomic()呼び出しはセーブポイント側の分岐(db.py:430-440)に入り、やはりコミットされなくなります。end-to-endで再現できました:
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() # Reopen: rows are [0, 1, 2] — the delete, row 50, AND table u are all gone.
これは本当にひどいバグです!出荷しなくて本当によかった。とはいえ、少なくともこれは4.0.1のパッチリリースで直せるバグで、5.0を出さざるを得なくなるような設計上の欠陥ではなかったのが救いです。
37回のプロンプト、34回のコミット、30ファイルにわたる+1,321行追加・190行削除を経て、フィードバックを一つひとつ潰していき、その過程で他にもいくつかの設計改善を行いました。
コーディングエージェントの奇妙なところは、今回のような難しいタスクほど、かえって同時に別のことをする余裕が増えることです。エージェントが新しいタスクに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"}) # The new row is already saved - no commit() requireddb.execute()で実行する生のSQLについても同様で、書き込み文は実行されるとすぐにコミットされます。
commit()を呼ぶ必要はなく、変更を永続化するためにデータベースをクローズする必要もありません。トランザクションを意識する必要があるのは、正確に次の2つの場合だけです:
複数の書き込み操作をまとめ、すべて成功するかすべて失敗するようにしたい場合——db.atomic()を使ってください。
db.begin()で自分でトランザクションを管理している場合で、このときはあなたがコミットするまで何もコミットされません——ライブラリがあなたが開いたトランザクションを勝手にコミットすることはありません。
Fableのドキュメントをレビューしていて——ドキュメントの差分から先に見るのは、変更内容をざっと把握するのにとても良い方法だと感じています——こんな記述を見つけました:
db.atomic()とメソッドごとの自動トランザクションは、Pythonのデフォルトのトランザクション処理モードのコネクション向けに設計されています。Python 3.12以降のsqlite3.connect(..., autocommit=True)やautocommit=Falseオプションで作成されたコネクションはサポートされていません。そうしたコネクションではcommit()やrollback()の挙動が異なるためです。
正直なところ、Python 3.12で追加された比較的新しいautocommit設定に対してsqlite-utilsがどう振る舞うかは考えていませんでした。実際「そうしたコネクションでは挙動が異なる」というのは、ほぼテストスイート全体が失敗することを意味していたので、モデルと協力してこの違いがライブラリの動作を壊さないように対応しました。
そしてGPT-5.5による最終レビュー
以前は、あるモデルに別のモデルの仕事をレビューさせるという発想はどこか馬鹿げていると思っていました——妙に迷信めいて感じられたのです。ところが問題は、実際に効果があるということです。Anthropicの最良モデルにOpenAIの成果を、逆にOpenAIのモデルにAnthropicの成果をレビューさせることを習慣にするようになりました。面白い発見が得られることが、価値を感じるほど十分な頻度で起きているからです。
Codex DesktopとGPT-5.5 xhighに、こんなプロンプトを投げました:
Review changes since the last RC. Also confirm that the changelog is up-to-date.
それだけで、調査に値する2つの問題が見つかりました:
調査結果
- [P1] sqlite_utils/db.py:663
db.query()はdb.execute()を呼んだ後でなければ行を返さない文を拒否せず、sqlite_utils/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ドルで
残りわずかな期間、Fableの利用枠を増やすため、Claude Maxを月額100ドルから月額200ドルのプランにアップグレードしました。7月7日のFable終末を境に、Claude Max加入者であってもモデルのAPIコストを全額負担しなければならなくなるからです。
もしこれらのコストを直接支払っていたらいくらになっていたのか気になりました。Claude Code for webでリモート実行していたので、当初はその数値は手に入らないと思っていましたが、既存のセッション内でAgentsViewを実行すればコスト見積もりが取れることに気づいたのです!
Run "uvx agentsview --help" and then use that tool to calculate the cost of this session
Claudeはsession list --include-childrenコマンドの使い方を理解し、次の結果を出してきました:
| トランスクリプト | モデル | コスト |
|---|---|---|
| Main session | 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 |
このサブスクリプションに入っていて本当によかったです!本当は自分自身のアドバイスに従って、より安いモデルを使ったサブエージェントをもっと活用すべきでした。
claude.ai/settings/usageに現在表示されているのはこちらです:

ほかにもFableを活用した大きなプロジェクトがいくつか進行中で、値上げのタイミングにちょうどFableのバーが100%に達するようにすることを目指しています。
sqlite-utils 4.0rc2の完全なリリースノート
RCの完全なリリースノートはこちらです。各変更が入るたびに、Fableにchangelogの「Unreleased」セクションへ追記させ、それを都度レビューしました。これにはちょっとした副次効果があって、changelogのコミット履歴がリリースに含まれた各変更の簡潔な要約として機能しています。
以前はリリースノートは手書きするという方針でしたが、正直なところ、これらは自分で作るよりも出来が良いです。リリースノートは、退屈で、予測可能で、正確である必要があるため、エージェントに外注しても構わない文章の良い例だと思います。
破壊的変更:
db.execute()で実行される書き込み文は、すでにトランザクションが開いている場合を除き、自動的にコミットされるようになりました。その場合は既存のトランザクションに参加します。以前は暗黙のトランザクションが開かれ、何かがコミットするまで開いたままでした——同じコネクションで読む分には書き込みが成功したように見えても、コネクションを閉じると静かにロールバックされていました。未コミットのdb.execute()書き込みのロールバックに依存していたコードは、新しいdb.begin()メソッドで明示的なトランザクションを先に開くようにしてください。トランザクションモデルの詳細はTransactions and saving your changesに記載されています。db.query()は、返されたジェネレータが最初にイテレートされるのを待つのではなく、呼ばれた時点でSQLをすぐに実行するようになりました。行の取得自体はイテレーション中に遅延的に行われます。SQLエラーは呼び出し元で即座に送出されるようになり、INSERT ... RETURNINGのような文は結果をイテレートしなくても即座に実行・コミットされ、行を返さない文を渡すと——以前は静かに何も起きないだけでしたが——db.execute()の使用を推奨するValueErrorが送出されるようになりました。この方法で拒否された文は、エラーが送出される前にロールバックされるため、データベースには影響を与えません。- Python APIのバリデーションエラーは
AssertionErrorではなくValueErrorを送出するようになりました。以前は、列なしでの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フラグが削除されました。CSV/TSVデータの型検出は4.0a1以降デフォルトになっているため、このフラグは何もしていませんでした——使っていた場合は単に削除してください。検出を無効にする--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()が変更をコミットせず、コネクションを開いたトランザクション内に残したままにするバグを修正しました。その結果、それらの作業や後続の書き込みが、コネクションを閉じたときに静かにロールバックされる可能性がありました。3つとも他の書き込みメソッドと同様にdb.atomic()を使うようになりました。sqlite-utils drop-tableコマンドはビューを削除しようとしなくなり、drop-viewはテーブルを削除しようとしなくなりました。以前は名前が一致すれば誤った種類のオブジェクトを静かに削除していました。どちらも正しいコマンドを使うように促すエラーで終了するようになりました。- 新しいマイグレーションシステムによって適用されるマイグレーションは、マイグレーションが適用されたという記録とともにトランザクション内で実行されるようになりました。マイグレーションが例外を送出した場合、その変更はロールバックされ保留状態のままになるため、エラーを修正した後に安全に再適用できます。
VACUUMを実行するようなトランザクション内で実行できないマイグレーションは、@migrations(transactional=False)でオプトアウトできます——Migrations and transactionsを参照してください。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()コンテキストマネージャーの代替として使えます。- 新しいドキュメント: Transactions and saving your changesでトランザクションの仕組みと変更がコミットされるタイミングを説明し、新しいUpgradingページでメジャーバージョン間の移行に必要な変更を詳述しています。
記事をランダムに読む
コメント
ログインしてコメントする