Simon Willisonが sqlite-utils 4.0rc1 をリリースした。自分は datasette 周りのツールをたまに手元で使うので、わりとこのへんのアップデートは追っている。今回の RC はメジャーバージョンなので破壊的変更が入っていて、手元のコードを直す必要があるか確認しながら読んだ。
まず新機能の話から。目玉が2つある。migrations のサポートと、`db.atomic()` によるネストトランザクションだ。
migrations は以前から sqlite-migrate というパッケージとして別に存在していた。Simon 本人が数年運用して実績を積んだので、今回 sqlite-utils に統合した、という流れらしい。実際に LLM ツールなどのプロジェクトで使われていたと本文に書いてある。
書き方はこんな感じ。
デコレータで migration 関数を積み上げていくスタイル。Django の migration と違うのは、reverse migration が存在しない点だ。間違えたら別の migration で打ち消す設計になっている。これ、最初はちょっと「え、戻せないの?」ってなるけど、よく考えると個人開発レベルのプロジェクトなら余計な複雑さを排除した方が楽なんだよな。自分が以前 Alembic を小規模ツールに突っ込んでハマったことを思い出した。あのときは downgrade スクリプトが壊れてて、本番の SQLite ファイルを手で直す羽目になった。あの経験からすると、シンプルさを優先したこの設計は好みに合う。
CLI からも使える。
これが地味にえぐい。Python スクリプトとして書いた migration ファイルを、CLI から直接叩ける。CI に組み込むのが楽になる。
もう一個の新機能、`db.atomic()` はネストしたトランザクションを扱うための API だ。SQLite には savepoint という仕組みでネストトランザクションを実現できるが、素の sqlite3 モジュールではそれを使うのが少し面倒だった。今回の `db.atomic()` は Django や Peewee の `atomic()` からターミノロジーを借りたとある。
コードを見るとこういう使い方になる。外側の `atomic()` の中で内側の `atomic()` が例外で rollback されても、外側のトランザクションは続行できる。自分がいまチームで作っているデータパイプラインのコードでも、似たような処理を `try-except` でゴリゴリ書いていたので、これは素直に羨ましかった。
ただ Simon 本人も「まだあまりテストが行き届いていないので、テスターに試してほしい」とコメントしている。RC なのでここはまだ様子見。
今回のメジャーバンプで気になったのが `FLOAT` → `REAL` への型変更だ。float のカラムを自動検出で作ると、以前は `FLOAT` 型で作られていた。4.0 からは `REAL` になる。SQLite 的には `REAL` が正しい型名なので仕様の修正ではあるけど、既存データや外部ツールとの兼ね合いで意図せず影響が出るケースはゼロじゃない。
もう一個、`db.table(name)` が view に使えなくなった点。view にアクセスするときは `db.view(name)` を使う仕様に変わった。これは API の責務を明確にした変更だと理解した。自分のコードでは view は触っていなかったので直接の影響はなかったが、チームに共有はしておく。
あと Python 3.8 のサポートが切れた。うちのチームで使っているマシンの CI は 3.11 なので問題なし。でも古いプロジェクトを引き継ぎで受けたりすると、こういうところでいきなり詰まることがある。3.8 を使っている人は要注意だ。
とりあえず手元の個人開発リポジトリで `pip install sqlite-utils==4.0rc1` して動かしてみた。migration まわりは想像より素直に動いた。stable リリース前に issue を拾えるなら OSS コントリビュートのチャンスでもある。rc を触ったことがない人も、まず `pip install` してドキュメントのサンプルをコピペするだけで感触はつかめるので、気が向いたら試してみてほしい。
まず新機能の話から。目玉が2つある。migrations のサポートと、`db.atomic()` によるネストトランザクションだ。
migrations が本体に入った
migrations は以前から sqlite-migrate というパッケージとして別に存在していた。Simon 本人が数年運用して実績を積んだので、今回 sqlite-utils に統合した、という流れらしい。実際に LLM ツールなどのプロジェクトで使われていたと本文に書いてある。
書き方はこんな感じ。
from sqlite_utils import Database, 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)デコレータで migration 関数を積み上げていくスタイル。Django の migration と違うのは、reverse migration が存在しない点だ。間違えたら別の migration で打ち消す設計になっている。これ、最初はちょっと「え、戻せないの?」ってなるけど、よく考えると個人開発レベルのプロジェクトなら余計な複雑さを排除した方が楽なんだよな。自分が以前 Alembic を小規模ツールに突っ込んでハマったことを思い出した。あのときは downgrade スクリプトが壊れてて、本番の SQLite ファイルを手で直す羽目になった。あの経験からすると、シンプルさを優先したこの設計は好みに合う。
CLI からも使える。
sqlite-utils migrate creatures.db migrations.pyこれが地味にえぐい。Python スクリプトとして書いた migration ファイルを、CLI から直接叩ける。CI に組み込むのが楽になる。
db.atomic() が入った理由を考える
もう一個の新機能、`db.atomic()` はネストしたトランザクションを扱うための API だ。SQLite には savepoint という仕組みでネストトランザクションを実現できるが、素の sqlite3 モジュールではそれを使うのが少し面倒だった。今回の `db.atomic()` は Django や Peewee の `atomic()` からターミノロジーを借りたとある。
コードを見るとこういう使い方になる。外側の `atomic()` の中で内側の `atomic()` が例外で rollback されても、外側のトランザクションは続行できる。自分がいまチームで作っているデータパイプラインのコードでも、似たような処理を `try-except` でゴリゴリ書いていたので、これは素直に羨ましかった。
ただ Simon 本人も「まだあまりテストが行き届いていないので、テスターに試してほしい」とコメントしている。RC なのでここはまだ様子見。
破壊的変更をチェックした
今回のメジャーバンプで気になったのが `FLOAT` → `REAL` への型変更だ。float のカラムを自動検出で作ると、以前は `FLOAT` 型で作られていた。4.0 からは `REAL` になる。SQLite 的には `REAL` が正しい型名なので仕様の修正ではあるけど、既存データや外部ツールとの兼ね合いで意図せず影響が出るケースはゼロじゃない。
もう一個、`db.table(name)` が view に使えなくなった点。view にアクセスするときは `db.view(name)` を使う仕様に変わった。これは API の責務を明確にした変更だと理解した。自分のコードでは view は触っていなかったので直接の影響はなかったが、チームに共有はしておく。
あと Python 3.8 のサポートが切れた。うちのチームで使っているマシンの CI は 3.11 なので問題なし。でも古いプロジェクトを引き継ぎで受けたりすると、こういうところでいきなり詰まることがある。3.8 を使っている人は要注意だ。
とりあえず手元の個人開発リポジトリで `pip install sqlite-utils==4.0rc1` して動かしてみた。migration まわりは想像より素直に動いた。stable リリース前に issue を拾えるなら OSS コントリビュートのチャンスでもある。rc を触ったことがない人も、まず `pip install` してドキュメントのサンプルをコピペするだけで感触はつかめるので、気が向いたら試してみてほしい。