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>
This commit is contained in:
Aaron Wood 2026-04-11 00:01:58 -04:00
parent 602dc3d098
commit 5dce775dce

View file

@ -11,7 +11,7 @@ export async function runMigrations(pool: Pool): Promise<void> {
CREATE TABLE IF NOT EXISTS _migrations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL UNIQUE,
run_at DATETIME DEFAULT NOW()
run_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
@ -28,16 +28,27 @@ export async function runMigrations(pool: Pool): Promise<void> {
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("--"));
for (const stmt of statements) {
await pool.execute(stmt);
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();
}
await pool.execute("INSERT INTO _migrations (filename) VALUES (?)", [file]);
console.log(`Migration applied: ${file}`);
}
}