feat: minimal stabilization of migration 004 with atomic execution

- Single SQL source: Define MIG_004_SQL constant to eliminate duplicate SQL definitions
- Atomic execution: Add BEGIN IMMEDIATE/COMMIT/ROLLBACK around migration execution
- Name-only check: Skip migrations already recorded in migrations table
- Guarded operations: Replace table-wide cleanups with conditional UPDATE/DELETE

Changes:
- migration.ts: Extract migration 004 SQL into MIG_004_SQL constant
- migration.ts: Use guarded DELETE/UPDATE to prevent accidental data loss
- migrationService.ts: Wrap migration execution in explicit transactions
- migrationService.ts: Reorder checks to prioritize name-only skipping

Benefits:
- Prevents partial migration failures from corrupting database state
- Eliminates SQL duplication and maintenance overhead
- Maintains existing APIs and logging behavior
- Reduces risk of data loss during migration execution

Test results: All migration tests passing, ID generation working correctly
This commit is contained in:
Matthew Raymer
2025-09-17 06:52:43 +00:00
parent 0fae8bbda6
commit 2f495f6767
2 changed files with 92 additions and 74 deletions

View File

@@ -649,15 +649,15 @@ export async function runMigrations<T>(
// Check 1: Is it recorded as applied in migrations table?
const isRecordedAsApplied = appliedMigrations.has(migration.name);
// Check 2: Does the schema already exist in the database?
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
// Skip if already recorded as applied
// Skip if already recorded as applied (name-only check)
if (isRecordedAsApplied) {
skippedCount++;
continue;
}
// Check 2: Does the schema already exist in the database?
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
// Handle case where schema exists but isn't recorded
if (isSchemaPresent) {
try {
@@ -687,46 +687,58 @@ export async function runMigrations<T>(
}
try {
// Execute the migration SQL as single atomic operation
// Execute the migration SQL as single atomic operation with transaction
if (isDevelopment) {
migrationLog(`🔧 [Migration] Executing SQL for: ${migration.name}`);
migrationLog(`🔧 [Migration] SQL content: ${migration.sql}`);
}
const execResult = await sqlExec(migration.sql);
// Begin transaction for atomic execution
await sqlExec("BEGIN IMMEDIATE");
try {
const execResult = await sqlExec(migration.sql);
if (isDevelopment) {
migrationLog(
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
);
}
// Validate the migration was applied correctly (only in development)
if (isDevelopment) {
const validation = await validateMigrationApplication(
migration,
sqlQuery,
);
if (!validation.isValid) {
logger.warn(
`⚠️ [Migration] Validation failed for ${migration.name}:`,
validation.errors,
if (isDevelopment) {
migrationLog(
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
);
}
}
// Record that the migration was applied
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
// Validate the migration was applied correctly (only in development)
if (isDevelopment) {
const validation = await validateMigrationApplication(
migration,
sqlQuery,
);
if (!validation.isValid) {
logger.warn(
`⚠️ [Migration] Validation failed for ${migration.name}:`,
validation.errors,
);
}
}
// Only log success in development
if (isDevelopment) {
migrationLog(
`🎉 [Migration] Successfully applied: ${migration.name}`,
);
// Record that the migration was applied
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
// Commit transaction
await sqlExec("COMMIT");
// Only log success in development
if (isDevelopment) {
migrationLog(
`🎉 [Migration] Successfully applied: ${migration.name}`,
);
}
appliedCount++;
} catch (error) {
// Rollback transaction on any error
await sqlExec("ROLLBACK");
throw error;
}
appliedCount++;
} catch (error) {
logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);