mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-05 12:45:11 -05:00
## Description
> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution
This change updates both `bigquery-sql` and `bigquery-execute-sql` tools
to format `NUMERIC` and `BIGNUMERIC` values as decimal strings (e.g.,
"9.5") instead of rational fractions (e.g., "19/2"). This ensures the
tools' output matches the BigQuery REST API JSON format.
Key changes:
- Added `NormalizeValue` function in
`internal/tools/bigquery/bigquerycommon` to handle `*big.Rat` conversion
with 38-digit precision and trailing zero trimming.
- Updated `bigquery-sql` and `bigquery-execute-sql` to use
`NormalizeValue`.
- Added comprehensive tests in
`internal/tools/bigquery/bigquerycommon/conversion_test.go`.
With these changes the formatting for NUMERIC and BIGNUMERIC is fixed.
**Before:**
```
[
{
"id": 3,
"numeric_value": "1"
},
{
"id": 2,
"numeric_value": "333333333/1000000000"
},
{
"id": 4,
"numeric_value": "12341/10"
},
{
"id": 1,
"numeric_value": "19/2"
}
]
```
**After:**
```
[
{
"id": 3,
"numeric_value": "1"
},
{
"id": 2,
"numeric_value": "0.333333333"
},
{
"id": 4,
"numeric_value": "1234.1"
},
{
"id": 1,
"numeric_value": "9.5"
}
]
```
## PR Checklist
> Thank you for opening a Pull Request! Before submitting your PR, there
are a
> few things you can do to make sure it goes smoothly:
- [x] Make sure you reviewed
[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [ ] Make sure to open an issue as a
[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
before writing your code! That way we can discuss the change, evaluate
designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involve a breaking change
🛠️ Fixes #1194
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Averi Kitsch <akitsch@google.com>
174 lines
6.0 KiB
Go
174 lines
6.0 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"
|
|
"math/big"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
bigqueryapi "cloud.google.com/go/bigquery"
|
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
|
bigqueryrestapi "google.golang.org/api/bigquery/v2"
|
|
)
|
|
|
|
// 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) (*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,
|
|
},
|
|
},
|
|
}
|
|
|
|
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 "string":
|
|
return "STRING", nil
|
|
case "integer":
|
|
return "INT64", nil
|
|
case "float":
|
|
return "FLOAT64", nil
|
|
case "boolean":
|
|
return "BOOL", 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.NewStringParameterWithDefault(datasetKey, datasetID, datasetDescription)
|
|
} 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.NewStringParameterWithDefault(projectKey, defaultProjectID, projectDescription)
|
|
|
|
return projectParam, datasetParam
|
|
}
|
|
|
|
// NormalizeValue converts BigQuery specific types to standard JSON-compatible types.
|
|
// Specifically, it handles *big.Rat (used for NUMERIC/BIGNUMERIC) by converting
|
|
// them to decimal strings with up to 38 digits of precision, trimming trailing zeros.
|
|
// It recursively handles slices (arrays) and maps (structs) using reflection.
|
|
func NormalizeValue(v any) any {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
// Handle *big.Rat specifically.
|
|
if rat, ok := v.(*big.Rat); ok {
|
|
// Convert big.Rat to a decimal string.
|
|
// Use a precision of 38 digits (enough for BIGNUMERIC and NUMERIC)
|
|
// and trim trailing zeros to match BigQuery's behavior.
|
|
s := rat.FloatString(38)
|
|
if strings.Contains(s, ".") {
|
|
s = strings.TrimRight(s, "0")
|
|
s = strings.TrimRight(s, ".")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Use reflection for slices and maps to handle various underlying types.
|
|
rv := reflect.ValueOf(v)
|
|
switch rv.Kind() {
|
|
case reflect.Slice, reflect.Array:
|
|
// Preserve []byte as is, so json.Marshal encodes it as Base64 string (BigQuery BYTES behavior).
|
|
if rv.Type().Elem().Kind() == reflect.Uint8 {
|
|
return v
|
|
}
|
|
newSlice := make([]any, rv.Len())
|
|
for i := 0; i < rv.Len(); i++ {
|
|
newSlice[i] = NormalizeValue(rv.Index(i).Interface())
|
|
}
|
|
return newSlice
|
|
case reflect.Map:
|
|
// Ensure keys are strings to produce a JSON-compatible map.
|
|
if rv.Type().Key().Kind() != reflect.String {
|
|
return v
|
|
}
|
|
newMap := make(map[string]any, rv.Len())
|
|
iter := rv.MapRange()
|
|
for iter.Next() {
|
|
newMap[iter.Key().String()] = NormalizeValue(iter.Value().Interface())
|
|
}
|
|
return newMap
|
|
}
|
|
|
|
return v
|
|
}
|