Skip to content

Commit

Permalink
bigquery: support scalar query parameters
Browse files Browse the repository at this point in the history
Initial support for query parameters. This CL supports only scalar
parameters (no arrays or structs).

Query parameters are cumbersome to use in the underlying API for two
reasons: all scalar values must be encoded as strings, and each value
must be accompanied by a type. To improve the ergonomics, this client
accepts Go values for parameters, converting them to strings and
determining their types.

As @c0b pointed out, the parameter mode is unnecessary, so we omit it
from the surface and the RPC request.

See #390.

Change-Id: Id4c1ba740bdee00979efbffe7e0f8276ed80ff00
Reviewed-on: https://code-review.googlesource.com/9557
Reviewed-by: Ross Light <[email protected]>
  • Loading branch information
jba committed Nov 30, 2016
1 parent a2e776e commit 2861f2e
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 4 deletions.
7 changes: 7 additions & 0 deletions bigquery/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}

func getClient(t *testing.T) *Client {
if client == nil {
t.Skip("Integration tests skipped")
}
return client
}

// If integration tests will be run, create a unique bucket for them.
func initIntegrationTest() {
flag.Parse() // needed for testing.Short()
Expand Down
69 changes: 69 additions & 0 deletions bigquery/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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 bigquery

import (
"encoding/base64"
"fmt"
"time"

bq "google.golang.org/api/bigquery/v2"
)

// See https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp-type.
var timestampFormat = "2006-01-02 15:04:05.999999-07:00"

var (
int64ParamType = &bq.QueryParameterType{Type: "INT64"}
float64ParamType = &bq.QueryParameterType{Type: "FLOAT64"}
boolParamType = &bq.QueryParameterType{Type: "BOOL"}
stringParamType = &bq.QueryParameterType{Type: "STRING"}
bytesParamType = &bq.QueryParameterType{Type: "BYTES"}
timestampParamType = &bq.QueryParameterType{Type: "TIMESTAMP"}
)

func paramType(x interface{}) (*bq.QueryParameterType, error) {
switch x.(type) {
case int, int8, int16, int32, int64, uint8, uint16, uint32:
return int64ParamType, nil
case float32, float64:
return float64ParamType, nil
case bool:
return boolParamType, nil
case string:
return stringParamType, nil
case time.Time:
return timestampParamType, nil
case []byte:
return bytesParamType, nil
default:
return nil, fmt.Errorf("Go type %T cannot be represented as a parameter type", x)
}
}

func paramValue(x interface{}) (bq.QueryParameterValue, error) {
// convenience function for scalar value
sval := func(s string) bq.QueryParameterValue {
return bq.QueryParameterValue{Value: s}
}
switch x := x.(type) {
case []byte:
return sval(base64.StdEncoding.EncodeToString(x)), nil
case time.Time:
return sval(x.Format(timestampFormat)), nil
default:
return sval(fmt.Sprint(x)), nil
}
}
132 changes: 132 additions & 0 deletions bigquery/params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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 bigquery

import (
"bytes"
"context"
"math"
"reflect"
"testing"
"time"

bq "google.golang.org/api/bigquery/v2"
)

var scalarTests = []struct {
val interface{}
want string
}{
{int64(0), "0"},
{3.14, "3.14"},
{3.14159e-87, "3.14159e-87"},
{true, "true"},
{"string", "string"},
{"\u65e5\u672c\u8a9e\n", "\u65e5\u672c\u8a9e\n"},
{math.NaN(), "NaN"},
{[]byte("foo"), "Zm9v"}, // base64 encoding of "foo"
{time.Date(2016, 3, 20, 4, 22, 9, 5000, time.FixedZone("neg1-2", -3720)),
"2016-03-20 04:22:09.000005-01:02"},
}

func TestParamValueScalar(t *testing.T) {
for _, test := range scalarTests {
got, err := paramValue(test.val)
if err != nil {
t.Errorf("%v: got %v, want nil", test.val, err)
continue
}
if got.ArrayValues != nil {
t.Errorf("%v, ArrayValues: got %v, expected nil", test.val, got.ArrayValues)
}
if got.StructValues != nil {
t.Errorf("%v, StructValues: got %v, expected nil", test.val, got.StructValues)
}
if got.Value != test.want {
t.Errorf("%v: got %q, want %q", test.val, got.Value, test.want)
}
}
}

func TestParamTypeScalar(t *testing.T) {
for _, test := range []struct {
val interface{}
want *bq.QueryParameterType
}{
{0, int64ParamType},
{uint32(32767), int64ParamType},
{3.14, float64ParamType},
{float32(3.14), float64ParamType},
{math.NaN(), float64ParamType},
{true, boolParamType},
{"", stringParamType},
{"string", stringParamType},
{time.Now(), timestampParamType},
{[]byte("foo"), bytesParamType},
} {
got, err := paramType(test.val)
if err != nil {
t.Fatal(err)
}
if got != test.want {
t.Errorf("%v (%T): got %v, want %v", test.val, test.val, got, test.want)
}
}
}

func TestIntegration_ScalarParam(t *testing.T) {
ctx := context.Background()
c := getClient(t)
for _, test := range scalarTests {
q := c.Query("select ?")
q.Parameters = []QueryParameter{{Value: test.val}}
it, err := q.Read(ctx)
if err != nil {
t.Fatal(err)
}
var val []Value
err = it.Next(&val)
if err != nil {
t.Fatal(err)
}
if len(val) != 1 {
t.Fatalf("got %d values, want 1", len(val))
}
got := val[0]
if !equal(got, test.val) {
t.Errorf("\ngot %#v (%T)\nwant %#v (%T)", got, got, test.val, test.val)
}
}
}

func equal(x1, x2 interface{}) bool {
if reflect.TypeOf(x1) != reflect.TypeOf(x2) {
return false
}
switch x1 := x1.(type) {
case float64:
if math.IsNaN(x1) {
return math.IsNaN(x2.(float64))
}
return x1 == x2
case time.Time:
// BigQuery is only accurate to the microsecond.
return x1.Round(time.Microsecond).Equal(x2.(time.Time).Round(time.Microsecond))
case []byte:
return bytes.Equal(x1, x2.([]byte))
default:
return x1 == x2
}
}
51 changes: 48 additions & 3 deletions bigquery/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ type QueryConfig struct {
// UseStandardSQL causes the query to use standard SQL.
// The default is false (using legacy SQL).
UseStandardSQL bool

// Parameters is a list of query parameters. The presence of parameters
// implies the use of standard SQL.
// If the query uses positional syntax ("?"), then no parameter may have a name.
// If the query uses named syntax ("@p"), then all parameters must have names.
// It is illegal to mix positional and named syntax.
Parameters []QueryParameter
}

// QueryPriority species a priority with which a query is to be executed.
Expand All @@ -98,6 +105,25 @@ const (
InteractivePriority QueryPriority = "INTERACTIVE"
)

type QueryParameter struct {
// Name is used for named parameter mode.
// It must match the name in the query case-insensitively.
Name string

// Value is the value of the parameter.
// The following Go types are supported, with their corresponding
// Bigquery types:
// int, int8, int16, int32, int64, uint8, uint16, uint32: INT64
// Note that uint, uint64 and uintptr are not supported, because
// they may contain values that cannot fit into a 64-bit signed integer.
// float32, float64: FLOAT64
// bool: BOOL
// string: STRING
// []byte: BYTES
// time.Time: TIMESTAMP
Value interface{}
}

// A Query queries data from a BigQuery table. Use Client.Query to create a Query.
type Query struct {
client *Client
Expand All @@ -122,7 +148,9 @@ func (q *Query) Run(ctx context.Context) (*Job, error) {
}
setJobRef(job, q.JobID, q.client.projectID)

q.QueryConfig.populateJobQueryConfig(job.Configuration.Query)
if err := q.QueryConfig.populateJobQueryConfig(job.Configuration.Query); err != nil {
return nil, err
}
j, err := q.client.service.insertJob(ctx, q.client.projectID, &insertJobConf{job: job})
if err != nil {
return nil, err
Expand All @@ -131,7 +159,7 @@ func (q *Query) Run(ctx context.Context) (*Job, error) {
return j, nil
}

func (q *QueryConfig) populateJobQueryConfig(conf *bq.JobConfigurationQuery) {
func (q *QueryConfig) populateJobQueryConfig(conf *bq.JobConfigurationQuery) error {
conf.Query = q.Q

if len(q.TableDefinitions) > 0 {
Expand Down Expand Up @@ -168,12 +196,29 @@ func (q *QueryConfig) populateJobQueryConfig(conf *bq.JobConfigurationQuery) {
if q.MaxBytesBilled >= 1 {
conf.MaximumBytesBilled = q.MaxBytesBilled
}
if q.UseStandardSQL {
if q.UseStandardSQL || len(q.Parameters) > 0 {
conf.UseLegacySql = false
conf.ForceSendFields = append(conf.ForceSendFields, "UseLegacySql")
}

if q.Dst != nil && !q.Dst.implicitTable() {
conf.DestinationTable = q.Dst.tableRefProto()
}
for _, p := range q.Parameters {
pv, err := paramValue(p.Value)
if err != nil {
return err
}
pt, err := paramType(p.Value)
if err != nil {
return err
}
qp := &bq.QueryParameter{
Name: p.Name,
ParameterValue: &pv,
ParameterType: pt,
}
conf.QueryParameters = append(conf.QueryParameters, qp)
}
return nil
}
2 changes: 1 addition & 1 deletion bigquery/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,6 @@ func convertBasicType(val string, typ FieldType) (Value, error) {
f, err := strconv.ParseFloat(val, 64)
return Value(time.Unix(0, int64(f*1e9))), err
default:
return nil, errors.New("unrecognized type")
return nil, fmt.Errorf("unrecognized type: %s", typ)
}
}

0 comments on commit 2861f2e

Please sign in to comment.