Repository: googleapis/java-spanner-jdbc
Pull request #185 (2020-07-27 20:52:38)

feat: add support for NUMERIC type
Adds support for the `NUMERIC` data type.

codecov[bot]: # [Codecov](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185?src=pr&el=h1) Report
> Merging [#185](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185?src=pr&el=desc) into [master](https://codecov.io/gh/googleapis/java-spanner-jdbc/commit/70f7c13b23571d167bcc6ff6671b9274b2131351?el=desc) will **decrease** coverage by `0.76%`.
> The diff coverage is `55.17%`.

[![Impacted file tree graph](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/graphs/tree.svg?width=650&height=150&src=pr&token=ODlczvbGrj)](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##             master     #185      +/-   ##
============================================
- Coverage     72.17%   71.40%   -0.77%     
- Complexity     1094     1104      +10     
============================================
  Files            24       24              
  Lines          3292     3396     +104     
  Branches        495      525      +30     
============================================
+ Hits           2376     2425      +49     
- Misses          723      756      +33     
- Partials        193      215      +22     
```


| [Impacted Files](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...ud/spanner/jdbc/AbstractJdbcPreparedStatement.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9BYnN0cmFjdEpkYmNQcmVwYXJlZFN0YXRlbWVudC5qYXZh) | `90.24% <0.00%> (-0.61%)` | `51.00 <0.00> (-1.00)` | |
| [...ogle/cloud/spanner/jdbc/JdbcParameterMetaData.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9KZGJjUGFyYW1ldGVyTWV0YURhdGEuamF2YQ==) | `15.27% <0.00%> (ø)` | `5.00 <0.00> (ø)` | |
| [...a/com/google/cloud/spanner/jdbc/JdbcResultSet.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9KZGJjUmVzdWx0U2V0LmphdmE=) | `74.24% <0.00%> (-4.57%)` | `186.00 <0.00> (ø)` | |
| [...ogle/cloud/spanner/jdbc/JdbcResultSetMetaData.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9KZGJjUmVzdWx0U2V0TWV0YURhdGEuamF2YQ==) | `78.68% <0.00%> (-7.76%)` | `41.00 <0.00> (ø)` | |
| [...google/cloud/spanner/jdbc/AbstractJdbcWrapper.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9BYnN0cmFjdEpkYmNXcmFwcGVyLmphdmE=) | `68.96% <30.76%> (-8.35%)` | `53.00 <2.00> (+2.00)` | :arrow_down: |
| [.../google/cloud/spanner/jdbc/JdbcParameterStore.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9KZGJjUGFyYW1ldGVyU3RvcmUuamF2YQ==) | `50.49% <45.45%> (+0.11%)` | `127.00 <0.00> (+1.00)` | |
| [...va/com/google/cloud/spanner/jdbc/JdbcDataType.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9KZGJjRGF0YVR5cGUuamF2YQ==) | `58.06% <50.00%> (-0.87%)` | `4.00 <0.00> (ø)` | |
| [...m/google/cloud/spanner/jdbc/JdbcTypeConverter.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9KZGJjVHlwZUNvbnZlcnRlci5qYXZh) | `72.19% <55.55%> (-1.61%)` | `99.00 <0.00> (+8.00)` | :arrow_down: |
| [...oogle/cloud/spanner/jdbc/JdbcDatabaseMetaData.java](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvamRiYy9KZGJjRGF0YWJhc2VNZXRhRGF0YS5qYXZh) | `95.47% <100.00%> (+0.24%)` | `180.00 <0.00> (ø)` | |
| ... and [1 more](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185?src=pr&el=footer). Last update [70f7c13...ecb6d6e](https://codecov.io/gh/googleapis/java-spanner-jdbc/pull/185?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

skuruppu: @olavloite the tests are now passing 🎉

If you could please take a look at the other failures, that would be awesome.
olavloite: @skuruppu Ah, cool. I'll look into the other failures and update the PR.

Patch:
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java
index 52ac32c..bbd5975 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java
@@ -128,7 +128,7 @@ abstract class AbstractJdbcPreparedStatement extends JdbcStatement implements Pr
   @Override
   public void setBigDecimal(int parameterIndex, BigDecimal value) throws SQLException {
     checkClosed();
-    parameters.setParameter(parameterIndex, value, Types.DECIMAL);
+    parameters.setParameter(parameterIndex, value, Types.NUMERIC);
   }
 
   @Override
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
index aef5638..6555668 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
@@ -19,6 +19,8 @@ package com.google.cloud.spanner.jdbc;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.Code;
 import com.google.common.base.Preconditions;
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.sql.Date;
 import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
@@ -44,6 +46,7 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     if (type.equals(Type.date())) return Types.DATE;
     if (type.equals(Type.float64())) return Types.DOUBLE;
     if (type.equals(Type.int64())) return Types.BIGINT;
+    if (type.equals(Type.numeric())) return Types.NUMERIC;
     if (type.equals(Type.string())) return Types.NVARCHAR;
     if (type.equals(Type.timestamp())) return Types.TIMESTAMP;
     if (type.getCode() == Code.ARRAY) return Types.ARRAY;
@@ -60,6 +63,8 @@ abstract class AbstractJdbcWrapper implements Wrapper {
         || sqlType == Types.INTEGER
         || sqlType == Types.SMALLINT
         || sqlType == Types.TINYINT) return Type.int64().getCode().name();
+    if (sqlType == Types.NUMERIC || sqlType == Types.DECIMAL)
+      return Type.numeric().getCode().name();
     if (sqlType == Types.NVARCHAR) return Type.string().getCode().name();
     if (sqlType == Types.TIMESTAMP) return Type.timestamp().getCode().name();
     if (sqlType == Types.ARRAY) return Code.ARRAY.name();
@@ -77,6 +82,7 @@ abstract class AbstractJdbcWrapper implements Wrapper {
         || sqlType == Types.INTEGER
         || sqlType == Types.SMALLINT
         || sqlType == Types.TINYINT) return Long.class.getName();
+    if (sqlType == Types.NUMERIC || sqlType == Types.DECIMAL) return BigDecimal.class.getName();
     if (sqlType == Types.NVARCHAR) return String.class.getName();
     if (sqlType == Types.TIMESTAMP) return Timestamp.class.getName();
     if (sqlType == Types.ARRAY) return Object.class.getName();
@@ -96,6 +102,7 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     if (type == Type.date()) return Date.class.getName();
     if (type == Type.float64()) return Double.class.getName();
     if (type == Type.int64()) return Long.class.getName();
+    if (type == Type.numeric()) return BigDecimal.class.getName();
     if (type == Type.string()) return String.class.getName();
     if (type == Type.timestamp()) return Timestamp.class.getName();
     if (type.getCode() == Code.ARRAY) {
@@ -104,6 +111,7 @@ abstract class AbstractJdbcWrapper implements Wrapper {
       if (type.getArrayElementType() == Type.date()) return Date[].class.getName();
       if (type.getArrayElementType() == Type.float64()) return Double[].class.getName();
       if (type.getArrayElementType() == Type.int64()) return Long[].class.getName();
+      if (type.getArrayElementType() == Type.numeric()) return BigDecimal[].class.getName();
       if (type.getArrayElementType() == Type.string()) return String[].class.getName();
       if (type.getArrayElementType() == Type.timestamp()) return Timestamp[].class.getName();
     }
@@ -122,6 +130,16 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     return (byte) val;
   }
 
+  /** Cast value and throw {@link SQLException} if out-of-range. */
+  static byte checkedCastToByte(BigDecimal val) throws SQLException {
+    try {
+      return val.byteValueExact();
+    } catch (ArithmeticException e) {
+      throw JdbcSqlExceptionFactory.of(
+          String.format(OUT_OF_RANGE_MSG, "byte", val), com.google.rpc.Code.OUT_OF_RANGE);
+    }
+  }
+
   /** Cast value and throw {@link SQLException} if out-of-range. */
   static short checkedCastToShort(long val) throws SQLException {
     if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
@@ -131,6 +149,16 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     return (short) val;
   }
 
+  /** Cast value and throw {@link SQLException} if out-of-range. */
+  static short checkedCastToShort(BigDecimal val) throws SQLException {
+    try {
+      return val.shortValueExact();
+    } catch (ArithmeticException e) {
+      throw JdbcSqlExceptionFactory.of(
+          String.format(OUT_OF_RANGE_MSG, "short", val), com.google.rpc.Code.OUT_OF_RANGE);
+    }
+  }
+
   /** Cast value and throw {@link SQLException} if out-of-range. */
   static int checkedCastToInt(long val) throws SQLException {
     if (val > Integer.MAX_VALUE || val < Integer.MIN_VALUE) {
@@ -140,6 +168,16 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     return (int) val;
   }
 
+  /** Cast value and throw {@link SQLException} if out-of-range. */
+  static int checkedCastToInt(BigDecimal val) throws SQLException {
+    try {
+      return val.intValueExact();
+    } catch (ArithmeticException e) {
+      throw JdbcSqlExceptionFactory.of(
+          String.format(OUT_OF_RANGE_MSG, "int", val), com.google.rpc.Code.OUT_OF_RANGE);
+    }
+  }
+
   /** Cast value and throw {@link SQLException} if out-of-range. */
   static float checkedCastToFloat(double val) throws SQLException {
     if (val > Float.MAX_VALUE || val < -Float.MAX_VALUE) {
@@ -163,6 +201,26 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     }
   }
 
+  /** Cast value and throw {@link SQLException} if out-of-range. */
+  static BigInteger checkedCastToBigInteger(BigDecimal val) throws SQLException {
+    try {
+      return val.toBigIntegerExact();
+    } catch (ArithmeticException e) {
+      throw JdbcSqlExceptionFactory.of(
+          String.format(OUT_OF_RANGE_MSG, "BigInteger", val), com.google.rpc.Code.OUT_OF_RANGE);
+    }
+  }
+
+  /** Cast value and throw {@link SQLException} if out-of-range. */
+  static long checkedCastToLong(BigDecimal val) throws SQLException {
+    try {
+      return val.longValueExact();
+    } catch (ArithmeticException e) {
+      throw JdbcSqlExceptionFactory.of(
+          String.format(OUT_OF_RANGE_MSG, "long", val), com.google.rpc.Code.OUT_OF_RANGE);
+    }
+  }
+
   /**
    * Parses the given string value as a double. Throws {@link SQLException} if the string is not a
    * valid double value.
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
index 1c984c7..1dc5f0e 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
@@ -19,6 +19,7 @@ package com.google.cloud.spanner.jdbc;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.Code;
+import java.math.BigDecimal;
 import java.sql.Date;
 import java.sql.Timestamp;
 import java.sql.Types;
@@ -175,6 +176,32 @@ enum JdbcDataType {
       return Type.int64();
     }
   },
+  NUMERIC {
+    @Override
+    public int getSqlType() {
+      return Types.NUMERIC;
+    }
+
+    @Override
+    public Class<BigDecimal> getJavaClass() {
+      return BigDecimal.class;
+    }
+
+    @Override
+    public Code getCode() {
+      return Code.NUMERIC;
+    }
+
+    @Override
+    public List<BigDecimal> getArrayElements(ResultSet rs, int columnIndex) {
+      return rs.getBigDecimalList(columnIndex);
+    }
+
+    @Override
+    public Type getSpannerType() {
+      return Type.numeric();
+    }
+  },
   STRING {
     @Override
     public int getSqlType() {
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java
index 2217659..508a2c3 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java
@@ -1203,6 +1203,44 @@ class JdbcDatabaseMetaData extends AbstractJdbcWrapper implements DatabaseMetaDa
                     .to((Long) null)
                     .set("NUM_PREC_RADIX")
                     .to((Long) null)
+                    .build(),
+                Struct.newBuilder()
+                    .set("TYPE_NAME")
+                    .to("NUMERIC")
+                    .set("DATA_TYPE")
+                    .to(Types.NUMERIC) // 2
+                    .set("PRECISION")
+                    .to(2621440L)
+                    .set("LITERAL_PREFIX")
+                    .to((String) null)
+                    .set("LITERAL_SUFFIX")
+                    .to((String) null)
+                    .set("CREATE_PARAMS")
+                    .to((String) null)
+                    .set("NULLABLE")
+                    .to(DatabaseMetaData.typeNullable)
+                    .set("CASE_SENSITIVE")
+                    .to(false)
+                    .set("SEARCHABLE")
+                    .to(DatabaseMetaData.typePredBasic)
+                    .set("UNSIGNED_ATTRIBUTE")
+                    .to(false)
+                    .set("FIXED_PREC_SCALE")
+                    .to(false)
+                    .set("AUTO_INCREMENT")
+                    .to(false)
+                    .set("LOCAL_TYPE_NAME")
+                    .to("NUMERIC")
+                    .set("MINIMUM_SCALE")
+                    .to(0)
+                    .set("MAXIMUM_SCALE")
+                    .to(0)
+                    .set("SQL_DATA_TYPE")
+                    .to((Long) null)
+                    .set("SQL_DATETIME_SUB")
+                    .to((Long) null)
+                    .set("NUM_PREC_RADIX")
+                    .to(10)
                     .build())));
   }
 
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
index f78f1f0..59b10b6 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
@@ -98,7 +98,7 @@ class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMeta
     } else if (Double.class.isAssignableFrom(value.getClass())) {
       return Types.DOUBLE;
     } else if (BigDecimal.class.isAssignableFrom(value.getClass())) {
-      return Types.DECIMAL;
+      return Types.NUMERIC;
     } else if (Date.class.isAssignableFrom(value.getClass())) {
       return Types.DATE;
     } else if (Timestamp.class.isAssignableFrom(value.getClass())) {
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
index 5a139a9..807666c 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
@@ -211,11 +211,9 @@ class JdbcParameterStore {
       case Types.BLOB:
       case Types.CLOB:
       case Types.NCLOB:
-        return true;
       case Types.NUMERIC:
       case Types.DECIMAL:
-        // currently not supported as Cloud Spanner does not have any decimal data type.
-        return false;
+        return true;
     }
     return false;
   }
@@ -235,11 +233,9 @@ class JdbcParameterStore {
       case Types.FLOAT:
       case Types.REAL:
       case Types.DOUBLE:
-        return value instanceof Number;
       case Types.NUMERIC:
       case Types.DECIMAL:
-        // currently not supported as Cloud Spanner does not have any decimal data type.
-        return false;
+        return value instanceof Number;
       case Types.CHAR:
       case Types.VARCHAR:
       case Types.LONGVARCHAR:
@@ -473,9 +469,18 @@ class JdbcParameterStore {
         throw JdbcSqlExceptionFactory.of(value + " is not a valid double", Code.INVALID_ARGUMENT);
       case Types.NUMERIC:
       case Types.DECIMAL:
-        // currently not supported as Cloud Spanner does not have any decimal data type.
+        if (value instanceof Number) {
+          if (value instanceof BigDecimal) {
+            return binder.to((BigDecimal) value);
+          }
+          try {
+            return binder.to(new BigDecimal(value.toString()));
+          } catch (NumberFormatException e) {
+            // ignore and fall through to the exception.
+          }
+        }
         throw JdbcSqlExceptionFactory.of(
-            "DECIMAL/NUMERIC values are not supported", Code.INVALID_ARGUMENT);
+            value + " is not a valid BigDecimal", Code.INVALID_ARGUMENT);
       case Types.CHAR:
       case Types.VARCHAR:
       case Types.LONGVARCHAR:
@@ -604,8 +609,7 @@ class JdbcParameterStore {
     } else if (Double.class.isAssignableFrom(value.getClass())) {
       return binder.to(((Double) value).doubleValue());
     } else if (BigDecimal.class.isAssignableFrom(value.getClass())) {
-      // currently not supported
-      return null;
+      return binder.to((BigDecimal) value);
     } else if (Date.class.isAssignableFrom(value.getClass())) {
       Date dateValue = (Date) value;
       return binder.to(JdbcTypeConverter.toGoogleDate(dateValue));
@@ -693,8 +697,7 @@ class JdbcParameterStore {
           return binder.toFloat64Array((double[]) null);
         case Types.NUMERIC:
         case Types.DECIMAL:
-          throw JdbcSqlExceptionFactory.of(
-              "DECIMAL/NUMERIC values are not supported", Code.INVALID_ARGUMENT);
+          return binder.toNumericArray(null);
         case Types.CHAR:
         case Types.VARCHAR:
         case Types.LONGVARCHAR:
@@ -755,8 +758,7 @@ class JdbcParameterStore {
     } else if (Double[].class.isAssignableFrom(value.getClass())) {
       return binder.toFloat64Array(toDoubleList((Double[]) value));
     } else if (BigDecimal[].class.isAssignableFrom(value.getClass())) {
-      // currently not supported
-      return null;
+      return binder.toNumericArray(Arrays.asList((BigDecimal[]) value));
     } else if (Date[].class.isAssignableFrom(value.getClass())) {
       return binder.toDateArray(JdbcTypeConverter.toGoogleDates((Date[]) value));
     } else if (Timestamp[].class.isAssignableFrom(value.getClass())) {
@@ -810,9 +812,7 @@ class JdbcParameterStore {
         return binder.to((com.google.cloud.Date) null);
       case Types.NUMERIC:
       case Types.DECIMAL:
-        // currently not supported
-        throw JdbcSqlExceptionFactory.of(
-            "DECIMAL/NUMERIC values are not supported", Code.INVALID_ARGUMENT);
+        return binder.to((BigDecimal) null);
       case Types.DOUBLE:
         return binder.to((Double) null);
       case Types.FLOAT:
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
index 2e4014b..3f10b0c 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
@@ -138,6 +138,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
         return isNull ? null : Double.toString(spanner.getDouble(spannerIndex));
       case INT64:
         return isNull ? null : Long.toString(spanner.getLong(spannerIndex));
+      case NUMERIC:
+        return isNull ? null : spanner.getBigDecimal(spannerIndex).toString();
       case STRING:
         return isNull ? null : spanner.getString(spannerIndex);
       case TIMESTAMP:
@@ -162,6 +164,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
         return isNull ? false : spanner.getDouble(spannerIndex) != 0D;
       case INT64:
         return isNull ? false : spanner.getLong(spannerIndex) != 0L;
+      case NUMERIC:
+        return isNull ? false : !spanner.getBigDecimal(spannerIndex).equals(BigDecimal.ZERO);
       case STRING:
         return isNull ? false : Boolean.valueOf(spanner.getString(spannerIndex));
       case BYTES:
@@ -189,6 +193,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
             : checkedCastToByte(Double.valueOf(spanner.getDouble(spannerIndex)).longValue());
       case INT64:
         return isNull ? (byte) 0 : checkedCastToByte(spanner.getLong(spannerIndex));
+      case NUMERIC:
+        return isNull ? (byte) 0 : checkedCastToByte(spanner.getBigDecimal(spannerIndex));
       case STRING:
         return isNull ? (byte) 0 : checkedCastToByte(parseLong(spanner.getString(spannerIndex)));
       case BYTES:
@@ -216,6 +222,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
             : checkedCastToShort(Double.valueOf(spanner.getDouble(spannerIndex)).longValue());
       case INT64:
         return isNull ? 0 : checkedCastToShort(spanner.getLong(spannerIndex));
+      case NUMERIC:
+        return isNull ? 0 : checkedCastToShort(spanner.getBigDecimal(spannerIndex));
       case STRING:
         return isNull ? 0 : checkedCastToShort(parseLong(spanner.getString(spannerIndex)));
       case BYTES:
@@ -243,6 +251,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
             : checkedCastToInt(Double.valueOf(spanner.getDouble(spannerIndex)).longValue());
       case INT64:
         return isNull ? 0 : checkedCastToInt(spanner.getLong(spannerIndex));
+      case NUMERIC:
+        return isNull ? 0 : checkedCastToInt(spanner.getBigDecimal(spannerIndex));
       case STRING:
         return isNull ? 0 : checkedCastToInt(parseLong(spanner.getString(spannerIndex)));
       case BYTES:
@@ -268,6 +278,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
         return isNull ? 0L : Double.valueOf(spanner.getDouble(spannerIndex)).longValue();
       case INT64:
         return isNull ? 0L : spanner.getLong(spannerIndex);
+      case NUMERIC:
+        return isNull ? 0L : checkedCastToLong(spanner.getBigDecimal(spannerIndex));
       case STRING:
         return isNull ? 0L : parseLong(spanner.getString(spannerIndex));
       case BYTES:
@@ -293,6 +305,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
         return isNull ? 0 : checkedCastToFloat(spanner.getDouble(spannerIndex));
       case INT64:
         return isNull ? 0 : checkedCastToFloat(spanner.getLong(spannerIndex));
+      case NUMERIC:
+        return isNull ? 0 : spanner.getBigDecimal(spannerIndex).floatValue();
       case STRING:
         return isNull ? 0 : checkedCastToFloat(parseDouble(spanner.getString(spannerIndex)));
       case BYTES:
@@ -318,6 +332,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
         return isNull ? 0 : spanner.getDouble(spannerIndex);
       case INT64:
         return isNull ? 0 : spanner.getLong(spannerIndex);
+      case NUMERIC:
+        return isNull ? 0 : spanner.getBigDecimal(spannerIndex).doubleValue();
       case STRING:
         return isNull ? 0 : parseDouble(spanner.getString(spannerIndex));
       case BYTES:
@@ -354,6 +370,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case BOOL:
       case FLOAT64:
       case INT64:
+      case NUMERIC:
       case BYTES:
       case STRUCT:
       case ARRAY:
@@ -377,6 +394,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case DATE:
       case FLOAT64:
       case INT64:
+      case NUMERIC:
       case BYTES:
       case STRUCT:
       case ARRAY:
@@ -401,6 +419,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case BOOL:
       case FLOAT64:
       case INT64:
+      case NUMERIC:
       case BYTES:
       case STRUCT:
       case ARRAY:
@@ -555,6 +574,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
     if (type == Type.date()) return getDate(columnIndex);
     if (type == Type.float64()) return getDouble(columnIndex);
     if (type == Type.int64()) return getLong(columnIndex);
+    if (type == Type.numeric()) return getBigDecimal(columnIndex);
     if (type == Type.string()) return getString(columnIndex);
     if (type == Type.timestamp()) return getTimestamp(columnIndex);
     if (type.getCode() == Code.ARRAY) return getArray(columnIndex);
@@ -630,6 +650,9 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case INT64:
         res = isNull ? null : BigDecimal.valueOf(spanner.getLong(spannerIndex));
         break;
+      case NUMERIC:
+        res = isNull ? null : spanner.getBigDecimal(spannerIndex);
+        break;
       case STRING:
         try {
           res = isNull ? null : new BigDecimal(spanner.getString(spannerIndex));
@@ -724,6 +747,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case BOOL:
       case FLOAT64:
       case INT64:
+      case NUMERIC:
       case BYTES:
       case STRUCT:
       case ARRAY:
@@ -752,6 +776,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case DATE:
       case FLOAT64:
       case INT64:
+      case NUMERIC:
       case BYTES:
       case STRUCT:
       case ARRAY:
@@ -783,6 +808,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case BOOL:
       case FLOAT64:
       case INT64:
+      case NUMERIC:
       case BYTES:
       case STRUCT:
       case ARRAY:
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java
index 4292c92..a598b10 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java
@@ -79,7 +79,7 @@ class JdbcResultSetMetaData extends AbstractJdbcWrapper implements ResultSetMeta
   @Override
   public boolean isSigned(int column) throws SQLException {
     int type = getColumnType(column);
-    return type == Types.DOUBLE || type == Types.BIGINT;
+    return type == Types.DOUBLE || type == Types.BIGINT || type == Types.NUMERIC;
   }
 
   @Override
@@ -99,6 +99,8 @@ class JdbcResultSetMetaData extends AbstractJdbcWrapper implements ResultSetMeta
         return 14;
       case Types.BIGINT:
         return 10;
+      case Types.NUMERIC:
+        return 14;
       case Types.NVARCHAR:
         int length = getPrecision(column);
         return length == 0 ? DEFAULT_COL_DISPLAY_SIZE_FOR_VARIABLE_LENGTH_COLS : length;
@@ -136,6 +138,8 @@ class JdbcResultSetMetaData extends AbstractJdbcWrapper implements ResultSetMeta
         return 14;
       case Types.BIGINT:
         return 10;
+      case Types.NUMERIC:
+        return 14;
       case Types.TIMESTAMP:
         return 24;
       default:
@@ -150,7 +154,7 @@ class JdbcResultSetMetaData extends AbstractJdbcWrapper implements ResultSetMeta
   @Override
   public int getScale(int column) throws SQLException {
     int colType = getColumnType(column);
-    if (colType == Types.DOUBLE) return 15;
+    if (colType == Types.DOUBLE || colType == Types.NUMERIC) return 15;
     return 0;
   }
 
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
index e922e58..c1f4b92 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
@@ -80,43 +80,58 @@ class JdbcTypeConverter {
         if (type.getCode() == Code.BOOL) return value;
         if (type.getCode() == Code.INT64) return Boolean.valueOf((Long) value != 0);
         if (type.getCode() == Code.FLOAT64) return Boolean.valueOf((Double) value != 0d);
+        if (type.getCode() == Code.NUMERIC)
+          return Boolean.valueOf(!((BigDecimal) value).equals(BigDecimal.ZERO));
       }
       if (targetType.equals(BigDecimal.class)) {
         if (type.getCode() == Code.BOOL) return (Boolean) value ? BigDecimal.ONE : BigDecimal.ZERO;
         if (type.getCode() == Code.INT64) return BigDecimal.valueOf((Long) value);
+        if (type.getCode() == Code.NUMERIC) return value;
       }
       if (targetType.equals(Long.class)) {
         if (type.getCode() == Code.BOOL) return (Boolean) value ? 1L : 0L;
         if (type.getCode() == Code.INT64) return value;
+        if (type.getCode() == Code.NUMERIC)
+          return AbstractJdbcWrapper.checkedCastToLong((BigDecimal) value);
       }
       if (targetType.equals(Integer.class)) {
         if (type.getCode() == Code.BOOL) return (Boolean) value ? 1 : 0;
         if (type.getCode() == Code.INT64) return AbstractJdbcWrapper.checkedCastToInt((Long) value);
+        if (type.getCode() == Code.NUMERIC)
+          return AbstractJdbcWrapper.checkedCastToInt((BigDecimal) value);
       }
       if (targetType.equals(Short.class)) {
         if (type.getCode() == Code.BOOL) return (Boolean) value ? 1 : 0;
         if (type.getCode() == Code.INT64)
           return AbstractJdbcWrapper.checkedCastToShort((Long) value);
+        if (type.getCode() == Code.NUMERIC)
+          return AbstractJdbcWrapper.checkedCastToShort((BigDecimal) value);
       }
       if (targetType.equals(Byte.class)) {
         if (type.getCode() == Code.BOOL) return (Boolean) value ? 1 : 0;
         if (type.getCode() == Code.INT64)
           return AbstractJdbcWrapper.checkedCastToByte((Long) value);
+        if (type.getCode() == Code.NUMERIC)
+          return AbstractJdbcWrapper.checkedCastToByte((BigDecimal) value);
       }
       if (targetType.equals(BigInteger.class)) {
         if (type.getCode() == Code.BOOL) return (Boolean) value ? BigInteger.ONE : BigInteger.ZERO;
         if (type.getCode() == Code.INT64) return BigInteger.valueOf((Long) value);
+        if (type.getCode() == Code.NUMERIC)
+          return AbstractJdbcWrapper.checkedCastToBigInteger((BigDecimal) value);
       }
       if (targetType.equals(Float.class)) {
         if (type.getCode() == Code.BOOL)
           return (Boolean) value ? Float.valueOf(1f) : Float.valueOf(0f);
         if (type.getCode() == Code.FLOAT64)
           return AbstractJdbcWrapper.checkedCastToFloat((Double) value);
+        if (type.getCode() == Code.NUMERIC) return ((BigDecimal) value).floatValue();
       }
       if (targetType.equals(Double.class)) {
         if (type.getCode() == Code.BOOL)
           return (Boolean) value ? Double.valueOf(1d) : Double.valueOf(0d);
         if (type.getCode() == Code.FLOAT64) return value;
+        if (type.getCode() == Code.NUMERIC) return ((BigDecimal) value).doubleValue();
       }
       if (targetType.equals(java.sql.Date.class)) {
         if (type.getCode() == Code.DATE) return value;
@@ -175,6 +190,10 @@ class JdbcTypeConverter {
         (type.getCode() == Code.TIMESTAMP && value.getClass().equals(java.sql.Timestamp.class))
             || type.getCode() != Code.TIMESTAMP,
         "input type is timestamp, but input value is not an instance of java.sql.Timestamp");
+    JdbcPreconditions.checkArgument(
+        (type.getCode() == Code.NUMERIC && value.getClass().equals(BigDecimal.class))
+            || type.getCode() != Code.NUMERIC,
+        "input type is numeric, but input value is not an instance of BigDecimal");
   }
 
   @SuppressWarnings("deprecation")
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
index 4060161..8b29c15 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
@@ -22,6 +22,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU
     WHEN SPANNER_TYPE = 'DATE' THEN 91
     WHEN SPANNER_TYPE = 'FLOAT64' THEN 8
     WHEN SPANNER_TYPE = 'INT64' THEN -5
+    WHEN SPANNER_TYPE = 'NUMERIC' THEN 2
     WHEN SPANNER_TYPE LIKE 'STRING%' THEN -9
     WHEN SPANNER_TYPE = 'TIMESTAMP' THEN 93
   END AS DATA_TYPE,
@@ -30,6 +31,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU
     WHEN STRPOS(SPANNER_TYPE, '(')=0 THEN
       CASE
         WHEN SPANNER_TYPE = 'INT64' OR SPANNER_TYPE = 'ARRAY<INT64>' THEN 19
+        WHEN SPANNER_TYPE = 'NUMERIC' OR SPANNER_TYPE = 'ARRAY<NUMERIC>' THEN 15
         WHEN SPANNER_TYPE = 'FLOAT64' OR SPANNER_TYPE = 'ARRAY<FLOAT64>' THEN 15
         WHEN SPANNER_TYPE = 'BOOL' OR SPANNER_TYPE = 'ARRAY<BOOL>' THEN NULL
         WHEN SPANNER_TYPE = 'DATE' OR SPANNER_TYPE = 'ARRAY<DATE>' THEN 10
@@ -45,6 +47,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU
   END AS DECIMAL_DIGITS,
   CASE
     WHEN SPANNER_TYPE LIKE '%INT64%' THEN 10
+    WHEN SPANNER_TYPE LIKE '%NUMERIC%' THEN 10
     WHEN SPANNER_TYPE LIKE '%FLOAT64%' THEN 2
     ELSE NULL
   END AS NUM_PREC_RADIX,


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
index 50a696f..01184c2 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
@@ -18,6 +18,7 @@ package com.google.cloud.spanner.jdbc;
 
 import static org.junit.Assert.assertEquals;
 
+import java.math.BigDecimal;
 import java.sql.Date;
 import java.sql.SQLException;
 import java.sql.Timestamp;
@@ -54,6 +55,13 @@ public class JdbcArrayTest {
     assertEquals(array.getBaseType(), Types.BIGINT);
     assertEquals(((Long[]) array.getArray(1, 1))[0], Long.valueOf(1L));
 
+    array =
+        JdbcArray.createArray("NUMERIC", new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN});
+    assertEquals(array.getBaseType(), Types.NUMERIC);
+    assertEquals(((BigDecimal[]) array.getArray(1, 1))[0], BigDecimal.ONE);
+    assertEquals(((BigDecimal[]) array.getArray(2, 1))[0], null);
+    assertEquals(((BigDecimal[]) array.getArray(3, 1))[0], BigDecimal.TEN);
+
     array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
     assertEquals(array.getBaseType(), Types.NVARCHAR);
     assertEquals(((String[]) array.getArray(1, 1))[0], "foo");
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java
index 48d5521..b3697a0 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java
@@ -461,6 +461,8 @@ public class JdbcDatabaseMetaDataTest {
       assertThat(rs.getString("TYPE_NAME"), is(equalTo("DATE")));
       assertThat(rs.next(), is(true));
       assertThat(rs.getString("TYPE_NAME"), is(equalTo("TIMESTAMP")));
+      assertThat(rs.next(), is(true));
+      assertThat(rs.getString("TYPE_NAME"), is(equalTo("NUMERIC")));
       assertThat(rs.next(), is(false));
       ResultSetMetaData rsmd = rs.getMetaData();
       assertThat(rsmd.getColumnCount(), is(equalTo(18)));
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java
index 60911a8..bdeda19 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java
@@ -328,8 +328,10 @@ public class JdbcGrpcErrorTest {
     mockSpanner.setExecuteStreamingSqlExecutionTime(
         SimulatedExecutionTime.ofException(serverException));
     try (java.sql.Connection connection = createConnection()) {
-      connection.createStatement().executeQuery(SELECT1.getSql());
-      fail("missing expected exception");
+      try (java.sql.ResultSet rs = connection.createStatement().executeQuery(SELECT1.getSql())) {
+        rs.next();
+        fail("missing expected exception");
+      }
     } catch (SQLException e) {
       assertThat(testExceptionMatcher.matches(e)).isTrue();
     }
@@ -341,8 +343,10 @@ public class JdbcGrpcErrorTest {
         SimulatedExecutionTime.ofException(serverException));
     try (java.sql.Connection connection = createConnection()) {
       connection.setAutoCommit(false);
-      connection.createStatement().executeQuery(SELECT1.getSql());
-      fail("missing expected exception");
+      try (java.sql.ResultSet rs = connection.createStatement().executeQuery(SELECT1.getSql())) {
+        rs.next();
+        fail("missing expected exception");
+      }
     } catch (SQLException e) {
       assertThat(testExceptionMatcher.matches(e)).isTrue();
     }
@@ -355,8 +359,10 @@ public class JdbcGrpcErrorTest {
     try (java.sql.Connection connection = createConnection()) {
       connection.setAutoCommit(false);
       connection.setReadOnly(true);
-      connection.createStatement().executeQuery(SELECT1.getSql());
-      fail("missing expected exception");
+      try (java.sql.ResultSet rs = connection.createStatement().executeQuery(SELECT1.getSql())) {
+        rs.next();
+        fail("missing expected exception");
+      }
     } catch (SQLException e) {
       assertThat(testExceptionMatcher.matches(e)).isTrue();
     }
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
index 1b201f9..624a031 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
@@ -120,16 +120,8 @@ public class JdbcParameterStoreTest {
         is(true));
     verifyParameter(params, Value.string("test"));
 
-    // test unsupported types
-    boolean expectedException = false;
-    try {
-      params.setParameter(1, BigDecimal.ONE, Types.DECIMAL);
-    } catch (SQLException e) {
-      if (e instanceof JdbcSqlException) {
-        expectedException = ((JdbcSqlException) e).getCode() == Code.INVALID_ARGUMENT;
-      }
-    }
-    assertThat(expectedException, is(true));
+    params.setParameter(1, BigDecimal.ONE, Types.DECIMAL);
+    verifyParameter(params, Value.numeric(BigDecimal.ONE));
 
     // types that should lead to int64
     for (int type : new int[] {Types.TINYINT, Types.SMALLINT, Types.INTEGER, Types.BIGINT}) {
@@ -281,6 +273,32 @@ public class JdbcParameterStoreTest {
       assertThat((BigDecimal) params.getParameter(1), is(equalTo(BigDecimal.ZERO)));
       verifyParameter(params, Value.bool(false));
     }
+
+    // types that should lead to numeric
+    for (int type : new int[] {Types.DECIMAL, Types.NUMERIC}) {
+      params.setParameter(1, BigDecimal.ONE, type);
+      assertThat((BigDecimal) params.getParameter(1), is(equalTo(BigDecimal.ONE)));
+      verifyParameter(params, Value.numeric(BigDecimal.ONE));
+
+      params.setParameter(1, (byte) 1, type);
+      assertThat((Byte) params.getParameter(1), is(equalTo((byte) 1)));
+      verifyParameter(params, Value.numeric(BigDecimal.ONE));
+      params.setParameter(1, (short) 1, type);
+      assertThat((Short) params.getParameter(1), is(equalTo((short) 1)));
+      verifyParameter(params, Value.numeric(BigDecimal.ONE));
+      params.setParameter(1, 1, type);
+      assertThat((Integer) params.getParameter(1), is(equalTo(1)));
+      verifyParameter(params, Value.numeric(BigDecimal.ONE));
+      params.setParameter(1, 1L, type);
+      assertThat((Long) params.getParameter(1), is(equalTo(1L)));
+      verifyParameter(params, Value.numeric(BigDecimal.ONE));
+      params.setParameter(1, (float) 1, type);
+      assertThat((Float) params.getParameter(1), is(equalTo((float) 1)));
+      verifyParameter(params, Value.numeric(BigDecimal.valueOf(1.0)));
+      params.setParameter(1, (double) 1, type);
+      assertThat((Double) params.getParameter(1), is(equalTo((double) 1)));
+      verifyParameter(params, Value.numeric(BigDecimal.valueOf(1.0)));
+    }
   }
 
   @Test
@@ -353,9 +371,6 @@ public class JdbcParameterStoreTest {
       assertInvalidParameter(params, new Object(), type);
     }
 
-    assertInvalidParameter(params, BigDecimal.ONE, Types.DECIMAL);
-    assertInvalidParameter(params, BigDecimal.ZERO, Types.NUMERIC);
-
     // test setting closed readers and streams.
     for (int type :
         new int[] {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
index bcf0454..c5d9396 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
@@ -34,7 +34,6 @@ import com.google.cloud.spanner.connection.Connection;
 import com.google.rpc.Code;
 import java.io.ByteArrayInputStream;
 import java.io.StringReader;
-import java.math.BigDecimal;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.sql.Date;
@@ -211,15 +210,6 @@ public class JdbcPreparedStatementTest {
   private void testSetUnsupportedTypes(PreparedStatement ps) {
     // TODO: Rewrite these tests using functional interfaces when Java8 is available.
     boolean expectedException = false;
-    try {
-      ps.setBigDecimal(5, BigDecimal.valueOf(1l));
-    } catch (SQLException e) {
-      if (e instanceof JdbcSqlException) {
-        expectedException = ((JdbcSqlException) e).getCode() == Code.INVALID_ARGUMENT;
-      }
-    }
-    assertThat(expectedException, is(true));
-    expectedException = false;
     try {
       ps.setRef(38, (Ref) null);
     } catch (SQLException e) {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java
index 58ef03a..d7c844a 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java
@@ -71,7 +71,8 @@ public class JdbcTypeConverterTest {
           Type.float64(),
           Type.int64(),
           Type.string(),
-          Type.timestamp()
+          Type.timestamp(),
+          Type.numeric()
         }) {
       assertConvertThrows(testValue, Type.array(t), Boolean.class, Code.INVALID_ARGUMENT);
       assertConvertThrows(testValue, Type.array(t), Byte.class, Code.INVALID_ARGUMENT);
@@ -292,6 +293,43 @@ public class JdbcTypeConverterTest {
     }
   }
 
+  @Test
+  public void testConvertNumeric() throws SQLException {
+    BigDecimal[] testValues =
+        new BigDecimal[] {
+          BigDecimal.ZERO,
+          BigDecimal.ONE.negate(),
+          BigDecimal.ONE,
+          BigDecimal.valueOf(Double.MIN_VALUE),
+          BigDecimal.valueOf(Double.MAX_VALUE),
+          BigDecimal.valueOf(Float.MIN_VALUE),
+          BigDecimal.valueOf(Float.MAX_VALUE),
+          BigDecimal.valueOf(Float.MAX_VALUE + 1D)
+        };
+    for (BigDecimal d : testValues) {
+      assertThat(convert(d, Type.numeric(), BigDecimal.class)).isEqualTo(d);
+      assertThat(convert(d, Type.numeric(), Double.class)).isEqualTo(d.doubleValue());
+      assertThat(convert(d, Type.numeric(), Float.class)).isEqualTo(d.floatValue());
+      assertThat(convert(d, Type.numeric(), String.class)).isEqualTo(String.valueOf(d));
+      assertThat(convert(d, Type.numeric(), Boolean.class))
+          .isEqualTo(Boolean.valueOf(!d.equals(BigDecimal.ZERO)));
+      if (d.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0
+          || d.compareTo(BigDecimal.valueOf(Long.MIN_VALUE)) < 0
+          || d.scale() > 0) {
+        assertConvertThrows(d, Type.numeric(), Long.class, Code.OUT_OF_RANGE);
+      } else {
+        assertThat(convert(d, Type.numeric(), Long.class)).isEqualTo(d.longValue());
+      }
+      if (d.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE)) > 0
+          || d.compareTo(BigDecimal.valueOf(Integer.MIN_VALUE)) < 0
+          || d.scale() > 0) {
+        assertConvertThrows(d, Type.numeric(), Integer.class, Code.OUT_OF_RANGE);
+      } else {
+        assertThat(convert(d, Type.numeric(), Integer.class)).isEqualTo(d.intValue());
+      }
+    }
+  }
+
   private void assertConvertThrows(Object t, Type type, Class<?> destinationType, Code code)
       throws SQLException {
     try {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SpannerJdbcExceptionMatcher.java b/src/test/java/com/google/cloud/spanner/jdbc/SpannerJdbcExceptionMatcher.java
index 4cd5e9b..be5fed2 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/SpannerJdbcExceptionMatcher.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/SpannerJdbcExceptionMatcher.java
@@ -55,7 +55,7 @@ public final class SpannerJdbcExceptionMatcher<T extends JdbcSqlException> exten
         return exception.getErrorCode() == errorCode.getNumber();
       }
       return exception.getErrorCode() == errorCode.getNumber()
-          && exception.getMessage().endsWith(": " + message);
+          && exception.getMessage().contains(": " + message);
     }
     return false;
   }
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java
index 9cf57b3..711363c 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java
@@ -104,6 +104,7 @@ public class ITJdbcDatabaseMetaDataTest extends ITAbstractJdbcTest {
           new Column("ColDate", Types.DATE, "DATE", 10, null, null, false, null),
           new Column("ColTimestamp", Types.TIMESTAMP, "TIMESTAMP", 35, null, null, false, null),
           new Column("ColCommitTS", Types.TIMESTAMP, "TIMESTAMP", 35, null, null, false, null),
+          new Column("ColNumeric", Types.NUMERIC, "NUMERIC", 15, null, 10, false, null),
           new Column("ColInt64Array", Types.ARRAY, "ARRAY<INT64>", 19, null, 10, true, null),
           new Column("ColFloat64Array", Types.ARRAY, "ARRAY<FLOAT64>", 15, 16, 2, true, null),
           new Column("ColBoolArray", Types.ARRAY, "ARRAY<BOOL>", null, null, null, true, null),
@@ -131,7 +132,8 @@ public class ITJdbcDatabaseMetaDataTest extends ITAbstractJdbcTest {
               null),
           new Column("ColDateArray", Types.ARRAY, "ARRAY<DATE>", 10, null, null, true, null),
           new Column(
-              "ColTimestampArray", Types.ARRAY, "ARRAY<TIMESTAMP>", 35, null, null, true, null));
+              "ColTimestampArray", Types.ARRAY, "ARRAY<TIMESTAMP>", 35, null, null, true, null),
+          new Column("ColNumericArray", Types.ARRAY, "ARRAY<NUMERIC>", 15, null, 10, true, null));
 
   @Test
   public void testGetColumns() throws SQLException {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
index 60ddb3c..799b494 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
@@ -31,6 +31,7 @@ import com.google.common.base.Strings;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.StringReader;
+import java.math.BigDecimal;
 import java.sql.BatchUpdateException;
 import java.sql.Connection;
 import java.sql.Date;
@@ -749,6 +750,94 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
     }
   }
 
+  @Test
+  public void test08_InsertAllColumnTypes() throws SQLException {
+    try (Connection con = createConnection()) {
+      try (PreparedStatement ps =
+          con.prepareStatement(
+              "INSERT INTO TableWithAllColumnTypes ("
+                  + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, "
+                  + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray"
+                  + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
+        ps.setLong(1, 1L);
+        ps.setDouble(2, 2D);
+        ps.setBoolean(3, true);
+        ps.setString(4, "test");
+        ps.setString(5, "testtest");
+        ps.setBytes(6, "test".getBytes());
+        ps.setBytes(7, "testtest".getBytes());
+        ps.setDate(8, new Date(System.currentTimeMillis()));
+        ps.setTimestamp(9, new Timestamp(System.currentTimeMillis()));
+        ps.setBigDecimal(10, BigDecimal.TEN);
+        ps.setArray(11, con.createArrayOf("INT64", new Long[] {1L, 2L, 3L}));
+        ps.setArray(12, con.createArrayOf("FLOAT64", new Double[] {1.1D, 2.2D, 3.3D}));
+        ps.setArray(
+            13, con.createArrayOf("BOOL", new Boolean[] {Boolean.TRUE, null, Boolean.FALSE}));
+        ps.setArray(14, con.createArrayOf("STRING", new String[] {"1", "2", "3"}));
+        ps.setArray(15, con.createArrayOf("STRING", new String[] {"3", "2", "1"}));
+        ps.setArray(
+            16,
+            con.createArrayOf(
+                "BYTES", new byte[][] {"1".getBytes(), "2".getBytes(), "3".getBytes()}));
+        ps.setArray(
+            17,
+            con.createArrayOf(
+                "BYTES", new byte[][] {"333".getBytes(), "222".getBytes(), "111".getBytes()}));
+        ps.setArray(
+            18,
+            con.createArrayOf(
+                "DATE", new Date[] {new Date(System.currentTimeMillis()), null, new Date(0)}));
+        ps.setArray(
+            19,
+            con.createArrayOf(
+                "TIMESTAMP",
+                new Timestamp[] {
+                  new Timestamp(System.currentTimeMillis()), null, new Timestamp(0)
+                }));
+        ps.setArray(
+            20,
+            con.createArrayOf("NUMERIC", new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN}));
+        assertThat(ps.executeUpdate(), is(equalTo(1)));
+      }
+      try (ResultSet rs =
+          con.createStatement().executeQuery("SELECT * FROM TableWithAllColumnTypes")) {
+        assertThat(rs.next(), is(true));
+        assertThat(rs.getLong(1), is(equalTo(1L)));
+        assertThat(rs.getDouble(2), is(equalTo(2D)));
+        assertThat(rs.getBoolean(3), is(true));
+        assertThat(rs.getString(4), is(equalTo("test")));
+        assertThat(rs.getString(5), is(equalTo("testtest")));
+        assertThat(rs.getBytes(6), is(equalTo("test".getBytes())));
+        assertThat(rs.getBytes(7), is(equalTo("testtest".getBytes())));
+        assertThat(rs.getDate(8), is(notNullValue()));
+        assertThat(rs.getTimestamp(9), is(notNullValue()));
+        assertThat(rs.getTime(10), is(notNullValue())); // Commit timestamp
+        assertThat(rs.getBigDecimal(11), is(equalTo(BigDecimal.TEN)));
+        assertThat((Long[]) rs.getArray(12).getArray(), is(equalTo(new Long[] {1L, 2L, 3L})));
+        assertThat(
+            (Double[]) rs.getArray(13).getArray(), is(equalTo(new Double[] {1.1D, 2.2D, 3.3D})));
+        assertThat(
+            (Boolean[]) rs.getArray(14).getArray(), is(equalTo(new Boolean[] {true, null, false})));
+        assertThat(
+            (String[]) rs.getArray(15).getArray(), is(equalTo(new String[] {"1", "2", "3"})));
+        assertThat(
+            (String[]) rs.getArray(16).getArray(), is(equalTo(new String[] {"3", "2", "1"})));
+        assertThat(
+            (byte[][]) rs.getArray(17).getArray(),
+            is(equalTo(new byte[][] {"1".getBytes(), "2".getBytes(), "3".getBytes()})));
+        assertThat(
+            (byte[][]) rs.getArray(18).getArray(),
+            is(equalTo(new byte[][] {"333".getBytes(), "222".getBytes(), "111".getBytes()})));
+        assertThat(((Date[]) rs.getArray(19).getArray()).length, is(equalTo(3)));
+        assertThat(((Timestamp[]) rs.getArray(20).getArray()).length, is(equalTo(3)));
+        assertThat(
+            (BigDecimal[]) rs.getArray(21).getArray(),
+            is(equalTo(new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN})));
+        assertThat(rs.next(), is(false));
+      }
+    }
+  }
+
   private void assertDefaultParameterMetaData(ParameterMetaData pmd, int expectedParamCount)
       throws SQLException {
     assertThat(pmd.getParameterCount(), is(equalTo(expectedParamCount)));
diff --git a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql
index 5bbb90f..4d05cdf 100644
--- a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql
+++ b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql
@@ -73,6 +73,7 @@ CREATE TABLE TableWithAllColumnTypes (
   ColDate		DATE		NOT NULL,
   ColTimestamp	TIMESTAMP	NOT NULL,
   ColCommitTS	TIMESTAMP	NOT NULL OPTIONS (allow_commit_timestamp=true),
+  ColNumeric	NUMERIC		NOT NULL,
   
   ColInt64Array		ARRAY<INT64>,
   ColFloat64Array	ARRAY<FLOAT64>,
@@ -82,7 +83,8 @@ CREATE TABLE TableWithAllColumnTypes (
   ColBytesArray		ARRAY<BYTES(100)>,
   ColBytesMaxArray	ARRAY<BYTES(MAX)>,
   ColDateArray		ARRAY<DATE>,
-  ColTimestampArray	ARRAY<TIMESTAMP>
+  ColTimestampArray	ARRAY<TIMESTAMP>,
+  ColNumericArray	ARRAY<NUMERIC>
 ) PRIMARY KEY (ColInt64)
 ;