Each migration file now runs in a transaction - if any statement fails, the entire file is rolled back and no _migrations record is written. Also documents the naive semicolon-split constraint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
import type { Pool } from "mysql2/promise";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const MIGRATIONS_DIR = path.join(__dirname, "..", "migrations");
|
|
|
|
export async function runMigrations(pool: Pool): Promise<void> {
|
|
await pool.execute(`
|
|
CREATE TABLE IF NOT EXISTS _migrations (
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
filename VARCHAR(255) NOT NULL UNIQUE,
|
|
run_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
const files = fs
|
|
.readdirSync(MIGRATIONS_DIR)
|
|
.filter((f) => f.endsWith(".sql"))
|
|
.sort();
|
|
|
|
for (const file of files) {
|
|
const [rows] = await pool.execute<import("mysql2").RowDataPacket[]>(
|
|
"SELECT id FROM _migrations WHERE filename = ?",
|
|
[file]
|
|
);
|
|
if (rows.length > 0) continue;
|
|
|
|
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), "utf-8");
|
|
// NOTE: This splitter is intentionally naive — migration files must not
|
|
// contain semicolons inside string literals or block comments.
|
|
const statements = sql
|
|
.split(";")
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0 && !s.startsWith("--"));
|
|
|
|
const conn = await pool.getConnection();
|
|
await conn.beginTransaction();
|
|
try {
|
|
for (const stmt of statements) {
|
|
await conn.execute(stmt);
|
|
}
|
|
await conn.execute("INSERT INTO _migrations (filename) VALUES (?)", [file]);
|
|
await conn.commit();
|
|
console.log(`Migration applied: ${file}`);
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}
|
|
}
|