From 5dce775dce7cd340e10fc53fef139670a22cefbc Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Sat, 11 Apr 2026 00:01:58 -0400 Subject: [PATCH] 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 --- server/src/migrate.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/server/src/migrate.ts b/server/src/migrate.ts index ed93837..862adfd 100644 --- a/server/src/migrate.ts +++ b/server/src/migrate.ts @@ -11,7 +11,7 @@ export async function runMigrations(pool: Pool): Promise { 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 { 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}`); } }