コアDBマイグレーション
このページで分かること
- コアマイグレーションの対象(EmDash自身のテーブルと、コンテンツテーブルの標準の列)と、前にしか進まないこと
- ビルド・マイグレーション・デプロイ・確認の流れ(
.emdash/migrations.jsonとemdash migrate)、対象のデータベースの指定、D1の用意とCIでの実行 - 実行時のモード(
auto、check、manual)の段階的な切り替え、ローリングデプロイ中の互換性、巻き戻しの限界、PostgreSQLの所有者の修復、トラブルシューティング
このページの目次
EmDashのコアマイグレーションは、EmDash自身のテーブルと、コンテンツテーブルの標準の列を更新します。自分のコレクションやフィールドの作成、削除、名前の変更はしません。コンテンツモデルの変更については、運用中サイトのスキーマ変更を参照してください。
実行時のマイグレーションのモードは既定で auto のため、既存のデプロイは起動時に未適用のコアマイグレーションを適用し続けます。デプロイで管理するマイグレーションでは、新しいアプリケーションのコードがリクエストを受け取る前に、ビルドがそのデータベースをマイグレーションします。そのうえで、実行時にはそのデプロイの工程を検証するか、信頼します。
コアマイグレーションは前にしか進みません。確実に完了した文の後からコマンドを再試行できるように書かれていますが、中断したリモートのコマンドは、結果があいまいな状態を残すことがあります。安全な対応は、同じデータベースを emdash migrate --status で調べることです。マイグレーション全体が実行された、または何も実行されなかったと決めつけないでください。
やさしい解説
WordPressでも、本体を更新すると「データベースの更新が必要です」と表示され、データベースの構造が新しいバージョンに合わせて変わることがあります。EmDashのコアマイグレーションも同じで、EmDash自身が使うテーブルの構造を更新します。自分で作ったコレクションやフィールドは変更しません。一度適用したマイグレーションは元に戻す手順がないため、途中で止まった場合も「全部終わった」「何も起きていない」と考えず、emdash migrate --status で状態を確かめます。
ビルド、マイグレーション、デプロイ、確認
Astroのビルドまたは同期(sync)は、.emdash/migrations.json を書き出します。秘密情報を含まないこのマニフェストには、そのビルドが使ったEmDashの正確なバージョン、順序付きのマイグレーションの集合、ロケールの設定、アダプターのマイグレーション実行部が記録されます。
これらのコマンドは、マニフェストを生成した依存パッケージを持つプロジェクトで実行します。まずビルドし、対象を調べます。
pnpm build
pnpm emdash migrate --status
表示された対象が意図したデータベースであることを確認した後、対話形式のマイグレーションを開始します。確定する前に、プロンプトでもう一度対象を確認します。その後、同じビルドをデプロイし、デプロイしたスキーマを確認します。
pnpm emdash migrate
pnpm wrangler deploy
pnpm emdash migrate --check
emdash migrate --status は、データベースを変更せずに、適用済み、未適用、不明のマイグレーションを報告します。オプションを付けない emdash migrate コマンドは、対象を表示し、未適用のマイグレーションを適用する前に確認を求めます。
--check はマイグレーションを適用しません。既知のマイグレーションが未適用の場合や、ビルドが知らないマイグレーションの記録がデータベースにある場合は、0以外の終了コードで終了します。checkの「作業が必要」を示す0以外の終了コードなしで同じマイグレーションの集合を調べたい場合は、--status を使います。CLIリファレンスでは、未適用、不明、確認、中断、運用上のエラーの各終了コードを区別して説明しています。
非対話形式の適用と、--json を付けたすべての適用には、--expected-target-fingerprint が必要です。解決された対象が一致しない場合、コマンドは失敗します。これらのオプションは自動化したデプロイのジョブで使います。上の対話形式の手順では使いません。
別の場所に保存したマニフェストには、--manifest path/to/migrations.json を使います。ローカルで調べる場合は、--from-config [--config astro.config.mjs] を使うと、Astroのフックを実行したりサーバーを起動したりせずに、信頼できるプロジェクトの設定を明示的に評価します。デプロイのパイプラインでは、ビルドのマニフェストを使ってください。
やさしい解説
流れは「ビルド → emdash migrate --status で対象を確認 → emdash migrate で適用 → デプロイ → emdash migrate --check で確認」です。ビルドのときに作られる .emdash/migrations.json には、そのビルドで必要なマイグレーションの一覧が記録されており、emdash migrate はこれを見てデータベースを更新します。このファイルはビルドのたびに作られるため、Gitには含めません。
データベースを明示的に選ぶ
設定したアダプターは、秘密情報を含まない対象の情報をマニフェストに書き込みます。認証情報は環境変数に置いたままで、マイグレーションのコマンドだけが読み込みます。
| アダプター | マニフェストに書かれる対象 | 既定の認証情報の変数 | 役立つ上書き |
|---|---|---|---|
| SQLite | データベースのパスまたは file: URL |
— | --database <path> |
| libSQL | 公開URL | TURSO_AUTH_TOKEN |
migrationAuthTokenEnv を設定する |
| PostgreSQL | 接続用の変数の名前 | DATABASE_URL |
--database-url-env <name> |
| Cloudflare D1 | Wranglerのバインディング名 | CLOUDFLARE_API_TOKEN |
--d1、--account-id、--wrangler-config、--wrangler-env |
| Hyperdrive | プライマリーのバインディングと、オリジンの変数の名前 | バインディングごとの、オリジンに直接接続する変数 | migrationConnectionStringEnv を設定する |
SQLiteの相対パスは、インストールされたEmDashパッケージやシェルの現在のサブディレクトリーからではなく、プロジェクトのルートから解決されます。PostgreSQL、libSQL、Hyperdriveの対象のラベルには、認証情報とURLのパラメーターは含まれません。
マイグレーションの前にD1を用意する
D1データベースの作成と、そのスキーマのマイグレーションは別の操作です。emdash migrate が、存在しないデータベースを作成することはありません。
-
データベースを用意し、本番のUUIDを記録します。
pnpm wrangler d1 create my-site-production -
そのUUIDを、
wrangler.jsoncの意図したバインディングと環境に追加します。 -
サイトをビルドし、D1のバインディングが
.emdash/migrations.jsonに記録されるようにします。 -
アカウントIDと、D1 Edit権限を持つ範囲を絞ったAPIトークンを設定します。選択された対象を調べてから、対話形式のマイグレーションを実行します。アカウントとデータベースが意図した本番のデータベースと一致する場合にだけ、プロンプトで確定します。
export CLOUDFLARE_ACCOUNT_ID="..." export CLOUDFLARE_API_TOKEN="..." pnpm emdash migrate \ --status \ --wrangler-config wrangler.jsonc \ --wrangler-env production pnpm emdash migrate \ --wrangler-config wrangler.jsonc \ --wrangler-env production
代わりに、--account-id と --d1 <database-uuid-or-name> を指定することもできます。名前で検索する場合は、ちょうど1つのデータベースに解決される必要があります。プレビューのID、仮のID、アカウントの食い違い、あいまいなバインディングは、安全側に倒して失敗します。
CIでD1のマイグレーションを設定する
D1には、PostgreSQLが使うマイグレーション用のアドバイザリーロックがありません。1つのアカウントとデータベースのUUIDに対して、マイグレーションのジョブは同時に1つまでしか実行しないでください。
CIの環境に、次のシークレットと変数を設定します。
- シークレット
CLOUDFLARE_API_TOKEN:D1 Edit権限を持つ、範囲を絞ったトークン。 - 変数
CLOUDFLARE_ACCOUNT_ID:データベースを所有するCloudflareのアカウントID。 - 変数
D1_DATABASE_ID:本番のD1データベースのUUID。 - 変数
EMDASH_TARGET_FINGERPRINT:ローカルでアカウントとデータベースを確認した後に、emdash migrate --statusが表示したフィンガープリント。
次のGitHub Actionsのワークフローは、これらの値を使い、変わることのない2つのD1の識別子の両方で同時実行のグループを分けます。適用のステップは非対話形式のため、確認済みの対象のフィンガープリントを明示的に渡します。
name: Deploy
on:
workflow_dispatch:
concurrency:
group: emdash-migrations-${{ vars.CLOUDFLARE_ACCOUNT_ID }}-${{ vars.D1_DATABASE_ID }}
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Inspect EmDash migration target
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
pnpm emdash migrate --status --json \
--account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
--d1 "${{ vars.D1_DATABASE_ID }}"
- name: Apply EmDash migrations
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
EMDASH_TARGET_FINGERPRINT: ${{ vars.EMDASH_TARGET_FINGERPRINT }}
run: |
pnpm emdash migrate \
--account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
--d1 "${{ vars.D1_DATABASE_ID }}" \
--expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
- run: pnpm wrangler deploy
- name: Check EmDash migrations
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
pnpm emdash migrate --check \
--account-id "${{ vars.CLOUDFLARE_ACCOUNT_ID }}" \
--d1 "${{ vars.D1_DATABASE_ID }}"
EMDASH_TARGET_FINGERPRINT は、変わった対象をローカルで確認した後にだけ更新します。フィンガープリントに認証情報は含まれませんが、アカウントとデータベースを確かめずに変更すると、誤ったデータベースをマイグレーションすることを防ぐ仕組みがなくなります。
Hyperdriveはオリジンに接続する
Hyperdriveのマイグレーション実行部は、オリジンのPostgreSQLに直接接続します。マイグレーションの通信をHyperdrive経由で送ることはなく、省略可能なキャッシュ付きのバインディングも使わず、Workerのプライベートネットワークへの到達性も引き継ぎません。
デプロイを実行する環境(ランナー)は、オリジンに到達できる必要があります。既定のバインディングごとの変数が適さない場合は、hyperdrive() に migrationConnectionStringEnv を設定し、その変数はマイグレーションのジョブにだけ渡します。実行時のHyperdriveの認証情報と、デプロイ時にオリジンに直接接続する認証情報は分けておきます。
実行時の強制を段階的に取り入れる
次のEmDashインテグレーションの設定は、開発では自動のマイグレーションを残したまま、実行時の強制を有効にします。
emdash({
database,
migrations: {
runtime: "check",
dev: "auto",
},
});
autoは、後方互換のある既定値です。実行時の起動処理が、未適用のマイグレーションを確認して適用します。checkは、一方向の状態の問い合わせを1回実行し、既知のマイグレーションが未適用の場合は、リクエストを処理する前に503を返します。ローリングデプロイの間は、互換性のある新しいビルドの記録を許容します。manualは、実行時にマイグレーションも状態の問い合わせもしません。デプロイのパイプラインが、すべてのビルドで確実に適用と確認をするようになってから使います。
同じ成果物を複数の環境に順に昇格させる場合は、EMDASH_MIGRATIONS_MODE で実行時のモードを上書きできます。初期設定と開発用の迂回ルートは、有効なモードに従います。check や manual の裏で、黙ってマイグレーションすることはできません。
慎重な進め方は、デプロイのジョブを導入する間は auto、ジョブが確実に動くようになったら check、すべてのデプロイで外部の確認を必須にしたら manual、という順です。
やさしい解説
3つのモードは「誰がデータベースを更新するか」の違いです。auto はサイトが起動時に自分で更新します(WordPressの自動更新に近い動きです)。check はサイトは更新せず、更新が済んでいなければエラー(503)を返します。manual はサイトは何も確かめず、更新は完全にデプロイの工程に任せます。まず auto のまま emdash migrate を使うデプロイの流れを作り、それが安定してから check、manual の順に切り替えます。
ローリングデプロイ中の互換性
コアマイグレーションは、拡張・デプロイ・縮小(expand/deploy/contract)の順序に従います。デプロイの間、古いアプリケーションと新しいアプリケーションのisolateが一時的に、拡張されたデータベースに対して同時に動くことがあり、データの埋め戻しがまだ進行中のこともあります。デプロイしたすべてのバージョンがそのスキーマを使わなくなるまで、スキーマを縮小しないでください。
適用済みの不明なマイグレーションの記録は、このローリングデプロイの方向に限って、実行時の check で許容されます。CLIの厳密な確認はそれらを報告し、適用は変更を拒否します。データベースのほうが新しいか、マイグレーションの履歴が分岐している可能性があるためです。
巻き戻しの限界
以前のアプリケーションの成果物をデプロイしても、コアマイグレーションは元に戻りません。未適用のマイグレーションを適用する前に、復元できるデータベースのバックアップを取り、それに対応するアプリケーションの成果物を記録します。以前のアプリケーションがマイグレーション後のスキーマで動かない場合は、マイグレーション前のデータベースとアプリケーションを一緒に復元します。運用上の巻き戻しとして、_emdash_migrations の行を削除したり、マイグレーション内部の down() 関数を実行したりしないでください。
PostgreSQLの所有者の混在を修復する
既存のPostgreSQLのサイトで、EmDashのオブジェクトが複数の所有者によって作成され、その後のマイグレーションが must be owner of table のようなエラーで失敗する場合は、この手順書を使います。EmDashのメインの接続が今後も使い続ける、基準のロールを決めます。所有者を変更する前に、復元できるデータベースのバックアップを取り、アプリケーションの通信とスキーマの変更を止めます。
有効なスキーマのすべてのテーブルを調べます。
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
pg_get_userbyid(c.relowner) AS owner
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
AND c.relkind IN ('r', 'p')
ORDER BY c.relname;
EmDashのオブジェクトには、_emdash_* と _plugin_* のシステムテーブル、ec_* のコレクションのテーブル、content_taxonomies、media、options、revisions、taxonomies のような接頭辞のないテーブルが含まれます。EmDash専用のスキーマでは、すべてのアプリケーションのテーブルの所有者が基準のロールである必要があります。
EmDashは、メディアの使用状況のトリガーが使うPostgreSQLの関数も作成します。関数の所有者を調べ、修復のコマンドのために各関数の引数のシグネチャーを控えておきます。
SELECT
n.nspname AS schema_name,
p.proname AS function_name,
pg_get_function_identity_arguments(p.oid) AS arguments,
pg_get_userbyid(p.proowner) AS owner
FROM pg_proc AS p
JOIN pg_namespace AS n ON n.oid = p.pronamespace
WHERE n.nspname = current_schema()
ORDER BY p.proname, arguments;
所有者が一致しないオブジェクトを、所有者を変更できるスーパーユーザーまたはプロバイダーのロールで1つずつ移します。例の名前をそのままコピーせず、調べた一覧にある実際のスキーマ、オブジェクト、ロール、関数のシグネチャーを使います。
ALTER TABLE emdash.content_taxonomies OWNER TO emdash_app;
ALTER TABLE emdash.ec_posts OWNER TO emdash_app;
ALTER FUNCTION emdash.emdash_media_usage_capture_work() OWNER TO emdash_app;
テーブルの所有者を変更すると、そのテーブルに付いているインデックス、制約、トリガーも対象になりますが、独立したトリガー関数は対象になりません。EmDashのすべてのテーブルと関数の所有者が基準のロールになるまで、2つの調査のクエリを繰り返します。その後、そのロールで接続し、通信を再開する前に current_database()、current_schema()、マイグレーションの状態を確かめます。
スーパーユーザーではないロールが所有者を移すには、そのロールがオブジェクトを所有しているか所有権を継承していること、新しい所有者に SET ROLE できること、新しい所有者がスキーマに対して CREATE を持っていることが必要です。マネージドのPostgreSQLのプロバイダーでは、移すためにプロバイダーの管理用のロールが必要な場合があります。
トラブルシューティング
- No migration manifest found。 先にプロジェクトをビルドまたは同期します。標準以外の場所にある成果物には
--manifestを使い、ローカルで調べる場合は明示的に--from-configを選びます。 - The artifact does not match project EmDash。 アプリケーションとマニフェストを一緒にビルドし直してデプロイします。グローバルにインストールしたものではなく、プロジェクトのCLIを実行します。
- 対象がない、またはあいまいである。 先に対象を用意し、そのうえでデータベースのパス、接続用の変数の名前、D1のセレクター、またはWranglerの設定と環境を明示的に指定します。EmDashは、関係のない環境変数やバインディングから推測しません。
- 対象のフィンガープリントが変わった。 作業を止め、表示されたアカウント、環境、データベース名、UUID、パスを確認します。意図した対象であることを確かめてから、期待するフィンガープリントを更新します。
- 不明なマイグレーションの記録がある。 記録を削除したり、適用を再実行したりしないでください。アプリケーションの成果物が意図したバージョンであることを確認し、より新しいビルドや分岐したビルドがデータベースをマイグレーションしていないかを調べます。
- D1への書き込みの結果があいまいである。 マイグレーションのコマンドを再実行しないでください。同じアカウントとデータベースのUUIDに対して
emdash migrate --statusを実行して結果を調べ、マイグレーションが途中で止まっていた場合はエスカレーションします。 - 日時の正規化に手作業での確認が必要である。 古い形式の日時が、サイトに設定したタイムゾーンで、夏時間により重複する時間帯または存在しない時間帯に当たっています。エラーには、影響を受ける各コンテンツの行またはリビジョンが一覧で表示されます。それらの値を明示的なUTCオフセット付きの値に直してから、マイグレーションを再試行します。マイグレーションの事前チェックは、保存されているすべての値を解決できるまで、どの日時も書き込みません。
- Hyperdriveが接続できない。 デプロイを実行する環境からオリジンのPostgreSQLに到達できるかを試し、オリジンに直接接続する変数を確かめます。WorkerからHyperdriveに接続できても、ランナーがオリジンに到達できるとは限りません。