From ce3d33b223991273467b0377696ccbf3826acb42 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 10:49:16 -0700 Subject: [PATCH 1/5] feat(bigquery): validate integration against live API docs, add dataset/table lifecycle + query-result tools - Cross-checked existing query/list_datasets/list_tables/get_table/insert_rows tools against BigQuery REST v2 docs; no critical issues found - Added 6 new tools covered by the existing bigquery OAuth scope: create/delete dataset, create/delete table, list table data, get query results - Wired new operations into the block (subblocks, conditions, tools.config), registry, and barrel exports --- apps/sim/blocks/blocks/google_bigquery.ts | 277 ++++++++++++++++-- .../tools/google_bigquery/create_dataset.ts | 122 ++++++++ .../sim/tools/google_bigquery/create_table.ts | 157 ++++++++++ .../tools/google_bigquery/delete_dataset.ts | 82 ++++++ .../sim/tools/google_bigquery/delete_table.ts | 75 +++++ .../google_bigquery/get_query_results.ts | 175 +++++++++++ apps/sim/tools/google_bigquery/index.ts | 6 + .../tools/google_bigquery/list_table_data.ts | 133 +++++++++ apps/sim/tools/google_bigquery/types.ts | 105 +++++++ apps/sim/tools/registry.ts | 12 + 10 files changed, 1125 insertions(+), 19 deletions(-) create mode 100644 apps/sim/tools/google_bigquery/create_dataset.ts create mode 100644 apps/sim/tools/google_bigquery/create_table.ts create mode 100644 apps/sim/tools/google_bigquery/delete_dataset.ts create mode 100644 apps/sim/tools/google_bigquery/delete_table.ts create mode 100644 apps/sim/tools/google_bigquery/get_query_results.ts create mode 100644 apps/sim/tools/google_bigquery/list_table_data.ts diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 0f2fad19920..e95cae3a3aa 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -22,9 +22,15 @@ export const GoogleBigQueryBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Run Query', id: 'query' }, + { label: 'Get Query Results', id: 'get_query_results' }, { label: 'List Datasets', id: 'list_datasets' }, + { label: 'Create Dataset', id: 'create_dataset' }, + { label: 'Delete Dataset', id: 'delete_dataset' }, { label: 'List Tables', id: 'list_tables' }, { label: 'Get Table', id: 'get_table' }, + { label: 'Create Table', id: 'create_table' }, + { label: 'Delete Table', id: 'delete_table' }, + { label: 'List Table Data', id: 'list_table_data' }, { label: 'Insert Rows', id: 'insert_rows' }, ], value: () => 'query', @@ -93,7 +99,10 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, title: 'Max Results', type: 'short-input', placeholder: 'Maximum rows to return', - condition: { field: 'operation', value: ['query', 'list_datasets', 'list_tables'] }, + condition: { + field: 'operation', + value: ['query', 'list_datasets', 'list_tables', 'list_table_data', 'get_query_results'], + }, }, { id: 'defaultDatasetId', @@ -107,7 +116,46 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, title: 'Location', type: 'short-input', placeholder: 'Processing location (e.g., US, EU)', - condition: { field: 'operation', value: 'query' }, + condition: { field: 'operation', value: ['query', 'get_query_results'] }, + }, + + { + id: 'jobId', + title: 'Job ID', + type: 'short-input', + placeholder: 'Enter BigQuery job ID', + condition: { field: 'operation', value: 'get_query_results' }, + required: { field: 'operation', value: 'get_query_results' }, + }, + { + id: 'timeoutMs', + title: 'Timeout (ms)', + type: 'short-input', + placeholder: 'How long to wait for the job to complete', + mode: 'advanced', + condition: { field: 'operation', value: 'get_query_results' }, + }, + + { + id: 'newDatasetId', + title: 'Dataset ID', + type: 'short-input', + placeholder: 'ID for the new BigQuery dataset', + condition: { field: 'operation', value: 'create_dataset' }, + required: { field: 'operation', value: 'create_dataset' }, + }, + { + id: 'datasetLocation', + title: 'Location', + type: 'short-input', + placeholder: 'Geographic location (e.g., US, EU)', + condition: { field: 'operation', value: 'create_dataset' }, + }, + { + id: 'deleteContents', + title: 'Delete Contents', + type: 'switch', + condition: { field: 'operation', value: 'delete_dataset' }, }, { @@ -120,8 +168,30 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, placeholder: 'Select BigQuery dataset', dependsOn: ['credential', 'projectId'], mode: 'basic', - condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + condition: { + field: 'operation', + value: [ + 'list_tables', + 'get_table', + 'insert_rows', + 'delete_dataset', + 'create_table', + 'delete_table', + 'list_table_data', + ], + }, + required: { + field: 'operation', + value: [ + 'list_tables', + 'get_table', + 'insert_rows', + 'delete_dataset', + 'create_table', + 'delete_table', + 'list_table_data', + ], + }, }, { id: 'datasetId', @@ -130,8 +200,30 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'datasetId', placeholder: 'Enter BigQuery dataset ID', mode: 'advanced', - condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + condition: { + field: 'operation', + value: [ + 'list_tables', + 'get_table', + 'insert_rows', + 'delete_dataset', + 'create_table', + 'delete_table', + 'list_table_data', + ], + }, + required: { + field: 'operation', + value: [ + 'list_tables', + 'get_table', + 'insert_rows', + 'delete_dataset', + 'create_table', + 'delete_table', + 'list_table_data', + ], + }, }, { @@ -144,8 +236,14 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, placeholder: 'Select BigQuery table', dependsOn: ['credential', 'projectId', 'datasetSelector'], mode: 'basic', - condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['get_table', 'insert_rows'] }, + condition: { + field: 'operation', + value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], + }, + required: { + field: 'operation', + value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], + }, }, { id: 'tableId', @@ -154,8 +252,77 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'tableId', placeholder: 'Enter BigQuery table ID', mode: 'advanced', - condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['get_table', 'insert_rows'] }, + condition: { + field: 'operation', + value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], + }, + required: { + field: 'operation', + value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], + }, + }, + + { + id: 'newTableId', + title: 'Table ID', + type: 'short-input', + placeholder: 'ID for the new BigQuery table', + condition: { field: 'operation', value: 'create_table' }, + required: { field: 'operation', value: 'create_table' }, + }, + { + id: 'schema', + title: 'Schema', + type: 'long-input', + placeholder: '[{"name": "id", "type": "STRING", "mode": "REQUIRED"}]', + condition: { field: 'operation', value: 'create_table' }, + required: { field: 'operation', value: 'create_table' }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of BigQuery column field definitions based on the user's description. +Each field should have "name", "type" (STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, DATE, RECORD, etc.), and optionally "mode" (NULLABLE, REQUIRED, REPEATED) and "description". + +Examples: +- "id and name" -> [{"name": "id", "type": "STRING", "mode": "REQUIRED"}, {"name": "name", "type": "STRING"}] +- "order with amount and timestamp" -> [{"name": "order_id", "type": "STRING", "mode": "REQUIRED"}, {"name": "amount", "type": "FLOAT"}, {"name": "created_at", "type": "TIMESTAMP"}] + +Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, + placeholder: 'Describe the table columns...', + generationType: 'json-object', + }, + }, + { + id: 'friendlyName', + title: 'Friendly Name', + type: 'short-input', + placeholder: 'Human-readable name', + mode: 'advanced', + condition: { field: 'operation', value: ['create_dataset', 'create_table'] }, + }, + { + id: 'description', + title: 'Description', + type: 'short-input', + placeholder: 'Description', + mode: 'advanced', + condition: { field: 'operation', value: ['create_dataset', 'create_table'] }, + }, + + { + id: 'selectedFields', + title: 'Selected Fields', + type: 'short-input', + placeholder: 'Comma-separated list of column names to return', + mode: 'advanced', + condition: { field: 'operation', value: 'list_table_data' }, + }, + { + id: 'startIndex', + title: 'Start Index', + type: 'short-input', + placeholder: 'Zero-based index of the starting row', + mode: 'advanced', + condition: { field: 'operation', value: ['list_table_data', 'get_query_results'] }, }, { @@ -197,15 +364,24 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, title: 'Page Token', type: 'short-input', placeholder: 'Pagination token', - condition: { field: 'operation', value: ['list_datasets', 'list_tables'] }, + condition: { + field: 'operation', + value: ['list_datasets', 'list_tables', 'list_table_data', 'get_query_results'], + }, }, ], tools: { access: [ 'google_bigquery_query', + 'google_bigquery_get_query_results', 'google_bigquery_list_datasets', + 'google_bigquery_create_dataset', + 'google_bigquery_delete_dataset', 'google_bigquery_list_tables', 'google_bigquery_get_table', + 'google_bigquery_create_table', + 'google_bigquery_delete_table', + 'google_bigquery_list_table_data', 'google_bigquery_insert_rows', ], config: { @@ -213,12 +389,24 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, switch (params.operation) { case 'query': return 'google_bigquery_query' + case 'get_query_results': + return 'google_bigquery_get_query_results' case 'list_datasets': return 'google_bigquery_list_datasets' + case 'create_dataset': + return 'google_bigquery_create_dataset' + case 'delete_dataset': + return 'google_bigquery_delete_dataset' case 'list_tables': return 'google_bigquery_list_tables' case 'get_table': return 'google_bigquery_get_table' + case 'create_table': + return 'google_bigquery_create_table' + case 'delete_table': + return 'google_bigquery_delete_table' + case 'list_table_data': + return 'google_bigquery_list_table_data' case 'insert_rows': return 'google_bigquery_insert_rows' default: @@ -226,12 +414,28 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, } }, params: (params) => { - const { oauthCredential, rows, maxResults, ...rest } = params + const { + oauthCredential, + rows, + schema, + maxResults, + timeoutMs, + newDatasetId, + newTableId, + datasetLocation, + ...rest + } = params return { ...rest, oauthCredential, + ...(params.operation === 'create_dataset' && newDatasetId && { datasetId: newDatasetId }), + ...(params.operation === 'create_dataset' && + datasetLocation && { location: datasetLocation }), + ...(params.operation === 'create_table' && newTableId && { tableId: newTableId }), ...(rows && { rows: typeof rows === 'string' ? rows : JSON.stringify(rows) }), + ...(schema && { schema: typeof schema === 'string' ? schema : JSON.stringify(schema) }), ...(maxResults !== undefined && maxResults !== '' && { maxResults: Number(maxResults) }), + ...(timeoutMs !== undefined && timeoutMs !== '' && { timeoutMs: Number(timeoutMs) }), } }, }, @@ -248,8 +452,19 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, description: 'Default dataset for unqualified table names', }, location: { type: 'string', description: 'Processing location' }, + jobId: { type: 'string', description: 'BigQuery job ID' }, + timeoutMs: { type: 'number', description: 'How long to wait for the job to complete' }, + newDatasetId: { type: 'string', description: 'ID for the new BigQuery dataset' }, + datasetLocation: { type: 'string', description: 'Geographic location for the new dataset' }, + deleteContents: { type: 'boolean', description: 'Whether to delete tables inside the dataset' }, datasetId: { type: 'string', description: 'BigQuery dataset ID' }, tableId: { type: 'string', description: 'BigQuery table ID' }, + newTableId: { type: 'string', description: 'ID for the new BigQuery table' }, + schema: { type: 'string', description: 'JSON array of column field definitions' }, + friendlyName: { type: 'string', description: 'Human-readable name' }, + description: { type: 'string', description: 'Description' }, + selectedFields: { type: 'string', description: 'Comma-separated list of column names' }, + startIndex: { type: 'string', description: 'Zero-based index of the starting row' }, rows: { type: 'string', description: 'JSON array of row objects to insert' }, skipInvalidRows: { type: 'boolean', description: 'Whether to skip invalid rows during insert' }, ignoreUnknownValues: { @@ -270,18 +485,35 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, datasets: { type: 'json', description: 'Array of dataset objects (list_datasets)' }, tables: { type: 'json', description: 'Array of table objects (list_tables)' }, totalItems: { type: 'number', description: 'Total items count (list_tables)' }, - tableId: { type: 'string', description: 'Table ID (get_table)' }, - datasetId: { type: 'string', description: 'Dataset ID (get_table)' }, - type: { type: 'string', description: 'Table type (get_table)' }, - description: { type: 'string', description: 'Table description (get_table)' }, + tableId: { type: 'string', description: 'Table ID (get_table, create_table)' }, + datasetId: { + type: 'string', + description: 'Dataset ID (get_table, create_table, create_dataset)', + }, + type: { type: 'string', description: 'Table type (get_table, create_table)' }, + description: { + type: 'string', + description: 'Table or dataset description (get_table, create_table, create_dataset)', + }, + friendlyName: { type: 'string', description: 'Human-readable name (create_dataset)' }, numRows: { type: 'string', description: 'Row count (get_table)' }, numBytes: { type: 'string', description: 'Size in bytes (get_table)' }, - schema: { type: 'json', description: 'Column definitions (get_table)' }, - creationTime: { type: 'string', description: 'Creation time (get_table)' }, + schema: { type: 'json', description: 'Column definitions (get_table, create_table)' }, + creationTime: { + type: 'string', + description: 'Creation time (get_table, create_table, create_dataset)', + }, lastModifiedTime: { type: 'string', description: 'Last modified time (get_table)' }, - location: { type: 'string', description: 'Data location (get_table)' }, + location: { + type: 'string', + description: 'Data location (get_table, create_table, create_dataset)', + }, insertedRows: { type: 'number', description: 'Rows inserted (insert_rows)' }, errors: { type: 'json', description: 'Insert errors (insert_rows)' }, + deleted: { + type: 'boolean', + description: 'Whether the resource was deleted (delete_dataset, delete_table)', + }, nextPageToken: { type: 'string', description: 'Token for next page of results' }, }, } @@ -381,5 +613,12 @@ export const GoogleBigQueryBlockMeta = { content: '# Load Rows to Table\n\nUse BigQuery to write structured records into a table.\n\n## Steps\n1. Confirm the target dataset and table, and Get Table to verify the expected columns and types.\n2. Shape the incoming records to match the table schema exactly.\n3. Use Insert Rows to write the batch.\n\n## Output\nReturn the count of rows inserted and any rows rejected with their error. If types did not match the schema, report which fields failed rather than silently dropping data.', }, + { + name: 'provision-dataset-and-table', + description: + 'Create a new BigQuery dataset and table with a defined schema for a new pipeline or logging destination.', + content: + '# Provision Dataset and Table\n\nUse BigQuery to set up a new destination for structured data.\n\n## Steps\n1. Use Create Dataset with a dataset ID and location.\n2. Define the column schema (name, type, mode) for the target table.\n3. Use Create Table with that schema inside the new dataset.\n\n## Output\nReturn the created dataset ID, table ID, and the resolved schema so downstream steps can insert rows with confidence the columns match.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/google_bigquery/create_dataset.ts b/apps/sim/tools/google_bigquery/create_dataset.ts new file mode 100644 index 00000000000..d1dd99e8025 --- /dev/null +++ b/apps/sim/tools/google_bigquery/create_dataset.ts @@ -0,0 +1,122 @@ +import type { + GoogleBigQueryCreateDatasetParams, + GoogleBigQueryCreateDatasetResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryCreateDatasetTool: ToolConfig< + GoogleBigQueryCreateDatasetParams, + GoogleBigQueryCreateDatasetResponse +> = { + id: 'google_bigquery_create_dataset', + name: 'BigQuery Create Dataset', + description: 'Create a new dataset in a Google BigQuery project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID for the new BigQuery dataset', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Geographic location for the dataset (e.g., "US", "EU")', + }, + friendlyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Human-readable name for the dataset', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the dataset', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + datasetReference: { + projectId: params.projectId, + datasetId: params.datasetId, + }, + } + if (params.location) body.location = params.location + if (params.friendlyName) body.friendlyName = params.friendlyName + if (params.description) body.description = params.description + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to create BigQuery dataset' + throw new Error(errorMessage) + } + + return { + success: true, + output: { + datasetId: data.datasetReference?.datasetId ?? null, + projectId: data.datasetReference?.projectId ?? null, + friendlyName: data.friendlyName ?? null, + description: data.description ?? null, + location: data.location ?? null, + creationTime: data.creationTime ?? null, + }, + } + }, + + outputs: { + datasetId: { type: 'string', description: 'Unique dataset identifier' }, + projectId: { type: 'string', description: 'Project ID containing this dataset' }, + friendlyName: { + type: 'string', + description: 'Descriptive name for the dataset', + optional: true, + }, + description: { type: 'string', description: 'Dataset description', optional: true }, + location: { + type: 'string', + description: 'Geographic location where the data resides', + optional: true, + }, + creationTime: { + type: 'string', + description: 'Dataset creation time (milliseconds since epoch)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/create_table.ts b/apps/sim/tools/google_bigquery/create_table.ts new file mode 100644 index 00000000000..75fbcadeea9 --- /dev/null +++ b/apps/sim/tools/google_bigquery/create_table.ts @@ -0,0 +1,157 @@ +import type { + GoogleBigQueryCreateTableParams, + GoogleBigQueryCreateTableResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryCreateTableTool: ToolConfig< + GoogleBigQueryCreateTableParams, + GoogleBigQueryCreateTableResponse +> = { + id: 'google_bigquery_create_table', + name: 'BigQuery Create Table', + description: 'Create a new table in a Google BigQuery dataset', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID for the new BigQuery table', + }, + schema: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of column field definitions, e.g. [{"name":"id","type":"STRING","mode":"REQUIRED"}]', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the table', + }, + friendlyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Human-readable name for the table', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}/tables`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const fields = typeof params.schema === 'string' ? JSON.parse(params.schema) : params.schema + + const body: Record = { + tableReference: { + projectId: params.projectId, + datasetId: params.datasetId, + tableId: params.tableId, + }, + schema: { fields }, + } + if (params.description) body.description = params.description + if (params.friendlyName) body.friendlyName = params.friendlyName + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to create BigQuery table' + throw new Error(errorMessage) + } + + const schema = (data.schema?.fields ?? []).map( + (f: { name: string; type: string; mode?: string; description?: string }) => ({ + name: f.name, + type: f.type, + mode: f.mode ?? null, + description: f.description ?? null, + }) + ) + + return { + success: true, + output: { + tableId: data.tableReference?.tableId ?? null, + datasetId: data.tableReference?.datasetId ?? null, + projectId: data.tableReference?.projectId ?? null, + type: data.type ?? null, + description: data.description ?? null, + schema, + creationTime: data.creationTime ?? null, + location: data.location ?? null, + }, + } + }, + + outputs: { + tableId: { type: 'string', description: 'Table ID' }, + datasetId: { type: 'string', description: 'Dataset ID' }, + projectId: { type: 'string', description: 'Project ID' }, + type: { type: 'string', description: 'Table type (usually TABLE)', optional: true }, + description: { type: 'string', description: 'Table description', optional: true }, + schema: { + type: 'array', + description: 'Array of column definitions', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Column name' }, + type: { type: 'string', description: 'Data type' }, + mode: { + type: 'string', + description: 'Column mode (NULLABLE, REQUIRED, or REPEATED)', + optional: true, + }, + description: { type: 'string', description: 'Column description', optional: true }, + }, + }, + }, + creationTime: { + type: 'string', + description: 'Table creation time (milliseconds since epoch)', + optional: true, + }, + location: { + type: 'string', + description: 'Geographic location where the table resides', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/delete_dataset.ts b/apps/sim/tools/google_bigquery/delete_dataset.ts new file mode 100644 index 00000000000..fca5affc7b6 --- /dev/null +++ b/apps/sim/tools/google_bigquery/delete_dataset.ts @@ -0,0 +1,82 @@ +import type { + GoogleBigQueryDeleteDatasetParams, + GoogleBigQueryDeleteDatasetResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryDeleteDatasetTool: ToolConfig< + GoogleBigQueryDeleteDatasetParams, + GoogleBigQueryDeleteDatasetResponse +> = { + id: 'google_bigquery_delete_dataset', + name: 'BigQuery Delete Dataset', + description: 'Delete a dataset from a Google BigQuery project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID to delete', + }, + deleteContents: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to delete tables inside the dataset (default: false)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}` + ) + if (params.deleteContents !== undefined) { + url.searchParams.set('deleteContents', String(params.deleteContents)) + } + return url.toString() + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json() + const errorMessage = data.error?.message || 'Failed to delete BigQuery dataset' + throw new Error(errorMessage) + } + + return { + success: true, + output: { + deleted: true, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the dataset was deleted' }, + }, +} diff --git a/apps/sim/tools/google_bigquery/delete_table.ts b/apps/sim/tools/google_bigquery/delete_table.ts new file mode 100644 index 00000000000..5162fb172a1 --- /dev/null +++ b/apps/sim/tools/google_bigquery/delete_table.ts @@ -0,0 +1,75 @@ +import type { + GoogleBigQueryDeleteTableParams, + GoogleBigQueryDeleteTableResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryDeleteTableTool: ToolConfig< + GoogleBigQueryDeleteTableParams, + GoogleBigQueryDeleteTableResponse +> = { + id: 'google_bigquery_delete_table', + name: 'BigQuery Delete Table', + description: 'Delete a table from a Google BigQuery dataset', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery table ID to delete', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}/tables/${encodeURIComponent(params.tableId.trim())}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json() + const errorMessage = data.error?.message || 'Failed to delete BigQuery table' + throw new Error(errorMessage) + } + + return { + success: true, + output: { + deleted: true, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the table was deleted' }, + }, +} diff --git a/apps/sim/tools/google_bigquery/get_query_results.ts b/apps/sim/tools/google_bigquery/get_query_results.ts new file mode 100644 index 00000000000..89c14307a1f --- /dev/null +++ b/apps/sim/tools/google_bigquery/get_query_results.ts @@ -0,0 +1,175 @@ +import type { + GoogleBigQueryGetQueryResultsParams, + GoogleBigQueryGetQueryResultsResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryGetQueryResultsTool: ToolConfig< + GoogleBigQueryGetQueryResultsParams, + GoogleBigQueryGetQueryResultsResponse +> = { + id: 'google_bigquery_get_query_results', + name: 'BigQuery Get Query Results', + description: + 'Fetch results for a previously submitted BigQuery job, or the next page of a Run Query result', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + jobId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the BigQuery job to fetch results for', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of rows to return', + }, + timeoutMs: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'How long to wait for the job to complete, in milliseconds', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Processing location of the job (e.g., "US", "EU")', + }, + startIndex: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Zero-based index of the starting row', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/queries/${encodeURIComponent(params.jobId.trim())}` + ) + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + if (params.maxResults !== undefined && params.maxResults !== null) { + const maxResults = Number(params.maxResults) + if (Number.isFinite(maxResults) && maxResults > 0) { + url.searchParams.set('maxResults', String(maxResults)) + } + } + if (params.timeoutMs !== undefined && params.timeoutMs !== null) { + url.searchParams.set('timeoutMs', String(Number(params.timeoutMs))) + } + if (params.location) url.searchParams.set('location', params.location) + if (params.startIndex) url.searchParams.set('startIndex', params.startIndex) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to fetch BigQuery query results' + throw new Error(errorMessage) + } + + const columns = (data.schema?.fields ?? []).map((f: { name: string }) => f.name) + const rows = (data.rows ?? []).map((row: { f: Array<{ v: unknown }> }) => { + const obj: Record = {} + row.f.forEach((field, index) => { + obj[columns[index]] = field.v ?? null + }) + return obj + }) + + return { + success: true, + output: { + columns, + rows, + totalRows: data.totalRows ?? null, + jobComplete: data.jobComplete ?? false, + totalBytesProcessed: data.totalBytesProcessed ?? null, + cacheHit: data.cacheHit ?? null, + jobReference: data.jobReference ?? null, + pageToken: data.pageToken ?? null, + }, + } + }, + + outputs: { + columns: { + type: 'array', + description: 'Array of column names from the query result', + items: { type: 'string', description: 'Column name' }, + }, + rows: { + type: 'array', + description: 'Array of row objects keyed by column name', + items: { + type: 'object', + description: 'Row with column name/value pairs', + }, + }, + totalRows: { + type: 'string', + description: 'Total number of rows in the complete result set', + optional: true, + }, + jobComplete: { type: 'boolean', description: 'Whether the job has completed' }, + totalBytesProcessed: { + type: 'string', + description: 'Total bytes processed by the query', + optional: true, + }, + cacheHit: { + type: 'boolean', + description: 'Whether the query result was served from cache', + optional: true, + }, + jobReference: { + type: 'object', + description: 'Job reference (useful when jobComplete is false)', + optional: true, + properties: { + projectId: { type: 'string', description: 'Project ID containing the job' }, + jobId: { type: 'string', description: 'Unique job identifier' }, + location: { type: 'string', description: 'Geographic location of the job' }, + }, + }, + pageToken: { + type: 'string', + description: 'Token for fetching additional result pages', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/index.ts b/apps/sim/tools/google_bigquery/index.ts index ea1aa73371a..b88b3afd385 100644 --- a/apps/sim/tools/google_bigquery/index.ts +++ b/apps/sim/tools/google_bigquery/index.ts @@ -1,5 +1,11 @@ +export { googleBigQueryCreateDatasetTool } from '@/tools/google_bigquery/create_dataset' +export { googleBigQueryCreateTableTool } from '@/tools/google_bigquery/create_table' +export { googleBigQueryDeleteDatasetTool } from '@/tools/google_bigquery/delete_dataset' +export { googleBigQueryDeleteTableTool } from '@/tools/google_bigquery/delete_table' +export { googleBigQueryGetQueryResultsTool } from '@/tools/google_bigquery/get_query_results' export { googleBigQueryGetTableTool } from '@/tools/google_bigquery/get_table' export { googleBigQueryInsertRowsTool } from '@/tools/google_bigquery/insert_rows' export { googleBigQueryListDatasetsTool } from '@/tools/google_bigquery/list_datasets' +export { googleBigQueryListTableDataTool } from '@/tools/google_bigquery/list_table_data' export { googleBigQueryListTablesTool } from '@/tools/google_bigquery/list_tables' export { googleBigQueryQueryTool } from '@/tools/google_bigquery/query' diff --git a/apps/sim/tools/google_bigquery/list_table_data.ts b/apps/sim/tools/google_bigquery/list_table_data.ts new file mode 100644 index 00000000000..c93be577048 --- /dev/null +++ b/apps/sim/tools/google_bigquery/list_table_data.ts @@ -0,0 +1,133 @@ +import type { + GoogleBigQueryListTableDataParams, + GoogleBigQueryListTableDataResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryListTableDataTool: ToolConfig< + GoogleBigQueryListTableDataParams, + GoogleBigQueryListTableDataResponse +> = { + id: 'google_bigquery_list_table_data', + name: 'BigQuery List Table Data', + description: + 'Preview rows from a Google BigQuery table without running a query. Pair with Get Table to know the column order.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery table ID', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of rows to return', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + startIndex: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Zero-based index of the starting row', + }, + selectedFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of column names to return', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}/tables/${encodeURIComponent(params.tableId.trim())}/data` + ) + if (params.maxResults !== undefined && params.maxResults !== null) { + const maxResults = Number(params.maxResults) + if (Number.isFinite(maxResults) && maxResults > 0) { + url.searchParams.set('maxResults', String(maxResults)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + if (params.startIndex) url.searchParams.set('startIndex', params.startIndex) + if (params.selectedFields) url.searchParams.set('selectedFields', params.selectedFields) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to list BigQuery table data' + throw new Error(errorMessage) + } + + const rows = (data.rows ?? []).map((row: { f: Array<{ v: unknown }> }) => + row.f.map((field) => field.v ?? null) + ) + + return { + success: true, + output: { + rows, + totalRows: data.totalRows ?? null, + pageToken: data.pageToken ?? null, + }, + } + }, + + outputs: { + rows: { + type: 'array', + description: 'Array of rows, each a raw array of column values in schema order', + items: { type: 'array', description: 'Row values in column order' }, + }, + totalRows: { + type: 'string', + description: 'Total number of rows in the table', + optional: true, + }, + pageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/types.ts b/apps/sim/tools/google_bigquery/types.ts index 2f2a6515ecf..3ffc605bac6 100644 --- a/apps/sim/tools/google_bigquery/types.ts +++ b/apps/sim/tools/google_bigquery/types.ts @@ -37,6 +37,49 @@ export interface GoogleBigQueryInsertRowsParams extends GoogleBigQueryBaseParams ignoreUnknownValues?: boolean } +export interface GoogleBigQueryCreateDatasetParams extends GoogleBigQueryBaseParams { + datasetId: string + location?: string + friendlyName?: string + description?: string +} + +export interface GoogleBigQueryDeleteDatasetParams extends GoogleBigQueryBaseParams { + datasetId: string + deleteContents?: boolean +} + +export interface GoogleBigQueryCreateTableParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string + schema: string + description?: string + friendlyName?: string +} + +export interface GoogleBigQueryDeleteTableParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string +} + +export interface GoogleBigQueryListTableDataParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string + maxResults?: number + pageToken?: string + startIndex?: string + selectedFields?: string +} + +export interface GoogleBigQueryGetQueryResultsParams extends GoogleBigQueryBaseParams { + jobId: string + pageToken?: string + maxResults?: number + timeoutMs?: number + location?: string + startIndex?: string +} + interface GoogleBigQueryJobReference { projectId: string jobId: string @@ -117,3 +160,65 @@ export interface GoogleBigQueryInsertRowsResponse extends ToolResponse { }> } } + +export interface GoogleBigQueryCreateDatasetResponse extends ToolResponse { + output: { + datasetId: string + projectId: string + friendlyName: string | null + description: string | null + location: string | null + creationTime: string | null + } +} + +export interface GoogleBigQueryDeleteDatasetResponse extends ToolResponse { + output: { + deleted: boolean + } +} + +export interface GoogleBigQueryCreateTableResponse extends ToolResponse { + output: { + tableId: string + datasetId: string + projectId: string + type: string | null + description: string | null + schema: Array<{ + name: string + type: string + mode: string | null + description: string | null + }> + creationTime: string | null + location: string | null + } +} + +export interface GoogleBigQueryDeleteTableResponse extends ToolResponse { + output: { + deleted: boolean + } +} + +export interface GoogleBigQueryListTableDataResponse extends ToolResponse { + output: { + rows: unknown[][] + totalRows: string | null + pageToken: string | null + } +} + +export interface GoogleBigQueryGetQueryResultsResponse extends ToolResponse { + output: { + columns: string[] + rows: Record[] + totalRows: string | null + jobComplete: boolean + totalBytesProcessed: string | null + cacheHit: boolean | null + jobReference: GoogleBigQueryJobReference | null + pageToken: string | null + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 02c6f7c8e7b..0127ea51bd9 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1304,9 +1304,15 @@ import { googleAppsheetFindRowsTool, } from '@/tools/google_appsheet' import { + googleBigQueryCreateDatasetTool, + googleBigQueryCreateTableTool, + googleBigQueryDeleteDatasetTool, + googleBigQueryDeleteTableTool, + googleBigQueryGetQueryResultsTool, googleBigQueryGetTableTool, googleBigQueryInsertRowsTool, googleBigQueryListDatasetsTool, + googleBigQueryListTableDataTool, googleBigQueryListTablesTool, googleBigQueryQueryTool, } from '@/tools/google_bigquery' @@ -7738,6 +7744,12 @@ export const tools: Record = { google_bigquery_list_tables: googleBigQueryListTablesTool, google_bigquery_get_table: googleBigQueryGetTableTool, google_bigquery_insert_rows: googleBigQueryInsertRowsTool, + google_bigquery_create_dataset: googleBigQueryCreateDatasetTool, + google_bigquery_delete_dataset: googleBigQueryDeleteDatasetTool, + google_bigquery_create_table: googleBigQueryCreateTableTool, + google_bigquery_delete_table: googleBigQueryDeleteTableTool, + google_bigquery_list_table_data: googleBigQueryListTableDataTool, + google_bigquery_get_query_results: googleBigQueryGetQueryResultsTool, google_vault_create_matters_export: createMattersExportTool, google_vault_list_matters_export: listMattersExportTool, google_vault_create_matters_holds: createMattersHoldsTool, From 6c44630d1b36e1c61a441e8550fd40f6f6ca3b62 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 11:10:32 -0700 Subject: [PATCH 2/5] fix(bigquery): address Greptile + Cursor Bugbot review findings - create_table: guard JSON.parse on the schema field with a clear error, trim tableReference IDs in the request body - create_dataset: trim datasetId in the request body - block: stop leaking a stale query "location" value into create_dataset when datasetLocation is empty; clarify pageToken output description covers list_table_data and get_query_results too --- apps/sim/blocks/blocks/google_bigquery.ts | 8 +++++++- apps/sim/tools/google_bigquery/create_dataset.ts | 2 +- apps/sim/tools/google_bigquery/create_table.ts | 11 ++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index e95cae3a3aa..afb1e7deeb5 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -423,11 +423,14 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, newDatasetId, newTableId, datasetLocation, + location, ...rest } = params return { ...rest, oauthCredential, + ...(['query', 'get_query_results'].includes(String(params.operation)) && + location && { location }), ...(params.operation === 'create_dataset' && newDatasetId && { datasetId: newDatasetId }), ...(params.operation === 'create_dataset' && datasetLocation && { location: datasetLocation }), @@ -481,7 +484,10 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, totalBytesProcessed: { type: 'string', description: 'Bytes processed (query)' }, cacheHit: { type: 'boolean', description: 'Whether result was cached (query)' }, jobReference: { type: 'json', description: 'Job reference for incomplete queries (query)' }, - pageToken: { type: 'string', description: 'Token for additional result pages (query)' }, + pageToken: { + type: 'string', + description: 'Token for additional result pages (query, list_table_data, get_query_results)', + }, datasets: { type: 'json', description: 'Array of dataset objects (list_datasets)' }, tables: { type: 'json', description: 'Array of table objects (list_tables)' }, totalItems: { type: 'number', description: 'Total items count (list_tables)' }, diff --git a/apps/sim/tools/google_bigquery/create_dataset.ts b/apps/sim/tools/google_bigquery/create_dataset.ts index d1dd99e8025..c892e5dd501 100644 --- a/apps/sim/tools/google_bigquery/create_dataset.ts +++ b/apps/sim/tools/google_bigquery/create_dataset.ts @@ -69,7 +69,7 @@ export const googleBigQueryCreateDatasetTool: ToolConfig< const body: Record = { datasetReference: { projectId: params.projectId, - datasetId: params.datasetId, + datasetId: params.datasetId.trim(), }, } if (params.location) body.location = params.location diff --git a/apps/sim/tools/google_bigquery/create_table.ts b/apps/sim/tools/google_bigquery/create_table.ts index 75fbcadeea9..2fdfab60ada 100644 --- a/apps/sim/tools/google_bigquery/create_table.ts +++ b/apps/sim/tools/google_bigquery/create_table.ts @@ -73,13 +73,18 @@ export const googleBigQueryCreateTableTool: ToolConfig< 'Content-Type': 'application/json', }), body: (params) => { - const fields = typeof params.schema === 'string' ? JSON.parse(params.schema) : params.schema + let fields: unknown + try { + fields = typeof params.schema === 'string' ? JSON.parse(params.schema) : params.schema + } catch { + throw new Error('Schema must be valid JSON, e.g. [{"name":"id","type":"STRING"}]') + } const body: Record = { tableReference: { projectId: params.projectId, - datasetId: params.datasetId, - tableId: params.tableId, + datasetId: params.datasetId.trim(), + tableId: params.tableId.trim(), }, schema: { fields }, } From e6c07dcd0684d8349ca62bbb2a5d9031f94b45e6 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 11:24:45 -0700 Subject: [PATCH 3/5] fix(bigquery): address second round of Cursor Bugbot findings - create_table: validate parsed schema is a non-empty array of field objects with a name, not just syntactically valid JSON - get_query_results: guard timeoutMs with Number.isFinite before appending to the query string, matching maxResults --- apps/sim/tools/google_bigquery/create_table.ts | 13 ++++++++++++- apps/sim/tools/google_bigquery/get_query_results.ts | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/google_bigquery/create_table.ts b/apps/sim/tools/google_bigquery/create_table.ts index 2fdfab60ada..f17fb2f1ea4 100644 --- a/apps/sim/tools/google_bigquery/create_table.ts +++ b/apps/sim/tools/google_bigquery/create_table.ts @@ -77,7 +77,18 @@ export const googleBigQueryCreateTableTool: ToolConfig< try { fields = typeof params.schema === 'string' ? JSON.parse(params.schema) : params.schema } catch { - throw new Error('Schema must be valid JSON, e.g. [{"name":"id","type":"STRING"}]') + fields = null + } + if ( + !Array.isArray(fields) || + fields.length === 0 || + !fields.every( + (f) => f && typeof f === 'object' && typeof (f as { name?: unknown }).name === 'string' + ) + ) { + throw new Error( + 'Schema must be a JSON array of column field definitions, e.g. [{"name":"id","type":"STRING"}]' + ) } const body: Record = { diff --git a/apps/sim/tools/google_bigquery/get_query_results.ts b/apps/sim/tools/google_bigquery/get_query_results.ts index 89c14307a1f..f4f0b056e4e 100644 --- a/apps/sim/tools/google_bigquery/get_query_results.ts +++ b/apps/sim/tools/google_bigquery/get_query_results.ts @@ -83,7 +83,10 @@ export const googleBigQueryGetQueryResultsTool: ToolConfig< } } if (params.timeoutMs !== undefined && params.timeoutMs !== null) { - url.searchParams.set('timeoutMs', String(Number(params.timeoutMs))) + const timeoutMs = Number(params.timeoutMs) + if (Number.isFinite(timeoutMs) && timeoutMs > 0) { + url.searchParams.set('timeoutMs', String(timeoutMs)) + } } if (params.location) url.searchParams.set('location', params.location) if (params.startIndex) url.searchParams.set('startIndex', params.startIndex) From d6ba6be77d2123a8079b9dccece2bf417e5dfd08 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 11:34:34 -0700 Subject: [PATCH 4/5] fix(bigquery): final alignment pass from independent multi-agent validation - Add missing projectId block output (returned by get_table/create_table/create_dataset but was silently dropped from the block's output schema) - Switch query/rows/schema subBlocks from long-input to code, matching the JSON/SQL field convention used by every other DB integration (clickhouse, mongodb, postgresql, supabase) --- apps/sim/blocks/blocks/google_bigquery.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index afb1e7deeb5..3c23606be15 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -67,7 +67,7 @@ export const GoogleBigQueryBlock: BlockConfig = { { id: 'query', title: 'SQL Query', - type: 'long-input', + type: 'code', placeholder: 'SELECT * FROM `project.dataset.table` LIMIT 100', condition: { field: 'operation', value: 'query' }, required: { field: 'operation', value: 'query' }, @@ -273,7 +273,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, { id: 'schema', title: 'Schema', - type: 'long-input', + type: 'code', placeholder: '[{"name": "id", "type": "STRING", "mode": "REQUIRED"}]', condition: { field: 'operation', value: 'create_table' }, required: { field: 'operation', value: 'create_table' }, @@ -328,7 +328,7 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, { id: 'rows', title: 'Rows', - type: 'long-input', + type: 'code', placeholder: '[{"column1": "value1", "column2": 42}]', condition: { field: 'operation', value: 'insert_rows' }, required: { field: 'operation', value: 'insert_rows' }, @@ -496,6 +496,10 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, type: 'string', description: 'Dataset ID (get_table, create_table, create_dataset)', }, + projectId: { + type: 'string', + description: 'Project ID (get_table, create_table, create_dataset)', + }, type: { type: 'string', description: 'Table type (get_table, create_table)' }, description: { type: 'string', From 6ca710166d778c41b3404a174f9ee199f6658977 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 11:51:29 -0700 Subject: [PATCH 5/5] fix(bigquery): stop leaking stale datasetId/tableId into create_dataset/create_table Same class of bug as the earlier location leak: params() spread hidden datasetId/tableId from ...rest unconditionally, so a stale value from a previously-selected operation could survive into create_dataset/create_table if the dedicated new-ID field was somehow empty. Gate datasetId/tableId (and location) to the operations that actually use them, via shared op-list constants also reused by the corresponding subblock conditions. --- apps/sim/blocks/blocks/google_bigquery.ts | 101 +++++++--------------- 1 file changed, 30 insertions(+), 71 deletions(-) diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 3c23606be15..e9a25d93b57 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -3,6 +3,18 @@ import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +const EXISTING_DATASET_OPS = [ + 'list_tables', + 'get_table', + 'insert_rows', + 'delete_dataset', + 'create_table', + 'delete_table', + 'list_table_data', +] +const EXISTING_TABLE_OPS = ['get_table', 'insert_rows', 'delete_table', 'list_table_data'] +const LOCATION_OPS = ['query', 'get_query_results'] + export const GoogleBigQueryBlock: BlockConfig = { type: 'google_bigquery', name: 'Google BigQuery', @@ -116,7 +128,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, title: 'Location', type: 'short-input', placeholder: 'Processing location (e.g., US, EU)', - condition: { field: 'operation', value: ['query', 'get_query_results'] }, + condition: { field: 'operation', value: LOCATION_OPS }, }, { @@ -168,30 +180,8 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, placeholder: 'Select BigQuery dataset', dependsOn: ['credential', 'projectId'], mode: 'basic', - condition: { - field: 'operation', - value: [ - 'list_tables', - 'get_table', - 'insert_rows', - 'delete_dataset', - 'create_table', - 'delete_table', - 'list_table_data', - ], - }, - required: { - field: 'operation', - value: [ - 'list_tables', - 'get_table', - 'insert_rows', - 'delete_dataset', - 'create_table', - 'delete_table', - 'list_table_data', - ], - }, + condition: { field: 'operation', value: EXISTING_DATASET_OPS }, + required: { field: 'operation', value: EXISTING_DATASET_OPS }, }, { id: 'datasetId', @@ -200,30 +190,8 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'datasetId', placeholder: 'Enter BigQuery dataset ID', mode: 'advanced', - condition: { - field: 'operation', - value: [ - 'list_tables', - 'get_table', - 'insert_rows', - 'delete_dataset', - 'create_table', - 'delete_table', - 'list_table_data', - ], - }, - required: { - field: 'operation', - value: [ - 'list_tables', - 'get_table', - 'insert_rows', - 'delete_dataset', - 'create_table', - 'delete_table', - 'list_table_data', - ], - }, + condition: { field: 'operation', value: EXISTING_DATASET_OPS }, + required: { field: 'operation', value: EXISTING_DATASET_OPS }, }, { @@ -236,14 +204,8 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, placeholder: 'Select BigQuery table', dependsOn: ['credential', 'projectId', 'datasetSelector'], mode: 'basic', - condition: { - field: 'operation', - value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], - }, - required: { - field: 'operation', - value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], - }, + condition: { field: 'operation', value: EXISTING_TABLE_OPS }, + required: { field: 'operation', value: EXISTING_TABLE_OPS }, }, { id: 'tableId', @@ -252,14 +214,8 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'tableId', placeholder: 'Enter BigQuery table ID', mode: 'advanced', - condition: { - field: 'operation', - value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], - }, - required: { - field: 'operation', - value: ['get_table', 'insert_rows', 'delete_table', 'list_table_data'], - }, + condition: { field: 'operation', value: EXISTING_TABLE_OPS }, + required: { field: 'operation', value: EXISTING_TABLE_OPS }, }, { @@ -424,17 +380,20 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, newTableId, datasetLocation, location, + datasetId, + tableId, ...rest } = params + const operation = String(params.operation) return { ...rest, oauthCredential, - ...(['query', 'get_query_results'].includes(String(params.operation)) && - location && { location }), - ...(params.operation === 'create_dataset' && newDatasetId && { datasetId: newDatasetId }), - ...(params.operation === 'create_dataset' && - datasetLocation && { location: datasetLocation }), - ...(params.operation === 'create_table' && newTableId && { tableId: newTableId }), + ...(LOCATION_OPS.includes(operation) && location && { location }), + ...(EXISTING_DATASET_OPS.includes(operation) && datasetId && { datasetId }), + ...(EXISTING_TABLE_OPS.includes(operation) && tableId && { tableId }), + ...(operation === 'create_dataset' && newDatasetId && { datasetId: newDatasetId }), + ...(operation === 'create_dataset' && datasetLocation && { location: datasetLocation }), + ...(operation === 'create_table' && newTableId && { tableId: newTableId }), ...(rows && { rows: typeof rows === 'string' ? rows : JSON.stringify(rows) }), ...(schema && { schema: typeof schema === 'string' ? schema : JSON.stringify(schema) }), ...(maxResults !== undefined && maxResults !== '' && { maxResults: Number(maxResults) }),