Files
wehub-resource-sync e04ed9c211
CF: Deploy Dev Docs / deploy (push) Has been cancelled
Sync Labels / build (push) Has been cancelled
tests / unit tests (macos-latest) (push) Has been cancelled
tests / unit tests (windows-latest) (push) Has been cancelled
tests / unit tests (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:32:45 +08:00

168 lines
6.4 KiB
Go

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bigquerycommon
import (
"context"
"fmt"
"regexp"
"sort"
"strings"
bigqueryapi "cloud.google.com/go/bigquery"
"github.com/googleapis/mcp-toolbox/internal/util/parameters"
bigqueryrestapi "google.golang.org/api/bigquery/v2"
)
// validBQTableID matches BigQuery table identifiers in 'dataset.table' or
// 'project.dataset.table' form. Components are restricted to letters, digits,
// and underscores — the character set that BigQuery allows for dataset and
// table IDs and that is safe to interpolate inside a backtick-quoted SQL
// identifier.
var validBQTableID = regexp.MustCompile(`^[a-zA-Z0-9_-]+(\.([a-zA-Z0-9_]+)){1,2}$`)
// validBQColumnName matches BigQuery column names: a letter or underscore
// followed by letters, digits, or underscores.
var validBQColumnName = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
// ValidTableID returns true if s is a safe BigQuery table identifier of the
// form 'dataset.table' or 'project.dataset.table'. Values that fail this check
// must not be interpolated into backtick-quoted SQL.
func ValidTableID(s string) bool {
return validBQTableID.MatchString(s)
}
// ValidColumnName returns true if s is a safe BigQuery column name.
// Values that fail this check must not be interpolated as SQL identifiers
// or into single-quoted SQL string arguments that represent column references.
func ValidColumnName(s string) bool {
return validBQColumnName.MatchString(s)
}
// DryRunQuery performs a dry run of the SQL query to validate it and get metadata.
func DryRunQuery(ctx context.Context, restService *bigqueryrestapi.Service, projectID string, location string, sql string, params []*bigqueryrestapi.QueryParameter, connProps []*bigqueryapi.ConnectionProperty, maximumBytesBilled int64) (*bigqueryrestapi.Job, error) {
useLegacySql := false
restConnProps := make([]*bigqueryrestapi.ConnectionProperty, len(connProps))
for i, prop := range connProps {
restConnProps[i] = &bigqueryrestapi.ConnectionProperty{Key: prop.Key, Value: prop.Value}
}
jobToInsert := &bigqueryrestapi.Job{
JobReference: &bigqueryrestapi.JobReference{
ProjectId: projectID,
Location: location,
},
Configuration: &bigqueryrestapi.JobConfiguration{
DryRun: true,
Query: &bigqueryrestapi.JobConfigurationQuery{
Query: sql,
UseLegacySql: &useLegacySql,
ConnectionProperties: restConnProps,
QueryParameters: params,
MaximumBytesBilled: maximumBytesBilled,
},
},
}
insertResponse, err := restService.Jobs.Insert(projectID, jobToInsert).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("failed to insert dry run job: %w", err)
}
return insertResponse, nil
}
// BQTypeStringFromToolType converts a tool parameter type string to a BigQuery standard SQL type string.
func BQTypeStringFromToolType(toolType string) (string, error) {
switch toolType {
case parameters.TypeString:
return "STRING", nil
case parameters.TypeInt:
return "INT64", nil
case parameters.TypeFloat:
return "FLOAT64", nil
case parameters.TypeBool:
return "BOOL", nil
case parameters.TypeMap:
return "STRUCT", nil
default:
return "", fmt.Errorf("unsupported tool parameter type for BigQuery: %s", toolType)
}
}
// InitializeDatasetParameters generates project and dataset tool parameters based on allowedDatasets.
func InitializeDatasetParameters(
allowedDatasets []string,
defaultProjectID string,
projectKey, datasetKey string,
projectDescription, datasetDescription string,
) (projectParam, datasetParam parameters.Parameter) {
if len(allowedDatasets) > 0 {
if len(allowedDatasets) == 1 {
parts := strings.Split(allowedDatasets[0], ".")
defaultProjectID = parts[0]
datasetID := parts[1]
projectDescription += fmt.Sprintf(" Must be `%s`.", defaultProjectID)
datasetDescription += fmt.Sprintf(" Must be `%s`.", datasetID)
datasetParam = parameters.NewStringParameter(datasetKey, datasetDescription, parameters.WithStringDefault(datasetID))
} else {
datasetIDsByProject := make(map[string][]string)
for _, ds := range allowedDatasets {
parts := strings.Split(ds, ".")
project := parts[0]
dataset := parts[1]
datasetIDsByProject[project] = append(datasetIDsByProject[project], fmt.Sprintf("`%s`", dataset))
}
var datasetDescriptions, projectIDList []string
for project, datasets := range datasetIDsByProject {
sort.Strings(datasets)
projectIDList = append(projectIDList, fmt.Sprintf("`%s`", project))
datasetList := strings.Join(datasets, ", ")
datasetDescriptions = append(datasetDescriptions, fmt.Sprintf("%s from project `%s`", datasetList, project))
}
sort.Strings(projectIDList)
sort.Strings(datasetDescriptions)
projectDescription += fmt.Sprintf(" Must be one of the following: %s.", strings.Join(projectIDList, ", "))
datasetDescription += fmt.Sprintf(" Must be one of the allowed datasets: %s.", strings.Join(datasetDescriptions, "; "))
datasetParam = parameters.NewStringParameter(datasetKey, datasetDescription)
}
} else {
datasetParam = parameters.NewStringParameter(datasetKey, datasetDescription)
}
projectParam = parameters.NewStringParameter(projectKey, projectDescription, parameters.WithStringDefault(defaultProjectID))
return projectParam, datasetParam
}
// StripSingleQuotes removes leading and trailing single quotes from a string if both are present.
func StripSingleQuotes(s string) string {
if len(s) >= 2 && s[0] == '\'' && s[len(s)-1] == '\'' {
return s[1 : len(s)-1]
}
return s
}
// ValidColumnParam returns true if s (stripped of leading/trailing single quotes) is a safe column name.
func ValidColumnParam(s string) bool {
return ValidColumnName(StripSingleQuotes(s))
}
// ValidContributionMetricParam returns true if s (stripped of leading/trailing single quotes) is a safe contribution metric (does not contain single quotes).
func ValidContributionMetricParam(s string) bool {
return !strings.ContainsRune(StripSingleQuotes(s), '\'')
}