Skip to content

Commit 43755d3

Browse files
authored
feat(bigquery): support nullable params and geography params (#4225)
* feat(bigquery): support nullable params, geography params Details: * Support the passing of Null<type> values for query parameters. * Add support for GEOGRAPHY param type, which was added after the initial GEOGRAPHY type support launched.
1 parent 0232e6f commit 43755d3

File tree

2 files changed

+240
-52
lines changed

2 files changed

+240
-52
lines changed

Diff for: bigquery/params.go

+120-18
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ var (
7676
timestampParamType = &bq.QueryParameterType{Type: "TIMESTAMP"}
7777
numericParamType = &bq.QueryParameterType{Type: "NUMERIC"}
7878
bigNumericParamType = &bq.QueryParameterType{Type: "BIGNUMERIC"}
79+
geographyParamType = &bq.QueryParameterType{Type: "GEOGRAPHY"}
7980
)
8081

8182
var (
@@ -108,16 +109,19 @@ type QueryParameter struct {
108109
// Arrays and slices of the above.
109110
// Structs of the above. Only the exported fields are used.
110111
//
111-
// BigQuery does not support params of type GEOGRAPHY. For users wishing
112-
// to parameterize Geography values, use string parameters and cast in the
113-
// SQL query, e.g. `SELECT ST_GeogFromText(@string_param) as geo`
112+
// For scalar values, you can supply the Null types within this library
113+
// to send the appropriate NULL values (e.g. NullInt64, NullString, etc).
114114
//
115115
// When a QueryParameter is returned inside a QueryConfig from a call to
116116
// Job.Config:
117117
// Integers are of type int64.
118118
// Floating-point values are of type float64.
119119
// Arrays are of type []interface{}, regardless of the array element type.
120120
// Structs are of type map[string]interface{}.
121+
//
122+
// When valid (non-null) Null types are sent, they come back as the Go types indicated
123+
// above. Null strings will report in query statistics as a valid empty
124+
// string.
121125
Value interface{}
122126
}
123127

@@ -132,7 +136,7 @@ func (p QueryParameter) toBQ() (*bq.QueryParameter, error) {
132136
}
133137
return &bq.QueryParameter{
134138
Name: p.Name,
135-
ParameterValue: &pv,
139+
ParameterValue: pv,
136140
ParameterType: pt,
137141
}, nil
138142
}
@@ -142,16 +146,26 @@ func paramType(t reflect.Type) (*bq.QueryParameterType, error) {
142146
return nil, errors.New("bigquery: nil parameter")
143147
}
144148
switch t {
145-
case typeOfDate:
149+
case typeOfDate, typeOfNullDate:
146150
return dateParamType, nil
147-
case typeOfTime:
151+
case typeOfTime, typeOfNullTime:
148152
return timeParamType, nil
149-
case typeOfDateTime:
153+
case typeOfDateTime, typeOfNullDateTime:
150154
return dateTimeParamType, nil
151-
case typeOfGoTime:
155+
case typeOfGoTime, typeOfNullTimestamp:
152156
return timestampParamType, nil
153157
case typeOfRat:
154158
return numericParamType, nil
159+
case typeOfNullBool:
160+
return boolParamType, nil
161+
case typeOfNullFloat64:
162+
return float64ParamType, nil
163+
case typeOfNullInt64:
164+
return int64ParamType, nil
165+
case typeOfNullString:
166+
return stringParamType, nil
167+
case typeOfNullGeography:
168+
return geographyParamType, nil
155169
}
156170
switch t.Kind() {
157171
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32:
@@ -207,17 +221,64 @@ func paramType(t reflect.Type) (*bq.QueryParameterType, error) {
207221
return nil, fmt.Errorf("bigquery: Go type %s cannot be represented as a parameter type", t)
208222
}
209223

210-
func paramValue(v reflect.Value) (bq.QueryParameterValue, error) {
211-
var res bq.QueryParameterValue
224+
func paramValue(v reflect.Value) (*bq.QueryParameterValue, error) {
225+
res := &bq.QueryParameterValue{}
212226
if !v.IsValid() {
213227
return res, errors.New("bigquery: nil parameter")
214228
}
215229
t := v.Type()
216230
switch t {
231+
232+
// Handle all the custom null types as a group first, as they all have the same logic when invalid.
233+
case typeOfNullInt64,
234+
typeOfNullString,
235+
typeOfNullGeography,
236+
typeOfNullFloat64,
237+
typeOfNullBool,
238+
typeOfNullTimestamp,
239+
typeOfNullDate,
240+
typeOfNullTime,
241+
typeOfNullDateTime:
242+
// Shared: If the Null type isn't valid, we have no value to send.
243+
// However, the backend requires us to send the QueryParameterValue with
244+
// the fields empty.
245+
if !v.FieldByName("Valid").Bool() {
246+
// Ensure we don't send a default value by using NullFields in the JSON
247+
// serialization.
248+
res.NullFields = append(res.NullFields, "Value")
249+
return res, nil
250+
}
251+
// For cases where the Null type is valid, populate the scalar value as needed.
252+
switch t {
253+
case typeOfNullInt64:
254+
res.Value = fmt.Sprint(v.FieldByName("Int64").Interface())
255+
case typeOfNullString:
256+
res.Value = fmt.Sprint(v.FieldByName("StringVal").Interface())
257+
case typeOfNullGeography:
258+
res.Value = fmt.Sprint(v.FieldByName("GeographyVal").Interface())
259+
case typeOfNullFloat64:
260+
res.Value = fmt.Sprint(v.FieldByName("Float64").Interface())
261+
case typeOfNullBool:
262+
res.Value = fmt.Sprint(v.FieldByName("Bool").Interface())
263+
case typeOfNullTimestamp:
264+
res.Value = v.FieldByName("Timestamp").Interface().(time.Time).Format(timestampFormat)
265+
case typeOfNullDate:
266+
res.Value = v.FieldByName("Date").Interface().(civil.Date).String()
267+
case typeOfNullTime:
268+
res.Value = CivilTimeString(v.FieldByName("Time").Interface().(civil.Time))
269+
case typeOfNullDateTime:
270+
res.Value = CivilDateTimeString(v.FieldByName("DateTime").Interface().(civil.DateTime))
271+
}
272+
// We expect to produce a value in all these cases, so force send if the result is the empty
273+
// string.
274+
if res.Value == "" {
275+
res.ForceSendFields = append(res.ForceSendFields, "Value")
276+
}
277+
return res, nil
278+
217279
case typeOfDate:
218280
res.Value = v.Interface().(civil.Date).String()
219281
return res, nil
220-
221282
case typeOfTime:
222283
// civil.Time has nanosecond resolution, but BigQuery TIME only microsecond.
223284
// (If we send nanoseconds, then when we try to read the result we get "query job
@@ -253,11 +314,11 @@ func paramValue(v reflect.Value) (bq.QueryParameterValue, error) {
253314
for i := 0; i < v.Len(); i++ {
254315
val, err := paramValue(v.Index(i))
255316
if err != nil {
256-
return bq.QueryParameterValue{}, err
317+
return nil, err
257318
}
258-
vals = append(vals, &val)
319+
vals = append(vals, val)
259320
}
260-
return bq.QueryParameterValue{ArrayValues: vals}, nil
321+
return &bq.QueryParameterValue{ArrayValues: vals}, nil
261322

262323
case reflect.Ptr:
263324
if t.Elem().Kind() != reflect.Struct {
@@ -274,16 +335,16 @@ func paramValue(v reflect.Value) (bq.QueryParameterValue, error) {
274335
case reflect.Struct:
275336
fields, err := fieldCache.Fields(t)
276337
if err != nil {
277-
return bq.QueryParameterValue{}, err
338+
return nil, err
278339
}
279340
res.StructValues = map[string]bq.QueryParameterValue{}
280341
for _, f := range fields {
281342
fv := v.FieldByIndex(f.Index)
282343
fp, err := paramValue(fv)
283344
if err != nil {
284-
return bq.QueryParameterValue{}, err
345+
return nil, err
285346
}
286-
res.StructValues[f.Name] = fp
347+
res.StructValues[f.Name] = *fp
287348
}
288349
return res, nil
289350
}
@@ -317,10 +378,12 @@ var paramTypeToFieldType = map[string]FieldType{
317378
timeParamType.Type: TimeFieldType,
318379
numericParamType.Type: NumericFieldType,
319380
bigNumericParamType.Type: BigNumericFieldType,
381+
geographyParamType.Type: GeographyFieldType,
320382
}
321383

322384
// Convert a parameter value from the service to a Go value. This is similar to, but
323-
// not quite the same as, converting data values.
385+
// not quite the same as, converting data values. Namely, rather than returning nil
386+
// directly, we wrap them in the appropriate Null types (NullInt64, etc).
324387
func convertParamValue(qval *bq.QueryParameterValue, qtype *bq.QueryParameterType) (interface{}, error) {
325388
switch qtype.Type {
326389
case "ARRAY":
@@ -334,14 +397,53 @@ func convertParamValue(qval *bq.QueryParameterValue, qtype *bq.QueryParameterTyp
334397
}
335398
return convertParamStruct(qval.StructValues, qtype.StructTypes)
336399
case "TIMESTAMP":
400+
if isNullScalar(qval) {
401+
return NullTimestamp{Valid: false}, nil
402+
}
337403
return time.Parse(timestampFormat, qval.Value)
338404
case "DATETIME":
405+
if isNullScalar(qval) {
406+
return NullDateTime{Valid: false}, nil
407+
}
339408
return parseCivilDateTime(qval.Value)
340409
default:
410+
if isNullScalar(qval) {
411+
switch qtype.Type {
412+
case "INT64":
413+
return NullInt64{Valid: false}, nil
414+
case "STRING":
415+
return NullString{Valid: false}, nil
416+
case "FLOAT64":
417+
return NullFloat64{Valid: false}, nil
418+
case "BOOL":
419+
return NullBool{Valid: false}, nil
420+
case "DATE":
421+
return NullDate{Valid: false}, nil
422+
case "TIME":
423+
return NullTime{Valid: false}, nil
424+
case "GEOGRAPHY":
425+
return NullGeography{Valid: false}, nil
426+
}
427+
428+
}
341429
return convertBasicType(qval.Value, paramTypeToFieldType[qtype.Type])
342430
}
343431
}
344432

433+
// isNullScalar determines if the input is meant to represent a null scalar
434+
// value.
435+
func isNullScalar(qval *bq.QueryParameterValue) bool {
436+
if qval == nil {
437+
return true
438+
}
439+
for _, v := range qval.NullFields {
440+
if v == "Value" {
441+
return true
442+
}
443+
}
444+
return false
445+
}
446+
345447
// convertParamArray converts a query parameter array value to a Go value. It
346448
// always returns a []interface{}.
347449
func convertParamArray(elVals []*bq.QueryParameterValue, elType *bq.QueryParameterType) ([]interface{}, error) {

0 commit comments

Comments
 (0)