データベースのマイグレーション(版管理)を行う

Recipe ID: db-003

アプリのバージョンアップに伴い、データベース定義(カラム追加など)を変更するためのマイグレーションロジックの管理パターンを紹介します。

前提条件

Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

{
  "permissions": [
    ...,
    "sql:default"
  ]
}

簡易マイグレーション関数の実装

専用のテーブル作成やスキーマ変更を順番に適用する関数を作成します。

import Database from '@tauri-apps/plugin-sql';

const MIGRATIONS = [
  // Version 1: 初期テーブル作成
  `CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);`,
  
  // Version 2: users テーブル追加
  `CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);`,
  
  // Version 3: email カラム追加
  `ALTER TABLE users ADD COLUMN email TEXT;`
];

async function migrate(db: Database) {
  // 現在のバージョンを取得(PRAGMA user_version は SQLite 固有のバージョン管理領域)
  const result = await db.select<[{user_version: number}]>('PRAGMA user_version');
  let currentVersion = result[0].user_version;

  console.log(`Current DB Version: ${currentVersion}`);

  // 未適用のマイグレーションを実行
  for (let i = currentVersion; i < MIGRATIONS.length; i++) {
    console.log(`Applying migration version ${i + 1}...`);
    try {
      await db.execute('BEGIN TRANSACTION');
      await db.execute(MIGRATIONS[i]);
      // バージョン番号更新
      await db.execute(`PRAGMA user_version = ${i + 1}`);
      await db.execute('COMMIT');
    } catch (e) {
      await db.execute('ROLLBACK');
      console.error(`Migration failed at version ${i + 1}`, e);
      throw e;
    }
  }
}

// 実行例: DB接続とマイグレーションをまとめて実行
export async function initDatabase() {
  try {
    // データベースに接続
    const db = await Database.load('sqlite:myapp.db');
    
    // マイグレーション実行
    await migrate(db);
    
    console.log('Database initialized successfully');
    return db;
  } catch (error) {
    console.error('Database initialization failed:', error);
    throw error;
  }
}

この migrate 関数を、Database.load 直後に呼び出すことで、常に最新のスキーマ状態を保つことができます。
SQL ファイルを分割して管理したい場合は、Vite の import.meta.glob (?raw) 機能を使って文字列として読み込むなどの工夫が可能です。