fix(Snowflake Node): Fix issue with Insert and Update operations not working (backport to 1.x) (#29812)
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.13.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions

Co-authored-by: Jon <jonathan.bennetts@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
n8n-assistant[bot] 2026-05-06 16:03:40 +00:00 committed by GitHub
parent 6c8536ecf3
commit aec110f198
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 82 additions and 14 deletions

View File

@ -73,6 +73,44 @@ export async function destroy(conn: snowflake.Connection) {
});
}
export function escapeSnowflakeIdentifier(identifier: string): string {
if (identifier.startsWith('"') && identifier.endsWith('"') && identifier.length > 2) {
// Already quoted — preserve case (Snowflake quoted identifiers are case-sensitive)
const bare = identifier.slice(1, -1).replace(/""/g, '"');
return `"${bare.replace(/"/g, '""')}"`;
}
// Snowflake stores unquoted identifiers as UPPERCASE by default; uppercase for compatibility
return `"${identifier.toUpperCase().replace(/"/g, '""')}"`;
}
export function escapeSnowflakeObjectIdentifier(identifier: string): string {
const parts: string[] = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < identifier.length; i++) {
const char = identifier[i];
if (char === '"') {
if (inQuotes && identifier[i + 1] === '"') {
// Escaped double-quote inside a quoted identifier
current += '""';
i++;
} else {
inQuotes = !inQuotes;
current += char;
}
} else if (char === '.' && !inQuotes) {
parts.push(current);
current = '';
} else {
current += char;
}
}
parts.push(current);
return parts.map(escapeSnowflakeIdentifier).join('.');
}
export async function execute(
conn: snowflake.Connection,
sqlText: string,

View File

@ -13,6 +13,8 @@ import { getResolvables } from '@utils/utilities';
import {
connect,
destroy,
escapeSnowflakeIdentifier,
escapeSnowflakeObjectIdentifier,
execute,
getConnectionOptions,
type SnowflakeCredential,
@ -210,9 +212,11 @@ export class Snowflake implements INodeType {
const table = this.getNodeParameter('table', 0) as string;
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map((column) => column.trim());
const query = `INSERT INTO IDENTIFIER(?)(${columns.map(() => 'IDENTIFIER(?)').join(',')}) VALUES (${columns.map(() => '?').join(',')})`;
const quotedTable = escapeSnowflakeObjectIdentifier(table);
const quotedColumns = columns.map(escapeSnowflakeIdentifier);
const query = `INSERT INTO ${quotedTable} (${quotedColumns.join(',')}) VALUES (${columns.map(() => '?').join(',')})`;
const data = this.helpers.copyInputItems(items, columns);
const binds = data.map((element) => [table, ...columns, ...Object.values(element)]);
const binds = data.map((element) => [...Object.values(element)]);
await execute(connection, query, binds as unknown as snowflake.InsertBinds);
data.forEach((d, i) => {
const executionData = this.helpers.constructExecutionMetaData(
@ -237,15 +241,18 @@ export class Snowflake implements INodeType {
columns.unshift(updateKey);
}
const query = `UPDATE IDENTIFIER(?) SET ${columns.map(() => 'IDENTIFIER(?) = ?').join(',')} WHERE IDENTIFIER(?) = ?;`;
const quotedTable = escapeSnowflakeObjectIdentifier(table);
const quotedColumns = columns.map(escapeSnowflakeIdentifier);
const quotedUpdateKey = escapeSnowflakeIdentifier(updateKey);
const query = `UPDATE ${quotedTable} SET ${quotedColumns.map((col) => `${col} = ?`).join(',')} WHERE ${quotedUpdateKey} = ?;`;
const data = this.helpers.copyInputItems(items, columns);
const binds = data.map((element) => {
const values = Object.values(element);
const rowBinds: unknown[] = [table];
columns.forEach((col, idx) => {
rowBinds.push(col, values[idx]);
const rowBinds: unknown[] = [];
columns.forEach((_col, idx) => {
rowBinds.push(values[idx]);
});
rowBinds.push(updateKey, element[updateKey]);
rowBinds.push(element[updateKey]);
return rowBinds;
});
for (let i = 0; i < binds.length; i++) {

View File

@ -1,9 +1,33 @@
import crypto from 'crypto';
import { getConnectionOptions } from '../GenericFunctions';
import { escapeSnowflakeObjectIdentifier, getConnectionOptions } from '../GenericFunctions';
jest.mock('crypto');
describe('escapeSnowflakeObjectIdentifier', () => {
it('quotes a single-part identifier', () => {
expect(escapeSnowflakeObjectIdentifier('orders')).toBe('"ORDERS"');
});
it('quotes each segment of a two-part identifier', () => {
expect(escapeSnowflakeObjectIdentifier('schema.orders')).toBe('"SCHEMA"."ORDERS"');
});
it('quotes each segment of a three-part identifier', () => {
expect(escapeSnowflakeObjectIdentifier('db.schema.orders')).toBe('"DB"."SCHEMA"."ORDERS"');
});
it('preserves case for pre-quoted identifiers', () => {
expect(escapeSnowflakeObjectIdentifier('"myTable"')).toBe('"myTable"');
});
it('does not split on dots inside quoted segments', () => {
expect(escapeSnowflakeObjectIdentifier('"my.schema"."my.table"')).toBe(
'"my.schema"."my.table"',
);
});
});
describe('getConnectionOptions', () => {
const commonOptions = {
account: 'test-account',

View File

@ -39,8 +39,8 @@ describe('Test Snowflake, insert - parameter binding', () => {
expect(mockExecute).toHaveBeenCalledTimes(1);
expect(mockExecute).toHaveBeenCalledWith(
expect.objectContaining({
sqlText: 'INSERT INTO IDENTIFIER(?)(IDENTIFIER(?),IDENTIFIER(?)) VALUES (?,?)',
binds: [['orders', 'name', 'status', 'Alice', 'active']],
sqlText: 'INSERT INTO "ORDERS" ("NAME","STATUS") VALUES (?,?)',
binds: [['Alice', 'active']],
}),
);
},

View File

@ -39,12 +39,11 @@ describe('Test Snowflake, update - parameter binding', () => {
// UPDATE executes one query per row; one input row → one call
expect(mockExecute).toHaveBeenCalledTimes(1);
// Columns list is ["id", "status"] (updateKey "id" prepended since not in "status")
// Binds: [table, col1, val1, col2, val2, updateKey, updateKeyValue]
// Binds: [id_value, status_value, updateKey_value]
expect(mockExecute).toHaveBeenCalledWith(
expect.objectContaining({
sqlText:
'UPDATE IDENTIFIER(?) SET IDENTIFIER(?) = ?,IDENTIFIER(?) = ? WHERE IDENTIFIER(?) = ?;',
binds: ['orders', 'id', 1, 'status', 'shipped', 'id', 1],
sqlText: 'UPDATE "ORDERS" SET "ID" = ?,"STATUS" = ? WHERE "ID" = ?;',
binds: [1, 'shipped', 1],
}),
);
},