From b6405b0480d5d938882d5779dcafa3532bd6d5e8 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Sat, 8 Aug 2026 01:30:13 -0700 Subject: [PATCH] fix(snowflake): scope block params by operation --- apps/sim/blocks/blocks/snowflake.ts | 118 ++++++++++++++++++++++----- apps/sim/tools/snowflake/sql.test.ts | 64 +++++++++++++++ 2 files changed, 160 insertions(+), 22 deletions(-) diff --git a/apps/sim/blocks/blocks/snowflake.ts b/apps/sim/blocks/blocks/snowflake.ts index e5ade128e0..064a229819 100644 --- a/apps/sim/blocks/blocks/snowflake.ts +++ b/apps/sim/blocks/blocks/snowflake.ts @@ -599,30 +599,104 @@ export const SnowflakeBlock: BlockConfig = { tool: (params) => `snowflake_${params.operation}`, params: (params) => { const jsonBudget = { bytes: 0 } - return { - async: optionalBoolean(params.async), - bindings: parseJson(params.bindings, 'Bindings', jsonBudget), - database: params.database || params.contextDatabase || undefined, - schema: params.schema || params.contextSchema || undefined, - taskName: params.taskName || params.taskNameFilter || undefined, - rows: parseJson(params.rows, 'Rows', jsonBudget), - matchColumns: parseJson(params.matchColumns, 'Match columns', jsonBudget), - filters: parseJson(params.filters, 'Filters', jsonBudget), - procedureArguments: parseJson( - params.procedureArguments, - 'Procedure arguments', - jsonBudget - ), - partition: optionalNumber(params.partition), + const statementParams = () => ({ timeout: optionalNumber(params.timeout), maxRows: optionalNumber(params.maxRows), - limit: optionalNumber(params.limit), - purge: optionalBoolean(params.purge), - force: optionalBoolean(params.force), - retryLast: optionalBoolean(params.retryLast), - errorOnly: optionalBoolean(params.errorOnly), - includeViews: optionalBoolean(params.includeViews), - onError: copyOnError(params.onError, params.onErrorThreshold), + }) + const contextParams = () => ({ + ...statementParams(), + database: params.contextDatabase || undefined, + schema: params.contextSchema || undefined, + }) + const objectParams = () => ({ + ...statementParams(), + database: params.database || undefined, + schema: params.schema || undefined, + }) + + switch (params.operation) { + case 'execute_sql': + return { + ...contextParams(), + async: optionalBoolean(params.async), + bindings: parseJson(params.bindings, 'Bindings', jsonBudget), + } + case 'get_statement': + return { + partition: optionalNumber(params.partition), + maxRows: optionalNumber(params.maxRows), + } + case 'insert_rows': + return { + ...objectParams(), + rows: parseJson(params.rows, 'Rows', jsonBudget), + } + case 'update_rows': + case 'upsert_rows': + return { + ...objectParams(), + rows: parseJson(params.rows, 'Rows', jsonBudget), + matchColumns: parseJson(params.matchColumns, 'Match columns', jsonBudget), + } + case 'delete_rows': + return { + ...objectParams(), + filters: parseJson(params.filters, 'Filters', jsonBudget), + } + case 'load_data': + return { + ...objectParams(), + purge: optionalBoolean(params.purge), + force: optionalBoolean(params.force), + onError: copyOnError(params.onError, params.onErrorThreshold), + } + case 'list_warehouses': + case 'get_warehouse': + case 'resume_warehouse': + case 'suspend_warehouse': + case 'cancel_task_run': + case 'get_task_run_output': + return contextParams() + case 'list_tasks': + return { + ...objectParams(), + limit: optionalNumber(params.limit), + } + case 'get_task': + return objectParams() + case 'run_task': + return { + ...objectParams(), + retryLast: optionalBoolean(params.retryLast), + } + case 'list_task_runs': + return { + ...contextParams(), + taskName: params.taskNameFilter || undefined, + limit: optionalNumber(params.limit), + errorOnly: optionalBoolean(params.errorOnly), + } + case 'get_task_run': + return { + ...contextParams(), + taskName: params.taskNameFilter || undefined, + } + case 'introspect_schema': + return { + ...objectParams(), + includeViews: optionalBoolean(params.includeViews), + } + case 'call_procedure': + return { + ...objectParams(), + procedureArguments: parseJson( + params.procedureArguments, + 'Procedure arguments', + jsonBudget + ), + } + default: + return {} } }, }, diff --git a/apps/sim/tools/snowflake/sql.test.ts b/apps/sim/tools/snowflake/sql.test.ts index cf307ece16..61fb9b7e66 100644 --- a/apps/sim/tools/snowflake/sql.test.ts +++ b/apps/sim/tools/snowflake/sql.test.ts @@ -72,6 +72,70 @@ describe('Snowflake SQL builders', () => { ).toThrow('exceeds') }) + it('only coerces fields used by the selected block operation', () => { + const mapParams = SnowflakeBlock.tools.config.params + if (!mapParams) throw new Error('Snowflake block must map tool parameters') + + expect(() => + mapParams({ + operation: 'execute_sql', + rows: '{invalid', + filters: '{invalid', + procedureArguments: '{invalid', + onError: 'SKIP_FILE_NUMBER', + }) + ).not.toThrow() + expect(() => + mapParams({ + operation: 'delete_rows', + rows: '{invalid', + filters: '{"id":1}', + }) + ).not.toThrow() + expect(() => + mapParams({ + operation: 'load_data', + onError: 'SKIP_FILE_NUMBER', + }) + ).toThrow('threshold') + }) + + it('maps overlapping block fields according to the selected operation', () => { + const mapParams = SnowflakeBlock.tools.config.params + if (!mapParams) throw new Error('Snowflake block must map tool parameters') + const finalParams = (params: Record) => ({ + ...params, + ...mapParams(params), + }) + const staleFields = { + database: 'OBJECT_DB', + schema: 'OBJECT_SCHEMA', + contextDatabase: 'CONTEXT_DB', + contextSchema: 'CONTEXT_SCHEMA', + taskName: 'TASK_DEFINITION', + taskNameFilter: 'TASK_HISTORY_FILTER', + } + + expect(finalParams({ operation: 'execute_sql', ...staleFields })).toMatchObject({ + database: 'CONTEXT_DB', + schema: 'CONTEXT_SCHEMA', + }) + expect(finalParams({ operation: 'insert_rows', ...staleFields, rows: '[]' })).toMatchObject({ + database: 'OBJECT_DB', + schema: 'OBJECT_SCHEMA', + }) + expect(finalParams({ operation: 'list_task_runs', ...staleFields })).toMatchObject({ + database: 'CONTEXT_DB', + schema: 'CONTEXT_SCHEMA', + taskName: 'TASK_HISTORY_FILTER', + }) + expect(finalParams({ operation: 'get_task', ...staleFields })).toMatchObject({ + database: 'OBJECT_DB', + schema: 'OBJECT_SCHEMA', + taskName: 'TASK_DEFINITION', + }) + }) + it('builds a bound multi-row INSERT in stable column order', () => { const result = buildInsertRows({ ...table,