cloud.google.com/go/bigquery: respect microsecond precision of Timestamps
With this change, the time.Time values returned by convertBasicType
always correspond to a round number of microseconds since the epoch,
which matches the microsecond precision that BigQuery maintains
internally.
Change-Id: I0adb0aaaa918c184eb8ba4a888396f41f1a94d14
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/44530
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Seth Hollyman <shollyman@google.com>
diff --git a/bigquery/value.go b/bigquery/value.go
index 128dd41..a7c3ed0 100644
--- a/bigquery/value.go
+++ b/bigquery/value.go
@@ -870,8 +870,10 @@
return nil, err
}
secs := math.Trunc(f)
- nanos := (f - secs) * 1e9
- return Value(time.Unix(int64(secs), int64(nanos)).UTC()), nil
+ // Timestamps in BigQuery have microsecond precision, so we must
+ // return a round number of microseconds.
+ micros := math.Trunc((f-secs)*1e6 + 0.5)
+ return Value(time.Unix(int64(secs), int64(micros)*1000).UTC()), nil
case DateFieldType:
return civil.ParseDate(val)
case TimeFieldType:
diff --git a/bigquery/value_test.go b/bigquery/value_test.go
index 7540c05..1e5bb2d 100644
--- a/bigquery/value_test.go
+++ b/bigquery/value_test.go
@@ -106,6 +106,44 @@
}
}
+func TestConvertTimePrecision(t *testing.T) {
+ tcs := []struct {
+ // Internally, BigQuery stores timestamps as microsecond-precision
+ // floats.
+ bq float64
+ want time.Time
+ }{
+ {
+ bq: 1555593697.154358,
+ want: time.Unix(1555593697, 154358*1000),
+ },
+ {
+ bq: 1555593697.154359,
+ want: time.Unix(1555593697, 154359*1000),
+ },
+ {
+ bq: 1555593697.154360,
+ want: time.Unix(1555593697, 154360*1000),
+ },
+ }
+ for _, tc := range tcs {
+ bqS := fmt.Sprintf("%.6f", tc.bq)
+ t.Run(bqS, func(t *testing.T) {
+ got, err := convertBasicType(bqS, TimestampFieldType)
+ if err != nil {
+ t.Fatalf("convertBasicType failed: %v", err)
+ }
+ gotT, ok := got.(time.Time)
+ if !ok {
+ t.Fatalf("got a %T from convertBasicType, want a time.Time; got = %v", got, got)
+ }
+ if !gotT.Equal(tc.want) {
+ t.Errorf("got %v from convertBasicType, want %v", gotT, tc.want)
+ }
+ })
+ }
+}
+
func TestConvertNullValues(t *testing.T) {
schema := Schema{{Type: StringFieldType}}
row := &bq.TableRow{