sqlite-utils 4.0rc2、主にClaude Fable(約149.25ドル分)が執筆
Simon Willison は Claude Fable を活用して sqlite-utils 4.0 のリリース前レビューを行い、重大なデータ消失バグを発見・修正した。
キーポイント
AI による重大バグの発見
Claude Fable が生成したレポートにより、開発者自身が見逃していた「delete_where()」関数のトランザクション不整合という致命的な欠陥が特定された。
コスト対効果の高い活用事例
約 149.25 ドル(数日間の Max サブスクリプション)の費用で、リリース前の最終チェックとして AI を活用し、SemVer に基づく堅牢な安定版リリースを達成した。
技術的詳細:接続ポイズニング
delete_where() が atomic() ワッパーなしで実行され、接続状態が不整合になり、その後のすべての操作(挿入やテーブル作成)がコミットされないという深刻な挙動を示した。
AI コードレビューの限界と可能性
エンドツーエンドの再現コードを含む詳細なレポートを生成し、人間では気づきにくい論理的欠陥を検出できる AI の実用性を示している。
影響分析・編集コメントを表示
影響分析
この記事は、LLM をソフトウェア開発の品質保証プロセスに組み込む具体的な成功事例を示しており、特に「人間が見落としがちな論理バグ」を検出する能力を証明しています。また、AI ツールへの短期的な投資(サブスクリプション利用)が、重大なリリース後のデータ損失リスクを防ぐという明確な ROI を生むことを示唆しており、開発現場における AI 活用戦略の参考となります。
編集コメント
AI が単なるコード生成だけでなく、複雑なシステム状態の解析やバグ発見という「品質保証」領域で人間を補完する役割を果たす実例です。開発者が AI に信頼を寄せ、最終的なリリース判断に活用した点は、今後の開発ワークフローの変化を示唆しています。
数週間前に sqlite-utils 4.0rc1 のリリースについて記事を書きました。Claude Fable は Max サブスクリプションであと数日しか利用できないため、SemVer を遵守し、互換性のないメジャーバージョンを可能な限り稀に保とうとする私の信念に基づき、本当に安心できる 4.0 安定版リリースに至るまで支援してくれるか試してみることにしました。
iPhone のウェブ版 Claude Code で、以下のプロンプトから始めました:
**
安定版 4.0 リリースを公開する前の最終レビュー - 後で修正すると破壊的変更となるような最後の瞬間の事象を見逃さないことが極めて重要です
**
これが彼が作成してくれた 最初のレポート です。私自身はまだ遭遇していなかったいくつかの*重大な*問題が見つかりました。そのうち Fable 氏が「リリース阻止要因」と分類したものは 5 つありました。その中でも特に深刻なものを以下に挙げます:
- delete_where() がコミットせず、接続を汚染する(データ損失)**
Table.delete_where() (sqlite_utils/db.py:2948) は、atomic() ラッパーなしで裸の self.db.execute() を通じて DELETE を実行します。これは db.py:2944 の Table.delete() と対比されますが、こちらは正しくラップされています。その結果、接続は in_transaction=True のまま残され、すべての*後続*する atomic() 呼び出しがセーブポイント分岐(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 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 分を要することがあるからです。私はハーフムーンベイの 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() を呼び出す必要はなく、変更を永続化するためにデータベースを閉じる必要もありません。トランザクションについて考える必要があるのは、ちょうど 2 つの状況だけです:
- 複数の書き込み操作をまとめて実行し、すべて成功するか、すべて失敗するようにしたい場合は、db.atomic() を使用してください。
- db.begin() で自分でトランザクションを管理している場合、明示的にコミットするまで何もコミットされません。ライブラリがあなたが開いたトランザクションを勝手にコミットすることはありません。
Fable のドキュメントを見直した際、変更点の初歩的な理解を築くための最良の方法の一つは、まずドキュメントの編集履歴を確認することだと感じました。私は この詳細 に気づきました:
db.atomic() と自動的なメソッドごとのトランザクションは、Python のデフォルトのトランザクション処理モードにおける接続向けに設計されています。Python 3.12+ の sqlite3.connect(..., autocommit=True) または autocommit=False オプションで作成された接続には対応していません。なぜなら、それらの接続では commit() と rollback() の動作が異なるためです。
私は、sqlite-utils が Python 3.12 で追加されたより最近の 自動コミット設定 にどのように反応するかについては考えていませんでした。結果として、「それらの接続で異なる動作をする」というのは、ほぼすべてのテストスイートが失敗することを意味しており、私はモデルと協力して この違い がライブラリの動作を壊さないようにしました。
GPT-5.5 による最終レビュー
以前は、あるモデルが別のモデルの作業を検証するという考えは少しばかげていると感じていました - 奇妙な迷信のように思えました。問題は*実際に機能する*ことです - 私はAnthropic の最良のモデルに OpenAI の作業を検証させ、その逆も行う習慣を身につけました。なぜなら、興味深い結果が得られることが十分に多く、価値があるからです。
私は Codex Desktop と GPT-5.5 xhigh に以下のプロンプトを与えました:
「最後の RC 以降の変更点をレビューしてください。また、変更履歴(changelog)が最新であることを確認してください。」
これだけで、調査する価値のある 2 つの問題が見つかりました:
発見事項
- [P1] sqlite_utils/db.py:663 の db.query() は、db.execute() を呼び出した後にのみ非行文(non-row statements)を拒否し、sqlite_utils/db.py:705 ではそれらの書き込みが最初に自動コミットされます。そのため、db.query("update ...") を実行すると ValueError が発生しますが、更新はすでにコミットされています。これは「行を返す 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 を発する前に更新をコミットしてしまいます。また、INSERT ... RETURNING のコミットは返されたジェネレータの末尾に存在していたため、イテレータを使い果たさない限り実行されず — next(db.query(...)) または反復処理を行わない呼び出しではトランザクションが開いたままとなり、変更履歴やドキュメントが約束している内容と矛盾します。
修正を含む PR と、Claude Code の完全な転記 です。このコードをレビューしたことで、SQLite のトランザクションセマンティクスにおけるエッジケースのより良いメンタルモデルを構築できました!
推定(補助金なし)コスト $149.25 で
私は Claude Max の月額$200プランにアップグレードしました(以前は月額$100でした)。これは、Claude Max サブスクライバーもこのモデルに対して API 料金を全額支払わなければならない 7月7日のフェイブル・アポカリプス までの残り期間における、私の Fabel アロケーションを増やすためです。
もし私がこれらのコストを直接支払っていた場合、いくらかかるのか気になりました。最初は、Claude Code for web を使用してリモートで作業を行ったため、その数値が利用できないのではないかと考えましたが、その後、既存のセッション内で AgentsView を実行すれば、そのコスト見積もりを取得できることに気づきました!
「uvx agentsview --help」を実行し、そのツールを使ってこのセッションのコストを計算してください
Claude はセッションリストの--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
私はそのサブスクリプションに加入していて本当に良かったと思います。実際、自分のアドバイスに従い、より安価なモデルを活用するサブエージェントにより強く依存すべきでした。
現在、claude.ai/settings/usage で表示されているのは以下の通りです:

また、Fable を駆使した他の主要プロジェクトも複数進行中で、価格引き上げの直前にその Fable のバーを 100% に到達させることを目指しています。
sqlite-utils 4.0rc2 の完全リリースノート
RC 用の 完全なリリースノート をここに掲載します。私は Fable に、各変更が適用されるたびにこれらを変更履歴の「未公開」セクションに追加させ、その都度レビューを行いました。この手法には、changelog のコミット履歴 が、リリースに含まれる各変更の簡潔な要約として機能するという素晴らしい副次的効果があります。
過去には私は手書きでリリースノートを作成する方針を持っていましたが、正直に言って、これらは私が自分で作成したものよりも優れています。リリースノートは、退屈で予測可能かつ正確である必要があるため、エージェントへのアウトソーシングを許容できる文章の好例です。
破壊的変更:
- db.execute() で実行された文は、すでにトランザクションが開かれている場合はそのトランザクションに追加されるようになります。それ以外の場合は自動的にコミットされます。以前は暗黙的なトランザクションが開始され、明示的にコミットされるまで開いたままとなり、同じ接続で読み取った際には書き込みが機能しているように見えたものの、接続が閉じると静かにロールバックされていました。コミットされていない db.execute() による書き込みのロールバックに依存していたコードは、明示的なトランザクションを開くために新しい db.begin() メソッドを使用する必要があります。トランザクションモデルの詳細については、「Transactions and saving your changes」を参照してください。
- db.query() は、返されたジェネレーターが最初に反復処理されるのを待たず、呼び出された直ちに SQL を実行します。行の取得は依然として反復処理中に遅延読み込みされます。SQL エラーは呼び出し元で即座に発生し、INSERT ... RETURNING などの文は結果を反復処理する必要なく即座に実行されコミットされます。また、行を返さない文を渡した場合(以前は静かな無操作だった)は ValueError が発生し、代わりに db.execute() の使用が推奨されます。このように拒否された文はエラーが発生する前にロールバックされるため、データベースには影響しません。
- 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 コマンドもテーブルを削除しようとするのを拒否します。以前は名前が一致した場合にそれぞれ誤った種類のオブジェクトを静かに削除していましたが、現在は正しいコマンドの使用を促すエラーで終了します。
新しい migrations システム によって適用されるマイグレーションは、現在トランザクション内で実行され、マイグレーションが適用されたという記録とともに処理されます。もしマイグレーションで例外が発生した場合、その変更はロールバックされ、マイグレーションは未完了のまま残るため、エラー修正後に安全に再適用できます。VACUUM を実行するなど、トランザクション内で実行できないマイグレーションについては、@migrations(transactional=False) を使用して除外することも可能です - 詳しくは <a href="https://sqlite-utils.datasette.io/en/latest/migration
原文を表示
I wrote about the sqlite-utils 4.0rc1 release a couple of weeks ago. Since we only have Claude Fable on our Max subscriptions for a few more days, I decided to see if it could help me get to a 4.0 stable release that I felt truly comfortable about, since I try to keep to SemVer and like my incompatible major versions to be as rare as possible.
I started with this prompt, in Claude Code for web on my iPhone:
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
Here's that initial report it created for me. There were some *significant* problems that I hadn't myself encountered yet - 5 that Fable categorized as "release blockers". Here's the worst of the bunch:
1. delete_where() never commits and poisons the connection (data loss)
Table.delete_where() (sqlite_utils/db.py:2948) runs its DELETE via a bare self.db.execute() with no atomic() wrapper — compare Table.delete() at db.py:2944, which wraps correctly. The connection is left in_transaction=True, so every subsequent atomic() call takes the savepoint branch (db.py:430-440) and never commits either.
Reproduced 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.
That's a really bad bug! Very glad I didn't ship that, although at least it would have been a bug I could fix in a 4.0.1 point release, not a design flaw that would force a 5.0.
Over the course of 37 prompts, 34 commits and +1,321 -190 code changes over 30 separate files, we worked through the entire set of feedback in turn, making several other design improvements along the way.
A weird thing about coding agents is that harder tasks like this one actually provide *more* opportunity to do other things at the same time, since the agent sometimes needs 10-15 minutes to churn away on a new task. I went out to enjoy the Half Moon Bay 4th of July parade, occasionally checking in and prompting the next step for Fable from my phone.
Full details [in the PR](https://github.com/simonw/sqlite-utils/pull/767) and [this shared transcript](https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd). I switched to my laptop for the final review, which I conducted through GitHub's PR interface.
The most significant changes relate to transaction handling, which was the signature new feature in [the earlier RC](https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/#new-feature-db-atomic-transactions). The new RC now includes [comprehensive documentation](https://sqlite-utils.datasette.io/en/latest/python-api.html#transactions-and-saving-your-changes) on the new transaction model, the intro to which I'll quote here in full:
> Every method in this library that writes to the database - insert(), upsert(), update(), delete(), delete_where(), transform(), create_table(), create_index(), enable_fts() and the rest - runs inside its own transaction and commits it before returning. Your changes are saved to disk as soon as the method call finishes:
> ```
db = Database("data.db")
db.table("news").insert({"headline": "Dog wins award"})
# The new row is already saved - no commit() requiredThe same applies to raw SQL executed with db.execute() - a write statement is committed as soon as it has run.
You never need to call commit(), and you do not need to close the database to persist your changes. There are exactly two situations where you need to think about transactions:
You want to group several write operations together, so they either all succeed or all fail - use db.atomic().
You are managing a transaction yourself with db.begin(), in which case nothing is committed until you commit - the library will never commit a transaction you opened.
In reviewing Fable's documentation - I find that reviewing the documentation edits first is an *excellent* way to build an initial understanding of what has changed - I spotted this detail:
db.atomic() and the automatic per-method transactions are designed for connections in Python's default transaction handling mode. Connections created with the Python 3.12+ sqlite3.connect(..., autocommit=True) or autocommit=False options are not supported, because commit() and rollback() behave differently on those connections.
I admit I hadn't thought about how sqlite-utils would react to the more recent autocommit setting, added in Python 3.12. It turns out "behave differently on those connections" equated to almost the entire test suite failing, so I worked with the model to ensure that this difference would not break how the library works.
And a final review by GPT-5.5
I used to think that the idea of having one model review the work of another was somewhat absurd - it felt weirdly superstitious. The problem is *it really does work* - I've started habitually having Anthropic's best model review OpenAI's work and vice versa, because I've had that turn up interesting results often enough to be valuable.
I prompted Codex Desktop and GPT-5.5 xhigh with the following:
Review changes since the last RC. Also confirm that the changelog is up-to-date.
Which was enough to turn up two issues worth investigating:
Findings
[P1] sqlite_utils/db.py:663 db.query() now rejects non-row statements only after calling db.execute(), and sqlite_utils/db.py:705 auto-commits those writes first. So db.query("update ...") raises ValueError but the update is already committed. That is a surprising side effect for a method documented as “can only be used with SQL that returns rows.”
[P1] sqlite_utils/db.py:672 INSERT ... RETURNING through db.query() only commits after the returned generator is fully exhausted. db.query("insert ... returning ...") without iteration, or common next(db.query(...)) usage, leaves the transaction open and the write can be rolled back on close. This contradicts docs/changelog.rst:15 and docs/python-api.rst:232, which say it takes effect without iteration.
I pasted that into a fresh Fable session, which ran some experiments to confirm the problem:
Both findings were confirmed. db.query() called self.execute() first, which auto-commits writes, and only then checked cursor.description — so db.query("update ...") committed the update before raising ValueError. And the INSERT ... RETURNING commit lived at the end of the returned generator, so it never fired unless you exhausted the iterator — next(db.query(...)) or an un-iterated call left the transaction open, contradicting what the changelog and docs promise.
Here's the PR with the fix, and the full Claude Code transcript. Reviewing this code helped me build a better mental model of the edge cases of SQLite transaction semantics!
For an estimated (unsubsidized) cost of $149.25
I upgraded to the Claude Max $200/month plan (I was previously on $100/month) to increase my Fable allowance for the remaining time until the July 7th Fablepocalypse, when even Claude Max subscribers will have to pay full API cost for the model.
I was curious as to how much this would have cost me if I had been paying those costs directly. At first I thought those numbers weren't available to me since I had run the work remotely using Claude Code for web, and then I realized I could run AgentsView inside that existing session to get that cost estimate!
Run "uvx agentsview --help" and then use that tool to calculate the cost of this session
Claude figured out how to use the session list --include-children command and came out with the following:
Transcript
Model
Cost
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
Total
$149.25
I'm very glad I'm on that subscription! I really should have followed my own advice and leaned more heavily into subagents with cheaper models.
Here's what claude.ai/settings/usage is showing me right now:

I have several other major Fable-driven projects on the go right now as well, with the goal of hitting 100% on that Fable bar just in time for the price increase.
The full release notes for sqlite-utils 4.0rc2
Here are the full release notes for the RC. I had Fable add these to an "Unreleased" section of the changelog as each change landed, reviewing them as it went. This has the neat side effect that the commit history of the changelog acts as a concise summary of each of the changes that went into the release.
In the past I've had a policy of writing release notes by hand, but honestly these are better than I would have created myself. Release notes are a great example of writing that I'm OK to outsource to agents because they need to be boring, predictable and accurate.
Breaking changes:
- Write statements executed with db.execute() are now committed automatically, unless a transaction is already open in which case they join it. Previously they opened an implicit transaction that stayed open until something committed it - writes appeared to work when read on the same connection but were silently rolled back when the connection closed. Code that relied on rolling back uncommitted db.execute() writes should use the new db.begin() method to open an explicit transaction first. The transaction model is documented in full at Transactions and saving your changes.
- db.query() now executes its SQL as soon as it is called, rather than waiting until the returned generator is first iterated. Rows are still fetched lazily during iteration. SQL errors are now raised at the call site, statements such as INSERT ... RETURNING are executed and committed immediately without needing to iterate over their results, and passing a statement that returns no rows - previously a silent no-op - now raises a ValueError recommending db.execute() instead. A statement rejected this way is rolled back before the error is raised, so it has no effect on the database.
- Python API validation errors now raise ValueError instead of AssertionError. Previously invalid arguments - such as create_table() with no columns, transform() on a table that does not exist, or passing both ignore=True and replace=True - were rejected using bare assert statements, which are silently skipped when Python runs with the -O flag. Code that caught AssertionError for these cases should catch ValueError instead.
- table.upsert() and table.upsert_all() now raise PrimaryKeyRequired if a record is missing a value for any primary key column, or has a value of None for one. Previously such records - which can never match an existing row - were quietly inserted as brand new rows, or triggered a confusing KeyError after the insert had already taken place.
- db.enable_wal() and db.disable_wal() now raise a sqlite_utils.db.TransactionError if called while a transaction is open. Previously they would silently commit the open transaction as a side effect of changing the journal mode, breaking the rollback guarantee of db.atomic() and of user-managed transactions.
- The View class no longer has an enable_fts() method. It existed only to raise NotImplementedError, since full-text search is not supported for views - calling it now raises AttributeError instead, and the method no longer appears in the API reference. The sqlite-utils enable-fts command shows a clean error when pointed at a view.
- The no-op -d/--detect-types flag has been removed from the insert and upsert commands. Type detection has been the default for CSV/TSV data since 4.0a1, so the flag did nothing - invocations using it should simply drop it. --no-detect-types remains available to disable detection.
- Database() now raises a sqlite_utils.db.TransactionError if passed a connection created with the Python 3.12+ sqlite3.connect(..., autocommit=True) or autocommit=False options. commit() and rollback() behave differently on those connections, which previously caused every write made by the library to be silently discarded when the connection closed.
Everything else:
- Fixed a bug where table.delete_where(), table.optimize() and table.rebuild_fts() did not commit their changes, leaving the connection inside an open transaction. Their work - and any subsequent writes - could then be silently rolled back when the connection was closed. All three now use db.atomic(), consistent with the other write methods.
- The sqlite-utils drop-table command now refuses to drop a view, and drop-view refuses to drop a table. Previously each would silently drop the wrong type of object if the name matched. Both now exit with an error suggesting the correct command to use.
Migrations applied by the new migrations system now run inside a transaction, together with the record of the migration having been applied. If a migration raises an exception its changes are rolled back and it stays pending, so it can be safely re-applied after the error is fixed. Migrations that cannot run inside a transaction, such as those executing VACUUM, can opt out using @migrations(transactional=False) - see <a href="https://sqlite-utils.datasette.io/en/latest/migration
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み