69 lines
1.9 KiB
TypeScript
69 lines
1.9 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { SqliteDatabase } from './sqlite';
|
|
|
|
/**
|
|
* SQLファイルベースのMigration機構。
|
|
* - Migrationファイルをファイル名順に実行する
|
|
* - 実行済みファイルは再実行しない
|
|
* - 1ファイルごとにトランザクションを張り、失敗時はロールバックする
|
|
*/
|
|
export class Migrator {
|
|
constructor(
|
|
private readonly db: SqliteDatabase,
|
|
private readonly migrationsDir: string
|
|
) {}
|
|
|
|
/**
|
|
* 未適用のMigrationをファイル名順に実行する
|
|
*/
|
|
migrate(): void {
|
|
this.ensureMigrationsTable();
|
|
|
|
const applied = this.db.query<{ filename: string }>(
|
|
'SELECT filename FROM schema_migrations'
|
|
);
|
|
const appliedSet = new Set(applied.map((x) => x.filename));
|
|
|
|
const files = fs
|
|
.readdirSync(this.migrationsDir)
|
|
.filter((file) => file.endsWith('.sql'))
|
|
.sort();
|
|
|
|
for (const file of files) {
|
|
if (appliedSet.has(file)) continue;
|
|
|
|
const fullPath = path.join(this.migrationsDir, file);
|
|
const sql = fs.readFileSync(fullPath, 'utf-8');
|
|
|
|
this.db.transaction(() => {
|
|
this.db.execute(sql);
|
|
this.db.execute(
|
|
`INSERT INTO schema_migrations (filename, applied_at) VALUES (@filename, @appliedAt)`,
|
|
{ filename: file, appliedAt: new Date().toISOString() }
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 適用済みMigration一覧を取得する
|
|
*/
|
|
getAppliedMigrations(): { filename: string; applied_at: string }[] {
|
|
this.ensureMigrationsTable();
|
|
return this.db.query<{ filename: string; applied_at: string }>(
|
|
'SELECT filename, applied_at FROM schema_migrations ORDER BY filename'
|
|
);
|
|
}
|
|
|
|
private ensureMigrationsTable(): void {
|
|
this.db.execute(`
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
filename TEXT NOT NULL UNIQUE,
|
|
applied_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
}
|
|
}
|