データベーススキーママイグレーション機能を追加した sqlite-utils 4.0 のリリース
Simon Willison が SQLite の主要ラッパーライブラリ「sqlite-utils」のメジャーバージョンアップ 4.0 をリリースし、データベーススキーママイグレーション機能やネストトランザクションなどを追加した。
キーポイント
データベーススキーママイグレーションの実装
Python ファイルで定義されたマイグレーションを自動的に追跡・適用する仕組みが導入され、SQLite の制約を超えたテーブル変換が可能になった。
高度なテーブル変換機能の提供
標準 SQL では困難な列追加や型変更を安全に行うため、一時的なテーブル作成とデータコピーを行う `table.transform()` メソッドが強化された。
ネストトランザクションと複合外部キーのサポート
新しい `db.atomic()` メソッドによるネストトランザクションや、複数の列にまたがる複合外部キー(compound foreign keys)への対応が追加された。
重大な変更点とアップグレードガイド
バージョン 4.0 ではいくつかの破壊的変更が含まれており、ユーザーは公式のアップグレードガイドに従って移行する必要がある。
影響分析・編集コメントを表示
影響分析
このアップデートは、SQLite をバックエンドとするアプリケーション開発において、データベーススキーマのライフサイクル管理を劇的に改善するものです。特に、マイグレーションの自動化と高度なテーブル変換機能により、大規模かつ複雑なデータ構造を持つシステムでも、安全かつ効率的な運用が可能になります。
編集コメント
AI アプリケーション開発においても、バックエンドのデータ構造が複雑化・大規模化する中で、スキーマ変更の自動化は不可欠な要素です。このツールは、LLM の出力結果を保存するテーブル構造の変更など、頻繁に発生する要件変化に対応するための強力な手段となります。
今朝、私は sqlite-utils 4.0 をリリースしました。これは同プロジェクトの 124 回目のリリースであり、2020 年 11 月の 3.0 以来初のメジャーバージョンアップです。このバージョンでは、いくつかの小さくても重要な破壊的変更(このアップグレードガイド に記載)に加え、データベーススキーママイグレーション、新しい db.atomic() メソッドによるネストトランザクション、そして複合外部キーのサポートという 3 つの主要機能が導入されました。
sqlite-utils を用いたデータベーススキーママイグレーション
スキーママイグレーションとは、SQLite データベースに対して適用する一連の変更と、どのマイグレーションが既に適用済みで、どれが未適用(pending)であるかを追跡し、未適用のものを適用するためのメカニズムを定義したものです。
マイグレーションは sqlite-utils Python ライブラリ を用いた Python ファイルで定義されます。このライブラリには、SQLite の ALTER TABLE 文ではサポートされていない拡張されたテーブル変更機能を提供する強力な table.transform() メソッドが含まれています。
(table.transform() は、SQLite のドキュメント 推奨されるパターン を実装しています。新しいスキーマを持つ新しい一時テーブルを作成し、データをコピーした後、古いテーブルを削除して、その場所に一時テーブルの名前を変更します。)
以下は、"creatures" という名前のテーブルを作成し、2 番目のステップで列を追加し、3 番目のステップで 2 つの列の型を変更する移行ファイルの例です:
from sqlite_utils import Migrations
migrations = Migrations("creatures")
@migrations()
def create_table(db):
db["creatures"].create(
{"id": int, "name": str, "species": str},
pk="id",
)
@migrations()
def add_weight(db):
db["creatures"].add_column("weight", float)
@migrations()
def change_column_types(db):
db["creatures"].transform(types={"species": int, "weight": str})
これを migrations.py として保存し、新しいデータベースに対して以下のように実行します:
uvx sqlite-utils migrate data.db migrations.py
その後、そのデータベースのスキーマを確認すると:
uvx sqlite-utils schema data.db
次のような SQL が表示されます:
CREATE TABLE "_sqlite_migrations" (
"id" INTEGER PRIMARY KEY,
"migration_set" TEXT,
"name" TEXT,
"applied_at" TEXT
);
CREATE UNIQUE INDEX "idx__sqlite_migrations_migration_set_name"
ON "_sqlite_migrations" ("migration_set", "name");
CREATE TABLE "creatures" (
"id" INTEGER PRIMARY KEY,
"name" TEXT,
"species" INTEGER,
"weight" TEXT
);
_sqlite_migrations テーブルは、どのマイグレーション関数が実行されたかを追跡するために使用されます。上記の creatures テーブルは、3 つのすべてのマイグレーションが適用された後のスキーマです。
未完了および適用済みのマイグレーションの一覧を表示するには、以下を実行してください:
uvx sqlite-utils migrate data.db migrations.py --list
出力:
Migrations for: creatures
Applied:
create_table - 2026-07-07 17:58:41.360051+00:00
add_weight - 2026-07-07 17:58:41.360608+00:00
change_column_types - 2026-07-07 18:01:15.802000+00:00
Pending:
(none)
マイグレーションファイルを指定しない場合、sqlite-utils migrate data.db コマンドは現在のディレクトリおよびそのサブディレクトリをスキャンし、migrations.py という名前のファイルを探して、それらに見つかった Migrations() インスタンスをすべて適用します。
migrations.apply(db) メソッドを使用して、Python コードからもマイグレーションを実行できます。これは、複数のバージョンにわたって独自のデータベーススキーマを管理するツールを構築する際に有用です。私の LLM ツール は、このパターンのバージョンを数年間使用しており、llm/embeddings_migrations.py にその例が示されています。
先行事例
このパターンの私の最も好きな実装は、Andrew Godwin によって開発された Django のマイグレーション です。これは彼の以前のプロジェクト South に基づいています。面白い事実:Andrew、Russ Keith-Magee、そして私は、2008 年の最初の DjangoCon で開催された スキーマ進化パネル で、Django 向けの競合するスキーママイグレーションのアプローチを発表しました!私の試みは dmigrations と名付けられ、ロンドンの Global Radio のチームと共に開発されました。
Django のマイグレーションは、モデル定義から自動的に生成でき、以前のバージョンへのロールバック機能も備えています。sqlite-utils のアプローチは意図的にシンプルです:Django と異なり、sqlite-utils はモデル定義 ORM ではなく、プログラムによるテーブル作成を推奨するため、自動的にマイグレーションを生成するために使用できるものは何もありません。
ロールバック機能はスキップすることにしました。私の経験則では、これはほとんど使用されない機能だからです。SQLite プロジェクトにおいてロールバックを実現する簡単な方法は、マイグレーションを適用する前にデータベースファイルのコピーを作成することです!
sqlite-migrate からの移行
sqlite-utils のマイグレーション設計はすでに 3 年前のものですが、当初は sqlite-migrate という別パッケージとしてリリースしました。これはベータ版の段階から大きく発展することはありませんでした。
そのパッケージを十分に多くの場所で使用してきたことで、その設計に自信を持てるようになりました。そこで、成長する sqlite-utils/Datasette/LLM エコシステム内の他のすべてのツールでデフォルトで使用可能にするため、これを sqlite-utils の機能として昇格させることにしました。
sqlite-migrate には 最後のリリース を行い、これにより sqlite-utils>=4 に依存するように変更し、__init__.py ファイルを以下の内容に置き換えました:
from sqlite_utils import Migrations
__all__ = ["Migrations"]
sqlite-migrate に依存している既存のプロジェクトは、一切の変更を加えることなく引き続き動作します。
sqlite-utils 4.0 のその他の機能
このバージョンのリリースノートに、いくつかのインライン注釈を付記しました:
4.0 リリースには、マイナーな後方互換性のない修正が含まれているためメジャーバージョン番号が bump されました。また、3 つの主要な新機能を導入しています:
- データベースマイグレーション。プロジェクトのスキーマを時間とともに進化させるための構造化されたメカニズムを提供します。(#752)
私はマイグレーションを、このブログ記事の理由となる象徴的な新機能と捉えています。
- db.atomic() を介したネストトランザクションのサポートに加え、ライブラリ全体にわたるトランザクションの動作に関する多数の改善。(#755)
sqlite-utils は長らくデータベーストランザクションとの関係が曖昧なものでした。これは、私が 2018 年にこのライブラリの設計を始めた当時、SQLite 自体におけるトランザクションの仕組みについて十分に理解していなかったことにも起因します。
マイグレーション機能をコアライブラリに追加したことで、私はついにこの難題を解決しようと決意しました。なぜなら、トランザクションはマイグレーションシステムをより安全にし、推論しやすくするからです。
その結果、以下のような db.atomic() コンテキストマネージャーを中心に構築することになりました:
with db.atomic():
db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
db.table("dogs").insert({"id": 2, "name": "Pancakes"})
SQLite は Savepoints をサポートしており、その結果として db.atomic() はネスト可能となり、トランザクション内でトランザクションを実行できます。とても素晴らしい機能です!
- 複合外部キーのサポート。table.foreign_keys を通じた作成、変換、およびインスペクションが可能になりました。(#594)
これは、コーディングエージェントにすべての未解決の課題とプルリクエストをレビューさせ、後で追加すると破壊的変更となるため 4.0 リリースに含まれるべき機能がないか尋ねた際に生まれました。そのエージェントは正しく、複合外部キーがまさにそのような機能であると特定しました。
まず、table.foreign_keys 列挙メソッドの破壊的変更から始め、その後、Claude Fable 5 が複合外部キー*作成*のより複雑な作業をライブラリに統合できるかどうかを試してみることにしました。その API デザインは、私がまさに適切だと感じたものであり、ライブラリの残りの部分がすでに採用している方法と一貫していました。
その他の注目すべき変更点には以下があります:
- アップサート(Upserts)は now SQLite の INSERT ... ON CONFLICT ... DO UPDATE SET 構文を使用し、既存のテーブル主キーを自動的に検出し、必須の主キー値が欠落しているレコードを拒否します。(#652)
これは、私が破壊的変更を含むバージョン 4.0 へのアップグレードを検討するきっかけとなった変更です。私はこれを、行の挿入、更新、削除を追跡するためにトリガーを使用する sqlite-chronicle のサポートを強化するために実装しました。
- db.query() は now 即座に実行され、行を返さない文を拒否します。書き込みや DDL(Data Definition Language)には db.execute() を使用してください。
おそらく最も破壊的な変更です - この結果として、私のコード内のいくつかの場所を db.query() から db.execute() へ切り替えるために更新する必要がありました。
- CSV および TSV のインポートでは、デフォルトで列の型が検出されるようになりました。一方、既存のテーブルへの挿入操作では、そのテーブルの列型が保持されます。(#679)
sqlite-utils の insert コマンドにおける data.db creatures creatures.csv --detect-types フラグは、CSV 内のデータに基づいて列型(text, integer, real)を自動的に検出できるようにするために後から追加された機能です。これはデフォルトであるべきであり、バージョン 4.0 のリリースによってそれが実現されました。
table.extract()およびextractsパラメータは、すべての値が NULL の場合でもルックアップテーブルのレコードを作成しなくなりました。(#186)
このリリースで解決された最も古い問題です。根本的なバグは私が 2020 年 10 月に報告しました。
後方互換性のない変更の詳細については、3.x から 4.0 へのアップグレード をご覧ください。
4.0 プリリリースサイクル中に実装された機能と修正の詳細なリリースノートは、4.0a0、4.0a1、4.0rc1、4.0rc2、4.0rc3、および 4.0rc4 で確認できます。
アップグレードガイドは、Claude Fable 5、Claude Opus 4.8、および GPT-5.5 によって完全に執筆されました。リリースノートについても同様です。
これは、私が徐々にロボットへのアウトソーシングに慣れてきたようなドキュメンテーションの一種です。人々を何かに対して説得する必要も、意見を表明する必要もありません。その役割は、可能な限り正確で詳細であることです。私はリリースノートを注意深くレビューしましたが、それが正確かつ包括的であることを確認できます。
Claude Fable 5 の貢献は大きかった
sqlite-utils 4.0 の最初のアルファ版を 1 年以上前 にリリースしました。メジャーバージョン番号で取り組むことができた多くの他のマイナーな設計上の欠陥を見つけ、整理する必要がある作業量のために、安定版のリリースには足踏みしていました。
Claude Fable 5(および、やや劣りますが Opus 4.8 と GPT-5.5)からの支援は、慣性を克服し、このライブラリに費やすことのできる時間を最大限に活用するために必要なブーストを私にもたらしてくれました。
Fable は API デザインにおいて*非常に優れたセンス*を持っており、より開放的な目標を与えれば執拗に先手を打つ傾向があります。私が最も成功したプロンプトは、最終リリース候補だと考えていたものに対して行ったレビュータスクでした:
タグ付けされた最後の 3.x リリース以降の main ブランチの変更点をレビューする - これらを sqlite-utils 4.0 としてリリースしようとしている。これは非常に長い期間、後方互換性を損なう修正を約束しない安定版である。
変更ログとアップグレードガイドを確認し、v4 の新機能をすべて試すためのスクラッチスクリプトを作成すること - そのスクリプトは保存するが、コミットはしないこと
私はこれを Codex Desktop での GPT-5.5 xhigh と Claude Code での Fable 5 で試した。
GPT-5.5 は 5 つの Python スクリプト を作成したが、特に興味深いものは見つからなかった - その最終レポートはこちら。
Fable 5 は 12 のスクリプト を作成し、リリースの妨げとなる問題 4 つと追加の問題 10 をそのレポートで特定した。また、見事な統合再現スクリプト を構築した。これを実行すると、以下の結果が出力される:
=== 1. db.execute() の失敗した書き込みにより、暗黙的なトランザクションが開いたままになる ===
失敗後の in_transaction: True
バグ:接続を閉じた際に 'other' テーブルが静かに失われる
=== 2. 先頭の ';' が query() の最初のトークンスキャナをバイパスする ===
バグ:OperationalError: no such savepoint: sqlite_utils_query が発生
バグ:ロールバックにもかかわらず行が永続化された(count=1)
=== 3. query() で拒否された書き込み PRAGMA も依然として有効になる ===
バグ:'rejected' ステートメント実行後に user_version=5 となる(ドキュメントでは効果なしと記載されている)
=== 4. 暗黙的な複合外部キー
原文を表示
This morning I released sqlite-utils 4.0, the 124th release of that project and the first major version bump since 3.0 in November 2020. In addition to some small but significant breaking changes (described in this upgrade guide), this version introduces three major features: database migrations, nested transactions (via a new db.atomic() method), and support for compound foreign keys.
Database schema migrations using sqlite-utils
Schema migrations define a sequence of changes to be made to a SQLite database, plus a mechanism for tracking which migrations have been applied and applying any that are found to be pending.
Migrations are defined in Python files using the sqlite-utils Python library, which includes a powerful table.transform() method providing enhanced alter table capabilities that are not supported by SQLite's ALTER TABLE statement.
(table.transform() implements the pattern recommended by the SQLite documentation - create a new temporary table with the new schema, copy across the data, then drop the old table and rename the temporary one in its place.)
Here's an example migration file which creates a table called creatures, adds an additional column to it in a second step, then changes the types of two of the columns in a third:
from sqlite_utils import Migrations
migrations = Migrations("creatures")
@migrations()
def create_table(db):
db["creatures"].create(
{"id": int, "name": str, "species": str},
pk="id",
)
@migrations()
def add_weight(db):
db["creatures"].add_column("weight", float)
@migrations()
def change_column_types(db):
db["creatures"].transform(types={"species": int, "weight": str})Save that as migrations.py and run it against a fresh database like this:
uvx sqlite-utils migrate data.db migrations.pyThen if you check the schema of that database:
uvx sqlite-utils schema data.dbYou'll see this SQL:
CREATE TABLE "_sqlite_migrations" (
"id" INTEGER PRIMARY KEY,
"migration_set" TEXT,
"name" TEXT,
"applied_at" TEXT
);
CREATE UNIQUE INDEX "idx__sqlite_migrations_migration_set_name"
ON "_sqlite_migrations" ("migration_set", "name");
CREATE TABLE "creatures" (
"id" INTEGER PRIMARY KEY,
"name" TEXT,
"species" INTEGER,
"weight" TEXT
);The _sqlite_migrations table is used to keep track of which migration functions have been run. The creatures table above is the schema after all three migrations have been applied.
To see a list of migrations, both pending and applied, run this:
uvx sqlite-utils migrate data.db migrations.py --listOutput:
Migrations for: creatures
Applied:
create_table - 2026-07-07 17:58:41.360051+00:00
add_weight - 2026-07-07 17:58:41.360608+00:00
change_column_types - 2026-07-07 18:01:15.802000+00:00
Pending:
(none)
If you don't specify a migrations file, the sqlite-utils migrate data.db command will scan the current directory and its subdirectories for files called migrations.py and apply any Migrations() instances it finds in them.
You can also execute migrations from Python code using the migrations.apply(db) method, which is useful for building tools that manage their own database schemas over multiple versions. My own LLM tool has been using a version of this pattern for several years now, as shown in llm/embeddings_migrations.py.
Prior art
My favorite implementation of this pattern remains Django's Migrations, developed by Andrew Godwin based on his earlier project South. Fun fact: Andrew, Russ Keith-Magee, and I presented our competing approaches to schema migrations for Django on the Schema Evolution panel at the very first DjangoCon back in 2008! My attempt was called dmigrations, developed with a team at Global Radio in London.
Django's migrations can be automatically generated from model definitions and include the ability to roll back to a previous version. The sqlite-utils approach is deliberately simpler: unlike Django, sqlite-utils encourages programmatic table creation rather than a model definition ORM, so there isn't anything we can use to automatically generate migrations.
I decided to skip rollback, since in my experience it's a feature that is rarely used. With a SQLite project, an easy way to achieve rollback is to create a copy of your database file before you apply the migrations!
Migrating from sqlite-migrate
The design of sqlite-utils migrations is three years old now - I had originally released it as a separate package called sqlite-migrate, which never quite graduated beyond a beta release.
I've used that package in enough places now that I'm confident in the design, so I've decided to promote it to a feature of sqlite-utils to make it available by default to all of the other tools in the growing sqlite-utils/Datasette/LLM ecosystem.
I made one last release of sqlite-migrate, which switches it to depend on sqlite-utils>=4 and replaces the __init__.py file with the following:
from sqlite_utils import Migrations
__all__ = ["Migrations"]Any existing project that depends on sqlite-migrate should continue to work without alterations.
Everything else in sqlite-utils 4.0
Here are the release notes for this version, with some inline annotations:
The 4.0 release includes some minor backwards-incompatible fixes (hence the major version number bump) and introduces three major new features:
Database migrations, providing a structured mechanism for evolving a project’s schema over time. (#752)
I think of migrations as the signature new feature, hence this blog post.
Nested transaction support via db.atomic(), plus numerous improvements to how transactions work across the library. (#755)
sqlite-utils has long had a confused relationship with database transactions, partly because when I started designing the library back in 2018 I didn't yet have a great feel for how those worked in SQLite itself.
Adding migrations to the core library made me determined to finally crack this nut, since transactions make migration systems a whole lot safer and easier to reason about.
I ended up building this around a db.atomic() context manager which looks like this:
with db.atomic():
db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
db.table("dogs").insert({"id": 2, "name": "Pancakes"})SQLite supports Savepoints, and as a result db.atomic() can be nested to carry out transactions inside of transactions. It's pretty neat!
Support for compound foreign keys, including creation, transformation and introspection through table.foreign_keys. (#594)
This came about when I asked a coding agent to review all open issues and PRs for things that should be included in a 4.0 release since they would represent breaking changes if I added them later, and it correctly identified that compound foreign keys were exactly that kind of feature.
I started with a breaking change to the table.foreign_keys introspection method, and then decided to see if Claude Fable 5 could handle the more fiddly job of integrating compound foreign key *creation* into the library. The API design it helped create felt exactly right to me - consistent with how the rest of the library worked already.
Other notable changes include:
Upserts now use SQLite’s INSERT ... ON CONFLICT ... DO UPDATE SET syntax, detect existing table primary keys automatically and reject records that are missing required primary key values. (#652)
This was the change that first pushed me to consider a breaking-change 4.0 version bump. I built this to help support sqlite-chronicle, which uses triggers to keep track of rows in a table that have been inserted, updated or deleted.
db.query() now executes immediately and rejects statements that do not return rows; use db.execute() for writes and DDL.
Probably the most disruptive breaking change - I've had to update a few places in my own code to switch from db.query() to db.execute() as a result.
CSV and TSV imports now detect column types by default, while inserts into existing tables preserve those tables’ column types. (#679)
The sqlite-utils insert data.db creatures creatures.csv --detect-types flag was a later addition to allow column types (text, integer, real) to be automatically detected based on the data in a CSV. It should be the default, and releasing a 4.0 means I can make it so.
table.extract() and extracts= no longer create lookup table records for all-null values. (#186)
The oldest issue addressed by this release - the underlying bug was opened (by me) in October 2020.
See Upgrading from 3.x to 4.0 for details on backwards-incompatible changes.
The detailed release notes for the features and fixes shipped during the 4.0 pre-release cycle are available in 4.0a0, 4.0a1, 4.0rc1, 4.0rc2, 4.0rc3 and 4.0rc4.
The upgrade guide was entirely written by Claude Fable 5, Claude Opus 4.8 and GPT-5.5. The same is true of the release notes.
This is the kind of documentation I've slowly become comfortable outsourcing to the robots. It doesn't need to convince people of anything, or express any opinions - its job is to be as accurate and detailed as possible. I've reviewed the release notes closely and can confirm they are accurate and comprehensive.
Claude Fable 5 helped a lot
I released the first alpha of sqlite-utils 4.0 over a year ago. I've been dragging my heels on the stable release because of the amount of work it would take to track down and clean up the many other minor design flaws that a major version number allowed me to take on.
Assistance from Claude Fable 5 (and to a lesser extent Opus 4.8 and GPT-5.5) gave me just the boost I needed to overcome inertia and make the most of the time I could afford to spend on this library.
Fable has *really good taste* in API design, and is relentlessly proactive if you give it a more open goal. My most successful prompt was a review task that I issued against what I thought was the last release candidate:
review the changes on main since the last tagged 3.x release - I am about to ship them as sqlite-utils 4.0, a stable version that promises no backwards-incompatible fixes for a very long time.
review the changelog and upgrade guide, and write yourself scratch scripts to try out all of the new features in v4 - save those scripts but don't commit them
I tried this with GPT-5.5 xhigh in Codex Desktop and Fable 5 in Claude Code.
GPT-5.5 wrote 5 Python scripts and didn't turn up anything particularly interesting - its final report is here.
Fable 5 wrote 12 scripts, identified 4 release blockers and 10 additional issues in its report, and built a neat combined repro script, which, when run, output the following:
=== 1. Failed db.execute() write leaves an implicit transaction open ===
in_transaction after failed write: True
BUG: table 'other' silently lost when connection closed
=== 2. Leading ';' bypasses the query() first-token scanner ===
BUG: raised OperationalError: no such savepoint: sqlite_utils_query
BUG: row persisted despite rollback (count=1)
=== 3. Rejected write PRAGMA via query() still takes effect ===
BUG: user_version=5 after 'rejected' statement (docs say no effect)
=== 4. Implicit compound FK res
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み