Repository: GoogleCloudPlatform/pgadapter
Pull request #383 (2022-10-10 13:15:10)

perf: do not create a new parser for each column
Convert a Spanner value directly to the PostgreSQL equivalent without creating a Parser object. This reduces the amount of memory that is allocated and must be garbage collected for each row that is sent to the PostgreSQL client.

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#383](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (ac2dd1f) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/86e4f39253214bbb64e34e1e7a63aceaf9c6e516?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (86e4f39) will **decrease** coverage by `0.22%`.
> The diff coverage is `71.25%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #383      +/-   ##
========================================================
- Coverage                 86.38%   86.15%   -0.23%     
- Complexity                 1841     1876      +35     
========================================================
  Files                       121      121              
  Lines                      6050     6112      +62     
  Branches                    836      837       +1     
========================================================
+ Hits                       5226     5266      +40     
- Misses                      592      605      +13     
- Partials                    232      241       +9     
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `86.15% <71.25%> (-0.23%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [.../cloud/spanner/pgadapter/parsers/StringParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvU3RyaW5nUGFyc2VyLmphdmE=) | `83.33% <33.33%> (-10.00%)` | :arrow_down: |
| [.../cloud/spanner/pgadapter/parsers/BinaryParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvQmluYXJ5UGFyc2VyLmphdmE=) | `78.57% <60.00%> (-4.04%)` | :arrow_down: |
| [...le/cloud/spanner/pgadapter/parsers/DateParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvRGF0ZVBhcnNlci5qYXZh) | `87.23% <66.66%> (-3.47%)` | :arrow_down: |
| [.../cloud/spanner/pgadapter/parsers/DoubleParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvRG91YmxlUGFyc2VyLmphdmE=) | `82.35% <66.66%> (-3.86%)` | :arrow_down: |
| [...le/cloud/spanner/pgadapter/parsers/LongParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvTG9uZ1BhcnNlci5qYXZh) | `83.33% <66.66%> (-3.77%)` | :arrow_down: |
| [...oud/spanner/pgadapter/parsers/TimestampParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvVGltZXN0YW1wUGFyc2VyLmphdmE=) | `91.52% <66.66%> (-4.63%)` | :arrow_down: |
| [...cloud/spanner/pgadapter/parsers/NumericParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvTnVtZXJpY1BhcnNlci5qYXZh) | `82.35% <71.42%> (-3.86%)` | :arrow_down: |
| [...e/cloud/spanner/pgadapter/parsers/JsonbParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvSnNvbmJQYXJzZXIuamF2YQ==) | `91.89% <75.00%> (-4.99%)` | :arrow_down: |
| [...cloud/spanner/pgadapter/parsers/BooleanParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvQm9vbGVhblBhcnNlci5qYXZh) | `90.19% <76.92%> (-4.68%)` | :arrow_down: |
| [...oogle/cloud/spanner/pgadapter/utils/Converter.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL0NvbnZlcnRlci5qYXZh) | `80.00% <80.00%> (+5.00%)` | :arrow_up: |
| ... and [2 more](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/383/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform)


Patch:
diff --git a/.gitignore b/.gitignore
index 9e3e3254..70ccbb78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ target/
 *.class
 *.lst
 output.txt
+__pycache__
 
 src/test/golang/**/*.h
 src/test/golang/**/*.so
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java
index a8a33fa8..4b5003ef 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
  * Translate wire protocol to array. Since arrays house any other specified types (including
  * potentially arrays), we use all parser types to parse each item within.
  */
-class ArrayParser extends Parser<List<?>> {
+public class ArrayParser extends Parser<List<?>> {
   private static final String STRING_TOGGLE = "\"";
   private static final String ARRAY_DELIMITER = ",";
   private static final String PG_ARRAY_OPEN = "{", PG_ARRAY_CLOSE = "}";
@@ -40,7 +40,7 @@ class ArrayParser extends Parser<List<?>> {
   private final Type arrayElementType;
   private final boolean isStringEquivalent;
 
-  ArrayParser(ResultSet item, int position) {
+  public ArrayParser(ResultSet item, int position) {
     if (item != null) {
       this.arrayElementType = item.getColumnType(position).getArrayElementType();
       if (this.arrayElementType.getCode() == Code.ARRAY) {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java
index 4666a8bc..f01a60c6 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java
@@ -18,6 +18,7 @@ import com.google.api.core.InternalApi;
 import com.google.cloud.ByteArray;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import java.nio.charset.StandardCharsets;
 import javax.annotation.Nonnull;
@@ -79,6 +80,19 @@ public class BinaryParser extends Parser<ByteArray> {
     return this.item == null ? null : this.item.toByteArray();
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+      case POSTGRESQL_BINARY:
+        return resultSet.getBytes(position).toByteArray();
+      case POSTGRESQL_TEXT:
+        return ("\\x" + Utils.toHexString(resultSet.getBytes(position).toByteArray()))
+            .getBytes(StandardCharsets.UTF_8);
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
+    }
+  }
+
   @Override
   public void bind(Statement.Builder statementBuilder, String name) {
     statementBuilder.bind(name).to(this.item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java
index cb276fa8..d22c33a6 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java
@@ -19,8 +19,10 @@ import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.common.collect.ImmutableSet;
+import java.nio.charset.StandardCharsets;
 import java.util.Locale;
 import java.util.Set;
 import javax.annotation.Nonnull;
@@ -34,6 +36,15 @@ import org.postgresql.util.ByteConverter;
 public class BooleanParser extends Parser<Boolean> {
   private static final String TRUE_VALUE = "t";
   private static final String FALSE_VALUE = "f";
+  private static final byte[] TRUE_VALUE_BYTES = new byte[] {'t'};
+  private static final byte[] FALSE_VALUE_BYTES = new byte[] {'f'};
+  private static final byte[] TRUE_VALUE_BYTES_BINARY = new byte[1];
+  private static final byte[] FALSE_VALUE_BYTES_BINARY = new byte[1];
+
+  static {
+    ByteConverter.bool(TRUE_VALUE_BYTES_BINARY, 0, true);
+    ByteConverter.bool(FALSE_VALUE_BYTES_BINARY, 0, false);
+  }
   // See https://www.postgresql.org/docs/current/datatype-boolean.html
   private static final Set<String> TRUE_VALUES =
       ImmutableSet.of("t", "tr", "tru", "true", "y", "ye", "yes", "on", "1");
@@ -102,11 +113,28 @@ public class BooleanParser extends Parser<Boolean> {
     if (this.item == null) {
       return null;
     }
+    return convertToPGBinary(this.item);
+  }
+
+  static byte[] convertToPGBinary(boolean value) {
     byte[] result = new byte[1];
-    ByteConverter.bool(result, 0, this.item);
+    ByteConverter.bool(result, 0, value);
     return result;
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+        return Boolean.toString(resultSet.getBoolean(position)).getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_TEXT:
+        return resultSet.getBoolean(position) ? TRUE_VALUE_BYTES : FALSE_VALUE_BYTES;
+      case POSTGRESQL_BINARY:
+        return resultSet.getBoolean(position) ? TRUE_VALUE_BYTES_BINARY : FALSE_VALUE_BYTES_BINARY;
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
+    }
+  }
+
   @Override
   public void bind(Statement.Builder statementBuilder, String name) {
     statementBuilder.bind(name).to(this.item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java
index 4d914283..489a2069 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java
@@ -20,8 +20,10 @@ import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.common.base.Preconditions;
+import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
@@ -114,13 +116,28 @@ public class DateParser extends Parser<Date> {
     if (this.item == null) {
       return null;
     }
-    LocalDate localDate =
-        LocalDate.of(this.item.getYear(), this.item.getMonth(), this.item.getDayOfMonth());
+    return convertToPG(this.item);
+  }
+
+  static byte[] convertToPG(Date value) {
+    LocalDate localDate = LocalDate.of(value.getYear(), value.getMonth(), value.getDayOfMonth());
     long days = localDate.toEpochDay() - PG_EPOCH_DAYS;
     int daysAsInt = validateRange(days);
     return IntegerParser.binaryParse(daysAsInt);
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+      case POSTGRESQL_TEXT:
+        return resultSet.getDate(position).toString().getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_BINARY:
+        return convertToPG(resultSet.getDate(position));
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
+    }
+  }
+
   /**
    * Dates are stored as long, but technically cannot be longer than int. Here we ensure that is the
    * case.
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java
index e57ce737..db1982f3 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java
@@ -19,7 +19,9 @@ import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import java.nio.charset.StandardCharsets;
 import javax.annotation.Nonnull;
 import org.postgresql.util.ByteConverter;
 
@@ -74,11 +76,27 @@ public class DoubleParser extends Parser<Double> {
     if (this.item == null) {
       return null;
     }
+    return convertToPG(this.item);
+  }
+
+  static byte[] convertToPG(double value) {
     byte[] result = new byte[8];
-    ByteConverter.float8(result, 0, this.item);
+    ByteConverter.float8(result, 0, value);
     return result;
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+      case POSTGRESQL_TEXT:
+        return Double.toString(resultSet.getDouble(position)).getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_BINARY:
+        return convertToPG(resultSet.getDouble(position));
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
+    }
+  }
+
   @Override
   public void bind(Statement.Builder statementBuilder, String name) {
     statementBuilder.bind(name).to(this.item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/JsonbParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/JsonbParser.java
index 91198fc4..5aa65219 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/JsonbParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/JsonbParser.java
@@ -17,6 +17,7 @@ package com.google.cloud.spanner.pgadapter.parsers;
 import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.error.Severity;
@@ -76,15 +77,31 @@ public class JsonbParser extends Parser<String> {
     if (this.item == null) {
       return null;
     }
-    byte[] value = this.item.getBytes(StandardCharsets.UTF_8);
-    byte[] result = new byte[value.length + 1];
+    return convertToPG(this.item);
+  }
+
+  static byte[] convertToPG(String value) {
+    byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+    byte[] result = new byte[bytes.length + 1];
     // Set version = 1
     result[0] = 1;
-    System.arraycopy(value, 0, result, 1, value.length);
+    System.arraycopy(bytes, 0, result, 1, bytes.length);
 
     return result;
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+      case POSTGRESQL_TEXT:
+        return resultSet.getPgJsonb(position).getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_BINARY:
+        return convertToPG(resultSet.getPgJsonb(position));
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
+    }
+  }
+
   @Override
   public void bind(Statement.Builder statementBuilder, String name) {
     statementBuilder.bind(name).to(this.item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java
index 760f1757..2cc03942 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java
@@ -19,7 +19,9 @@ import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import java.nio.charset.StandardCharsets;
 import javax.annotation.Nonnull;
 import org.postgresql.util.ByteConverter;
 
@@ -81,11 +83,27 @@ public class LongParser extends Parser<Long> {
     if (this.item == null) {
       return null;
     }
+    return convertToPG(this.item);
+  }
+
+  static byte[] convertToPG(long value) {
     byte[] result = new byte[8];
-    ByteConverter.int8(result, 0, this.item);
+    ByteConverter.int8(result, 0, value);
     return result;
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+      case POSTGRESQL_TEXT:
+        return Long.toString(resultSet.getLong(position)).getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_BINARY:
+        return convertToPG(resultSet.getLong(position));
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
+    }
+  }
+
   @Override
   public void bind(Statement.Builder statementBuilder, String name) {
     statementBuilder.bind(name).to(this.item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/NumericParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/NumericParser.java
index 4b2e49cb..bc573576 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/NumericParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/NumericParser.java
@@ -20,6 +20,7 @@ import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 import javax.annotation.Nonnull;
@@ -28,6 +29,12 @@ import org.postgresql.util.ByteConverter;
 /** Translate from wire protocol to {@link Number}. */
 @InternalApi
 public class NumericParser extends Parser<String> {
+  private static final byte[] NAN = new byte[8];
+
+  static {
+    ByteConverter.int2(NAN, 4, (short) 0xC000);
+  }
+
   NumericParser(ResultSet item, int position) {
     this.item = item.isNull(position) ? null : item.getString(position);
   }
@@ -73,14 +80,30 @@ public class NumericParser extends Parser<String> {
     if (this.item == null) {
       return null;
     }
-    if (this.item.equalsIgnoreCase("NaN")) {
-      return "NaN".getBytes(StandardCharsets.UTF_8);
+    return convertToPG(this.item);
+  }
+
+  static byte[] convertToPG(@Nonnull String value) {
+    if (value.equalsIgnoreCase("NaN")) {
+      return NAN;
     }
     try {
-      return ByteConverter.numeric(new BigDecimal(this.item));
+      return ByteConverter.numeric(new BigDecimal(value));
     } catch (NumberFormatException exception) {
       throw SpannerExceptionFactory.newSpannerException(
-          ErrorCode.INVALID_ARGUMENT, "Invalid numeric value: " + this.item);
+          ErrorCode.INVALID_ARGUMENT, "Invalid numeric value: " + value);
+    }
+  }
+
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+      case POSTGRESQL_TEXT:
+        return resultSet.getString(position).getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_BINARY:
+        return convertToPG(resultSet.getString(position));
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
     }
   }
 
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java
index 74d7d75e..f4fcb43b 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java
@@ -53,6 +53,16 @@ public class StringParser extends Parser<String> {
     return this.item == null ? null : this.item.getBytes(StandardCharsets.UTF_8);
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position) {
+    return resultSet.getString(position).getBytes(StandardCharsets.UTF_8);
+  }
+
+  public static byte[] binaryParse(ResultSet resultSet, int position) {
+    return resultSet.isNull(position)
+        ? null
+        : resultSet.getString(position).getBytes(StandardCharsets.UTF_8);
+  }
+
   @Override
   public void bind(Statement.Builder statementBuilder, String name) {
     statementBuilder.bind(name).to(this.item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java
index b73b1627..802122fe 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java
@@ -20,6 +20,7 @@ import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.common.base.Preconditions;
 import java.nio.charset.StandardCharsets;
@@ -137,14 +138,32 @@ public class TimestampParser extends Parser<Timestamp> {
     if (this.item == null) {
       return null;
     }
+    return convertToPG(this.item);
+  }
+
+  static byte[] convertToPG(Timestamp value) {
     long microseconds =
-        ((this.item.getSeconds() - PG_EPOCH_SECONDS) * MICROSECONDS_IN_SECOND)
-            + (this.item.getNanos() / NANOSECONDS_IN_MICROSECONDS);
+        ((value.getSeconds() - PG_EPOCH_SECONDS) * MICROSECONDS_IN_SECOND)
+            + (value.getNanos() / NANOSECONDS_IN_MICROSECONDS);
     byte[] result = new byte[8];
     ByteConverter.int8(result, 0, microseconds);
     return result;
   }
 
+  public static byte[] convertToPG(ResultSet resultSet, int position, DataFormat format) {
+    switch (format) {
+      case SPANNER:
+        return resultSet.getTimestamp(position).toString().getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_TEXT:
+        return toPGString(resultSet.getTimestamp(position).toString())
+            .getBytes(StandardCharsets.UTF_8);
+      case POSTGRESQL_BINARY:
+        return convertToPG(resultSet.getTimestamp(position));
+      default:
+        throw new IllegalArgumentException("unknown data format: " + format);
+    }
+  }
+
   /**
    * Converts the given {@link Timestamp} to a text value that is understood by PostgreSQL. That
    * means a space delimiter between date and time values, and no trailing 'Z'.
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java
index 64e56fc2..d18ba9c5 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/Converter.java
@@ -15,8 +15,18 @@
 package com.google.cloud.spanner.pgadapter.utils;
 
 import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
-import com.google.cloud.spanner.pgadapter.parsers.Parser;
+import com.google.cloud.spanner.pgadapter.parsers.ArrayParser;
+import com.google.cloud.spanner.pgadapter.parsers.BinaryParser;
+import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
+import com.google.cloud.spanner.pgadapter.parsers.DateParser;
+import com.google.cloud.spanner.pgadapter.parsers.DoubleParser;
+import com.google.cloud.spanner.pgadapter.parsers.JsonbParser;
+import com.google.cloud.spanner.pgadapter.parsers.LongParser;
+import com.google.cloud.spanner.pgadapter.parsers.NumericParser;
+import com.google.cloud.spanner.pgadapter.parsers.StringParser;
+import com.google.cloud.spanner.pgadapter.parsers.TimestampParser;
 import com.google.common.base.Preconditions;
 
 /** Utility class for converting between generic PostgreSQL conversions. */
@@ -27,14 +37,40 @@ public class Converter {
    * may not contain a null value.
    *
    * @param result The {@link ResultSet} to read the data from.
-   * @param columnarIndex The columnar index.
+   * @param position The column index.
    * @param format The {@link DataFormat} format to use to encode the data.
    * @return a byte array containing the data in the specified format.
    */
-  public static byte[] parseData(ResultSet result, int columnarIndex, DataFormat format) {
-    Preconditions.checkArgument(
-        !result.isNull(columnarIndex), "Column may not contain a null value");
-    Parser<?> parser = Parser.create(result, result.getColumnType(columnarIndex), columnarIndex);
-    return parser.parse(format);
+  public static byte[] convertToPG(ResultSet result, int position, DataFormat format) {
+    Preconditions.checkArgument(!result.isNull(position), "Column may not contain a null value");
+    Type type = result.getColumnType(position);
+    switch (type.getCode()) {
+      case BOOL:
+        return BooleanParser.convertToPG(result, position, format);
+      case BYTES:
+        return BinaryParser.convertToPG(result, position, format);
+      case DATE:
+        return DateParser.convertToPG(result, position, format);
+      case FLOAT64:
+        return DoubleParser.convertToPG(result, position, format);
+      case INT64:
+        return LongParser.convertToPG(result, position, format);
+      case PG_NUMERIC:
+        return NumericParser.convertToPG(result, position, format);
+      case STRING:
+        return StringParser.convertToPG(result, position);
+      case TIMESTAMP:
+        return TimestampParser.convertToPG(result, position, format);
+      case PG_JSONB:
+        return JsonbParser.convertToPG(result, position, format);
+      case ARRAY:
+        ArrayParser arrayParser = new ArrayParser(result, position);
+        return arrayParser.parse(format);
+      case NUMERIC:
+      case JSON:
+      case STRUCT:
+      default:
+        throw new IllegalArgumentException("Illegal or unknown element type: " + type);
+    }
   }
 }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/DataRowResponse.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/DataRowResponse.java
index 1baf6096..8eca0993 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/DataRowResponse.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireoutput/DataRowResponse.java
@@ -55,7 +55,7 @@ public class DataRowResponse extends WireOutput {
         columns.add(null);
       } else {
         DataFormat format = DataFormat.getDataFormat(column_index, statement, mode, options);
-        byte[] column = Converter.parseData(this.resultSet, column_index, format);
+        byte[] column = Converter.convertToPG(this.resultSet, column_index, format);
         length += column.length;
         columns.add(column);
       }


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java
index 09d5c4c2..c4671ab2 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java
@@ -268,7 +268,7 @@ public abstract class AbstractMockServerTest {
   protected static MockSpannerServiceImpl mockSpanner;
   protected static MockDatabaseAdminImpl mockDatabaseAdmin;
   protected static MockInstanceAdminImpl mockInstanceAdmin;
-  private static Server spannerServer;
+  protected static Server spannerServer;
   protected static ProxyServer pgServer;
 
   protected List<WireMessage> getWireMessages() {
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
index 08de4451..fb8824ad 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -22,13 +22,19 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
+import com.google.cloud.NoCredentials;
 import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
 import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
 import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.RandomResultSetGenerator;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.PreparedType;
@@ -46,6 +52,7 @@ import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
 import com.google.spanner.v1.Type;
 import com.google.spanner.v1.TypeAnnotationCode;
 import com.google.spanner.v1.TypeCode;
+import io.grpc.ManagedChannelBuilder;
 import io.grpc.Status;
 import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
@@ -1563,74 +1570,179 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
 
   @Test
   public void testRandomResults() throws SQLException {
-    final int fetchSize = 3;
-    try (Connection connection = DriverManager.getConnection(createUrl())) {
-      connection.setAutoCommit(false);
-      try (PreparedStatement statement = connection.prepareStatement(SELECT_RANDOM.getSql())) {
-        statement.setFetchSize(fetchSize);
-        try (ResultSet resultSet = statement.executeQuery()) {
-          int rowCount = 0;
-          while (resultSet.next()) {
-            for (int col = 0; col < resultSet.getMetaData().getColumnCount(); col++) {
-              // TODO: Remove once we have a replacement for pg_type, as the JDBC driver will try to
-              //       read type information from the backend when it hits an 'unknown' type (jsonb
-              //       is not one of the types that the JDBC driver will load automatically).
-              if (col == 5 || col == 14) {
-                resultSet.getString(col + 1);
-              } else {
-                resultSet.getObject(col + 1);
+    for (boolean binary : new boolean[] {false, true}) {
+      // Also get the random results using the normal Spanner client to compare the results with
+      // what is returned by PGAdapter.
+      Spanner spanner =
+          SpannerOptions.newBuilder()
+              .setProjectId("p")
+              .setHost(String.format("http://localhost:%d", spannerServer.getPort()))
+              .setCredentials(NoCredentials.getInstance())
+              .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
+              .setClientLibToken("pg-adapter")
+              .build()
+              .getService();
+      DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
+      com.google.cloud.spanner.ResultSet spannerResult =
+          client.singleUse().executeQuery(SELECT_RANDOM);
+
+      String binaryTransferEnable =
+          "&binaryTransferEnable="
+              + ImmutableList.of(
+                      Oid.BOOL,
+                      Oid.BYTEA,
+                      Oid.VARCHAR,
+                      Oid.NUMERIC,
+                      Oid.FLOAT8,
+                      Oid.INT8,
+                      Oid.DATE,
+                      Oid.TIMESTAMPTZ)
+                  .stream()
+                  .map(String::valueOf)
+                  .collect(Collectors.joining(","));
+
+      final int fetchSize = 3;
+      try (Connection connection =
+          DriverManager.getConnection(createUrl() + binaryTransferEnable)) {
+        connection.setAutoCommit(false);
+        connection.unwrap(PGConnection.class).setPrepareThreshold(binary ? -1 : 5);
+        try (PreparedStatement statement = connection.prepareStatement(SELECT_RANDOM.getSql())) {
+          statement.setFetchSize(fetchSize);
+          try (ResultSet resultSet = statement.executeQuery()) {
+            int rowCount = 0;
+            while (resultSet.next()) {
+              assertTrue(spannerResult.next());
+              for (int col = 0; col < resultSet.getMetaData().getColumnCount(); col++) {
+                // TODO: Remove once we have a replacement for pg_type, as the JDBC driver will try
+                // to read type information from the backend when it hits an 'unknown' type (jsonb
+                // is not one of the types that the JDBC driver will load automatically).
+                if (col == 5 || col == 14) {
+                  resultSet.getString(col + 1);
+                } else {
+                  resultSet.getObject(col + 1);
+                }
               }
+              assertEqual(spannerResult, resultSet, binary);
+              rowCount++;
             }
-            rowCount++;
+            assertEquals(RANDOM_RESULTS_ROW_COUNT, rowCount);
           }
-          assertEquals(RANDOM_RESULTS_ROW_COUNT, rowCount);
         }
+        connection.commit();
       }
-      connection.commit();
-    }
-    // The ExecuteSql request should only be sent once to Cloud Spanner.
-    assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
-    ExecuteSqlRequest executeRequest =
-        mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
-    assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
-    assertEquals(SELECT_RANDOM.getSql(), executeRequest.getSql());
 
-    // PGAdapter should receive 5 Execute messages:
-    // 1. BEGIN
-    // 2. Execute - fetch rows 1, 2
-    // 3. Execute - fetch rows 3, 4
-    // 4. Execute - fetch rows 5
-    // 5. COMMIT
-    if (pgServer != null) {
-      List<DescribeMessage> describeMessages = getWireMessagesOfType(DescribeMessage.class);
-      assertEquals(1, describeMessages.size());
-      DescribeMessage describeMessage = describeMessages.get(0);
-      assertEquals(PreparedType.Portal, describeMessage.getType());
+      // Close the resources used by the normal Spanner client.
+      spannerResult.close();
+      spanner.close();
 
-      List<ExecuteMessage> executeMessages =
-          getWireMessagesOfType(ExecuteMessage.class).stream()
-              .filter(message -> !JDBC_STARTUP_STATEMENTS.contains(message.getSql()))
-              .collect(Collectors.toList());
-      int expectedExecuteMessageCount =
-          RANDOM_RESULTS_ROW_COUNT / fetchSize
-              + ((RANDOM_RESULTS_ROW_COUNT % fetchSize) > 0 ? 1 : 0)
-              + 2;
-      assertEquals(expectedExecuteMessageCount, executeMessages.size());
-      assertEquals("", executeMessages.get(0).getName());
-      for (ExecuteMessage executeMessage : executeMessages.subList(1, executeMessages.size() - 1)) {
-        assertEquals(describeMessage.getName(), executeMessage.getName());
-        assertEquals(fetchSize, executeMessage.getMaxRows());
+      // The ExecuteSql request should only be sent once to Cloud Spanner by PGAdapter.
+      // The normal Spanner client will also send it once to Spanner.
+      assertEquals(binary ? 3 : 2, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+      ExecuteSqlRequest executeRequest =
+          mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(binary ? 2 : 1);
+      assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
+      assertEquals(SELECT_RANDOM.getSql(), executeRequest.getSql());
+
+      // PGAdapter should receive 5 Execute messages:
+      // 1. BEGIN
+      // 2. Execute - fetch rows 1, 2
+      // 3. Execute - fetch rows 3, 4
+      // 4. Execute - fetch rows 5
+      // 5. COMMIT
+      if (pgServer != null) {
+        List<DescribeMessage> describeMessages = getWireMessagesOfType(DescribeMessage.class);
+        assertEquals(1, describeMessages.size());
+        DescribeMessage describeMessage = describeMessages.get(0);
+        assertEquals(
+            binary ? PreparedType.Statement : PreparedType.Portal, describeMessage.getType());
+
+        List<ExecuteMessage> executeMessages =
+            getWireMessagesOfType(ExecuteMessage.class).stream()
+                .filter(message -> !JDBC_STARTUP_STATEMENTS.contains(message.getSql()))
+                .collect(Collectors.toList());
+        int expectedExecuteMessageCount =
+            RANDOM_RESULTS_ROW_COUNT / fetchSize
+                + ((RANDOM_RESULTS_ROW_COUNT % fetchSize) > 0 ? 1 : 0)
+                + 2;
+        assertEquals(expectedExecuteMessageCount, executeMessages.size());
+        assertEquals("", executeMessages.get(0).getName());
+        for (ExecuteMessage executeMessage :
+            executeMessages.subList(1, executeMessages.size() - 1)) {
+          assertEquals(fetchSize, executeMessage.getMaxRows());
+        }
+        assertEquals("", executeMessages.get(executeMessages.size() - 1).getName());
+
+        List<ParseMessage> parseMessages =
+            getWireMessagesOfType(ParseMessage.class).stream()
+                .filter(message -> !JDBC_STARTUP_STATEMENTS.contains(message.getSql()))
+                .collect(Collectors.toList());
+        assertEquals(3, parseMessages.size());
+        assertEquals("BEGIN", parseMessages.get(0).getStatement().getSql());
+        assertEquals(SELECT_RANDOM.getSql(), parseMessages.get(1).getStatement().getSql());
+        assertEquals("COMMIT", parseMessages.get(2).getStatement().getSql());
       }
-      assertEquals("", executeMessages.get(executeMessages.size() - 1).getName());
+      mockSpanner.clearRequests();
+      pgServer.clearDebugMessages();
+    }
+  }
 
-      List<ParseMessage> parseMessages =
-          getWireMessagesOfType(ParseMessage.class).stream()
-              .filter(message -> !JDBC_STARTUP_STATEMENTS.contains(message.getSql()))
-              .collect(Collectors.toList());
-      assertEquals(3, parseMessages.size());
-      assertEquals("BEGIN", parseMessages.get(0).getStatement().getSql());
-      assertEquals(SELECT_RANDOM.getSql(), parseMessages.get(1).getStatement().getSql());
-      assertEquals("COMMIT", parseMessages.get(2).getStatement().getSql());
+  private void assertEqual(
+      com.google.cloud.spanner.ResultSet spannerResult, ResultSet pgResult, boolean binary)
+      throws SQLException {
+    assertEquals(spannerResult.getColumnCount(), pgResult.getMetaData().getColumnCount());
+    for (int col = 0; col < spannerResult.getColumnCount(); col++) {
+      if (spannerResult.isNull(col)) {
+        assertNull(pgResult.getObject(col + 1));
+        assertTrue(pgResult.wasNull());
+        continue;
+      }
+
+      switch (spannerResult.getColumnType(col).getCode()) {
+        case BOOL:
+          if (!binary) {
+            // Skip for binary for now, as there is a bug in the PG JDBC driver for decoding binary
+            // bool values.
+            assertEquals(spannerResult.getBoolean(col), pgResult.getBoolean(col + 1));
+          }
+          break;
+        case INT64:
+          assertEquals(spannerResult.getLong(col), pgResult.getLong(col + 1));
+          break;
+        case FLOAT64:
+          assertEquals(spannerResult.getDouble(col), pgResult.getDouble(col + 1), 0.0d);
+          break;
+        case PG_NUMERIC:
+        case STRING:
+          assertEquals(spannerResult.getString(col), pgResult.getString(col + 1));
+          break;
+        case BYTES:
+          assertArrayEquals(spannerResult.getBytes(col).toByteArray(), pgResult.getBytes(col + 1));
+          break;
+        case TIMESTAMP:
+          // Compare milliseconds, as PostgreSQL does not natively support nanosecond precision, and
+          // this is lost when using binary encoding.
+          assertEquals(
+              spannerResult.getTimestamp(col).toSqlTimestamp().getTime(),
+              pgResult.getTimestamp(col + 1).getTime());
+          break;
+        case DATE:
+          assertEquals(
+              LocalDate.of(
+                  spannerResult.getDate(col).getYear(),
+                  spannerResult.getDate(col).getMonth(),
+                  spannerResult.getDate(col).getDayOfMonth()),
+              pgResult.getDate(col + 1).toLocalDate());
+          break;
+        case PG_JSONB:
+          assertEquals(spannerResult.getPgJsonb(col), pgResult.getString(col + 1));
+          break;
+        case ARRAY:
+          break;
+        case NUMERIC:
+        case JSON:
+        case STRUCT:
+          fail("unsupported PG type: " + spannerResult.getColumnType(col));
+      }
     }
   }
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java
index 270ff1bc..6b03bf3a 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ParserTest.java
@@ -411,12 +411,14 @@ public class ParserTest {
   @Test
   public void testNumericParsingNaN() {
     String value = "NaN";
+    byte[] byteResult = new byte[8];
+    ByteConverter.int2(byteResult, 4, (short) 0xC000);
 
     byte[] stringResult = {'N', 'a', 'N'};
 
     NumericParser parser = new NumericParser(value);
 
-    validate(parser, stringResult, stringResult, stringResult);
+    validate(parser, byteResult, stringResult, stringResult);
     assertEquals(value, parser.getItem());
     validateCreateText(stringResult, Oid.NUMERIC, value);
   }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java
index 5670296e..f169aa64 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParserTest.java
@@ -22,9 +22,16 @@ import static org.junit.Assert.assertThrows;
 
 import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.ResultSets;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.Type;
+import com.google.cloud.spanner.Type.StructField;
+import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import com.google.common.collect.ImmutableList;
 import java.nio.charset.StandardCharsets;
 import java.util.Random;
 import org.junit.Test;
@@ -64,6 +71,19 @@ public class TimestampParserTest {
                 FormatCode.TEXT)
             .spannerParse());
     assertNull(new TimestampParser(null).spannerParse());
+
+    ResultSet resultSet =
+        ResultSets.forRows(
+            Type.struct(StructField.of("ts", Type.timestamp())),
+            ImmutableList.of(
+                Struct.newBuilder()
+                    .set("ts")
+                    .to(Timestamp.parseTimestamp("2022-07-08T07:22:59.123456789Z"))
+                    .build()));
+    resultSet.next();
+    assertArrayEquals(
+        "2022-07-08T07:22:59.123456789Z".getBytes(StandardCharsets.UTF_8),
+        TimestampParser.convertToPG(resultSet, 0, DataFormat.SPANNER));
   }
 
   @Test