76
76
timestampParamType = & bq.QueryParameterType {Type : "TIMESTAMP" }
77
77
numericParamType = & bq.QueryParameterType {Type : "NUMERIC" }
78
78
bigNumericParamType = & bq.QueryParameterType {Type : "BIGNUMERIC" }
79
+ geographyParamType = & bq.QueryParameterType {Type : "GEOGRAPHY" }
79
80
)
80
81
81
82
var (
@@ -108,16 +109,19 @@ type QueryParameter struct {
108
109
// Arrays and slices of the above.
109
110
// Structs of the above. Only the exported fields are used.
110
111
//
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).
114
114
//
115
115
// When a QueryParameter is returned inside a QueryConfig from a call to
116
116
// Job.Config:
117
117
// Integers are of type int64.
118
118
// Floating-point values are of type float64.
119
119
// Arrays are of type []interface{}, regardless of the array element type.
120
120
// 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.
121
125
Value interface {}
122
126
}
123
127
@@ -132,7 +136,7 @@ func (p QueryParameter) toBQ() (*bq.QueryParameter, error) {
132
136
}
133
137
return & bq.QueryParameter {
134
138
Name : p .Name ,
135
- ParameterValue : & pv ,
139
+ ParameterValue : pv ,
136
140
ParameterType : pt ,
137
141
}, nil
138
142
}
@@ -142,16 +146,26 @@ func paramType(t reflect.Type) (*bq.QueryParameterType, error) {
142
146
return nil , errors .New ("bigquery: nil parameter" )
143
147
}
144
148
switch t {
145
- case typeOfDate :
149
+ case typeOfDate , typeOfNullDate :
146
150
return dateParamType , nil
147
- case typeOfTime :
151
+ case typeOfTime , typeOfNullTime :
148
152
return timeParamType , nil
149
- case typeOfDateTime :
153
+ case typeOfDateTime , typeOfNullDateTime :
150
154
return dateTimeParamType , nil
151
- case typeOfGoTime :
155
+ case typeOfGoTime , typeOfNullTimestamp :
152
156
return timestampParamType , nil
153
157
case typeOfRat :
154
158
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
155
169
}
156
170
switch t .Kind () {
157
171
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) {
207
221
return nil , fmt .Errorf ("bigquery: Go type %s cannot be represented as a parameter type" , t )
208
222
}
209
223
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 {}
212
226
if ! v .IsValid () {
213
227
return res , errors .New ("bigquery: nil parameter" )
214
228
}
215
229
t := v .Type ()
216
230
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
+
217
279
case typeOfDate :
218
280
res .Value = v .Interface ().(civil.Date ).String ()
219
281
return res , nil
220
-
221
282
case typeOfTime :
222
283
// civil.Time has nanosecond resolution, but BigQuery TIME only microsecond.
223
284
// (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) {
253
314
for i := 0 ; i < v .Len (); i ++ {
254
315
val , err := paramValue (v .Index (i ))
255
316
if err != nil {
256
- return bq. QueryParameterValue {} , err
317
+ return nil , err
257
318
}
258
- vals = append (vals , & val )
319
+ vals = append (vals , val )
259
320
}
260
- return bq.QueryParameterValue {ArrayValues : vals }, nil
321
+ return & bq.QueryParameterValue {ArrayValues : vals }, nil
261
322
262
323
case reflect .Ptr :
263
324
if t .Elem ().Kind () != reflect .Struct {
@@ -274,16 +335,16 @@ func paramValue(v reflect.Value) (bq.QueryParameterValue, error) {
274
335
case reflect .Struct :
275
336
fields , err := fieldCache .Fields (t )
276
337
if err != nil {
277
- return bq. QueryParameterValue {} , err
338
+ return nil , err
278
339
}
279
340
res .StructValues = map [string ]bq.QueryParameterValue {}
280
341
for _ , f := range fields {
281
342
fv := v .FieldByIndex (f .Index )
282
343
fp , err := paramValue (fv )
283
344
if err != nil {
284
- return bq. QueryParameterValue {} , err
345
+ return nil , err
285
346
}
286
- res .StructValues [f .Name ] = fp
347
+ res .StructValues [f .Name ] = * fp
287
348
}
288
349
return res , nil
289
350
}
@@ -317,10 +378,12 @@ var paramTypeToFieldType = map[string]FieldType{
317
378
timeParamType .Type : TimeFieldType ,
318
379
numericParamType .Type : NumericFieldType ,
319
380
bigNumericParamType .Type : BigNumericFieldType ,
381
+ geographyParamType .Type : GeographyFieldType ,
320
382
}
321
383
322
384
// 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).
324
387
func convertParamValue (qval * bq.QueryParameterValue , qtype * bq.QueryParameterType ) (interface {}, error ) {
325
388
switch qtype .Type {
326
389
case "ARRAY" :
@@ -334,14 +397,53 @@ func convertParamValue(qval *bq.QueryParameterValue, qtype *bq.QueryParameterTyp
334
397
}
335
398
return convertParamStruct (qval .StructValues , qtype .StructTypes )
336
399
case "TIMESTAMP" :
400
+ if isNullScalar (qval ) {
401
+ return NullTimestamp {Valid : false }, nil
402
+ }
337
403
return time .Parse (timestampFormat , qval .Value )
338
404
case "DATETIME" :
405
+ if isNullScalar (qval ) {
406
+ return NullDateTime {Valid : false }, nil
407
+ }
339
408
return parseCivilDateTime (qval .Value )
340
409
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
+ }
341
429
return convertBasicType (qval .Value , paramTypeToFieldType [qtype .Type ])
342
430
}
343
431
}
344
432
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
+
345
447
// convertParamArray converts a query parameter array value to a Go value. It
346
448
// always returns a []interface{}.
347
449
func convertParamArray (elVals []* bq.QueryParameterValue , elType * bq.QueryParameterType ) ([]interface {}, error ) {
0 commit comments