fix: prevent database migration restart failures
- Move migrations table insert inside PostgreSQL transaction for atomicity - Add PostgreSQL error codes 42701 (duplicate column), 42710 (duplicate object), and 23505 (unique violation) to error handling - Make migrations 006 and 008 idempotent with column existence checks Fixes #107
This commit is contained in:
@@ -1,22 +1,27 @@
|
|||||||
exports.up = async function(knex) {
|
exports.up = async function(knex) {
|
||||||
// Add photo_counter column to photo_categories table
|
// Check if photo_counter column already exists to make migration idempotent
|
||||||
await knex.schema.alterTable('photo_categories', function(table) {
|
const hasPhotoCounter = await knex.schema.hasColumn('photo_categories', 'photo_counter');
|
||||||
table.integer('photo_counter').defaultTo(0).notNullable();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize counters based on existing photos
|
if (!hasPhotoCounter) {
|
||||||
const categories = await knex('photo_categories').select('id');
|
// Add photo_counter column to photo_categories table
|
||||||
|
await knex.schema.alterTable('photo_categories', function(table) {
|
||||||
|
table.integer('photo_counter').defaultTo(0).notNullable();
|
||||||
|
});
|
||||||
|
|
||||||
for (const category of categories) {
|
// Initialize counters based on existing photos
|
||||||
const photoCount = await knex('photos')
|
const categories = await knex('photo_categories').select('id');
|
||||||
.where('category_id', category.id)
|
|
||||||
.count('id as count')
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (photoCount && photoCount.count > 0) {
|
for (const category of categories) {
|
||||||
await knex('photo_categories')
|
const photoCount = await knex('photos')
|
||||||
.where('id', category.id)
|
.where('category_id', category.id)
|
||||||
.update({ photo_counter: photoCount.count });
|
.count('id as count')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (photoCount && photoCount.count > 0) {
|
||||||
|
await knex('photo_categories')
|
||||||
|
.where('id', category.id)
|
||||||
|
.update({ photo_counter: photoCount.count });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
exports.up = async function(knex) {
|
exports.up = async function(knex) {
|
||||||
// Add language-specific columns to email_templates
|
// Check which columns already exist to make migration idempotent
|
||||||
await knex.schema.alterTable('email_templates', function(table) {
|
const hasSubjectEn = await knex.schema.hasColumn('email_templates', 'subject_en');
|
||||||
// Add English versions (rename existing columns for consistency)
|
const hasSubjectDe = await knex.schema.hasColumn('email_templates', 'subject_de');
|
||||||
table.renameColumn('subject', 'subject_en');
|
const hasSubjectOriginal = await knex.schema.hasColumn('email_templates', 'subject');
|
||||||
table.renameColumn('body_html', 'body_html_en');
|
|
||||||
table.renameColumn('body_text', 'body_text_en');
|
|
||||||
|
|
||||||
// Add German versions
|
// Only rename columns if they haven't been renamed yet
|
||||||
table.string('subject_de');
|
if (hasSubjectOriginal && !hasSubjectEn) {
|
||||||
table.text('body_html_de');
|
await knex.schema.alterTable('email_templates', function(table) {
|
||||||
table.text('body_text_de');
|
table.renameColumn('subject', 'subject_en');
|
||||||
});
|
table.renameColumn('body_html', 'body_html_en');
|
||||||
|
table.renameColumn('body_text', 'body_text_en');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Copy existing values to German columns as defaults
|
// Only add German columns if they don't exist
|
||||||
await knex('email_templates').update({
|
if (!hasSubjectDe) {
|
||||||
subject_de: knex.raw('subject_en'),
|
await knex.schema.alterTable('email_templates', function(table) {
|
||||||
body_html_de: knex.raw('body_html_en'),
|
table.string('subject_de');
|
||||||
body_text_de: knex.raw('body_text_en')
|
table.text('body_html_de');
|
||||||
});
|
table.text('body_text_de');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy existing values to German columns as defaults
|
||||||
|
await knex('email_templates').update({
|
||||||
|
subject_de: knex.raw('subject_en'),
|
||||||
|
body_html_de: knex.raw('body_html_en'),
|
||||||
|
body_text_de: knex.raw('body_text_en')
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = async function(knex) {
|
exports.down = async function(knex) {
|
||||||
|
|||||||
@@ -72,21 +72,32 @@ async function runMigrationSafely(filepath) {
|
|||||||
console.log(`Running migration: ${filepath}`);
|
console.log(`Running migration: ${filepath}`);
|
||||||
|
|
||||||
// Run migration in a transaction if possible
|
// Run migration in a transaction if possible
|
||||||
|
// IMPORTANT: Include the migrations table insert INSIDE the transaction
|
||||||
|
// to ensure atomicity between schema changes and tracking
|
||||||
if (db.client.config.client === 'pg') {
|
if (db.client.config.client === 'pg') {
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await migration.up(trx);
|
await migration.up(trx);
|
||||||
|
// Insert migration record inside transaction for atomicity
|
||||||
|
await trx('migrations').insert({ filename });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await migration.up(db);
|
await migration.up(db);
|
||||||
|
await db('migrations').insert({ filename });
|
||||||
}
|
}
|
||||||
|
|
||||||
await db('migrations').insert({ filename });
|
|
||||||
console.log(`Migration ${filepath} completed successfully`);
|
console.log(`Migration ${filepath} completed successfully`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Check if error is because schema already exists
|
// Check if error is because schema already exists
|
||||||
if (error.code === '42P07' || // PostgreSQL: relation already exists
|
// PostgreSQL error codes:
|
||||||
error.code === 'SQLITE_ERROR' && error.message.includes('already exists')) {
|
// - 42P07: duplicate_table (relation already exists)
|
||||||
|
// - 42701: duplicate_column (column already exists)
|
||||||
|
// - 42710: duplicate_object (constraint, index, etc. already exists)
|
||||||
|
// - 23505: unique_violation (migration record already exists)
|
||||||
|
const schemaExistsErrors = ['42P07', '42701', '42710', '23505'];
|
||||||
|
const isSQLiteAlreadyExists = error.code === 'SQLITE_ERROR' && error.message.includes('already exists');
|
||||||
|
|
||||||
|
if (schemaExistsErrors.includes(error.code) || isSQLiteAlreadyExists) {
|
||||||
console.log(`Migration ${filepath} - schema already exists, marking as applied`);
|
console.log(`Migration ${filepath} - schema already exists, marking as applied`);
|
||||||
await markMigrationAsApplied(path.basename(filepath));
|
await markMigrationAsApplied(path.basename(filepath));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -29,8 +29,19 @@ async function runMigration(filepath) {
|
|||||||
|
|
||||||
if (migration.up) {
|
if (migration.up) {
|
||||||
console.log(`Running migration: ${filepath}`);
|
console.log(`Running migration: ${filepath}`);
|
||||||
await migration.up(db);
|
|
||||||
await db('migrations').insert({ filename });
|
// Run migration in a transaction if PostgreSQL to ensure atomicity
|
||||||
|
// between schema changes and migration tracking
|
||||||
|
if (db.client.config.client === 'pg') {
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await migration.up(trx);
|
||||||
|
await trx('migrations').insert({ filename });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await migration.up(db);
|
||||||
|
await db('migrations').insert({ filename });
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Migration ${filepath} completed`);
|
console.log(`Migration ${filepath} completed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user