darkwatch/server/src/migrate.ts
Aaron Wood 5dce775dce fix: add transaction wrapping and SQL splitter documentation in migrate.ts
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>
2026-04-11 00:01:58 -04:00

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();
}
}
}