From 1ecf82a5c29baccae68adeda04994b0e8864ab6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Mar 2026 14:00:52 +0100 Subject: [PATCH] catalog-backend: wrap MySQL/SQLite migration branches in explicit transactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search FK migration uses transaction: false for PostgreSQL's benefit, but this left MySQL and SQLite branches non-atomic. A failure between dropForeign and the new addForeign would leave the table with no FK constraint. Wrap those branches in explicit knex.transaction() calls. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- ...20260214000000_search_fk_final_entities.js | 101 ++++++++++-------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js index 6c2136de4d..4f3885aa17 100644 --- a/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js +++ b/plugins/catalog-backend/migrations/20260214000000_search_fk_final_entities.js @@ -90,34 +90,41 @@ exports.up = async function up(knex) { await knex('search').whereIn('entity_id', ids).delete(); } - // Drop old FK and add new one. MySQL does not support NOT VALID, but - // the table is already clean so validation is fast. - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('final_entities') - .onDelete('CASCADE'); + // Drop old FK and add new one inside an explicit transaction, since the + // global transaction wrapper is disabled for this migration. MySQL does + // not support NOT VALID, but the table is already clean so validation + // is fast. + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('final_entities') + .onDelete('CASCADE'); + }); }); } else { - // SQLite: simple approach, locking is not a concern - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); + // SQLite: wrap in an explicit transaction since the global transaction + // wrapper is disabled for this migration. + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); - await knex('search') - .whereNotIn('entity_id', knex('final_entities').select('entity_id')) - .delete(); + await trx('search') + .whereNotIn('entity_id', trx('final_entities').select('entity_id')) + .delete(); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('final_entities') - .onDelete('CASCADE'); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('final_entities') + .onDelete('CASCADE'); + }); }); } }; @@ -177,31 +184,35 @@ exports.down = async function down(knex) { await knex('search').whereIn('entity_id', ids).delete(); } - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('refresh_state') - .onDelete('CASCADE'); + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE'); + }); }); } else { - await knex.schema.alterTable('search', table => { - table.dropForeign(['entity_id']); - }); + await knex.transaction(async trx => { + await trx.schema.alterTable('search', table => { + table.dropForeign(['entity_id']); + }); - await knex('search') - .whereNotIn('entity_id', knex('refresh_state').select('entity_id')) - .delete(); + await trx('search') + .whereNotIn('entity_id', trx('refresh_state').select('entity_id')) + .delete(); - await knex.schema.alterTable('search', table => { - table - .foreign('entity_id') - .references('entity_id') - .inTable('refresh_state') - .onDelete('CASCADE'); + await trx.schema.alterTable('search', table => { + table + .foreign('entity_id') + .references('entity_id') + .inTable('refresh_state') + .onDelete('CASCADE'); + }); }); } };