fix(MySQL Node): Do not replace $ values with null (#17327)

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
Dana
2025-07-24 16:53:52 +02:00
committed by GitHub
parent 251e892a09
commit 4b626e5282
6 changed files with 256 additions and 5 deletions

View File

@@ -10,6 +10,7 @@ import { NodeOperationError } from 'n8n-workflow';
import type {
Mysql2Pool,
ParameterMatch,
QueryMode,
QueryValues,
QueryWithValues,
@@ -35,11 +36,119 @@ export function escapeSqlIdentifier(identifier: string): string {
.join('.');
}
export const prepareQueryAndReplacements = (rawQuery: string, replacements?: QueryValues) => {
function findParameterMatches(rawQuery: string, regex: RegExp): ParameterMatch[] {
const matches: ParameterMatch[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(rawQuery)) !== null) {
matches.push({
match: match[0],
index: match.index,
paramNumber: match[1],
isName: match[0].includes(':name'),
});
}
return matches;
}
function isInsideQuotes(rawQuery: string, index: number): boolean {
const beforeMatch = rawQuery.substring(0, index);
const singleQuoteCount = (beforeMatch.match(/'/g) || []).length;
const doubleQuoteCount = (beforeMatch.match(/"/g) || []).length;
return singleQuoteCount % 2 !== 0 || doubleQuoteCount % 2 !== 0;
}
function filterValidMatches(matches: ParameterMatch[], rawQuery: string): ParameterMatch[] {
return matches.filter(({ index }) => !isInsideQuotes(rawQuery, index));
}
function processParameterReplacements(
query: string,
validMatches: ParameterMatch[],
replacements: QueryValues,
): string {
let processedQuery = query;
for (const { match: matchStr, paramNumber, isName } of validMatches.reverse()) {
const matchIndex = Number(paramNumber) - 1;
if (matchIndex >= 0 && matchIndex < replacements.length) {
if (isName) {
processedQuery = processedQuery.replace(
matchStr,
escapeSqlIdentifier(replacements[matchIndex].toString()),
);
} else {
processedQuery = processedQuery.replace(matchStr, '?');
}
}
}
return processedQuery;
}
function extractValuesFromMatches(
validMatches: ParameterMatch[],
replacements: QueryValues,
): QueryValues {
const nonNameMatches = validMatches.filter((match) => !match.isName);
nonNameMatches.sort((a, b) => Number(a.paramNumber) - Number(b.paramNumber));
const values: QueryValues = [];
for (const { paramNumber } of nonNameMatches) {
const matchIndex = Number(paramNumber) - 1;
if (matchIndex >= 0 && matchIndex < replacements.length) {
values.push(replacements[matchIndex]);
}
}
return values;
}
function validateReferencedParameters(
validMatches: ParameterMatch[],
replacements: QueryValues,
): void {
for (const match of validMatches) {
const paramIndex = Number(match.paramNumber) - 1;
if (paramIndex >= replacements.length || paramIndex < 0) {
throw new Error(
`Parameter $${match.paramNumber} referenced in query but no replacement value provided at index ${paramIndex + 1}`,
);
}
}
}
export const prepareQueryAndReplacements = (
rawQuery: string,
nodeVersion: number,
replacements?: QueryValues,
) => {
if (replacements === undefined) {
return { query: rawQuery, values: [] };
}
// in UI for replacements we use syntax identical to Postgres Query Replacement, but we need to convert it to mysql2 replacement syntax
if (nodeVersion >= 2.5) {
const regex = /\$(\d+)(?::name)?/g;
const matches = findParameterMatches(rawQuery, regex);
const validMatches = filterValidMatches(matches, rawQuery);
validateReferencedParameters(validMatches, replacements);
const query = processParameterReplacements(rawQuery, validMatches, replacements);
const values = extractValuesFromMatches(validMatches, replacements);
return { query, values };
}
return prepareQueryLegacy(rawQuery, replacements);
};
export const prepareQueryLegacy = (
rawQuery: string,
replacements: QueryValues,
): QueryWithValues => {
let query: string = rawQuery;
const values: QueryValues = [];