Repository: cloudspannerecosystem/harbourbridge (go)

Issue:
Update postgres and mysql with NUMERIC
In spanner/v1.10.0, we have supported NUMERIC type. So we would like to update postgres and mysql with NUMERIC type. 

Pull request:
#87: feat: support NUMERIC (roll forward)
Fixes #87. Add support for Spanner NUMERIC to schema and data conversion (roll forward).

This was first submitted as PR98, but was rolled back because of client issues with big.Rat.

This PR consists of:
- a clean revert of cloudspannerecosystem/harbourbridge#102 (revert of #87)
- a fix for the client issue (numeric values now passed as a string via spanner.NumericString)
- integration test has been expanded to include numeric (it now includes all core types)
- updates github workflow to use recent version of emulator (to get numeric support)

Original PR description:
Fixes #87. Add support for Spanner NUMERIC to schema and data conversion.

Before this PR, HarbourBridge mapped PostgreSQL and MySQL NUMERIC/DECIMAL types to Spanner's FLOAT64 because Spanner did not originally support NUMERIC. However, Spanner has started supporting NUMERIC type from v1.10.0 (around Sept/2020). We've been planning to support it in HarbourBridge since then.

We'd be holding off on doing so while all of the work for user-guided schema migration was ongoing (aka webui work). We've now reached a point where most of the potentially conflicting changes from that work have landed. So it's time to push ahead with NUMERIC support.

Patch:
diff --git a/mysql/README.md b/mysql/README.md
index 0ec3f73..a422141 100644
--- a/mysql/README.md
+++ b/mysql/README.md
@@ -88,7 +88,7 @@ The HarbourBridge tool maps MySQL types to Spanner types as follows:
 | `CHAR(N)`                                         | `STRING(N)`     | c                               |
 | `DATE`                                            | `DATE`          |                                 |
 | `DATETIME`                                        | `TIMESTAMP`     | t                               |
-| `DECIMAL`, `NUMERIC`                              | `FLOAT64`       | p                               |
+| `DECIMAL`, `NUMERIC`                              | `NUMERIC`       | p                               |
 | `DOUBLE`                                          | `FLOAT64`       |                                 |
 | `ENUM`                                            | `STRING(MAX)`   |                                 |
 | `FLOAT`                                           | `FLOAT64`       | s                               |
@@ -100,21 +100,23 @@ The HarbourBridge tool maps MySQL types to Spanner types as follows:
 | `VARCHAR`                                         | `STRING(MAX)`   |                                 |
 | `VARCHAR(N)`                                      | `STRING(N)`     | c                               |
 
-Spanner does not support `spatial` datatypes of MySQL. Along with `spatial` datatypes,
-all other types map to `STRING(MAX)`. Some of the mappings in this table
-represent loss of precision (marked p), differences in treatment of timezones (marked t), differences in
-treatment of fixed-length character types (marked c), and changes in storage
-size (marked s). We discuss these, as well as other limits and notes on
-schema conversion, in the following sections.
+Spanner does not support `spatial` datatypes of MySQL. Along with `spatial`
+datatypes, all other types map to `STRING(MAX)`. Some of the mappings in this
+table represent potential changes of precision (marked p), differences in
+treatment of timezones (marked t), differences in treatment of fixed-length
+character types (marked c), and changes in storage size (marked s). We discuss
+these, as well as other limits and notes on schema conversion, in the following
+sections.
 
 ### `DECIMAL` and `NUMERIC`
 
-Spanner does not support decimal types, so these are mapped to `FLOAT64`. For
-some decimal types (e.g. `DECIMAL(7, 3))` this mapping will preserve precision.
-But for others, the decimal type does not fit in `FLOAT64`; HarbourBridge
-generates a warning in such cases. In general, mapping `DECIMAL` to `FLOAT64`
-can be useful for evaluation purposes, but it is not recommended for production
-use. Note that in MySQL, NUMERIC is implemented as DECIMAL, so the remarks about DECIMAL apply equally to NUMERIC.
+[Spanner's NUMERIC
+type](https://cloud.google.com/spanner/docs/data-types#decimal_type) can store
+up to 29 digits before the decimal point and up to 9 after the decimal point.
+MySQL's NUMERIC type can potentially support higher precision than this, so
+please verify that Spanner's NUMERIC support meets your application needs.  Note
+that in MySQL, NUMERIC is implemented as DECIMAL, so the remarks about DECIMAL
+apply equally to NUMERIC.
 
 ### `TIMESTAMP` and `DATETIME`
 
diff --git a/mysql/data.go b/mysql/data.go
index 28b4793..c051fa8 100644
--- a/mysql/data.go
+++ b/mysql/data.go
@@ -16,6 +16,7 @@ package mysql
 
 import (
 	"fmt"
+	"math/big"
 	"math/bits"
 	"strconv"
 	"strings"
@@ -114,6 +115,8 @@ func convScalar(spannerType ddl.Type, srcTypeName string, TimezoneOffset string,
 		return convFloat64(val)
 	case ddl.Int64:
 		return convInt64(val)
+	case ddl.Numeric:
+		return convNumeric(val)
 	case ddl.String:
 		return val, nil
 	case ddl.Timestamp:
@@ -161,6 +164,19 @@ func convInt64(val string) (int64, error) {
 	return i, err
 }
 
+// convNumeric maps a source database string value (representing a numeric)
+// into a string representing a valid Spanner numeric.
+// Ideally we would just return a *big.Rat, but spanner.Mutation
+// doesn't currently support use of *big.Rat.
+// TODO: return *big.Rat when client library supports it.
+func convNumeric(val string) (string, error) {
+	r := new(big.Rat)
+	if _, ok := r.SetString(val); !ok {
+		return "", fmt.Errorf("can't convert %q to big.Rat", val)
+	}
+	return spanner.NumericString(r), nil
+}
+
 // convTimestamp maps a source DB timestamp into a go Time Spanner timestamp
 // It handles both datetime and timestamp conversions.
 func convTimestamp(srcTypeName string, TimezoneOffset string, val string) (t time.Time, err error) {
diff --git a/mysql/toddl.go b/mysql/toddl.go
index de9cd8d..05c5b77 100644
--- a/mysql/toddl.go
+++ b/mysql/toddl.go
@@ -116,15 +116,16 @@ func toSpannerType(conv *internal.Conv, id string, mods []int64) (ddl.Type, []in
 	case "float":
 		maxExpectedMods(2)
 		return ddl.Type{Name: ddl.Float64}, []internal.SchemaIssue{internal.Widened}
-	case "numeric", "decimal": // Map all numeric and decimal types to float64.
+	case "numeric", "decimal":
 		maxExpectedMods(2)
-		if len(mods) > 0 && mods[0] <= 15 {
-			// float64 can represent this numeric type faithfully.
-			// Note: int64 has 53 bits for mantissa, which is ~15.96
-			// decimal digits.
-			return ddl.Type{Name: ddl.Float64}, []internal.SchemaIssue{internal.DecimalThatFits}
-		}
-		return ddl.Type{Name: ddl.Float64}, []internal.SchemaIssue{internal.Decimal}
+		// MySQL's NUMERIC type can store up to 65 digits, with up to 30 after the
+		// the decimal point. Spanner's NUMERIC type can store up to 29 digits before the
+		// decimal point and up to 9 after the decimal point -- it is equivalent to
+		// MySQL's NUMERIC(38,9) type.
+		//
+		// TODO: Generate appropriate SchemaIssue to warn of different precision
+		// capabilities between MySQL and Spanner NUMERIC.
+		return ddl.Type{Name: ddl.Numeric}, nil
 	case "bigint":
 		maxExpectedMods(1)
 		return ddl.Type{Name: ddl.Int64}, nil
diff --git a/postgres/README.md b/postgres/README.md
index f99b61c..36aeed5 100644
--- a/postgres/README.md
+++ b/postgres/README.md
@@ -88,7 +88,7 @@ The HarbourBridge tool maps PostgreSQL types to Spanner types as follows:
 | `DATE`             | `DATE`                 |                                           |
 | `DOUBLE PRECISION` | `FLOAT64`              |                                           |
 | `INTEGER`          | `INT64`                | s                                         |
-| `NUMERIC`          | `FLOAT64`              | p                                         |
+| `NUMERIC`          | `NUMERIC`              | p                                         |
 | `REAL`             | `FLOAT64`              | s                                         |
 | `SERIAL`           | `INT64`                | a, s                                      |
 | `SMALLINT`         | `INT64`                | s                                         |
@@ -100,20 +100,19 @@ The HarbourBridge tool maps PostgreSQL types to Spanner types as follows:
 | `ARRAY(`pgtype`)`  | `ARRAY(`spannertype`)` | if scalar type pgtype maps to spannertype |
 
 All other types map to `STRING(MAX)`. Some of the mappings in this table
-represent loss of precision (marked p), dropped autoincrement functionality
-(marked a), differences in treatment of timezones (marked t), differences in
-treatment of fixed-length character types (marked c), and changes in storage
-size (marked s). We discuss these, as well as other limits and notes on
-schema conversion, in the following sections.
+represent potential changes of precision (marked p), dropped autoincrement
+functionality (marked a), differences in treatment of timezones (marked t),
+differences in treatment of fixed-length character types (marked c), and changes
+in storage size (marked s). We discuss these, as well as other limits and notes
+on schema conversion, in the following sections.
 
 ### `NUMERIC`
 
-Spanner does not support numeric types, so these are mapped to `FLOAT64`. For
-some numeric types (e.g. `NUMERIC(7, 3))` this mapping will preserve precision.
-But for others, the numeric type does not fit in `FLOAT64`; HarbourBridge
-generates a warning in such cases. In general, mapping `NUMERIC` to `FLOAT64`
-can be useful for evaluation purposes, but it is not recommended for production
-use.
+[Spanner's NUMERIC
+type](https://cloud.google.com/spanner/docs/data-types#decimal_type) can store
+up to 29 digits before the decimal point and up to 9 after the decimal point.
+PostgreSQL's NUMERIC type can potentially support higher precision that this, so
+please verify that Spanner's NUMERIC support meets your application needs.
 
 ### `BIGSERIAL` and `SERIAL`
 
diff --git a/postgres/data.go b/postgres/data.go
index 6a8083d..3f7ce60 100644
--- a/postgres/data.go
+++ b/postgres/data.go
@@ -17,6 +17,7 @@ package postgres
 import (
 	"encoding/hex"
 	"fmt"
+	"math/big"
 	"math/bits"
 	"reflect"
 	"strconv"
@@ -125,6 +126,8 @@ func convScalar(spannerType ddl.Type, srcTypeName string, location *time.Locatio
 		return convFloat64(val)
 	case ddl.Int64:
 		return convInt64(val)
+	case ddl.Numeric:
+		return convNumeric(val)
 	case ddl.String:
 		return val, nil
 	case ddl.Timestamp:
@@ -177,6 +180,19 @@ func convInt64(val string) (int64, error) {
 	return i, err
 }
 
+// convNumeric maps a source database string value (representing a numeric)
+// into a string representing a valid Spanner numeric.
+// Ideally we would just return a *big.Rat, but spanner.Mutation
+// doesn't currently support use of *big.Rat.
+// TODO: return *big.Rat when client library supports it.
+func convNumeric(val string) (string, error) {
+	r := new(big.Rat)
+	if _, ok := r.SetString(val); !ok {
+		return "", fmt.Errorf("can't convert %q to big.Rat", val)
+	}
+	return spanner.NumericString(r), nil
+}
+
 // convTimestamp maps a source DB timestamp into a go Time (which
 // is translated to a Spanner timestamp by the go Spanner client library).
 // It handles both timestamptz and timestamp conversions.
diff --git a/postgres/toddl.go b/postgres/toddl.go
index 66cb11f..65960a7 100644
--- a/postgres/toddl.go
+++ b/postgres/toddl.go
@@ -87,6 +87,9 @@ func schemaToDDL(conv *internal.Conv) error {
 // mapping.  toSpannerType returns the Spanner type and a list of type
 // conversion issues encountered.
 func toSpannerType(conv *internal.Conv, id string, mods []int64) (ddl.Type, []internal.SchemaIssue) {
+	// TODO: Remove use of maxExpectedMods. It was added as a sanity check for the
+	// initial version of HarbourBridge. It has now out-lived its usefulness: it isn't
+	// uncovering issues (and it's unlikely to) and it's cluttering code.
 	maxExpectedMods := func(n int) {
 		if len(mods) > n {
 			conv.Unexpected(fmt.Sprintf("Found %d mods while processing type id=%s", len(mods), id))
@@ -127,15 +130,21 @@ func toSpannerType(conv *internal.Conv, id string, mods []int64) (ddl.Type, []in
 	case "int2", "smallint":
 		maxExpectedMods(0)
 		return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
-	case "numeric": // Map all numeric types to float64.
+	case "numeric":
 		maxExpectedMods(2)
-		if len(mods) > 0 && mods[0] <= 15 {
-			// float64 can represent this numeric type faithfully.
-			// Note: int64 has 53 bits for mantissa, which is ~15.96
-			// decimal digits.
-			return ddl.Type{Name: ddl.Float64}, []internal.SchemaIssue{internal.NumericThatFits}
-		}
-		return ddl.Type{Name: ddl.Float64}, []internal.SchemaIssue{internal.Numeric}
+		// PostgreSQL's NUMERIC type can have a specified precision of up to 1000
+		// digits (and scale can be anything from 0 up to the value of 'precision').
+		// If precision and scale are not specified, then values of any precision
+		// or scale can be stored, up to the implementation's limits (can be up to
+		// 131072 digits before the decimal point and up to 16383 digits after
+		// the decimal point).
+		// Spanner's NUMERIC type can store up to 29 digits before the
+		// decimal point and up to 9 after the decimal point -- it is
+		// equivalent to PostgreSQL's NUMERIC(38,9) type.
+		//
+		// TODO: Generate appropriate SchemaIssue to warn of different precision
+		// capabilities between PostgreSQL and Spanner NUMERIC.
+		return ddl.Type{Name: ddl.Numeric}, nil
 	case "serial":
 		maxExpectedMods(0)
 		return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Serial}


Test patch:
diff --git a/.github/workflows/integration-tests-against-emulator.yaml b/.github/workflows/integration-tests-against-emulator.yaml
index 99af4b4..67e71d9 100644
--- a/.github/workflows/integration-tests-against-emulator.yaml
+++ b/.github/workflows/integration-tests-against-emulator.yaml
@@ -17,10 +17,14 @@ jobs:
 
     services:
       emulator:
-        image: gcr.io/cloud-spanner-emulator/emulator:0.8.0
+        image: gcr.io/cloud-spanner-emulator/emulator:1.1.1
         ports:
           - 9010:9010
           - 9020:9020
+        # needed because numeric support isn't on by default in emulator:1.1.1
+        # note: future versions will default enable this and deprecate the flag
+        # TODO: upgrade emuator version, confirm enable_numeric_type is no longer needed and delete it
+        args: ["-enable_numeric_type"]
       postgres:
         image: postgres:9.6
         env:
diff --git a/integration_test.go b/integration_test.go
index 8edb3fc..3caecc5 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"log"
+	"math/big"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -238,7 +239,7 @@ func checkResults(t *testing.T, dbPath string) {
 
 	checkBigInt(ctx, t, client)
 	checkTimestamps(ctx, t, client)
-	checkDateBytesBool(ctx, t, client)
+	checkCoreTypes(ctx, t, client)
 	checkArrays(ctx, t, client)
 }
 
@@ -287,11 +288,15 @@ func checkTimestamps(ctx context.Context, t *testing.T, client *spanner.Client)
 	}
 }
 
-func checkDateBytesBool(ctx context.Context, t *testing.T, client *spanner.Client) {
-	var date spanner.NullDate
-	var bytesVal []byte
+func checkCoreTypes(ctx context.Context, t *testing.T, client *spanner.Client) {
 	var boolVal bool
-	iter := client.Single().Read(ctx, "test2", spanner.Key{1}, []string{"a", "b", "c"})
+	var bytesVal []byte
+	var date spanner.NullDate
+	var floatVal float64
+	var intVal int64
+	var numericVal big.Rat
+	var stringVal string
+	iter := client.Single().Read(ctx, "test2", spanner.Key{1}, []string{"a", "b", "c", "d", "e", "f", "g"})
 	defer iter.Stop()
 	for {
 		row, err := iter.Next()
@@ -301,18 +306,30 @@ func checkDateBytesBool(ctx context.Context, t *testing.T, client *spanner.Clien
 		if err != nil {
 			t.Fatal(err)
 		}
-		if err := row.Columns(&date, &bytesVal, &boolVal); err != nil {
+		if err := row.Columns(&boolVal, &bytesVal, &date, &floatVal, &intVal, &numericVal, &stringVal); err != nil {
 			t.Fatal(err)
 		}
 	}
-	if got, want := date.String(), "2019-10-28"; got != want {
-		t.Fatalf("date is not correct: got %v, want %v", got, want)
+	if got, want := boolVal, true; got != want {
+		t.Fatalf("bool value is not correct: got %v, want %v", got, want)
 	}
 	if got, want := string(bytesVal), "\x00\x01\x02\x03ޭ\xbe\xef"; got != want {
 		t.Fatalf("bytes are not correct: got %v, want %v", got, want)
 	}
-	if got, want := boolVal, true; got != want {
-		t.Fatalf("bool value is not correct: got %v, want %v", got, want)
+	if got, want := date.String(), "2019-10-28"; got != want {
+		t.Fatalf("date is not correct: got %v, want %v", got, want)
+	}
+	if got, want := floatVal, 99.9; got != want {
+		t.Fatalf("float value is not correct: got %v, want %v", got, want)
+	}
+	if got, want := intVal, int64(42); got != want {
+		t.Fatalf("int value is not correct: got %v, want %v", got, want)
+	}
+	if got, want := spanner.NumericString(&numericVal), "1234567890123456789012345678.123456789"; got != want {
+		t.Fatalf("numeric value is not correct: got %v, want %v", got, want)
+	}
+	if got, want := stringVal, "hi"; got != want {
+		t.Fatalf("string value is not correct: got %v, want %v", got, want)
 	}
 }
 
diff --git a/mysql/infoschema_test.go b/mysql/infoschema_test.go
index f01e5db..87ae624 100644
--- a/mysql/infoschema_test.go
+++ b/mysql/infoschema_test.go
@@ -165,7 +165,7 @@ func TestProcessInfoSchemaMYSQL(t *testing.T) {
 				"c":   ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: int64(1)}},
 				"c8":  ddl.ColumnDef{Name: "c8", T: ddl.Type{Name: ddl.String, Len: int64(8)}},
 				"d":   ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.Date}},
-				"dec": ddl.ColumnDef{Name: "dec", T: ddl.Type{Name: ddl.Float64}},
+				"dec": ddl.ColumnDef{Name: "dec", T: ddl.Type{Name: ddl.Numeric}},
 				"f8":  ddl.ColumnDef{Name: "f8", T: ddl.Type{Name: ddl.Float64}},
 				"f4":  ddl.ColumnDef{Name: "f4", T: ddl.Type{Name: ddl.Float64}},
 				"i8":  ddl.ColumnDef{Name: "i8", T: ddl.Type{Name: ddl.Int64}},
@@ -183,13 +183,12 @@ func TestProcessInfoSchemaMYSQL(t *testing.T) {
 	assert.Equal(t, expectedSchema, stripSchemaComments(conv.SpSchema))
 	assert.Equal(t, len(conv.Issues["cart"]), 0)
 	expectedIssues := map[string][]internal.SchemaIssue{
-		"bs":  []internal.SchemaIssue{internal.DefaultValue},
-		"dec": []internal.SchemaIssue{internal.Decimal},
-		"f4":  []internal.SchemaIssue{internal.Widened},
-		"i4":  []internal.SchemaIssue{internal.Widened, internal.AutoIncrement},
-		"i2":  []internal.SchemaIssue{internal.Widened},
-		"si":  []internal.SchemaIssue{internal.Widened, internal.DefaultValue},
-		"ts":  []internal.SchemaIssue{internal.Datetime},
+		"bs": []internal.SchemaIssue{internal.DefaultValue},
+		"f4": []internal.SchemaIssue{internal.Widened},
+		"i4": []internal.SchemaIssue{internal.Widened, internal.AutoIncrement},
+		"i2": []internal.SchemaIssue{internal.Widened},
+		"si": []internal.SchemaIssue{internal.Widened, internal.DefaultValue},
+		"ts": []internal.SchemaIssue{internal.Datetime},
 	}
 	assert.Equal(t, expectedIssues, conv.Issues["test"])
 	assert.Equal(t, int64(0), conv.Unexpecteds())
diff --git a/mysql/mysqldump_test.go b/mysql/mysqldump_test.go
index 837416c..3ca5d29 100644
--- a/mysql/mysqldump_test.go
+++ b/mysql/mysqldump_test.go
@@ -42,7 +42,7 @@ func TestProcessMySQLDump_Scalar(t *testing.T) {
 		{"blob", ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
 		{"char(42)", ddl.Type{Name: ddl.String, Len: int64(42)}},
 		{"date", ddl.Type{Name: ddl.Date}},
-		{"decimal(4,10)", ddl.Type{Name: ddl.Float64}},
+		{"decimal(4,10)", ddl.Type{Name: ddl.Numeric}},
 		{"double(4,10)", ddl.Type{Name: ddl.Float64}},
 		{"float(4,10)", ddl.Type{Name: ddl.Float64}},
 		{"integer", ddl.Type{Name: ddl.Int64}},
@@ -557,7 +557,7 @@ CREATE TABLE test (a text PRIMARY KEY, b text);`,
 	INSERT INTO test (id, a, b, c) VALUES (1,'2019-10-29',4.444,5.44444);
 	`,
 			expectedData: []spannerData{
-				spannerData{table: "test", cols: []string{"id", "a", "b", "c"}, vals: []interface{}{int64(1), getDate("2019-10-29"), float64(4.444), float64(5.44444)}}},
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c"}, vals: []interface{}{int64(1), getDate("2019-10-29"), float64(4.444), "5.444440000"}}},
 		},
 		{
 			name: "Data conversion: smallint, mediumint, bigint, double",
diff --git a/mysql/toddl_test.go b/mysql/toddl_test.go
index 242a6fa..6579b17 100644
--- a/mysql/toddl_test.go
+++ b/mysql/toddl_test.go
@@ -36,7 +36,7 @@ func TestToSpannerType(t *testing.T) {
 			"b": schema.Column{Name: "b", Type: schema.Type{Name: "float"}},
 			"c": schema.Column{Name: "c", Type: schema.Type{Name: "tinyint", Mods: []int64{1}}},
 			"d": schema.Column{Name: "d", Type: schema.Type{Name: "varchar", Mods: []int64{6}}},
-			"e": schema.Column{Name: "e", Type: schema.Type{Name: "double"}},
+			"e": schema.Column{Name: "e", Type: schema.Type{Name: "numeric"}},
 			"f": schema.Column{Name: "f", Type: schema.Type{Name: "timestamp"}},
 		},
 		PrimaryKeys: []schema.Key{schema.Key{Column: "a"}},
@@ -54,7 +54,7 @@ func TestToSpannerType(t *testing.T) {
 			"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Float64}},
 			"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Bool}},
 			"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.String, Len: int64(6)}},
-			"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Float64}},
+			"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Numeric}},
 			"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.Timestamp}},
 		},
 		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
diff --git a/postgres/infoschema_test.go b/postgres/infoschema_test.go
index bca0465..4155b76 100644
--- a/postgres/infoschema_test.go
+++ b/postgres/infoschema_test.go
@@ -173,7 +173,7 @@ func TestProcessInfoSchema(t *testing.T) {
 				"i8":    ddl.ColumnDef{Name: "i8", T: ddl.Type{Name: ddl.Int64}},
 				"i4":    ddl.ColumnDef{Name: "i4", T: ddl.Type{Name: ddl.Int64}},
 				"i2":    ddl.ColumnDef{Name: "i2", T: ddl.Type{Name: ddl.Int64}},
-				"num":   ddl.ColumnDef{Name: "num", T: ddl.Type{Name: ddl.Float64}},
+				"num":   ddl.ColumnDef{Name: "num", T: ddl.Type{Name: ddl.Numeric}},
 				"s":     ddl.ColumnDef{Name: "s", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
 				"ts":    ddl.ColumnDef{Name: "ts", T: ddl.Type{Name: ddl.Timestamp}},
 				"tz":    ddl.ColumnDef{Name: "tz", T: ddl.Type{Name: ddl.Timestamp}},
@@ -193,7 +193,6 @@ func TestProcessInfoSchema(t *testing.T) {
 		"f4":   []internal.SchemaIssue{internal.Widened},
 		"i4":   []internal.SchemaIssue{internal.Widened},
 		"i2":   []internal.SchemaIssue{internal.Widened},
-		"num":  []internal.SchemaIssue{internal.Numeric},
 		"s":    []internal.SchemaIssue{internal.Widened, internal.DefaultValue},
 		"ts":   []internal.SchemaIssue{internal.Timestamp},
 	}
diff --git a/postgres/pgdump_test.go b/postgres/pgdump_test.go
index aac3bac..fee988f 100644
--- a/postgres/pgdump_test.go
+++ b/postgres/pgdump_test.go
@@ -48,14 +48,14 @@ func TestProcessPgDump(t *testing.T) {
 		{"bytea", ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
 		{"char(42)", ddl.Type{Name: ddl.String, Len: int64(42)}},
 		{"date", ddl.Type{Name: ddl.Date}},
-		{"decimal", ddl.Type{Name: ddl.Float64}}, // pg parser maps this to numeric.
+		{"decimal", ddl.Type{Name: ddl.Numeric}}, // pg parser maps this to numeric.
 		{"double precision", ddl.Type{Name: ddl.Float64}},
 		{"float8", ddl.Type{Name: ddl.Float64}},
 		{"float4", ddl.Type{Name: ddl.Float64}},
 		{"integer", ddl.Type{Name: ddl.Int64}},
-		{"numeric", ddl.Type{Name: ddl.Float64}},
-		{"numeric(4)", ddl.Type{Name: ddl.Float64}},
-		{"numeric(6, 4)", ddl.Type{Name: ddl.Float64}},
+		{"numeric", ddl.Type{Name: ddl.Numeric}},
+		{"numeric(4)", ddl.Type{Name: ddl.Numeric}},
+		{"numeric(6, 4)", ddl.Type{Name: ddl.Numeric}},
 		{"real", ddl.Type{Name: ddl.Float64}},
 		{"smallint", ddl.Type{Name: ddl.Int64}},
 		{"text", ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
@@ -504,7 +504,7 @@ COPY test (id, a, b, c, d) FROM stdin;
 \.
 `,
 			expectedData: []spannerData{
-				spannerData{table: "test", cols: []string{"id", "a", "b", "c", "d"}, vals: []interface{}{int64(1), int64(88), int64(44), int64(22), float64(444.9876)}}},
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c", "d"}, vals: []interface{}{int64(1), int64(88), int64(44), int64(22), "444.987600000"}}},
 		},
 		{
 			name: "Data conversion: serial, text, timestamp, timestamptz, varchar",
diff --git a/postgres/report_test.go b/postgres/report_test.go
index 8624389..8409986 100644
--- a/postgres/report_test.go
+++ b/postgres/report_test.go
@@ -100,12 +100,9 @@ Data conversion: OK (94% of 1000 rows written to Spanner).
 Warnings
 1) Column 'synth_id' was added because this table didn't have a primary key.
    Spanner requires a primary key for every table.
-2) Column 'a': type numeric is mapped to float64. Spanner does not support
-   numeric. This type mapping could lose precision and is not recommended for
-   production use.
-3) Column 'c': type int4[4][2] is mapped to string(max). Spanner doesn't support
+2) Column 'c': type int4[4][2] is mapped to string(max). Spanner doesn't support
    multi-dimensional arrays.
-4) Column 'd': type circle is mapped to string(max). No appropriate Spanner
+3) Column 'd': type circle is mapped to string(max). No appropriate Spanner
    type.
 
 Note
diff --git a/postgres/toddl_test.go b/postgres/toddl_test.go
index 253690a..34664f3 100644
--- a/postgres/toddl_test.go
+++ b/postgres/toddl_test.go
@@ -56,7 +56,7 @@ func TestToSpannerType(t *testing.T) {
 			"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Float64}},
 			"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Bool}},
 			"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.String, Len: int64(6)}},
-			"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Float64}},
+			"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Numeric}},
 			"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.Timestamp}},
 		},
 		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
@@ -65,7 +65,6 @@ func TestToSpannerType(t *testing.T) {
 	assert.Equal(t, expected, actual)
 	expectedIssues := map[string][]internal.SchemaIssue{
 		"b": []internal.SchemaIssue{internal.Widened},
-		"e": []internal.SchemaIssue{internal.Numeric},
 	}
 	assert.Equal(t, expectedIssues, conv.Issues[name])
 }
diff --git a/test_data/pg_dump.test.out b/test_data/pg_dump.test.out
index 11225f6..bb81a92 100644
--- a/test_data/pg_dump.test.out
+++ b/test_data/pg_dump.test.out
@@ -50,9 +50,13 @@ ALTER TABLE public.test OWNER TO postgres;
 
 CREATE TABLE public.test2 (
     id integer NOT NULL,
-    a date,
+    a boolean,
     b bytea,
-    c boolean
+    c date,
+    d double precision,
+    e integer,
+    f numeric(38, 9),
+    g text
 );
 
 
@@ -98,9 +102,9 @@ COPY public.test (id, t, tz) FROM stdin;
 -- Data for Name: test2; Type: TABLE DATA; Schema: public; Owner: postgres
 --
 
-COPY public.test2 (id, a, b, c) FROM stdin;
-1	2019-10-28	\\x00010203deadbeef	t
-2	2018-11-28	\\x00010203424344	f
+COPY public.test2 (id, a, b, c, d, e, f, g) FROM stdin;
+1	t	\\x00010203deadbeef	2019-10-28	99.9	42	1234567890123456789012345678.123456789	hi
+2	f	\\x00010203424344	2018-11-28	12.3	6	1234567890123456789012345.1223456	hello
 \.