10 KiB
name, description
| name | description |
|---|---|
| create-migration | Misskey の TypeORM マイグレーションを公式 CLI (migration:generate / migration:create) で正しく生成し、SPDX ヘッダー付与・up/down 整合・check-migrations 確認まで誘導する。エンティティのスキーマ変更を含むあらゆる DB 変更、または手書き SQL によるデータ移行が必要な時に使用する。 |
Misskey マイグレーション作成スキル
packages/backend/migration/ に新規 TypeORM マイグレーションを追加するためのワークフロー。
大前提 (絶対 NG)
- 既にマージ済み (develop / master) のマイグレーションファイルを編集しない (AGENTS.md §3)。本番履歴の改変は深刻なデータ不整合を引き起こす。スキーマ変更は 常に新しいタイムスタンプで新規ファイル を作る。
- ファイル名のタイムスタンプ部分を後から書き換えない (順序が壊れる)。
作り方は AGENTS.md §3 の「
Date.now()で UNIX ms を取得 →{ms}-{PascalName}.jsを手書き」が最低ライン。エンティティ差分から自動生成したい (= TypeORM のmigration:generateを使う) 場合は本 skill の手順に従う。どちらでも構わないが、エンティティ変更を伴う時は CLI 経由のほうが取り漏れが減るので推奨。
ステップ 1: どちらの方式を使うか決める
| 状況 | 方式 |
|---|---|
エンティティ (packages/backend/src/models/*.ts) を @Column / @Index / @Entity 等で先に変更し、差分から自動生成したい |
typeorm migration:generate (本 skill の手順) |
手書き SQL / データ移行 / CREATE INDEX CONCURRENTLY など、エンティティ差分では表現できない変更 |
typeorm migration:create で空雛形を作るか、migrate-new command で手書き雛形を作る |
| 列追加 1 本のような小規模変更で、既存ファイルをコピーした方が速い | AGENTS.md §3 の手順 (Date.now() + 手書き) でよい |
迷ったら まずエンティティを変更 → migration:generate が原則。既存 342 ファイルのほぼすべてが queryRunner.query(\SQL...`)` の raw SQL なので、CLI 出力でも手書きでもスタイルは揃う。
ステップ 2: CLI 実行
ルートディレクトリから以下を実行する。<PascalName> は変更内容を表す PascalCase (例: AddBirthdayIndex, AddCategoryToAvatarDecorations)。
2-A. エンティティ差分から生成
CONTRIBUTING.md §Migration作成方法 に記載の基本形:
# packages/backend ディレクトリで実行する場合 (CONTRIBUTING.md 記載形式)
pnpm dlx typeorm migration:generate -d ormconfig.js -o --esm <PascalName>
リポジトリルートから実行する場合 (AI が使う推奨形式。pnpm --filter backend exec を使うと backend の TypeORM バージョンと一致するため確実):
pnpm --filter backend exec typeorm migration:generate -d ormconfig.js -o --esm migration/<PascalName>
--esmについて:-o/--outputJsは「TS ではなく JS を出力する」オプション、--esmは「ESM 形式 (export class ...) で出力する」オプション。Misskey の既存 migration はすべて ESM JS であるため 両方が必須。--esmを省略すると CommonJS 形式の JS が生成されスタイルが揃わない。
事前準備:
pnpm build-preを実行してbuilt/meta.jsonを生成する (loadConfig()がbuilt/meta.jsonを必須とするため。pnpm build済みであれば不要)。.config/default.ymlが存在すること (なければ.config/example.ymlを参考に作成する)。pnpm --filter backend compile-configを実行してbuilt/.config.jsonを生成する (ormconfig.jsがloadConfig()経由で必須とする。未実行だと "Compiled configuration file not found." エラーになる)。pnpm --filter backend buildでエンティティを最新ビルド (CLI はbuilt/を読む)。- ローカル DB を起動する (
docker compose -f compose.local-db.yml up -d)。
2-B. 空の手書きマイグレーション
pnpm --filter backend exec typeorm migration:create -o --esm migration/<PascalName>
ローカル DB の起動とビルドは不要。空の up / down だけが生成される。
-o --esmを 必ず付ける。これが無いと<UnixMs>-<PascalName>.ts(CommonJS / TS 出力) が生成されるが、Misskey のormconfig.jsはmigration/*.jsだけを読み、既存の他 migration も全てexport class ... { async up(queryRunner) {...} }の ESM JS 形式なので、後で手作業で.ts → .jsリネーム +import { MigrationInterface }削除 +class ... implements MigrationInterface削除をしないと走らない。-o --esmを付ければそのまま.jsESM で出るので、後処理は SPDX ヘッダー付与 (ステップ 3) だけで済む。
ステップ 3: SPDX ヘッダー付与
CLI 出力には SPDX ヘッダーが含まれない。必ず冒頭に追加する (CI の spdx ジョブが失敗するため)。
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
ステップ 4: up / down の整合確認
up()の各ステートメントに対し、down()で完全に巻き戻せること。- 列追加 (
ADD COLUMN) ↔ 列削除 (DROP COLUMN)、テーブル作成 ↔ テーブル削除、 FK 追加 ↔ FK 削除、インデックス作成 ↔ インデックス削除 を必ずペアで書く。 down()を空のまま残さない。本番ロールバック時に詰む。
インデックス追加時の注意 (CREATE INDEX CONCURRENTLY)
大規模テーブルへの CREATE INDEX は本番で長時間ロックする恐れがある。CONCURRENTLY で発行するときは migration 側にも対応が必要: PostgreSQL は CREATE INDEX CONCURRENTLY を transaction 内で実行できないため、migration class に以下を仕込んで TypeORM に「この migration は transaction を張らない」と指示する。
参照実装: packages/backend/migration/1745378064470-composite-note-index.js。
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
export class CompositeNoteIndex1745378064470 {
name = 'CompositeNoteIndex1745378064470';
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
async up(queryRunner) {
const concurrently = isConcurrentIndexMigrationEnabled;
if (concurrently) {
// CREATE INDEX CONCURRENTLY ...
} else {
// CREATE INDEX ...
}
}
async down(queryRunner) {
// 同様に環境変数で分岐
}
}
要点:
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;が必須。これがないとCREATE INDEX CONCURRENTLYが transaction 内で実行されてERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction blockで失敗する。- 環境変数
MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1がデフォルト OFF。OFF のときは普通のCREATE INDEX(transaction 内) で動く必要がある。up/down双方を環境変数で分岐させる。 ormconfig.jsのmigrationsTransactionModeは 環境変数で切り替わる:MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1のときだけ'each'(各 migration が個別 transaction)、未設定時は'all'(全 migration を 1 つの transaction でラップ) (ormconfig.js:19)。普段は'all'前提なので、CONCURRENTLY を使う migration を書く時だけこのフラグの存在を意識すれば良い。
関連エンティティとの一致
migration:generate を使った場合、エンティティ側の @Column / @Entity 修正と DB スキーマが食い違うとビルド全体がズレる。生成後に該当エンティティと SQL の対応を目視確認すること。
ステップ 5: 検証
ルートから実行:
# 未反映の差分が無いか (新規 migration が生成すべき DDL を取り逃していないか)
pnpm --filter backend check-migrations
# ローカル DB に適用
pnpm migrate
# ロールバック (down が壊れていないか)
pnpm revert
# 再適用 (順方向にもう一度通す)
pnpm migrate
check-migrations の実体は scripts/check_migrations_clean.js。TypeORM の dataSource.driver.createSchemaBuilder().log() で pending DDL を取得し、upQueries / downQueries のいずれかが残っていれば非ゼロ終了する。順序検査ではなく「エンティティと migration が同期しているか」の検査。
ステップ 6: 既存ファイル参照テンプレ
新規ファイルを書くときは、変更パターンが近い既存ファイルを 必ずひとつ開いて並べて書く。スタイルが激しくズレた PR は差し戻されやすい。
| パターン | 参照ファイル |
|---|---|
| インデックス追加 + 関数定義 | packages/backend/migration/1767169026317-birthday-index.js |
| 列追加のみ | packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js |
| テーブル新規作成 + FK | packages/backend/migration/1761569941833-add-channel-muting.js |
クラス命名規則は PascalCase 名 + 13 桁タイムスタンプ (例: class BirthdayIndex1767169026317)。name プロパティもクラス名と同一文字列にする。
ステップ 7: CHANGELOG (ユーザー影響がある場合)
スキーマ変更がユーザーに見える挙動を生む場合のみ、CHANGELOG.md の ## Unreleased → ### Server または ### General に 1 行追加する (AGENTS.md §CHANGELOG 参照)。内部リファクタや純粋なインデックス追加は不要。