feat(bigquery): validate integration + add dataset/table lifecycle tools (#5480)

* 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

* 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

* 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

* 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)

* 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.
This commit is contained in:
Waleed
2026-07-07 12:10:03 -07:00
committed by GitHub
parent f36461465b
commit 8174182e69
10 changed files with 1116 additions and 22 deletions
+230 -22
View File
@@ -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',
@@ -22,9 +34,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',
@@ -61,7 +79,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' },
@@ -93,7 +111,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 +128,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: LOCATION_OPS },
},
{
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 +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'] },
required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
condition: { field: 'operation', value: EXISTING_DATASET_OPS },
required: { field: 'operation', value: EXISTING_DATASET_OPS },
},
{
id: 'datasetId',
@@ -130,8 +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'] },
required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
condition: { field: 'operation', value: EXISTING_DATASET_OPS },
required: { field: 'operation', value: EXISTING_DATASET_OPS },
},
{
@@ -144,8 +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'] },
required: { field: 'operation', value: ['get_table', 'insert_rows'] },
condition: { field: 'operation', value: EXISTING_TABLE_OPS },
required: { field: 'operation', value: EXISTING_TABLE_OPS },
},
{
id: 'tableId',
@@ -154,14 +214,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: EXISTING_TABLE_OPS },
required: { field: 'operation', value: EXISTING_TABLE_OPS },
},
{
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: 'code',
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'] },
},
{
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' },
@@ -197,15 +320,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 +345,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 +370,34 @@ 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,
location,
datasetId,
tableId,
...rest
} = params
const operation = String(params.operation)
return {
...rest,
oauthCredential,
...(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) }),
...(timeoutMs !== undefined && timeoutMs !== '' && { timeoutMs: Number(timeoutMs) }),
}
},
},
@@ -248,8 +414,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: {
@@ -266,22 +443,46 @@ 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)' },
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)',
},
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',
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 +582,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
@@ -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<string, unknown> = {
datasetReference: {
projectId: params.projectId,
datasetId: params.datasetId.trim(),
},
}
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,
},
},
}
@@ -0,0 +1,173 @@
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) => {
let fields: unknown
try {
fields = typeof params.schema === 'string' ? JSON.parse(params.schema) : params.schema
} catch {
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<string, unknown> = {
tableReference: {
projectId: params.projectId,
datasetId: params.datasetId.trim(),
tableId: params.tableId.trim(),
},
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,
},
},
}
@@ -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' },
},
}
@@ -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' },
},
}
@@ -0,0 +1,178 @@
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) {
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)
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<string, unknown> = {}
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,
},
},
}
+6
View File
@@ -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'
@@ -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,
},
},
}
+105
View File
@@ -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<string, unknown>[]
totalRows: string | null
jobComplete: boolean
totalBytesProcessed: string | null
cacheHit: boolean | null
jobReference: GoogleBigQueryJobReference | null
pageToken: string | null
}
}
+12
View File
@@ -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<string, ToolConfig> = {
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,