mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
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
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:
parent
6c8536ecf3
commit
aec110f198
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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']],
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user