2026年7月7日

本日、sqlite-utils 4.0をリリースしました。これは同プロジェクトの124回目のリリースであり、2020年11月の3.0以来初のメジャーバージョンアップとなります。小規模ながら重要な破壊的変更(アップグレードガイドで説明)に加え、本バージョンでは3つの主要機能が導入されました:データベースマイグレーション、新しいdb.atomic()メソッドによるネストされたトランザクション、および複合外部キーのサポートです。

sqlite-utilsを使用したデータベーススキーママイグレーション

スキーママイグレーションは、SQLiteデータベースに加えられる一連の変更を定義し、適用済みのマイグレーションを追跡し、保留中のマイグレーションを適用する仕組みを提供します。

マイグレーションはsqlite-utils Pythonライブラリを使用したPythonファイルで定義されます。このライブラリには、SQLiteのALTER TABLE文ではサポートされていない拡張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で確認できます。

先行事例

このパターンの実装として、私のお気に入りはDjangoのマイグレーションです。これはAndrew Godwinが以前のプロジェクトSouthを基に開発したものです。面白い事実として、Andrew、Russ Keith-Magee、そして私は2008年の最初のDjangoConで、Django向けスキーママイグレーションに関する競合アプローチをSchema Evolutionパネルで発表しました!私の試みはdmigrationsと呼ばれ、ロンドンのGlobal Radioのチームと共同で開発しました。

Djangoのマイグレーションはモデル定義から自動生成可能で、以前のバージョンへのロールバック機能を含んでいます。sqlite-utilsのアプローチは意図的にシンプルです:Djangoとは異なり、sqlite-utilsはモデル定義ORMではなくプログラムによるテーブル作成を推奨するため、マイグレーションを自動生成するための手段はありません。

ロールバックはスキップすることにしました。私の経験では、これはほとんど使用されない機能だからです。SQLiteプロジェクトでは、マイグレーションを適用する前にデータベースファイルのコピーを作成することで、簡単にロールバックを実現できます!

sqlite-migrateからの移行

sqlite-utilsマイグレーションの設計はすでに3年前のもので、当初はsqlite-migrateという別パッケージとしてリリースしましたが、ベータ版を超えることはありませんでした。

このパッケージを十分な場所で使用した結果、設計に自信を持てたため、sqlite-utilsの機能として昇格させ、成長を続けるsqlite-utils/Datasette/LLMエコシステムの他のすべてのツールでデフォルトで利用できるようにすることにしました。

sqlite-migrate最後のリリースを行い、sqlite-utils>=4に依存するように切り替え、__init__.pyファイルを以下のように置き換えました:

from sqlite_utils import Migrations

__all__ = ["Migrations"]

sqlite-migrateに依存する既存のプロジェクトは、変更なしで引き続き動作します。

sqlite-utils 4.0のその他の機能

本バージョンのリリースノートを、インライン注釈付きで以下に示します:

4.0リリースには、いくつかの軽微な後方互換性のない修正(メジャーバージョン番号のバンプの理由)が含まれており、3つの主要な新機能が導入されています:

マイグレーションはシグネチャとなる新機能と考えているため、このブログ記事を書きました。

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はセーブポイントをサポートしており、その結果db.atomic()はトランザクション内でトランザクションを実行するためにネストできます。これは非常に便利です!

これは、4.0リリースに含めるべき未解決の問題やPRをコーディングエージェントにレビューさせた際に、後で追加すると破壊的変更になる機能を正しく特定したことがきっかけでした。

まずtable.foreign_keysイントロスペクションメソッドの破壊的変更から始め、その後Claude Fable 5が複合外部キーの作成をライブラリに統合するというより複雑な作業を処理できるかどうかを試すことにしました。API設計は、ライブラリの既存の動作と一貫性のあるまさに適切なものに感じられました。

その他の主な変更点:

  • アップサートがSQLiteのINSERT ... ON CONFLICT ... DO UPDATE SET構文を使用するようになり、既存のテーブルの主キーを自動検出し、必須の主キー値が欠けているレコードを拒否します。(#652

これが、破壊的変更を伴う4.0バージョンアップを最初に検討するきっかけとなった変更です。sqlite-chronicleのサポートのために構築しました。これはトリガーを使用して、テーブルに挿入・更新・削除された行を追跡します。

  • db.query()は即座に実行され、行を返さないステートメントを拒否します。書き込みとDDLには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.0a04.0a14.0rc14.0rc24.0rc3、および4.0rc4で確認できます。

アップグレードガイドは、Claude Fable 5、Claude Opus 4.8、およびGPT-5.5によって完全に書かれました。リリースノートも同様です。

これは、ロボットにアウトソースすることに徐々に慣れてきたドキュメントの種類です。説得力を持たせたり、意見を述べたりする必要はなく、正確で詳細であることが仕事です。リリースノートを綿密にレビューし、正確で包括的であることを確認しました。

Claude Fable 5が大きく貢献

sqlite-utils 4.0の最初のアルファ版は1年以上前にリリースしました。メジャーバージョン番号によって許容される多くの軽微な設計上の欠陥を追跡・修正する作業量のため、安定版リリースが遅れていました。

Claude Fable 5(およびClaude Opus 4.8、GPT-5.5)の支援により、惰性を克服し、このライブラリに費やせる時間を最大限に活用するのに十分な後押しを得られました。

FableはAPI設計において本当に優れたセンスを持っており、よりオープンな目標を与えると積極的に行動します。最も成功したプロンプトは、最後のリリース候補だと思っていたものに対して発行したレビュータスクでした:

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

GPT-5.5 xhigh in Codex DesktopとClaude CodeのFable 5で試しました。

GPT-5.5は5つのPythonスクリプトを作成しましたが、特に興味深いものは見つかりませんでした — 最終レポートはこちら

Fable 5は12のスクリプトを作成し、4つのリリースブロッカーおよび10の追加問題をレポートで特定し、実行時に以下の出力を生成する巧妙な統合再現スクリプトを構築しました:

=== 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 resolves pk columns in table order, not PK order ===
  BUG: other_columns reported as ('b', 'a'), should be ('a', 'b')
  BUG: transform of valid data raised IntegrityError: FOREIGN KEY constraint failed

=== 5. ForeignKey (now a dataclass) is no longer hashable ===
  BUG: cannot use 'sqlite_utils.db.ForeignKey' as a set element (unhashable type: 'ForeignKey')

=== 6. Mixed ForeignKey objects and tuples in foreign_keys= rejected ===
  BUG: foreign_keys= should be a list of tuples

=== 7. insert --csv into an EXISTING table transforms its column types ===
  BUG: existing zip '01234' is now 1234 (column type: int)

=== 8. insert(pk=, alter=True) regression: InvalidColumns before alter runs ===
  BUG: InvalidColumns: Invalid primary key column ['id'] for table t with columns ['a']

=== 9. migrate --stop-before an already-applied migration applies everything ===
  BUG: m2 was applied despite --stop-before m1 (m1 already applied)

=== 10. ensure_autocommit_on() silently commits an open transaction ===
  BUG: row survived rollback (count=1) - transaction was committed

私はそれらのほとんどに同意しました。ここに、16のコミットを含むPRがあり、順番にそれらを解決しました。

最新の最先端モデルの支援なしに構築した場合と比べて、sqlite-utils 4.0が大幅に品質の高いリリースであることは疑いありません。