【Prisma】Migrationスクリプトを書いてみた

こんにちは、フリーランスエンジニアの太田雅昭です。

PrismaのMigrate

Prismaでは、通常CLIでMigrationを行います。しかしアプリ実行時に行いたくなる時もあります。今回、アプリ内で実行できるスクリプトを書いてみました。

Migrateするコード

以下のようにして使用できるコードを作成しました。

const migrationsPath = 'xxx'; // migrationsディレクトリのパス
const prisma = new PrismaClient()
await migrate({ prisma, migrationsPath })

なお、prisma.schemaで指定したdbは、prisma migrate devで最新になるため、そもそもコードで追う必要がありません。そのため、prisma.schemaで指定したのとは違うdbを指定することを前提にしています。ユーザー個別のdbといった場合ですね。

以下がコードです。Prisma6.1.0で動作確認しています。

なお厳密な実装ではないので、あしからずご了承くださいますと幸いです。

import { PrismaClient } from '@prisma/client'
import fs from 'fs-extra'
import path from 'path'

interface Migration {
  id: string
  checksum: string
  finished_at: Date
  migration_name: string
  logs: string | null
  rolled_back_at: Date | null
  started_at: Date
  applied_steps_count: bigint
}

type Params = {
  prisma: PrismaClient
  migrationsPath: string
}

export async function migrate(params: Params) {
  const { prisma, migrationsPath } = params

  console.log('Migrating...')
  await createMigrationsTable(prisma)
  const appliedNames = await getAppliedNames(prisma)
  const pendingMigrations = getMigrations(migrationsPath).filter(
    (m) => !appliedNames.includes(m.name)
  )
  for (const { name, script } of pendingMigrations) {
    await applyMigration(prisma, name, script)
  }
  console.log('Migration done')
}

async function applyMigration(prisma: PrismaClient, name: string, script: string) {
  await prisma.$transaction(async (tx) => {
    for (const sql of parseScript(script)) {
      await tx.$executeRawUnsafe(sql)
    }
    await tx.$executeRaw`
      insert into _prisma_migrations(id, migration_name, checksum, started_at, finished_at, applied_steps_count) 
      values (${crypto.randomUUID()}, ${name}, '', ${new Date()}, ${new Date()}, 1)
    `
    console.log(`Applied: ${name}`)
  })
}

function getMigrations(migrationsPath: string) {
  const dirs = fs
    .readdirSync(migrationsPath, { withFileTypes: true })
    .filter((dirent) => dirent.isDirectory())
    .map((dirent) => dirent.name)
    .sort((a, b) => a.localeCompare(b))
  return dirs.map((dir) => ({
    name: dir,
    script: fs.readFileSync(path.join(migrationsPath, dir, 'migration.sql'), 'utf-8')
  }))
}

function parseScript(script: string): string[] {
  return script.split(';').filter((q) => q.trim() !== '')
}

async function createMigrationsTable(prisma: PrismaClient) {
  await prisma.$executeRaw`
    CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
      "id"                    TEXT PRIMARY KEY NOT NULL,
      "checksum"              TEXT NOT NULL,
      "finished_at"           DATETIME,
      "migration_name"        TEXT NOT NULL,
      "logs"                  TEXT,
      "rolled_back_at"        DATETIME,
      "started_at"            DATETIME NOT NULL DEFAULT current_timestamp,
      "applied_steps_count"   INTEGER UNSIGNED NOT NULL DEFAULT 0
  )`
}

async function getAppliedNames(prisma: PrismaClient) {
  const appliedMigrations = (await prisma.$queryRaw`
    select * from _prisma_migrations order by id
  `) as Migration[]
  return appliedMigrations.map((m) => m.migration_name)
}