Repository: googleapis/google-cloud-go
Pull request #2779 (2020-08-26 15:53:09)

feat(bigquery): add support for bignumeric
This PR adds basic support for the BIGNUMERIC type in BigQuery.

This library has several cases where it tries to infer the appropriate BigQuery type from a native Go type.  For `big.Rat` types, we continue the existing behavior of mapping them to NUMERIC, as the big.Rat doesn't
provide a general way of indicating desired precision or scale to determine whether BIGNUMERIC is a more
appropriate mapping.


Patch:
diff --git a/bigquery/params.go b/bigquery/params.go
index 5957022618..68bb3fa01a 100644
--- a/bigquery/params.go
+++ b/bigquery/params.go
@@ -65,16 +65,17 @@ func (e invalidFieldNameError) Error() string {
 var fieldCache = fields.NewCache(bqTagParser, nil, nil)
 
 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"}
-	dateParamType      = &bq.QueryParameterType{Type: "DATE"}
-	timeParamType      = &bq.QueryParameterType{Type: "TIME"}
-	dateTimeParamType  = &bq.QueryParameterType{Type: "DATETIME"}
-	timestampParamType = &bq.QueryParameterType{Type: "TIMESTAMP"}
-	numericParamType   = &bq.QueryParameterType{Type: "NUMERIC"}
+	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"}
+	dateParamType       = &bq.QueryParameterType{Type: "DATE"}
+	timeParamType       = &bq.QueryParameterType{Type: "TIME"}
+	dateTimeParamType   = &bq.QueryParameterType{Type: "DATETIME"}
+	timestampParamType  = &bq.QueryParameterType{Type: "TIMESTAMP"}
+	numericParamType    = &bq.QueryParameterType{Type: "NUMERIC"}
+	bigNumericParamType = &bq.QueryParameterType{Type: "BIGNUMERIC"}
 )
 
 var (
@@ -233,6 +234,9 @@ func paramValue(v reflect.Value) (bq.QueryParameterValue, error) {
 		return res, nil
 
 	case typeOfRat:
+		// big.Rat types don't communicate scale or precision, so we cannot
+		// disambiguate between NUMERIC and BIGNUMERIC.  For now, we'll continue
+		// to honor previous behavior and send as Numeric type.
 		res.Value = NumericString(v.Interface().(*big.Rat))
 		return res, nil
 	}
@@ -304,14 +308,15 @@ func bqToQueryParameter(q *bq.QueryParameter) (QueryParameter, error) {
 }
 
 var paramTypeToFieldType = map[string]FieldType{
-	int64ParamType.Type:   IntegerFieldType,
-	float64ParamType.Type: FloatFieldType,
-	boolParamType.Type:    BooleanFieldType,
-	stringParamType.Type:  StringFieldType,
-	bytesParamType.Type:   BytesFieldType,
-	dateParamType.Type:    DateFieldType,
-	timeParamType.Type:    TimeFieldType,
-	numericParamType.Type: NumericFieldType,
+	int64ParamType.Type:      IntegerFieldType,
+	float64ParamType.Type:    FloatFieldType,
+	boolParamType.Type:       BooleanFieldType,
+	stringParamType.Type:     StringFieldType,
+	bytesParamType.Type:      BytesFieldType,
+	dateParamType.Type:       DateFieldType,
+	timeParamType.Type:       TimeFieldType,
+	numericParamType.Type:    NumericFieldType,
+	bigNumericParamType.Type: BigNumericFieldType,
 }
 
 // Convert a parameter value from the service to a Go value. This is similar to, but
diff --git a/bigquery/schema.go b/bigquery/schema.go
index a9a50ef000..dcb5c6f9b7 100644
--- a/bigquery/schema.go
+++ b/bigquery/schema.go
@@ -182,23 +182,27 @@ const (
 	// GeographyFieldType is a string field type.  Geography types represent a set of points
 	// on the Earth's surface, represented in Well Known Text (WKT) format.
 	GeographyFieldType FieldType = "GEOGRAPHY"
+	// BigNumericFieldType is a numeric field type that supports values of larger precision
+	// and scale than the NumericFieldType.
+	BigNumericFieldType FieldType = "BIGNUMERIC"
 )
 
 var (
 	errEmptyJSONSchema = errors.New("bigquery: empty JSON schema")
 	fieldTypes         = map[FieldType]bool{
-		StringFieldType:    true,
-		BytesFieldType:     true,
-		IntegerFieldType:   true,
-		FloatFieldType:     true,
-		BooleanFieldType:   true,
-		TimestampFieldType: true,
-		RecordFieldType:    true,
-		DateFieldType:      true,
-		TimeFieldType:      true,
-		DateTimeFieldType:  true,
-		NumericFieldType:   true,
-		GeographyFieldType: true,
+		StringFieldType:     true,
+		BytesFieldType:      true,
+		IntegerFieldType:    true,
+		FloatFieldType:      true,
+		BooleanFieldType:    true,
+		TimestampFieldType:  true,
+		RecordFieldType:     true,
+		DateFieldType:       true,
+		TimeFieldType:       true,
+		DateTimeFieldType:   true,
+		NumericFieldType:    true,
+		GeographyFieldType:  true,
+		BigNumericFieldType: true,
 	}
 	// The API will accept alias names for the types based on the Standard SQL type names.
 	fieldAliases = map[FieldType]FieldType{
@@ -346,6 +350,10 @@ func inferFieldSchema(fieldName string, rt reflect.Type, nullable bool) (*FieldS
 	case typeOfDateTime:
 		return &FieldSchema{Required: true, Type: DateTimeFieldType}, nil
 	case typeOfRat:
+		// We automatically infer big.Rat values as NUMERIC as we cannot
+		// determine precision/scale from the type.  Users who want the
+		// larger precision of BIGNUMERIC need to manipulate the inferred
+		// schema.
 		return &FieldSchema{Required: !nullable, Type: NumericFieldType}, nil
 	}
 	if ft := nullableFieldType(rt); ft != "" {
diff --git a/bigquery/value.go b/bigquery/value.go
index 361341c2bb..cae28be046 100644
--- a/bigquery/value.go
+++ b/bigquery/value.go
@@ -407,6 +407,13 @@ func determineSetFunc(ftype reflect.Type, stype FieldType) setFunc {
 				return setNull(v, x, func() interface{} { return x.(*big.Rat) })
 			}
 		}
+
+	case BigNumericFieldType:
+		if ftype == typeOfRat {
+			return func(v reflect.Value, x interface{}) error {
+				return setNull(v, x, func() interface{} { return x.(*big.Rat) })
+			}
+		}
 	}
 	return nil
 }
@@ -692,7 +699,7 @@ func structFieldToUploadValue(vfield reflect.Value, schemaField *FieldSchema) (i
 }
 
 func toUploadValue(val interface{}, fs *FieldSchema) interface{} {
-	if fs.Type == TimeFieldType || fs.Type == DateTimeFieldType || fs.Type == NumericFieldType {
+	if fs.Type == TimeFieldType || fs.Type == DateTimeFieldType || fs.Type == NumericFieldType || fs.Type == BigNumericFieldType {
 		return toUploadValueReflect(reflect.ValueOf(val), fs)
 	}
 	return val
@@ -721,6 +728,13 @@ func toUploadValueReflect(v reflect.Value, fs *FieldSchema) interface{} {
 		return formatUploadValue(v, fs, func(v reflect.Value) string {
 			return NumericString(v.Interface().(*big.Rat))
 		})
+	case BigNumericFieldType:
+		if r, ok := v.Interface().(*big.Rat); ok && r == nil {
+			return nil
+		}
+		return formatUploadValue(v, fs, func(v reflect.Value) string {
+			return BigNumericString(v.Interface().(*big.Rat))
+		})
 	default:
 		if !fs.Repeated || v.Len() > 0 {
 			return v.Interface()
@@ -786,6 +800,12 @@ const (
 
 	// NumericScaleDigits is the maximum number of digits after the decimal point in a NUMERIC value.
 	NumericScaleDigits = 9
+
+	// BigNumericPrecisionDigits is the maximum number of full digits in a BIGNUMERIC value.
+	BigNumericPrecisionDigits = 76
+
+	// BigNumericScaleDigits is the maximum number of full digits in a BIGNUMERIC value.
+	BigNumericScaleDigits = 38
 )
 
 // NumericString returns a string representing a *big.Rat in a format compatible
@@ -795,6 +815,12 @@ func NumericString(r *big.Rat) string {
 	return r.FloatString(NumericScaleDigits)
 }
 
+// BigNumericString returns a string representing a *big.Rat in a format compatible with BigQuery
+// SQL.  It returns a floating point literal with 38 digits after the decimal point.
+func BigNumericString(r *big.Rat) string {
+	return r.FloatString(BigNumericScaleDigits)
+}
+
 // convertRows converts a series of TableRows into a series of Value slices.
 // schema is used to interpret the data from rows; its length must match the
 // length of each row.
@@ -913,6 +939,12 @@ func convertBasicType(val string, typ FieldType) (Value, error) {
 			return nil, fmt.Errorf("bigquery: invalid NUMERIC value %q", val)
 		}
 		return Value(r), nil
+	case BigNumericFieldType:
+		r, ok := (&big.Rat{}).SetString(val)
+		if !ok {
+			return nil, fmt.Errorf("bigquery: invalid BIGNUMERIC value %q", val)
+		}
+		return Value(r), nil
 	case GeographyFieldType:
 		return val, nil
 	default:
diff --git a/go.sum b/go.sum
index ea567ca6a7..b75391c1aa 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,7 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0 h1:a/O/bK/vWrYGOTFtH8di4rBxMZnmkjy+Y5LxpDwo+dA=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
 cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@@ -348,6 +349,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuA
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=


Test patch:
diff --git a/bigquery/integration_test.go b/bigquery/integration_test.go
index 14573e2bb3..60959d0a0c 100644
--- a/bigquery/integration_test.go
+++ b/bigquery/integration_test.go
@@ -1434,6 +1434,7 @@ func TestIntegration_InsertAndReadNullable(t *testing.T) {
 	ctm := civil.Time{Hour: 15, Minute: 4, Second: 5, Nanosecond: 6000}
 	cdt := civil.DateTime{Date: testDate, Time: ctm}
 	rat := big.NewRat(33, 100)
+	rat2 := big.NewRat(66, 100)
 	geo := "POINT(-122.198939 47.669865)"
 
 	// Nil fields in the struct.
@@ -1455,20 +1456,21 @@ func TestIntegration_InsertAndReadNullable(t *testing.T) {
 
 	// Populate the struct with values.
 	testInsertAndReadNullable(t, testStructNullable{
-		String:    NullString{"x", true},
-		Bytes:     []byte{1, 2, 3},
-		Integer:   NullInt64{1, true},
-		Float:     NullFloat64{2.3, true},
-		Boolean:   NullBool{true, true},
-		Timestamp: NullTimestamp{testTimestamp, true},
-		Date:      NullDate{testDate, true},
-		Time:      NullTime{ctm, true},
-		DateTime:  NullDateTime{cdt, true},
-		Numeric:   rat,
-		Geography: NullGeography{geo, true},
-		Record:    &subNullable{X: NullInt64{4, true}},
+		String:     NullString{"x", true},
+		Bytes:      []byte{1, 2, 3},
+		Integer:    NullInt64{1, true},
+		Float:      NullFloat64{2.3, true},
+		Boolean:    NullBool{true, true},
+		Timestamp:  NullTimestamp{testTimestamp, true},
+		Date:       NullDate{testDate, true},
+		Time:       NullTime{ctm, true},
+		DateTime:   NullDateTime{cdt, true},
+		Numeric:    rat,
+		BigNumeric: rat2,
+		Geography:  NullGeography{geo, true},
+		Record:     &subNullable{X: NullInt64{4, true}},
 	},
-		[]Value{"x", []byte{1, 2, 3}, int64(1), 2.3, true, testTimestamp, testDate, ctm, cdt, rat, geo, []Value{int64(4)}})
+		[]Value{"x", []byte{1, 2, 3}, int64(1), 2.3, true, testTimestamp, testDate, ctm, cdt, rat, rat2, geo, []Value{int64(4)}})
 }
 
 func testInsertAndReadNullable(t *testing.T, ts testStructNullable, wantRow []Value) {
diff --git a/bigquery/schema_test.go b/bigquery/schema_test.go
index 5bd10849ac..ae936980c7 100644
--- a/bigquery/schema_test.go
+++ b/bigquery/schema_test.go
@@ -1041,7 +1041,8 @@ func TestSchemaFromJSON(t *testing.T) {
 	{"name":"flat_date","type":"DATE","mode":"NULLABLE","description":"Flat required DATE"},
 	{"name":"flat_time","type":"TIME","mode":"REQUIRED","description":"Flat nullable TIME"},
 	{"name":"flat_datetime","type":"DATETIME","mode":"NULLABLE","description":"Flat required DATETIME"},
-	{"name":"flat_numeric","type":"NUMERIC","mode":"REQUIRED","description":"Flat nullable NUMERIC"},
+	{"name":"flat_numeric","type":"NUMERIC","mode":"REQUIRED","description":"Flat required NUMERIC"},
+	{"name":"flat_bignumeric","type":"BIGNUMERIC","mode":"NULLABLE","description":"Flat nullable BIGNUMERIC"},
 	{"name":"flat_geography","type":"GEOGRAPHY","mode":"REQUIRED","description":"Flat required GEOGRAPHY"},
 	{"name":"aliased_integer","type":"INT64","mode":"REQUIRED","description":"Aliased required integer"},
 	{"name":"aliased_boolean","type":"BOOL","mode":"NULLABLE","description":"Aliased nullable boolean"},
@@ -1058,7 +1059,8 @@ func TestSchemaFromJSON(t *testing.T) {
 				fieldSchema("Flat required DATE", "flat_date", "DATE", false, false, nil),
 				fieldSchema("Flat nullable TIME", "flat_time", "TIME", false, true, nil),
 				fieldSchema("Flat required DATETIME", "flat_datetime", "DATETIME", false, false, nil),
-				fieldSchema("Flat nullable NUMERIC", "flat_numeric", "NUMERIC", false, true, nil),
+				fieldSchema("Flat required NUMERIC", "flat_numeric", "NUMERIC", false, true, nil),
+				fieldSchema("Flat nullable BIGNUMERIC", "flat_bignumeric", "BIGNUMERIC", false, false, nil),
 				fieldSchema("Flat required GEOGRAPHY", "flat_geography", "GEOGRAPHY", false, true, nil),
 				fieldSchema("Aliased required integer", "aliased_integer", "INTEGER", false, true, nil),
 				fieldSchema("Aliased nullable boolean", "aliased_boolean", "BOOLEAN", false, false, nil),
diff --git a/bigquery/value_test.go b/bigquery/value_test.go
index 1e5bb2d37f..4d08336ed0 100644
--- a/bigquery/value_test.go
+++ b/bigquery/value_test.go
@@ -36,6 +36,7 @@ func TestConvertBasicValues(t *testing.T) {
 		{Type: BooleanFieldType},
 		{Type: BytesFieldType},
 		{Type: NumericFieldType},
+		{Type: BigNumericFieldType},
 		{Type: GeographyFieldType},
 	}
 	row := &bq.TableRow{
@@ -46,6 +47,7 @@ func TestConvertBasicValues(t *testing.T) {
 			{V: "true"},
 			{V: base64.StdEncoding.EncodeToString([]byte("foo"))},
 			{V: "123.123456789"},
+			{V: "99999999999999999999999999999999999999.99999999999999999999999999999999999999"},
 			{V: testGeography},
 		},
 	}
@@ -54,7 +56,9 @@ func TestConvertBasicValues(t *testing.T) {
 		t.Fatalf("error converting: %v", err)
 	}
 
-	want := []Value{"a", int64(1), 1.2, true, []byte("foo"), big.NewRat(123123456789, 1e9), testGeography}
+	bigRatVal := new(big.Rat)
+	bigRatVal.SetString("99999999999999999999999999999999999999.99999999999999999999999999999999999999")
+	want := []Value{"a", int64(1), 1.2, true, []byte("foo"), big.NewRat(123123456789, 1e9), bigRatVal, testGeography}
 	if !testutil.Equal(got, want) {
 		t.Errorf("converting basic values: got:\n%v\nwant:\n%v", got, want)
 	}
@@ -444,6 +448,7 @@ func TestValuesSaverConvertsToMap(t *testing.T) {
 					{Name: "strField", Type: StringFieldType},
 					{Name: "dtField", Type: DateTimeFieldType},
 					{Name: "nField", Type: NumericFieldType},
+					{Name: "bigNumField", Type: BigNumericFieldType},
 					{Name: "geoField", Type: GeographyFieldType},
 				},
 				InsertID: "iid",
@@ -452,16 +457,18 @@ func TestValuesSaverConvertsToMap(t *testing.T) {
 						Date: civil.Date{Year: 1, Month: 2, Day: 3},
 						Time: civil.Time{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7000}},
 					big.NewRat(123456789000, 1e9),
+					big.NewRat(1, 3),
 					testGeography,
 				},
 			},
 			wantInsertID: "iid",
 			wantRow: map[string]Value{
-				"intField": 1,
-				"strField": "a",
-				"dtField":  "0001-02-03 04:05:06.000007",
-				"nField":   "123.456789000",
-				"geoField": testGeography,
+				"intField":    1,
+				"strField":    "a",
+				"dtField":     "0001-02-03 04:05:06.000007",
+				"nField":      "123.456789000",
+				"bigNumField": "0.33333333333333333333333333333333333333",
+				"geoField":    testGeography,
 			},
 		},
 		{
@@ -603,6 +610,8 @@ func TestStructSaver(t *testing.T) {
 		{Name: "p", Type: IntegerFieldType, Required: false},
 		{Name: "n", Type: NumericFieldType, Required: false},
 		{Name: "nr", Type: NumericFieldType, Repeated: true},
+		{Name: "bn", Type: BigNumericFieldType, Required: false},
+		{Name: "bnr", Type: BigNumericFieldType, Repeated: true},
 		{Name: "g", Type: GeographyFieldType, Required: false},
 		{Name: "gr", Type: GeographyFieldType, Repeated: true},
 	}
@@ -619,6 +628,8 @@ func TestStructSaver(t *testing.T) {
 			P       NullInt64
 			N       *big.Rat
 			NR      []*big.Rat
+			BN      *big.Rat
+			BNR     []*big.Rat
 			G       NullGeography
 			GR      []string // Repeated Geography
 		}
@@ -654,6 +665,8 @@ func TestStructSaver(t *testing.T) {
 		P:       NullInt64{Valid: true, Int64: 17},
 		N:       big.NewRat(123456, 1000),
 		NR:      []*big.Rat{big.NewRat(3, 1), big.NewRat(56789, 1e5)},
+		BN:      big.NewRat(1, 3),
+		BNR:     []*big.Rat{big.NewRat(1, 3), big.NewRat(1, 2)},
 		G:       NullGeography{Valid: true, GeographyVal: "POINT(-122.350220 47.649154)"},
 		GR:      []string{"POINT(-122.350220 47.649154)", "POINT(-122.198939 47.669865)"},
 	}
@@ -667,6 +680,8 @@ func TestStructSaver(t *testing.T) {
 		"p":       NullInt64{Valid: true, Int64: 17},
 		"n":       "123.456000000",
 		"nr":      []string{"3.000000000", "0.567890000"},
+		"bn":      "0.33333333333333333333333333333333333333",
+		"bnr":     []string{"0.33333333333333333333333333333333333333", "0.50000000000000000000000000000000000000"},
 		"g":       NullGeography{Valid: true, GeographyVal: "POINT(-122.350220 47.649154)"},
 		"gr":      []string{"POINT(-122.350220 47.649154)", "POINT(-122.198939 47.669865)"},
 	}
@@ -729,20 +744,24 @@ func TestStructSaverErrors(t *testing.T) {
 	}
 }
 
-func TestNumericString(t *testing.T) {
+func TestNumericStrings(t *testing.T) {
 	for _, test := range []struct {
-		in   *big.Rat
-		want string
+		description    string
+		in             *big.Rat
+		wantNumeric    string
+		wantBigNumeric string
 	}{
-		{big.NewRat(2, 3), "0.666666667"}, // round to 9 places
-		{big.NewRat(1, 2), "0.500000000"},
-		{big.NewRat(1, 2*1e8), "0.000000005"},
-		{big.NewRat(5, 1e10), "0.000000001"},   // round up the 5 in the 10th decimal place
-		{big.NewRat(-5, 1e10), "-0.000000001"}, // round half away from zero
+		{"repeating with rounding", big.NewRat(2, 3), "0.666666667", "0.66666666666666666666666666666666666667"},
+		{"all zero padding", big.NewRat(1, 2), "0.500000000", "0.50000000000000000000000000000000000000"},
+		{"zero pad with digit", big.NewRat(1, 2*1e8), "0.000000005", "0.00000000500000000000000000000000000000"},
+		{"smaller rounding case 1", big.NewRat(5, 1e10), "0.000000001", "0.00000000050000000000000000000000000000"},
+		{"smaller rounding case 2", big.NewRat(-5, 1e10), "-0.000000001", "-0.00000000050000000000000000000000000000"},
 	} {
-		got := NumericString(test.in)
-		if got != test.want {
-			t.Errorf("%v: got %q, want %q", test.in, got, test.want)
+		if got := NumericString(test.in); got != test.wantNumeric {
+			t.Errorf("case %q, val %v as numeric: got %q, want %q", test.description, test.in, got, test.wantNumeric)
+		}
+		if got := BigNumericString(test.in); got != test.wantBigNumeric {
+			t.Errorf("case %q, val %v as bignumeric: got %q, want %q", test.description, test.in, got, test.wantBigNumeric)
 		}
 	}
 }
@@ -887,6 +906,7 @@ var (
 		{Name: "T", Type: TimeFieldType},
 		{Name: "DT", Type: DateTimeFieldType},
 		{Name: "N", Type: NumericFieldType},
+		{Name: "BN", Type: BigNumericFieldType},
 		{Name: "G", Type: GeographyFieldType},
 		{Name: "nested", Type: RecordFieldType, Schema: Schema{
 			{Name: "nestS", Type: StringFieldType},
@@ -895,16 +915,17 @@ var (
 		{Name: "t", Type: StringFieldType},
 	}
 
-	testTimestamp = time.Date(2016, 11, 5, 7, 50, 22, 8, time.UTC)
-	testDate      = civil.Date{Year: 2016, Month: 11, Day: 5}
-	testTime      = civil.Time{Hour: 7, Minute: 50, Second: 22, Nanosecond: 8}
-	testDateTime  = civil.DateTime{Date: testDate, Time: testTime}
-	testNumeric   = big.NewRat(123, 456)
+	testTimestamp  = time.Date(2016, 11, 5, 7, 50, 22, 8, time.UTC)
+	testDate       = civil.Date{Year: 2016, Month: 11, Day: 5}
+	testTime       = civil.Time{Hour: 7, Minute: 50, Second: 22, Nanosecond: 8}
+	testDateTime   = civil.DateTime{Date: testDate, Time: testTime}
+	testNumeric    = big.NewRat(123, 456)
+	testBigNumeric = big.NewRat(456, 789)
 	// testGeography is a WKT string representing a single point.
 	testGeography = "POINT(-122.350220 47.649154)"
 
 	testValues = []Value{"x", "y", []byte{1, 2, 3}, int64(7), int64(8), 3.14, true,
-		testTimestamp, testDate, testTime, testDateTime, testNumeric, testGeography,
+		testTimestamp, testDate, testTime, testDateTime, testNumeric, testBigNumeric, testGeography,
 		[]Value{"nested", int64(17)}, "z"}
 )
 
@@ -918,6 +939,7 @@ type testStruct1 struct {
 	By     []byte
 	F      float64
 	N      *big.Rat
+	BN     *big.Rat
 	G      string
 	Nested nested
 	Tagged string `bigquery:"t"`
@@ -952,6 +974,7 @@ func TestStructLoader(t *testing.T) {
 		S2:     "y",
 		By:     []byte{1, 2, 3},
 		N:      big.NewRat(123, 456),
+		BN:     big.NewRat(456, 789),
 		G:      testGeography,
 		Nested: nested{NestS: "nested", NestI: 17},
 		Tagged: "z",
@@ -1043,18 +1066,19 @@ func TestStructLoaderRepeated(t *testing.T) {
 }
 
 type testStructNullable struct {
-	String    NullString
-	Bytes     []byte
-	Integer   NullInt64
-	Float     NullFloat64
-	Boolean   NullBool
-	Timestamp NullTimestamp
-	Date      NullDate
-	Time      NullTime
-	DateTime  NullDateTime
-	Numeric   *big.Rat
-	Geography NullGeography
-	Record    *subNullable
+	String     NullString
+	Bytes      []byte
+	Integer    NullInt64
+	Float      NullFloat64
+	Boolean    NullBool
+	Timestamp  NullTimestamp
+	Date       NullDate
+	Time       NullTime
+	DateTime   NullDateTime
+	Numeric    *big.Rat
+	BigNumeric *big.Rat
+	Geography  NullGeography
+	Record     *subNullable
 }
 
 type subNullable struct {
@@ -1072,6 +1096,7 @@ var testStructNullableSchema = Schema{
 	{Name: "Time", Type: TimeFieldType, Required: false},
 	{Name: "DateTime", Type: DateTimeFieldType, Required: false},
 	{Name: "Numeric", Type: NumericFieldType, Required: false},
+	{Name: "BigNumeric", Type: BigNumericFieldType, Required: false},
 	{Name: "Geography", Type: GeographyFieldType, Required: false},
 	{Name: "Record", Type: RecordFieldType, Required: false, Schema: Schema{
 		{Name: "X", Type: IntegerFieldType, Required: false},
@@ -1088,24 +1113,25 @@ func TestStructLoaderNullable(t *testing.T) {
 	}
 
 	nonnilVals := []Value{"x", []byte{1, 2, 3}, int64(1), 2.3, true, testTimestamp, testDate, testTime,
-		testDateTime, big.NewRat(1, 2), testGeography, []Value{int64(4)}}
+		testDateTime, big.NewRat(1, 2), big.NewRat(3, 4), testGeography, []Value{int64(4)}}
 
 	// All ts fields are nil. Loading non-nil values will cause them all to
 	// be allocated.
 	mustLoad(t, &ts, testStructNullableSchema, nonnilVals)
 	want = testStructNullable{
-		String:    NullString{StringVal: "x", Valid: true},
-		Bytes:     []byte{1, 2, 3},
-		Integer:   NullInt64{Int64: 1, Valid: true},
-		Float:     NullFloat64{Float64: 2.3, Valid: true},
-		Boolean:   NullBool{Bool: true, Valid: true},
-		Timestamp: NullTimestamp{Timestamp: testTimestamp, Valid: true},
-		Date:      NullDate{Date: testDate, Valid: true},
-		Time:      NullTime{Time: testTime, Valid: true},
-		DateTime:  NullDateTime{DateTime: testDateTime, Valid: true},
-		Numeric:   big.NewRat(1, 2),
-		Geography: NullGeography{GeographyVal: testGeography, Valid: true},
-		Record:    &subNullable{X: NullInt64{Int64: 4, Valid: true}},
+		String:     NullString{StringVal: "x", Valid: true},
+		Bytes:      []byte{1, 2, 3},
+		Integer:    NullInt64{Int64: 1, Valid: true},
+		Float:      NullFloat64{Float64: 2.3, Valid: true},
+		Boolean:    NullBool{Bool: true, Valid: true},
+		Timestamp:  NullTimestamp{Timestamp: testTimestamp, Valid: true},
+		Date:       NullDate{Date: testDate, Valid: true},
+		Time:       NullTime{Time: testTime, Valid: true},
+		DateTime:   NullDateTime{DateTime: testDateTime, Valid: true},
+		Numeric:    big.NewRat(1, 2),
+		BigNumeric: big.NewRat(3, 4),
+		Geography:  NullGeography{GeographyVal: testGeography, Valid: true},
+		Record:     &subNullable{X: NullInt64{Int64: 4, Valid: true}},
 	}
 	if diff := testutil.Diff(ts, want); diff != "" {
 		t.Error(diff)
@@ -1114,7 +1140,7 @@ func TestStructLoaderNullable(t *testing.T) {
 	// Struct pointers are reused, byte slices are not.
 	want = ts
 	want.Bytes = []byte{17}
-	vals2 := []Value{nil, []byte{17}, nil, nil, nil, nil, nil, nil, nil, nil, nil, []Value{int64(7)}}
+	vals2 := []Value{nil, []byte{17}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, []Value{int64(7)}}
 	mustLoad(t, &ts, testStructNullableSchema, vals2)
 	if ts.Record != want.Record {
 		t.Error("record pointers not identical")