mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
working, but certain create, delete update queries are not
This commit is contained in:
@@ -9,7 +9,7 @@ export const SnowflakeBlock: BlockConfig<SnowflakeResponse> = {
|
||||
description: 'Execute queries on Snowflake data warehouse',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Snowflake into your workflow. Execute SQL queries, list databases, schemas, and tables, and describe table structures in your Snowflake data warehouse.',
|
||||
'Integrate Snowflake into your workflow. Execute SQL queries, insert, update, and delete rows, list databases, schemas, and tables, and describe table structures in your Snowflake data warehouse.',
|
||||
docsLink: 'https://docs.sim.ai/tools/snowflake',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
@@ -21,6 +21,9 @@ export const SnowflakeBlock: BlockConfig<SnowflakeResponse> = {
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Execute Query', id: 'execute_query' },
|
||||
{ label: 'Insert Rows', id: 'insert_rows' },
|
||||
{ label: 'Update Rows', id: 'update_rows' },
|
||||
{ label: 'Delete Rows', id: 'delete_rows' },
|
||||
{ label: 'List Databases', id: 'list_databases' },
|
||||
{ label: 'List Schemas', id: 'list_schemas' },
|
||||
{ label: 'List Tables', id: 'list_tables' },
|
||||
@@ -36,7 +39,6 @@ export const SnowflakeBlock: BlockConfig<SnowflakeResponse> = {
|
||||
id: 'credential',
|
||||
title: 'Snowflake Account',
|
||||
type: 'oauth-input',
|
||||
provider: 'snowflake',
|
||||
serviceId: 'snowflake',
|
||||
requiredScopes: [],
|
||||
placeholder: 'Select Snowflake account',
|
||||
@@ -235,6 +237,9 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
'list_file_formats',
|
||||
'list_stages',
|
||||
'describe_table',
|
||||
'insert_rows',
|
||||
'update_rows',
|
||||
'delete_rows',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -246,7 +251,16 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_tables', 'list_views', 'list_file_formats', 'list_stages', 'describe_table'],
|
||||
value: [
|
||||
'list_tables',
|
||||
'list_views',
|
||||
'list_file_formats',
|
||||
'list_stages',
|
||||
'describe_table',
|
||||
'insert_rows',
|
||||
'update_rows',
|
||||
'delete_rows',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -257,7 +271,51 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'describe_table',
|
||||
value: ['describe_table', 'insert_rows', 'update_rows', 'delete_rows'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'columns',
|
||||
title: 'Columns',
|
||||
type: 'long-input',
|
||||
placeholder: '["column1", "column2", "column3"]',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'insert_rows',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder: '[["value1", "value2", "value3"], ["value4", "value5", "value6"]]',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'insert_rows',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updates',
|
||||
title: 'Updates',
|
||||
type: 'long-input',
|
||||
placeholder: '{"column1": "new_value", "column2": 123, "updated_at": "2024-01-01"}',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'update_rows',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'whereClause',
|
||||
title: 'WHERE Clause',
|
||||
type: 'long-input',
|
||||
placeholder: 'id = 123 (leave empty to update/delete ALL rows)',
|
||||
required: false,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['update_rows', 'delete_rows'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -274,6 +332,9 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
tools: {
|
||||
access: [
|
||||
'snowflake_execute_query',
|
||||
'snowflake_insert_rows',
|
||||
'snowflake_update_rows',
|
||||
'snowflake_delete_rows',
|
||||
'snowflake_list_databases',
|
||||
'snowflake_list_schemas',
|
||||
'snowflake_list_tables',
|
||||
@@ -288,6 +349,12 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
switch (params.operation) {
|
||||
case 'execute_query':
|
||||
return 'snowflake_execute_query'
|
||||
case 'insert_rows':
|
||||
return 'snowflake_insert_rows'
|
||||
case 'update_rows':
|
||||
return 'snowflake_update_rows'
|
||||
case 'delete_rows':
|
||||
return 'snowflake_delete_rows'
|
||||
case 'list_databases':
|
||||
return 'snowflake_list_databases'
|
||||
case 'list_schemas':
|
||||
@@ -405,6 +472,83 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
break
|
||||
}
|
||||
|
||||
case 'insert_rows': {
|
||||
if (!params.database || !params.schema || !params.table) {
|
||||
throw new Error('Database, Schema, and Table are required for insert_rows operation')
|
||||
}
|
||||
if (!params.columns || !params.values) {
|
||||
throw new Error('Columns and Values are required for insert_rows operation')
|
||||
}
|
||||
|
||||
// Parse columns and values if they are strings
|
||||
let columns = params.columns
|
||||
let values = params.values
|
||||
|
||||
if (typeof columns === 'string') {
|
||||
try {
|
||||
columns = JSON.parse(columns)
|
||||
} catch (e) {
|
||||
throw new Error('Columns must be a valid JSON array')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof values === 'string') {
|
||||
try {
|
||||
values = JSON.parse(values)
|
||||
} catch (e) {
|
||||
throw new Error('Values must be a valid JSON array of arrays')
|
||||
}
|
||||
}
|
||||
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
baseParams.table = params.table
|
||||
baseParams.columns = columns
|
||||
baseParams.values = values
|
||||
if (params.timeout) baseParams.timeout = Number.parseInt(params.timeout)
|
||||
break
|
||||
}
|
||||
|
||||
case 'update_rows': {
|
||||
if (!params.database || !params.schema || !params.table) {
|
||||
throw new Error('Database, Schema, and Table are required for update_rows operation')
|
||||
}
|
||||
if (!params.updates) {
|
||||
throw new Error('Updates object is required for update_rows operation')
|
||||
}
|
||||
|
||||
// Parse updates if it's a string
|
||||
let updates = params.updates
|
||||
if (typeof updates === 'string') {
|
||||
try {
|
||||
updates = JSON.parse(updates)
|
||||
} catch (e) {
|
||||
throw new Error('Updates must be a valid JSON object')
|
||||
}
|
||||
}
|
||||
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
baseParams.table = params.table
|
||||
baseParams.updates = updates
|
||||
if (params.whereClause) baseParams.whereClause = params.whereClause
|
||||
if (params.timeout) baseParams.timeout = Number.parseInt(params.timeout)
|
||||
break
|
||||
}
|
||||
|
||||
case 'delete_rows': {
|
||||
if (!params.database || !params.schema || !params.table) {
|
||||
throw new Error('Database, Schema, and Table are required for delete_rows operation')
|
||||
}
|
||||
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
baseParams.table = params.table
|
||||
if (params.whereClause) baseParams.whereClause = params.whereClause
|
||||
if (params.timeout) baseParams.timeout = Number.parseInt(params.timeout)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${operation}`)
|
||||
}
|
||||
@@ -426,6 +570,13 @@ Return ONLY the SQL query - no explanations, no markdown code blocks, no extra t
|
||||
database: { type: 'string', description: 'Database name' },
|
||||
schema: { type: 'string', description: 'Schema name' },
|
||||
table: { type: 'string', description: 'Table name' },
|
||||
columns: { type: 'json', description: 'Array of column names for insert operation' },
|
||||
values: { type: 'json', description: 'Array of arrays containing values for insert operation' },
|
||||
updates: {
|
||||
type: 'json',
|
||||
description: 'Object containing column-value pairs for update operation',
|
||||
},
|
||||
whereClause: { type: 'string', description: 'WHERE clause for update/delete operations' },
|
||||
timeout: { type: 'string', description: 'Query timeout in seconds' },
|
||||
},
|
||||
outputs: {
|
||||
|
||||
@@ -1011,8 +1011,10 @@ import {
|
||||
import { smsSendTool } from '@/tools/sms'
|
||||
import { smtpSendMailTool } from '@/tools/smtp'
|
||||
import {
|
||||
snowflakeDeleteRowsTool,
|
||||
snowflakeDescribeTableTool,
|
||||
snowflakeExecuteQueryTool,
|
||||
snowflakeInsertRowsTool,
|
||||
snowflakeListDatabasesTool,
|
||||
snowflakeListFileFormatsTool,
|
||||
snowflakeListSchemasTool,
|
||||
@@ -1020,6 +1022,7 @@ import {
|
||||
snowflakeListTablesTool,
|
||||
snowflakeListViewsTool,
|
||||
snowflakeListWarehousesTool,
|
||||
snowflakeUpdateRowsTool,
|
||||
} from '@/tools/snowflake'
|
||||
import {
|
||||
checkCommandExistsTool as sshCheckCommandExistsTool,
|
||||
@@ -2437,6 +2440,9 @@ export const tools: Record<string, ToolConfig> = {
|
||||
zoom_delete_recording: zoomDeleteRecordingTool,
|
||||
zoom_list_past_participants: zoomListPastParticipantsTool,
|
||||
snowflake_execute_query: snowflakeExecuteQueryTool,
|
||||
snowflake_insert_rows: snowflakeInsertRowsTool,
|
||||
snowflake_update_rows: snowflakeUpdateRowsTool,
|
||||
snowflake_delete_rows: snowflakeDeleteRowsTool,
|
||||
snowflake_list_databases: snowflakeListDatabasesTool,
|
||||
snowflake_list_schemas: snowflakeListSchemasTool,
|
||||
snowflake_list_tables: snowflakeListTablesTool,
|
||||
|
||||
194
apps/sim/tools/snowflake/delete_rows.ts
Normal file
194
apps/sim/tools/snowflake/delete_rows.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeDeleteRowsParams,
|
||||
SnowflakeDeleteRowsResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeDeleteRowsTool')
|
||||
|
||||
/**
|
||||
* Build DELETE SQL statement from parameters
|
||||
*/
|
||||
function buildDeleteSQL(
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
whereClause?: string
|
||||
): string {
|
||||
const fullTableName = `${database}.${schema}.${table}`
|
||||
|
||||
let sql = `DELETE FROM ${fullTableName}`
|
||||
|
||||
// Add WHERE clause if provided
|
||||
if (whereClause?.trim()) {
|
||||
sql += ` WHERE ${whereClause}`
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export const snowflakeDeleteRowsTool: ToolConfig<
|
||||
SnowflakeDeleteRowsParams,
|
||||
SnowflakeDeleteRowsResponse
|
||||
> = {
|
||||
id: 'snowflake_delete_rows',
|
||||
name: 'Snowflake Delete Rows',
|
||||
description: 'Delete rows from a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Table name',
|
||||
},
|
||||
whereClause: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'WHERE clause to filter rows to delete (e.g., "id = 123" or "status = \'inactive\' AND created_at < \'2024-01-01\'"). WARNING: If not provided, ALL rows will be deleted.',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Query timeout in seconds (default: 60)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeDeleteRowsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeDeleteRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
}),
|
||||
body: (params: SnowflakeDeleteRowsParams) => {
|
||||
// Build DELETE SQL
|
||||
const deleteSQL = buildDeleteSQL(
|
||||
params.database,
|
||||
params.schema,
|
||||
params.table,
|
||||
params.whereClause
|
||||
)
|
||||
|
||||
logger.info('Building DELETE statement', {
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
hasWhereClause: !!params.whereClause,
|
||||
})
|
||||
|
||||
// Log warning if no WHERE clause provided
|
||||
if (!params.whereClause) {
|
||||
logger.warn('DELETE statement has no WHERE clause - ALL rows will be deleted', {
|
||||
table: `${params.database}.${params.schema}.${params.table}`,
|
||||
})
|
||||
}
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: deleteSQL,
|
||||
timeout: params.timeout || 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeDeleteRowsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to delete rows from Snowflake table', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
table: params ? `${params.database}.${params.schema}.${params.table}` : 'unknown',
|
||||
})
|
||||
throw new Error(`Failed to delete rows: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract number of rows deleted from response
|
||||
const rowsDeleted = data.statementStatusUrl ? 'unknown' : 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
statementHandle: data.statementHandle,
|
||||
rowsDeleted,
|
||||
message: `Successfully deleted rows from ${params?.database}.${params?.schema}.${params?.table}`,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Delete operation result',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { snowflakeDeleteRowsTool } from '@/tools/snowflake/delete_rows'
|
||||
import { snowflakeDescribeTableTool } from '@/tools/snowflake/describe_table'
|
||||
import { snowflakeExecuteQueryTool } from '@/tools/snowflake/execute_query'
|
||||
import { snowflakeInsertRowsTool } from '@/tools/snowflake/insert_rows'
|
||||
import { snowflakeListDatabasesTool } from '@/tools/snowflake/list_databases'
|
||||
import { snowflakeListFileFormatsTool } from '@/tools/snowflake/list_file_formats'
|
||||
import { snowflakeListSchemasTool } from '@/tools/snowflake/list_schemas'
|
||||
@@ -7,6 +9,7 @@ import { snowflakeListStagesTool } from '@/tools/snowflake/list_stages'
|
||||
import { snowflakeListTablesTool } from '@/tools/snowflake/list_tables'
|
||||
import { snowflakeListViewsTool } from '@/tools/snowflake/list_views'
|
||||
import { snowflakeListWarehousesTool } from '@/tools/snowflake/list_warehouses'
|
||||
import { snowflakeUpdateRowsTool } from '@/tools/snowflake/update_rows'
|
||||
|
||||
export {
|
||||
snowflakeExecuteQueryTool,
|
||||
@@ -18,4 +21,7 @@ export {
|
||||
snowflakeListWarehousesTool,
|
||||
snowflakeListFileFormatsTool,
|
||||
snowflakeListStagesTool,
|
||||
snowflakeInsertRowsTool,
|
||||
snowflakeUpdateRowsTool,
|
||||
snowflakeDeleteRowsTool,
|
||||
}
|
||||
|
||||
231
apps/sim/tools/snowflake/insert_rows.ts
Normal file
231
apps/sim/tools/snowflake/insert_rows.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeInsertRowsParams,
|
||||
SnowflakeInsertRowsResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeInsertRowsTool')
|
||||
|
||||
/**
|
||||
* Build INSERT SQL statement from parameters
|
||||
*/
|
||||
function buildInsertSQL(
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
columns: string[],
|
||||
values: any[][]
|
||||
): string {
|
||||
const fullTableName = `${database}.${schema}.${table}`
|
||||
const columnList = columns.join(', ')
|
||||
|
||||
// Build values clause for multiple rows
|
||||
const valuesClause = values
|
||||
.map((rowValues) => {
|
||||
const formattedValues = rowValues.map((val) => {
|
||||
if (val === null || val === undefined) {
|
||||
return 'NULL'
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
// Escape single quotes by doubling them
|
||||
return `'${val.replace(/'/g, "''")}'`
|
||||
}
|
||||
if (typeof val === 'boolean') {
|
||||
return val ? 'TRUE' : 'FALSE'
|
||||
}
|
||||
return String(val)
|
||||
})
|
||||
return `(${formattedValues.join(', ')})`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return `INSERT INTO ${fullTableName} (${columnList}) VALUES ${valuesClause}`
|
||||
}
|
||||
|
||||
export const snowflakeInsertRowsTool: ToolConfig<
|
||||
SnowflakeInsertRowsParams,
|
||||
SnowflakeInsertRowsResponse
|
||||
> = {
|
||||
id: 'snowflake_insert_rows',
|
||||
name: 'Snowflake Insert Rows',
|
||||
description: 'Insert rows into a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Table name',
|
||||
},
|
||||
columns: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Array of column names to insert data into',
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Array of arrays containing values to insert. Each inner array represents one row and must match the order of columns.',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Query timeout in seconds (default: 60)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeInsertRowsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeInsertRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
}),
|
||||
body: (params: SnowflakeInsertRowsParams) => {
|
||||
// Validate inputs
|
||||
if (!Array.isArray(params.columns) || params.columns.length === 0) {
|
||||
throw new Error('Columns must be a non-empty array')
|
||||
}
|
||||
|
||||
if (!Array.isArray(params.values) || params.values.length === 0) {
|
||||
throw new Error('Values must be a non-empty array')
|
||||
}
|
||||
|
||||
// Validate each row has correct number of values
|
||||
for (let i = 0; i < params.values.length; i++) {
|
||||
if (!Array.isArray(params.values[i])) {
|
||||
throw new Error(`Values row ${i} must be an array`)
|
||||
}
|
||||
if (params.values[i].length !== params.columns.length) {
|
||||
throw new Error(
|
||||
`Values row ${i} has ${params.values[i].length} values but ${params.columns.length} columns were specified`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Build INSERT SQL
|
||||
const insertSQL = buildInsertSQL(
|
||||
params.database,
|
||||
params.schema,
|
||||
params.table,
|
||||
params.columns,
|
||||
params.values
|
||||
)
|
||||
|
||||
logger.info('Building INSERT statement', {
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
columnCount: params.columns.length,
|
||||
rowCount: params.values.length,
|
||||
})
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: insertSQL,
|
||||
timeout: params.timeout || 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeInsertRowsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to insert rows into Snowflake table', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
table: params ? `${params.database}.${params.schema}.${params.table}` : 'unknown',
|
||||
})
|
||||
throw new Error(`Failed to insert rows: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Get number of rows inserted from response
|
||||
const rowsInserted = data.statementStatusUrl ? params?.values.length || 0 : 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
statementHandle: data.statementHandle,
|
||||
rowsInserted,
|
||||
message: `Successfully inserted ${rowsInserted} row(s) into ${params?.database}.${params?.schema}.${params?.table}`,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Insert operation result with row count',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -247,6 +247,83 @@ export interface SnowflakeListStagesResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for inserting rows
|
||||
*/
|
||||
export interface SnowflakeInsertRowsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
table: string
|
||||
columns: string[]
|
||||
values: any[][]
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for updating rows
|
||||
*/
|
||||
export interface SnowflakeUpdateRowsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
table: string
|
||||
updates: Record<string, any>
|
||||
whereClause?: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for deleting rows
|
||||
*/
|
||||
export interface SnowflakeDeleteRowsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
table: string
|
||||
whereClause?: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for insert rows operation
|
||||
*/
|
||||
export interface SnowflakeInsertRowsResponse extends ToolResponse {
|
||||
output: {
|
||||
statementHandle?: string
|
||||
rowsInserted?: number
|
||||
message?: string
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for update rows operation
|
||||
*/
|
||||
export interface SnowflakeUpdateRowsResponse extends ToolResponse {
|
||||
output: {
|
||||
statementHandle?: string
|
||||
rowsUpdated?: number | string
|
||||
message?: string
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for delete rows operation
|
||||
*/
|
||||
export interface SnowflakeDeleteRowsResponse extends ToolResponse {
|
||||
output: {
|
||||
statementHandle?: string
|
||||
rowsDeleted?: number | string
|
||||
message?: string
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic Snowflake response type for the block
|
||||
*/
|
||||
@@ -260,3 +337,6 @@ export type SnowflakeResponse =
|
||||
| SnowflakeListWarehousesResponse
|
||||
| SnowflakeListFileFormatsResponse
|
||||
| SnowflakeListStagesResponse
|
||||
| SnowflakeInsertRowsResponse
|
||||
| SnowflakeUpdateRowsResponse
|
||||
| SnowflakeDeleteRowsResponse
|
||||
|
||||
233
apps/sim/tools/snowflake/update_rows.ts
Normal file
233
apps/sim/tools/snowflake/update_rows.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeUpdateRowsParams,
|
||||
SnowflakeUpdateRowsResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeUpdateRowsTool')
|
||||
|
||||
/**
|
||||
* Build UPDATE SQL statement from parameters
|
||||
*/
|
||||
function buildUpdateSQL(
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
updates: Record<string, any>,
|
||||
whereClause?: string
|
||||
): string {
|
||||
const fullTableName = `${database}.${schema}.${table}`
|
||||
|
||||
// Build SET clause
|
||||
const setClause = Object.entries(updates)
|
||||
.map(([column, value]) => {
|
||||
let formattedValue: string
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
formattedValue = 'NULL'
|
||||
} else if (typeof value === 'string') {
|
||||
// Escape single quotes by doubling them
|
||||
formattedValue = `'${value.replace(/'/g, "''")}'`
|
||||
} else if (typeof value === 'boolean') {
|
||||
formattedValue = value ? 'TRUE' : 'FALSE'
|
||||
} else {
|
||||
formattedValue = String(value)
|
||||
}
|
||||
|
||||
return `${column} = ${formattedValue}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
let sql = `UPDATE ${fullTableName} SET ${setClause}`
|
||||
|
||||
// Add WHERE clause if provided
|
||||
if (whereClause?.trim()) {
|
||||
sql += ` WHERE ${whereClause}`
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export const snowflakeUpdateRowsTool: ToolConfig<
|
||||
SnowflakeUpdateRowsParams,
|
||||
SnowflakeUpdateRowsResponse
|
||||
> = {
|
||||
id: 'snowflake_update_rows',
|
||||
name: 'Snowflake Update Rows',
|
||||
description: 'Update rows in a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'snowflake',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Snowflake',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Table name',
|
||||
},
|
||||
updates: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Object containing column-value pairs to update (e.g., {"status": "active", "updated_at": "2024-01-01"})',
|
||||
},
|
||||
whereClause: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'WHERE clause to filter rows to update (e.g., "id = 123" or "status = \'pending\' AND created_at < \'2024-01-01\'"). If not provided, all rows will be updated.',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Query timeout in seconds (default: 60)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeUpdateRowsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeUpdateRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'OAUTH',
|
||||
}),
|
||||
body: (params: SnowflakeUpdateRowsParams) => {
|
||||
// Validate inputs
|
||||
if (
|
||||
!params.updates ||
|
||||
typeof params.updates !== 'object' ||
|
||||
Object.keys(params.updates).length === 0
|
||||
) {
|
||||
throw new Error('Updates must be a non-empty object with column-value pairs')
|
||||
}
|
||||
|
||||
// Build UPDATE SQL
|
||||
const updateSQL = buildUpdateSQL(
|
||||
params.database,
|
||||
params.schema,
|
||||
params.table,
|
||||
params.updates,
|
||||
params.whereClause
|
||||
)
|
||||
|
||||
logger.info('Building UPDATE statement', {
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
updateColumnCount: Object.keys(params.updates).length,
|
||||
hasWhereClause: !!params.whereClause,
|
||||
})
|
||||
|
||||
// Log warning if no WHERE clause provided
|
||||
if (!params.whereClause) {
|
||||
logger.warn('UPDATE statement has no WHERE clause - all rows will be updated', {
|
||||
table: `${params.database}.${params.schema}.${params.table}`,
|
||||
})
|
||||
}
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: updateSQL,
|
||||
timeout: params.timeout || 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeUpdateRowsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to update rows in Snowflake table', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
table: params ? `${params.database}.${params.schema}.${params.table}` : 'unknown',
|
||||
})
|
||||
throw new Error(`Failed to update rows: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract number of rows updated from response
|
||||
const rowsUpdated = data.statementStatusUrl ? 'unknown' : 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
statementHandle: data.statementHandle,
|
||||
rowsUpdated,
|
||||
message: `Successfully updated rows in ${params?.database}.${params?.schema}.${params?.table}`,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Update operation result',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user