Repository: GoogleCloudPlatform/pgadapter
Pull request #319 (2022-08-17 10:01:19)

fix: allow 'T' in timestamp values in COPY operations
COPY operations used the `TimestampUtils` from the PG JDBC driver. This
class does not accept timestamp values that contain a 'T' as a separator
between the date and time value. Instead, we'll rely on our own
Timestamp parser that we already have in place for wire-protocol
conversion.

Fixes b/240602999

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/319?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#319](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/319?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (268931d) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/dc32af4a7cd6b4546292c2936b805a635acf8bf1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (dc32af4) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #319      +/-   ##
========================================================
- Coverage                 83.04%   83.03%   -0.02%     
- Complexity                 1453     1454       +1     
========================================================
  Files                       113      113              
  Lines                      5085     5081       -4     
  Branches                    657      657              
========================================================
- Hits                       4223     4219       -4     
  Misses                      650      650              
  Partials                    212      212              
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `83.03% <100.00%> (-0.02%)` | :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/319?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...le/cloud/spanner/pgadapter/utils/CopyInParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/319/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL0NvcHlJblBhcnNlci5qYXZh) | `50.00% <ø> (-10.00%)` | :arrow_down: |
| [...cloud/spanner/pgadapter/parsers/BooleanParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/319/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvQm9vbGVhblBhcnNlci5qYXZh) | `87.17% <100.00%> (ø)` | |
| [...oud/spanner/pgadapter/parsers/TimestampParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/319/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3BhcnNlcnMvVGltZXN0YW1wUGFyc2VyLmphdmE=) | `90.00% <100.00%> (ø)` | |
| [...e/cloud/spanner/pgadapter/utils/CsvCopyParser.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/319/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL0NzdkNvcHlQYXJzZXIuamF2YQ==) | `100.00% <100.00%> (ø)` | |

: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/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java
index 6d6f6941..f5b013fd 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
@@ -37,7 +37,7 @@ public class BooleanParser extends Parser<Boolean> {
   private static final Set<String> TRUE_VALUES =
       ImmutableSet.of("t", "tr", "tru", "true", "y", "ye", "yes", "on", "1");
   private static final Set<String> FALSE_VALUES =
-      ImmutableSet.of("f", "fa", "fal", "fals", "false", "n", "no", "of", "off");
+      ImmutableSet.of("f", "fa", "fal", "fals", "false", "n", "no", "of", "off", "0");
 
   BooleanParser(ResultSet item, int position) {
     this.item = item.getBoolean(position);
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 69ce0930..c2a77ac9 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
@@ -57,7 +57,7 @@ public class TimestampParser extends Parser<Timestamp> {
           .parseLenient()
           .parseCaseInsensitive()
           .appendPattern("yyyy-MM-dd HH:mm:ss")
-          .appendFraction(ChronoField.MICRO_OF_SECOND, 0, 9, true)
+          .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
           .appendOffset("+HH:mm", "Z")
           .toFormatter();
 
@@ -73,12 +73,7 @@ public class TimestampParser extends Parser<Timestamp> {
     if (item != null) {
       switch (formatCode) {
         case TEXT:
-          String stringValue = toPGString(new String(item, StandardCharsets.UTF_8));
-          TemporalAccessor temporalAccessor = TIMESTAMP_FORMATTER.parse(stringValue);
-          this.item =
-              Timestamp.ofTimeSecondsAndNanos(
-                  temporalAccessor.getLong(ChronoField.INSTANT_SECONDS),
-                  temporalAccessor.get(ChronoField.NANO_OF_SECOND));
+          this.item = toTimestamp(new String(item, StandardCharsets.UTF_8));
           break;
         case BINARY:
           this.item = toTimestamp(item);
@@ -102,6 +97,15 @@ public class TimestampParser extends Parser<Timestamp> {
     return Timestamp.ofTimeSecondsAndNanos(javaSeconds, javaNanos);
   }
 
+  /** Converts the given string value to a {@link Timestamp}. */
+  public static Timestamp toTimestamp(String value) {
+    String stringValue = toPGString(value);
+    TemporalAccessor temporalAccessor = TIMESTAMP_FORMATTER.parse(stringValue);
+    return Timestamp.ofTimeSecondsAndNanos(
+        temporalAccessor.getLong(ChronoField.INSTANT_SECONDS),
+        temporalAccessor.get(ChronoField.NANO_OF_SECOND));
+  }
+
   /**
    * Checks whether the given text contains a timestamp that can be parsed by PostgreSQL.
    *
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/CopyInParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/CopyInParser.java
index 8a701a30..b3072d7b 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/CopyInParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/CopyInParser.java
@@ -23,7 +23,6 @@ import java.io.PipedOutputStream;
 import java.util.Iterator;
 import javax.annotation.Nullable;
 import org.apache.commons.csv.CSVFormat;
-import org.postgresql.jdbc.TimestampUtils;
 
 /** Common interface for parsers that implement one or more of the PostgreSQL COPY formats. */
 @InternalApi
@@ -51,10 +50,6 @@ public interface CopyInParser {
     }
   }
 
-  static TimestampUtils createDefaultTimestampUtils() {
-    return new TimestampUtils(false, () -> null);
-  }
-
   /** Returns an iterator of COPY records. */
   Iterator<CopyRecord> iterator();
 
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParser.java
index 6db275bf..ba6c19bb 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParser.java
@@ -16,12 +16,14 @@ package com.google.cloud.spanner.pgadapter.utils;
 
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
+import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
+import com.google.cloud.spanner.pgadapter.parsers.TimestampParser;
 import com.google.common.collect.Iterators;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -29,7 +31,6 @@ import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.io.Reader;
 import java.nio.charset.StandardCharsets;
-import java.sql.Timestamp;
 import java.time.format.DateTimeParseException;
 import java.util.Iterator;
 import java.util.logging.Level;
@@ -38,13 +39,11 @@ import org.apache.commons.codec.binary.Hex;
 import org.apache.commons.csv.CSVFormat;
 import org.apache.commons.csv.CSVParser;
 import org.apache.commons.csv.CSVRecord;
-import org.postgresql.jdbc.TimestampUtils;
 
 /** Implementation of {@link CopyInParser} for the TEXT and CSV formats. */
 class CsvCopyParser implements CopyInParser {
   private static final Logger logger = Logger.getLogger(CsvCopyParser.class.getName());
 
-  private final TimestampUtils timestampUtils = CopyInParser.createDefaultTimestampUtils();
   private final CSVFormat format;
   private final PipedOutputStream payload;
   private final int pipeBufferSize;
@@ -64,8 +63,7 @@ class CsvCopyParser implements CopyInParser {
   @Override
   public Iterator<CopyRecord> iterator() {
     return Iterators.transform(
-        parser.iterator(),
-        record -> new CsvCopyRecord(this.timestampUtils, record, this.hasHeader));
+        parser.iterator(), record -> new CsvCopyRecord(record, this.hasHeader));
   }
 
   @Override
@@ -91,12 +89,10 @@ class CsvCopyParser implements CopyInParser {
   }
 
   static class CsvCopyRecord implements CopyRecord {
-    private final TimestampUtils timestampUtils;
     private final CSVRecord record;
     private final boolean hasHeader;
 
-    CsvCopyRecord(TimestampUtils timestampUtils, CSVRecord record, boolean hasHeader) {
-      this.timestampUtils = timestampUtils;
+    CsvCopyRecord(CSVRecord record, boolean hasHeader) {
       this.record = record;
       this.hasHeader = hasHeader;
     }
@@ -114,17 +110,16 @@ class CsvCopyParser implements CopyInParser {
     @Override
     public Value getValue(Type type, String columnName) throws SpannerException {
       String recordValue = record.get(columnName);
-      return getSpannerValue(type, recordValue, this.timestampUtils);
+      return getSpannerValue(type, recordValue);
     }
 
     @Override
     public Value getValue(Type type, int columnIndex) throws SpannerException {
       String recordValue = record.get(columnIndex);
-      return getSpannerValue(type, recordValue, this.timestampUtils);
+      return getSpannerValue(type, recordValue);
     }
 
-    static Value getSpannerValue(Type type, String recordValue, TimestampUtils timestampUtils)
-        throws SpannerException {
+    static Value getSpannerValue(Type type, String recordValue) throws SpannerException {
       try {
         switch (type.getCode()) {
           case STRING:
@@ -152,9 +147,9 @@ class CsvCopyParser implements CopyInParser {
           case DATE:
             return Value.date(recordValue == null ? null : Date.parseDate(recordValue));
           case TIMESTAMP:
-            Timestamp timestamp = timestampUtils.toTimestamp(null, recordValue);
-            return Value.timestamp(
-                timestamp == null ? null : com.google.cloud.Timestamp.of(timestamp));
+            Timestamp timestamp =
+                recordValue == null ? null : TimestampParser.toTimestamp(recordValue);
+            return Value.timestamp(timestamp);
           default:
             SpannerException spannerException =
                 SpannerExceptionFactory.newSpannerException(


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParserTest.java
index 920da9e5..feb47cfd 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParserTest.java
@@ -14,6 +14,7 @@
 
 package com.google.cloud.spanner.pgadapter.parsers;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -61,10 +62,24 @@ public class BooleanParserTest {
     assertThrows(IllegalArgumentException.class, () -> BooleanParser.toBoolean("foo"));
   }
 
+  @Test
+  public void testBinaryParse() {
+    assertArrayEquals(new byte[] {1}, new BooleanParser(Boolean.TRUE).binaryParse());
+    assertArrayEquals(new byte[] {0}, new BooleanParser(Boolean.FALSE).binaryParse());
+    assertNull(new BooleanParser(null).binaryParse());
+  }
+
   @Test
   public void testStringParse() {
     assertEquals("t", new BooleanParser(Boolean.TRUE).stringParse());
     assertEquals("f", new BooleanParser(Boolean.FALSE).stringParse());
     assertNull(new BooleanParser(null).stringParse());
   }
+
+  @Test
+  public void testSpannerParse() {
+    assertEquals("true", new BooleanParser(Boolean.TRUE).spannerParse());
+    assertEquals("false", new BooleanParser(Boolean.FALSE).spannerParse());
+    assertNull(new BooleanParser(null).spannerParse());
+  }
 }
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 21235e0a..8a4c7a79 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
@@ -15,6 +15,7 @@
 package com.google.cloud.spanner.pgadapter.parsers;
 
 import static com.google.cloud.spanner.pgadapter.parsers.Parser.PG_EPOCH_SECONDS;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
@@ -22,6 +23,8 @@ import static org.junit.Assert.assertThrows;
 import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import java.nio.charset.StandardCharsets;
 import java.util.Random;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,6 +49,20 @@ public class TimestampParserTest {
     SpannerException spannerException =
         assertThrows(SpannerException.class, () -> TimestampParser.toTimestamp(new byte[4]));
     assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode());
+
+    assertArrayEquals(data, new TimestampParser(TimestampParser.toTimestamp(data)).binaryParse());
+    assertNull(new TimestampParser(null).binaryParse());
+  }
+
+  @Test
+  public void testSpannerParse() {
+    assertEquals(
+        "2022-07-08T07:22:59.123456789Z",
+        new TimestampParser(
+                "2022-07-08 07:22:59.123456789+00".getBytes(StandardCharsets.UTF_8),
+                FormatCode.TEXT)
+            .spannerParse());
+    assertNull(new TimestampParser(null).spannerParse());
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyInParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyInParserTest.java
index 00e96036..5d772087 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyInParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyInParserTest.java
@@ -14,7 +14,6 @@
 
 package com.google.cloud.spanner.pgadapter.utils;
 
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.cloud.spanner.pgadapter.parsers.copy.CopyTreeParser.CopyOptions.Format;
@@ -50,9 +49,4 @@ public class CopyInParserTest {
         CopyInParser.create(Format.BINARY, null, new PipedOutputStream(), 256, false);
     assertTrue(parser instanceof BinaryCopyParser);
   }
-
-  @Test
-  public void testCreateTimestampUtils() {
-    assertNotNull(CopyInParser.createDefaultTimestampUtils());
-  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParserTest.java
index f6290d69..18876d5c 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/utils/CsvCopyParserTest.java
@@ -14,12 +14,17 @@
 
 package com.google.cloud.spanner.pgadapter.utils;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 
+import com.google.cloud.ByteArray;
+import com.google.cloud.Date;
+import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.StructField;
+import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.pgadapter.utils.CsvCopyParser.CsvCopyRecord;
 import java.io.DataOutputStream;
 import java.io.IOException;
@@ -66,49 +71,151 @@ public class CsvCopyParserTest {
     parser.close();
   }
 
+  @Test
+  public void testGetSpannerValueBool() {
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "t"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "tr"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "tru"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "true"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "1"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "on"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "y"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "ye"));
+    assertEquals(Value.bool(true), CsvCopyRecord.getSpannerValue(Type.bool(), "yes"));
+
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "f"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "fa"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "fal"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "fals"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "false"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "0"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "off"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "of"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "n"));
+    assertEquals(Value.bool(false), CsvCopyRecord.getSpannerValue(Type.bool(), "no"));
+
+    assertEquals(Value.bool(null), CsvCopyRecord.getSpannerValue(Type.bool(), null));
+  }
+
+  @Test
+  public void testGetSpannerValueBytes() {
+    assertEquals(
+        Value.bytes(ByteArray.copyFrom("test")),
+        CsvCopyRecord.getSpannerValue(Type.bytes(), "\\x74657374"));
+    assertEquals(
+        Value.bytes(ByteArray.copyFrom("")), CsvCopyRecord.getSpannerValue(Type.bytes(), "\\x"));
+    assertEquals(Value.bytes(null), CsvCopyRecord.getSpannerValue(Type.bytes(), null));
+  }
+
+  @Test
+  public void testGetSpannerValueInt64() {
+    assertEquals(Value.int64(-1L), CsvCopyRecord.getSpannerValue(Type.int64(), "-1"));
+    assertEquals(Value.int64(1L), CsvCopyRecord.getSpannerValue(Type.int64(), "1"));
+    assertEquals(Value.int64(0L), CsvCopyRecord.getSpannerValue(Type.int64(), "0"));
+    assertEquals(
+        Value.int64(Long.MAX_VALUE),
+        CsvCopyRecord.getSpannerValue(Type.int64(), "9223372036854775807"));
+    assertEquals(
+        Value.int64(Long.MIN_VALUE),
+        CsvCopyRecord.getSpannerValue(Type.int64(), "-9223372036854775808"));
+    assertEquals(Value.int64(null), CsvCopyRecord.getSpannerValue(Type.int64(), null));
+  }
+
+  @Test
+  public void testGetSpannerValueFloat64() {
+    assertEquals(Value.float64(-1.0D), CsvCopyRecord.getSpannerValue(Type.float64(), "-1.0"));
+    assertEquals(Value.float64(0.0D), CsvCopyRecord.getSpannerValue(Type.float64(), "0.0"));
+    assertEquals(Value.float64(1.0D), CsvCopyRecord.getSpannerValue(Type.float64(), "1.0"));
+    assertEquals(Value.float64(null), CsvCopyRecord.getSpannerValue(Type.float64(), null));
+  }
+
+  @Test
+  public void testGetSpannerValueNumeric() {
+    assertEquals(Value.pgNumeric("-1.0"), CsvCopyRecord.getSpannerValue(Type.pgNumeric(), "-1.0"));
+    assertEquals(Value.pgNumeric("0.0"), CsvCopyRecord.getSpannerValue(Type.pgNumeric(), "0.0"));
+    assertEquals(Value.pgNumeric("1.0"), CsvCopyRecord.getSpannerValue(Type.pgNumeric(), "1.0"));
+    assertEquals(Value.pgNumeric(null), CsvCopyRecord.getSpannerValue(Type.pgNumeric(), null));
+  }
+
+  @Test
+  public void testGetSpannerValueString() {
+    assertEquals(Value.string("test"), CsvCopyRecord.getSpannerValue(Type.string(), "test"));
+    assertEquals(Value.string(""), CsvCopyRecord.getSpannerValue(Type.string(), ""));
+    assertEquals(Value.string(null), CsvCopyRecord.getSpannerValue(Type.string(), null));
+  }
+
+  @Test
+  public void testGetSpannerValueDate() {
+    assertEquals(
+        Value.date(Date.parseDate("2022-08-17")),
+        CsvCopyRecord.getSpannerValue(Type.date(), "2022-08-17"));
+    assertEquals(Value.date(null), CsvCopyRecord.getSpannerValue(Type.date(), null));
+  }
+
+  @Test
+  public void testGetSpannerValueTimestamp() {
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2093-08-02T14:53:40.481913Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2093-08-02T14:53:40.481913+00"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17T10:11:12.123456789Z"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17 10:11:12.123456789Z"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17 10:11:12.123456789+00"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17 10:11:12.123456789+00:00"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17T10:11:12.123456789+00"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17T10:11:12.123456789+00:00"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17 12:11:12.123456789+02"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17T12:11:12.123456789+02"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17 08:11:12.123456789-02"));
+    assertEquals(
+        Value.timestamp(Timestamp.parseTimestamp("2022-08-17T10:11:12.123456789Z")),
+        CsvCopyRecord.getSpannerValue(Type.timestamp(), "2022-08-17T08:11:12.123456789-02"));
+    assertEquals(Value.date(null), CsvCopyRecord.getSpannerValue(Type.date(), null));
+  }
+
   @Test
   public void testGetSpannerValue_InvalidBytesValue() {
     assertThrows(
-        SpannerException.class,
-        () ->
-            CsvCopyRecord.getSpannerValue(
-                Type.bytes(), "value", CopyInParser.createDefaultTimestampUtils()));
+        SpannerException.class, () -> CsvCopyRecord.getSpannerValue(Type.bytes(), "value"));
   }
 
   @Test
   public void testGetSpannerValue_InvalidNumberValue() {
     assertThrows(
-        SpannerException.class,
-        () ->
-            CsvCopyRecord.getSpannerValue(
-                Type.int64(), "value", CopyInParser.createDefaultTimestampUtils()));
+        SpannerException.class, () -> CsvCopyRecord.getSpannerValue(Type.int64(), "value"));
   }
 
   @Test
   public void testGetSpannerValue_InvalidBoolValue() {
-    assertThrows(
-        SpannerException.class,
-        () ->
-            CsvCopyRecord.getSpannerValue(
-                Type.bool(), "value", CopyInParser.createDefaultTimestampUtils()));
+    assertThrows(SpannerException.class, () -> CsvCopyRecord.getSpannerValue(Type.bool(), "value"));
   }
 
   @Test
   public void testGetSpannerValue_InvalidDateValue() {
-    assertThrows(
-        SpannerException.class,
-        () ->
-            CsvCopyRecord.getSpannerValue(
-                Type.date(), "value", CopyInParser.createDefaultTimestampUtils()));
+    assertThrows(SpannerException.class, () -> CsvCopyRecord.getSpannerValue(Type.date(), "value"));
   }
 
   @Test
   public void testGetSpannerValue_InvalidTimestampValue() {
     assertThrows(
-        SpannerException.class,
-        () ->
-            CsvCopyRecord.getSpannerValue(
-                Type.timestamp(), "value", CopyInParser.createDefaultTimestampUtils()));
+        SpannerException.class, () -> CsvCopyRecord.getSpannerValue(Type.timestamp(), "value"));
   }
 
   @Test
@@ -117,8 +224,6 @@ public class CsvCopyParserTest {
         SpannerException.class,
         () ->
             CsvCopyRecord.getSpannerValue(
-                Type.struct(StructField.of("f1", Type.string())),
-                "value",
-                CopyInParser.createDefaultTimestampUtils()));
+                Type.struct(StructField.of("f1", Type.string())), "value"));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java
index bb98626b..e6512c23 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java
@@ -14,7 +14,6 @@
 
 package com.google.cloud.spanner.pgadapter.utils;
 
-import static com.google.cloud.spanner.pgadapter.utils.CopyInParser.createDefaultTimestampUtils;
 import static com.google.cloud.spanner.pgadapter.utils.MutationWriter.calculateSize;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -538,9 +537,7 @@ public class MutationWriterTest {
               new StringReader("col\n\\N\n"), CSVFormat.POSTGRESQL_TEXT.withFirstRecordAsHeader());
       CSVRecord record = parser.getRecords().get(0);
 
-      Mutation mutation =
-          mutationWriter.buildMutation(
-              new CsvCopyRecord(createDefaultTimestampUtils(), record, true));
+      Mutation mutation = mutationWriter.buildMutation(new CsvCopyRecord(record, true));
 
       assertEquals(String.format("Type: %s", type), 1, mutation.asMap().size());
     }