fix(MySQL Node): Support "Continue on Error" for connection-related errors (#25032)

This commit is contained in:
RomanDavydchuk 2026-01-29 17:30:51 +02:00 committed by GitHub
parent daba1e2846
commit f3e2930f0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 6 deletions

View File

@ -101,9 +101,7 @@ describe('MySQL Integration - NODE-4174', () => {
expect(result[0][0].json).toHaveProperty('message');
});
// NODE-4174: createPool() is outside try-catch, so connection errors bypass continueOnFail.
// Remove .fails when fixed.
test.fails('bug: connection error should return error item with continueOnFail', async () => {
test('connection error should return error item with continueOnFail', async () => {
const params = {
resource: 'database',
operation: 'executeQuery',

View File

@ -207,4 +207,40 @@ describe('Test MySql V2, runQueries', () => {
expect(connectionReleaseSpy).toBeCalledTimes(1);
});
it('should return error item with continueOnFail = true for connection error', async () => {
const nodeOptions: IDataObject = { queryBatching: BATCH_MODE.SINGLE, nodeVersion: 2 };
const pool = createFakePool(fakeConnection);
pool.getConnection = jest.fn(() => {
throw new Error('ECONNREFUSED');
});
const fakeExecuteFunction = createMockExecuteFunction({}, mySqlMockNode);
fakeExecuteFunction.continueOnFail = () => true;
const result = await configureQueryRunner.call(
fakeExecuteFunction,
nodeOptions,
pool,
)([{ query: 'SELECT * FROM my_table WHERE id = ?', values: [55] }]);
expect(result).toEqual([{ json: expect.objectContaining({ message: 'Connection refused' }) }]);
});
it('should throw error when continueOnFail = false for connection error', async () => {
const nodeOptions: IDataObject = { queryBatching: BATCH_MODE.SINGLE, nodeVersion: 2 };
const pool = createFakePool(fakeConnection);
pool.getConnection = jest.fn(() => {
throw new Error('ECONNREFUSED');
});
const fakeExecuteFunction = createMockExecuteFunction({}, mySqlMockNode);
fakeExecuteFunction.continueOnFail = () => false;
await expect(
configureQueryRunner.call(
fakeExecuteFunction,
nodeOptions,
pool,
)([{ query: 'SELECT * FROM my_table WHERE id = ?', values: [55] }]),
).rejects.toThrow('Connection refused');
});
});

View File

@ -4,6 +4,7 @@ import type { IDataObject, INodeExecutionData, SSHCredentials } from 'n8n-workfl
export type Mysql2Connection = mysql2.Connection;
export type Mysql2Pool = mysql2.Pool;
export type Mysql2OkPacket = mysql2.OkPacket;
export type Mysql2PoolConnection = mysql2.PoolConnection;
export type QueryValues = Array<string | number | IDataObject>;
export type QueryWithValues = { query: string; values: QueryValues };

View File

@ -10,6 +10,7 @@ import { NodeOperationError } from 'n8n-workflow';
import type {
Mysql2Pool,
Mysql2PoolConnection,
ParameterMatch,
QueryMode,
QueryValues,
@ -320,7 +321,17 @@ export function configureQueryRunner(
let returnData: INodeExecutionData[] = [];
const mode = (options.queryBatching as QueryMode) || BATCH_MODE.SINGLE;
const connection = await pool.getConnection();
let connection: Mysql2PoolConnection;
try {
connection = await pool.getConnection();
} catch (e) {
const error = parseMySqlError.call(this, e);
if (!this.continueOnFail()) {
throw error;
}
return [{ json: { message: error.message, error: { ...error } } }];
}
if (mode === BATCH_MODE.SINGLE) {
const formattedQueries = queries.map(({ query, values }) => connection.format(query, values));
@ -533,7 +544,7 @@ export function addWhereClauses(
}${valueReplacement}${operator}`;
});
return [`${query}${whereQuery}`, replacements.concat(...values)];
return [`${query}${whereQuery}`, replacements.concat.apply(replacements, values)];
}
export function addSortRules(
@ -552,7 +563,7 @@ export function addSortRules(
orderByQuery += ` ${escapeSqlIdentifier(rule.column)} ${direction}${endWith}`;
});
return [`${query}${orderByQuery}`, replacements.concat(...values)];
return [`${query}${orderByQuery}`, replacements.concat.apply(replacements, values)];
}
export function replaceEmptyStringsByNulls(