From 0044cbc1fcc3756acd72b9cf34d67e0b2703ae04 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Sat, 24 Oct 2026 20:33:20 +0100 Subject: [PATCH] Fix v2.19.1 migration schema connection ownership --- src/db/migrations.js | 19 +++++++--------- test/timetable-migrations.test.js | 38 +++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/db/migrations.js b/src/db/migrations.js index b14fd5a..bbb6e93 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -830,14 +830,13 @@ const VERSIONED_MIGRATIONS = [ version: '2.19.1', label: 'v2.19.1 rebuild managed font references', run: async function (pool) { - const connection = await pool.getConnection(); try { - await connection.beginTransaction(); - await connection.query('DELETE FROM c_content_references WHERE reference_type = ?', ['font-family']); + await pool.beginTransaction(); + await pool.query('DELETE FROM c_content_references WHERE reference_type = ?', ['font-family']); let lastSlideId = 0; while (true) { - const [slides] = await connection.query( + const [slides] = await pool.query( 'SELECT id, content_json FROM c_slides WHERE id > ? AND content_json IS NOT NULL ORDER BY id ASC LIMIT 500', [lastSlideId] ); @@ -847,13 +846,13 @@ const VERSIONED_MIGRATIONS = [ references.push.apply(references, collectContentReferences('slide', slide.id, parseJsonSafe(slide.content_json) || {}) .filter(function (reference) { return reference.reference_type === 'font-family'; })); }); - await insertContentReferences(connection, references); + await insertContentReferences(pool, references); lastSlideId = Number(slides[slides.length - 1].id); } let lastRegionId = 0; while (true) { - const [regions] = await connection.query( + const [regions] = await pool.query( 'SELECT id, template_id, region_key, default_content_json FROM c_template_regions WHERE id > ? AND default_content_json IS NOT NULL ORDER BY id ASC LIMIT 500', [lastRegionId] ); @@ -865,16 +864,14 @@ const VERSIONED_MIGRATIONS = [ references.push.apply(references, collectContentReferences('template', region.template_id, regionContent) .filter(function (reference) { return reference.reference_type === 'font-family'; })); }); - await insertContentReferences(connection, references); + await insertContentReferences(pool, references); lastRegionId = Number(regions[regions.length - 1].id); } - await connection.commit(); + await pool.commit(); } catch (error) { - await connection.rollback(); + await pool.rollback(); throw error; - } finally { - connection.release(); } } } diff --git a/test/timetable-migrations.test.js b/test/timetable-migrations.test.js index ecfc613..5606d8f 100644 --- a/test/timetable-migrations.test.js +++ b/test/timetable-migrations.test.js @@ -9,11 +9,9 @@ function createPool(responses) { const queries = []; return { queries, - async getConnection() { return this; }, async beginTransaction() { queries.push(['START TRANSACTION']); }, async commit() { queries.push(['COMMIT']); }, async rollback() { queries.push(['ROLLBACK']); }, - release() { queries.push(['RELEASE CONNECTION']); }, async query(sql, params) { queries.push([sql, params]); const text = String(sql); @@ -244,11 +242,11 @@ test('2.19.1 migration rebuilds only managed font references for all content in assert.ok(pool.queries.some(([sql, params]) => sql.startsWith('SELECT id, content_json') && params[0] === 118)); assert.ok(pool.queries.some(([sql, params]) => sql.startsWith('SELECT id, template_id, region_key') && params[0] === 900)); assert.ok(!pool.queries.some(([sql]) => /^(UPDATE|ALTER|TRUNCATE)/.test(sql))); - assert.deepEqual(pool.queries.slice(-2), [['COMMIT'], ['RELEASE CONNECTION']]); + assert.deepEqual(pool.queries.slice(-1), [['COMMIT']]); assert.deepEqual(await getPendingMigrations(pool, { currentVersion: '2.19.1' }), []); }); -test('2.19.1 font repair rolls back and releases its connection on failure', async () => { +test('2.19.1 font repair rolls back without releasing the caller-owned connection on failure', async () => { const pool = createPool([ { match: sql => sql.includes('information_schema.COLUMNS'), result: [[{ column_count: 1 }]] }, { match: sql => sql.includes('information_schema.TABLES'), result: [[{ table_count: 0 }]] } @@ -261,6 +259,36 @@ test('2.19.1 font repair rolls back and releases its connection on failure', asy }; await assert.rejects(migration.run(pool), /read failed/); - assert.deepEqual(pool.queries.slice(-2), [['ROLLBACK'], ['RELEASE CONNECTION']]); + assert.deepEqual(pool.queries.slice(-1), [['ROLLBACK']]); assert.ok(!pool.queries.some(([sql]) => sql === 'COMMIT')); +}); + +test('schema startup runs the 2.19.1 repair on its existing locked connection', async () => { + const { ensureSchema } = require('../src/db'); + const connection = createPool([ + { match: sql => sql.includes('SELECT GET_LOCK'), result: [[{ lock_acquired: 1 }]] }, + { match: (sql, params) => sql.includes('information_schema.TABLES') && params[0] === 'o_app_state', result: [[{ table_count: 1 }]] }, + { match: sql => sql.startsWith('SELECT state_value'), result: [[{ state_value: '2.19.0' }]] }, + { match: sql => sql.includes('information_schema.COLUMNS'), result: [[{ column_count: 1 }]] }, + { match: sql => sql.includes('information_schema.TABLES'), result: [[{ table_count: 0 }]] } + ]); + connection.release = function () { connection.queries.push(['RELEASE CONNECTION']); }; + let acquisitions = 0; + + await ensureSchema({ + async getConnection() { + acquisitions += 1; + return connection; + } + }); + + assert.equal(acquisitions, 1); + const commitIndex = connection.queries.findIndex(([sql]) => sql === 'COMMIT'); + const versionIndex = connection.queries.findIndex(([sql, params]) => sql.startsWith('UPDATE o_app_state') && params[0] === '2.19.1'); + assert.ok(commitIndex > 0); + assert.ok(versionIndex > commitIndex); + assert.deepEqual(connection.queries.slice(-2), [ + ['SELECT RELEASE_LOCK(?)', ['pulse_signage_schema_lock']], + ['RELEASE CONNECTION'] + ]); }); \ No newline at end of file