AIニュース最前線
最新ニュースAI日報Hacker日報週報動画AIツールトレンド企業

AIニュース最前線

世界中のAI最新情報を日本語で毎時更新

最新ニュース日報トレンド企業プレミアムRSS
© 2026 ainew.jp特定商取引法に基づく表記
ニュース一覧元記事を開く
Answer.AI·2025年6月13日 09:00·約21分で読める

fastmigrateの紹介

#SQLite#データベースマイグレーション#Python#オープンソースツール
TL;DR

Answer.AI は、SQLite に特化し ORM に依存しないシンプルなデータベースマイグレーションツール「fastmigrate」を公開した。

AI深層分析2026年5月3日 01:14
3
注目/ 5段階
深度40%
4
関連度30%
2
実用性20%
5
革新性10%
3

キーポイント

1

シンプルさの追求

複雑な機能や ORM の依存を排除し、マイグレーションの本質的なパターンに焦点を当てた軽量なツールとして設計されている。

2

SQLite 特化とスクリプトベース

主に SQLite を対象とし、マイグレーションを単なるスクリプトディレクトリとして扱うことで、複雑な用語や設定を不要としている。

3

バージョン管理の明確化

アプリケーションコードが最新バージョンのデータベースのみを意識すればよく、スキーマ変更による破壊を防ぐための明示的なバージョン管理を実現する。

4

バージョン管理の導入

データベースの状態を暗黙的なものから、マイグレーションスクリプトに基づく明示的なバージョン管理へ移行することで、複雑な状態処理の問題を解決します。

5

単一目的のスクリプト設計

各マイグレーションスクリプトは特定のバージョン間(例:5 から 6)の変換のみを行う孤立した単一目的のファイルとして定義され、名前でバージョンを識別可能にします。

6

アプリケーションの複雑さ削減

マイグレーションを使用することで、アプリケーションは最新バージョンのデータベースのみを対象とし、過去のすべての状態を処理する必要がなくなります。

7

明示的なバージョン管理

各データベースバージョンには対応するスクリプトが1つだけ存在し、システムの状態や管理が明確で整理されたものになります。

影響分析・編集コメントを表示

影響分析

このツールは、特に軽量な Python アプリケーションや SQLite を使用するプロジェクトにおいて、データベーススキーマ管理の複雑さを劇的に軽減する可能性を秘めています。ORM に縛られない設計により、開発者がアーキテクチャの本質に集中できる環境を提供し、技術的な負債を防ぐための実用的な解決策となります。

編集コメント

ORM に依存しない軽量なマイグレーションツールの登場は、特定のユースケースにおいて開発体験を大幅に向上させる有望な動きです。

注意

TLDR: この投稿では、Python のデータベースマイグレーションツールである fastmigrate について紹介します。SQLite に焦点を当てており、特定の ORM ライブラリは必要としません。SQLite と直接作業を行い、シンプルさを保ちたい場合に適しています。手順については、fastmigrate リポジトリをご覧ください。

マイグレーションについて話しましょう!

image
image

私が言っているのは、あの「移動」のことではありません。

ええと、データベースマイグレーションパターンについて話しましょう。

マイグレーションは、データベース内の変更を管理するための強力なアーキテクチャパターンです。これにより、アプリケーションコードはデータベースの最新バージョンのみを知っていればよくなり、データベース自体を更新するために使用するコードも簡素化されます。

しかし、多くのデータベースヘルパーライブラリが同時に非常に複雑な他の多くのことを行うため、この基本的なパターンのシンプルさが隠れてしまい、見過ごされがちです。

そこで今日、私たちはデータベースマイグレーションのためのライブラリおよびコマンドラインツールである fastmigrate をリリースします。これは、ツール自体をシンプルにすることで、基盤となるパターンのシンプルさを尊重しています。提供されるコマンドは少数です。マイグレーションを単なるスクリプトのディレクトリとして扱います。本質的なアイデアを理解すればよく、余計な専門用語を覚える必要はありません。私たちはこれを気に入っています!

本記事では、データベースマイグレーションとは何か、そしてそれがどのような問題を解決するのかを一般論として説明し、その後、fastmigrate を用いた SQLite でのマイグレーションの実行方法を具体例を通じて解説します。

マイグレーションが解決する問題

マイグレーションが解決する中核的な問題は、アプリケーションを破損させることなくデータベーススキーマ(およびその他の基本構造)を変更しやすくすることです。これを実現するために、アプリケーションコードの変更と同様に、データベースのバージョンを明示的かつ管理可能なものとしています。

そうでない場合に複雑性がどのように蔓延するかを理解するために、アプリ開発における典型的な一連の出来事を考えてみましょう。アプリが初めて実行される際、扱うべき状況は一つだけです。それはまだデータベースが存在せず、作成する必要があるケースです。この時点でのアプリの起動コードは以下のようになるかもしれません:

App v1

db.execute("CREATE TABLE documents (id INT, content TEXT);")

しかし待ってください… ユーザーがその同じアプリを二度目実行したとき、テーブルはすでに存在しています。つまり実際には、コードは二つの可能なケースを処理する必要があります。それはテーブルが存在しないケースと、すでに存在しているケースです。

そこで、次のバージョンのアプリでは、初期化コードを以下のように更新します:

App v2

db.execute("CREATE TABLE IF NOT EXISTS documents (id INT, content TEXT);")

その後、データベースに新しい列を追加することを決定するかもしれません。そのため、アプリの三番目のバージョンでは二行目を追加します:

App v3

db.execute("CREATE TABLE IF NOT EXISTS documents (id INT, content TEXT);")

db.execute("ALTER TABLE documents ADD COLUMN title TEXT;")

しかし、待ってください…もし列が既に存在する場合は、このようにテーブルを変更したくありません。そのため、App v4 ではそのようなケースを処理するためにより複雑なロジックが必要になります。そして、さらに続く問題もあります。

この些細な例さえも、適切に処理されなければバグを生じさせます。実際のアプリケーションでは、テーブル間の関係性を導入し、その後変更を加えていくにつれて、こうした問題はより微妙で多数存在し、ストレスの多いものとなります。なぜなら、間違ったステップを一つ踏むだけでユーザーデータを失う可能性があるからです。

起こるのは、新しいバージョンが出るたびに、データベースの状態が一つだけでなく、ありとあらゆる過去の状態を処理する必要があるため、アプリケーションコードが複雑化していくという事実です。

これを避けるには、データベースの更新を別々に強制し、アプリケーションコードがデータベースから何を期待すべきかを正確に把握できるようにする必要があります。しかし、アプリがデータベースを管理しており、各ユーザーが自分自身のアプリインストールを実行するタイミングを決定できる場合(モバイルアプリ、デスクトップアプリ、あるいはユーザーごとにデータベースを持つ Web アプリの場合など)、これは必ずしも実現可能ではありません。単一のデータベースを持つシステムであっても、別々のデータベース更新を強制することは、管理すべき重要な新しい種類の変更——つまり、アプリケーションコードの変更と繊細に連携させる必要があるデータベース変更——を導入することになります。

これが問題の核心です。デフォルトでは、これらのさまざまなデータベース状態は暗黙的であり、管理されていないからです。

アプリケーションコードにおいて、git コミットはコードのバージョンと、その変更を生み出した内容を明確に指定します。その後、デプロイメントシステムにより、ユーザーが次に目にするアプリケーションのバージョンを正確に制御できます。しかし、データベースにおいては、何らかのシステムがない限り、単に「過去のコードによって生成された、名前のない状態にある」ということしか分かりません。アプリケーションコードを非常にうまく管理するバージョン管理ツールやデプロイメントツールは、自動的にアプリケーションが次に参照するデータベースのバージョンを制御するものではありません。

マイグレーションがこの問題をどう解決するか

データベースマイグレーションパターンは、2 つの主要な対策によってこの問題を解決します:

第一に、マイグレーションに基づいたデータベースバージョンの定義です。名前のないデータベース状態について推論するのではなく、データベースの明示的なバージョン管理を導入します。

これをどのように行うのでしょうか?マイグレーションスクリプトを用います。マイグレーションスクリプトは、孤立した単一目的のスクリプトであり、その唯一の仕事は、データベースをあるバージョン(例:5)から次のバージョン(例:6)へ移行させることです。

Fastmigrate はこの仕組みをシンプルに保ち、生成されるデータベースバージョンに基づいてスクリプト名を付与します。例えば、0006-add_user.sql という名前のスクリプトは、データベースバージョン 6 を生成する唯一のスクリプトでなければなりません。根本的な意味において、マイグレーションスクリプト内のバージョン番号は、認識されるデータベースバージョンのセットを定義します。したがって、git のコミットログを見るように、これらのバージョンを生成したスクリプトをリストアップすることで、過去のデータベースバージョンを確認できます:

$ ls -1 migrations/

0001-initialize.sql

0002-add-title-to-documents.sql

0003-add-users-table.sql

この構造化されたアプローチは、次の重要な施策を可能にします。

第二に、アプリケーションコードが特定のデータベースバージョンを対象とするように記述することです。データベース進化のコードをこれらのマイグレーションスクリプトに移管することで、アプリケーションコードはデータベースの変更について気にする必要がなくなり、最新のデータベースバージョンのみを対象とすればよくなります。

アプリケーションは、fastmigrate などのマイグレーションライブラリに依存して、必要なすべてのマイグレーションを実行させることができます。これには、開発環境で新規インスタンスを起動する際に、ゼロからすべてのマイグレーションを再実行して最新のデータベースバージョンを作成する場合もあれば、直近のデータベースバージョンを最新の状態に更新するために最新のマイグレーションのみを適用する場合もあります。あるいは、その中間のケースもあり得ます。重要なのは、アプリケーション側がそれを気にする必要がないという点です。

簡素化の程度を測る一つの方法は、システムの異なる部分が処理しなければならないケース数がどれだけ減ったかを数えることです。

マイグレーションを行う前、アプリケーションコードは、すべての可能な過去のデータベース状態を処理する責任を負っていました。それらの状態が何であるかを覚えて理解するために、ますます注意深い配慮が必要となる場合でもです。

マイグレーションの後では、すべてが明示的であり、読みやすく、構造化されています。アプリケーションは単一のデータベースバージョンのみと連携する責任を負います。そして、各データベースバージョンには、それを前の1つのバージョンから生成するスクリプトが正確に1つだけ存在します。(あまりにも清潔!あなたもため息をつきたくなりませんか?あああ…)

機能

マイグレーションなし

マイグレーションあり

DB 状態

数えきれず、名前もない

imageimage の明示的なバージョン

DB 管理

なし

imageimage の分離されたマイグレーションスクリプト(各バージョンごとに1つ)

アプリ要件

アプリはすべての DB 状態をサポートし、DB 変更を管理する必要がある

アプリは最新の DB バージョンのみをサポートすればよい

fastmigrate の使い方

前の例をもう一度追って、これが fastmigrate でどのように機能するかを見てみましょう。

進化するデータベーススキーマのロジックをアプリケーションの起動処理に埋め込むのではなく、一連のマイグレーションスクリプトを定義します。これらのスクリプトは SQL ですが、Python やシェルスクリプトを使用することもできます。その後、アプリケーションは fastmigrate の API を使用して、必要に応じてそれらのスクリプトを実行し、データベースを自動的に最新の期待されるバージョンに更新します。

最初のマイグレーションスクリプトでテーブルを作成します。migrations/ というディレクトリを作成し、その中に 0001-initialize.sql というファイルを入れてください。

-- migrations/0001-initialize.sql

CREATE TABLE documents (

id INTEGER PRIMARY KEY,

content TEXT

);

0001 というプレフィックスが鍵となります。これは、このスクリプトが最初に実行されるべきものであり、同時にデータベースのバージョン 1 を生成することを示しています。

PyPI からインストールしてアプリで使用できるようにするには、pip install fastmigrate を実行してください。

これでアプリケーションの起動コードは、fastmigrate を利用してデータベースの作成および/または更新を行うことができます。app.py という名前のファイルに、以下のように入力します:

from fastmigrate.core import create_db, run_migrations, get_db_version

db_path = "./app.db"

migrations_dir = "./migrations/"

バージョン管理されたデータベースが存在することを保証する。

データベースが存在しない場合は作成され、バージョン 0 に設定される。

データベースが既に存在する場合は何もしない。

create_db(db_path)

migrations_dir から未適用のマイグレーションをすべて適用する。

success = run_migrations(db_path, migrations_dir)

if not success:

print("Database migration failed! Application cannot continue.")

exit(1) # または、アプリ固有のエラーハンドリング処理を行う

このポイント以降、アプリケーションコードは 'documents' テーブルが 0001-initialize.sql で定義されたとおりに確実に存在すると安全に仮定できる。

データベースは現在バージョン 1 です。

version = get_db_version(db_path)

print(f"Database is at version {version}")

この Python コードを初めて実行する際、create_db() がデータベースを初期化し、メタデータを挿入して管理対象のデータベースであることを示すとともに、バージョン 0 に設定します。これは、現在のバージョンと管理対象データベースであることを示す小さな _meta テーブルを追加することで実現されます。

次に、run_migrations() 関数は 0001-initialize.sql を検出します。バージョン 1 がデータベースの現在のバージョン 0 より大きいため、この関数がスクリプトを実行し、データベースのバージョンを 1 にマークします。

その後の実行では、新しいマイグレーションスクリプトが追加されていない場合、run_migrations() はデータベースがすでにバージョン 1 に達していることを確認し、それ以上の処理は行いません。

python3 app.py でアプリを実行すると、何度実行してもデータベースはバージョン 1 であると報告されます。また、ディレクトリ内に作成されたデータベースファイル data.db も確認できます。

では、スキーマの進化についてはどうでしょうか?

ドキュメントテーブルに title カラムが必要だと判断した場合、そのカラムを追加するマイグレーションスクリプトを新たに追加するだけで済みます。

この変更により、データベースのバージョン 2 が定義されます。migrations ディレクトリには、0002-add-title-to-documents.sql という名前のファイルを追加してください。

-- migrations/0002-add-title-to-documents.sql

ALTER TABLE documents ADD COLUMN title TEXT;

重要な点は、アプリケーションの起動コードは変更されないことです。上記で示した Python のスニペットがそのまま維持されます。

このコードが、以前バージョン 1(つまり 0001-initialize.sql しか適用されていない状態)にあったデータベース上で実行されると、以下の手順が行われます:

create_db(db_path) がデータベースの存在とバージョン 1 であることを確認します。

run_migrations() は migrations/ ディレクトリをスキャンし、0002-add-title-to-documents.sql を発見します。このスクリプトのバージョン(2)がデータベースの現在のバージョン(1)より大きいため、新しいスクリプトが実行されます。

正常に実行されると、fastmigrate はデータベースのバージョンを 2 にマークします。

これらの fastmigrate コール後に実行されるアプリケーションコードは、now documents テーブルに id、content、および新しい title カラムがあると想定できるようになります。

python3 app.py でアプリを再度実行すると、データベースがバージョン 2 にあると報告されます。

もし内部でこれがどのように動作しているか気になる場合は、決して神秘的な仕組みではありません。fastmigrate は_meta テーブルを追加することでデータベースにマークを付けます。これは sqlite3 実行ファイルを使用して直接確認できます:

$ sqlite3 app.db .tables

_meta documents

中身を確認すると、バージョンが 2 になっていることがわかります:

$ sqlite3 app.db "select * from _meta;"

1|2

ただし、これは実装の詳細に過ぎません。重要な点はアプローチの転換です。

複雑な条件分岐ロジックは、アプリケーションのメイン起動シーケンスから完全に削除されました。

スキーマ変更は、小さく明確に名前付けされたバージョン管理された SQL スクリプトに隔離されます。

データベーススキーマが進化しても、アプリケーションのコア起動ルーチン (create_db(), run_migrations()) は安定しています。

残りのアプリケーションコード、つまり実際にデータベースを使用する部分は、常に最高番号のマイグレーションスクリプトで定義された単一の最新スキーマバージョンを前提として記述できます。古いデータベース構造に対する条件分岐パスは不要です。

この「アペンドオンリー」アプローチでは、後続の変更に対して常に新しい番号の大きいスクリプトを追加するため、データベースの進化が明示的かつ管理されやすく、統合も容易になります。ターゲットとなるスキーマバージョンに到達する責任は fastmigrate に委譲されます。

コードをバージョン管理システムにチェックインする際は、新しいデータベースバージョンを定義するマイグレーションスクリプトと、その新しいデータベースバージョンを必要とするアプリケーションコードの両方を必ず含めるように注意してください。そうすれば、アプリケーションコードは常に必要なデータベースバージョンを正確に参照することになります。

コマンドラインでのテスト

新しいマイグレーションスクリプトをアプリに統合する前に、もちろんテストを行う必要があります。マイグレーションスクリプトは単独で実行できるように設計されているため、これは非常に簡単です。対話型で実行をサポートするために、fastmigrate はコマンドラインインターフェース (CLI) も提供しています。

アプリが作成したデータベースを検査したい場合は、バージョンチェックコマンドを実行します:

$ fastmigrate_check_version --db app.db

FastMigrate version: 0.3.0

Database version: 2

CLI コマンドの名前が API と一致する場合、それらは全く同じ動作を行います。fastmigrate_create_db は fastmigrate.create_db と同様に動作し、fastmigrate_run_migrations は fastmigrate.run_migrations と同様です。

例えば、以下のコマンドを実行して、空の管理用データベースを作成し、その上でマイグレーションを実行できます:

$ fastmigrate_create_db --db data.db

データベースを data.db に作成中

data.db にバージョン 0 の新しいバージョン管理付き SQLite データベースが作成されました。

$ fastmigrate_run_migrations --db data.db --migrations migrations/

マイグレーション 1 を適用中:0001-initialize.sql

✓ データベースがバージョン 1 に更新されました (0.00 秒)

マイグレーション 2 を適用中:0002-add-title-to-documents.sql

✓ データベースがバージョン 2 に更新されました (0.00 秒)

マイグレーション完了

• 2 つのマイグレーションが適用されました

• データベースは現在バージョン 2 です

• 合計所要時間:0.00 秒

学ぶべき新しいことはありません!

新しいマイグレーションを導入する際の推奨ワークフローの詳細な手順については、マイグレーションを安全に追加する方法に関するガイドをご覧ください。

また、fastmigrate の外で開始されたデータベースを取得し、管理対象のデータベースとして登録するためのガイダンスも用意されています。技術的にはこれは単にデータベースのバージョンを示すプライベートメタデータ(metadata)を追加するだけの作業ですが、ツールは登録しようとしているデータベースと同等のデータベースを初期化する必要があるため、ドラフト版の 0001-initialize.sql マイグレーションスクリプトを生成することで、開始時のサポートを提供します。この生成されたスクリプトはあくまでドラフトであり、ご自身のデータベースに正確に適しているかどうかを手動で必ず確認する必要があります。

シンプル=明確=落ち着き

もう一度その地図を見て、私たちの祖先がエアコンもポッドキャストも AI チャットボットもない状態で数千マイルを旅したことを考えてみてください。それは過酷なものでしたが、はい、私たちはそれほどひどい状況にあるわけではありません。

しかしながら、本番環境のデータベースの進化を管理するのはストレスがかかります。

これは自然なことです。なぜなら、それはユーザーのデータだからです。ほとんどのソフトウェアの全目的は、そのデータを処理・保存することにあります。したがって、もしデータベースを誤操作すれば、ソフトウェアはその存在する主な理由において失敗したことになります。

そのようなストレスに対する解毒剤は明確さです。自分が何をしているのかを知りたいのです。

誰かが git のコミットをハッシュ値で参照したときに感じる温かい安心感を想像してみてください。(んー。)その感覚が生まれるのは、ハッシュ値が曖昧さを含まないからです。git に 2 つのコミットハッシュの間に変更されたファイルを計算させれば、答えの意味が正確にわかります。データベースについても同じような明確さを持ちたいものです。

マイグレーションパターンは、データベースに単純なバージョン番号を持たせることでその明確さをもたらします。この番号により、データベースがどの状態にあるかがわかり、したがってアプリケーションが期待できることが正確に特定できます。

そして、これはシンプルなアイデアなので、必要なツールもシンプルで十分です。

だからこそ fastmigrate は、create_db、get_db_version、run_migrations という数個の主要なコマンドのみを導入し、ファイルの一覧表示や整数の意味解釈など、すでに知っている機能に依存しています。

一方、多くの既存のデータベースツールは複雑です。なぜなら、それらは他にも多くの機能を備えているからです——オブジェクトリレーショナルマッパー(ORM)、テンプレートシステム、さまざまなバックエンドへのサポート、異なる構文を持つ複数の設定ファイルに対する要件など。もしあなたのシステムの複雑さがこれらすべてを必要とするレベルにまで成長しているなら、それがまさに必要なものです。

しかし、システムをシンプルに保つことができるのであれば、シンプルな解決策の方がより良い結果をもたらします。理解しやすく、使いやすく、頭の中や手の中で扱いやすいものになります。ニンジンをおろすとき、あなたは鋭い包丁を望みますか?それとも、特別なニンジンスライサーアタッチメント付きのフードプロセッサーを望みますか?そのアタッチメントを取り付ける方法を理解するために、マニュアルを読まなければならないようなものです。

fastmigrate は、そのような鋭い包丁になることを目指しています。あなたがそれを明確さと自信を持って扱えるよう願っています!

原文を表示

Note

TLDR: This post introduces fastmigrate, a Python database migration tool. It focuses on sqlite, and it does not require any particular ORM library. It’s suitable if you want to work directly with sqlite and keep things simple. For instructions, check out the fastmigrate repo.

Let’s talk migrations!

image
image

not the migrations we’re talking about

Uh, no. Let’s talk about the database migration pattern.

Migrations represent a powerful architectural pattern for managing change in your database. They let you write your application code so that it only needs to know about the latest version of your database, and they simplify the code you use to update the database itself.

But it is easy to overlook this pattern because many database helper libraries do so many other things at the same time, in such a complex fashion, that they obscure the simplicity of this basic pattern.

So today, we’re releasing fastmigrate, a library and command line tool for database migrations. It embraces the simplicity of the underlying pattern by being a simple tool itself. It provides a small set of commands. It treats migrations as just a directory of your own scripts. It only requires understanding the essential idea, not a lot of extra jargon. We like it!

This article will explain what database migrations are in general and what problem they solve, and then illustrate how to do migrations in sqlite with fastmigrate.

The problem which migrations solve

The core problem which migrations solve is to make it easier to change your database schema (and other basic structures) without breaking your application. They do this by making database versions explicit and managed, just like the changes in your application code.

To see how complexity creeps in otherwise, consider a typical sequence of events in developing an app. The first time the app runs, it only needs to handle one situation, the case where there is no database yet and it needs to create one. At this point, your app’s startup code might look like this:

App v1

db.execute("CREATE TABLE documents (id INT, content TEXT);")

But wait… The second time a user runs that same app, the table will already exist. So in fact your code should handle two possible cases – the case where the table does not exist, and the case where it already exists.

So in the next version of your app, you update your initialization code to the following:

App v2

db.execute("CREATE TABLE IF NOT EXISTS documents (id INT, content TEXT);")

Later, you might decide to add a new column to the database. So in your app’s third version, you add a second line:

App v3

db.execute("CREATE TABLE IF NOT EXISTS documents (id INT, content TEXT);")

db.execute("ALTER TABLE documents ADD COLUMN title TEXT;")

But wait again… You don’t want to alter the table like this if the column already exists. So App v4 will need more complex logic to handle that case. And so on.

Even this trivial example would create bugs if not handled properly. In a real app, as you introduce and then modify table relationships, such issues become more subtle, numerous, and stressful since one wrong step can lose user data.

What happens is that, with every new version, your application’s code grows more complicated because it is required to handle not just one state of the database but every possible previous state.

To avoid this, you would need to force separate database updates so that your application code knew exactly what to expect from the database. This is often not feasible when the app manages the database and every user gets to decide when to run their own installation of the app, as is the case in a mobile app, a desktop app, or a webapp with one database per user. Even in systems with a single database, forcing separate database updates would introduce an important new kind of change to manage – that is, database changes, which would need to be delicately coupled with changes in your application code.

This gets to the heart of the problem, which is that by default these various database states are implicit and unmanaged.

With your application code, a git commit unambiguously specifies both a version of your code and the change which produced it. Then, your deployment system lets you control exactly which version of your application your users will see next. But with your database, without some system, all you know is that the database is in some unnamed state produced by previous code. The version control and deployment tools which so nicely manage your application code will not automatically control which version of the database your application sees next.

How migrations solve this problem

The database migration pattern solves this problem with two key measures:

First, defining database versions, based on migrations. Instead of reasoning about unnamed database state, we introduce explicit version management of your database.

How do we do this? With migration scripts. A migration script is an isolated, single-purpose script whose only job is to take the database from one version (e.g., 5) to the next version (e.g., 6).

Fastmigrate keeps this simple and names the scripts based on the database version they produce so that, for instance, the script named 0006-add_user.sql must be the one and only script which produces database version 6. In a fundamental sense, the version numbers in the migration scripts define the set of recognized database versions. Thus, you can see the past version of your database by listing the scripts which produced those versions, just like looking at a log of git commits:

$ ls -1 migrations/

0001-initialize.sql

0002-add-title-to-documents.sql

0003-add-users-table.sql

This structured approach enables the next key measure.

Second, writing the app to target one database version. Moving the database evolution code into these migration scripts means that the application code can forget about database changes and target only one version of the database, the latest version.

The application can rely on a migration library, like fastmigrate, to run whatever migrations are needed. That might mean recapitulating all the migrations to create the latest version of the database from nothing when running a fresh instance in development. Or it might mean applying only the latest migration, to bring a recent database version up to date. Or it might mean something in between. The point is, the application does not need to care.

One way to measure the simplification is to count how many fewer cases different parts of your system need to handle.

Before migrations, your application code was in effect responsible for handling all possible previous database states, even when it would have required increasingly careful attention to remember and understand just what all those states were. After migrations, everything is explicit, legible, and factored. The application is responsible for working with just one database version. And every database version has exactly one script which produces it from one previous version. (So clean! Doesn’t it make you want to sigh? Ahhhh…)

Feature

Without migrations

With migrations

DB States

Uncounted, unnamed

imageimage explicit versions

DB Management

None

imageimage isolated migration scripts, one per version

App Requirements

App must support all DB states, and manage DB changes

App must support only one DB version, the latest

How to use fastmigrate

Let us follow the previous example again, and see how this works in fastmigrate.

Instead of embedding the evolving database schema logic into your app’s startup, you will define a series of migration scripts. These scripts are SQL, but you could also use Python or shell scripts. Your application will then use fastmigrate’s API to run those scripts as needed, bringing the database to the latest expected version automatically.

Your first migration script creates the table. Create a directory migrations/ and in that directory put the file 0001-initialize.sql.

-- migrations/0001-initialize.sql

CREATE TABLE documents (

id INTEGER PRIMARY KEY,

content TEXT

);

The 0001 prefix is key: it indicates this is the first script to run, and also that it produces version 1 of your database.

Run pip install fastmigrate to install it from PyPi, so your app can use it.

Now your application startup code can rely on fastmigrate to create and/or update the database. Create your app, in a file called app.py:

from fastmigrate.core import create_db, run_migrations, get_db_version

db_path = "./app.db"

migrations_dir = "./migrations/"

Ensures a versioned database exists.

If no db exists, it's created and set to version 0.

If a db exists, nothing happens

create_db(db_path)

Apply any pending migrations from migrations_dir.

success = run_migrations(db_path, migrations_dir)

if not success:

print("Database migration failed! Application cannot continue.")

exit(1) # Or your app's specific error handling

After this point, your application code can safely assume

the 'documents' table exists exactly as defined in 0001-initialize.sql.

The database is now at version 1.

version = get_db_version(db_path)

print(f"Database is at version {version}")

The first time this Python code runs, create_db() initializes your database, and inserts metadata to mark it as a managed database with version 0. This is done by adding a small _meta table, which stores the current version and indicates it is a managed database.

Then, the function run_migrations() sees 0001-initialize.sql. Since version 1 is greater than the database’s current version 0, the function executes it, and marks the database’s version to 1. On subsequent runs, if no new migration scripts have been added, run_migrations() sees the database is already at version 1 and does nothing further.

You can run your app now, with python3 app.py, and the app will report that the db is at version 1, no matter how many times you run it. You will also be able to see in your directory data.db, the database file it created.

But what about schema evolution?

When you decide your documents table needs a title column, you only need to add a migration script which adds the column.

This change defines version 2 of your database. In the migrations directory, add a file named 0002-add-title-to-documents.sql.

-- migrations/0002-add-title-to-documents.sql

ALTER TABLE documents ADD COLUMN title TEXT;

The key point is, your application startup code does not change: It remains the same Python snippet shown above.

When that code runs on a database which was previously at version 1 (i.e., where only 0001-initialize.sql had been applied), the following happens:

create_db(db_path) confirms the database exists and is at version 1.

run_migrations() scans the migrations/ directory. It finds 0002-add-title-to-documents.sql. Since the script’s version (2) is greater than the database’s current version (1), it executes this new script.

After successful execution, fastmigrate marks the database’s version to 2.

Your application code, which runs after these fastmigrate calls, can now assume the documents table has id, content, and the new title column.

Run your app again, with python3 app.py, and now it will report the database is at version 2.

If you are curious how this works under the hood, it is nothing occult. Fastmigrate marks a database by adding the _meta table, which you can see directly by using the sqlite3 executable:

$ sqlite3 app.db .tables

_meta documents

You can look in it to see the version is now 2:

$ sqlite3 app.db "select * from _meta;"

1|2

But this an implementation detail. The crucial point is the shift in approach:

The complex conditional logic is entirely removed from your application’s main startup sequence.

Schema changes are isolated into small, clearly named, versioned SQL scripts.

Your application’s core startup routine (create_db(), run_migrations()) is stable, even as the database schema evolves.

The rest of your application code, the part that actually uses the database, can always be written to expect the single, latest schema version defined by the highest-numbered migration script. It doesn’t need conditional paths for older database structures.

This "append-only" approach to migrations, where you always add new, higher-numbered scripts for subsequent changes, makes your database evolution explicit, managed, and easy to integrate. The responsibility for reaching the target schema version is delegated to fastmigrate.

When you check your code into version control, you should take care to include the migration script which defines the new database version along with the application code which requires that new database version. Then, your application code will always see exactly the database version which it requires.

Testing on the command line

Before integrating a new migration script into your app, you will of course want to test it. This is straightforward since migration scripts are designed to run in isolation. To help run them interactively, fastmigrate also provides a command line interface (CLI).

If you want to inspect the database your app just created, you can run the check version command:

$ fastmigrate_check_version --db app.db

FastMigrate version: 0.3.0

Database version: 2

When the names of CLI commands match the API, they do exactly the same thing. fastmigrate_create_db behaves just like fastmigrate.create_db, fastmigrate_run_migrations like fastmigrate.run_migrations, and so on.

For instance, you can run these commands to create an empty managed db and run migrations on it:

$ fastmigrate_create_db --db data.db

Creating database at data.db

Created new versioned SQLite database with version=0 at: data.db

$ fastmigrate_run_migrations --db data.db --migrations migrations/

Applying migration 1: 0001-initialize.sql

✓ Database updated to version 1 (0.00s)

Applying migration 2: 0002-add-title-to-documents.sql

✓ Database updated to version 2 (0.00s)

Migration Complete

• 2 migrations applied

• Database now at version 2

• Total time: 0.00 seconds

Nothing new to learn!

For a more detailed walkthrough of the recommended workflow when introducing a new migration, please see our guide on safely adding migrations.

There is also guidance on taking a database which started outside of fastmigrate, and enrolling it as a managed database. Technically, this is nothing more than adding the private metadata which marks the database’s version. But the tool will gives you some help in getting started by generated a draft 0001-initialize.sql migration script, since you will need one which initializes a database equivalent to the database which you are enrolling. This generated script is only a draft since you should definitely verify manually that it is correct for your database.

Simple = Clear = Calm

Check out that map again and consider that our ancestors traveled thousands of miles, without even having air conditioning, podcasts, and AI chatbots to flatter them. It was rough and, yes, we don’t have it so bad.

But nevertheless, managing the evolution of a production database is stressful.

This is natural enough, since it’s the user’s data. The whole purpose of most software is to transform and store that data. So if you mess up your database, your software has failed at its main reason for existing.

The antidote to that stress is clarity. You want to know what you are doing.

Consider that warm feeling of comfort you get when someone refers to a git commit by its hash. (Mmmm.) That feeling is because a hash is unambiguous. If you ask git to compute which files changed between two commit hashes, you know exactly what the answer means. You want to have the same clarity regarding your database.

The migrations pattern brings that by ensuring your database has a simple version number which tells you what state it is in and, therefor, exactly what your application can expect.

And since it’s a simple idea, it needs only a simple tool.

That is why fastmigrate introduces only a few main commands – create_db, get_db_version, and run_migrations – and relies on things you already know, like how to list files and interpret an integer.

In contrast, many existing database tools are complex because they provide a lot of other things as well – object-relational mappers, templating systems, support for various backends, requirements for multiple config files with different syntaxes. If your system has grown in complexity to the point where it needs all that, then that is what you need.

But if you are able to keep your system simple, then a simple solution will serve you better. It will be easier to understand, easier to use, easier to hold in your head and in your hand. If you were chopping a carrot, would you want a good sharp knife? Or a food processor, with a special carrot-chopping attachment, which you need to read the manual of just to figure out how to attach it?

fastmigrate aims to be a good sharp knife. May you wield it with clarity and confidence!

この記事をシェア

関連記事

Simon Willison Blog★32026年4月16日 08:16

Datasette 1.0a27のリリース

Datasette開発者はバージョン1.0a27を公開し、Django形式のCSRFトークン廃止とモダンブラウザヘッダーへの移行など、セキュリティ関連の主要な変更を実施した。

KDnuggets★32026年6月10日 21:00

退屈な PDF タスクを自動化する Python スクリプト 5 つ

KDnuggets は、PDF の処理や変換など日常的な作業を自動化するための有用な Python スクリプト 5 つを紹介した。

Simon Willison Blog★32026年6月6日 12:53

MicroPython と WASM を用いたサンドボックス環境での Python コード実行

Simon Willison は、コード実行のサンドボックス環境を実現する新アルファパッケージ「micropython-wasm」を公開し、Datasette Agent のプラグインとして利用を開始した。

今日のまとめ

AI日報で今日の重要ニュースをまとめ読み

ニュース一覧に戻る元記事を読む