Repository: cloudspannerecosystem/harbourbridge
Pull request #102 (2020-12-29 04:16:09)

Revert "#87: feat: support NUMERIC"
Reverts cloudspannerecosystem/harbourbridge#98

HarbourBridge is using an older version of the Cloud Spanner client library -- in particular, a version that doesn't support NUMERIC. This means that after #98, any source tables with NUMERIC will be converted to Spanner tables with NUMERIC, and then have all of their data rejected.

Reverting for the moment while I resolve the client version issue.


Patch:
diff --git a/mysql/README.md b/mysql/README.md
index a422141..0ec3f73 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`                              | `NUMERIC`       | p                               |
+| `DECIMAL`, `NUMERIC`                              | `FLOAT64`       | p                               |
 | `DOUBLE`                                          | `FLOAT64`       |                                 |
 | `ENUM`                                            | `STRING(MAX)`   |                                 |
 | `FLOAT`                                           | `FLOAT64`       | s                               |
@@ -100,23 +100,21 @@ 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 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.
+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.
 
 ### `DECIMAL` and `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.
+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.
 
 ### `TIMESTAMP` and `DATETIME`
 
diff --git a/mysql/data.go b/mysql/data.go
index bce28a9..28b4793 100644
--- a/mysql/data.go
+++ b/mysql/data.go
@@ -16,7 +16,6 @@ package mysql
 
 import (
 	"fmt"
-	"math/big"
 	"math/bits"
 	"strconv"
 	"strings"
@@ -115,8 +114,6 @@ 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:
@@ -164,14 +161,6 @@ func convInt64(val string) (int64, error) {
 	return i, err
 }
 
-func convNumeric(val string) (*big.Rat, error) {
-	r := new(big.Rat)
-	if _, ok := r.SetString(val); !ok {
-		return r, fmt.Errorf("can't convert %q to big.Rat", val)
-	}
-	return 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 05c5b77..de9cd8d 100644
--- a/mysql/toddl.go
+++ b/mysql/toddl.go
@@ -116,16 +116,15 @@ 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":
+	case "numeric", "decimal": // Map all numeric and decimal types to float64.
 		maxExpectedMods(2)
-		// 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
+		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}
 	case "bigint":
 		maxExpectedMods(1)
 		return ddl.Type{Name: ddl.Int64}, nil
diff --git a/postgres/README.md b/postgres/README.md
index 36aeed5..f99b61c 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`          | `NUMERIC`              | p                                         |
+| `NUMERIC`          | `FLOAT64`              | p                                         |
 | `REAL`             | `FLOAT64`              | s                                         |
 | `SERIAL`           | `INT64`                | a, s                                      |
 | `SMALLINT`         | `INT64`                | s                                         |
@@ -100,19 +100,20 @@ 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 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.
+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.
 
 ### `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.
-PostgreSQL's NUMERIC type can potentially support higher precision that this, so
-please verify that Spanner's NUMERIC support meets your application needs.
+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.
 
 ### `BIGSERIAL` and `SERIAL`
 
diff --git a/postgres/data.go b/postgres/data.go
index e18556c..6a8083d 100644
--- a/postgres/data.go
+++ b/postgres/data.go
@@ -17,7 +17,6 @@ package postgres
 import (
 	"encoding/hex"
 	"fmt"
-	"math/big"
 	"math/bits"
 	"reflect"
 	"strconv"
@@ -126,8 +125,6 @@ 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:
@@ -180,14 +177,6 @@ func convInt64(val string) (int64, error) {
 	return i, err
 }
 
-func convNumeric(val string) (*big.Rat, error) {
-	r := new(big.Rat)
-	if _, ok := r.SetString(val); !ok {
-		return r, fmt.Errorf("can't convert %q to big.Rat", val)
-	}
-	return 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 65960a7..66cb11f 100644
--- a/postgres/toddl.go
+++ b/postgres/toddl.go
@@ -87,9 +87,6 @@ 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))
@@ -130,21 +127,15 @@ 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":
+	case "numeric": // Map all numeric types to float64.
 		maxExpectedMods(2)
-		// 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
+		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}
 	case "serial":
 		maxExpectedMods(0)
 		return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Serial}


Test patch:
diff --git a/mysql/infoschema_test.go b/mysql/infoschema_test.go
index 87ae624..f01e5db 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.Numeric}},
+				"dec": ddl.ColumnDef{Name: "dec", T: ddl.Type{Name: ddl.Float64}},
 				"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,12 +183,13 @@ 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},
-		"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},
+		"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},
 	}
 	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 bd8502f..837416c 100644
--- a/mysql/mysqldump_test.go
+++ b/mysql/mysqldump_test.go
@@ -17,7 +17,6 @@ package mysql
 import (
 	"bufio"
 	"fmt"
-	"math/big"
 	"math/bits"
 	"strings"
 	"testing"
@@ -43,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.Numeric}},
+		{"decimal(4,10)", ddl.Type{Name: ddl.Float64}},
 		{"double(4,10)", ddl.Type{Name: ddl.Float64}},
 		{"float(4,10)", ddl.Type{Name: ddl.Float64}},
 		{"integer", ddl.Type{Name: ddl.Int64}},
@@ -558,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), mkRat(t, "5.44444")}}},
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c"}, vals: []interface{}{int64(1), getDate("2019-10-29"), float64(4.444), float64(5.44444)}}},
 		},
 		{
 			name: "Data conversion: smallint, mediumint, bigint, double",
@@ -782,10 +781,3 @@ func normalizeSpace(s string) string {
 func bitReverse(i int64) int64 {
 	return int64(bits.Reverse64(uint64(i)))
 }
-
-func mkRat(t *testing.T, v string) *big.Rat {
-	r := new(big.Rat)
-	_, ok := r.SetString(v)
-	assert.True(t, ok, fmt.Sprintf("Can't convert %q to big.Rat", v))
-	return r
-}
diff --git a/mysql/toddl_test.go b/mysql/toddl_test.go
index 6579b17..242a6fa 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: "numeric"}},
+			"e": schema.Column{Name: "e", Type: schema.Type{Name: "double"}},
 			"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.Numeric}},
+			"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Float64}},
 			"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 4155b76..bca0465 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.Numeric}},
+				"num":   ddl.ColumnDef{Name: "num", T: ddl.Type{Name: ddl.Float64}},
 				"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,6 +193,7 @@ 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 e636678..aac3bac 100644
--- a/postgres/pgdump_test.go
+++ b/postgres/pgdump_test.go
@@ -17,7 +17,6 @@ package postgres
 import (
 	"bufio"
 	"fmt"
-	"math/big"
 	"math/bits"
 	"strings"
 	"testing"
@@ -49,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.Numeric}}, // pg parser maps this to numeric.
+		{"decimal", ddl.Type{Name: ddl.Float64}}, // 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.Numeric}},
-		{"numeric(4)", ddl.Type{Name: ddl.Numeric}},
-		{"numeric(6, 4)", ddl.Type{Name: ddl.Numeric}},
+		{"numeric", ddl.Type{Name: ddl.Float64}},
+		{"numeric(4)", ddl.Type{Name: ddl.Float64}},
+		{"numeric(6, 4)", ddl.Type{Name: ddl.Float64}},
 		{"real", ddl.Type{Name: ddl.Float64}},
 		{"smallint", ddl.Type{Name: ddl.Int64}},
 		{"text", ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
@@ -505,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), mkRat(t, "444.9876")}}},
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c", "d"}, vals: []interface{}{int64(1), int64(88), int64(44), int64(22), float64(444.9876)}}},
 		},
 		{
 			name: "Data conversion: serial, text, timestamp, timestamptz, varchar",
@@ -753,10 +752,3 @@ func printJSON(s string) {
 func printJSONType(ty string) {
 	printJSON(fmt.Sprintf("CREATE TABLE t (a %s);", ty))
 }
-
-func mkRat(t *testing.T, v string) *big.Rat {
-	r := new(big.Rat)
-	_, ok := r.SetString(v)
-	assert.True(t, ok, fmt.Sprintf("Can't convert %q to big.Rat", v))
-	return r
-}
diff --git a/postgres/report_test.go b/postgres/report_test.go
index 8409986..8624389 100644
--- a/postgres/report_test.go
+++ b/postgres/report_test.go
@@ -100,9 +100,12 @@ 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 'c': type int4[4][2] is mapped to string(max). Spanner doesn't support
+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
    multi-dimensional arrays.
-3) Column 'd': type circle is mapped to string(max). No appropriate Spanner
+4) 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 34664f3..253690a 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.Numeric}},
+			"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Float64}},
 			"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.Timestamp}},
 		},
 		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
@@ -65,6 +65,7 @@ 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])
 }