feat(integrations): add Snowflake PAT integration

This commit is contained in:
Bill Leoutsakos
2026-08-08 01:14:30 -07:00
parent 3b0651ed9c
commit 2deca33d15
38 changed files with 5108 additions and 3 deletions
+2
View File
@@ -211,6 +211,7 @@ import {
SlackIcon,
SmartleadIcon,
SmtpIcon,
SnowflakeIcon,
SportmonksIcon,
SQSIcon,
SquareIcon,
@@ -496,6 +497,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
slack: SlackIcon,
smartlead: SmartleadIcon,
smtp: SmtpIcon,
snowflake: SnowflakeIcon,
sportmonks: SportmonksIcon,
sqs: SQSIcon,
square: SquareIcon,
@@ -222,6 +222,7 @@
"slack",
"smartlead",
"smtp",
"snowflake",
"sportmonks",
"sqs",
"square",
@@ -0,0 +1,926 @@
---
title: Snowflake
description: Query data and manage warehouses and tasks in Snowflake
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="snowflake"
color="#F7FBFD"
/>
## Usage Instructions
Connect with a Snowflake programmatic access token to execute SQL, synchronize structured rows, load staged data, manage warehouses and tasks, inspect schemas, and call stored procedures.
## Actions
### Snowflake Execute SQL
Execute one parameterized SQL statement through the Snowflake SQL API.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `statement` | string | Yes | One Snowflake SQL statement to execute |
| `bindings` | json | No | Snowflake bindings keyed by 1-based position, each with type and string value |
| `async` | boolean | No | Return immediately with a statement handle |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Get Statement
Check a running or completed statement and retrieve exactly one result partition. Canceled or failed statements are returned as errors.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `statementHandle` | string | Yes | Statement handle returned by Snowflake |
| `partition` | number | No | Zero-based result partition to retrieve; defaults to 0 |
| `maxRows` | number | No | Maximum rows returned from this partition, from 1 to 10000 |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Cancel Statement
Cancel a running Snowflake SQL API statement.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `statementHandle` | string | Yes | Statement handle returned by Snowflake |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Insert Rows
Insert up to 1000 structured JSON rows using bound values.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `table` | string | Yes | Target Snowflake table name within the selected database and schema context |
| `rows` | json | Yes | Non-empty JSON array of row objects with matching keys |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Update Rows
Update matching rows with a bound MERGE statement without inserting new rows.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `table` | string | Yes | Target Snowflake table name within the selected database and schema context |
| `rows` | json | Yes | Non-empty JSON array of row objects with matching keys |
| `matchColumns` | array | Yes | Columns used to match target rows |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Upsert Rows
Update matching rows and insert unmatched rows with a bound MERGE statement.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `table` | string | Yes | Target Snowflake table name within the selected database and schema context |
| `rows` | json | Yes | Non-empty JSON array of row objects with matching keys |
| `matchColumns` | array | Yes | Columns used to match target rows |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Delete Rows
Delete rows matching a required set of bound equality filters.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `table` | string | Yes | Target Snowflake table name within the selected database and schema context |
| `filters` | json | Yes | Non-empty JSON object of column equality filters combined with AND |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Load Data
Load files from an existing Snowflake stage with COPY INTO.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Target database name |
| `schema` | string | Yes | Target schema name |
| `table` | string | Yes | Target table name |
| `stagePath` | string | Yes | Existing stage path, for example @my_stage/path |
| `fileFormat` | string | No | Optional named file format |
| `pattern` | string | No | Optional regular expression used to select staged files |
| `onError` | string | No | COPY error handling: ABORT_STATEMENT, CONTINUE, SKIP_FILE, SKIP_FILE_&lt;count&gt;, or SKIP_FILE_&lt;percent&gt;% |
| `purge` | boolean | No | Remove successfully loaded files from the stage |
| `force` | boolean | No | Reload files even when Snowflake has loaded them before |
| `matchByColumnName` | string | No | CASE_SENSITIVE, CASE_INSENSITIVE, or NONE |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake List Warehouses
List warehouses visible to the active Snowflake role.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `nameLike` | string | No | Optional SQL LIKE pattern for warehouse names |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Get Warehouse
Get the full details for a Snowflake virtual warehouse.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `warehouseName` | string | Yes | Warehouse name |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Resume Warehouse
Resume a Snowflake virtual warehouse if it is suspended.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `warehouseName` | string | Yes | Warehouse name |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Suspend Warehouse
Suspend a Snowflake virtual warehouse.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `warehouseName` | string | Yes | Warehouse name |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake List Tasks
List tasks in a Snowflake schema.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `nameLike` | string | No | Optional SQL LIKE pattern for task names |
| `limit` | number | No | Maximum task rows, from 1 to 10000 |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Get Task
Describe a Snowflake task.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `taskName` | string | Yes | Task name |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Run Task
Run a Snowflake task immediately, optionally retrying its last failed graph.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `taskName` | string | Yes | Task name |
| `retryLast` | boolean | No | Retry the last failed task graph run |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake List Task Runs
Query up to seven days of Snowflake task history, capped at 10000 rows.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskName` | string | No | Optional task name filter |
| `startTime` | string | No | Optional inclusive ISO timestamp within the last seven days |
| `endTime` | string | No | Optional exclusive ISO timestamp within the last seven days |
| `errorOnly` | boolean | No | Return only failed task runs |
| `limit` | number | No | Maximum task runs, from 1 to 10000 |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Get Task Run
Find one task history record by query ID within Snowflake’s seven-day window and 10000 most recent records after optional filters.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `queryId` | string | Yes | Task run query ID from TASK_HISTORY |
| `taskName` | string | No | Optional task name used to narrow the 10000-record history window |
| `startTime` | string | No | Optional inclusive ISO timestamp within the last seven days |
| `endTime` | string | No | Optional exclusive ISO timestamp within the last seven days |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Cancel Task Run
Cancel a running task query with SYSTEM$CANCEL_QUERY.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `queryId` | string | Yes | Running task query ID from TASK_HISTORY |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Get Task Run Output
Read a task query result with RESULT_SCAN during Snowflake’s 24-hour retention window using the task owner role.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `queryId` | string | Yes | Completed task query ID; task results require the task owner role, while manual query results require the same user |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Introspect Schema
Inspect table and column metadata through Snowflake INFORMATION_SCHEMA views.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database containing the INFORMATION_SCHEMA views |
| `schema` | string | No | Optional exact schema name filter |
| `table` | string | No | Optional exact table name filter |
| `includeViews` | boolean | No | Include views as well as base tables |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
### Snowflake Call Procedure
Call a stored procedure with explicitly typed Snowflake bindings.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `database` | string | Yes | Database name |
| `schema` | string | Yes | Schema name |
| `procedureName` | string | Yes | Stored procedure name |
| `procedureArguments` | array | No | Ordered argument bindings with a Snowflake type and string value |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `statementHandle` | string | Snowflake statement handle |
| `status` | string | Statement status: SUCCEEDED, RUNNING, or CANCELED |
| `code` | string | Snowflake response code |
| `sqlState` | string | SQLSTATE response code |
| `message` | string | Snowflake status message |
| `columns` | array | Documented Snowflake result column metadata |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Snowflake data type |
| ↳ `length` | number | Column length |
| ↳ `precision` | number | Numeric precision |
| ↳ `scale` | number | Numeric scale |
| ↳ `nullable` | boolean | Whether the column is nullable |
| `rows` | array | One Snowflake result partition as arrays of string or null column values |
| `totalRows` | number | Total result rows |
| `partitions` | array | Available result partitions and their documented sizes |
| ↳ `rowCount` | number | Rows in the partition |
| ↳ `compressedSize` | number | Compressed bytes |
| ↳ `uncompressedSize` | number | Uncompressed bytes |
| `currentPartition` | number | Zero-based partition returned |
| `nextPartition` | number | Next partition to request, if one exists |
| `truncated` | boolean | Whether additional rows or partitions remain |
| `rowsInserted` | number | Rows inserted by a DML statement |
| `rowsUpdated` | number | Rows updated by a DML statement |
| `rowsDeleted` | number | Rows deleted by a DML statement |
| `duplicateRowsUpdated` | number | Duplicate rows updated by a DML statement |
| `rowsAffected` | number | Total inserted, updated, and deleted rows |
+815
View File
@@ -0,0 +1,815 @@
import { SnowflakeIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { SnowflakeStatementResponse } from '@/tools/snowflake/types'
import { addSnowflakeRequestBytes } from '@/tools/snowflake/utils'
const statementOperations = [
'execute_sql',
'insert_rows',
'update_rows',
'upsert_rows',
'delete_rows',
'load_data',
'list_warehouses',
'get_warehouse',
'resume_warehouse',
'suspend_warehouse',
'list_tasks',
'get_task',
'run_task',
'list_task_runs',
'get_task_run',
'cancel_task_run',
'get_task_run_output',
'introspect_schema',
'call_procedure',
]
const contextOnlyOperations = [
'execute_sql',
'list_warehouses',
'get_warehouse',
'resume_warehouse',
'suspend_warehouse',
'list_task_runs',
'get_task_run',
'cancel_task_run',
'get_task_run_output',
]
const dataOperations = ['insert_rows', 'update_rows', 'upsert_rows', 'delete_rows', 'load_data']
const taskDefinitionOperations = ['list_tasks', 'get_task', 'run_task']
function parseJson(value: unknown, label: string, budget: { bytes: number }): unknown {
if (value === undefined || value === null || value === '') return undefined
if (typeof value !== 'string') return value
budget.bytes = addSnowflakeRequestBytes(budget.bytes, value)
try {
return JSON.parse(value)
} catch {
throw new Error(`${label} must be valid JSON`)
}
}
function optionalNumber(value: unknown): number | undefined {
if (value === undefined || value === null || value === '') return undefined
const number = Number(value)
if (!Number.isFinite(number)) throw new Error(`${String(value)} must be a number`)
return number
}
function optionalBoolean(value: unknown): boolean | undefined {
if (value === undefined || value === null || value === '') return undefined
if (value === true || value === 'true') return true
if (value === false || value === 'false') return false
throw new Error(`${String(value)} must be true or false`)
}
function copyOnError(value: unknown, threshold: unknown): string | undefined {
if (value === undefined || value === null || value === '') return undefined
if (value !== 'SKIP_FILE_NUMBER' && value !== 'SKIP_FILE_PERCENT') return String(value)
const number = optionalNumber(threshold)
if (number === undefined || number <= 0) {
throw new Error('Skip file threshold must be greater than zero')
}
if (value === 'SKIP_FILE_PERCENT' && number > 100) {
throw new Error('Skip file percentage must be between 1 and 100')
}
if (value === 'SKIP_FILE_NUMBER' && !Number.isInteger(number)) {
throw new Error('Skip file error count must be a positive integer')
}
return `SKIP_FILE_${number}${value === 'SKIP_FILE_PERCENT' ? '%' : ''}`
}
export const SnowflakeBlock: BlockConfig<SnowflakeStatementResponse> = {
type: 'snowflake',
name: 'Snowflake',
description: 'Query data and manage warehouses and tasks in Snowflake',
authMode: AuthMode.ApiKey,
longDescription:
'Connect with a Snowflake programmatic access token to execute SQL, synchronize structured rows, load staged data, manage warehouses and tasks, inspect schemas, and call stored procedures.',
docsLink: 'https://docs.sim.ai/integrations/snowflake',
category: 'tools',
integrationType: IntegrationType.Databases,
bgColor: '#F7FBFD',
icon: SnowflakeIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Execute SQL', id: 'execute_sql' },
{ label: 'Get Statement', id: 'get_statement' },
{ label: 'Cancel Statement', id: 'cancel_statement' },
{ label: 'Insert Rows', id: 'insert_rows' },
{ label: 'Update Rows', id: 'update_rows' },
{ label: 'Upsert Rows', id: 'upsert_rows' },
{ label: 'Delete Rows', id: 'delete_rows' },
{ label: 'Load Data', id: 'load_data' },
{ label: 'List Warehouses', id: 'list_warehouses' },
{ label: 'Get Warehouse', id: 'get_warehouse' },
{ label: 'Resume Warehouse', id: 'resume_warehouse' },
{ label: 'Suspend Warehouse', id: 'suspend_warehouse' },
{ label: 'List Tasks', id: 'list_tasks' },
{ label: 'Get Task', id: 'get_task' },
{ label: 'Run Task', id: 'run_task' },
{ label: 'List Task Runs', id: 'list_task_runs' },
{ label: 'Get Task Run', id: 'get_task_run' },
{ label: 'Cancel Task Run', id: 'cancel_task_run' },
{ label: 'Get Task Run Output', id: 'get_task_run_output' },
{ label: 'Introspect Schema', id: 'introspect_schema' },
{ label: 'Call Procedure', id: 'call_procedure' },
],
value: () => 'execute_sql',
},
{
id: 'statement',
title: 'SQL Statement',
type: 'code',
placeholder: 'SELECT * FROM ANALYTICS.PUBLIC.EVENTS LIMIT 100',
condition: { field: 'operation', value: 'execute_sql' },
required: { field: 'operation', value: 'execute_sql' },
},
{
id: 'bindings',
title: 'Bindings',
type: 'code',
placeholder: '{"1":{"type":"TEXT","value":"active"}}',
condition: { field: 'operation', value: 'execute_sql' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt:
'Generate a JSON object keyed by 1-based binding position. Each value must contain a Snowflake binding type and a string value, for example {"1":{"type":"TEXT","value":"active"}}. Return ONLY the JSON object - no explanations, no extra text.',
placeholder: 'Describe the values and Snowflake types to bind...',
},
},
{
id: 'async',
title: 'Run Asynchronously',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'execute_sql' },
mode: 'advanced',
},
{
id: 'statementHandle',
title: 'Statement Handle',
type: 'short-input',
placeholder: 'Enter the Snowflake statement handle',
condition: { field: 'operation', value: ['get_statement', 'cancel_statement'] },
required: { field: 'operation', value: ['get_statement', 'cancel_statement'] },
},
{
id: 'partition',
title: 'Result Partition',
type: 'short-input',
placeholder: '0',
condition: { field: 'operation', value: 'get_statement' },
mode: 'advanced',
},
{
id: 'database',
title: 'Database',
type: 'short-input',
placeholder: 'ANALYTICS',
condition: {
field: 'operation',
value: [
...dataOperations,
...taskDefinitionOperations,
'introspect_schema',
'call_procedure',
],
},
required: {
field: 'operation',
value: [
...dataOperations,
...taskDefinitionOperations,
'introspect_schema',
'call_procedure',
],
},
},
{
id: 'schema',
title: 'Schema',
type: 'short-input',
placeholder: 'PUBLIC',
condition: {
field: 'operation',
value: [
...dataOperations,
...taskDefinitionOperations,
'introspect_schema',
'call_procedure',
],
},
required: {
field: 'operation',
value: [...dataOperations, ...taskDefinitionOperations, 'call_procedure'],
},
},
{
id: 'table',
title: 'Table',
type: 'short-input',
placeholder: 'EVENTS',
condition: { field: 'operation', value: [...dataOperations, 'introspect_schema'] },
required: { field: 'operation', value: dataOperations },
},
{
id: 'rows',
title: 'Rows',
type: 'code',
placeholder: '[{"id":1,"status":"active"}]',
condition: { field: 'operation', value: ['insert_rows', 'update_rows', 'upsert_rows'] },
required: { field: 'operation', value: ['insert_rows', 'update_rows', 'upsert_rows'] },
wandConfig: {
enabled: true,
prompt:
'Generate a non-empty JSON array of flat row objects. Every row must have the same keys and the batch must contain at most 1000 rows. Return ONLY the JSON array - no explanations, no extra text.',
placeholder: 'Describe the records to write...',
},
},
{
id: 'matchColumns',
title: 'Match Columns',
type: 'code',
placeholder: '["id"]',
condition: { field: 'operation', value: ['update_rows', 'upsert_rows'] },
required: { field: 'operation', value: ['update_rows', 'upsert_rows'] },
wandConfig: {
enabled: true,
prompt:
'Generate a JSON array containing the row column names that uniquely match target records, for example ["tenant_id","id"]. Return ONLY the JSON array - no explanations, no extra text.',
placeholder: 'Describe the columns that identify a row...',
},
},
{
id: 'filters',
title: 'Equality Filters',
type: 'code',
placeholder: '{"status":"expired","tenant_id":42}',
condition: { field: 'operation', value: 'delete_rows' },
required: { field: 'operation', value: 'delete_rows' },
wandConfig: {
enabled: true,
prompt:
'Generate a non-empty JSON object of column equality filters. All filters are combined with AND. Return ONLY the JSON object - no explanations, no extra text.',
placeholder: 'Describe the exact rows to delete...',
},
},
{
id: 'stagePath',
title: 'Stage Path',
type: 'short-input',
placeholder: '@RAW_STAGE/2026/08',
condition: { field: 'operation', value: 'load_data' },
required: { field: 'operation', value: 'load_data' },
},
{
id: 'fileFormat',
title: 'Named File Format',
type: 'short-input',
placeholder: 'ANALYTICS.PUBLIC.CSV_FORMAT',
condition: { field: 'operation', value: 'load_data' },
mode: 'advanced',
},
{
id: 'pattern',
title: 'File Pattern',
type: 'short-input',
placeholder: '.*[.]csv',
condition: { field: 'operation', value: 'load_data' },
mode: 'advanced',
},
{
id: 'onError',
title: 'On Error',
type: 'dropdown',
options: [
{ label: 'Abort Statement', id: 'ABORT_STATEMENT' },
{ label: 'Continue', id: 'CONTINUE' },
{ label: 'Skip File', id: 'SKIP_FILE' },
{ label: 'Skip File After Error Count', id: 'SKIP_FILE_NUMBER' },
{ label: 'Skip File After Error Percentage', id: 'SKIP_FILE_PERCENT' },
],
value: () => 'ABORT_STATEMENT',
condition: { field: 'operation', value: 'load_data' },
mode: 'advanced',
},
{
id: 'onErrorThreshold',
title: 'Skip File Threshold',
type: 'short-input',
placeholder: '10',
condition: { field: 'onError', value: ['SKIP_FILE_NUMBER', 'SKIP_FILE_PERCENT'] },
required: { field: 'onError', value: ['SKIP_FILE_NUMBER', 'SKIP_FILE_PERCENT'] },
mode: 'advanced',
},
{
id: 'purge',
title: 'Purge Loaded Files',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'load_data' },
mode: 'advanced',
},
{
id: 'force',
title: 'Force Reload',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'load_data' },
mode: 'advanced',
},
{
id: 'matchByColumnName',
title: 'Match by Column Name',
type: 'dropdown',
options: [
{ label: 'None', id: 'NONE' },
{ label: 'Case Sensitive', id: 'CASE_SENSITIVE' },
{ label: 'Case Insensitive', id: 'CASE_INSENSITIVE' },
],
value: () => 'NONE',
condition: { field: 'operation', value: 'load_data' },
mode: 'advanced',
},
{
id: 'warehouseName',
title: 'Warehouse Name',
type: 'short-input',
placeholder: 'COMPUTE_WH',
condition: {
field: 'operation',
value: ['get_warehouse', 'resume_warehouse', 'suspend_warehouse'],
},
required: {
field: 'operation',
value: ['get_warehouse', 'resume_warehouse', 'suspend_warehouse'],
},
},
{
id: 'nameLike',
title: 'Name Pattern',
type: 'short-input',
placeholder: 'ETL%',
condition: { field: 'operation', value: ['list_warehouses', 'list_tasks'] },
mode: 'advanced',
},
{
id: 'taskName',
title: 'Task Name',
type: 'short-input',
placeholder: 'DAILY_LOAD',
condition: {
field: 'operation',
value: ['get_task', 'run_task'],
},
required: { field: 'operation', value: ['get_task', 'run_task'] },
},
{
id: 'taskNameFilter',
title: 'Task Name Filter',
type: 'short-input',
placeholder: 'DAILY_LOAD',
condition: { field: 'operation', value: ['list_task_runs', 'get_task_run'] },
mode: 'advanced',
},
{
id: 'retryLast',
title: 'Retry Last Failed Run',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'run_task' },
mode: 'advanced',
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: '100',
condition: { field: 'operation', value: ['list_tasks', 'list_task_runs'] },
mode: 'advanced',
},
{
id: 'startTime',
title: 'Scheduled Time From',
type: 'short-input',
placeholder: '2026-08-01T00:00:00Z',
condition: { field: 'operation', value: ['list_task_runs', 'get_task_run'] },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt:
'Convert the requested start date and time to an ISO 8601 timestamp within the last seven days. Return ONLY the timestamp - no explanations, no extra text.',
placeholder: 'Describe the beginning of the task history window...',
generationType: 'timestamp',
},
},
{
id: 'endTime',
title: 'Scheduled Time To',
type: 'short-input',
placeholder: '2026-08-07T00:00:00Z',
condition: { field: 'operation', value: ['list_task_runs', 'get_task_run'] },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt:
'Convert the requested end date and time to an ISO 8601 timestamp. Return ONLY the timestamp - no explanations, no extra text.',
placeholder: 'Describe the end of the task history window...',
generationType: 'timestamp',
},
},
{
id: 'errorOnly',
title: 'Errors Only',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'list_task_runs' },
mode: 'advanced',
},
{
id: 'queryId',
title: 'Task Run Query ID',
type: 'short-input',
placeholder: 'Snowflake query UUID',
condition: {
field: 'operation',
value: ['get_task_run', 'cancel_task_run', 'get_task_run_output'],
},
required: {
field: 'operation',
value: ['get_task_run', 'cancel_task_run', 'get_task_run_output'],
},
},
{
id: 'includeViews',
title: 'Include Views',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'introspect_schema' },
mode: 'advanced',
},
{
id: 'procedureName',
title: 'Procedure Name',
type: 'short-input',
placeholder: 'REFRESH_MODEL',
condition: { field: 'operation', value: 'call_procedure' },
required: { field: 'operation', value: 'call_procedure' },
},
{
id: 'procedureArguments',
title: 'Procedure Arguments',
type: 'code',
placeholder: '[{"type":"TEXT","value":"daily"}]',
condition: { field: 'operation', value: 'call_procedure' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt:
'Generate an ordered JSON array of Snowflake procedure bindings. Each item must contain a supported Snowflake binding type and a string value, for example {"type":"TEXT","value":"daily"}. Return ONLY the JSON array - no explanations, no extra text.',
placeholder: 'Describe the procedure arguments in order...',
},
},
{
id: 'warehouse',
title: 'Execution Warehouse',
type: 'short-input',
placeholder: 'COMPUTE_WH',
condition: { field: 'operation', value: statementOperations },
mode: 'advanced',
},
{
id: 'contextDatabase',
title: 'Execution Database',
type: 'short-input',
placeholder: 'ANALYTICS',
condition: { field: 'operation', value: contextOnlyOperations },
mode: 'advanced',
},
{
id: 'contextSchema',
title: 'Execution Schema',
type: 'short-input',
placeholder: 'PUBLIC',
condition: { field: 'operation', value: contextOnlyOperations },
mode: 'advanced',
},
{
id: 'role',
title: 'Execution Role',
type: 'short-input',
placeholder: 'ANALYST',
condition: { field: 'operation', value: statementOperations },
mode: 'advanced',
},
{
id: 'timeout',
title: 'Timeout (seconds)',
type: 'short-input',
placeholder: '60',
condition: { field: 'operation', value: statementOperations },
mode: 'advanced',
},
{
id: 'maxRows',
title: 'Maximum Rows',
type: 'short-input',
placeholder: '1000',
condition: {
field: 'operation',
value: [...statementOperations, 'get_statement'],
},
mode: 'advanced',
},
{
id: 'host',
title: 'Account Host',
type: 'short-input',
placeholder: 'myorg-myaccount.snowflakecomputing.com',
required: true,
},
{
id: 'apiKey',
title: 'Programmatic Access Token',
type: 'short-input',
placeholder: 'Enter your Snowflake PAT',
password: true,
required: true,
},
],
tools: {
access: [
'snowflake_execute_sql',
'snowflake_get_statement',
'snowflake_cancel_statement',
'snowflake_insert_rows',
'snowflake_update_rows',
'snowflake_upsert_rows',
'snowflake_delete_rows',
'snowflake_load_data',
'snowflake_list_warehouses',
'snowflake_get_warehouse',
'snowflake_resume_warehouse',
'snowflake_suspend_warehouse',
'snowflake_list_tasks',
'snowflake_get_task',
'snowflake_run_task',
'snowflake_list_task_runs',
'snowflake_get_task_run',
'snowflake_cancel_task_run',
'snowflake_get_task_run_output',
'snowflake_introspect_schema',
'snowflake_call_procedure',
],
config: {
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),
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),
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
host: { type: 'string', description: 'Snowflake account hostname' },
apiKey: { type: 'string', description: 'Snowflake programmatic access token' },
statement: { type: 'string', description: 'Single SQL statement' },
bindings: {
type: 'string',
description: 'Typed SQL bindings as a JSON object keyed by 1-based positions',
},
async: { type: 'boolean', description: 'Run the SQL statement asynchronously' },
statementHandle: { type: 'string', description: 'Snowflake SQL API statement handle' },
partition: { type: 'number', description: 'Zero-based result partition' },
database: { type: 'string', description: 'Snowflake database name' },
schema: { type: 'string', description: 'Snowflake schema name' },
table: { type: 'string', description: 'Snowflake table name' },
rows: { type: 'string', description: 'Structured rows as a JSON array' },
matchColumns: { type: 'string', description: 'Match columns as a JSON array' },
filters: { type: 'string', description: 'Equality filters as a JSON object' },
stagePath: { type: 'string', description: 'Existing Snowflake stage path' },
fileFormat: { type: 'string', description: 'Named Snowflake file format' },
pattern: { type: 'string', description: 'COPY file selection pattern' },
onError: { type: 'string', description: 'COPY error handling policy' },
onErrorThreshold: { type: 'number', description: 'COPY skip-file error threshold' },
purge: { type: 'boolean', description: 'Purge successfully loaded staged files' },
force: { type: 'boolean', description: 'Force staged files to load again' },
matchByColumnName: { type: 'string', description: 'COPY column-name matching policy' },
warehouseName: { type: 'string', description: 'Warehouse to retrieve or change' },
nameLike: { type: 'string', description: 'SQL LIKE pattern for object names' },
taskName: { type: 'string', description: 'Snowflake task name' },
taskNameFilter: { type: 'string', description: 'Optional task history name filter' },
retryLast: { type: 'boolean', description: 'Retry the last failed task graph' },
limit: { type: 'number', description: 'Maximum list results' },
startTime: { type: 'string', description: 'Task history start timestamp' },
endTime: { type: 'string', description: 'Task history end timestamp' },
errorOnly: { type: 'boolean', description: 'Only return failed task runs' },
queryId: { type: 'string', description: 'Task run query UUID' },
includeViews: { type: 'boolean', description: 'Include views in schema introspection' },
procedureName: { type: 'string', description: 'Stored procedure name' },
procedureArguments: {
type: 'string',
description: 'Typed procedure bindings as an ordered JSON array of type/value objects',
},
warehouse: { type: 'string', description: 'Statement execution warehouse' },
contextDatabase: { type: 'string', description: 'Optional statement database context' },
contextSchema: { type: 'string', description: 'Optional statement schema context' },
role: { type: 'string', description: 'Statement execution role' },
timeout: { type: 'number', description: 'Statement timeout in seconds' },
maxRows: { type: 'number', description: 'Maximum result rows, up to 10000' },
},
outputs: {
statementHandle: { type: 'string', description: 'Snowflake statement handle' },
status: { type: 'string', description: 'SUCCEEDED, RUNNING, or CANCELED' },
code: { type: 'string', description: 'Snowflake response code' },
sqlState: { type: 'string', description: 'Snowflake SQLSTATE code' },
message: { type: 'string', description: 'Snowflake response message' },
columns: { type: 'json', description: 'Typed Snowflake result column metadata' },
rows: { type: 'json', description: 'One result partition as string/null arrays' },
totalRows: { type: 'number', description: 'Total result rows' },
partitions: { type: 'json', description: 'Available result partition metadata' },
currentPartition: { type: 'number', description: 'Returned partition number' },
nextPartition: { type: 'number', description: 'Next partition number' },
truncated: { type: 'boolean', description: 'Whether more result rows or partitions remain' },
rowsInserted: { type: 'number', description: 'Rows inserted' },
rowsUpdated: { type: 'number', description: 'Rows updated' },
rowsDeleted: { type: 'number', description: 'Rows deleted' },
duplicateRowsUpdated: { type: 'number', description: 'Duplicate rows updated' },
rowsAffected: { type: 'number', description: 'Total rows inserted, updated, and deleted' },
},
}
export const SnowflakeBlockMeta = {
tags: ['data-warehouse', 'data-analytics', 'cloud'],
url: 'https://www.snowflake.com',
templates: [
{
icon: SnowflakeIcon,
title: 'Snowflake asynchronous reporting',
prompt:
'Build a scheduled workflow that starts a long-running Snowflake report asynchronously, polls its statement handle until complete, summarizes the result, and posts the report to Slack.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['reporting', 'automation'],
alsoIntegrations: ['slack'],
},
{
icon: SnowflakeIcon,
title: 'Snowflake incremental upsert',
prompt:
'Create a workflow that receives changed customer records, validates their keys, and upserts them into a Snowflake dimension table in bounded batches.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['sync', 'automation'],
},
{
icon: SnowflakeIcon,
title: 'Snowflake staged ingestion',
prompt:
'Build a workflow that loads newly staged CSV files into a Snowflake table with a named file format, records COPY results, and alerts on rejected files.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['data', 'monitoring'],
},
{
icon: SnowflakeIcon,
title: 'Snowflake task failure monitor',
prompt:
'Create a workflow that checks recent Snowflake task failures, retrieves the failed run details, summarizes the likely cause, and posts an actionable Slack alert.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['monitoring', 'devops'],
alsoIntegrations: ['slack'],
},
{
icon: SnowflakeIcon,
title: 'Snowflake warehouse cost control',
prompt:
'Build a scheduled workflow that reviews Snowflake warehouse state and suspends an approved idle warehouse after notifying the data platform channel.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['finance', 'devops'],
alsoIntegrations: ['slack'],
},
{
icon: SnowflakeIcon,
title: 'Snowflake schema-drift audit',
prompt:
'Create a daily workflow that introspects a Snowflake schema, compares its tables and columns with the previous snapshot, and reports breaking schema changes.',
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'engineering',
tags: ['monitoring', 'analysis'],
},
{
icon: SnowflakeIcon,
title: 'Snowflake procedure orchestrator',
prompt:
'Build a workflow that validates typed arguments, calls a Snowflake stored procedure, captures its output, and routes failures for operator review.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['automation', 'devops'],
},
],
skills: [
{
name: 'query-snowflake-data',
description: 'Run a bounded parameterized Snowflake query and summarize its result.',
content:
'# Query Snowflake Data\n\n## Steps\n1. Confirm the warehouse and SQL statement.\n2. Use typed bindings for values.\n3. Execute the statement with a bounded row limit.\n4. If it is asynchronous, poll the handle and request only the needed partition.\n\n## Output\nReturn the rows, column meanings, and a concise summary.',
},
{
name: 'sync-snowflake-rows',
description: 'Insert, update, or upsert structured records safely in Snowflake.',
content:
'# Synchronize Snowflake Rows\n\n## Steps\n1. Confirm the target table and record keys.\n2. Keep batches at or below 1000 records.\n3. Choose insert, update, or upsert and provide match columns when needed.\n4. Report Snowflake DML statistics.\n\n## Output\nReturn inserted, updated, deleted, and total affected row counts.',
},
{
name: 'load-snowflake-stage',
description: 'Load files already present in a Snowflake stage into a table.',
content:
'# Load a Snowflake Stage\n\n## Steps\n1. Confirm the target table and existing stage path.\n2. Select the named file format and COPY options.\n3. Run the staged load.\n4. Review every returned file status before reporting success.\n\n## Output\nSummarize loaded and rejected files and any COPY errors.',
},
{
name: 'monitor-snowflake-tasks',
description: 'Inspect Snowflake task history and diagnose failed runs.',
content:
'# Monitor Snowflake Tasks\n\n## Steps\n1. Query the relevant time window within seven days.\n2. Filter to failures when appropriate.\n3. Get the selected run by query ID.\n4. Retrieve output only when it remains available and access permits it.\n\n## Output\nReturn run state, timing, query ID, and a failure summary.',
},
{
name: 'control-snowflake-warehouse',
description: 'Inspect and deliberately resume or suspend a Snowflake warehouse.',
content:
'# Control a Snowflake Warehouse\n\n## Steps\n1. List or describe the target warehouse.\n2. Confirm its name and current state.\n3. Resume or suspend only when requested.\n4. Verify the statement completed.\n\n## Output\nReturn the warehouse name, prior state, requested action, and result.',
},
{
name: 'audit-snowflake-schema',
description: 'Inspect Snowflake tables and columns for schema review or drift detection.',
content:
'# Audit a Snowflake Schema\n\n## Steps\n1. Choose a database and optional schema or table filter.\n2. Decide whether views belong in scope.\n3. Introspect the bounded metadata result.\n4. Compare names, types, nullability, and ordering.\n\n## Output\nReturn the schema inventory and notable compatibility risks.',
},
{
name: 'call-snowflake-procedure',
description: 'Call a Snowflake stored procedure with explicitly typed arguments.',
content:
'# Call a Snowflake Procedure\n\n## Steps\n1. Confirm the fully qualified procedure and argument order.\n2. Assign each argument an explicit Snowflake binding type and string value.\n3. Execute the call and inspect its result.\n\n## Output\nReturn the procedure result and statement handle.',
},
],
} as const satisfies BlockMeta
+3
View File
@@ -284,6 +284,7 @@ import { SixtyfourBlock, SixtyfourBlockMeta } from '@/blocks/blocks/sixtyfour'
import { SlackBlock, SlackBlockMeta, SlackV2Block } from '@/blocks/blocks/slack'
import { SmartleadBlock, SmartleadBlockMeta } from '@/blocks/blocks/smartlead'
import { SmtpBlock, SmtpBlockMeta } from '@/blocks/blocks/smtp'
import { SnowflakeBlock, SnowflakeBlockMeta } from '@/blocks/blocks/snowflake'
import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks'
import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify'
import { SQSBlock, SQSBlockMeta } from '@/blocks/blocks/sqs'
@@ -607,6 +608,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
slack_v2: SlackV2Block,
smartlead: SmartleadBlock,
smtp: SmtpBlock,
snowflake: SnowflakeBlock,
sportmonks: SportmonksBlock,
spotify: SpotifyBlock,
sqs: SQSBlock,
@@ -895,6 +897,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
slack: SlackBlockMeta,
smartlead: SmartleadBlockMeta,
smtp: SmtpBlockMeta,
snowflake: SnowflakeBlockMeta,
sportmonks: SportmonksBlockMeta,
spotify: SpotifyBlockMeta,
sqs: SQSBlockMeta,
@@ -209,6 +209,7 @@ import {
SlackIcon,
SmartleadIcon,
SmtpIcon,
SnowflakeIcon,
SportmonksIcon,
SQSIcon,
SquareIcon,
@@ -476,6 +477,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
slack: SlackIcon,
smartlead: SmartleadIcon,
smtp: SmtpIcon,
snowflake: SnowflakeIcon,
sportmonks: SportmonksIcon,
sqs: SQSIcon,
square: SquareIcon,
+103
View File
@@ -18151,6 +18151,109 @@
"integrationType": "email",
"tags": ["messaging", "automation"]
},
{
"type": "snowflake",
"slug": "snowflake",
"name": "Snowflake",
"description": "Query data and manage warehouses and tasks in Snowflake",
"longDescription": "Connect with a Snowflake programmatic access token to execute SQL, synchronize structured rows, load staged data, manage warehouses and tasks, inspect schemas, and call stored procedures.",
"bgColor": "#F7FBFD",
"iconName": "SnowflakeIcon",
"docsUrl": "https://docs.sim.ai/integrations/snowflake",
"operations": [
{
"name": "Execute SQL",
"description": "Execute one parameterized SQL statement through the Snowflake SQL API."
},
{
"name": "Get Statement",
"description": "Check a running or completed statement and retrieve exactly one result partition. Canceled or failed statements are returned as errors."
},
{
"name": "Cancel Statement",
"description": "Cancel a running Snowflake SQL API statement."
},
{
"name": "Insert Rows",
"description": "Insert up to 1000 structured JSON rows using bound values."
},
{
"name": "Update Rows",
"description": "Update matching rows with a bound MERGE statement without inserting new rows."
},
{
"name": "Upsert Rows",
"description": "Update matching rows and insert unmatched rows with a bound MERGE statement."
},
{
"name": "Delete Rows",
"description": "Delete rows matching a required set of bound equality filters."
},
{
"name": "Load Data",
"description": "Load files from an existing Snowflake stage with COPY INTO."
},
{
"name": "List Warehouses",
"description": "List warehouses visible to the active Snowflake role."
},
{
"name": "Get Warehouse",
"description": "Get the full details for a Snowflake virtual warehouse."
},
{
"name": "Resume Warehouse",
"description": "Resume a Snowflake virtual warehouse if it is suspended."
},
{
"name": "Suspend Warehouse",
"description": "Suspend a Snowflake virtual warehouse."
},
{
"name": "List Tasks",
"description": "List tasks in a Snowflake schema."
},
{
"name": "Get Task",
"description": "Describe a Snowflake task."
},
{
"name": "Run Task",
"description": "Run a Snowflake task immediately, optionally retrying its last failed graph."
},
{
"name": "List Task Runs",
"description": "Query up to seven days of Snowflake task history, capped at 10000 rows."
},
{
"name": "Get Task Run",
"description": "Find one task history record by query ID within Snowflake’s seven-day window and 10000 most recent records after optional filters."
},
{
"name": "Cancel Task Run",
"description": "Cancel a running task query with SYSTEM$CANCEL_QUERY."
},
{
"name": "Get Task Run Output",
"description": "Read a task query result with RESULT_SCAN during Snowflake’s 24-hour retention window using the task owner role."
},
{
"name": "Introspect Schema",
"description": "Inspect table and column metadata through Snowflake INFORMATION_SCHEMA views."
},
{
"name": "Call Procedure",
"description": "Call a stored procedure with explicitly typed Snowflake bindings."
}
],
"operationCount": 21,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationType": "databases",
"tags": ["data-warehouse", "data-analytics", "cloud"]
},
{
"type": "sportmonks",
"slug": "sportmonks",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+44
View File
@@ -3797,6 +3797,29 @@ import {
} from '@/tools/smartlead'
import { smsSendTool } from '@/tools/sms'
import { smtpSendMailTool } from '@/tools/smtp'
import {
snowflakeCallProcedureTool,
snowflakeCancelStatementTool,
snowflakeCancelTaskRunTool,
snowflakeDeleteRowsTool,
snowflakeExecuteSqlTool,
snowflakeGetStatementTool,
snowflakeGetTaskRunOutputTool,
snowflakeGetTaskRunTool,
snowflakeGetTaskTool,
snowflakeGetWarehouseTool,
snowflakeInsertRowsTool,
snowflakeIntrospectSchemaTool,
snowflakeListTaskRunsTool,
snowflakeListTasksTool,
snowflakeListWarehousesTool,
snowflakeLoadDataTool,
snowflakeResumeWarehouseTool,
snowflakeRunTaskTool,
snowflakeSuspendWarehouseTool,
snowflakeUpdateRowsTool,
snowflakeUpsertRowsTool,
} from '@/tools/snowflake'
import {
sportmonksCoreGetCitiesTool,
sportmonksCoreGetCityTool,
@@ -5366,6 +5389,27 @@ export const tools: Record<string, ToolConfig> = {
sendgrid_delete_template: sendGridDeleteTemplateTool,
sendgrid_create_template_version: sendGridCreateTemplateVersionTool,
smtp_send_mail: smtpSendMailTool,
snowflake_call_procedure: snowflakeCallProcedureTool,
snowflake_cancel_statement: snowflakeCancelStatementTool,
snowflake_cancel_task_run: snowflakeCancelTaskRunTool,
snowflake_delete_rows: snowflakeDeleteRowsTool,
snowflake_execute_sql: snowflakeExecuteSqlTool,
snowflake_get_statement: snowflakeGetStatementTool,
snowflake_get_task: snowflakeGetTaskTool,
snowflake_get_task_run: snowflakeGetTaskRunTool,
snowflake_get_task_run_output: snowflakeGetTaskRunOutputTool,
snowflake_get_warehouse: snowflakeGetWarehouseTool,
snowflake_insert_rows: snowflakeInsertRowsTool,
snowflake_introspect_schema: snowflakeIntrospectSchemaTool,
snowflake_list_task_runs: snowflakeListTaskRunsTool,
snowflake_list_tasks: snowflakeListTasksTool,
snowflake_list_warehouses: snowflakeListWarehousesTool,
snowflake_load_data: snowflakeLoadDataTool,
snowflake_resume_warehouse: snowflakeResumeWarehouseTool,
snowflake_run_task: snowflakeRunTaskTool,
snowflake_suspend_warehouse: snowflakeSuspendWarehouseTool,
snowflake_update_rows: snowflakeUpdateRowsTool,
snowflake_upsert_rows: snowflakeUpsertRowsTool,
sportmonks_football_expected_by_player: sportmonksExpectedByPlayerTool,
sportmonks_football_expected_by_team: sportmonksExpectedByTeamTool,
sportmonks_football_get_all_commentaries: sportmonksGetAllCommentariesTool,
@@ -0,0 +1,70 @@
import { buildCallProcedure } from '@/tools/snowflake/sql'
import type {
SnowflakeCallProcedureParams,
SnowflakeCallProcedureResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const callProcedureTool: ToolConfig<
SnowflakeCallProcedureParams,
SnowflakeCallProcedureResponse
> = {
id: 'snowflake_call_procedure',
version: '1.0.0',
name: 'Snowflake Call Procedure',
description: 'Call a stored procedure with explicitly typed Snowflake bindings.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
procedureName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Stored procedure name',
},
procedureArguments: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description: 'Ordered argument bindings with a Snowflake type and string value',
items: {
type: 'object',
required: ['type', 'value'],
additionalProperties: false,
properties: {
type: { type: 'string', description: 'Snowflake binding type' },
value: { type: 'string', description: 'Snowflake binding value' },
},
},
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildCallProcedure(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
@@ -0,0 +1,40 @@
import type {
SnowflakeCancelStatementParams,
SnowflakeCancelStatementResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const cancelStatementTool: ToolConfig<
SnowflakeCancelStatementParams,
SnowflakeCancelStatementResponse
> = {
id: 'snowflake_cancel_statement',
name: 'Snowflake Cancel Statement',
description: 'Cancel a running Snowflake SQL API statement.',
version: '1.0.0',
params: {
...snowflakeBaseParams,
statementHandle: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Statement handle returned by Snowflake',
},
},
request: {
url: (params) =>
`${normalizeSnowflakeHost(params.host)}/api/v2/statements/${encodeURIComponent(params.statementHandle.trim())}/cancel`,
method: 'POST',
headers: getSnowflakeHeaders,
},
transformResponse: (response, params) =>
transformSnowflakeResponse(response, 0, 1, true, params?.statementHandle.trim()),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
@@ -0,0 +1,42 @@
import { buildCancelTaskRun } from '@/tools/snowflake/sql'
import type {
SnowflakeCancelTaskRunResponse,
SnowflakeTaskRunParams,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const cancelTaskRunTool: ToolConfig<SnowflakeTaskRunParams, SnowflakeCancelTaskRunResponse> =
{
id: 'snowflake_cancel_task_run',
version: '1.0.0',
name: 'Snowflake Cancel Task Run',
description: 'Cancel a running task query with SYSTEM$CANCEL_QUERY.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
queryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Running task query ID from TASK_HISTORY',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildCancelTaskRun(params)),
},
transformResponse: (response, params) =>
transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+58
View File
@@ -0,0 +1,58 @@
import { buildDeleteRows } from '@/tools/snowflake/sql'
import type {
SnowflakeDeleteRowsParams,
SnowflakeDeleteRowsResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteRowsTool: ToolConfig<SnowflakeDeleteRowsParams, SnowflakeDeleteRowsResponse> = {
id: 'snowflake_delete_rows',
version: '1.0.0',
name: 'Snowflake Delete Rows',
description: 'Delete rows matching a required set of bound equality filters.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target Snowflake table name within the selected database and schema context',
},
filters: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Non-empty JSON object of column equality filters combined with AND',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildDeleteRows(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+57
View File
@@ -0,0 +1,57 @@
import { normalizeBindings } from '@/tools/snowflake/sql'
import type {
SnowflakeExecuteSqlParams,
SnowflakeExecuteSqlResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const executeSqlTool: ToolConfig<SnowflakeExecuteSqlParams, SnowflakeExecuteSqlResponse> = {
id: 'snowflake_execute_sql',
version: '1.0.0',
name: 'Snowflake Execute SQL',
description: 'Execute one parameterized SQL statement through the Snowflake SQL API.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
statement: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'One Snowflake SQL statement to execute',
},
bindings: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Snowflake bindings keyed by 1-based position, each with type and string value',
},
async: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Return immediately with a statement handle',
},
},
request: {
url: (params) =>
`${normalizeSnowflakeHost(params.host)}/api/v2/statements${params.async === true ? '?async=true' : ''}`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) =>
buildSnowflakeStatementBody(params, {
statement: params.statement,
bindings: normalizeBindings(params.bindings),
}),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+71
View File
@@ -0,0 +1,71 @@
import type {
SnowflakeGetStatementParams,
SnowflakeGetStatementResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
getSnowflakeHeaders,
normalizeMaxRows,
normalizeSnowflakeHost,
snowflakeBaseParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
function partitionNumber(value?: number): number {
const partition = value ?? 0
if (!Number.isInteger(partition) || partition < 0) {
throw new Error('partition must be a non-negative integer')
}
return partition
}
export const getStatementTool: ToolConfig<
SnowflakeGetStatementParams,
SnowflakeGetStatementResponse
> = {
id: 'snowflake_get_statement',
name: 'Snowflake Get Statement',
description:
'Check a running or completed statement and retrieve exactly one result partition. Canceled or failed statements are returned as errors.',
version: '1.0.0',
params: {
...snowflakeBaseParams,
statementHandle: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Statement handle returned by Snowflake',
},
partition: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Zero-based result partition to retrieve; defaults to 0',
},
maxRows: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum rows returned from this partition, from 1 to 10000',
},
},
request: {
url: (params) => {
const partition = partitionNumber(params.partition)
normalizeMaxRows(params.maxRows)
return `${normalizeSnowflakeHost(params.host)}/api/v2/statements/${encodeURIComponent(params.statementHandle.trim())}?partition=${partition}`
},
method: 'GET',
headers: getSnowflakeHeaders,
},
transformResponse: (response, params) =>
transformSnowflakeResponse(
response,
partitionNumber(params?.partition),
params?.maxRows,
false,
params?.statementHandle.trim()
),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+49
View File
@@ -0,0 +1,49 @@
import { buildGetTask } from '@/tools/snowflake/sql'
import type { SnowflakeGetTaskResponse, SnowflakeTaskParams } from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const getTaskTool: ToolConfig<SnowflakeTaskParams, SnowflakeGetTaskResponse> = {
id: 'snowflake_get_task',
version: '1.0.0',
name: 'Snowflake Get Task',
description: 'Describe a Snowflake task.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
taskName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Task name',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildGetTask(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+56
View File
@@ -0,0 +1,56 @@
import { buildGetTaskRun } from '@/tools/snowflake/sql'
import type { SnowflakeGetTaskRunResponse, SnowflakeTaskRunParams } from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const getTaskRunTool: ToolConfig<SnowflakeTaskRunParams, SnowflakeGetTaskRunResponse> = {
id: 'snowflake_get_task_run',
version: '1.0.0',
name: 'Snowflake Get Task Run',
description:
'Find one task history record by query ID within Snowflake’s seven-day window and 10000 most recent records after optional filters.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
queryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Task run query ID from TASK_HISTORY',
},
taskName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional task name used to narrow the 10000-record history window',
},
startTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional inclusive ISO timestamp within the last seven days',
},
endTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional exclusive ISO timestamp within the last seven days',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildGetTaskRun(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
@@ -0,0 +1,45 @@
import { buildGetTaskRunOutput } from '@/tools/snowflake/sql'
import type {
SnowflakeGetTaskRunOutputResponse,
SnowflakeTaskRunParams,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const getTaskRunOutputTool: ToolConfig<
SnowflakeTaskRunParams,
SnowflakeGetTaskRunOutputResponse
> = {
id: 'snowflake_get_task_run_output',
version: '1.0.0',
name: 'Snowflake Get Task Run Output',
description:
'Read a task query result with RESULT_SCAN during Snowflake’s 24-hour retention window using the task owner role.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
queryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Completed task query ID; task results require the task owner role, while manual query results require the same user',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildGetTaskRunOutput(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+42
View File
@@ -0,0 +1,42 @@
import { buildGetWarehouse } from '@/tools/snowflake/sql'
import type {
SnowflakeGetWarehouseResponse,
SnowflakeWarehouseParams,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const getWarehouseTool: ToolConfig<SnowflakeWarehouseParams, SnowflakeGetWarehouseResponse> =
{
id: 'snowflake_get_warehouse',
version: '1.0.0',
name: 'Snowflake Get Warehouse',
description: 'Get the full details for a Snowflake virtual warehouse.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
warehouseName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Warehouse name',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildGetWarehouse(params)),
},
transformResponse: (response, params) =>
transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+45
View File
@@ -0,0 +1,45 @@
import { callProcedureTool } from '@/tools/snowflake/call_procedure'
import { cancelStatementTool } from '@/tools/snowflake/cancel_statement'
import { cancelTaskRunTool } from '@/tools/snowflake/cancel_task_run'
import { deleteRowsTool } from '@/tools/snowflake/delete_rows'
import { executeSqlTool } from '@/tools/snowflake/execute_sql'
import { getStatementTool } from '@/tools/snowflake/get_statement'
import { getTaskTool } from '@/tools/snowflake/get_task'
import { getTaskRunTool } from '@/tools/snowflake/get_task_run'
import { getTaskRunOutputTool } from '@/tools/snowflake/get_task_run_output'
import { getWarehouseTool } from '@/tools/snowflake/get_warehouse'
import { insertRowsTool } from '@/tools/snowflake/insert_rows'
import { introspectSchemaTool } from '@/tools/snowflake/introspect_schema'
import { listTaskRunsTool } from '@/tools/snowflake/list_task_runs'
import { listTasksTool } from '@/tools/snowflake/list_tasks'
import { listWarehousesTool } from '@/tools/snowflake/list_warehouses'
import { loadDataTool } from '@/tools/snowflake/load_data'
import { resumeWarehouseTool } from '@/tools/snowflake/resume_warehouse'
import { runTaskTool } from '@/tools/snowflake/run_task'
import { suspendWarehouseTool } from '@/tools/snowflake/suspend_warehouse'
import { updateRowsTool } from '@/tools/snowflake/update_rows'
import { upsertRowsTool } from '@/tools/snowflake/upsert_rows'
export * from '@/tools/snowflake/types'
export const snowflakeCallProcedureTool = callProcedureTool
export const snowflakeCancelStatementTool = cancelStatementTool
export const snowflakeCancelTaskRunTool = cancelTaskRunTool
export const snowflakeDeleteRowsTool = deleteRowsTool
export const snowflakeExecuteSqlTool = executeSqlTool
export const snowflakeGetStatementTool = getStatementTool
export const snowflakeGetTaskTool = getTaskTool
export const snowflakeGetTaskRunTool = getTaskRunTool
export const snowflakeGetTaskRunOutputTool = getTaskRunOutputTool
export const snowflakeGetWarehouseTool = getWarehouseTool
export const snowflakeInsertRowsTool = insertRowsTool
export const snowflakeIntrospectSchemaTool = introspectSchemaTool
export const snowflakeListTaskRunsTool = listTaskRunsTool
export const snowflakeListTasksTool = listTasksTool
export const snowflakeListWarehousesTool = listWarehousesTool
export const snowflakeLoadDataTool = loadDataTool
export const snowflakeResumeWarehouseTool = resumeWarehouseTool
export const snowflakeRunTaskTool = runTaskTool
export const snowflakeSuspendWarehouseTool = suspendWarehouseTool
export const snowflakeUpdateRowsTool = updateRowsTool
export const snowflakeUpsertRowsTool = upsertRowsTool
+58
View File
@@ -0,0 +1,58 @@
import { buildInsertRows } from '@/tools/snowflake/sql'
import type {
SnowflakeInsertRowsParams,
SnowflakeInsertRowsResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const insertRowsTool: ToolConfig<SnowflakeInsertRowsParams, SnowflakeInsertRowsResponse> = {
id: 'snowflake_insert_rows',
version: '1.0.0',
name: 'Snowflake Insert Rows',
description: 'Insert up to 1000 structured JSON rows using bound values.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target Snowflake table name within the selected database and schema context',
},
rows: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Non-empty JSON array of row objects with matching keys',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildInsertRows(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
@@ -0,0 +1,61 @@
import { buildIntrospectSchema } from '@/tools/snowflake/sql'
import type {
SnowflakeIntrospectSchemaParams,
SnowflakeIntrospectSchemaResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const introspectSchemaTool: ToolConfig<
SnowflakeIntrospectSchemaParams,
SnowflakeIntrospectSchemaResponse
> = {
id: 'snowflake_introspect_schema',
version: '1.0.0',
name: 'Snowflake Introspect Schema',
description: 'Inspect table and column metadata through Snowflake INFORMATION_SCHEMA views.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database containing the INFORMATION_SCHEMA views',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional exact schema name filter',
},
table: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional exact table name filter',
},
includeViews: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include views as well as base tables',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildIntrospectSchema(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
@@ -0,0 +1,72 @@
import { buildListTaskRuns } from '@/tools/snowflake/sql'
import type {
SnowflakeListTaskRunsParams,
SnowflakeListTaskRunsResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const listTaskRunsTool: ToolConfig<
SnowflakeListTaskRunsParams,
SnowflakeListTaskRunsResponse
> = {
id: 'snowflake_list_task_runs',
version: '1.0.0',
name: 'Snowflake List Task Runs',
description: 'Query up to seven days of Snowflake task history, capped at 10000 rows.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
taskName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional task name filter',
},
startTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional inclusive ISO timestamp within the last seven days',
},
endTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional exclusive ISO timestamp within the last seven days',
},
errorOnly: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Return only failed task runs',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum task runs, from 1 to 10000',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) =>
buildSnowflakeStatementBody(
{ ...params, maxRows: params.maxRows ?? params.limit },
buildListTaskRuns(params)
),
},
transformResponse: (response, params) =>
transformSnowflakeResponse(response, 0, params?.maxRows ?? params?.limit),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+60
View File
@@ -0,0 +1,60 @@
import { buildListTasks } from '@/tools/snowflake/sql'
import type { SnowflakeListTasksParams, SnowflakeListTasksResponse } from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const listTasksTool: ToolConfig<SnowflakeListTasksParams, SnowflakeListTasksResponse> = {
id: 'snowflake_list_tasks',
version: '1.0.0',
name: 'Snowflake List Tasks',
description: 'List tasks in a Snowflake schema.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
nameLike: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional SQL LIKE pattern for task names',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum task rows, from 1 to 10000',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) =>
buildSnowflakeStatementBody(
{ ...params, maxRows: params.maxRows ?? params.limit },
buildListTasks(params)
),
},
transformResponse: (response, params) =>
transformSnowflakeResponse(response, 0, params?.maxRows ?? params?.limit),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
@@ -0,0 +1,43 @@
import { buildListWarehouses } from '@/tools/snowflake/sql'
import type {
SnowflakeListWarehousesParams,
SnowflakeListWarehousesResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const listWarehousesTool: ToolConfig<
SnowflakeListWarehousesParams,
SnowflakeListWarehousesResponse
> = {
id: 'snowflake_list_warehouses',
version: '1.0.0',
name: 'Snowflake List Warehouses',
description: 'List warehouses visible to the active Snowflake role.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
nameLike: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional SQL LIKE pattern for warehouse names',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildListWarehouses(params.nameLike)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+92
View File
@@ -0,0 +1,92 @@
import { buildLoadData } from '@/tools/snowflake/sql'
import type { SnowflakeLoadDataParams, SnowflakeLoadDataResponse } from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const loadDataTool: ToolConfig<SnowflakeLoadDataParams, SnowflakeLoadDataResponse> = {
id: 'snowflake_load_data',
version: '1.0.0',
name: 'Snowflake Load Data',
description: 'Load files from an existing Snowflake stage with COPY INTO.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target schema name',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target table name',
},
stagePath: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Existing stage path, for example @my_stage/path',
},
fileFormat: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional named file format',
},
pattern: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional regular expression used to select staged files',
},
onError: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'COPY error handling: ABORT_STATEMENT, CONTINUE, SKIP_FILE, SKIP_FILE_<count>, or SKIP_FILE_<percent>%',
},
purge: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Remove successfully loaded files from the stage',
},
force: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Reload files even when Snowflake has loaded them before',
},
matchByColumnName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'CASE_SENSITIVE, CASE_INSENSITIVE, or NONE',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildLoadData(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
@@ -0,0 +1,43 @@
import { buildResumeWarehouse } from '@/tools/snowflake/sql'
import type {
SnowflakeResumeWarehouseResponse,
SnowflakeWarehouseParams,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const resumeWarehouseTool: ToolConfig<
SnowflakeWarehouseParams,
SnowflakeResumeWarehouseResponse
> = {
id: 'snowflake_resume_warehouse',
version: '1.0.0',
name: 'Snowflake Resume Warehouse',
description: 'Resume a Snowflake virtual warehouse if it is suspended.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
warehouseName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Warehouse name',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildResumeWarehouse(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+55
View File
@@ -0,0 +1,55 @@
import { buildRunTask } from '@/tools/snowflake/sql'
import type { SnowflakeRunTaskParams, SnowflakeRunTaskResponse } from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const runTaskTool: ToolConfig<SnowflakeRunTaskParams, SnowflakeRunTaskResponse> = {
id: 'snowflake_run_task',
version: '1.0.0',
name: 'Snowflake Run Task',
description: 'Run a Snowflake task immediately, optionally retrying its last failed graph.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
taskName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Task name',
},
retryLast: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Retry the last failed task graph run',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildRunTask(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+329
View File
@@ -0,0 +1,329 @@
import { describe, expect, it } from 'vitest'
import { SnowflakeBlock } from '@/blocks/blocks/snowflake'
import {
buildCallProcedure,
buildCancelTaskRun,
buildDeleteRows,
buildGetTask,
buildGetTaskRun,
buildGetTaskRunOutput,
buildGetWarehouse,
buildInsertRows,
buildIntrospectSchema,
buildListTaskRuns,
buildListTasks,
buildListWarehouses,
buildLoadData,
buildResumeWarehouse,
buildRunTask,
buildSuspendWarehouse,
buildUpdateRows,
buildUpsertRows,
identifier,
normalizeBindings,
qualifiedIdentifier,
} from '@/tools/snowflake/sql'
import { MAX_REQUEST_BYTES, MAX_WRITE_ROWS } from '@/tools/snowflake/utils'
const context = { host: 'acme.snowflakecomputing.com', apiKey: 'secret' }
const table = { ...context, database: 'ANALYTICS', schema: 'PUBLIC', table: 'EVENTS' }
const queryId = '01b71944-0301-b428-0000-69f706bf0001'
describe('Snowflake SQL builders', () => {
it('accepts safe identifiers and rejects SQL fragments', () => {
expect(identifier('safe_name')).toBe('safe_name')
expect(identifier('"Case Sensitive"')).toBe('"Case Sensitive"')
expect(qualifiedIdentifier('DB', 'SCHEMA', 'TABLE')).toBe('DB.SCHEMA.TABLE')
expect(() => identifier('users; DROP TABLE users')).toThrow('Invalid Snowflake identifier')
})
it('validates explicit bindings', () => {
expect(normalizeBindings({ '1': { type: 'DATE', value: '2026-01-01' } })).toEqual({
'1': { type: 'DATE', value: '2026-01-01' },
})
expect(() => normalizeBindings({ '0': { type: 'TEXT', value: 'x' } })).toThrow('positive')
expect(() => normalizeBindings({ '1': { type: 'NOPE', value: 'x' } } as never)).toThrow(
'Unsupported'
)
expect(() =>
normalizeBindings({ '1': { type: 'TEXT', value: 'x'.repeat(MAX_REQUEST_BYTES) } })
).toThrow('exceeds')
expect(SnowflakeBlock.inputs.bindings.description).toContain(
'object keyed by 1-based positions'
)
expect(SnowflakeBlock.inputs.procedureArguments.description).toContain('ordered JSON array')
})
it('rejects oversized JSON block inputs before parsing', () => {
const mapParams = SnowflakeBlock.tools.config.params
if (!mapParams) throw new Error('Snowflake block must map tool parameters')
expect(() =>
mapParams({
operation: 'insert_rows',
rows: `[{"payload":"${'x'.repeat(MAX_REQUEST_BYTES)}"}]`,
})
).toThrow('exceeds')
expect(() =>
mapParams({
operation: 'update_rows',
rows: `[{"payload":"${'x'.repeat(MAX_REQUEST_BYTES / 2)}"}]`,
matchColumns: `["${'x'.repeat(MAX_REQUEST_BYTES / 2)}"]`,
})
).toThrow('exceeds')
})
it('builds a bound multi-row INSERT in stable column order', () => {
const result = buildInsertRows({
...table,
rows: [
{ id: 1, payload: { ok: true } },
{ id: 2, payload: null },
],
})
expect(result.statement).toBe(
'INSERT INTO ANALYTICS.PUBLIC.EVENTS (id, payload) VALUES (?, PARSE_JSON(?)), (?, NULL)'
)
expect(result.bindings).toEqual({
'1': { type: 'FIXED', value: '1' },
'2': { type: 'TEXT', value: '{"ok":true}' },
'3': { type: 'FIXED', value: '2' },
})
})
it('builds update-only and upsert MERGE statements with bound values', () => {
const params = { ...table, rows: [{ id: 1, name: 'Ada' }], matchColumns: ['ID'] }
const update = buildUpdateRows(params)
const upsert = buildUpsertRows(params)
expect(update.statement).toContain('WHEN MATCHED THEN UPDATE SET target.name = source.name')
expect(update.statement).not.toContain('WHEN NOT MATCHED')
expect(upsert.statement).toContain('WHEN NOT MATCHED THEN INSERT (id, name)')
expect(upsert.bindings).toEqual({
'1': { type: 'FIXED', value: '1' },
'2': { type: 'TEXT', value: 'Ada' },
})
expect(() => buildUpdateRows({ ...params, matchColumns: ['id', 'ID'] })).toThrow('duplicate')
expect(() => buildUpdateRows({ ...params, matchColumns: ['id', 'name', 'extra'] })).toThrow(
'cannot exceed'
)
})
it('rejects malformed or oversized structured writes', () => {
expect(() => buildInsertRows({ ...table, rows: [] })).toThrow('non-empty')
expect(() => buildInsertRows({ ...table, rows: [{ id: 1 }, { other: 2 }] })).toThrow(
'same columns'
)
expect(() =>
buildInsertRows({
...table,
rows: Array.from({ length: MAX_WRITE_ROWS + 1 }, (_, id) => ({ id })),
})
).toThrow('cannot exceed')
expect(() =>
buildInsertRows({
...table,
rows: [{ payload: { value: 'x'.repeat(MAX_REQUEST_BYTES) } }],
})
).toThrow('exceeds')
expect(() => buildInsertRows({ ...table, rows: [{ id: 1 }, { ID: 2 }] })).toThrow(
'same columns'
)
expect(() => buildInsertRows({ ...table, rows: [{ id: 1, ID: 2 }] })).toThrow(
'duplicate Snowflake column identifiers'
)
expect(() => buildInsertRows({ ...table, rows: [{ ID: 1, '"ID"': 2 }] })).toThrow(
'duplicate Snowflake column identifiers'
)
expect(() =>
buildInsertRows({ ...table, rows: [{ id: Number.MAX_SAFE_INTEGER + 1 }] })
).toThrow('safe integers')
})
it('requires delete filters and binds every filter value', () => {
expect(() => buildDeleteRows({ ...table, filters: {} })).toThrow('cannot be empty')
expect(() =>
buildDeleteRows({ ...table, filters: { payload: 'x'.repeat(MAX_REQUEST_BYTES) } })
).toThrow('exceeds')
expect(buildDeleteRows({ ...table, filters: { id: 7, deleted_at: null } })).toEqual({
statement: 'DELETE FROM ANALYTICS.PUBLIC.EVENTS WHERE id = ? AND deleted_at IS NULL',
bindings: { '1': { type: 'FIXED', value: '7' } },
})
})
it('builds COPY INTO with the supported stage and copy options', () => {
expect(
buildLoadData({
...table,
stagePath: '@RAW_STAGE/2026/08',
fileFormat: 'ANALYTICS.PUBLIC.CSV_FORMAT',
pattern: '.*[.]csv',
onError: 'CONTINUE',
purge: true,
force: false,
matchByColumnName: 'CASE_INSENSITIVE',
}).statement
).toBe(
"COPY INTO ANALYTICS.PUBLIC.EVENTS FROM @RAW_STAGE/2026/08 FILE_FORMAT = (FORMAT_NAME = 'ANALYTICS.PUBLIC.CSV_FORMAT') PATTERN = '.*[.]csv' ON_ERROR = 'CONTINUE' PURGE = TRUE FORCE = FALSE MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE"
)
expect(() => buildLoadData({ ...table, stagePath: '@stage/path; DROP TABLE x' })).toThrow(
'stagePath'
)
expect(
buildLoadData({
...table,
stagePath: '@"Raw Stage"/daily',
fileFormat: 'ANALYTICS.PUBLIC."CSV Format"',
}).statement
).toContain(
`FROM @"Raw Stage"/daily FILE_FORMAT = (FORMAT_NAME = 'ANALYTICS.PUBLIC."CSV Format"')`
)
expect(
buildLoadData({
...table,
stagePath: '@ANALYTICS.PUBLIC.%EVENTS/daily',
onError: 'SKIP_FILE_10',
}).statement
).toContain("FROM @ANALYTICS.PUBLIC.%EVENTS/daily ON_ERROR = 'SKIP_FILE_10'")
expect(
buildLoadData({ ...table, stagePath: '@RAW_STAGE', onError: 'SKIP_FILE_25%' }).statement
).toContain("ON_ERROR = 'SKIP_FILE_25%'")
expect(
buildLoadData({ ...table, stagePath: '@RAW_STAGE', onError: 'SKIP_FILE_2.5%' }).statement
).toContain("ON_ERROR = 'SKIP_FILE_2.5%'")
expect(() =>
buildLoadData({ ...table, stagePath: '@RAW_STAGE', onError: 'SKIP_FILE_0%' })
).toThrow('onError')
})
it('builds warehouse statements', () => {
expect(buildListWarehouses('ETL%').statement).toBe("SHOW WAREHOUSES LIKE 'ETL%'")
expect(buildGetWarehouse({ ...context, warehouseName: 'ETL_WH' }).statement).toBe(
`SHOW WAREHOUSES ->> SELECT * FROM $1 WHERE "name" = 'ETL_WH'`
)
expect(buildGetWarehouse({ ...context, warehouseName: `"etl_%'s"` }).statement).toBe(
`SHOW WAREHOUSES ->> SELECT * FROM $1 WHERE "name" = 'etl_%''s'`
)
expect(buildResumeWarehouse({ ...context, warehouseName: 'ETL_WH' }).statement).toBe(
'ALTER WAREHOUSE ETL_WH RESUME IF SUSPENDED'
)
expect(buildSuspendWarehouse({ ...context, warehouseName: 'ETL_WH' }).statement).toBe(
'ALTER WAREHOUSE ETL_WH SUSPEND'
)
})
it('builds task definition and execution statements', () => {
const task = { ...context, database: 'ANALYTICS', schema: 'PUBLIC', taskName: 'DAILY_LOAD' }
expect(buildListTasks({ ...task, limit: 25, nameLike: 'DAILY%' }).statement).toBe(
"SHOW TASKS LIKE 'DAILY%' IN SCHEMA ANALYTICS.PUBLIC LIMIT 25"
)
expect(buildGetTask(task).statement).toBe('DESCRIBE TASK ANALYTICS.PUBLIC.DAILY_LOAD')
expect(buildRunTask({ ...task, retryLast: true }).statement).toBe(
'EXECUTE TASK ANALYTICS.PUBLIC.DAILY_LOAD RETRY LAST'
)
})
it('builds bounded task history, run lookup, cancellation, and output statements', () => {
const history = buildListTaskRuns({
...context,
taskName: 'DAILY_LOAD',
startTime: '2026-08-01T00:00:00Z',
errorOnly: true,
limit: 50,
})
expect(history.statement).toContain('RESULT_LIMIT => 50, ERROR_ONLY => TRUE')
expect(history.bindings).toEqual({
'1': { type: 'TEXT', value: 'DAILY_LOAD' },
'2': { type: 'TEXT', value: '2026-08-01T00:00:00Z' },
})
const run = buildGetTaskRun({
...context,
queryId,
taskName: 'DAILY_LOAD',
startTime: '2026-08-01T00:00:00Z',
})
expect(run.statement).toContain('TASK_NAME => ?')
expect(run.statement).toContain('SCHEDULED_TIME_RANGE_START => TO_TIMESTAMP_LTZ(?)')
expect(run.statement).toContain('WHERE QUERY_ID = ?')
expect(run.bindings).toEqual({
'1': { type: 'TEXT', value: 'DAILY_LOAD' },
'2': { type: 'TEXT', value: '2026-08-01T00:00:00Z' },
'3': { type: 'TEXT', value: queryId },
})
expect(buildCancelTaskRun({ ...context, queryId }).statement).toContain('SYSTEM$CANCEL_QUERY')
expect(buildGetTaskRunOutput({ ...context, queryId }).statement).toContain('RESULT_SCAN')
expect(() => buildGetTaskRun({ ...context, queryId: "x' OR TRUE" })).toThrow('UUID')
})
it('builds bound schema introspection and typed procedure calls', () => {
const schema = buildIntrospectSchema({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
table: 'EVENTS',
})
expect(schema.statement).toContain('ANALYTICS.INFORMATION_SCHEMA.COLUMNS')
expect(schema.statement).toContain("t.TABLE_TYPE = 'BASE TABLE'")
expect(schema.bindings).toEqual({
'1': { type: 'TEXT', value: 'PUBLIC' },
'2': { type: 'TEXT', value: 'EVENTS' },
})
expect(
buildIntrospectSchema({
...context,
database: 'analytics',
schema: 'public',
table: 'events',
}).bindings
).toEqual({
'1': { type: 'TEXT', value: 'PUBLIC' },
'2': { type: 'TEXT', value: 'EVENTS' },
})
expect(
buildIntrospectSchema({
...context,
database: '"Analytics DB"',
schema: '"Mixed Schema"',
table: '"events"',
}).bindings
).toEqual({
'1': { type: 'TEXT', value: 'Mixed Schema' },
'2': { type: 'TEXT', value: 'events' },
})
expect(
buildCallProcedure({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
procedureName: 'REFRESH_MODEL',
procedureArguments: [
{ type: 'TEXT', value: 'daily' },
{ type: 'BOOLEAN', value: 'true' },
],
})
).toEqual({
statement: 'CALL ANALYTICS.PUBLIC.REFRESH_MODEL(?, ?)',
bindings: {
'1': { type: 'TEXT', value: 'daily' },
'2': { type: 'BOOLEAN', value: 'true' },
},
})
expect(() =>
buildCallProcedure({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
procedureName: 'REFRESH_MODEL',
procedureArguments: { type: 'TEXT', value: 'x' } as never,
})
).toThrow('JSON array')
expect(() =>
buildCallProcedure({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
procedureName: 'REFRESH_MODEL',
procedureArguments: [{ type: 'TEXT', value: 'x'.repeat(MAX_REQUEST_BYTES) }],
})
).toThrow('exceeds')
})
})
+521
View File
@@ -0,0 +1,521 @@
import {
SNOWFLAKE_BINDING_TYPES,
type SnowflakeBinding,
type SnowflakeCallProcedureParams,
type SnowflakeDeleteRowsParams,
type SnowflakeInsertRowsParams,
type SnowflakeIntrospectSchemaParams,
type SnowflakeListTaskRunsParams,
type SnowflakeListTasksParams,
type SnowflakeLoadDataParams,
type SnowflakeRunTaskParams,
type SnowflakeTaskParams,
type SnowflakeTaskRunParams,
type SnowflakeUpdateRowsParams,
type SnowflakeWarehouseParams,
} from '@/tools/snowflake/types'
import {
addSnowflakeRequestBytes,
addSnowflakeRequestOverhead,
MAX_WRITE_ROWS,
normalizeMaxRows,
type SnowflakeStatementSpec,
} from '@/tools/snowflake/utils'
const UNQUOTED_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/
const QUOTED_IDENTIFIER = /^"(?:[^"]|"")+"$/
const QUERY_ID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
export function identifier(value: string): string {
const trimmed = value.trim()
if (UNQUOTED_IDENTIFIER.test(trimmed) || QUOTED_IDENTIFIER.test(trimmed)) return trimmed
throw new Error(`Invalid Snowflake identifier: ${value}`)
}
export function qualifiedIdentifier(...parts: string[]): string {
if (parts.length === 0 || parts.some((part) => !part?.trim())) {
throw new Error('Snowflake identifier parts cannot be empty')
}
return parts.map(identifier).join('.')
}
function splitQualifiedIdentifier(value: string): string[] {
const parts: string[] = []
let start = 0
let quoted = false
for (let index = 0; index < value.length; index += 1) {
if (value[index] === '"') {
if (quoted && value[index + 1] === '"') {
index += 1
} else {
quoted = !quoted
}
} else if (value[index] === '.' && !quoted) {
parts.push(value.slice(start, index))
start = index + 1
}
}
if (quoted) throw new Error(`Invalid Snowflake identifier: ${value}`)
parts.push(value.slice(start))
return parts
}
function qualifiedIdentifierValue(value: string): string {
return qualifiedIdentifier(...splitQualifiedIdentifier(value))
}
function identifierKey(value: string): string {
const valid = identifier(value)
if (!valid.startsWith('"')) return `resolved:${valid.toUpperCase()}`
const exactName = resolvedIdentifierName(valid)
return UNQUOTED_IDENTIFIER.test(exactName) && exactName === exactName.toUpperCase()
? `resolved:${exactName}`
: `quoted:${exactName}`
}
function resolvedIdentifierName(value: string): string {
const valid = identifier(value)
return valid.startsWith('"') ? valid.slice(1, -1).replaceAll('""', '"') : valid.toUpperCase()
}
function stringLiteral(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
function requireQueryId(queryId: string): string {
const trimmed = queryId.trim()
if (!QUERY_ID.test(trimmed)) throw new Error('queryId must be a Snowflake UUID')
return trimmed
}
export function normalizeBindings(
input?: Record<string, SnowflakeBinding>
): Record<string, SnowflakeBinding> | undefined {
if (input === undefined) return undefined
if (!input || Array.isArray(input) || typeof input !== 'object') {
throw new Error('bindings must be a JSON object keyed by 1-based positions')
}
const normalized: Record<string, SnowflakeBinding> = {}
let requestBytes = 0
let hasBindings = false
for (const position in input) {
if (!Object.hasOwn(input, position)) continue
hasBindings = true
const binding = input[position]
if (!/^[1-9][0-9]*$/.test(position)) {
throw new Error('binding keys must be positive integer positions')
}
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
throw new Error(`binding ${position} must contain type and value`)
}
if (!SNOWFLAKE_BINDING_TYPES.includes(binding.type)) {
throw new Error(`Unsupported Snowflake binding type: ${binding.type}`)
}
if (typeof binding.value !== 'string') {
throw new Error(`binding ${position} value must be a string`)
}
requestBytes = addSnowflakeRequestOverhead(requestBytes, 32)
requestBytes = addSnowflakeRequestBytes(requestBytes, position, binding.type, binding.value)
normalized[position] = { type: binding.type, value: binding.value }
}
return hasBindings ? normalized : undefined
}
class BindingsBuilder {
readonly bindings: Record<string, SnowflakeBinding> = {}
private position = 0
private requestBytes = 0
private addBinding(type: SnowflakeBinding['type'], value: string): string {
this.position += 1
const key = String(this.position)
this.requestBytes = addSnowflakeRequestOverhead(this.requestBytes, 32)
this.requestBytes = addSnowflakeRequestBytes(this.requestBytes, key, type, value)
this.bindings[key] = { type, value }
return '?'
}
add(value: unknown): string {
if (value === null || value === undefined) return 'NULL'
if (typeof value === 'boolean') {
return this.addBinding('BOOLEAN', String(value))
}
if (typeof value === 'number') {
if (!Number.isFinite(value)) throw new Error('Snowflake row values must be finite numbers')
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
throw new Error(
'Snowflake integer row values must be JavaScript safe integers; pass exact large numerics as strings or typed Execute SQL bindings'
)
}
return this.addBinding(Number.isInteger(value) ? 'FIXED' : 'REAL', String(value))
}
if (typeof value === 'string') {
return this.addBinding('TEXT', value)
}
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
assertJsonValueWithinRequestBudget(value)
this.addBinding('TEXT', JSON.stringify(value))
return 'PARSE_JSON(?)'
}
throw new Error(`Unsupported Snowflake row value type: ${typeof value}`)
}
}
function assertJsonValueWithinRequestBudget(value: unknown): void {
const pending: Array<{ value: unknown; leave?: boolean }> = [{ value }]
const active = new WeakSet<object>()
let requestBytes = 0
while (pending.length > 0) {
const item = pending.pop()
const current = item?.value
if (item?.leave && current && typeof current === 'object') {
active.delete(current)
continue
}
if (typeof current === 'string') {
requestBytes = addSnowflakeRequestOverhead(requestBytes, 4)
requestBytes = addSnowflakeRequestBytes(requestBytes, current)
continue
}
if (!current || typeof current !== 'object') {
requestBytes = addSnowflakeRequestOverhead(requestBytes, 2)
requestBytes = addSnowflakeRequestBytes(requestBytes, String(current))
continue
}
if (active.has(current)) throw new Error('Snowflake row JSON values cannot be circular')
active.add(current)
pending.push({ value: current, leave: true })
requestBytes = addSnowflakeRequestOverhead(requestBytes, 2)
if (Array.isArray(current)) {
for (const nestedValue of current) {
requestBytes = addSnowflakeRequestOverhead(requestBytes, 1)
pending.push({ value: nestedValue })
}
continue
}
for (const key in current) {
if (!Object.hasOwn(current, key)) continue
requestBytes = addSnowflakeRequestOverhead(requestBytes, 4)
requestBytes = addSnowflakeRequestBytes(requestBytes, key)
pending.push({ value: (current as Record<string, unknown>)[key] })
}
}
}
function validateRows(rows: Array<Record<string, unknown>>): string[] {
if (!Array.isArray(rows) || rows.length === 0) throw new Error('rows must be a non-empty array')
if (rows.length > MAX_WRITE_ROWS) throw new Error(`rows cannot exceed ${MAX_WRITE_ROWS} items`)
assertJsonValueWithinRequestBudget(rows)
const columns = Object.keys(rows[0] ?? {})
if (columns.length === 0) throw new Error('rows must contain at least one column')
const signature = [...columns].sort().join('\u0000')
for (const row of rows) {
if (!row || Array.isArray(row) || typeof row !== 'object') {
throw new Error('every row must be a JSON object')
}
if ([...Object.keys(row)].sort().join('\u0000') !== signature) {
throw new Error('every row must contain the same columns')
}
const identifierKeys = new Set<string>()
for (const column of Object.keys(row)) {
const key = identifierKey(column)
if (identifierKeys.has(key)) {
throw new Error(`rows contain duplicate Snowflake column identifiers: ${column}`)
}
identifierKeys.add(key)
}
}
return columns
}
function valuesSource(
rows: Array<Record<string, unknown>>,
columns: string[],
binds: BindingsBuilder
) {
const values = rows
.map((row) => `(${columns.map((column) => binds.add(row[column])).join(', ')})`)
.join(', ')
return `(SELECT * FROM VALUES ${values}) AS source (${columns.map(identifier).join(', ')})`
}
export function buildInsertRows(params: SnowflakeInsertRowsParams): SnowflakeStatementSpec {
const columns = validateRows(params.rows)
const binds = new BindingsBuilder()
const values = params.rows
.map((row) => `(${columns.map((column) => binds.add(row[column])).join(', ')})`)
.join(', ')
return {
statement: `INSERT INTO ${qualifiedIdentifier(params.database, params.schema, params.table)} (${columns.map(identifier).join(', ')}) VALUES ${values}`,
bindings: binds.bindings,
}
}
function buildMerge(params: SnowflakeUpdateRowsParams, upsert: boolean): SnowflakeStatementSpec {
const columns = validateRows(params.rows)
if (!Array.isArray(params.matchColumns) || params.matchColumns.length === 0) {
throw new Error('matchColumns must be a non-empty array')
}
if (params.matchColumns.length > columns.length) {
throw new Error('matchColumns cannot exceed the number of row columns')
}
const columnsByIdentifier = new Map(columns.map((column) => [identifierKey(column), column]))
const matchColumns: string[] = []
const seenMatchColumns = new Set<string>()
for (const matchColumn of params.matchColumns) {
const key = identifierKey(matchColumn)
if (seenMatchColumns.has(key)) {
throw new Error(`matchColumns contains a duplicate Snowflake identifier: ${matchColumn}`)
}
seenMatchColumns.add(key)
const column = columnsByIdentifier.get(key)
if (!column) throw new Error(`match column is missing from rows: ${matchColumn}`)
matchColumns.push(column)
}
const updateColumns = columns.filter((column) => !matchColumns.includes(column))
if (updateColumns.length === 0) throw new Error('rows must contain a column to update')
const binds = new BindingsBuilder()
const source = valuesSource(params.rows, columns, binds)
const target = qualifiedIdentifier(params.database, params.schema, params.table)
const on = matchColumns
.map((column) => `target.${identifier(column)} = source.${identifier(column)}`)
.join(' AND ')
const update = updateColumns
.map((column) => `target.${identifier(column)} = source.${identifier(column)}`)
.join(', ')
const insert = upsert
? ` WHEN NOT MATCHED THEN INSERT (${columns.map(identifier).join(', ')}) VALUES (${columns.map((column) => `source.${identifier(column)}`).join(', ')})`
: ''
return {
statement: `MERGE INTO ${target} AS target USING ${source} ON ${on} WHEN MATCHED THEN UPDATE SET ${update}${insert}`,
bindings: binds.bindings,
}
}
export function buildUpdateRows(params: SnowflakeUpdateRowsParams): SnowflakeStatementSpec {
return buildMerge(params, false)
}
export function buildUpsertRows(params: SnowflakeUpdateRowsParams): SnowflakeStatementSpec {
return buildMerge(params, true)
}
export function buildDeleteRows(params: SnowflakeDeleteRowsParams): SnowflakeStatementSpec {
if (!params.filters || Array.isArray(params.filters) || typeof params.filters !== 'object') {
throw new Error('filters must be a JSON object')
}
assertJsonValueWithinRequestBudget(params.filters)
const filters = Object.entries(params.filters)
if (filters.length === 0) throw new Error('filters cannot be empty')
const binds = new BindingsBuilder()
const where = filters
.map(([column, value]) =>
value === null || value === undefined
? `${identifier(column)} IS NULL`
: `${identifier(column)} = ${binds.add(value)}`
)
.join(' AND ')
return {
statement: `DELETE FROM ${qualifiedIdentifier(params.database, params.schema, params.table)} WHERE ${where}`,
bindings: binds.bindings,
}
}
function stagePath(value: string): string {
const trimmed = value.trim()
if (!trimmed.startsWith('@')) {
throw new Error('stagePath must be a simple Snowflake stage reference such as @stage/path')
}
let slash = -1
let quoted = false
for (let index = 1; index < trimmed.length; index += 1) {
if (trimmed[index] === '"') {
if (quoted && trimmed[index + 1] === '"') index += 1
else quoted = !quoted
} else if (trimmed[index] === '/' && !quoted) {
slash = index
break
}
}
if (quoted) throw new Error('stagePath contains an unterminated quoted identifier')
const reference = trimmed.slice(1, slash === -1 ? undefined : slash)
const path = slash === -1 ? '' : trimmed.slice(slash)
if (!/^\/[A-Za-z0-9_./=+@%$-]*$/.test(path) && path !== '') {
throw new Error('stagePath contains unsupported path characters')
}
if (reference === '~') return `@~${path}`
if (reference.startsWith('%')) return `@%${identifier(reference.slice(1))}${path}`
const referenceParts = splitQualifiedIdentifier(reference)
const finalPart = referenceParts.at(-1)
if (finalPart?.startsWith('%')) {
const namespace = referenceParts.slice(0, -1)
if (namespace.length === 0) throw new Error('stagePath contains an invalid table stage')
return `@${qualifiedIdentifier(...namespace)}.%${identifier(finalPart.slice(1))}${path}`
}
return `@${qualifiedIdentifierValue(reference)}${path}`
}
export function buildLoadData(params: SnowflakeLoadDataParams): SnowflakeStatementSpec {
const clauses = [
`COPY INTO ${qualifiedIdentifier(params.database, params.schema, params.table)}`,
`FROM ${stagePath(params.stagePath)}`,
]
if (params.fileFormat?.trim()) {
const formatName = qualifiedIdentifierValue(params.fileFormat.trim())
clauses.push(`FILE_FORMAT = (FORMAT_NAME = ${stringLiteral(formatName)})`)
}
if (params.pattern?.trim()) clauses.push(`PATTERN = ${stringLiteral(params.pattern.trim())}`)
if (params.onError) {
const percentMatch = params.onError.match(/^SKIP_FILE_([0-9]+(?:\.[0-9]+)?)%$/)
const percentage = percentMatch ? Number(percentMatch[1]) : undefined
const validOnError =
['ABORT_STATEMENT', 'CONTINUE', 'SKIP_FILE'].includes(params.onError) ||
/^SKIP_FILE_[1-9][0-9]*$/.test(params.onError) ||
(percentage !== undefined && percentage > 0 && percentage <= 100)
if (!validOnError) {
throw new Error('Unsupported COPY INTO onError value')
}
clauses.push(`ON_ERROR = ${stringLiteral(params.onError)}`)
}
if (params.purge !== undefined) clauses.push(`PURGE = ${params.purge ? 'TRUE' : 'FALSE'}`)
if (params.force !== undefined) clauses.push(`FORCE = ${params.force ? 'TRUE' : 'FALSE'}`)
if (params.matchByColumnName) {
if (!['CASE_SENSITIVE', 'CASE_INSENSITIVE', 'NONE'].includes(params.matchByColumnName)) {
throw new Error('Unsupported MATCH_BY_COLUMN_NAME value')
}
clauses.push(`MATCH_BY_COLUMN_NAME = ${params.matchByColumnName}`)
}
return { statement: clauses.join(' ') }
}
export function buildListWarehouses(nameLike?: string): SnowflakeStatementSpec {
return {
statement: `SHOW WAREHOUSES${nameLike?.trim() ? ` LIKE ${stringLiteral(nameLike.trim())}` : ''}`,
}
}
export function buildGetWarehouse(params: SnowflakeWarehouseParams): SnowflakeStatementSpec {
const warehouseName = resolvedIdentifierName(params.warehouseName)
return {
statement: `SHOW WAREHOUSES ->> SELECT * FROM $1 WHERE "name" = ${stringLiteral(warehouseName)}`,
}
}
export function buildResumeWarehouse(params: SnowflakeWarehouseParams): SnowflakeStatementSpec {
return { statement: `ALTER WAREHOUSE ${identifier(params.warehouseName)} RESUME IF SUSPENDED` }
}
export function buildSuspendWarehouse(params: SnowflakeWarehouseParams): SnowflakeStatementSpec {
return { statement: `ALTER WAREHOUSE ${identifier(params.warehouseName)} SUSPEND` }
}
export function buildListTasks(params: SnowflakeListTasksParams): SnowflakeStatementSpec {
const limit = normalizeMaxRows(params.limit ?? params.maxRows)
return {
statement: `SHOW TASKS${params.nameLike?.trim() ? ` LIKE ${stringLiteral(params.nameLike.trim())}` : ''} IN SCHEMA ${qualifiedIdentifier(params.database, params.schema)} LIMIT ${limit}`,
}
}
export function buildGetTask(params: SnowflakeTaskParams): SnowflakeStatementSpec {
return {
statement: `DESCRIBE TASK ${qualifiedIdentifier(params.database, params.schema, params.taskName)}`,
}
}
export function buildRunTask(params: SnowflakeRunTaskParams): SnowflakeStatementSpec {
return {
statement: `EXECUTE TASK ${qualifiedIdentifier(params.database, params.schema, params.taskName)}${params.retryLast ? ' RETRY LAST' : ''}`,
}
}
export function buildListTaskRuns(params: SnowflakeListTaskRunsParams): SnowflakeStatementSpec {
const limit = normalizeMaxRows(params.limit ?? params.maxRows)
const binds = new BindingsBuilder()
const args = [`RESULT_LIMIT => ${limit}`, `ERROR_ONLY => ${params.errorOnly ? 'TRUE' : 'FALSE'}`]
if (params.taskName?.trim()) args.push(`TASK_NAME => ${binds.add(params.taskName.trim())}`)
if (params.startTime?.trim()) {
args.push(
`SCHEDULED_TIME_RANGE_START => TO_TIMESTAMP_LTZ(${binds.add(params.startTime.trim())})`
)
}
if (params.endTime?.trim()) {
args.push(`SCHEDULED_TIME_RANGE_END => TO_TIMESTAMP_LTZ(${binds.add(params.endTime.trim())})`)
}
return {
statement: `SELECT * FROM TABLE(SNOWFLAKE.INFORMATION_SCHEMA.TASK_HISTORY(${args.join(', ')})) ORDER BY SCHEDULED_TIME DESC`,
bindings: binds.bindings,
}
}
export function buildGetTaskRun(params: SnowflakeTaskRunParams): SnowflakeStatementSpec {
const binds = new BindingsBuilder()
const args = ['RESULT_LIMIT => 10000']
if (params.taskName?.trim()) args.push(`TASK_NAME => ${binds.add(params.taskName.trim())}`)
if (params.startTime?.trim()) {
args.push(
`SCHEDULED_TIME_RANGE_START => TO_TIMESTAMP_LTZ(${binds.add(params.startTime.trim())})`
)
}
if (params.endTime?.trim()) {
args.push(`SCHEDULED_TIME_RANGE_END => TO_TIMESTAMP_LTZ(${binds.add(params.endTime.trim())})`)
}
const queryId = binds.add(requireQueryId(params.queryId))
return {
statement: `SELECT * FROM TABLE(SNOWFLAKE.INFORMATION_SCHEMA.TASK_HISTORY(${args.join(', ')})) WHERE QUERY_ID = ${queryId} LIMIT 1`,
bindings: binds.bindings,
}
}
export function buildCancelTaskRun(params: SnowflakeTaskRunParams): SnowflakeStatementSpec {
return {
statement: `SELECT SYSTEM$CANCEL_QUERY(${stringLiteral(requireQueryId(params.queryId))}) AS STATUS`,
}
}
export function buildGetTaskRunOutput(params: SnowflakeTaskRunParams): SnowflakeStatementSpec {
return {
statement: `SELECT * FROM TABLE(RESULT_SCAN(${stringLiteral(requireQueryId(params.queryId))}))`,
}
}
export function buildIntrospectSchema(
params: SnowflakeIntrospectSchemaParams
): SnowflakeStatementSpec {
const binds = new BindingsBuilder()
const filters: string[] = []
if (params.schema?.trim()) {
filters.push(`c.TABLE_SCHEMA = ${binds.add(resolvedIdentifierName(params.schema))}`)
}
if (params.table?.trim()) {
filters.push(`c.TABLE_NAME = ${binds.add(resolvedIdentifierName(params.table))}`)
}
if (!params.includeViews) filters.push("t.TABLE_TYPE = 'BASE TABLE'")
const where = filters.length > 0 ? ` WHERE ${filters.join(' AND ')}` : ''
return {
statement: `SELECT c.TABLE_CATALOG, c.TABLE_SCHEMA, c.TABLE_NAME, t.TABLE_TYPE, t.ROW_COUNT, t.BYTES, c.COLUMN_NAME, c.ORDINAL_POSITION, c.COLUMN_DEFAULT, c.IS_NULLABLE, c.DATA_TYPE, c.CHARACTER_MAXIMUM_LENGTH, c.NUMERIC_PRECISION, c.NUMERIC_SCALE, c.COMMENT FROM ${identifier(params.database)}.INFORMATION_SCHEMA.COLUMNS AS c JOIN ${identifier(params.database)}.INFORMATION_SCHEMA.TABLES AS t ON c.TABLE_CATALOG = t.TABLE_CATALOG AND c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME${where} ORDER BY c.TABLE_SCHEMA, c.TABLE_NAME, c.ORDINAL_POSITION`,
bindings: binds.bindings,
}
}
export function buildCallProcedure(params: SnowflakeCallProcedureParams): SnowflakeStatementSpec {
const procedureArguments = params.procedureArguments ?? []
if (!Array.isArray(procedureArguments)) {
throw new Error('procedureArguments must be a JSON array')
}
assertJsonValueWithinRequestBudget(procedureArguments)
const bindings: Record<string, SnowflakeBinding> = {}
const placeholders = procedureArguments.map((argument, index) => {
if (!SNOWFLAKE_BINDING_TYPES.includes(argument.type)) {
throw new Error(`Unsupported Snowflake binding type: ${argument.type}`)
}
if (typeof argument.value !== 'string')
throw new Error('Procedure argument values must be strings')
bindings[String(index + 1)] = argument
return '?'
})
return {
statement: `CALL ${qualifiedIdentifier(params.database, params.schema, params.procedureName)}(${placeholders.join(', ')})`,
bindings,
}
}
@@ -0,0 +1,43 @@
import { buildSuspendWarehouse } from '@/tools/snowflake/sql'
import type {
SnowflakeSuspendWarehouseResponse,
SnowflakeWarehouseParams,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const suspendWarehouseTool: ToolConfig<
SnowflakeWarehouseParams,
SnowflakeSuspendWarehouseResponse
> = {
id: 'snowflake_suspend_warehouse',
version: '1.0.0',
name: 'Snowflake Suspend Warehouse',
description: 'Suspend a Snowflake virtual warehouse.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
warehouseName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Warehouse name',
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildSuspendWarehouse(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+256
View File
@@ -0,0 +1,256 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
export const SNOWFLAKE_BINDING_TYPES = [
'FIXED',
'REAL',
'DECFLOAT',
'TEXT',
'BINARY',
'BOOLEAN',
'DATE',
'TIME',
'TIMESTAMP_TZ',
'TIMESTAMP_LTZ',
'TIMESTAMP_NTZ',
] as const
export const SNOWFLAKE_STATEMENT_OUTPUTS = {
statementHandle: { type: 'string', description: 'Snowflake statement handle' },
status: { type: 'string', description: 'Statement status: SUCCEEDED, RUNNING, or CANCELED' },
code: { type: 'string', description: 'Snowflake response code', nullable: true },
sqlState: { type: 'string', description: 'SQLSTATE response code', nullable: true },
message: { type: 'string', description: 'Snowflake status message' },
columns: {
type: 'array',
description: 'Documented Snowflake result column metadata',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Column name' },
type: { type: 'string', description: 'Snowflake data type' },
length: { type: 'number', description: 'Column length', nullable: true },
precision: { type: 'number', description: 'Numeric precision', nullable: true },
scale: { type: 'number', description: 'Numeric scale', nullable: true },
nullable: { type: 'boolean', description: 'Whether the column is nullable' },
},
},
},
rows: {
type: 'array',
description: 'One Snowflake result partition as arrays of string or null column values',
items: { type: 'array', description: 'A result row in column order' },
},
totalRows: { type: 'number', description: 'Total result rows', nullable: true },
partitions: {
type: 'array',
description: 'Available result partitions and their documented sizes',
items: {
type: 'object',
properties: {
rowCount: { type: 'number', description: 'Rows in the partition' },
compressedSize: { type: 'number', description: 'Compressed bytes', nullable: true },
uncompressedSize: { type: 'number', description: 'Uncompressed bytes' },
},
},
},
currentPartition: { type: 'number', description: 'Zero-based partition returned' },
nextPartition: {
type: 'number',
description: 'Next partition to request, if one exists',
nullable: true,
},
truncated: {
type: 'boolean',
description: 'Whether additional rows or partitions remain',
},
rowsInserted: { type: 'number', description: 'Rows inserted by a DML statement' },
rowsUpdated: { type: 'number', description: 'Rows updated by a DML statement' },
rowsDeleted: { type: 'number', description: 'Rows deleted by a DML statement' },
duplicateRowsUpdated: {
type: 'number',
description: 'Duplicate rows updated by a DML statement',
},
rowsAffected: { type: 'number', description: 'Total inserted, updated, and deleted rows' },
} satisfies Record<string, OutputProperty>
export type SnowflakeBindingType = (typeof SNOWFLAKE_BINDING_TYPES)[number]
export interface SnowflakeBinding {
type: SnowflakeBindingType
value: string
}
export interface SnowflakeBaseParams {
host: string
apiKey: string
}
export interface SnowflakeContextParams extends SnowflakeBaseParams {
warehouse?: string
database?: string
schema?: string
role?: string
timeout?: number
maxRows?: number
}
export interface SnowflakeExecuteSqlParams extends SnowflakeContextParams {
statement: string
bindings?: Record<string, SnowflakeBinding>
async?: boolean
}
export interface SnowflakeGetStatementParams extends SnowflakeBaseParams {
statementHandle: string
partition?: number
maxRows?: number
}
export interface SnowflakeCancelStatementParams extends SnowflakeBaseParams {
statementHandle: string
}
export interface SnowflakeTableParams extends SnowflakeContextParams {
database: string
schema: string
table: string
}
export interface SnowflakeInsertRowsParams extends SnowflakeTableParams {
rows: Array<Record<string, unknown>>
}
export interface SnowflakeUpdateRowsParams extends SnowflakeInsertRowsParams {
matchColumns: string[]
}
export type SnowflakeUpsertRowsParams = SnowflakeUpdateRowsParams
export interface SnowflakeDeleteRowsParams extends SnowflakeTableParams {
filters: Record<string, unknown>
}
export interface SnowflakeLoadDataParams extends SnowflakeTableParams {
stagePath: string
fileFormat?: string
pattern?: string
onError?: string
purge?: boolean
force?: boolean
matchByColumnName?: 'CASE_SENSITIVE' | 'CASE_INSENSITIVE' | 'NONE'
}
export interface SnowflakeListWarehousesParams extends SnowflakeContextParams {
nameLike?: string
}
export interface SnowflakeWarehouseParams extends SnowflakeContextParams {
warehouseName: string
}
export interface SnowflakeListTasksParams extends SnowflakeContextParams {
database: string
schema: string
nameLike?: string
limit?: number
}
export interface SnowflakeTaskParams extends SnowflakeContextParams {
database: string
schema: string
taskName: string
}
export interface SnowflakeRunTaskParams extends SnowflakeTaskParams {
retryLast?: boolean
}
export interface SnowflakeListTaskRunsParams extends SnowflakeContextParams {
taskName?: string
startTime?: string
endTime?: string
errorOnly?: boolean
limit?: number
}
export interface SnowflakeTaskRunParams extends SnowflakeContextParams {
queryId: string
taskName?: string
startTime?: string
endTime?: string
}
export interface SnowflakeIntrospectSchemaParams extends SnowflakeContextParams {
database: string
schema?: string
table?: string
includeViews?: boolean
}
export interface SnowflakeCallProcedureParams extends SnowflakeContextParams {
database: string
schema: string
procedureName: string
procedureArguments?: SnowflakeBinding[]
}
export interface SnowflakeColumn {
name: string
type: string
length: number | null
precision: number | null
scale: number | null
nullable: boolean
}
export interface SnowflakePartition {
rowCount: number
compressedSize: number | null
uncompressedSize: number
}
export interface SnowflakeStatementOutput {
statementHandle: string
status: 'SUCCEEDED' | 'RUNNING' | 'CANCELED'
code: string | null
sqlState: string | null
message: string
columns: SnowflakeColumn[]
rows: Array<Array<string | null>>
totalRows: number | null
partitions: SnowflakePartition[]
currentPartition: number
nextPartition: number | null
truncated: boolean
rowsInserted: number
rowsUpdated: number
rowsDeleted: number
duplicateRowsUpdated: number
rowsAffected: number
}
export interface SnowflakeStatementResponse extends ToolResponse {
output: SnowflakeStatementOutput
}
export type SnowflakeExecuteSqlResponse = SnowflakeStatementResponse
export type SnowflakeGetStatementResponse = SnowflakeStatementResponse
export type SnowflakeCancelStatementResponse = SnowflakeStatementResponse
export type SnowflakeInsertRowsResponse = SnowflakeStatementResponse
export type SnowflakeUpdateRowsResponse = SnowflakeStatementResponse
export type SnowflakeUpsertRowsResponse = SnowflakeStatementResponse
export type SnowflakeDeleteRowsResponse = SnowflakeStatementResponse
export type SnowflakeLoadDataResponse = SnowflakeStatementResponse
export type SnowflakeListWarehousesResponse = SnowflakeStatementResponse
export type SnowflakeGetWarehouseResponse = SnowflakeStatementResponse
export type SnowflakeResumeWarehouseResponse = SnowflakeStatementResponse
export type SnowflakeSuspendWarehouseResponse = SnowflakeStatementResponse
export type SnowflakeListTasksResponse = SnowflakeStatementResponse
export type SnowflakeGetTaskResponse = SnowflakeStatementResponse
export type SnowflakeRunTaskResponse = SnowflakeStatementResponse
export type SnowflakeListTaskRunsResponse = SnowflakeStatementResponse
export type SnowflakeGetTaskRunResponse = SnowflakeStatementResponse
export type SnowflakeCancelTaskRunResponse = SnowflakeStatementResponse
export type SnowflakeGetTaskRunOutputResponse = SnowflakeStatementResponse
export type SnowflakeIntrospectSchemaResponse = SnowflakeStatementResponse
export type SnowflakeCallProcedureResponse = SnowflakeStatementResponse
+65
View File
@@ -0,0 +1,65 @@
import { buildUpdateRows } from '@/tools/snowflake/sql'
import type {
SnowflakeUpdateRowsParams,
SnowflakeUpdateRowsResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const updateRowsTool: ToolConfig<SnowflakeUpdateRowsParams, SnowflakeUpdateRowsResponse> = {
id: 'snowflake_update_rows',
version: '1.0.0',
name: 'Snowflake Update Rows',
description: 'Update matching rows with a bound MERGE statement without inserting new rows.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target Snowflake table name within the selected database and schema context',
},
rows: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Non-empty JSON array of row objects with matching keys',
},
matchColumns: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'Columns used to match target rows',
items: { type: 'string' },
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildUpdateRows(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+65
View File
@@ -0,0 +1,65 @@
import { buildUpsertRows } from '@/tools/snowflake/sql'
import type {
SnowflakeUpsertRowsParams,
SnowflakeUpsertRowsResponse,
} from '@/tools/snowflake/types'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
normalizeSnowflakeHost,
snowflakeBaseParams,
snowflakeContextParams,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
export const upsertRowsTool: ToolConfig<SnowflakeUpsertRowsParams, SnowflakeUpsertRowsResponse> = {
id: 'snowflake_upsert_rows',
version: '1.0.0',
name: 'Snowflake Upsert Rows',
description: 'Update matching rows and insert unmatched rows with a bound MERGE statement.',
params: {
...snowflakeBaseParams,
...snowflakeContextParams,
database: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Database name',
},
schema: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Schema name',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target Snowflake table name within the selected database and schema context',
},
rows: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Non-empty JSON array of row objects with matching keys',
},
matchColumns: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'Columns used to match target rows',
items: { type: 'string' },
},
},
request: {
url: (params) => `${normalizeSnowflakeHost(params.host)}/api/v2/statements`,
method: 'POST',
headers: getSnowflakeHeaders,
body: (params) => buildSnowflakeStatementBody(params, buildUpsertRows(params)),
},
transformResponse: (response, params) => transformSnowflakeResponse(response, 0, params?.maxRows),
outputs: SNOWFLAKE_STATEMENT_OUTPUTS,
}
+459
View File
@@ -0,0 +1,459 @@
import { describe, expect, it } from 'vitest'
import * as snowflakeTools from '@/tools/snowflake'
import { cancelStatementTool } from '@/tools/snowflake/cancel_statement'
import { executeSqlTool } from '@/tools/snowflake/execute_sql'
import { getStatementTool } from '@/tools/snowflake/get_statement'
import { listTaskRunsTool } from '@/tools/snowflake/list_task_runs'
import { listTasksTool } from '@/tools/snowflake/list_tasks'
import { SNOWFLAKE_STATEMENT_OUTPUTS } from '@/tools/snowflake/types'
import {
buildSnowflakeStatementBody,
getSnowflakeHeaders,
MAX_REQUEST_BYTES,
MAX_RESPONSE_BYTES,
normalizeMaxRows,
normalizeSnowflakeHost,
transformSnowflakeResponse,
} from '@/tools/snowflake/utils'
import type { ToolConfig } from '@/tools/types'
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
describe('Snowflake SQL API transport', () => {
it('declares all 21 operations as complete standard tool configurations', () => {
const tools = Object.values(snowflakeTools).filter(
(value): value is ToolConfig =>
typeof value === 'object' && value !== null && 'id' in value && 'request' in value
)
const operationParams: Record<string, string[]> = {
snowflake_call_procedure: ['procedureName', 'procedureArguments'],
snowflake_cancel_statement: ['statementHandle'],
snowflake_cancel_task_run: ['queryId'],
snowflake_delete_rows: ['table', 'filters'],
snowflake_execute_sql: ['statement', 'bindings', 'async'],
snowflake_get_statement: ['statementHandle', 'partition', 'maxRows'],
snowflake_get_task: ['taskName'],
snowflake_get_task_run: ['queryId', 'taskName', 'startTime', 'endTime'],
snowflake_get_task_run_output: ['queryId'],
snowflake_get_warehouse: ['warehouseName'],
snowflake_insert_rows: ['table', 'rows'],
snowflake_introspect_schema: ['table', 'includeViews'],
snowflake_list_task_runs: ['taskName', 'startTime', 'endTime', 'errorOnly', 'limit'],
snowflake_list_tasks: ['nameLike', 'limit'],
snowflake_list_warehouses: ['nameLike'],
snowflake_load_data: [
'table',
'stagePath',
'fileFormat',
'pattern',
'onError',
'purge',
'force',
'matchByColumnName',
],
snowflake_resume_warehouse: ['warehouseName'],
snowflake_run_task: ['taskName', 'retryLast'],
snowflake_suspend_warehouse: ['warehouseName'],
snowflake_update_rows: ['table', 'rows', 'matchColumns'],
snowflake_upsert_rows: ['table', 'rows', 'matchColumns'],
}
const baseParams = ['host', 'apiKey']
const contextParams = ['warehouse', 'database', 'schema', 'role', 'timeout', 'maxRows']
expect(tools.map((tool) => tool.id).sort()).toEqual(Object.keys(operationParams).sort())
for (const tool of tools) {
const expectedParams = [
...baseParams,
...(tool.id === 'snowflake_get_statement' || tool.id === 'snowflake_cancel_statement'
? []
: contextParams),
...operationParams[tool.id],
]
expect(Object.keys(tool.params).sort()).toEqual([...new Set(expectedParams)].sort())
expect(tool.params.host).toMatchObject({ required: true, visibility: 'user-only' })
expect(tool.params.apiKey).toMatchObject({ required: true, visibility: 'user-only' })
expect(tool.request.url).toBeTypeOf('function')
expect(tool.request.method).toBe(tool.id === 'snowflake_get_statement' ? 'GET' : 'POST')
expect(tool.request.headers).toBeTypeOf('function')
expect(tool.transformResponse).toBeTypeOf('function')
expect(tool.outputs).toBe(SNOWFLAKE_STATEMENT_OUTPUTS)
expect(tool.version).toBe('1.0.0')
if (tool.id === 'snowflake_get_statement' || tool.id === 'snowflake_cancel_statement') {
expect(tool.request.body).toBeUndefined()
} else {
expect(tool.request.body).toBeTypeOf('function')
}
}
const toolsById = Object.fromEntries(tools.map((tool) => [tool.id, tool]))
const urlParams = {
host: 'acme.snowflakecomputing.com',
apiKey: 'token',
statementHandle: 'statement-handle',
partition: 2,
async: true,
}
const resolveUrl = (id: string) =>
(toolsById[id].request.url as unknown as (params: typeof urlParams) => string)(urlParams)
expect(resolveUrl('snowflake_get_statement')).toBe(
'https://acme.snowflakecomputing.com/api/v2/statements/statement-handle?partition=2'
)
expect(resolveUrl('snowflake_cancel_statement')).toBe(
'https://acme.snowflakecomputing.com/api/v2/statements/statement-handle/cancel'
)
expect(resolveUrl('snowflake_execute_sql')).toBe(
'https://acme.snowflakecomputing.com/api/v2/statements?async=true'
)
for (const tool of tools) {
if (
tool.id !== 'snowflake_get_statement' &&
tool.id !== 'snowflake_cancel_statement' &&
tool.id !== 'snowflake_execute_sql'
) {
expect(resolveUrl(tool.id)).toBe('https://acme.snowflakecomputing.com/api/v2/statements')
}
}
expect(toolsById.snowflake_list_tasks.params.database.required).toBe(true)
expect(toolsById.snowflake_list_tasks.params.schema.required).toBe(true)
})
it('normalizes Snowflake account hosts and rejects untrusted destinations', () => {
expect(normalizeSnowflakeHost('acme-prod.snowflakecomputing.com')).toBe(
'https://acme-prod.snowflakecomputing.com'
)
expect(normalizeSnowflakeHost('https://acme-prod.snowflakecomputing.cn')).toBe(
'https://acme-prod.snowflakecomputing.cn'
)
expect(() => normalizeSnowflakeHost('http://acme.snowflakecomputing.com')).toThrow('HTTPS')
expect(() => normalizeSnowflakeHost('snowflakecomputing.com.evil.test')).toThrow(
'account hostname'
)
expect(() => normalizeSnowflakeHost('https://user@acme.snowflakecomputing.com')).toThrow(
'only the account hostname'
)
expect(() => normalizeSnowflakeHost('acme.snowflakecomputing.com/api')).toThrow(
'only the account hostname'
)
})
it('sets PAT-specific authorization headers', () => {
expect(
getSnowflakeHeaders({ host: 'acme.snowflakecomputing.com', apiKey: ' secret ' })
).toMatchObject({
Authorization: 'Bearer secret',
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
Accept: 'application/json',
'Content-Type': 'application/json',
})
expect(() => getSnowflakeHeaders({ host: 'acme.snowflakecomputing.com', apiKey: ' ' })).toThrow(
'token is required'
)
})
it('uses the documented submit, status, partition, and cancel endpoints', () => {
const auth = { host: 'acme.snowflakecomputing.com', apiKey: 'secret' }
const executeUrl = executeSqlTool.request.url
const getUrl = getStatementTool.request.url
const cancelUrl = cancelStatementTool.request.url
if (
typeof executeUrl !== 'function' ||
typeof getUrl !== 'function' ||
typeof cancelUrl !== 'function'
) {
throw new Error('Snowflake tool URLs must be functions')
}
expect(executeUrl({ ...auth, statement: 'SELECT 1', async: true })).toBe(
'https://acme.snowflakecomputing.com/api/v2/statements?async=true'
)
expect(getUrl({ ...auth, statementHandle: ' handle ', partition: 2 })).toBe(
'https://acme.snowflakecomputing.com/api/v2/statements/handle?partition=2'
)
expect(cancelUrl({ ...auth, statementHandle: ' handle ' })).toBe(
'https://acme.snowflakecomputing.com/api/v2/statements/handle/cancel'
)
})
it('enforces result row and request byte limits', () => {
expect(normalizeMaxRows()).toBe(1000)
expect(normalizeMaxRows(10_000)).toBe(10_000)
expect(() => normalizeMaxRows(10_001)).toThrow('between 1 and 10000')
expect(() =>
buildSnowflakeStatementBody(
{ host: 'acme.snowflakecomputing.com', apiKey: 'secret' },
{ statement: 'x'.repeat(MAX_REQUEST_BYTES) }
)
).toThrow('exceeds')
})
it('builds a bounded SQL API request body with execution context and bindings', () => {
expect(
buildSnowflakeStatementBody(
{
host: 'acme.snowflakecomputing.com',
apiKey: 'secret',
warehouse: 'compute_wh',
database: 'analytics',
schema: '"Mixed Schema"',
role: '"Analyst ""Plus"""',
timeout: 30,
maxRows: 25,
},
{ statement: 'SELECT ?', bindings: { '1': { type: 'FIXED', value: '7' } } }
)
).toEqual({
statement: 'SELECT ?',
timeout: 30,
warehouse: 'COMPUTE_WH',
database: 'ANALYTICS',
schema: 'Mixed Schema',
role: 'Analyst "Plus"',
parameters: { rows_per_resultset: 25 },
bindings: { '1': { type: 'FIXED', value: '7' } },
})
})
it('uses list limits for the SQL API result-set cap when maxRows is omitted', () => {
const auth = { host: 'acme.snowflakecomputing.com', apiKey: 'secret' }
const listTasksBody = listTasksTool.request.body
const listRunsBody = listTaskRunsTool.request.body
if (typeof listTasksBody !== 'function' || typeof listRunsBody !== 'function') {
throw new Error('Snowflake list tool request bodies must be functions')
}
expect(
listTasksBody({ ...auth, database: 'ANALYTICS', schema: 'PUBLIC', limit: 5000 })
).toMatchObject({ parameters: { rows_per_resultset: 5000 } })
expect(listRunsBody({ ...auth, limit: 2500 })).toMatchObject({
parameters: { rows_per_resultset: 2500 },
})
})
it('normalizes completed results, partitions, strings/nulls, and DML statistics', async () => {
const result = await transformSnowflakeResponse(
jsonResponse({
statementHandle: 'handle',
message: 'Statement executed successfully.',
data: [
['1', null],
['2', 'x'],
],
resultSetMetaData: {
numRows: 4,
rowType: [{ name: 'ID', type: 'fixed', precision: 38, scale: 0, nullable: false }],
partitionInfo: [
{ rowCount: 2, compressedSize: 20, uncompressedSize: 40 },
{ rowCount: 2, compressedSize: 20, uncompressedSize: 40 },
],
stats: {
numRowsInserted: 2,
numRowsUpdated: 1,
numRowsDeleted: 3,
numDuplicateRowsUpdated: 1,
},
},
}),
0,
1
)
expect(result.output).toMatchObject({
statementHandle: 'handle',
status: 'SUCCEEDED',
rows: [['1', null]],
totalRows: 4,
nextPartition: 1,
truncated: true,
rowsInserted: 2,
rowsUpdated: 1,
rowsDeleted: 3,
duplicateRowsUpdated: 1,
rowsAffected: 6,
})
})
it.each([
{
columns: ['number of rows inserted'],
row: ['3'],
expected: { rowsInserted: 3, rowsAffected: 3 },
},
{
columns: ['number of rows updated', 'number of multi-joined rows updated'],
row: ['2', '1'],
expected: { rowsUpdated: 2, duplicateRowsUpdated: 1, rowsAffected: 2 },
},
{
columns: [
'number of rows inserted',
'number of rows updated',
'number of multi-joined rows updated',
],
row: ['1', '4', '2'],
expected: {
rowsInserted: 1,
rowsUpdated: 4,
duplicateRowsUpdated: 2,
rowsAffected: 5,
},
},
{
columns: ['number of rows deleted'],
row: ['7'],
expected: { rowsDeleted: 7, rowsAffected: 7 },
},
])('derives typed DML statistics from $columns', async ({ columns, row, expected }) => {
const result = await transformSnowflakeResponse(
jsonResponse({
data: [row],
resultSetMetaData: { rowType: columns.map((name) => ({ name, type: 'fixed' })) },
})
)
expect(result.output).toMatchObject(expected)
})
it('sums loaded rows from canonical COPY results', async () => {
const result = await transformSnowflakeResponse(
jsonResponse({
data: [
['one.csv', 'LOADED', '2'],
['two.csv', 'LOADED', '3'],
['three.csv', 'LOAD_FAILED', null],
],
resultSetMetaData: {
rowType: [
{ name: 'FILE', type: 'text' },
{ name: 'STATUS', type: 'text' },
{ name: 'ROWS_LOADED', type: 'fixed' },
],
},
})
)
expect(result.output).toMatchObject({ rowsInserted: 5, rowsAffected: 5 })
})
it('prefers metadata statistics over derived COPY counts', async () => {
const result = await transformSnowflakeResponse(
jsonResponse({
data: [
['one.csv', 'LOADED', '2'],
['two.csv', 'LOADED', '3'],
['three.csv', 'LOAD_FAILED', null],
],
resultSetMetaData: {
rowType: [
{ name: 'file', type: 'text' },
{ name: 'status', type: 'text' },
{ name: 'rows_loaded', type: 'fixed' },
],
stats: { numRowsInserted: 8, numRowsDeleted: 1 },
},
})
)
expect(result.output).toMatchObject({
rowsInserted: 8,
rowsDeleted: 1,
rowsAffected: 9,
})
})
it('fills missing metadata statistics from result columns', async () => {
const result = await transformSnowflakeResponse(
jsonResponse({
data: [['5', '6', '1']],
resultSetMetaData: {
rowType: [
{ name: 'number of rows inserted', type: 'fixed' },
{ name: 'number of rows updated', type: 'fixed' },
{ name: 'number of multi-joined rows updated', type: 'fixed' },
],
stats: { numRowsInserted: 2 },
},
})
)
expect(result.output).toMatchObject({
rowsInserted: 2,
rowsUpdated: 6,
duplicateRowsUpdated: 1,
rowsAffected: 8,
})
})
it.each([
{
columns: ['number of rows inserted', 'label'],
row: ['9', 'not a DML result'],
},
{ columns: ['number of rows deleted'], row: ['-1'] },
{ columns: ['number of rows updated'], row: ['1.5'] },
{ columns: ['number of rows inserted'], row: ['9007199254740992'] },
])('does not derive statistics from invalid result data', async ({ columns, row }) => {
const result = await transformSnowflakeResponse(
jsonResponse({
data: [row],
resultSetMetaData: { rowType: columns.map((name) => ({ name, type: 'fixed' })) },
})
)
expect(result.output).toMatchObject({
rowsInserted: 0,
rowsUpdated: 0,
rowsDeleted: 0,
duplicateRowsUpdated: 0,
rowsAffected: 0,
})
})
it('treats HTTP 202 with a statement handle as pending', async () => {
const result = await transformSnowflakeResponse(
jsonResponse({ statementHandle: 'handle', message: 'Running' }, 202)
)
expect(result.output.status).toBe('RUNNING')
})
it('treats HTTP 429 as a rate-limit error even when Snowflake returns a statement handle', async () => {
await expect(
transformSnowflakeResponse(
jsonResponse({ statementHandle: 'handle', message: 'Too many requests' }, 429)
)
).rejects.toThrow('Too many requests')
})
it('uses the documented Link header for subsequent partition pagination', async () => {
const response = jsonResponse({ statementHandle: 'handle', data: [['next']] })
response.headers.set(
'Link',
'</api/v2/statements/handle?partition=2>; rel="next", </api/v2/statements/handle?partition=3>; rel="last"'
)
const result = await transformSnowflakeResponse(response, 1, 100)
expect(result.output).toMatchObject({ currentPartition: 1, nextPartition: 2, truncated: true })
})
it('marks Snowflake incomplete result-set responses as truncated', async () => {
const result = await transformSnowflakeResponse(
jsonResponse({ statementHandle: 'handle', code: '391908', data: [['partial']] })
)
expect(result.output).toMatchObject({ code: '391908', truncated: true })
})
it('rejects HTTP and SQL-level failures', async () => {
await expect(
transformSnowflakeResponse(jsonResponse({ message: 'Forbidden', code: '390100' }, 401))
).rejects.toThrow('Forbidden')
await expect(
transformSnowflakeResponse(
jsonResponse({ sqlState: '42000', code: '001003', message: 'SQL compilation error' })
)
).rejects.toThrow('SQLSTATE 42000')
await expect(
transformSnowflakeResponse(
new Response('{}', { headers: { 'Content-Length': String(MAX_RESPONSE_BYTES + 1) } })
)
).rejects.toThrow('response exceeds')
})
})
+412
View File
@@ -0,0 +1,412 @@
import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits'
import type {
SnowflakeBaseParams,
SnowflakeBinding,
SnowflakeColumn,
SnowflakeContextParams,
SnowflakePartition,
SnowflakeStatementOutput,
SnowflakeStatementResponse,
} from '@/tools/snowflake/types'
import type { ToolConfig } from '@/tools/types'
export const DEFAULT_MAX_ROWS = 1_000
export const MAX_RESULT_ROWS = 10_000
export const MAX_WRITE_ROWS = 1_000
export const MAX_REQUEST_BYTES = 1024 * 1024
export const MAX_RESPONSE_BYTES = 32 * 1024 * 1024
const SNOWFLAKE_HOST_SUFFIXES = ['.snowflakecomputing.com', '.snowflakecomputing.cn']
interface SnowflakeApiColumn {
name?: string
type?: string
length?: number | null
precision?: number | null
scale?: number | null
nullable?: boolean
}
interface SnowflakeApiPartition {
rowCount?: number
compressedSize?: number
uncompressedSize?: number
}
interface SnowflakeApiResponse {
code?: string
sqlState?: string
message?: string
statementHandle?: string
statementStatusUrl?: string
data?: Array<Array<string | null>>
resultSetMetaData?: {
numRows?: number
rowType?: SnowflakeApiColumn[]
partitionInfo?: SnowflakeApiPartition[]
stats?: {
numRowsInserted?: number
numRowsUpdated?: number
numRowsDeleted?: number
numDuplicateRowsUpdated?: number
}
}
}
interface SnowflakeDmlStats {
rowsInserted?: number
rowsUpdated?: number
rowsDeleted?: number
duplicateRowsUpdated?: number
}
type SnowflakeDmlStat = keyof SnowflakeDmlStats
const DML_RESULT_COLUMNS: Record<string, SnowflakeDmlStat> = {
'number of rows inserted': 'rowsInserted',
'number of rows updated': 'rowsUpdated',
'number of rows deleted': 'rowsDeleted',
'number of multi-joined rows updated': 'duplicateRowsUpdated',
'number of duplicate rows updated': 'duplicateRowsUpdated',
}
const DML_RESULT_SHAPES = new Set([
'rowsDeleted',
'rowsInserted',
'rowsUpdated',
'duplicateRowsUpdated|rowsUpdated',
'duplicateRowsUpdated|rowsInserted|rowsUpdated',
'rowsInserted|rowsUpdated',
])
export interface SnowflakeStatementSpec {
statement: string
bindings?: Record<string, SnowflakeBinding>
}
type ToolParam = ToolConfig['params'][string]
export const snowflakeBaseParams = {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Snowflake account host, for example myorg-myaccount.snowflakecomputing.com',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Snowflake programmatic access token',
},
} satisfies Record<string, ToolParam>
export const snowflakeContextParams = {
warehouse: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Warehouse to use for this statement; defaults to the PAT user setting',
},
database: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Database context for this statement',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Schema context for this statement',
},
role: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Snowflake role to use for this statement',
},
timeout: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Statement timeout in seconds; 0 uses Snowflake maximum of 604800 seconds',
},
maxRows: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum result rows, from 1 to 10000; defaults to 1000',
},
} satisfies Record<string, ToolParam>
export function normalizeSnowflakeHost(host: string): string {
const raw = host.trim()
if (!raw) throw new Error('Snowflake host is required')
let url: URL
try {
url = new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`)
} catch {
throw new Error('Invalid Snowflake account host')
}
if (url.protocol !== 'https:') throw new Error('Snowflake host must use HTTPS')
if (url.href !== `https://${url.hostname}/`) {
throw new Error('Snowflake host must contain only the account hostname')
}
const hostname = url.hostname.toLowerCase()
if (!SNOWFLAKE_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix))) {
throw new Error('Snowflake host must be an official Snowflake account hostname')
}
return `https://${hostname}`
}
export function getSnowflakeHeaders(params: SnowflakeBaseParams): Record<string, string> {
const apiKey = params.apiKey.trim()
if (!apiKey) throw new Error('Snowflake programmatic access token is required')
return {
Authorization: `Bearer ${apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'sim-snowflake-integration/1.0',
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
}
}
export function normalizeMaxRows(value?: number): number {
const maxRows = value ?? DEFAULT_MAX_ROWS
if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > MAX_RESULT_ROWS) {
throw new Error(`maxRows must be an integer between 1 and ${MAX_RESULT_ROWS}`)
}
return maxRows
}
export function normalizeTimeout(value?: number): number | undefined {
if (value === undefined) return undefined
if (!Number.isInteger(value) || value < 0 || value > 604_800) {
throw new Error('timeout must be an integer between 0 and 604800 seconds')
}
return value
}
function normalizeContextName(value: string): string {
const trimmed = value.trim()
if (/^[A-Za-z_][A-Za-z0-9_$]*$/.test(trimmed)) return trimmed.toUpperCase()
if (/^"(?:[^"]|"")+"$/.test(trimmed)) {
return trimmed.slice(1, -1).replaceAll('""', '"')
}
return trimmed
}
export function addSnowflakeRequestBytes(total: number, ...values: string[]): number {
let next = total
for (const value of values) {
const remaining = MAX_REQUEST_BYTES - next
if (remaining < 0 || value.length > remaining) {
throw new Error(`Snowflake request body exceeds ${MAX_REQUEST_BYTES} bytes`)
}
next += new TextEncoder().encode(value).byteLength
if (next > MAX_REQUEST_BYTES) {
throw new Error(`Snowflake request body exceeds ${MAX_REQUEST_BYTES} bytes`)
}
}
return next
}
export function addSnowflakeRequestOverhead(total: number, bytes: number): number {
if (total > MAX_REQUEST_BYTES - bytes) {
throw new Error(`Snowflake request body exceeds ${MAX_REQUEST_BYTES} bytes`)
}
return total + bytes
}
export function buildSnowflakeStatementBody(
params: SnowflakeContextParams,
spec: SnowflakeStatementSpec
): Record<string, unknown> {
if (!/\S/.test(spec.statement)) throw new Error('Snowflake statement is required')
let requestBytes = addSnowflakeRequestBytes(256, spec.statement)
for (const value of [params.warehouse, params.database, params.schema, params.role]) {
if (value !== undefined) requestBytes = addSnowflakeRequestBytes(requestBytes, value)
}
let hasBindings = false
for (const position in spec.bindings) {
if (!Object.hasOwn(spec.bindings, position)) continue
hasBindings = true
const binding = spec.bindings[position]
requestBytes = addSnowflakeRequestOverhead(requestBytes, 32)
requestBytes = addSnowflakeRequestBytes(requestBytes, position, binding.type, binding.value)
}
const body: Record<string, unknown> = {
statement: spec.statement,
parameters: { rows_per_resultset: normalizeMaxRows(params.maxRows) },
}
const timeout = normalizeTimeout(params.timeout)
if (timeout !== undefined) body.timeout = timeout
if (params.warehouse?.trim()) body.warehouse = normalizeContextName(params.warehouse)
if (params.database?.trim()) body.database = normalizeContextName(params.database)
if (params.schema?.trim()) body.schema = normalizeContextName(params.schema)
if (params.role?.trim()) body.role = normalizeContextName(params.role)
if (hasBindings) body.bindings = spec.bindings
const size = new TextEncoder().encode(JSON.stringify(body)).byteLength
if (size > MAX_REQUEST_BYTES) {
throw new Error(`Snowflake request body exceeds ${MAX_REQUEST_BYTES} bytes`)
}
return body
}
function mapColumn(column: SnowflakeApiColumn): SnowflakeColumn {
return {
name: column.name ?? '',
type: column.type ?? '',
length: column.length ?? null,
precision: column.precision ?? null,
scale: column.scale ?? null,
nullable: column.nullable ?? true,
}
}
function mapPartition(partition: SnowflakeApiPartition): SnowflakePartition {
return {
rowCount: partition.rowCount ?? 0,
compressedSize: partition.compressedSize ?? null,
uncompressedSize: partition.uncompressedSize ?? 0,
}
}
function normalizeResultColumnName(name?: string): string {
return name?.trim().toLowerCase().replace(/\s+/g, ' ') ?? ''
}
function parseRowCount(value: string | null | undefined): number | undefined {
if (value === null || value === undefined || !/^[0-9]+$/.test(value.trim())) return undefined
const count = Number(value)
return Number.isSafeInteger(count) ? count : undefined
}
function deriveDmlStats(
columns: SnowflakeApiColumn[],
rows: Array<Array<string | null>>
): SnowflakeDmlStats {
const columnNames = columns.map((column) => normalizeResultColumnName(column.name))
const copyColumnIndexes = new Map(columnNames.map((name, index) => [name, index]))
if (
copyColumnIndexes.has('file') &&
copyColumnIndexes.has('status') &&
copyColumnIndexes.has('rows_loaded')
) {
const rowsLoadedIndex = copyColumnIndexes.get('rows_loaded')
if (rowsLoadedIndex === undefined) return {}
let rowsInserted = 0
let foundCount = false
for (const row of rows) {
const count = parseRowCount(row[rowsLoadedIndex])
if (count === undefined) continue
if (!Number.isSafeInteger(rowsInserted + count)) return {}
rowsInserted += count
foundCount = true
}
return foundCount ? { rowsInserted } : {}
}
if (rows.length !== 1) return {}
const statColumns = columnNames.map((name) => DML_RESULT_COLUMNS[name])
if (statColumns.some((column) => column === undefined)) return {}
const shape = [...new Set(statColumns)].sort().join('|')
if (!DML_RESULT_SHAPES.has(shape)) return {}
const stats: SnowflakeDmlStats = {}
for (const [index, stat] of statColumns.entries()) {
if (!stat) continue
const count = parseRowCount(rows[0][index])
if (count !== undefined) stats[stat] = count
}
return stats
}
export async function transformSnowflakeResponse(
response: Response,
currentPartition = 0,
requestedMaxRows = DEFAULT_MAX_ROWS,
canceled = false,
fallbackStatementHandle = ''
): Promise<SnowflakeStatementResponse> {
const data = await readSnowflakeResponse(response)
const pending = response.status === 202
if (!response.ok && !pending) {
throw new Error(
data.message ||
`Snowflake request failed with HTTP ${response.status}${data.code ? ` (${data.code})` : ''}`
)
}
if (data.sqlState && data.sqlState !== '00000' && !canceled) {
throw new Error(
`Snowflake statement failed (SQLSTATE ${data.sqlState}${data.code ? `, ${data.code}` : ''}): ${data.message ?? 'Unknown error'}`
)
}
const maxRows = normalizeMaxRows(requestedMaxRows)
const sourceRows = data.data ?? []
const rows = sourceRows.slice(0, maxRows)
const columns = (data.resultSetMetaData?.rowType ?? []).map(mapColumn)
const partitions = (data.resultSetMetaData?.partitionInfo ?? []).map(mapPartition)
const partitionCount = partitions.length
const linkedNextPartition = getLinkedPartition(response.headers.get('Link'), 'next')
const nextPartition =
linkedNextPartition ?? (currentPartition + 1 < partitionCount ? currentPartition + 1 : null)
const stats = data.resultSetMetaData?.stats
const derivedStats = deriveDmlStats(data.resultSetMetaData?.rowType ?? [], sourceRows)
const rowsInserted = stats?.numRowsInserted ?? derivedStats.rowsInserted ?? 0
const rowsUpdated = stats?.numRowsUpdated ?? derivedStats.rowsUpdated ?? 0
const rowsDeleted = stats?.numRowsDeleted ?? derivedStats.rowsDeleted ?? 0
const duplicateRowsUpdated =
stats?.numDuplicateRowsUpdated ?? derivedStats.duplicateRowsUpdated ?? 0
const output: SnowflakeStatementOutput = {
statementHandle: data.statementHandle ?? fallbackStatementHandle,
status: canceled ? 'CANCELED' : pending ? 'RUNNING' : 'SUCCEEDED',
code: data.code ?? null,
sqlState: data.sqlState ?? null,
message: data.message ?? (pending ? 'Statement is still running' : 'Statement completed'),
columns,
rows,
totalRows: data.resultSetMetaData?.numRows ?? null,
partitions,
currentPartition,
nextPartition,
truncated: data.code === '391908' || sourceRows.length > rows.length || nextPartition !== null,
rowsInserted,
rowsUpdated,
rowsDeleted,
duplicateRowsUpdated,
rowsAffected: rowsInserted + rowsUpdated + rowsDeleted,
}
return { success: true, output }
}
async function readSnowflakeResponse(response: Response): Promise<SnowflakeApiResponse> {
const text = await readResponseTextWithLimit(response, {
maxBytes: MAX_RESPONSE_BYTES,
label: 'Snowflake response',
})
if (!text) return {}
try {
return JSON.parse(text) as SnowflakeApiResponse
} catch {
throw new Error('Snowflake returned an invalid JSON response')
}
}
function getLinkedPartition(linkHeader: string | null, relation: string): number | null {
if (!linkHeader) return null
for (const link of linkHeader.split(',')) {
if (!new RegExp(`rel=["']?${relation}["']?`, 'i').test(link)) continue
const match = link.match(/[?&]partition=([0-9]+)/i)
if (match) return Number(match[1])
}
return null
}