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

SQL プラグインの Migration と add_migrations() でテーブル定義を版ごとに管理する。版の上げ方、適用済みの SQL を書き換えてはいけない理由、失敗したときの DB の状態と扱い方を示す。

データ保存 対象: Tauri 2.x 更新日: 読了目安: 約9分 db-003
目次
  1. 前提条件
  2. 1. バックエンドから実装する (Rust)
  3. 版の一覧を登録する
  4. 版を上げるときの決まり
  5. 起動時に適用する (preload)
  6. 2. フロントエンドから呼び出す (TypeScript)
  7. 動作確認
  8. よくあるエラーと対処法
  9. 注意点
  10. 関連レシピ

アプリの更新で列やテーブルを足したくなったとき、利用者の手元には旧版のテーブルとデータがすでにあります。SQL プラグインでは、Rust 側に「版番号と SQL」の一覧を登録しておくと、DB を開いたときに未適用の版だけを順に実行し、適用した版を DB 内の _sqlx_migrations テーブルに記録します。登録の仕方、版の上げ方、失敗したときに DB がどうなるかと、その扱い方を説明します。

前提条件

SQL プラグインの導入(npm run tauri add sql と sqlite 機能)は SQLite データベース操作 (CRUD) のとおりです。マイグレーションは Database.load() の中で実行されるので、そのための追加の権限はありません(load は sql:default に含まれます)。アプリがテーブルに書き込むなら sql:allow-execute も足します。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "sql:default",
    "sql:allow-execute"
  ]
}

1. バックエンドから実装する (Rust)

版の一覧を登録する

version は版番号、description は記録用の名前、sql はその版で実行する SQL で、; で区切って複数の文を書けます。add_migrations() の第 1 引数は、JS の Database.load() に渡す文字列と完全に同じにします。SQLite の ALTER TABLE でできるのは列の追加・名前の変更・削除などに限られ、型や制約を変えるには v3 のようにテーブルを作り直します。

use tauri_plugin_sql::{Migration, MigrationKind};

const DB_URL: &str = "sqlite:app.db"; // JS の Database.load() と同じ文字列

fn migrations() -> Vec<Migration> {
    vec![
        // v1: 最初のリリース
        Migration {
            version: 1,
            description: "create_notes",
            sql: "CREATE TABLE notes (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    body TEXT NOT NULL,
                    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
                  );",
            kind: MigrationKind::Up,
        },
        // v2: 列の追加。既存の行には DEFAULT の値が入る
        Migration {
            version: 2,
            description: "add_pinned",
            sql: "ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
                  CREATE INDEX idx_notes_pinned ON notes (pinned);",
            kind: MigrationKind::Up,
        },
        // v3: body の NOT NULL を外し、title を足す(制約の変更は作り直し)
        Migration {
            version: 3,
            description: "rebuild_notes",
            sql: "CREATE TABLE notes_new (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    title TEXT NOT NULL DEFAULT '',
                    body TEXT,
                    pinned INTEGER NOT NULL DEFAULT 0,
                    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
                  );
                  INSERT INTO notes_new (id, body, pinned, created_at)
                    SELECT id, body, pinned, created_at FROM notes;
                  DROP TABLE notes;
                  ALTER TABLE notes_new RENAME TO notes;
                  CREATE INDEX idx_notes_pinned ON notes (pinned);",
            kind: MigrationKind::Up,
        },
    ]
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(
            tauri_plugin_sql::Builder::default()
                .add_migrations(DB_URL, migrations())
                .build(),
        )
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

v1 のまま使っていた利用者が v3 のアプリを入れると、次に DB を開いたときに v2 と v3 が続けて実行されます。新規の利用者なら v1 から v3 まで実行されます。

版を上げるときの決まり

  • 新しい版は一覧の末尾に、前より大きい番号で足す。 番号は連番でなくても構いません。
  • 適用済みの版の SQL は 1 文字も変えない。 DB を開くときに記録と照合され、違うと「migration 2 was previously applied but has been modified」で開けなくなります。空白や改行コード(CRLF / LF)の違いも変更とみなされます。description は照合されません。
  • 適用済みの版を一覧から消さない。 消すと「migration 2 was previously applied but is missing in the resolved migrations」になります。新しい版で開いた DB を古い版のアプリで開いたときも同じエラーです。
  • 元に戻す処理も新しい版として足す。 MigrationKind::Down の版を登録しても、自動では実行されません。

長い SQL は include_str!("../migrations/0003_rebuild_notes.sql") でファイルに分けられます。その場合、Git の改行コード変換で照合に失敗しないよう、.gitattributes に *.sql text eol=lf を書いておきます。

起動時に適用する (preload)

tauri.conf.json に書いた DB は、ウィンドウが開く前に開かれてマイグレーションも実行されます。Rust のコマンドから最初に DB を使う場合に便利ですが、失敗するとプラグインの初期化エラー(PluginInitialization と「while executing migration 3: …」を含むメッセージ)で起動時に終了し、画面で知らせることができません。

{
  "plugins": {
    "sql": {
      "preload": ["sqlite:app.db"]
    }
  }
}

2. フロントエンドから呼び出す (TypeScript)

SQLite では版ごとに 1 つのトランザクションで実行されます。ある版が失敗すると、その版の変更はすべて取り消され、前の版までは適用済みのまま残ります。Database.load() はエラーで終わり、次に起動したときは失敗した版から実行し直します。SQL を直した更新版を配れば、続きから進みます。

注意したいのは、失敗した後に同じ起動中で load() をやり直すと、マイグレーションを実行しないまま古い定義の DB が開けてしまうことです。失敗は画面で知らせ、再試行しません。

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

type Applied = { version: number; description: string; installed_on: string };

let dbPromise: Promise<Database> | null = null;

// 未適用の版は load() の中で順に実行される
export function openDb(): Promise<Database> {
  dbPromise ??= Database.load('sqlite:app.db').catch((e: unknown) => {
    // 例: while executing migration 4: error returned from database: (code: 1) duplicate column name: pinned
    const p = document.createElement('p');
    p.textContent = `データベースを更新できませんでした: ${String(e)}`;
    document.body.replaceChildren(p);
    throw e; // 失敗した Promise を残し、同じ起動中に load() し直さない
  });
  return dbPromise;
}

// 適用済みの版(問い合わせ対応やログ用)
export async function appliedVersions(): Promise<Applied[]> {
  const db = await openDb();
  return db.select<Applied[]>(
    'SELECT version, description, installed_on FROM _sqlx_migrations ORDER BY version',
  );
}

動作確認

npm run tauri dev で起動して appliedVersions() の結果を console.log すると、次のように出ます(installed_on は UTC)。

[{ version: 1, description: "create_notes", installed_on: "2026-09-12 02:15:07" }, { version: 2, description: "add_pinned", ... }, { version: 3, description: "rebuild_notes", ... }]

失敗の扱いも試しておきます。v4 として既にある列を足す ALTER TABLE notes ADD COLUMN pinned INTEGER; を登録して起動し直すと、画面に「while executing migration 4: error returned from database: (code: 1) duplicate column name: pinned」が出て、記録は v3 までのままです。SQL を直して起動し直すと v4 が適用されます。開発中に最初からやり直すときは、アプリを閉じて設定フォルダーの app.db(あれば -wal と -shm も)を消します。

よくあるエラーと対処法

  • 「migration 2 was previously applied but has been modified」: 適用済みの SQL を変えました。開発中なら DB を消して作り直し、配布後なら SQL を元に戻して変更を新しい版で足します。
  • 「migration 3 was previously applied but is missing in the resolved migrations」: 版を一覧から消したか、新しい版で開いた DB を古いアプリで開きました。古いアプリへ戻す場合は DB も戻す必要があります。
  • マイグレーションが実行されず「no such table: notes」: add_migrations() と Database.load() の文字列が違います(sqlite:app.db と sqlite:./app.db など)。一致しないとエラーも出ずに素通りします。
  • 「duplicate column name」「no such column」などで load() が失敗する: その版の SQL の誤りです。その版は取り消されているので、直して起動し直せば続きから進みます。

注意点

  • 外部キーで参照されている表を作り直さない: このプラグインの接続では外部キーが有効で、マイグレーションの中から無効にすることもできません。v3 のような作り直しで DROP TABLE すると、ON DELETE CASCADE なら参照している子の行まで消え、付けていなければ外部キー違反でその版が失敗します。
  • 実行中に execute() で ALTER TABLE しない: 定義の変更は load() 時のマイグレーションで行います。実行中に列を変えると、ほかの接続で実行した SELECT * が空の結果を返すことがあります。列名を並べて SELECT する習慣も付けておきます。
  • 初期データ: 最初から大量のデータが入った DB を配るなら、DB ファイルを配布物に同梱 してから設定フォルダーへ複製する方法もあります(その DB も同じマイグレーションで作っておきます)。値を埋め込む SQL は プリペアドステートメント で書きます。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する