Repository: GoogleCloudPlatform/pgadapter
Pull request #571 (2023-01-03 13:27:15)

build: schedule a ycsb benchmark run daily
This will run the YCSB benchmarks once every day using a nightly build of PGAdapter.

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/571?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#571](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/571?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (420c9a5) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/9b0573e64ff66346fde2a26dd989359d6d32a019?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (9b0573e) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@                  Coverage Diff                  @@
##             postgresql-dialect     #571   +/-   ##
=====================================================
  Coverage                 88.41%   88.41%           
  Complexity                 2069     2069           
=====================================================
  Files                       122      122           
  Lines                      6791     6791           
  Branches                    956      956           
=====================================================
  Hits                       6004     6004           
  Misses                      558      558           
  Partials                    229      229           
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `88.41% <ø> (ø)` | |

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.


: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/.github/workflows/ycsb.yaml b/.github/workflows/ycsb.yaml
index 5c1e44ff..17b6d1a6 100644
--- a/.github/workflows/ycsb.yaml
+++ b/.github/workflows/ycsb.yaml
@@ -1,5 +1,8 @@
 on:
   workflow_dispatch:
+  schedule:
+    # Run at 05:36UTC every day.
+    - cron: '36 5 * * *'
 name: ycsb
 jobs:
   check-env:
@@ -18,12 +21,16 @@ jobs:
     timeout-minutes: 60
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - name: Setup GCloud
-        uses: google-github-actions/setup-gcloud@v0
+      - name: Checkout
+        uses: actions/checkout@v3
+      - id: 'auth'
+        uses: 'google-github-actions/auth@v1'
+        with:
+          credentials_json: '${{ secrets.CLOUD_SPANNER_PG_ADAPTER_SERVICE_ACCOUNT }}'
+      - name: 'Set up Cloud SDK'
+        uses: 'google-github-actions/setup-gcloud@v0'
         with:
-          project_id: ${{ secrets.GCP_PROJECT_ID }}
-          service_account_key: ${{ secrets.JSON_SERVICE_ACCOUNT_CREDENTIALS }}
           export_default_credentials: true
+          install_components: 'beta'
       - name: Create and start ycsb job
         run: ./benchmarks/ycsb/create-and-run-ycsb-job.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9a601e3..6b638710 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Changelog
 
+## [0.14.1](https://github.com/GoogleCloudPlatform/pgadapter/compare/v0.14.0...v0.14.1) (2023-01-06)
+
+
+### Bug Fixes
+
+* enable gracefully stopping Docker container ([#554](https://github.com/GoogleCloudPlatform/pgadapter/issues/554)) ([80e9c8b](https://github.com/GoogleCloudPlatform/pgadapter/commit/80e9c8b988a168b30d44335b5de3c09e57c18d76))
+
+
+### Performance Improvements
+
+* reduce conversion time for timestamptz, date and bytea ([#572](https://github.com/GoogleCloudPlatform/pgadapter/issues/572)) ([bb73990](https://github.com/GoogleCloudPlatform/pgadapter/commit/bb739901bd875a7c706377dc065c1936db56e5d7))
+
 ## [0.14.0](https://github.com/GoogleCloudPlatform/pgadapter/compare/v0.13.1...v0.14.0) (2022-12-16)
 
 
diff --git a/README.md b/README.md
index b15ba634..08c0e5d2 100644
--- a/README.md
+++ b/README.md
@@ -63,9 +63,9 @@ Use the `-s` option to specify a different local port than the default 5432 if y
 PostgreSQL running on your local system.
 
 <!--- {x-version-update-start:google-cloud-spanner-pgadapter:released} -->
-You can also download a specific version of the jar. Example (replace `v0.14.0` with the version you want to download):
+You can also download a specific version of the jar. Example (replace `v0.14.1` with the version you want to download):
 ```shell
-VERSION=v0.14.0
+VERSION=v0.14.1
 wget https://storage.googleapis.com/pgadapter-jar-releases/pgadapter-${VERSION}.tar.gz \
   && tar -xzvf pgadapter-${VERSION}.tar.gz
 java -jar pgadapter.jar -p my-project -i my-instance -d my-database
@@ -100,7 +100,7 @@ This option is only available for Java/JVM-based applications.
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-spanner-pgadapter</artifactId>
-  <version>0.14.0</version>
+  <version>0.14.1</version>
 </dependency>
 <!-- [END pgadapter_dependency] -->
 ```
diff --git a/benchmarks/ycsb/run.sh b/benchmarks/ycsb/run.sh
index 25b4a497..dfeb48e0 100644
--- a/benchmarks/ycsb/run.sh
+++ b/benchmarks/ycsb/run.sh
@@ -9,7 +9,7 @@ if [[ "$DATABASES" != *"$SPANNER_DATABASE"* ]]; then
   gcloud spanner databases create $SPANNER_DATABASE --instance=$SPANNER_INSTANCE --database-dialect='POSTGRESQL'
 fi
 
-java -jar pgadapter.jar -p $(gcloud --quiet config get project) -i $SPANNER_INSTANCE &
+java -jar pgadapter.jar -p $(gcloud --quiet config get project) -i $SPANNER_INSTANCE -r="minSessions=1000;maxSessions=1000;numChannels=20" &
 sleep 6
 export PGDATABASE=$SPANNER_DATABASE
 psql -h localhost -c "CREATE TABLE IF NOT EXISTS usertable (
diff --git a/build/Dockerfile b/build/Dockerfile
index 4884a263..69ca38f8 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -2,7 +2,7 @@
 #                                     BUILD                                    #
 ################################################################################
 
-FROM maven:3.8.4-eclipse-temurin-17-alpine AS build
+FROM maven:3.8.6-eclipse-temurin-17 AS build
 
 # Copy over build files to docker image.
 COPY LICENSE ./
diff --git a/build/startup.sh b/build/startup.sh
index 73c7ce3e..9fa27686 100755
--- a/build/startup.sh
+++ b/build/startup.sh
@@ -15,4 +15,4 @@ done
 cd /home/pgadapter
 COMMAND="java ${JAVA_ARGUMENTS} -jar pgadapter.jar ${ARGUMENTS}"
 echo $COMMAND
-eval " $COMMAND"
+exec $COMMAND
diff --git a/pom.xml b/pom.xml
index b4634fe2..320feb1e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,7 +40,7 @@
 
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-spanner-pgadapter</artifactId>
-  <version>0.14.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-pgadapter:current} -->
+  <version>0.14.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-pgadapter:current} -->
   <name>Google Cloud Spanner PostgreSQL Adapter</name>
   <packaging>jar</packaging>
   <description>
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 f01a60c6..1be85a8d 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
@@ -22,7 +22,6 @@ 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.core.Utils;
 import org.postgresql.util.PGbytea;
 
 /**
@@ -67,7 +66,21 @@ public class BinaryParser extends Parser<ByteArray> {
 
   @Override
   public String stringParse() {
-    return this.item == null ? null : "\\x" + Utils.toHexString(this.item.toByteArray());
+    return this.item == null ? null : bytesToHex(this.item.toByteArray());
+  }
+
+  private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
+
+  static String bytesToHex(byte[] bytes) {
+    char[] hexChars = new char[bytes.length * 2 + 2];
+    hexChars[0] = '\\';
+    hexChars[1] = 'x';
+    for (int j = 0; j < bytes.length; j++) {
+      int v = bytes[j] & 0xFF;
+      hexChars[j * 2 + 2] = HEX_ARRAY[v >>> 4];
+      hexChars[j * 2 + 3] = HEX_ARRAY[v & 0x0F];
+    }
+    return new String(hexChars);
   }
 
   @Override
@@ -86,7 +99,7 @@ public class BinaryParser extends Parser<ByteArray> {
       case POSTGRESQL_BINARY:
         return resultSet.getBytes(position).toByteArray();
       case POSTGRESQL_TEXT:
-        return ("\\x" + Utils.toHexString(resultSet.getBytes(position).toByteArray()))
+        return (bytesToHex(resultSet.getBytes(position).toByteArray()))
             .getBytes(StandardCharsets.UTF_8);
       default:
         throw new IllegalArgumentException("unknown data format: " + format);
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 489a2069..aa01c0df 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
@@ -22,9 +22,7 @@ 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;
 import javax.annotation.Nonnull;
@@ -87,28 +85,34 @@ public class DateParser extends Parser<Date> {
         localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth());
   }
 
-  /**
-   * Checks whether the given text contains a date that can be parsed by PostgreSQL.
-   *
-   * @param value The value to check. May not be <code>null</code>.
-   * @return <code>true</code> if the text contains a valid date.
-   */
-  static boolean isDate(String value) {
-    Preconditions.checkNotNull(value);
-    for (SimpleDateFormat dateFormat : VALID_DATE_FORMATS) {
-      try {
-        dateFormat.parse(value);
-        return true;
-      } catch (ParseException e) {
-        // ignore and try the next
-      }
-    }
-    return false;
-  }
-
   @Override
   public String stringParse() {
-    return this.item == null ? null : item.toString();
+    return this.item == null ? null : toString(this.item);
+  }
+
+  static String toString(Date date) {
+    int yearValue = date.getYear();
+    int monthValue = date.getMonth();
+    int dayValue = date.getDayOfMonth();
+    StringBuilder buf = new StringBuilder(10);
+    if (yearValue < 1000) {
+      // Note that Date cannot contain negative year numbers, so this value is always guaranteed to
+      // be between 1 and 999 (inclusive).
+      buf.append(yearValue + 10000).deleteCharAt(0);
+    } else {
+      // Year values beyond 9999 are not supported by Cloud Spanner, but it is allowed by the Date
+      // class. The ISO specification requires year numbers that have more than 4 digits to have a
+      // plus or minus sign.
+      if (yearValue > 9999) {
+        buf.append('+');
+      }
+      buf.append(yearValue);
+    }
+    return buf.append(monthValue < 10 ? "-0" : "-")
+        .append(monthValue)
+        .append(dayValue < 10 ? "-0" : "-")
+        .append(dayValue)
+        .toString();
   }
 
   @Override
@@ -130,7 +134,7 @@ public class DateParser extends Parser<Date> {
     switch (format) {
       case SPANNER:
       case POSTGRESQL_TEXT:
-        return resultSet.getDate(position).toString().getBytes(StandardCharsets.UTF_8);
+        return toString(resultSet.getDate(position)).getBytes(StandardCharsets.UTF_8);
       case POSTGRESQL_BINARY:
         return convertToPG(resultSet.getDate(position));
       default:
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 b013abcf..0db60f15 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
@@ -24,7 +24,6 @@ import com.google.cloud.spanner.pgadapter.ProxyServer.DataFormat;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
-import com.google.common.base.Preconditions;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.time.OffsetDateTime;
@@ -33,7 +32,6 @@ import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAccessor;
-import java.util.regex.Pattern;
 import javax.annotation.Nonnull;
 import org.postgresql.util.ByteConverter;
 
@@ -48,17 +46,6 @@ public class TimestampParser extends Parser<Timestamp> {
   private static final String ZERO_TIMEZONE = "Z";
   private static final String PG_ZERO_TIMEZONE = "+00";
 
-  /** Regular expression for parsing timestamps. */
-  private static final String TIMESTAMP_REGEX =
-      // yyyy-MM-dd
-      "(\\d{4})-(\\d{2})-(\\d{2})"
-          // ' 'HH:mm:ss.milliseconds
-          + "( (\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?)"
-          // 'Z' or time zone shift HH:mm following '+' or '-'
-          + "([Zz]|([+-])(\\d{2})(:(\\d{2}))?)?";
-
-  private static final Pattern TIMESTAMP_PATTERN = Pattern.compile(TIMESTAMP_REGEX);
-
   private static final DateTimeFormatter TIMESTAMP_OUTPUT_FORMATTER =
       new DateTimeFormatterBuilder()
           .parseLenient()
@@ -135,17 +122,6 @@ public class TimestampParser extends Parser<Timestamp> {
     }
   }
 
-  /**
-   * Checks whether the given text contains a timestamp that can be parsed by PostgreSQL.
-   *
-   * @param value The value to check. May not be <code>null</code>.
-   * @return <code>true</code> if the text contains a valid timestamp.
-   */
-  static boolean isTimestamp(String value) {
-    Preconditions.checkNotNull(value);
-    return TIMESTAMP_PATTERN.matcher(toPGString(value)).matches();
-  }
-
   @Override
   public String stringParse() {
     return this.item == null ? null : toPGString(this.item, sessionState.getTimezone());
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
index 74ec2701..4692e590 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
@@ -231,6 +231,7 @@ public class SessionState {
 
   private void internalSet(
       String extension, String name, String setting, Map<String, PGSetting> currentSettings) {
+    clearCachedValues();
     String key = toKey(extension, name);
     PGSetting newSetting = currentSettings.get(key);
     if (newSetting == null) {
@@ -252,6 +253,10 @@ public class SessionState {
     currentSettings.put(key, newSetting);
   }
 
+  private void clearCachedValues() {
+    cachedZoneId = null;
+  }
+
   /** Returns the current value of the specified setting. */
   public PGSetting get(String extension, String name) {
     return internalGet(toKey(extension, name), true);
@@ -403,8 +408,13 @@ public class SessionState {
         () -> DdlTransactionMode.valueOf(setting.getBootVal()));
   }
 
+  private ZoneId cachedZoneId;
+
   /** Returns the {@link ZoneId} of the current timezone for this session. */
   public ZoneId getTimezone() {
+    if (cachedZoneId != null) {
+      return cachedZoneId;
+    }
     PGSetting setting = internalGet(toKey(null, "timezone"), false);
     if (setting == null) {
       return ZoneId.systemDefault();
@@ -416,7 +426,7 @@ public class SessionState {
             setting::getResetVal,
             setting::getBootVal);
 
-    return zoneIdFromString(id);
+    return (cachedZoneId = zoneIdFromString(id));
   }
 
   private ZoneId zoneIdFromString(String value) {
diff --git a/versions.txt b/versions.txt
index 83771bf5..705f67c0 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-spanner-pgadapter:0.14.0:0.14.1-SNAPSHOT
+google-cloud-spanner-pgadapter:0.14.1:0.14.2-SNAPSHOT


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
index 59ad5586..5171e579 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
@@ -26,6 +26,8 @@ import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement;
 import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Value;
 import com.google.rpc.ResourceInfo;
 import com.google.spanner.admin.database.v1.Database;
 import com.google.spanner.admin.database.v1.DatabaseDialect;
@@ -37,6 +39,7 @@ import com.google.spanner.v1.DatabaseName;
 import com.google.spanner.v1.ExecuteBatchDmlRequest;
 import com.google.spanner.v1.ExecuteSqlRequest;
 import com.google.spanner.v1.SessionName;
+import com.google.spanner.v1.TypeCode;
 import io.grpc.Metadata;
 import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
@@ -307,6 +310,51 @@ public class EmulatedPsqlMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testTimezone() throws SQLException {
+    String sql = "select ts from foo";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(sql),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(createMetadata(ImmutableList.of(TypeCode.TIMESTAMP)))
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(
+                            Value.newBuilder()
+                                .setStringValue("2023-01-06T11:49:15.123456789Z")
+                                .build())
+                        .build())
+                .build()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl("my-db"))) {
+      connection.createStatement().execute("set time zone cet");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        while (resultSet.next()) {
+          assertEquals("2023-01-06 12:49:15.123456+01", resultSet.getString(1));
+        }
+      }
+      connection.createStatement().execute("set time zone ist");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        while (resultSet.next()) {
+          assertEquals("2023-01-06 17:19:15.123456+05:30", resultSet.getString(1));
+        }
+      }
+      connection.createStatement().execute("set time zone 'America/Los_Angeles'");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        while (resultSet.next()) {
+          assertEquals("2023-01-06 03:49:15.123456-08", resultSet.getString(1));
+        }
+      }
+      connection.createStatement().execute("set time zone -12");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        while (resultSet.next()) {
+          assertEquals("2023-01-05 23:49:15.123456-12", resultSet.getString(1));
+        }
+      }
+    }
+  }
+
   static StatusRuntimeException newStatusResourceNotFoundException(
       String shortName, String resourceType, String resourceName) {
     ResourceInfo resourceInfo =
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 78604fce..078843c0 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -42,6 +42,7 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.PreparedTy
 import com.google.cloud.spanner.pgadapter.wireprotocol.DescribeMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ExecuteMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage;
+import com.google.common.base.Stopwatch;
 import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ListValue;
 import com.google.protobuf.Value;
@@ -77,9 +78,11 @@ import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -3368,6 +3371,29 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
   }
 
+  @Ignore("Only used for manual performance testing")
+  @Test
+  public void testBasePerformance() throws SQLException {
+    final int numRuns = 1000;
+    String sql = "select * from random_benchmark";
+    RandomResultSetGenerator generator = new RandomResultSetGenerator(10, Dialect.POSTGRESQL);
+    for (int run = 0; run < numRuns; run++) {
+      mockSpanner.putStatementResult(
+          StatementResult.query(Statement.of(sql + run), generator.generate()));
+    }
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      Stopwatch watch = Stopwatch.createStarted();
+      for (int run = 0; run < numRuns; run++) {
+        try (ResultSet resultSet = connection.createStatement().executeQuery(sql + run)) {
+          while (resultSet.next()) {
+            // ignore
+          }
+        }
+      }
+      System.out.printf("Elapsed: %dms\n", watch.elapsed(TimeUnit.MILLISECONDS));
+    }
+  }
+
   private void verifySettingIsNull(Connection connection, String setting) throws SQLException {
     try (ResultSet resultSet =
         connection.createStatement().executeQuery(String.format("show %s", setting))) {
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DateParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DateParserTest.java
index e01399f4..4bed704b 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DateParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DateParserTest.java
@@ -49,4 +49,13 @@ public class DateParserTest {
         PGException.class,
         () -> new DateParser("2022".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
+
+  @Test
+  public void testToString() {
+    assertEquals("2023-01-06", DateParser.toString(Date.fromYearMonthDay(2023, 1, 6)));
+    assertEquals("0900-01-06", DateParser.toString(Date.fromYearMonthDay(900, 1, 6)));
+    assertEquals("0016-02-28", DateParser.toString(Date.fromYearMonthDay(16, 2, 28)));
+    assertEquals("0001-01-01", DateParser.toString(Date.fromYearMonthDay(1, 1, 1)));
+    assertEquals("+10000-01-01", DateParser.toString(Date.fromYearMonthDay(10000, 1, 1)));
+  }
 }
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 a11e0be3..ac9c51f2 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
@@ -17,10 +17,8 @@ package com.google.cloud.spanner.pgadapter.parsers;
 import static com.google.cloud.spanner.pgadapter.parsers.Parser.toOid;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -213,20 +211,6 @@ public class ParserTest {
     new DateParser(result, FormatCode.BINARY);
   }
 
-  @Test
-  public void testDateParsingDateValidityChecks() {
-    assertTrue(DateParser.isDate("1998-09-01 00:00"));
-    assertTrue(DateParser.isDate("1998-09-01 +00:00"));
-    assertTrue(DateParser.isDate("1998-09-01 -00:00"));
-    assertFalse(DateParser.isDate("This is not a date right?"));
-    assertFalse(DateParser.isDate("1998-09-01 *00:00"));
-    assertFalse(DateParser.isDate("1998-a-01 00:00"));
-    assertFalse(DateParser.isDate("1998-01-a 00:00"));
-    assertFalse(DateParser.isDate("1998-01-01 a:00"));
-    assertFalse(DateParser.isDate("1998-01-01 00:a"));
-    assertFalse(DateParser.isDate("1998-01 00:00"));
-  }
-
   @Test
   public void testStringParsing() {
     String value = "This is a String.";
@@ -254,34 +238,6 @@ public class ParserTest {
     validateCreateBinary(byteResult, Oid.TIMESTAMP, value);
   }
 
-  @Test
-  public void testTimestampParsingTimestampValidityChecks() {
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00"));
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00.0"));
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000"));
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000Z"));
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000z"));
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000+00"));
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000+00:00"));
-    assertTrue(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000-00:00"));
-    assertFalse(TimestampParser.isTimestamp("This is not a timestamp right?"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000z00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000z00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000+"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000-"));
-    assertFalse(TimestampParser.isTimestamp("99999-09-01 00:00:00.000000000+00:00"));
-    assertFalse(TimestampParser.isTimestamp("aaaa-09-01 00:00:00.000000000+00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-aa-01 00:00:00.000000000+00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-aa 00:00:00.000000000+00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 aa:00:00.000000000+00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:aa:00.000000000+00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:aa.000000000+00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.a+00:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000+aa:00"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000+00:aa"));
-    assertFalse(TimestampParser.isTimestamp("1998-09-01 00:00:00.000000000*00:00"));
-  }
-
   @Test
   public void testBinaryParsing() {
     ByteArray value = ByteArray.copyFrom(new byte[] {(byte) 0b01010101, (byte) 0b10101010});