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
168 lines
6.4 KiB
Go
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), '\'')
|
|
}
|