diff --git a/bigquery/integration_test.go b/bigquery/integration_test.go index 0d46640685a..ba8e3cab09a 100644 --- a/bigquery/integration_test.go +++ b/bigquery/integration_test.go @@ -1808,6 +1808,7 @@ func TestIntegration_QueryParameters(t *testing.T) { dtm := civil.DateTime{Date: d, Time: tm} ts := time.Date(2016, 3, 20, 15, 04, 05, 0, time.UTC) rat := big.NewRat(13, 10) + bigRat := big.NewRat(12345, 10e10) type ss struct { String string @@ -1828,73 +1829,73 @@ func TestIntegration_QueryParameters(t *testing.T) { }{ { "SELECT @val", - []QueryParameter{{"val", 1}}, + []QueryParameter{{Name: "val", Value: 1}}, []Value{int64(1)}, int64(1), }, { "SELECT @val", - []QueryParameter{{"val", 1.3}}, + []QueryParameter{{Name: "val", Value: 1.3}}, []Value{1.3}, 1.3, }, { "SELECT @val", - []QueryParameter{{"val", rat}}, + []QueryParameter{{Name: "val", Value: rat}}, []Value{rat}, rat, }, { "SELECT @val", - []QueryParameter{{"val", true}}, + []QueryParameter{{Name: "val", Value: true}}, []Value{true}, true, }, { "SELECT @val", - []QueryParameter{{"val", "ABC"}}, + []QueryParameter{{Name: "val", Value: "ABC"}}, []Value{"ABC"}, "ABC", }, { "SELECT @val", - []QueryParameter{{"val", []byte("foo")}}, + []QueryParameter{{Name: "val", Value: []byte("foo")}}, []Value{[]byte("foo")}, []byte("foo"), }, { "SELECT @val", - []QueryParameter{{"val", ts}}, + []QueryParameter{{Name: "val", Value: ts}}, []Value{ts}, ts, }, { "SELECT @val", - []QueryParameter{{"val", []time.Time{ts, ts}}}, + []QueryParameter{{Name: "val", Value: []time.Time{ts, ts}}}, []Value{[]Value{ts, ts}}, []interface{}{ts, ts}, }, { "SELECT @val", - []QueryParameter{{"val", dtm}}, + []QueryParameter{{Name: "val", Value: dtm}}, []Value{civil.DateTime{Date: d, Time: rtm}}, civil.DateTime{Date: d, Time: rtm}, }, { "SELECT @val", - []QueryParameter{{"val", d}}, + []QueryParameter{{Name: "val", Value: d}}, []Value{d}, d, }, { "SELECT @val", - []QueryParameter{{"val", tm}}, + []QueryParameter{{Name: "val", Value: tm}}, []Value{rtm}, rtm, }, { "SELECT @val", - []QueryParameter{{"val", s{ts, []string{"a", "b"}, ss{"c"}, []ss{{"d"}, {"e"}}}}}, + []QueryParameter{{Name: "val", Value: s{ts, []string{"a", "b"}, ss{"c"}, []ss{{"d"}, {"e"}}}}}, []Value{[]Value{ts, []Value{"a", "b"}, []Value{"c"}, []Value{[]Value{"d"}, []Value{"e"}}}}, map[string]interface{}{ "Timestamp": ts, @@ -1908,7 +1909,7 @@ func TestIntegration_QueryParameters(t *testing.T) { }, { "SELECT @val.Timestamp, @val.SubStruct.String", - []QueryParameter{{"val", s{Timestamp: ts, SubStruct: ss{"a"}}}}, + []QueryParameter{{Name: "val", Value: s{Timestamp: ts, SubStruct: ss{"a"}}}}, []Value{ts, "a"}, map[string]interface{}{ "Timestamp": ts, @@ -1917,6 +1918,147 @@ func TestIntegration_QueryParameters(t *testing.T) { "SubStructArray": nil, }, }, + { + "SELECT @val", + []QueryParameter{ + { + Name: "val", + Value: &QueryParameterValue{ + Type: StandardSQLDataType{ + TypeKind: "BIGNUMERIC", + }, + Value: BigNumericString(bigRat), + }, + }, + }, + []Value{bigRat}, + bigRat, + }, + { + "SELECT @val", + []QueryParameter{ + { + Name: "val", + Value: &QueryParameterValue{ + ArrayValue: []QueryParameterValue{ + {Value: "a"}, + {Value: "b"}, + }, + Type: StandardSQLDataType{ + ArrayElementType: &StandardSQLDataType{ + TypeKind: "STRING", + }, + }, + }, + }, + }, + []Value{[]Value{"a", "b"}}, + []interface{}{"a", "b"}, + }, + { + "SELECT @val", + []QueryParameter{ + { + Name: "val", + Value: &QueryParameterValue{ + StructValue: map[string]QueryParameterValue{ + "Timestamp": { + Value: ts, + }, + "BigNumericArray": { + ArrayValue: []QueryParameterValue{ + {Value: BigNumericString(bigRat)}, + {Value: BigNumericString(rat)}, + }, + }, + "ArraySingleValueStruct": { + ArrayValue: []QueryParameterValue{ + {StructValue: map[string]QueryParameterValue{ + "Number": { + Value: int64(42), + }, + }}, + {StructValue: map[string]QueryParameterValue{ + "Number": { + Value: int64(43), + }, + }}, + }, + }, + "SubStruct": { + StructValue: map[string]QueryParameterValue{ + "String": { + Value: "c", + }, + }, + }, + }, + Type: StandardSQLDataType{ + StructType: &StandardSQLStructType{ + Fields: []*StandardSQLField{ + { + Name: "Timestamp", + Type: &StandardSQLDataType{ + TypeKind: "TIMESTAMP", + }, + }, + { + Name: "BigNumericArray", + Type: &StandardSQLDataType{ + ArrayElementType: &StandardSQLDataType{ + TypeKind: "BIGNUMERIC", + }, + }, + }, + { + Name: "ArraySingleValueStruct", + Type: &StandardSQLDataType{ + ArrayElementType: &StandardSQLDataType{ + StructType: &StandardSQLStructType{ + Fields: []*StandardSQLField{ + { + Name: "Number", + Type: &StandardSQLDataType{ + TypeKind: "INT64", + }, + }, + }, + }, + }, + }, + }, + { + Name: "SubStruct", + Type: &StandardSQLDataType{ + StructType: &StandardSQLStructType{ + Fields: []*StandardSQLField{ + { + Name: "String", + Type: &StandardSQLDataType{ + TypeKind: "STRING", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + []Value{[]Value{ts, []Value{bigRat, rat}, []Value{[]Value{int64(42)}, []Value{int64(43)}}, []Value{"c"}}}, + map[string]interface{}{ + "Timestamp": ts, + "BigNumericArray": []interface{}{bigRat, rat}, + "ArraySingleValueStruct": []interface{}{ + map[string]interface{}{"Number": int64(42)}, + map[string]interface{}{"Number": int64(43)}, + }, + "SubStruct": map[string]interface{}{"String": "c"}, + }, + }, } for _, c := range testCases { q := client.Query(c.query) diff --git a/bigquery/params.go b/bigquery/params.go index 0f780dc308a..5cafbf85f06 100644 --- a/bigquery/params.go +++ b/bigquery/params.go @@ -82,12 +82,13 @@ var ( ) var ( - typeOfDate = reflect.TypeOf(civil.Date{}) - typeOfTime = reflect.TypeOf(civil.Time{}) - typeOfDateTime = reflect.TypeOf(civil.DateTime{}) - typeOfGoTime = reflect.TypeOf(time.Time{}) - typeOfRat = reflect.TypeOf(&big.Rat{}) - typeOfIntervalValue = reflect.TypeOf(&IntervalValue{}) + typeOfDate = reflect.TypeOf(civil.Date{}) + typeOfTime = reflect.TypeOf(civil.Time{}) + typeOfDateTime = reflect.TypeOf(civil.DateTime{}) + typeOfGoTime = reflect.TypeOf(time.Time{}) + typeOfRat = reflect.TypeOf(&big.Rat{}) + typeOfIntervalValue = reflect.TypeOf(&IntervalValue{}) + typeOfQueryParameterValue = reflect.TypeOf(&QueryParameterValue{}) ) // A QueryParameter is a parameter to a query. @@ -116,6 +117,15 @@ type QueryParameter struct { // For scalar values, you can supply the Null types within this library // to send the appropriate NULL values (e.g. NullInt64, NullString, etc). // + // To specify query parameters explicitly rather by inference, *QueryParameterValue can be used. + // For example, a BIGNUMERIC can be specified like this: + // &QueryParameterValue{ + // Type: StandardSQLDataType{ + // TypeKind: "BIGNUMERIC", + // }, + // Value: BigNumericString(*big.Rat), + // } + // // When a QueryParameter is returned inside a QueryConfig from a call to // Job.Config: // Integers are of type int64. @@ -129,12 +139,165 @@ type QueryParameter struct { Value interface{} } -func (p QueryParameter) toBQ() (*bq.QueryParameter, error) { +// QueryParameterValue is a go type for representing a explicit typed QueryParameter. +type QueryParameterValue struct { + // Type specifies the parameter type. See StandardSQLDataType for more. + // Scalar parameters and more complex types can be defined within this field. + // See examples on the value fields. + Type StandardSQLDataType + + // Value is the value of the parameter, if a simple scalar type. + // The default behavior for scalar values is to do type inference + // and format it accordingly. + // Because of that, depending on the parameter type, is recommended + // to send value as a String. + // We provide some formatter functions for some types: + // CivilTimeString(civil.Time) + // CivilDateTimeString(civil.DateTime) + // NumericString(*big.Rat) + // BigNumericString(*big.Rat) + // IntervalString(*IntervalValue) + // + // Example: + // + // &QueryParameterValue{ + // Type: StandardSQLDataType{ + // TypeKind: "BIGNUMERIC", + // }, + // Value: BigNumericString(*big.Rat), + // } + Value interface{} + + // ArrayValue is the array of values for the parameter. + // + // Must be used with QueryParameterValue.Type being a StandardSQLDataType + // with ArrayElementType filled with the given element type. + // + // Example of an array of strings : + // &QueryParameterValue{ + // Type: &StandardSQLDataType{ + // ArrayElementType: &StandardSQLDataType{ + // TypeKind: "STRING", + // }, + // }, + // ArrayValue: []QueryParameterValue{ + // {Value: "a"}, + // {Value: "b"}, + // }, + // } + // + // Example of an array of structs : + // &QueryParameterValue{ + // Type: &StandardSQLDataType{ + // ArrayElementType: &StandardSQLDataType{ + // StructType: &StandardSQLDataType{ + // Fields: []*StandardSQLField{ + // { + // Name: "NumberField", + // Type: &StandardSQLDataType{ + // TypeKind: "INT64", + // }, + // }, + // }, + // }, + // }, + // }, + // ArrayValue: []QueryParameterValue{ + // {StructValue: map[string]QueryParameterValue{ + // "NumberField": { + // Value: int64(42), + // }, + // }}, + // {StructValue: map[string]QueryParameterValue{ + // "NumberField": { + // Value: int64(43), + // }, + // }}, + // }, + // } + ArrayValue []QueryParameterValue + + // StructValue is the struct field values for the parameter. + // + // Must be used with QueryParameterValue.Type being a StandardSQLDataType + // with StructType filled with the given field types. + // + // Example: + // + // &QueryParameterValue{ + // Type: &StandardSQLDataType{ + // StructType{ + // Fields: []*StandardSQLField{ + // { + // Name: "StringField", + // Type: &StandardSQLDataType{ + // TypeKind: "STRING", + // }, + // }, + // { + // Name: "NumberField", + // Type: &StandardSQLDataType{ + // TypeKind: "INT64", + // }, + // }, + // }, + // }, + // }, + // StructValue: []map[string]QueryParameterValue{ + // "NumberField": { + // Value: int64(42), + // }, + // "StringField": { + // Value: "Value", + // }, + // }, + // } + StructValue map[string]QueryParameterValue +} + +func (p QueryParameterValue) toBQParamType() *bq.QueryParameterType { + return p.Type.toBQParamType() +} + +func (p QueryParameterValue) toBQParamValue() (*bq.QueryParameterValue, error) { + if len(p.ArrayValue) > 0 { + pv := &bq.QueryParameterValue{} + pv.ArrayValues = []*bq.QueryParameterValue{} + for _, v := range p.ArrayValue { + val, err := v.toBQParamValue() + if err != nil { + return nil, err + } + pv.ArrayValues = append(pv.ArrayValues, val) + } + return pv, nil + } + if len(p.StructValue) > 0 { + pv := &bq.QueryParameterValue{} + pv.StructValues = map[string]bq.QueryParameterValue{} + for name, param := range p.StructValue { + v, err := param.toBQParamValue() + if err != nil { + return nil, err + } + pv.StructValues[name] = *v + } + return pv, nil + } pv, err := paramValue(reflect.ValueOf(p.Value)) if err != nil { return nil, err } - pt, err := paramType(reflect.TypeOf(p.Value)) + return pv, nil +} + +func (p QueryParameter) toBQ() (*bq.QueryParameter, error) { + v := reflect.ValueOf(p.Value) + pv, err := paramValue(v) + if err != nil { + return nil, err + } + pt, err := paramType(reflect.TypeOf(p.Value), v) if err != nil { return nil, err } @@ -145,7 +308,7 @@ func (p QueryParameter) toBQ() (*bq.QueryParameter, error) { }, nil } -func paramType(t reflect.Type) (*bq.QueryParameterType, error) { +func paramType(t reflect.Type, v reflect.Value) (*bq.QueryParameterType, error) { if t == nil { return nil, errors.New("bigquery: nil parameter") } @@ -174,6 +337,8 @@ func paramType(t reflect.Type) (*bq.QueryParameterType, error) { return geographyParamType, nil case typeOfNullJSON: return jsonParamType, nil + case typeOfQueryParameterValue: + return v.Interface().(*QueryParameterValue).toBQParamType(), nil } switch t.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32: @@ -195,7 +360,7 @@ func paramType(t reflect.Type) (*bq.QueryParameterType, error) { fallthrough case reflect.Array: - et, err := paramType(t.Elem()) + et, err := paramType(t.Elem(), v) if err != nil { return nil, err } @@ -215,7 +380,7 @@ func paramType(t reflect.Type) (*bq.QueryParameterType, error) { return nil, err } for _, f := range fields { - pt, err := paramType(f.Type) + pt, err := paramType(f.Type, v) if err != nil { return nil, err } @@ -314,6 +479,8 @@ func paramValue(v reflect.Value) (*bq.QueryParameterValue, error) { case typeOfIntervalValue: res.Value = IntervalString(v.Interface().(*IntervalValue)) return res, nil + case typeOfQueryParameterValue: + return v.Interface().(*QueryParameterValue).toBQParamValue() } switch t.Kind() { case reflect.Slice: diff --git a/bigquery/params_test.go b/bigquery/params_test.go index 6be55301d0a..fa6a293051d 100644 --- a/bigquery/params_test.go +++ b/bigquery/params_test.go @@ -116,6 +116,12 @@ var scalarTests = []struct { dateTimeParamType, NullDateTime{Valid: false}}, {big.NewRat(12345, 1000), false, "12.345000000", numericParamType, big.NewRat(12345, 1000)}, + {&QueryParameterValue{ + Type: StandardSQLDataType{ + TypeKind: "BIGNUMERIC", + }, + Value: BigNumericString(big.NewRat(12345, 10e10)), + }, false, "0.00000012345000000000000000000000000000", bigNumericParamType, big.NewRat(12345, 10e10)}, {&IntervalValue{Years: 1, Months: 2, Days: 3}, false, "1-2 3 0:0:0", intervalParamType, &IntervalValue{Years: 1, Months: 2, Days: 3}}, {NullGeography{GeographyVal: "POINT(-122.335503 47.625536)", Valid: true}, false, "POINT(-122.335503 47.625536)", geographyParamType, "POINT(-122.335503 47.625536)"}, {NullGeography{Valid: false}, true, "", geographyParamType, NullGeography{Valid: false}}, @@ -179,7 +185,6 @@ func sval(s string) bq.QueryParameterValue { } func TestParamValueScalar(t *testing.T) { - nilValue := &bq.QueryParameterValue{ NullFields: []string{"Value"}, } @@ -250,7 +255,7 @@ func TestParamValueErrors(t *testing.T) { func TestParamType(t *testing.T) { for _, test := range scalarTests { - got, err := paramType(reflect.TypeOf(test.val)) + got, err := paramType(reflect.TypeOf(test.val), reflect.ValueOf(test.val)) if err != nil { t.Fatal(err) } @@ -268,7 +273,7 @@ func TestParamType(t *testing.T) { {[3]bool{}, &bq.QueryParameterType{Type: "ARRAY", ArrayType: boolParamType}}, {S1{}, s1ParamType}, } { - got, err := paramType(reflect.TypeOf(test.val)) + got, err := paramType(reflect.TypeOf(test.val), reflect.ValueOf(test.val)) if err != nil { t.Fatal(err) } @@ -282,7 +287,7 @@ func TestParamTypeErrors(t *testing.T) { for _, val := range []interface{}{ nil, uint(0), new([]int), make(chan int), } { - _, err := paramType(reflect.TypeOf(val)) + _, err := paramType(reflect.TypeOf(val), reflect.ValueOf(val)) if err == nil { t.Errorf("%v (%T): got nil, want error", val, val) } @@ -296,7 +301,7 @@ func TestConvertParamValue(t *testing.T) { if err != nil { t.Fatal(err) } - ptype, err := paramType(reflect.TypeOf(test.val)) + ptype, err := paramType(reflect.TypeOf(test.val), reflect.ValueOf(test.val)) if err != nil { t.Fatal(err) } @@ -307,7 +312,6 @@ func TestConvertParamValue(t *testing.T) { if !testutil.Equal(got, test.wantStat) { t.Errorf("%#v: wanted stat as %#v, got %#v", test.val, test.wantStat, got) } - } // Arrays. for _, test := range []struct { diff --git a/bigquery/standardsql.go b/bigquery/standardsql.go index 1101bbb0656..7f8ca6e1167 100644 --- a/bigquery/standardsql.go +++ b/bigquery/standardsql.go @@ -63,6 +63,23 @@ func (ssdt *StandardSQLDataType) toBQ() (*bq.StandardSqlDataType, error) { return bqdt, nil } +func (ssdt StandardSQLDataType) toBQParamType() *bq.QueryParameterType { + if ssdt.ArrayElementType != nil { + return &bq.QueryParameterType{Type: "ARRAY", ArrayType: ssdt.ArrayElementType.toBQParamType()} + } + if ssdt.StructType != nil { + var fts []*bq.QueryParameterTypeStructTypes + for _, field := range ssdt.StructType.Fields { + fts = append(fts, &bq.QueryParameterTypeStructTypes{ + Name: field.Name, + Type: field.Type.toBQParamType(), + }) + } + return &bq.QueryParameterType{Type: "STRUCT", StructTypes: fts} + } + return &bq.QueryParameterType{Type: ssdt.TypeKind} +} + func bqToStandardSQLDataType(bqdt *bq.StandardSqlDataType) (*StandardSQLDataType, error) { if bqdt == nil { return nil, nil