Repository: GoogleCloudPlatform/pgadapter
Pull request #332 (2022-08-23 09:18:08)

feat: PREPARE statement support
Add support for:
- `PREPARE` keyword (e.g. `prepare my_statement (bigint, varchar) as select * from my_table where id=$1 and name=$2`)
- `EXECUTE` keyword (e.g. `execute my_statement (1, 'test')`)
- `DEALLOCATE` keyword (e.g. `deallocate my_statement`)

These statements can be used to create and execute prepared statements in clients that normally only use the simple query protocol, such as `psql`.

See also
- PREPARE: https://www.postgresql.org/docs/current/sql-prepare.html
- EXECUTE: https://www.postgresql.org/docs/current/sql-execute.html
- DEALLOCATE: https://www.postgresql.org/docs/current/sql-deallocate.html


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

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #332      +/-   ##
========================================================
- Coverage                 83.36%   82.18%   -1.18%     
- Complexity                 1467     1507      +40     
========================================================
  Files                       113      114       +1     
  Lines                      5085     5294     +209     
  Branches                    657      685      +28     
========================================================
+ Hits                       4239     4351     +112     
- Misses                      635      724      +89     
- Partials                    211      219       +8     
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `82.18% <14.70%> (-1.18%)` | :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/332?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...spanner/pgadapter/statements/PrepareStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvUHJlcGFyZVN0YXRlbWVudC5qYXZh) | `0.00% <0.00%> (ø)` | |
| [...d/spanner/pgadapter/wireprotocol/QueryMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9RdWVyeU1lc3NhZ2UuamF2YQ==) | `100.00% <ø> (ø)` | |
| [...d/spanner/pgadapter/wireprotocol/ParseMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9QYXJzZU1lc3NhZ2UuamF2YQ==) | `77.58% <57.14%> (-3.55%)` | :arrow_down: |
| [...panner/pgadapter/wireprotocol/DescribeMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9EZXNjcmliZU1lc3NhZ2UuamF2YQ==) | `83.58% <91.66%> (-0.80%)` | :arrow_down: |
| [.../cloud/spanner/pgadapter/session/SessionState.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3Nlc3Npb24vU2Vzc2lvblN0YXRlLmphdmE=) | `97.67% <0.00%> (-2.33%)` | :arrow_down: |
| [...ud/spanner/pgadapter/metadata/OptionsMetadata.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL21ldGFkYXRhL09wdGlvbnNNZXRhZGF0YS5qYXZh) | `77.10% <0.00%> (-1.14%)` | :arrow_down: |
| [...panner/pgadapter/statements/BackendConnection.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvQmFja2VuZENvbm5lY3Rpb24uamF2YQ==) | `90.65% <0.00%> (-0.55%)` | :arrow_down: |
| [...gle/cloud/spanner/pgadapter/session/PGSetting.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3Nlc3Npb24vUEdTZXR0aW5nLmphdmE=) | `93.12% <0.00%> (+1.65%)` | :arrow_up: |
| [...spanner/pgadapter/wireprotocol/StartupMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9TdGFydHVwTWVzc2FnZS5qYXZh) | `87.75% <0.00%> (+1.70%)` | :arrow_up: |
| [...ner/pgadapter/statements/SimpleQueryStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/332/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvU2ltcGxlUXVlcnlTdGF0ZW1lbnQuamF2YQ==) | `93.44% <0.00%> (+3.27%)` | :arrow_up: |

: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/ConnectionHandler.java b/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
index 63160374..34bec3bd 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
@@ -57,9 +57,11 @@ import java.net.Socket;
 import java.security.SecureRandom;
 import java.text.MessageFormat;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
@@ -472,7 +474,8 @@ public class ConnectionHandler extends Thread {
 
   public IntermediatePreparedStatement getStatement(String statementName) {
     if (!hasStatement(statementName)) {
-      throw new IllegalStateException("Unregistered statement: " + statementName);
+      throw PGExceptionFactory.newPGException(
+          "prepared statement " + statementName + " does not exist");
     }
     return this.statementsMap.get(statementName);
   }
@@ -483,11 +486,19 @@ public class ConnectionHandler extends Thread {
 
   public void closeStatement(String statementName) {
     if (!hasStatement(statementName)) {
-      throw new IllegalStateException("Unregistered statement: " + statementName);
+      throw PGExceptionFactory.newPGException(
+          "prepared statement " + statementName + " does not exist");
     }
     this.statementsMap.remove(statementName);
   }
 
+  public void closeAllStatements() {
+    Set<String> names = new HashSet<>(this.statementsMap.keySet());
+    for (String statementName : names) {
+      closeStatement(statementName);
+    }
+  }
+
   public boolean hasStatement(String statementName) {
     return this.statementsMap.containsKey(statementName);
   }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java b/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
index 9c561acd..1aa5b00d 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
@@ -270,6 +270,11 @@ public class ProxyServer extends AbstractApiService {
     handler.start();
   }
 
+  /** Returns an immutable copy of the current connection handlers at this server. */
+  ImmutableList<ConnectionHandler> getConnectionHandlers() {
+    return ImmutableList.copyOf(this.handlers);
+  }
+
   /**
    * Saves the handler locally
    *
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 3e649e44..4666a8bc 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,8 +18,8 @@ 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.error.PGExceptionFactory;
 import java.nio.charset.StandardCharsets;
-import java.sql.SQLException;
 import javax.annotation.Nonnull;
 import org.postgresql.core.Utils;
 import org.postgresql.util.PGbytea;
@@ -46,9 +46,9 @@ public class BinaryParser extends Parser<ByteArray> {
           try {
             this.item = ByteArray.copyFrom(PGbytea.toBytes(item));
             break;
-          } catch (SQLException e) {
-            throw new IllegalArgumentException(
-                "Invalid binary value: " + new String(item, StandardCharsets.UTF_8), e);
+          } catch (Exception exception) {
+            throw PGExceptionFactory.newPGException(
+                "Invalid binary value: " + new String(item, StandardCharsets.UTF_8));
           }
         case BINARY:
           this.item = toByteArray(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 f5b013fd..cb276fa8 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,6 +19,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.error.PGExceptionFactory;
 import com.google.common.collect.ImmutableSet;
 import java.util.Locale;
 import java.util.Set;
@@ -79,7 +80,7 @@ public class BooleanParser extends Parser<Boolean> {
     } else if (FALSE_VALUES.contains(value)) {
       return false;
     } else {
-      throw new IllegalArgumentException(value + " is not a valid boolean value");
+      throw PGExceptionFactory.newPGException(value + " is not a valid boolean value");
     }
   }
 
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 98c6f0e0..4d914283 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,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.error.PGExceptionFactory;
 import com.google.common.base.Preconditions;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -57,7 +58,11 @@ public class DateParser extends Parser<Date> {
           String stringValue = new String(item, UTF8);
           // Use the first 10 characters of the date string, as the string might contain a timezone
           // identifier, which is not supported by parseDate(String).
-          this.item = Date.parseDate(stringValue.substring(0, 10));
+          if (stringValue.length() >= 10) {
+            this.item = Date.parseDate(stringValue.substring(0, 10));
+          } else {
+            throw PGExceptionFactory.newPGException("Invalid date value: " + stringValue);
+          }
           break;
         case BINARY:
           this.item = toDate(item);
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 73874f94..e57ce737 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,6 +19,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.error.PGExceptionFactory;
 import javax.annotation.Nonnull;
 import org.postgresql.util.ByteConverter;
 
@@ -38,7 +39,12 @@ public class DoubleParser extends Parser<Double> {
     if (item != null) {
       switch (formatCode) {
         case TEXT:
-          this.item = Double.valueOf(new String(item));
+          String stringValue = new String(item);
+          try {
+            this.item = Double.valueOf(stringValue);
+          } catch (Exception exception) {
+            throw PGExceptionFactory.newPGException("Invalid float8 value: " + stringValue);
+          }
           break;
         case BINARY:
           this.item = toDouble(item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/FloatParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/FloatParser.java
index 346486eb..3a88a503 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/FloatParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/FloatParser.java
@@ -15,6 +15,7 @@
 package com.google.cloud.spanner.pgadapter.parsers;
 
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import org.postgresql.util.ByteConverter;
 
 /** Translate from wire protocol to float. */
@@ -28,7 +29,12 @@ class FloatParser extends Parser<Float> {
     if (item != null) {
       switch (formatCode) {
         case TEXT:
-          this.item = Float.valueOf(new String(item));
+          String stringValue = new String(item);
+          try {
+            this.item = Float.valueOf(stringValue);
+          } catch (Exception exception) {
+            throw PGExceptionFactory.newPGException("Invalid float4 value: " + stringValue);
+          }
           break;
         case BINARY:
           this.item = ByteConverter.float4(item, 0);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java
index ede06a33..e8676706 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java
@@ -16,6 +16,7 @@ package com.google.cloud.spanner.pgadapter.parsers;
 
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import org.postgresql.util.ByteConverter;
 
 /** Translate from wire protocol to int. */
@@ -33,7 +34,12 @@ class IntegerParser extends Parser<Integer> {
     if (item != null) {
       switch (formatCode) {
         case TEXT:
-          this.item = Integer.valueOf(new String(item));
+          String stringValue = new String(item);
+          try {
+            this.item = Integer.valueOf(stringValue);
+          } catch (Exception exception) {
+            throw PGExceptionFactory.newPGException("Invalid int4 value: " + stringValue);
+          }
           break;
         case BINARY:
           this.item = ByteConverter.int4(item, 0);
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 e78f4386..760f1757 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,6 +19,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.error.PGExceptionFactory;
 import javax.annotation.Nonnull;
 import org.postgresql.util.ByteConverter;
 
@@ -38,7 +39,12 @@ public class LongParser extends Parser<Long> {
     if (item != null) {
       switch (formatCode) {
         case TEXT:
-          this.item = Long.valueOf(new String(item));
+          String stringValue = new String(item);
+          try {
+            this.item = Long.valueOf(stringValue);
+          } catch (Exception exception) {
+            throw PGExceptionFactory.newPGException("Invalid int8 value: " + stringValue);
+          }
           break;
         case BINARY:
           this.item = toLong(item);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ShortParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ShortParser.java
index 00db9af2..6147c221 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ShortParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ShortParser.java
@@ -15,6 +15,7 @@
 package com.google.cloud.spanner.pgadapter.parsers;
 
 import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import org.postgresql.util.ByteConverter;
 
 /** Translate from wire protocol to short. */
@@ -28,7 +29,12 @@ class ShortParser extends Parser<Short> {
     if (item != null) {
       switch (formatCode) {
         case TEXT:
-          this.item = Short.valueOf(new String(item));
+          String stringValue = new String(item);
+          try {
+            this.item = Short.valueOf(stringValue);
+          } catch (Exception exception) {
+            throw PGExceptionFactory.newPGException("Invalid int4 value: " + stringValue);
+          }
           break;
         case BINARY:
           this.item = ByteConverter.int2(item, 0);
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 c2a77ac9..b73b1627 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.error.PGExceptionFactory;
 import com.google.common.base.Preconditions;
 import java.nio.charset.StandardCharsets;
 import java.time.format.DateTimeFormatter;
@@ -99,11 +100,15 @@ public class TimestampParser extends Parser<Timestamp> {
 
   /** 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));
+    try {
+      String stringValue = toPGString(value);
+      TemporalAccessor temporalAccessor = TIMESTAMP_FORMATTER.parse(stringValue);
+      return Timestamp.ofTimeSecondsAndNanos(
+          temporalAccessor.getLong(ChronoField.INSTANT_SECONDS),
+          temporalAccessor.get(ChronoField.NANO_OF_SECOND));
+    } catch (Exception exception) {
+      throw PGExceptionFactory.newPGException("Invalid timestamp value: " + value);
+    }
   }
 
   /**
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java b/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
index 7d3f79cd..69218bc2 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
@@ -19,6 +19,7 @@ import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -415,7 +416,7 @@ public class PGSetting {
       // setting is not a valid boolean value.
       try {
         BooleanParser.toBoolean(value);
-      } catch (IllegalArgumentException exception) {
+      } catch (PGException exception) {
         throw invalidBoolError(getCasePreservingKey());
       }
     } else if ("integer".equals(this.vartype)) {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/InvalidStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/InvalidStatement.java
new file mode 100644
index 00000000..afa387be
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/InvalidStatement.java
@@ -0,0 +1,35 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+
+public class InvalidStatement extends IntermediatePortalStatement {
+
+  public InvalidStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement,
+      Exception exception) {
+    super(connectionHandler, options, name, parsedStatement, originalStatement);
+    setException(SpannerExceptionFactory.asSpannerException(exception));
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/PrepareStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/PrepareStatement.java
new file mode 100644
index 00000000..d8f6d20b
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/PrepareStatement.java
@@ -0,0 +1,212 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import com.google.api.core.InternalApi;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.metadata.DescribePortalMetadata;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.NoResult;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TableOrIndexName;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TypeDefinition;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.ManuallyCreatedToken;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.PreparedType;
+import com.google.cloud.spanner.pgadapter.wireprotocol.DescribeMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+import org.postgresql.core.Oid;
+
+@InternalApi
+public class PrepareStatement extends IntermediatePortalStatement {
+  private static final ImmutableMap<String, Integer> TYPE_NAME_TO_OID_MAPPING =
+      ImmutableMap.<String, Integer>builder()
+          .put("unknown", Oid.UNSPECIFIED)
+          .put("bigint", Oid.INT8)
+          .put("bigint[]", Oid.INT8_ARRAY)
+          .put("int8", Oid.INT8)
+          .put("int8[]", Oid.INT8_ARRAY)
+          .put("int4", Oid.INT4)
+          .put("int4[]", Oid.INT4_ARRAY)
+          .put("int", Oid.INT4)
+          .put("int[]", Oid.INT4_ARRAY)
+          .put("integer", Oid.INT4)
+          .put("integer[]", Oid.INT4_ARRAY)
+          .put("boolean", Oid.BOOL)
+          .put("boolean[]", Oid.BOOL_ARRAY)
+          .put("bool", Oid.BOOL)
+          .put("bool[]", Oid.BOOL_ARRAY)
+          .put("bytea", Oid.BYTEA)
+          .put("bytea[]", Oid.BYTEA_ARRAY)
+          .put("character varying", Oid.VARCHAR)
+          .put("character varying[]", Oid.VARCHAR_ARRAY)
+          .put("varchar", Oid.VARCHAR)
+          .put("varchar[]", Oid.VARCHAR_ARRAY)
+          .put("date", Oid.DATE)
+          .put("date[]", Oid.DATE_ARRAY)
+          .put("double precision", Oid.FLOAT8)
+          .put("double precision[]", Oid.FLOAT8_ARRAY)
+          .put("float8", Oid.FLOAT8)
+          .put("float8[]", Oid.FLOAT8_ARRAY)
+          .put("jsonb", Oid.JSONB)
+          .put("jsonb[]", Oid.JSONB_ARRAY)
+          .put("numeric", Oid.NUMERIC)
+          .put("numeric[]", Oid.NUMERIC_ARRAY)
+          .put("decimal", Oid.NUMERIC)
+          .put("decimal[]", Oid.NUMERIC_ARRAY)
+          .put("text", Oid.TEXT)
+          .put("text[]", Oid.TEXT_ARRAY)
+          .put("timestamp with time zone", Oid.TIMESTAMPTZ)
+          .put("timestamp with time zone[]", Oid.TIMESTAMPTZ_ARRAY)
+          .put("timestamptz", Oid.TIMESTAMPTZ)
+          .put("timestamptz[]", Oid.TIMESTAMPTZ_ARRAY)
+          .build();
+
+  static final class ParsedPreparedStatement {
+    final String name;
+    final int[] dataTypes;
+    final Statement originalPreparedStatement;
+    final ParsedStatement parsedPreparedStatement;
+
+    private ParsedPreparedStatement(String name, int[] dataTypes, String sql) {
+      this.name = name;
+      this.dataTypes = dataTypes;
+      this.originalPreparedStatement = Statement.of(sql);
+      this.parsedPreparedStatement =
+          AbstractStatementParser.getInstance(Dialect.POSTGRESQL).parse(originalPreparedStatement);
+    }
+  }
+
+  private final ParsedPreparedStatement preparedStatement;
+
+  public PrepareStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(connectionHandler, options, name, parsedStatement, originalStatement);
+    this.preparedStatement = parse(originalStatement.getSql());
+  }
+
+  @Override
+  public String getCommandTag() {
+    return "PREPARE";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    if (!this.executed) {
+      this.executed = true;
+      try {
+        new ParseMessage(
+                connectionHandler,
+                preparedStatement.name,
+                preparedStatement.dataTypes,
+                preparedStatement.parsedPreparedStatement,
+                preparedStatement.originalPreparedStatement)
+            .send();
+        new DescribeMessage(
+                connectionHandler,
+                PreparedType.Statement,
+                preparedStatement.name,
+                ManuallyCreatedToken.MANUALLY_CREATED_TOKEN)
+            .send();
+      } catch (Exception exception) {
+        setFutureStatementResult(Futures.immediateFailedFuture(exception));
+        return;
+      }
+      setFutureStatementResult(Futures.immediateFuture(new NoResult(getCommandTag())));
+    }
+  }
+
+  @Override
+  public Future<DescribePortalMetadata> describeAsync(BackendConnection backendConnection) {
+    // Return null to indicate that this PREPARE statement does not return any
+    // RowDescriptionResponse.
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public IntermediatePortalStatement bind(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    // COPY does not support binding any parameters, so we just return the same statement.
+    return this;
+  }
+
+  static ParsedPreparedStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("prepare")) {
+      throw PGExceptionFactory.newPGException("not a valid PREPARE statement: " + sql);
+    }
+    TableOrIndexName name = parser.readTableOrIndexName();
+    if (name == null || name.schema != null) {
+      throw PGExceptionFactory.newPGException("invalid prepared statement name");
+    }
+    ImmutableList.Builder<Integer> dataTypesBuilder = ImmutableList.builder();
+    if (parser.eatToken("(")) {
+      List<String> dataTypesNames = parser.parseExpressionList();
+      if (dataTypesNames == null || dataTypesNames.isEmpty()) {
+        throw PGExceptionFactory.newPGException("invalid data type list");
+      }
+      if (!parser.eatToken(")")) {
+        throw PGExceptionFactory.newPGException("missing closing parentheses in data type list");
+      }
+      dataTypesBuilder.addAll(
+          dataTypesNames.stream()
+              .map(PrepareStatement::dataTypeNameToOid)
+              .collect(Collectors.toList()));
+    }
+    if (!parser.eatKeyword("as")) {
+      throw PGExceptionFactory.newPGException("missing 'AS' keyword in PREPARE statement: " + sql);
+    }
+    return new ParsedPreparedStatement(
+        name.name,
+        dataTypesBuilder.build().stream().mapToInt(i -> i).toArray(),
+        parser.getSql().substring(parser.getPos()).trim());
+  }
+
+  static int dataTypeNameToOid(String type) {
+    SimpleParser parser = new SimpleParser(type);
+    TypeDefinition typeDefinition = parser.readType();
+    Integer oid =
+        TYPE_NAME_TO_OID_MAPPING.get(typeDefinition.getNameAndArrayBrackets().toLowerCase());
+    if (oid != null) {
+      return oid;
+    }
+    throw PGExceptionFactory.newPGException("unknown type name: " + type);
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java
index 9b4b2c50..3e35058d 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java
@@ -17,6 +17,7 @@ package com.google.cloud.spanner.pgadapter.statements;
 import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -82,6 +83,24 @@ public class SimpleParser {
     }
   }
 
+  static class TypeDefinition {
+    final String name;
+    final int length;
+    final int scale;
+    final boolean array;
+
+    TypeDefinition(String name, int length, int scale, boolean array) {
+      this.name = name;
+      this.length = length;
+      this.scale = scale;
+      this.array = array;
+    }
+
+    String getNameAndArrayBrackets() {
+      return array ? name + "[]" : name;
+    }
+  }
+
   static String unquoteOrFoldIdentifier(String identifier) {
     if (Strings.isNullOrEmpty(identifier)) {
       return null;
@@ -291,6 +310,46 @@ public class SimpleParser {
     return sql.substring(startPos, pos);
   }
 
+  TypeDefinition readType() {
+    StringBuilder name = new StringBuilder();
+    int length = 0;
+    int scale = 0;
+    boolean array = false;
+    String keyword;
+    while (!(keyword = readKeyword()).equalsIgnoreCase("")) {
+      if (name.length() > 0) {
+        name.append(' ');
+      }
+      name.append(keyword);
+    }
+    if (eatToken("(")) {
+      String lengthString = parseExpression();
+      length = parseInt(lengthString);
+      if (eatToken(",")) {
+        String scaleExpression = parseExpression();
+        scale = parseInt(scaleExpression);
+      }
+      if (!eatToken(")")) {
+        throw PGExceptionFactory.newPGException("missing ')' for type length");
+      }
+    }
+    if (eatToken("[")) {
+      if (!eatToken("]")) {
+        throw PGExceptionFactory.newPGException("missing ']' for array definition");
+      }
+      array = true;
+    }
+    return new TypeDefinition(name.toString(), length, scale, array);
+  }
+
+  int parseInt(String value) {
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException exception) {
+      throw PGExceptionFactory.toPGException(exception);
+    }
+  }
+
   TableOrIndexName readTableOrIndexName() {
     String nameOrSchema = readIdentifierPart();
     if (nameOrSchema == null) {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BindMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BindMessage.java
index c1631749..b3a4b251 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BindMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/BindMessage.java
@@ -53,13 +53,22 @@ public class BindMessage extends AbstractQueryProtocolMessage {
 
   /** Constructor for Bind messages that are constructed to execute a Query message. */
   public BindMessage(ConnectionHandler connection, ManuallyCreatedToken manuallyCreatedToken) {
+    this(connection, "", new byte[0][], manuallyCreatedToken);
+  }
+
+  /** Constructor for Bind messages that are created by EXECUTE statements. */
+  public BindMessage(
+      ConnectionHandler connection,
+      String statementName,
+      byte[][] parameters,
+      ManuallyCreatedToken manuallyCreatedToken) {
     super(connection, 4, manuallyCreatedToken);
     this.portalName = "";
-    this.statementName = "";
+    this.statementName = statementName;
     this.formatCodes = ImmutableList.of();
     this.resultFormatCodes = ImmutableList.of();
-    this.parameters = new byte[0][];
-    this.statement = connection.getStatement("");
+    this.parameters = parameters;
+    this.statement = connection.getStatement(statementName);
   }
 
   /** Given the prepared statement, bind it and save it locally. */
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java
index fe160787..3021f617 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java
@@ -250,6 +250,9 @@ public abstract class ControlMessage extends WireMessage {
       new EmptyQueryResponse(this.outputStream).send(false);
       return;
     }
+    if (statement.getStatementResult() == null) {
+      return;
+    }
 
     switch (statement.getStatementType()) {
       case DDL:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/DescribeMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/DescribeMessage.java
index 41c4c3ed..81eb90a2 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/DescribeMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/DescribeMessage.java
@@ -54,10 +54,23 @@ public class DescribeMessage extends AbstractQueryProtocolMessage {
 
   /** Constructor for manually created Describe messages from the simple query protocol. */
   public DescribeMessage(ConnectionHandler connection, ManuallyCreatedToken manuallyCreatedToken) {
+    this(connection, PreparedType.Portal, "", manuallyCreatedToken);
+  }
+
+  /** Constructor for manually created Describe messages from PREPARE statements. */
+  public DescribeMessage(
+      ConnectionHandler connection,
+      PreparedType type,
+      String name,
+      ManuallyCreatedToken manuallyCreatedToken) {
     super(connection, 4, manuallyCreatedToken);
-    this.type = PreparedType.Portal;
-    this.name = "";
-    this.statement = this.connection.getPortal(this.name);
+    this.type = type;
+    this.name = name;
+    if (this.type == PreparedType.Portal) {
+      this.statement = this.connection.getPortal(this.name);
+    } else {
+      this.statement = this.connection.getStatement(this.name);
+    }
   }
 
   @SuppressWarnings("unchecked")
@@ -173,17 +186,19 @@ public class DescribeMessage extends AbstractQueryProtocolMessage {
   public void handleDescribeStatement() throws Exception {
     try (DescribeStatementMetadata metadata =
         (DescribeStatementMetadata) this.statement.describe()) {
-      new ParameterDescriptionResponse(this.outputStream, metadata.getParameters()).send(false);
-      if (metadata.getResultSet() != null) {
-        new RowDescriptionResponse(
-                this.outputStream,
-                this.statement,
-                metadata.getResultSet(),
-                this.connection.getServer().getOptions(),
-                this.queryMode)
-            .send(false);
-      } else {
-        new NoDataResponse(this.outputStream).send(false);
+      if (isExtendedProtocol()) {
+        new ParameterDescriptionResponse(this.outputStream, metadata.getParameters()).send(false);
+        if (metadata.getResultSet() != null) {
+          new RowDescriptionResponse(
+                  this.outputStream,
+                  this.statement,
+                  metadata.getResultSet(),
+                  this.connection.getServer().getOptions(),
+                  this.queryMode)
+              .send(false);
+        } else {
+          new NoDataResponse(this.outputStream).send(false);
+        }
       }
     } catch (SpannerException exception) {
       this.handleError(exception);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
index 957d17d3..14af6c02 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
@@ -17,6 +17,9 @@ package com.google.cloud.spanner.pgadapter.wireprotocol;
 import static com.google.cloud.spanner.pgadapter.parsers.copy.Copy.parse;
 import static com.google.cloud.spanner.pgadapter.statements.SimpleParser.isCommand;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.COPY;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.DEALLOCATE;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.EXECUTE;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.PREPARE;
 
 import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.Dialect;
@@ -33,7 +36,11 @@ import com.google.cloud.spanner.pgadapter.parsers.copy.TokenMgrError;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement;
 import com.google.cloud.spanner.pgadapter.statements.CopyToStatement;
+import com.google.cloud.spanner.pgadapter.statements.DeallocateStatement;
+import com.google.cloud.spanner.pgadapter.statements.ExecuteStatement;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePreparedStatement;
+import com.google.cloud.spanner.pgadapter.statements.InvalidStatement;
+import com.google.cloud.spanner.pgadapter.statements.PrepareStatement;
 import com.google.cloud.spanner.pgadapter.wireoutput.ParseCompleteResponse;
 import com.google.common.base.Strings;
 import java.text.MessageFormat;
@@ -68,12 +75,22 @@ public class ParseMessage extends AbstractQueryProtocolMessage {
    */
   public ParseMessage(
       ConnectionHandler connection, ParsedStatement parsedStatement, Statement originalStatement) {
+    this(connection, "", new int[0], parsedStatement, originalStatement);
+  }
+
+  /** Constructor for manually created Parse messages that originate from a PREPARE statement. */
+  public ParseMessage(
+      ConnectionHandler connection,
+      String name,
+      int[] parameterDataTypes,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
     super(
         connection,
         5 + parsedStatement.getSqlWithoutComments().length(),
         ManuallyCreatedToken.MANUALLY_CREATED_TOKEN);
-    this.name = "";
-    this.parameterDataTypes = new int[0];
+    this.name = name;
+    this.parameterDataTypes = parameterDataTypes;
     this.statement =
         createStatement(connection, name, parsedStatement, originalStatement, parameterDataTypes);
   }
@@ -84,32 +101,63 @@ public class ParseMessage extends AbstractQueryProtocolMessage {
       ParsedStatement parsedStatement,
       Statement originalStatement,
       int[] parameterDataTypes) {
-    if (isCommand(COPY, originalStatement.getSql())) {
-      CopyOptions copyOptions = parseCopyStatement(parsedStatement);
-      if (copyOptions.getFromTo() == FromTo.FROM) {
-        return new CopyStatement(
+    try {
+      if (isCommand(COPY, originalStatement.getSql())) {
+        CopyOptions copyOptions = parseCopyStatement(parsedStatement);
+        if (copyOptions.getFromTo() == FromTo.FROM) {
+          return new CopyStatement(
+              connectionHandler,
+              connectionHandler.getServer().getOptions(),
+              name,
+              parsedStatement,
+              originalStatement);
+        } else if (copyOptions.getFromTo() == FromTo.TO) {
+          return new CopyToStatement(
+              connectionHandler, connectionHandler.getServer().getOptions(), name, copyOptions);
+        } else {
+          throw SpannerExceptionFactory.newSpannerException(
+              ErrorCode.INVALID_ARGUMENT, "Unsupported COPY direction: " + copyOptions.getFromTo());
+        }
+      } else if (isCommand(PREPARE, originalStatement.getSql())) {
+        return new PrepareStatement(
+            connectionHandler,
+            connectionHandler.getServer().getOptions(),
+            name,
+            parsedStatement,
+            originalStatement);
+      } else if (isCommand(EXECUTE, originalStatement.getSql())) {
+        return new ExecuteStatement(
+            connectionHandler,
+            connectionHandler.getServer().getOptions(),
+            name,
+            parsedStatement,
+            originalStatement);
+      } else if (isCommand(DEALLOCATE, originalStatement.getSql())) {
+        return new DeallocateStatement(
             connectionHandler,
             connectionHandler.getServer().getOptions(),
             name,
             parsedStatement,
             originalStatement);
-      } else if (copyOptions.getFromTo() == FromTo.TO) {
-        return new CopyToStatement(
-            connectionHandler, connectionHandler.getServer().getOptions(), name, copyOptions);
       } else {
-        throw SpannerExceptionFactory.newSpannerException(
-            ErrorCode.INVALID_ARGUMENT, "Unsupported COPY direction: " + copyOptions.getFromTo());
+        IntermediatePreparedStatement statement =
+            new IntermediatePreparedStatement(
+                connectionHandler,
+                connectionHandler.getServer().getOptions(),
+                name,
+                parsedStatement,
+                originalStatement);
+        statement.setParameterDataTypes(parameterDataTypes);
+        return statement;
       }
-    } else {
-      IntermediatePreparedStatement statement =
-          new IntermediatePreparedStatement(
-              connectionHandler,
-              connectionHandler.getServer().getOptions(),
-              name,
-              parsedStatement,
-              originalStatement);
-      statement.setParameterDataTypes(parameterDataTypes);
-      return statement;
+    } catch (Exception exception) {
+      return new InvalidStatement(
+          connectionHandler,
+          connectionHandler.getServer().getOptions(),
+          name,
+          parsedStatement,
+          originalStatement,
+          exception);
     }
   }
 
@@ -134,8 +182,10 @@ public class ParseMessage extends AbstractQueryProtocolMessage {
 
   @Override
   public void flush() throws Exception {
-    // The simple query protocol does not need the ParseComplete response.
-    if (isExtendedProtocol()) {
+    if (statement.hasException()) {
+      handleError(statement.getException());
+    } else if (isExtendedProtocol()) {
+      // The simple query protocol does not need the ParseComplete response.
       new ParseCompleteResponse(this.outputStream).send(false);
     }
   }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
index 7ee21079..f04488e0 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
@@ -25,6 +25,9 @@ import java.text.MessageFormat;
 public class QueryMessage extends ControlMessage {
   protected static final char IDENTIFIER = 'Q';
   public static final String COPY = "COPY";
+  public static final String PREPARE = "PREPARE";
+  public static final String EXECUTE = "EXECUTE";
+  public static final String DEALLOCATE = "DEALLOCATE";
 
   private final Statement originalStatement;
   private final SimpleQueryStatement simpleQueryStatement;


Test patch:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/DeallocateStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/DeallocateStatement.java
new file mode 100644
index 00000000..b0f9774c
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/DeallocateStatement.java
@@ -0,0 +1,120 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SimpleParser.unquoteOrFoldIdentifier;
+
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.metadata.DescribePortalMetadata;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.NoResult;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TableOrIndexName;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class DeallocateStatement extends IntermediatePortalStatement {
+  static final class ParsedDeallocateStatement {
+    final String name;
+
+    private ParsedDeallocateStatement(String name) {
+      this.name = name;
+    }
+  }
+
+  private final ParsedDeallocateStatement deallocateStatement;
+
+  public DeallocateStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(connectionHandler, options, name, parsedStatement, originalStatement);
+    this.deallocateStatement = parse(originalStatement.getSql());
+  }
+
+  @Override
+  public String getCommandTag() {
+    return "DEALLOCATE";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    this.executed = true;
+    try {
+      if (deallocateStatement.name == null) {
+        connectionHandler.closeAllStatements();
+      } else {
+        connectionHandler.closeStatement(deallocateStatement.name);
+      }
+    } catch (Exception exception) {
+      setFutureStatementResult(Futures.immediateFailedFuture(exception));
+      return;
+    }
+    setFutureStatementResult(Futures.immediateFuture(new NoResult()));
+  }
+
+  @Override
+  public Future<DescribePortalMetadata> describeAsync(BackendConnection backendConnection) {
+    // Return null to indicate that this EXECUTE statement does not return any
+    // RowDescriptionResponse.
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public IntermediatePortalStatement bind(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    // EXECUTE does not support binding any parameters, so we just return the same statement.
+    return this;
+  }
+
+  static ParsedDeallocateStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("deallocate")) {
+      throw PGExceptionFactory.newPGException("not a valid DEALLOCATE statement: " + sql);
+    }
+    parser.eatKeyword("prepare");
+    String statementName = null;
+    if (!parser.eatKeyword("all")) {
+      TableOrIndexName name = parser.readTableOrIndexName();
+      if (name == null || name.schema != null) {
+        throw PGExceptionFactory.newPGException("invalid prepared statement name");
+      }
+      statementName = unquoteOrFoldIdentifier(name.name);
+    }
+    parser.skipWhitespaces();
+    if (parser.getPos() < parser.getSql().length()) {
+      throw PGExceptionFactory.newPGException(
+          "Syntax error. Unexpected tokens: " + parser.getSql().substring(parser.getPos()));
+    }
+    return new ParsedDeallocateStatement(statementName);
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExecuteStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExecuteStatement.java
new file mode 100644
index 00000000..dd3c9c50
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExecuteStatement.java
@@ -0,0 +1,168 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SimpleParser.unquoteOrFoldIdentifier;
+
+import com.google.api.core.InternalApi;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.metadata.DescribePortalMetadata;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TableOrIndexName;
+import com.google.cloud.spanner.pgadapter.wireprotocol.BindMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.ManuallyCreatedToken;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.PreparedType;
+import com.google.cloud.spanner.pgadapter.wireprotocol.DescribeMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ExecuteMessage;
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.Futures;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+
+@InternalApi
+public class ExecuteStatement extends IntermediatePortalStatement {
+  static final class ParsedExecuteStatement {
+    final String name;
+    final byte[][] parameters;
+
+    private ParsedExecuteStatement(String name, byte[][] parameters) {
+      this.name = name;
+      this.parameters = parameters;
+    }
+  }
+
+  private final ParsedExecuteStatement executeStatement;
+
+  public ExecuteStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(connectionHandler, options, name, parsedStatement, originalStatement);
+    this.executeStatement = parse(originalStatement.getSql());
+  }
+
+  @Override
+  public String getCommandTag() {
+    return "EXECUTE";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    this.executed = true;
+    try {
+      new BindMessage(
+              connectionHandler,
+              executeStatement.name,
+              executeStatement.parameters,
+              ManuallyCreatedToken.MANUALLY_CREATED_TOKEN)
+          .send();
+      new DescribeMessage(
+              connectionHandler,
+              PreparedType.Portal,
+              /* name = */ "",
+              ManuallyCreatedToken.MANUALLY_CREATED_TOKEN)
+          .send();
+      new ExecuteMessage(connectionHandler, ManuallyCreatedToken.MANUALLY_CREATED_TOKEN).send();
+    } catch (Exception exception) {
+      setFutureStatementResult(Futures.immediateFailedFuture(exception));
+      return;
+    }
+    // Set a null result to indicate that this statement should not return any result.
+    setFutureStatementResult(Futures.immediateFuture(null));
+  }
+
+  @Override
+  public Future<DescribePortalMetadata> describeAsync(BackendConnection backendConnection) {
+    // Return null to indicate that this EXECUTE statement does not return any
+    // RowDescriptionResponse.
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public IntermediatePortalStatement bind(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    // EXECUTE does not support binding any parameters, so we just return the same statement.
+    return this;
+  }
+
+  static ParsedExecuteStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("execute")) {
+      throw PGExceptionFactory.newPGException("not a valid EXECUTE statement: " + sql);
+    }
+    TableOrIndexName name = parser.readTableOrIndexName();
+    if (name == null || name.schema != null) {
+      throw PGExceptionFactory.newPGException("invalid prepared statement name");
+    }
+    String statementName = unquoteOrFoldIdentifier(name.name);
+
+    List<String> parameters;
+    if (parser.eatToken("(")) {
+      List<String> parametersList = parser.parseExpressionList();
+      if (parametersList == null || parametersList.isEmpty()) {
+        throw PGExceptionFactory.newPGException("invalid parameter list");
+      }
+      if (!parser.eatToken(")")) {
+        throw PGExceptionFactory.newPGException("missing closing parentheses in parameters list");
+      }
+      parameters =
+          parametersList.stream().map(ExecuteStatement::unquoteString).collect(Collectors.toList());
+    } else {
+      parameters = Collections.emptyList();
+    }
+    parser.skipWhitespaces();
+    if (parser.getPos() < parser.getSql().length()) {
+      throw PGExceptionFactory.newPGException(
+          "Syntax error. Unexpected tokens: " + parser.getSql().substring(parser.getPos()));
+    }
+    return new ParsedExecuteStatement(
+        statementName,
+        parameters.stream()
+            .map(p -> p == null ? null : p.getBytes(StandardCharsets.UTF_8))
+            .toArray(byte[][]::new));
+  }
+
+  static String unquoteString(String parameter) {
+    if (parameter != null
+        && parameter.length() > 1
+        && parameter.charAt(0) == '\''
+        && parameter.charAt(parameter.length() - 1) == '\'') {
+      return parameter.substring(1, parameter.length() - 1);
+    }
+    if (parameter != null && parameter.trim().equalsIgnoreCase("null")) {
+      return null;
+    }
+    return parameter;
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java
index a9549af0..c9cc62dc 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatement.java
@@ -238,11 +238,13 @@ public class IntermediateStatement {
 
   public void setStatementResult(StatementResult statementResult) {
     this.statementResult = statementResult;
-    if (statementResult.getResultType() == ResultType.RESULT_SET) {
-      this.hasMoreData = statementResult.getResultSet().next();
-    } else if (statementResult instanceof NoResult
-        && ((NoResult) statementResult).hasCommandTag()) {
-      this.commandTag = ((NoResult) statementResult).getCommandTag();
+    if (statementResult != null) {
+      if (statementResult.getResultType() == ResultType.RESULT_SET) {
+        this.hasMoreData = statementResult.getResultSet().next();
+      } else if (statementResult instanceof NoResult
+          && ((NoResult) statementResult).hasCommandTag()) {
+        this.commandTag = ((NoResult) statementResult).getCommandTag();
+      }
     }
   }
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
index 23335f01..8eb460af 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
@@ -15,8 +15,10 @@
 package com.google.cloud.spanner.pgadapter;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -25,8 +27,10 @@ import static org.mockito.Mockito.when;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.ConnectionStatus;
+import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePortalStatement;
+import com.google.cloud.spanner.pgadapter.statements.IntermediatePreparedStatement;
 import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
@@ -41,6 +45,55 @@ import org.junit.runners.JUnit4;
 @RunWith(JUnit4.class)
 public class ConnectionHandlerTest {
 
+  @Test
+  public void testRegisterStatement() {
+    ProxyServer server = mock(ProxyServer.class);
+    Socket socket = mock(Socket.class);
+    InetAddress address = mock(InetAddress.class);
+    when(socket.getInetAddress()).thenReturn(address);
+    IntermediatePreparedStatement statement = mock(IntermediatePreparedStatement.class);
+
+    ConnectionHandler connection = new ConnectionHandler(server, socket);
+    connection.registerStatement("my-statement", statement);
+
+    assertTrue(connection.hasStatement("my-statement"));
+    assertSame(statement, connection.getStatement("my-statement"));
+
+    connection.closeStatement("my-statement");
+    assertFalse(connection.hasStatement("my-statement"));
+  }
+
+  @Test
+  public void testCloseUnknownStatement() {
+    ProxyServer server = mock(ProxyServer.class);
+    Socket socket = mock(Socket.class);
+    InetAddress address = mock(InetAddress.class);
+    when(socket.getInetAddress()).thenReturn(address);
+
+    ConnectionHandler connection = new ConnectionHandler(server, socket);
+
+    assertThrows(PGException.class, () -> connection.closeStatement("unknown"));
+  }
+
+  @Test
+  public void testCloseAll() {
+    ProxyServer server = mock(ProxyServer.class);
+    Socket socket = mock(Socket.class);
+    InetAddress address = mock(InetAddress.class);
+    when(socket.getInetAddress()).thenReturn(address);
+    IntermediatePreparedStatement statement1 = mock(IntermediatePreparedStatement.class);
+    IntermediatePreparedStatement statement2 = mock(IntermediatePreparedStatement.class);
+
+    ConnectionHandler connection = new ConnectionHandler(server, socket);
+    connection.registerStatement("my-statement1", statement1);
+    connection.registerStatement("my-statement2", statement2);
+
+    connection.closeAllStatements();
+
+    assertFalse(connection.hasStatement("my-statement1"));
+    assertFalse(connection.hasStatement("my-statement2"));
+  }
+
   @Test
   public void testTerminateClosesSocket() throws IOException {
     ProxyServer server = mock(ProxyServer.class);
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ControlMessageTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ControlMessageTest.java
index 4e80eeb6..976b3d8b 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ControlMessageTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ControlMessageTest.java
@@ -24,6 +24,8 @@ import com.google.cloud.spanner.pgadapter.ConnectionHandler.QueryMode;
 import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.TextFormat;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.NoResult;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.UpdateCount;
 import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.ManuallyCreatedToken;
@@ -70,6 +72,7 @@ public final class ControlMessageTest {
     when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata);
     when(intermediateStatement.getStatementType()).thenReturn(StatementType.UPDATE);
     when(intermediateStatement.getCommandTag()).thenReturn("INSERT");
+    when(intermediateStatement.getStatementResult()).thenReturn(new UpdateCount(1L));
     when(intermediateStatement.getUpdateCount()).thenReturn(1L);
 
     JSONParser parser = new JSONParser();
@@ -119,6 +122,7 @@ public final class ControlMessageTest {
     IntermediateStatement intermediateStatement = mock(IntermediateStatement.class);
     when(intermediateStatement.getCommandTag()).thenReturn("parse");
     when(intermediateStatement.getStatementType()).thenReturn(StatementType.UNKNOWN);
+    when(intermediateStatement.getStatementResult()).thenReturn(new NoResult("PARSE"));
 
     executeMessage.sendSpannerResult(intermediateStatement, QueryMode.SIMPLE, 0L);
 
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 a16a528c..6ddf6377 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
@@ -16,12 +16,15 @@ package com.google.cloud.spanner.pgadapter;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
 import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
 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.rpc.ResourceInfo;
 import com.google.spanner.admin.database.v1.Database;
@@ -267,6 +270,44 @@ public class EmulatedPsqlMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testPrepareExecuteDeallocate() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl("my-db"))) {
+      connection.createStatement().execute("prepare my_prepared_statement as SELECT 1");
+
+      assertEquals(1, pgServer.getNumberOfConnections());
+      ConnectionHandler connectionHandler = pgServer.getConnectionHandlers().get(0);
+      IntermediateStatement preparedStatement =
+          connectionHandler.getStatement("my_prepared_statement");
+      assertNotNull(preparedStatement);
+      assertEquals("SELECT 1", preparedStatement.getStatement());
+
+      try (java.sql.Statement statement = connection.createStatement()) {
+        assertTrue(statement.execute("execute my_prepared_statement"));
+        try (ResultSet resultSet = statement.getResultSet()) {
+          assertTrue(resultSet.next());
+          assertEquals(1L, resultSet.getLong(1));
+          assertFalse(resultSet.next());
+        }
+      }
+
+      connection.createStatement().execute("deallocate my_prepared_statement");
+      PGException exception =
+          assertThrows(
+              PGException.class, () -> connectionHandler.getStatement("my_prepared_statement"));
+      assertEquals(
+          "prepared statement my_prepared_statement does not exist", exception.getMessage());
+
+      SQLException sqlException =
+          assertThrows(
+              SQLException.class,
+              () -> connection.createStatement().execute("execute my_prepared_statement"));
+      assertEquals(
+          "ERROR: prepared statement my_prepared_statement does not exist",
+          sqlException.getMessage());
+    }
+  }
+
   static StatusRuntimeException newStatusResourceNotFoundException(
       String shortName, String resourceType, String resourceName) {
     ResourceInfo resourceInfo =
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
index aa9bc8ab..f7f9f728 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
@@ -27,6 +27,7 @@ import com.google.cloud.spanner.Database;
 import com.google.cloud.spanner.KeySet;
 import com.google.cloud.spanner.Mutation;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -223,6 +224,11 @@ public class ITPsqlTest implements IntegrationTest {
    * stderr text.
    */
   private Tuple<String, String> runUsingPsql(String sql) throws IOException, InterruptedException {
+    return runUsingPsql(ImmutableList.of(sql));
+  }
+
+  private Tuple<String, String> runUsingPsql(Iterable<String> commands)
+      throws IOException, InterruptedException {
     ProcessBuilder builder = new ProcessBuilder();
     String[] psqlCommand =
         new String[] {
@@ -246,7 +252,9 @@ public class ITPsqlTest implements IntegrationTest {
             new BufferedReader(new InputStreamReader(process.getInputStream()));
         BufferedReader errorReader =
             new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
-      writer.write(sql);
+      for (String sql : commands) {
+        writer.write(sql);
+      }
       writer.write("\\q\n");
       writer.flush();
       errors = errorReader.lines().collect(Collectors.joining("\n"));
@@ -327,6 +335,52 @@ public class ITPsqlTest implements IntegrationTest {
         output);
   }
 
+  @Test
+  public void testPrepareExecuteDeallocate() throws IOException, InterruptedException {
+    Tuple<String, String> result =
+        runUsingPsql(
+            ImmutableList.of(
+                "prepare insert_row as "
+                    + "insert into all_types values ($1, $2, $3, $4, $5, $6, $7, $8, $9);\n",
+                "prepare find_row as "
+                    + "select * from all_types "
+                    + "where col_bigint=$1 "
+                    + "and col_bool=$2 "
+                    + "and col_bytea=$3 "
+                    + "and col_float8=$4 "
+                    + "and col_int=$5 "
+                    + "and col_numeric=$6 "
+                    + "and col_timestamptz=$7 "
+                    + "and col_date=$8 "
+                    + "and col_varchar=$9;\n",
+                "execute find_row (1, true, '\\xaabbcc', 3.14, 100, 6.626, "
+                    + "'2022-09-06 17:14:49+02', '2022-09-06', 'hello world');\n",
+                "execute insert_row (1, true, '\\xaabbcc', 3.14, 100, 6.626, "
+                    + "'2022-09-06 17:14:49+02', '2022-09-06', 'hello world');\n",
+                "execute find_row (1, true, '\\xaabbcc', 3.14, 100, 6.626, "
+                    + "'2022-09-06 17:14:49+02', '2022-09-06', 'hello world');\n",
+                "deallocate find_row;\n",
+                "deallocate insert_row;\n"));
+    String output = result.x(), errors = result.y();
+    assertEquals("", errors);
+    assertEquals(
+        "PREPARE\n"
+            + "PREPARE\n"
+            + " col_bigint | col_bool | col_bytea | col_float8 | col_int | col_numeric | col_timestamptz | col_date | col_varchar \n"
+            + "------------+----------+-----------+------------+---------+-------------+-----------------+----------+-------------\n"
+            + "(0 rows)\n"
+            + "\n"
+            + "INSERT 0 1\n"
+            + " col_bigint | col_bool | col_bytea | col_float8 | col_int | col_numeric |    col_timestamptz     |  col_date  | col_varchar \n"
+            + "------------+----------+-----------+------------+---------+-------------+------------------------+------------+-------------\n"
+            + "          1 | t        | \\xaabbcc  |       3.14 |     100 |       6.626 | 2022-09-06 15:14:49+00 | 2022-09-06 | hello world\n"
+            + "(1 row)\n"
+            + "\n"
+            + "DEALLOCATE\n"
+            + "DEALLOCATE",
+        output);
+  }
+
   @Test
   public void testCopyFromPostgreSQLToCloudSpanner() throws Exception {
     int numRows = 100;
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
index 3f8becfe..6e967d5c 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
@@ -27,6 +27,7 @@ import com.google.spanner.v1.CommitRequest;
 import com.google.spanner.v1.ExecuteSqlRequest;
 import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
 import com.google.spanner.v1.RollbackRequest;
+import io.grpc.Status;
 import java.math.BigDecimal;
 import java.sql.Connection;
 import java.sql.DriverManager;
@@ -427,4 +428,106 @@ public class JdbcSimpleModeMockServerTest extends AbstractMockServerTest {
       }
     }
   }
+
+  @Test
+  public void testPrepareStatement() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (java.sql.Statement statement = connection.createStatement()) {
+        assertFalse(statement.execute("prepare my_statement as SELECT 1"));
+        assertTrue(statement.execute("execute my_statement"));
+        try (ResultSet resultSet = statement.getResultSet()) {
+          assertTrue(resultSet.next());
+          assertEquals(1L, resultSet.getLong(1));
+          assertFalse(resultSet.next());
+        }
+        assertFalse(statement.execute("deallocate my_statement"));
+      }
+    }
+  }
+
+  @Test
+  public void testInvalidPrepareStatement() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (java.sql.Statement statement = connection.createStatement()) {
+        assertThrows(SQLException.class, () -> statement.execute("prepare my_statement foo"));
+      }
+    }
+  }
+
+  @Test
+  public void testPrepareStatementWithError() throws SQLException {
+    String sql = "select * from non_existing_table where id=$1";
+    mockSpanner.putStatementResult(
+        StatementResult.exception(
+            com.google.cloud.spanner.Statement.of(sql),
+            Status.NOT_FOUND
+                .withDescription("Table non_existing_table not found")
+                .asRuntimeException()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (java.sql.Statement statement = connection.createStatement()) {
+        SQLException exception =
+            assertThrows(
+                SQLException.class, () -> statement.execute("prepare my_statement as " + sql));
+        assertEquals(
+            "ERROR: Table non_existing_table not found - Statement: 'select * from non_existing_table where id=$1'",
+            exception.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void testStatementIsConnectionSpecific() throws SQLException {
+    try (Connection connection1 = DriverManager.getConnection(createUrl());
+        Connection connection2 = DriverManager.getConnection(createUrl())) {
+      connection1.createStatement().execute("prepare my_statement as SELECT 1");
+      assertTrue(connection1.createStatement().execute("execute my_statement"));
+      assertThrows(
+          SQLException.class, () -> connection2.createStatement().execute("execute my_statement"));
+    }
+  }
+
+  @Test
+  public void testDeallocateAll() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (java.sql.Statement statement = connection.createStatement()) {
+        assertFalse(statement.execute("prepare my_statement1 as SELECT 1"));
+        assertFalse(statement.execute("prepare my_statement2 as SELECT 2"));
+        assertTrue(statement.execute("execute my_statement1"));
+        try (ResultSet resultSet = statement.getResultSet()) {
+          assertTrue(resultSet.next());
+          assertEquals(1L, resultSet.getLong(1));
+          assertFalse(resultSet.next());
+        }
+        assertTrue(statement.execute("execute my_statement2"));
+        try (ResultSet resultSet = statement.getResultSet()) {
+          assertTrue(resultSet.next());
+          assertEquals(2L, resultSet.getLong(1));
+          assertFalse(resultSet.next());
+        }
+        assertFalse(statement.execute("deallocate all"));
+
+        assertThrows(SQLException.class, () -> statement.execute("execute my_statement1"));
+        assertThrows(SQLException.class, () -> statement.execute("execute my_statement2"));
+      }
+    }
+  }
+
+  @Test
+  public void testDeallocateUnknownStatement() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (java.sql.Statement statement = connection.createStatement()) {
+        assertThrows(SQLException.class, () -> statement.execute("deallocate my_statement"));
+      }
+    }
+  }
+
+  @Test
+  public void testExecuteUnknownStatement() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (java.sql.Statement statement = connection.createStatement()) {
+        assertThrows(SQLException.class, () -> statement.execute("execute my_statement"));
+      }
+    }
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParserTest.java
index c00d28a2..7b78bfe0 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParserTest.java
@@ -16,8 +16,11 @@ package com.google.cloud.spanner.pgadapter.parsers;
 
 import static junit.framework.TestCase.assertNull;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 
 import com.google.cloud.ByteArray;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
 import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,5 +42,8 @@ public class BinaryParserTest {
     assertEquals(
         "\\x010203", new BinaryParser(ByteArray.copyFrom(new byte[] {1, 2, 3})).stringParse());
     assertNull(new BinaryParser(null).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new BinaryParser("\\xzz".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
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 feb47cfd..2935e7ed 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
@@ -23,6 +23,9 @@ import static org.junit.Assert.assertTrue;
 
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -59,7 +62,10 @@ public class BooleanParserTest {
     assertFalse(BooleanParser.toBoolean("fa"));
     assertFalse(BooleanParser.toBoolean("f"));
 
-    assertThrows(IllegalArgumentException.class, () -> BooleanParser.toBoolean("foo"));
+    assertThrows(PGException.class, () -> BooleanParser.toBoolean("foo"));
+    assertThrows(
+        PGException.class,
+        () -> new BooleanParser("bar".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 
   @Test
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 cb4f998d..e01399f4 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
@@ -21,6 +21,9 @@ import static org.junit.Assert.assertThrows;
 import com.google.cloud.Date;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -42,5 +45,8 @@ public class DateParserTest {
   public void testStringParse() {
     assertEquals("2022-07-08", new DateParser(Date.fromYearMonthDay(2022, 7, 8)).stringParse());
     assertNull(new DateParser(null).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new DateParser("2022".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParserTest.java
index cf9c8a13..f0600899 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParserTest.java
@@ -20,6 +20,9 @@ import static org.junit.Assert.assertThrows;
 
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+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;
@@ -45,5 +48,8 @@ public class DoubleParserTest {
   public void testStringParse() {
     assertEquals("3.14", new DoubleParser(3.14).stringParse());
     assertNull(new DoubleParser(null).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new DoubleParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/FloatParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/FloatParserTest.java
index 5f9ff369..f04b86ad 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/FloatParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/FloatParserTest.java
@@ -16,7 +16,11 @@ package com.google.cloud.spanner.pgadapter.parsers;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -28,5 +32,11 @@ public class FloatParserTest {
   public void testStringParse() {
     assertEquals("3.14", new FloatParser(3.14f).stringParse());
     assertNull(new FloatParser(null).stringParse());
+    assertEquals(
+        "123.456",
+        new FloatParser("123.456".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new FloatParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParserTest.java
index 02656ac8..142cf283 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParserTest.java
@@ -16,7 +16,11 @@ package com.google.cloud.spanner.pgadapter.parsers;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -28,5 +32,8 @@ public class IntegerParserTest {
   public void testStringParse() {
     assertEquals("100", new IntegerParser(100).stringParse());
     assertNull(new IntegerParser(null).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new IntegerParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/LongParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/LongParserTest.java
index 005b3c5d..411ef563 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/LongParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/LongParserTest.java
@@ -20,6 +20,9 @@ import static org.junit.Assert.assertThrows;
 
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+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;
@@ -53,5 +56,8 @@ public class LongParserTest {
   public void testStringParse() {
     assertEquals("100", new LongParser(100L).stringParse());
     assertNull(new LongParser(null).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new LongParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/NumericParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/NumericParserTest.java
index 31522215..99dc2a73 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/NumericParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/NumericParserTest.java
@@ -20,7 +20,9 @@ import static org.junit.Assert.assertThrows;
 
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
 import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
 import java.util.Random;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,6 +47,10 @@ public class NumericParserTest {
   public void testStringParse() {
     assertEquals("3.14", new NumericParser("3.14").stringParse());
     assertEquals("NaN", new NumericParser("NaN").stringParse());
+    // Numeric values are not checked client side, as they are sent as text all the way.
+    assertEquals(
+        "foo",
+        new NumericParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT).stringParse());
     assertNull(new NumericParser(null).stringParse());
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ShortParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ShortParserTest.java
index 18d8813e..f036fb99 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ShortParserTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/ShortParserTest.java
@@ -16,7 +16,11 @@ package com.google.cloud.spanner.pgadapter.parsers;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import java.nio.charset.StandardCharsets;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -28,5 +32,11 @@ public class ShortParserTest {
   public void testStringParse() {
     assertEquals("100", new ShortParser((short) 100).stringParse());
     assertNull(new ShortParser(null).stringParse());
+    assertEquals(
+        "-200",
+        new ShortParser("-200".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new ShortParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
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 8a4c7a79..5670296e 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
@@ -23,6 +23,7 @@ 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.error.PGException;
 import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
 import java.nio.charset.StandardCharsets;
 import java.util.Random;
@@ -72,5 +73,8 @@ public class TimestampParserTest {
         new TimestampParser(Timestamp.parseTimestamp("2022-07-08T07:22:59.123456789Z"))
             .stringParse());
     assertNull(new TimestampParser(null).stringParse());
+    assertThrows(
+        PGException.class,
+        () -> new TimestampParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/DeallocateStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/DeallocateStatementTest.java
new file mode 100644
index 00000000..f43dc575
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/DeallocateStatementTest.java
@@ -0,0 +1,50 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.DeallocateStatement.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DeallocateStatementTest {
+
+  @Test
+  public void testParse() {
+    assertEquals("foo", parse("deallocate foo").name);
+    assertEquals("foo", parse("deallocate prepare foo").name);
+    assertEquals("foo", parse("deallocate FOO").name);
+    assertEquals("foo", parse("deallocate\tfoo").name);
+    assertEquals("foo", parse("deallocate\nfoo").name);
+    assertEquals("foo", parse("deallocate/*comment*/foo").name);
+    assertEquals("foo", parse("deallocate \"foo\"").name);
+    assertEquals("Foo", parse("deallocate \"Foo\"").name);
+    assertEquals("prepare", parse("deallocate \"prepare\"").name);
+    assertNull(parse("deallocate all").name);
+    assertEquals("all", parse("deallocate \"all\"").name);
+
+    assertThrows(PGException.class, () -> parse("prepare foo"));
+    assertThrows(PGException.class, () -> parse("deallocate"));
+    assertThrows(PGException.class, () -> parse("deallocate prepare"));
+    assertThrows(PGException.class, () -> parse("deallocate foo bar"));
+    assertThrows(PGException.class, () -> parse("deallocate foo.bar"));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/ExecuteStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/ExecuteStatementTest.java
new file mode 100644
index 00000000..d07bd8ea
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/ExecuteStatementTest.java
@@ -0,0 +1,89 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.ExecuteStatement.parse;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import java.nio.charset.StandardCharsets;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ExecuteStatementTest {
+
+  @Test
+  public void testGetStatementType() {
+    ConnectionHandler connectionHandler = mock(ConnectionHandler.class);
+    when(connectionHandler.getConnectionMetadata()).thenReturn(mock(ConnectionMetadata.class));
+    assertEquals(
+        StatementType.CLIENT_SIDE,
+        new ExecuteStatement(
+                connectionHandler,
+                mock(OptionsMetadata.class),
+                "my-statement",
+                AbstractStatementParser.getInstance(Dialect.POSTGRESQL)
+                    .parse(Statement.of("execute foo")),
+                Statement.of("execute foo"))
+            .getStatementType());
+  }
+
+  @Test
+  public void testParse() {
+    assertEquals("foo", parse("execute foo").name);
+    assertEquals("foo", parse("execute FOO").name);
+    assertEquals("foo", parse("execute\tfoo").name);
+    assertEquals("foo", parse("execute\nfoo").name);
+    assertEquals("foo", parse("execute/*comment*/foo").name);
+    assertEquals("foo", parse("execute \"foo\"").name);
+    assertEquals("Foo", parse("execute \"Foo\"").name);
+    assertEquals("foo", parse("execute foo (1)").name);
+    assertEquals("foo", parse("execute foo (1, 'test')").name);
+
+    assertArrayEquals(
+        new byte[][] {param("1"), param("test")}, parse("execute foo (1, 'test')").parameters);
+    assertArrayEquals(
+        new byte[][] {param("3.14"), param("\\x55aa")},
+        parse("execute foo (3.14, '\\x55aa')").parameters);
+    assertArrayEquals(
+        new byte[][] {null, param("2000-01-01")},
+        parse("execute foo (null, '2000-01-01')").parameters);
+
+    assertThrows(PGException.class, () -> parse("foo"));
+    assertThrows(PGException.class, () -> parse("execute"));
+    assertThrows(PGException.class, () -> parse("execute foo bar"));
+    assertThrows(PGException.class, () -> parse("execute foo.bar"));
+    assertThrows(PGException.class, () -> parse("execute foo ()"));
+    assertThrows(PGException.class, () -> parse("execute foo (1) bar"));
+    assertThrows(PGException.class, () -> parse("execute foo (1"));
+  }
+
+  static byte[] param(String value) {
+    return value.getBytes(StandardCharsets.UTF_8);
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/PrepareStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/PrepareStatementTest.java
new file mode 100644
index 00000000..ee0f0124
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/PrepareStatementTest.java
@@ -0,0 +1,129 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.PrepareStatement.dataTypeNameToOid;
+import static com.google.cloud.spanner.pgadapter.statements.PrepareStatement.parse;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.statements.PrepareStatement.ParsedPreparedStatement;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.postgresql.core.Oid;
+
+@RunWith(JUnit4.class)
+public class PrepareStatementTest {
+
+  @Test
+  public void testDataTypeNameToOid() {
+    assertEquals(Oid.UNSPECIFIED, dataTypeNameToOid("unknown"));
+    assertEquals(Oid.INT8, dataTypeNameToOid("bigint"));
+    assertEquals(Oid.INT8_ARRAY, dataTypeNameToOid("bigint[]"));
+    assertEquals(Oid.INT8, dataTypeNameToOid("int8"));
+    assertEquals(Oid.INT8_ARRAY, dataTypeNameToOid("int8[]"));
+    assertEquals(Oid.INT4, dataTypeNameToOid("int"));
+    assertEquals(Oid.INT4_ARRAY, dataTypeNameToOid("int[]"));
+    assertEquals(Oid.INT4, dataTypeNameToOid("int4"));
+    assertEquals(Oid.INT4_ARRAY, dataTypeNameToOid("int4[]"));
+    assertEquals(Oid.INT4, dataTypeNameToOid("integer"));
+    assertEquals(Oid.INT4_ARRAY, dataTypeNameToOid("integer[]"));
+    assertEquals(Oid.BOOL, dataTypeNameToOid("boolean"));
+    assertEquals(Oid.BOOL_ARRAY, dataTypeNameToOid("boolean[]"));
+    assertEquals(Oid.BOOL, dataTypeNameToOid("bool"));
+    assertEquals(Oid.BOOL_ARRAY, dataTypeNameToOid("bool[]"));
+    assertEquals(Oid.DATE, dataTypeNameToOid("date"));
+    assertEquals(Oid.DATE_ARRAY, dataTypeNameToOid("date[]"));
+    assertEquals(Oid.FLOAT8, dataTypeNameToOid("double precision"));
+    assertEquals(Oid.FLOAT8_ARRAY, dataTypeNameToOid("double precision[]"));
+    assertEquals(Oid.FLOAT8, dataTypeNameToOid("float8"));
+    assertEquals(Oid.FLOAT8_ARRAY, dataTypeNameToOid("float8[]"));
+    assertEquals(Oid.JSONB, dataTypeNameToOid("jsonb"));
+    assertEquals(Oid.JSONB_ARRAY, dataTypeNameToOid("jsonb[]"));
+    assertEquals(Oid.NUMERIC, dataTypeNameToOid("numeric"));
+    assertEquals(Oid.NUMERIC_ARRAY, dataTypeNameToOid("numeric[]"));
+    assertEquals(Oid.NUMERIC, dataTypeNameToOid("decimal"));
+    assertEquals(Oid.NUMERIC_ARRAY, dataTypeNameToOid("decimal[]"));
+    assertEquals(Oid.NUMERIC, dataTypeNameToOid("numeric(1,1)"));
+    assertEquals(Oid.NUMERIC_ARRAY, dataTypeNameToOid("numeric(2, 1)[]"));
+
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("character varying"));
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("varchar"));
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("character varying(100)"));
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("varchar(100)"));
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("character varying (100)"));
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("varchar (100)"));
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("character     varying   ( 100 )  \t"));
+    assertEquals(Oid.VARCHAR, dataTypeNameToOid("varchar\t(100)  \n"));
+
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("character varying[]"));
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("varchar[]"));
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("character varying(100)[]"));
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("varchar(100)[]"));
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("character varying (100) []"));
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("varchar (100) []"));
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("character     varying   ( 100 )  \t[\n]"));
+    assertEquals(Oid.VARCHAR_ARRAY, dataTypeNameToOid("varchar\t(100)  \n[  ]"));
+
+    assertThrows(PGException.class, () -> dataTypeNameToOid("invalid_type"));
+    assertThrows(PGException.class, () -> dataTypeNameToOid("varchar(100"));
+    assertThrows(PGException.class, () -> dataTypeNameToOid("bigint["));
+    assertThrows(PGException.class, () -> dataTypeNameToOid("varchar(bar)"));
+    assertThrows(PGException.class, () -> dataTypeNameToOid("numeric(10, bar)"));
+  }
+
+  @Test
+  public void testParse() {
+    ParsedPreparedStatement statement = parse("prepare foo as select 1");
+    assertEquals("foo", statement.name);
+    assertArrayEquals(new int[] {}, statement.dataTypes);
+    assertEquals("select 1", statement.originalPreparedStatement.getSql());
+
+    statement = parse("prepare foo (boolean) as select * from bar where active=$1");
+    assertEquals("foo", statement.name);
+    assertArrayEquals(new int[] {Oid.BOOL}, statement.dataTypes);
+    assertEquals("select * from bar where active=$1", statement.originalPreparedStatement.getSql());
+
+    statement =
+        parse("prepare foo (boolean, bigint) as select * from bar where active=$1 and age=$2");
+    assertEquals("foo", statement.name);
+    assertArrayEquals(new int[] {Oid.BOOL, Oid.INT8}, statement.dataTypes);
+    assertEquals(
+        "select * from bar where active=$1 and age=$2",
+        statement.originalPreparedStatement.getSql());
+
+    statement =
+        parse(
+            "prepare foo ("
+                + "\tboolean,\n"
+                + "\tbigint,\n"
+                + "\tcharacter varying(100)\n"
+                + ") as select * from bar where active=$1 and age=$2");
+    assertEquals("foo", statement.name);
+    assertArrayEquals(new int[] {Oid.BOOL, Oid.INT8, Oid.VARCHAR}, statement.dataTypes);
+    assertEquals(
+        "select * from bar where active=$1 and age=$2",
+        statement.originalPreparedStatement.getSql());
+
+    assertThrows(PGException.class, () -> parse("prpra foo ( as select 1"));
+    assertThrows(PGException.class, () -> parse("prepare foo ( as select 1"));
+    assertThrows(PGException.class, () -> parse("prepare foo () as select 1"));
+    assertThrows(PGException.class, () -> parse("prepare (bigint) as select 1"));
+    assertThrows(PGException.class, () -> parse("prepare as select 1"));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
index 8a4b2fa1..573e5ab2 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
@@ -52,6 +52,7 @@ import com.google.cloud.spanner.pgadapter.session.PGSetting;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection.ConnectionState;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.UpdateCount;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement;
 import com.google.cloud.spanner.pgadapter.statements.ExtendedQueryProtocolHandler;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePortalStatement;
@@ -1183,6 +1184,7 @@ public class ProtocolTest {
     when(connectionHandler.getPortal("")).thenReturn(intermediatePortalStatement);
     when(intermediatePortalStatement.getCommandTag()).thenReturn("INSERT");
     when(intermediatePortalStatement.getStatementType()).thenReturn(StatementType.UPDATE);
+    when(intermediatePortalStatement.getStatementResult()).thenReturn(new UpdateCount(1L));
     when(intermediatePortalStatement.getUpdateCount()).thenReturn(1L);
     when(backendConnection.getConnectionState()).thenReturn(ConnectionState.TRANSACTION);
     OptionsMetadata options = mock(OptionsMetadata.class);