Repository: GoogleCloudPlatform/pgadapter
Pull request #462 (2022-11-04 14:43:30)

chore: refactor to pg exception
Use PGException instead of SpannerException so we can return the correct SQLState (when we know it) and hints.

Fixes b/246193644

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#462](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (96c97f4) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/41940113058b7467f187bce1775052e587887a86?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (4194011) will **increase** coverage by `0.10%`.
> The diff coverage is `80.95%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #462      +/-   ##
========================================================
+ Coverage                 87.32%   87.43%   +0.10%     
- Complexity                 2022     2026       +4     
========================================================
  Files                       122      122              
  Lines                      6651     6667      +16     
  Branches                    957      959       +2     
========================================================
+ Hits                       5808     5829      +21     
+ Misses                      594      589       -5     
  Partials                    249      249              
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `87.43% <80.95%> (+0.10%)` | :arrow_up: |

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/462?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [.../cloud/spanner/pgadapter/utils/MutationWriter.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL011dGF0aW9uV3JpdGVyLmphdmE=) | `92.12% <41.66%> (+0.58%)` | :arrow_up: |
| [...er/pgadapter/statements/IntermediateStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvSW50ZXJtZWRpYXRlU3RhdGVtZW50LmphdmE=) | `96.59% <50.00%> (+2.14%)` | :arrow_up: |
| [...panner/pgadapter/wireprotocol/DescribeMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9EZXNjcmliZU1lc3NhZ2UuamF2YQ==) | `86.11% <66.66%> (ø)` | |
| [...gle/cloud/spanner/pgadapter/error/PGException.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL2Vycm9yL1BHRXhjZXB0aW9uLmphdmE=) | `90.62% <85.71%> (+0.96%)` | :arrow_up: |
| [...panner/pgadapter/statements/BackendConnection.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvQmFja2VuZENvbm5lY3Rpb24uamF2YQ==) | `91.18% <87.50%> (+0.19%)` | :arrow_up: |
| [...ud/spanner/pgadapter/error/PGExceptionFactory.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL2Vycm9yL1BHRXhjZXB0aW9uRmFjdG9yeS5qYXZh) | `100.00% <100.00%> (ø)` | |
| [...google/cloud/spanner/pgadapter/error/SQLState.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL2Vycm9yL1NRTFN0YXRlLmphdmE=) | `99.43% <100.00%> (+0.64%)` | :arrow_up: |
| [...ud/spanner/pgadapter/statements/CopyStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvQ29weVN0YXRlbWVudC5qYXZh) | `95.05% <100.00%> (-0.03%)` | :arrow_down: |
| [...spanner/pgadapter/statements/InvalidStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvSW52YWxpZFN0YXRlbWVudC5qYXZh) | `100.00% <100.00%> (ø)` | |
| [...panner/pgadapter/wireprotocol/CopyDataMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9Db3B5RGF0YU1lc3NhZ2UuamF2YQ==) | `85.00% <100.00%> (ø)` | |
| ... and [3 more](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/462/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | |

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


Patch:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/error/PGException.java b/src/main/java/com/google/cloud/spanner/pgadapter/error/PGException.java
index 5b6650f9..ce6f99b9 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/error/PGException.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/error/PGException.java
@@ -26,9 +26,10 @@ import java.util.Objects;
 public class PGException extends RuntimeException {
   public static class Builder {
     private final String message;
-    private Severity severity;
+    private Severity severity = Severity.ERROR;
     private SQLState sqlState;
     private String hints;
+    private Throwable cause;
 
     private Builder(String message) {
       this.message = message;
@@ -49,15 +50,20 @@ public class PGException extends RuntimeException {
       return this;
     }
 
+    public Builder setCause(Throwable cause) {
+      this.cause = cause;
+      return this;
+    }
+
     public PGException build() {
-      return new PGException(severity, sqlState, message, hints);
+      return new PGException(cause, severity, sqlState, message, hints);
     }
   }
 
   public static Builder newBuilder(Exception cause) {
     Preconditions.checkNotNull(cause);
-    return new Builder(
-        cause.getMessage() == null ? cause.getClass().getName() : cause.getMessage());
+    return new Builder(cause.getMessage() == null ? cause.getClass().getName() : cause.getMessage())
+        .setCause(cause);
   }
 
   public static Builder newBuilder(String message) {
@@ -68,8 +74,9 @@ public class PGException extends RuntimeException {
   private final SQLState sqlState;
   private final String hints;
 
-  private PGException(Severity severity, SQLState sqlState, String message, String hints) {
-    super(message == null ? "" : message);
+  private PGException(
+      Throwable cause, Severity severity, SQLState sqlState, String message, String hints) {
+    super(message == null ? "" : message, cause);
     this.severity = severity;
     this.sqlState = sqlState;
     this.hints = hints;
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactory.java b/src/main/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactory.java
index 207cea96..72753eaa 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactory.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactory.java
@@ -14,6 +14,8 @@
 
 package com.google.cloud.spanner.pgadapter.error;
 
+import static com.google.cloud.spanner.pgadapter.statements.BackendConnection.TRANSACTION_ABORTED_ERROR;
+
 import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.SpannerException;
 import io.grpc.StatusRuntimeException;
@@ -58,17 +60,28 @@ public class PGExceptionFactory {
     return newPGException("Query cancelled", SQLState.QueryCanceled);
   }
 
+  /**
+   * Creates a new exception that indicates that the current transaction is in the aborted state.
+   */
+  public static PGException newTransactionAbortedException() {
+    return newPGException(TRANSACTION_ABORTED_ERROR, SQLState.InFailedSqlTransaction);
+  }
+
   /** Converts the given {@link SpannerException} to a {@link PGException}. */
   public static PGException toPGException(SpannerException spannerException) {
     return newPGException(extractMessage(spannerException));
   }
 
   /** Converts the given {@link Exception} to a {@link PGException}. */
-  public static PGException toPGException(Exception exception) {
-    if (exception instanceof SpannerException) {
-      return toPGException((SpannerException) exception);
+  public static PGException toPGException(Throwable throwable) {
+    if (throwable instanceof SpannerException) {
+      return toPGException((SpannerException) throwable);
+    }
+    if (throwable instanceof PGException) {
+      return (PGException) throwable;
     }
-    return newPGException(exception.getMessage());
+    return newPGException(
+        throwable.getMessage() == null ? throwable.getClass().getName() : throwable.getMessage());
   }
 
   private static final String NOT_FOUND_PREFIX =
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/error/SQLState.java b/src/main/java/com/google/cloud/spanner/pgadapter/error/SQLState.java
index 1d47d0b6..89be8e33 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/error/SQLState.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/error/SQLState.java
@@ -135,6 +135,20 @@ public enum SQLState {
   SqlJsonScalarRequired("2203F"),
   SqlJsonItemCannotBeCastToTargetType("2203G"),
 
+  // Class 25 - Invalid Transaction State
+  InvalidTransactionState("25000"),
+  ActiveSqlTransaction("25001"),
+  BranchTransactionAlreadyActive("25002"),
+  HeldCursorRequiresSameIsolationLevel("25008"),
+  InappropriateAccessModeForBranchTransaction("25003"),
+  InappropriateIsolationLevelForBranchTransaction("25004"),
+  NoActiveSqlTransactionForBranchTransaction("25005"),
+  ReadOnlySqlTransaction("25006"),
+  SchemaAndDataStatementMixingNotSupported("25007"),
+  NoActiveSqlTransaction("25P01"),
+  InFailedSqlTransaction("25P02"),
+  IdleInTransactionSessionTimeout("25P03"),
+
   // Class 42 — Syntax Error or Access Rule Violation
   SyntaxErrorOrAccessRuleViolation("42000"),
   SyntaxError("42601"),
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
index 69bdc609..7566bae8 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
@@ -44,7 +44,9 @@ import com.google.cloud.spanner.connection.ConnectionOptionsHelper;
 import com.google.cloud.spanner.connection.ResultSetHelper;
 import com.google.cloud.spanner.connection.StatementResult;
 import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
+import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.DdlTransactionMode;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
@@ -126,6 +128,12 @@ public class BackendConnection {
     DDL_BATCH,
   }
 
+  static <T> PGException setAndReturn(SettableFuture<T> future, Throwable throwable) {
+    PGException pgException = PGExceptionFactory.toPGException(throwable);
+    future.setException(pgException);
+    return pgException;
+  }
+
   /**
    * Buffered statements are kept in memory until a flush or sync message is received. This makes it
    * possible to batch multiple statements together when sending them to Cloud Spanner.
@@ -147,8 +155,7 @@ public class BackendConnection {
       // Only COMMIT or ROLLBACK is allowed if we are in an ABORTED transaction.
       if (connectionState == ConnectionState.ABORTED
           && !(isCommit(parsedStatement) || isRollback(parsedStatement))) {
-        throw SpannerExceptionFactory.newSpannerException(
-            ErrorCode.INVALID_ARGUMENT, TRANSACTION_ABORTED_ERROR);
+        throw PGExceptionFactory.newTransactionAbortedException();
       }
     }
   }
@@ -225,19 +232,16 @@ public class BackendConnection {
                             .executeQuery(statement))));
             return;
           } catch (Exception exception) {
-            result.setException(exception);
-            throw exception;
+            throw setAndReturn(result, exception);
           }
         }
         if (spannerException.getErrorCode() == ErrorCode.CANCELLED || Thread.interrupted()) {
-          result.setException(PGExceptionFactory.newQueryCancelledException());
+          throw setAndReturn(result, PGExceptionFactory.newQueryCancelledException());
         } else {
-          result.setException(spannerException);
+          throw setAndReturn(result, spannerException);
         }
-        throw spannerException;
       } catch (Throwable exception) {
-        result.setException(exception);
-        throw exception;
+        throw setAndReturn(result, exception);
       }
     }
 
@@ -307,8 +311,7 @@ public class BackendConnection {
           }
         }
       } catch (Exception exception) {
-        result.setException(exception);
-        throw exception;
+        throw setAndReturn(result, exception);
       }
     }
   }
@@ -674,9 +677,9 @@ public class BackendConnection {
             // Single DDL statements outside explicit transactions are always allowed. For a single
             // statement, there can also not be an implicit transaction that needs to be committed.
             if (transactionMode == TransactionMode.EXPLICIT) {
-              throw SpannerExceptionFactory.newSpannerException(
-                  ErrorCode.FAILED_PRECONDITION,
-                  "DDL statements are only allowed outside explicit transactions.");
+              throw PGExceptionFactory.newPGException(
+                  "DDL statements are only allowed outside explicit transactions.",
+                  SQLState.InvalidTransactionState);
             }
             // Fall-through to commit the transaction if necessary.
           case AutocommitExplicitTransaction:
@@ -700,23 +703,23 @@ public class BackendConnection {
       // We are in a batch of statements.
       switch (ddlTransactionMode) {
         case Single:
-          throw SpannerExceptionFactory.newSpannerException(
-              ErrorCode.FAILED_PRECONDITION,
-              "DDL statements are only allowed outside batches and transactions.");
+          throw PGExceptionFactory.newPGException(
+              "DDL statements are only allowed outside batches and transactions.",
+              SQLState.InvalidTransactionState);
         case Batch:
           if (spannerConnection.isInTransaction()
               || bufferedStatements.stream()
                   .anyMatch(statement -> !statement.parsedStatement.isDdl())) {
-            throw SpannerExceptionFactory.newSpannerException(
-                ErrorCode.FAILED_PRECONDITION,
-                "DDL statements are not allowed in mixed batches or transactions.");
+            throw PGExceptionFactory.newPGException(
+                "DDL statements are not allowed in mixed batches or transactions.",
+                SQLState.InvalidTransactionState);
           }
           break;
         case AutocommitImplicitTransaction:
           if (spannerConnection.isInTransaction() && transactionMode != TransactionMode.IMPLICIT) {
-            throw SpannerExceptionFactory.newSpannerException(
-                ErrorCode.FAILED_PRECONDITION,
-                "DDL statements are only allowed outside explicit transactions.");
+            throw PGExceptionFactory.newPGException(
+                "DDL statements are only allowed outside explicit transactions.",
+                SQLState.InvalidTransactionState);
           }
           // Fallthrough to commit the transaction if necessary.
         case AutocommitExplicitTransaction:
@@ -734,9 +737,8 @@ public class BackendConnection {
             }
           }
       }
-    } catch (SpannerException exception) {
-      bufferedStatement.result.setException(exception);
-      throw exception;
+    } catch (Throwable throwable) {
+      throw setAndReturn(bufferedStatement.result, throwable);
     }
   }
 
@@ -819,16 +821,10 @@ public class BackendConnection {
     Preconditions.checkArgument(
         canBeBatchedTogether(getStatementType(fromIndex), getStatementType(fromIndex + 1)));
     StatementType batchType = getStatementType(fromIndex);
-    switch (batchType) {
-      case UPDATE:
-        spannerConnection.startBatchDml();
-        break;
-      case DDL:
-        spannerConnection.startBatchDdl();
-        break;
-      default:
-        throw SpannerExceptionFactory.newSpannerException(
-            ErrorCode.INVALID_ARGUMENT, "Statement type is not supported for batching");
+    if (batchType == StatementType.UPDATE) {
+      spannerConnection.startBatchDml();
+    } else if (batchType == StatementType.DDL) {
+      spannerConnection.startBatchDdl();
     }
     List<StatementResult> statementResults = new ArrayList<>(getStatementCount());
     int index = fromIndex;
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyStatement.java
index f411f0ca..7f55cdf1 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyStatement.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/CopyStatement.java
@@ -17,7 +17,6 @@ package com.google.cloud.spanner.pgadapter.statements;
 import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ResultSet;
-import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.Type;
@@ -118,12 +117,6 @@ public class CopyStatement extends IntermediatePortalStatement {
     return this.exception != null;
   }
 
-  @Override
-  public SpannerException getException() {
-    // Do not clear exceptions on a CopyStatement.
-    return this.exception;
-  }
-
   @Override
   public long getUpdateCount() {
     // COPY statements continue to execute while the server continues to receive a stream of
@@ -393,8 +386,7 @@ public class CopyStatement extends IntermediatePortalStatement {
               mutationWriter,
               executor));
     } catch (Exception e) {
-      SpannerException spannerException = SpannerExceptionFactory.asSpannerException(e);
-      handleExecutionException(spannerException);
+      handleExecutionException(PGExceptionFactory.toPGException(e));
     }
   }
 
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
index afa387be..2cce42c8 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/InvalidStatement.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/InvalidStatement.java
@@ -14,10 +14,10 @@
 
 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.error.PGExceptionFactory;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 
 public class InvalidStatement extends IntermediatePortalStatement {
@@ -30,6 +30,6 @@ public class InvalidStatement extends IntermediatePortalStatement {
       Statement originalStatement,
       Exception exception) {
     super(connectionHandler, options, name, parsedStatement, originalStatement);
-    setException(SpannerExceptionFactory.asSpannerException(exception));
+    setException(PGExceptionFactory.toPGException(exception));
   }
 }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
index 50a895db..2dc290fe 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
@@ -34,7 +34,9 @@ import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.session.CopySettings;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection.UpdateCount;
@@ -106,7 +108,7 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
   private final Format copyFormat;
   private final CSVFormat csvFormat;
   private final boolean hasHeader;
-  private final CountDownLatch parserCreatedLatch = new CountDownLatch(1);
+  private final CountDownLatch pipeCreatedLatch = new CountDownLatch(1);
   private final PipedOutputStream payload = new PipedOutputStream();
   private final AtomicBoolean commit = new AtomicBoolean(false);
   private final AtomicBoolean rollback = new AtomicBoolean(false);
@@ -117,7 +119,7 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
   private final Object lock = new Object();
 
   @GuardedBy("lock")
-  private SpannerException exception;
+  private PGException exception;
 
   public MutationWriter(
       SessionState sessionState,
@@ -161,7 +163,7 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
       }
     }
     try {
-      parserCreatedLatch.await();
+      pipeCreatedLatch.await();
       this.payload.write(payload);
     } catch (InterruptedException | InterruptedIOException interruptedIOException) {
       // The IO operation was interrupted. This indicates that the user wants to cancel the COPY
@@ -173,11 +175,13 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
       // Ignore the exception if the executor has already been shutdown. That means that an error
       // occurred that ended the COPY operation while we were writing data to the buffer.
       if (!executorService.isShutdown()) {
-        SpannerException spannerException =
-            SpannerExceptionFactory.newSpannerException(
-                ErrorCode.INTERNAL, "Could not write copy data to buffer", e);
-        logger.log(Level.SEVERE, spannerException.getMessage(), spannerException);
-        throw spannerException;
+        PGException pgException =
+            PGException.newBuilder("Could not write copy data to buffer")
+                .setSQLState(SQLState.InternalError)
+                .setCause(e)
+                .build();
+        logger.log(Level.SEVERE, pgException.getMessage(), pgException);
+        throw pgException;
       }
     }
   }
@@ -205,7 +209,7 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
   @Override
   public StatementResult call() throws Exception {
     PipedInputStream inputStream = new PipedInputStream(payload, copySettings.getPipeBufferSize());
-    parserCreatedLatch.countDown();
+    pipeCreatedLatch.countDown();
     final CopyInParser parser = CopyInParser.create(copyFormat, csvFormat, inputStream, hasHeader);
     // This LinkedBlockingDeque holds a reference to all transactions that are currently active. The
     // max capacity of this deque is what ensures that we never have more than maxParallelism
@@ -231,12 +235,12 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
       while (!rollback.get() && iterator.hasNext()) {
         CopyRecord record = iterator.next();
         if (record.numColumns() != this.tableColumns.keySet().size()) {
-          throw SpannerExceptionFactory.newSpannerException(
-              ErrorCode.INVALID_ARGUMENT,
+          throw PGExceptionFactory.newPGException(
               "Invalid COPY data: Row length mismatched. Expected "
                   + this.tableColumns.keySet().size()
                   + " columns, but only found "
-                  + record.numColumns());
+                  + record.numColumns(),
+              SQLState.DataException);
         }
 
         Mutation mutation = buildMutation(record);
@@ -308,17 +312,17 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
       ApiFutures.allAsList(allCommitFutures).get();
     } catch (SpannerException e) {
       synchronized (lock) {
-        this.exception = e;
+        this.exception = PGExceptionFactory.toPGException(e);
         throw this.exception;
       }
     } catch (ExecutionException e) {
       synchronized (lock) {
-        this.exception = SpannerExceptionFactory.asSpannerException(e.getCause());
+        this.exception = PGExceptionFactory.toPGException(e.getCause());
         throw this.exception;
       }
     } catch (Exception e) {
       synchronized (lock) {
-        this.exception = SpannerExceptionFactory.asSpannerException(e);
+        this.exception = PGExceptionFactory.toPGException(e);
         throw this.exception;
       }
     } finally {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyDataMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyDataMessage.java
index d10b73e9..51bfb0b8 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyDataMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyDataMessage.java
@@ -15,8 +15,8 @@
 package com.google.cloud.spanner.pgadapter.wireprotocol;
 
 import com.google.api.core.InternalApi;
-import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement;
 import com.google.cloud.spanner.pgadapter.utils.MutationWriter;
 import java.text.MessageFormat;
@@ -57,7 +57,7 @@ public class CopyDataMessage extends ControlMessage {
     if (!statement.hasException()) {
       try {
         mutationWriter.addCopyData(this.payload);
-      } catch (SpannerException exception) {
+      } catch (PGException exception) {
         statement.handleExecutionException(exception);
         throw exception;
       }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyFailMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyFailMessage.java
index b2dbfe86..84b190c0 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyFailMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/CopyFailMessage.java
@@ -15,10 +15,10 @@
 package com.google.cloud.spanner.pgadapter.wireprotocol;
 
 import com.google.api.core.InternalApi;
-import com.google.cloud.spanner.ErrorCode;
-import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.ConnectionStatus;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement;
 import com.google.cloud.spanner.pgadapter.utils.MutationWriter;
 import java.text.MessageFormat;
@@ -49,7 +49,7 @@ public class CopyFailMessage extends ControlMessage {
       mutationWriter.rollback();
       statement.close();
       this.statement.handleExecutionException(
-          SpannerExceptionFactory.newSpannerException(ErrorCode.CANCELLED, this.errorMessage));
+          PGException.newBuilder(this.errorMessage).setSQLState(SQLState.QueryCanceled).build());
     }
     // Clear the COPY_IN status to indicate that we finished unsuccessfully. This will cause the
     // inline handling of incoming (copy) messages to stop and the server to return an error message
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 f11b9544..92b1e656 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
@@ -15,7 +15,6 @@
 package com.google.cloud.spanner.pgadapter.wireprotocol;
 
 import com.google.api.core.InternalApi;
-import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
@@ -93,7 +92,7 @@ public class DescribeMessage extends AbstractQueryProtocolMessage {
       } else {
         this.handleDescribeStatement();
       }
-    } catch (SpannerException e) {
+    } catch (Exception e) {
       handleError(e);
     }
   }
@@ -153,7 +152,7 @@ public class DescribeMessage extends AbstractQueryProtocolMessage {
                     this.connection.getServer().getOptions(),
                     this.queryMode)
                 .send(false);
-          } catch (SpannerException exception) {
+          } catch (Exception exception) {
             this.handleError(exception);
           }
         }
@@ -203,7 +202,7 @@ public class DescribeMessage extends AbstractQueryProtocolMessage {
           new NoDataResponse(this.outputStream).send(false);
         }
       }
-    } catch (SpannerException exception) {
+    } catch (Exception exception) {
       this.handleError(exception);
     }
   }


Test patch:
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 604e4ab4..f7e4a829 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
@@ -19,8 +19,6 @@ import static com.google.cloud.spanner.pgadapter.statements.SimpleParser.parseCo
 import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.ResultSet;
-import com.google.cloud.spanner.SpannerException;
-import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractStatementParser;
 import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
@@ -30,6 +28,7 @@ import com.google.cloud.spanner.connection.PostgreSQLStatementParser;
 import com.google.cloud.spanner.connection.StatementResult;
 import com.google.cloud.spanner.connection.StatementResult.ResultType;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.cloud.spanner.pgadapter.metadata.DescribeMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
@@ -37,6 +36,7 @@ import com.google.cloud.spanner.pgadapter.statements.BackendConnection.NoResult;
 import com.google.cloud.spanner.pgadapter.utils.Converter;
 import com.google.cloud.spanner.pgadapter.wireoutput.DataRowResponse;
 import com.google.cloud.spanner.pgadapter.wireoutput.WireOutput;
+import com.google.common.annotations.VisibleForTesting;
 import java.io.DataOutputStream;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
@@ -70,7 +70,7 @@ public class IntermediateStatement {
   protected StatementResult statementResult;
   protected boolean hasMoreData;
   protected Future<StatementResult> futureStatementResult;
-  protected SpannerException exception;
+  protected PGException exception;
   protected final ParsedStatement parsedStatement;
   protected final Statement originalStatement;
   protected final String command;
@@ -214,7 +214,8 @@ public class IntermediateStatement {
     return this.parsedStatement.getSqlWithoutComments();
   }
 
-  private void initFutureResult(ResultNotReadyBehavior resultNotReadyBehavior) {
+  @VisibleForTesting
+  void initFutureResult(ResultNotReadyBehavior resultNotReadyBehavior) {
     if (this.futureStatementResult != null) {
       if (resultNotReadyBehavior == ResultNotReadyBehavior.FAIL
           && !this.futureStatementResult.isDone()) {
@@ -223,12 +224,9 @@ public class IntermediateStatement {
       try {
         setStatementResult(this.futureStatementResult.get());
       } catch (ExecutionException executionException) {
-        setException(SpannerExceptionFactory.asSpannerException(executionException.getCause()));
+        setException(PGExceptionFactory.toPGException(executionException.getCause()));
       } catch (InterruptedException interruptedException) {
-        // TODO(b/246193644): Switch to PGException
-        setException(
-            SpannerExceptionFactory.asSpannerException(
-                PGExceptionFactory.newQueryCancelledException()));
+        setException(PGExceptionFactory.newQueryCancelledException());
       } finally {
         this.futureStatementResult = null;
       }
@@ -269,11 +267,11 @@ public class IntermediateStatement {
   }
 
   /** Returns any execution exception registered for this statement. */
-  public SpannerException getException() {
+  public PGException getException() {
     return this.exception;
   }
 
-  void setException(SpannerException exception) {
+  void setException(PGException exception) {
     // Do not override any exception that has already been registered. COPY statements can receive
     // multiple errors as they execute asynchronously while receiving a stream of data from the
     // client. We always return the first exception that we encounter.
@@ -287,7 +285,7 @@ public class IntermediateStatement {
    *
    * @param exception The exception to store.
    */
-  public void handleExecutionException(SpannerException exception) {
+  public void handleExecutionException(PGException exception) {
     setException(exception);
     this.hasMoreData = false;
   }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java
index 8b704d56..433aa7e7 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java
@@ -30,6 +30,7 @@ import static org.junit.Assume.assumeTrue;
 import com.google.cloud.spanner.MockSpannerServiceImpl;
 import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
 import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.common.base.Strings;
 import com.google.common.hash.Hashing;
@@ -765,7 +766,8 @@ public class CopyInMockServerTest extends AbstractMockServerTest {
               + "XX000\n"
               + "Expected CopyData ('d'), CopyDone ('c') or CopyFail ('f') messages, got: 'Q'\n"
               + "ERROR\n"
-              + "P0001\n"
+              + SQLState.QueryCanceled
+              + "\n"
               + "Error\n",
           errorMessage.toString());
 
@@ -1152,7 +1154,8 @@ public class CopyInMockServerTest extends AbstractMockServerTest {
         }
       }
       assertTrue(receivedErrorMessage);
-      assertEquals("ERROR\n" + "P0001\n" + "Changed my mind\n", errorMessage.toString());
+      assertEquals(
+          "ERROR\n" + SQLState.QueryCanceled + "\n" + "Changed my mind\n", errorMessage.toString());
     }
     assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
   }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java
index d965cb80..b3d1f3df 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/CopyOutMockServerTest.java
@@ -26,6 +26,7 @@ import com.google.cloud.Date;
 import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.MockSpannerServiceImpl;
+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.Type;
@@ -549,6 +550,26 @@ public class CopyOutMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testCopyOutPartitioned_NonExistingTable() throws SQLException {
+    StatusRuntimeException exception =
+        Status.NOT_FOUND.withDescription("Table my_table not found").asRuntimeException();
+    mockSpanner.putStatementResult(
+        StatementResult.exception(Statement.of("select * from my_table"), exception));
+    mockSpanner.setPartitionQueryExecutionTime(SimulatedExecutionTime.ofException(exception));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      CopyManager copyManager = new CopyManager(connection.unwrap(BaseConnection.class));
+      StringWriter writer = new StringWriter();
+      SQLException sqlException =
+          assertThrows(
+              SQLException.class, () -> copyManager.copyOut("COPY my_table TO STDOUT", writer));
+      assertEquals(
+          "ERROR: Table my_table not found - Statement: 'select * from my_table'",
+          sqlException.getMessage());
+    }
+  }
+
   static int findIndex(com.google.spanner.v1.ResultSet resultSet, String[] cols) {
     for (int index = 0; index < resultSet.getRowsCount(); index++) {
       boolean nullValuesEqual = true;
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ErrorHandlingTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ErrorHandlingTest.java
index 0bde76bb..3a00c983 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ErrorHandlingTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ErrorHandlingTest.java
@@ -26,6 +26,7 @@ import com.google.spanner.v1.RollbackRequest;
 import io.grpc.Status;
 import java.sql.Connection;
 import java.sql.DriverManager;
+import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -33,6 +34,7 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
+import org.postgresql.PGConnection;
 
 @RunWith(Parameterized.class)
 public class ErrorHandlingTest extends AbstractMockServerTest {
@@ -55,7 +57,10 @@ public class ErrorHandlingTest extends AbstractMockServerTest {
   public static void setupErrorResults() {
     mockSpanner.putStatementResult(
         StatementResult.exception(
-            Statement.of(INVALID_SELECT), Status.NOT_FOUND.asRuntimeException()));
+            Statement.of(INVALID_SELECT),
+            Status.NOT_FOUND
+                .withDescription("Table unknown_table not found")
+                .asRuntimeException()));
   }
 
   private String createUrl() {
@@ -70,7 +75,9 @@ public class ErrorHandlingTest extends AbstractMockServerTest {
       SQLException exception =
           assertThrows(
               SQLException.class, () -> connection.createStatement().executeQuery(INVALID_SELECT));
-      assertTrue(exception.getMessage(), exception.getMessage().contains("NOT_FOUND"));
+      assertEquals(
+          "ERROR: Table unknown_table not found - Statement: 'SELECT * FROM unknown_table'",
+          exception.getMessage());
 
       // The connection should be usable, as there was no transaction.
       assertTrue(connection.createStatement().execute("SELECT 1"));
@@ -85,7 +92,9 @@ public class ErrorHandlingTest extends AbstractMockServerTest {
       SQLException exception =
           assertThrows(
               SQLException.class, () -> connection.createStatement().executeQuery(INVALID_SELECT));
-      assertTrue(exception.getMessage(), exception.getMessage().contains("NOT_FOUND"));
+      assertEquals(
+          "ERROR: Table unknown_table not found - Statement: 'SELECT * FROM unknown_table'",
+          exception.getMessage());
 
       // The connection should be in the aborted state.
       exception =
@@ -108,7 +117,9 @@ public class ErrorHandlingTest extends AbstractMockServerTest {
       SQLException exception =
           assertThrows(
               SQLException.class, () -> connection.createStatement().executeQuery(INVALID_SELECT));
-      assertTrue(exception.getMessage(), exception.getMessage().contains("NOT_FOUND"));
+      assertEquals(
+          "ERROR: Table unknown_table not found - Statement: 'SELECT * FROM unknown_table'",
+          exception.getMessage());
 
       // The connection should be in the aborted state.
       exception =
@@ -126,4 +137,22 @@ public class ErrorHandlingTest extends AbstractMockServerTest {
     assertTrue(mockSpanner.countRequestsOfType(RollbackRequest.class) > 0);
     assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
   }
+
+  @Test
+  public void testInvalidPreparedQuery() throws SQLException {
+    for (int prepareThreshold : new int[] {-1, 0, 1, 5}) {
+      try (Connection connection = DriverManager.getConnection(createUrl())) {
+        PGConnection pgConnection = connection.unwrap(PGConnection.class);
+        pgConnection.setPrepareThreshold(prepareThreshold);
+        try (PreparedStatement preparedStatement = connection.prepareStatement(INVALID_SELECT)) {
+          SQLException exception =
+              assertThrows(SQLException.class, preparedStatement::executeQuery);
+          assertEquals(
+              "Prepare threshold: " + prepareThreshold,
+              "ERROR: Table unknown_table not found - Statement: 'SELECT * FROM unknown_table'",
+              exception.getMessage());
+        }
+      }
+    }
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/PGExceptionTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/PGExceptionTest.java
deleted file mode 100644
index 3a044cbf..00000000
--- a/src/test/java/com/google/cloud/spanner/pgadapter/PGExceptionTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// 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;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-import com.google.cloud.spanner.pgadapter.error.PGException;
-import com.google.cloud.spanner.pgadapter.error.SQLState;
-import com.google.cloud.spanner.pgadapter.error.Severity;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class PGExceptionTest {
-
-  @Test
-  public void testHints() {
-    assertNull(
-        PGException.newBuilder("test message")
-            .setSQLState(SQLState.InternalError)
-            .setSeverity(Severity.ERROR)
-            .build()
-            .getHints());
-    assertEquals(
-        "test hint\nsecond line",
-        PGException.newBuilder("test message")
-            .setSQLState(SQLState.InternalError)
-            .setSeverity(Severity.ERROR)
-            .setHints("test hint\nsecond line")
-            .build()
-            .getHints());
-  }
-}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionTest.java
index 734cceb9..efa25264 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionTest.java
@@ -16,6 +16,7 @@ package com.google.cloud.spanner.pgadapter.error;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -24,6 +25,24 @@ import org.junit.runners.JUnit4;
 @RunWith(JUnit4.class)
 public class PGExceptionTest {
 
+  @Test
+  public void testHints() {
+    assertNull(
+        PGException.newBuilder("test message")
+            .setSQLState(SQLState.InternalError)
+            .setSeverity(Severity.ERROR)
+            .build()
+            .getHints());
+    assertEquals(
+        "test hint\nsecond line",
+        PGException.newBuilder("test message")
+            .setSQLState(SQLState.InternalError)
+            .setSeverity(Severity.ERROR)
+            .setHints("test hint\nsecond line")
+            .build()
+            .getHints());
+  }
+
   @Test
   public void testEquals() {
     assertEquals(
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/golang/GormMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/golang/GormMockServerTest.java
index d4de29fb..4e93abca 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/golang/GormMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/golang/GormMockServerTest.java
@@ -804,7 +804,7 @@ public class GormMockServerTest extends AbstractMockServerTest {
     // Nested transactions are not supported, as we don't support savepoints.
     String res = gormTest.TestNestedTransaction(createConnString());
     assertEquals(
-        "failed to execute nested transaction: ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE P0001)",
+        "failed to execute nested transaction: ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE 25P02)",
         res);
     assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
   }
@@ -840,7 +840,7 @@ public class GormMockServerTest extends AbstractMockServerTest {
 
     String res = gormTest.TestErrorInTransaction(createConnString());
     assertEquals(
-        "failed to execute transaction: ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE P0001)",
+        "failed to execute transaction: ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE 25P02)",
         res);
     assertEquals(1, mockSpanner.countRequestsOfType(RollbackRequest.class));
     // This test also leads to 1 commit request. The reason for this is that the update statement is
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/ITNodePostgresTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/ITNodePostgresTest.java
index 20ef0172..e3463206 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/ITNodePostgresTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/ITNodePostgresTest.java
@@ -166,13 +166,13 @@ public class ITNodePostgresTest implements IntegrationTest {
   @Test
   public void testSelect1() throws Exception {
     String output = runTest("testSelect1", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nSELECT 1 returned: 1\n", output);
+    assertEquals("SELECT 1 returned: 1\n", output);
   }
 
   @Test
   public void testInsert() throws Exception {
     String output = runTest("testInsert", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
@@ -186,7 +186,7 @@ public class ITNodePostgresTest implements IntegrationTest {
   @Test
   public void testInsertExecutedTwice() throws Exception {
     String output = runTest("testInsertTwice", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nInserted 1 row(s)\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\nInserted 1 row(s)\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
@@ -202,7 +202,7 @@ public class ITNodePostgresTest implements IntegrationTest {
   @Test
   public void testInsertAutoCommit() throws IOException, InterruptedException {
     String output = runTest("testInsertAutoCommit", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
@@ -216,7 +216,7 @@ public class ITNodePostgresTest implements IntegrationTest {
   @Test
   public void testInsertAllTypes() throws IOException, InterruptedException {
     String output = runTest("testInsertAllTypes", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
@@ -244,7 +244,7 @@ public class ITNodePostgresTest implements IntegrationTest {
   public void testInsertAllTypesNull() throws IOException, InterruptedException {
     String output =
         runTest("testInsertAllTypesNull", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
@@ -269,7 +269,7 @@ public class ITNodePostgresTest implements IntegrationTest {
     String output =
         runTest(
             "testInsertAllTypesPreparedStatement", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nInserted 1 row(s)\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\nInserted 1 row(s)\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
@@ -312,7 +312,7 @@ public class ITNodePostgresTest implements IntegrationTest {
 
     String output = runTest("testSelectAllTypes", getHost(), testEnv.getServer().getLocalPort());
     assertEquals(
-        "\n\nSelected {"
+        "Selected {"
             + "\"col_bigint\":\"1\","
             + "\"col_bool\":true,"
             + "\"col_bytea\":{\"type\":\"Buffer\",\"data\":[116,101,115,116]},"
@@ -333,7 +333,7 @@ public class ITNodePostgresTest implements IntegrationTest {
 
     String output = runTest("testSelectAllTypes", getHost(), testEnv.getServer().getLocalPort());
     assertEquals(
-        "\n\nSelected {"
+        "Selected {"
             + "\"col_bigint\":\"1\","
             + "\"col_bool\":null,"
             + "\"col_bytea\":null,"
@@ -359,7 +359,7 @@ public class ITNodePostgresTest implements IntegrationTest {
     String output =
         runTest("testErrorInReadWriteTransaction", getHost(), testEnv.getServer().getLocalPort());
     assertEquals(
-        "\n\nInsert error: error: com.google.api.gax.rpc.AlreadyExistsException: io.grpc.StatusRuntimeException: ALREADY_EXISTS: Row [foo] in table users already exists\n"
+        "Insert error: error: com.google.api.gax.rpc.AlreadyExistsException: io.grpc.StatusRuntimeException: ALREADY_EXISTS: Row [foo] in table users already exists\n"
             + "Second insert failed with error: error: current transaction is aborted, commands ignored until end of transaction block\n"
             + "SELECT 1 returned: 1\n",
         output);
@@ -370,7 +370,7 @@ public class ITNodePostgresTest implements IntegrationTest {
     String output =
         runTest("testReadOnlyTransaction", getHost(), testEnv.getServer().getLocalPort());
 
-    assertEquals("\n\nexecuted read-only transaction\n", output);
+    assertEquals("executed read-only transaction\n", output);
   }
 
   @Test
@@ -378,7 +378,7 @@ public class ITNodePostgresTest implements IntegrationTest {
     String output =
         runTest("testReadOnlyTransactionWithError", getHost(), testEnv.getServer().getLocalPort());
     assertEquals(
-        "\n\ncurrent transaction is aborted, commands ignored until end of transaction block\n"
+        "current transaction is aborted, commands ignored until end of transaction block\n"
             + "[ { '?column?': '2' } ]\n",
         output);
   }
@@ -389,14 +389,14 @@ public class ITNodePostgresTest implements IntegrationTest {
 
     String output = runTest("testCopyTo", getHost(), testEnv.getServer().getLocalPort());
     assertEquals(
-        "\n\n1\tt\t\\\\x74657374\t3.14\t100\t6.626\t2022-02-16 13:18:02.123456789+00\t2022-03-29\ttest\t{\"key\": \"value\"}\n",
+        "1\tt\t\\\\x74657374\t3.14\t100\t6.626\t2022-02-16 13:18:02.123456789+00\t2022-03-29\ttest\t{\"key\": \"value\"}\n",
         output);
   }
 
   @Test
   public void testCopyFrom() throws Exception {
     String output = runTest("testCopyFrom", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nFinished copy operation\n", output);
+    assertEquals("Finished copy operation\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
@@ -410,7 +410,7 @@ public class ITNodePostgresTest implements IntegrationTest {
   @Test
   public void testDmlBatch() throws Exception {
     String output = runTest("testDmlBatch", getHost(), testEnv.getServer().getLocalPort());
-    assertEquals("\n\nexecuted dml batch\n", output);
+    assertEquals("executed dml batch\n", output);
 
     DatabaseClient client = testEnv.getSpanner().getDatabaseClient(database.getId());
     try (ResultSet resultSet =
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodeJSTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodeJSTest.java
index 215af367..6d14cf2c 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodeJSTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodeJSTest.java
@@ -44,7 +44,8 @@ public interface NodeJSTest {
     String currentPath = new java.io.File(".").getCanonicalPath();
     String testFilePath = String.format("%s/src/test/nodejs/%s", currentPath, directory);
     ProcessBuilder builder = new ProcessBuilder();
-    builder.command("npm", "start", testName, host, String.format("%d", port), database);
+    builder.command(
+        "npm", "--silent", "start", testName, host, String.format("%d", port), database);
     builder.directory(new File(testFilePath));
 
     Process process = builder.start();
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodePostgresMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodePostgresMockServerTest.java
index 05d9a628..501ad1b2 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodePostgresMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/NodePostgresMockServerTest.java
@@ -79,7 +79,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testSelect1", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nSELECT 1 returned: 1\n", output);
+    assertEquals("SELECT 1 returned: 1\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -112,7 +112,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testInsert", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -149,7 +149,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testInsertTwice", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nInserted 1 row(s)\nInserted 2 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\nInserted 2 row(s)\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -190,7 +190,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testInsertAutoCommit", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -266,7 +266,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testInsertAllTypes", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -320,7 +320,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testInsertAllTypesAllNull", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -419,7 +419,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
     String output =
         runTest("testInsertAllTypesPreparedStatement", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nInserted 1 row(s)\nInserted 1 row(s)\n", output);
+    assertEquals("Inserted 1 row(s)\nInserted 1 row(s)\n", output);
 
     // node-postgres will only send one parse message when using prepared statements. It never uses
     // DescribeStatement. It will send a new DescribePortal for each time the prepared statement is
@@ -463,7 +463,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
     String output = runTest("testSelectAllTypes", getHost(), pgServer.getLocalPort());
 
     assertEquals(
-        "\n\nSelected {"
+        "Selected {"
             + "\"col_bigint\":\"1\","
             + "\"col_bool\":true,"
             + "\"col_bytea\":{\"type\":\"Buffer\",\"data\":[116,101,115,116]},"
@@ -494,7 +494,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
     String output = runTest("testSelectAllTypes", getHost(), pgServer.getLocalPort());
 
     assertEquals(
-        "\n\nSelected {"
+        "Selected {"
             + "\"col_bigint\":null,"
             + "\"col_bool\":null,"
             + "\"col_bytea\":null,"
@@ -534,7 +534,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
     String output = runTest("testErrorInReadWriteTransaction", getHost(), pgServer.getLocalPort());
 
     assertEquals(
-        "\n\nInsert error: error: com.google.api.gax.rpc.AlreadyExistsException: io.grpc.StatusRuntimeException: ALREADY_EXISTS: Row with \"name\" 'foo' already exists\n"
+        "Insert error: error: com.google.api.gax.rpc.AlreadyExistsException: io.grpc.StatusRuntimeException: ALREADY_EXISTS: Row with \"name\" 'foo' already exists\n"
             + "Second insert failed with error: error: current transaction is aborted, commands ignored until end of transaction block\n"
             + "SELECT 1 returned: 1\n",
         output);
@@ -547,7 +547,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
   public void testReadOnlyTransaction() throws Exception {
     String output = runTest("testReadOnlyTransaction", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nexecuted read-only transaction\n", output);
+    assertEquals("executed read-only transaction\n", output);
 
     List<ExecuteSqlRequest> requests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -574,7 +574,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
     String output = runTest("testReadOnlyTransactionWithError", getHost(), pgServer.getLocalPort());
 
     assertEquals(
-        "\n\ncurrent transaction is aborted, commands ignored until end of transaction block\n"
+        "current transaction is aborted, commands ignored until end of transaction block\n"
             + "[ { C: '2' } ]\n",
         output);
   }
@@ -587,9 +587,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
     String output = runTest("testCopyTo", getHost(), pgServer.getLocalPort());
 
     assertEquals(
-        "\n"
-            + "\n"
-            + "1\tt\t\\\\x74657374\t3.14\t100\t6.626\t2022-02-16 13:18:02.123456789+00\t2022-03-29\ttest\t{\"key\": \"value\"}\n",
+        "1\tt\t\\\\x74657374\t3.14\t100\t6.626\t2022-02-16 13:18:02.123456789+00\t2022-03-29\ttest\t{\"key\": \"value\"}\n",
         output);
   }
 
@@ -599,7 +597,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testCopyFrom", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nFinished copy operation\n", output);
+    assertEquals("Finished copy operation\n", output);
   }
 
   @Test
@@ -619,7 +617,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testDmlBatch", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nexecuted dml batch\n", output);
+    assertEquals("executed dml batch\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -656,7 +654,7 @@ public class NodePostgresMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("testDdlBatch", getHost(), pgServer.getLocalPort());
 
-    assertEquals("\n\nexecuted ddl batch\n", output);
+    assertEquals("executed ddl batch\n", output);
     assertEquals(1, mockDatabaseAdmin.getRequests().size());
     assertEquals(UpdateDatabaseDdlRequest.class, mockDatabaseAdmin.getRequests().get(0).getClass());
     UpdateDatabaseDdlRequest request =
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/TypeORMMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/TypeORMMockServerTest.java
index e82ddeda..2aba30d0 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/TypeORMMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/nodejs/TypeORMMockServerTest.java
@@ -101,7 +101,7 @@ public class TypeORMMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("findOneUser", pgServer.getLocalPort());
 
-    assertEquals("\n\nFound user 1 with name Timber Saw\n", output);
+    assertEquals("Found user 1 with name Timber Saw\n", output);
 
     List<ExecuteSqlRequest> executeSqlRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
@@ -164,7 +164,7 @@ public class TypeORMMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("createUser", pgServer.getLocalPort());
 
-    assertEquals("\n\nFound user 1 with name Timber Saw\n", output);
+    assertEquals("Found user 1 with name Timber Saw\n", output);
 
     // Creating the user will use a read/write transaction. The query that checks whether the record
     // already exists will however not use that transaction, as each statement is executed in
@@ -256,7 +256,7 @@ public class TypeORMMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("updateUser", pgServer.getLocalPort());
 
-    assertEquals("\n\nUpdated user 1\n", output);
+    assertEquals("Updated user 1\n", output);
 
     // Updating the user will use a read/write transaction. The query that checks whether the record
     // already exists will however not use that transaction, as each statement is executed in
@@ -347,7 +347,7 @@ public class TypeORMMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("deleteUser", pgServer.getLocalPort());
 
-    assertEquals("\n\nDeleted user 1\n", output);
+    assertEquals("Deleted user 1\n", output);
 
     // Deleting the user will use a read/write transaction. The query that checks whether the record
     // already exists will however not use that transaction, as each statement is executed in
@@ -412,7 +412,7 @@ public class TypeORMMockServerTest extends AbstractMockServerTest {
     String output = runTest("findOneAllTypes", pgServer.getLocalPort());
 
     assertEquals(
-        "\n\nFound row 1\n"
+        "Found row 1\n"
             + "AllTypes {\n"
             + "  col_bigint: '1',\n"
             + "  col_bool: true,\n"
@@ -470,7 +470,7 @@ public class TypeORMMockServerTest extends AbstractMockServerTest {
 
     String output = runTest("createAllTypes", pgServer.getLocalPort());
 
-    assertEquals("\n\nCreated one record\n", output);
+    assertEquals("Created one record\n", output);
 
     List<ExecuteSqlRequest> insertRequests =
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
index 0ec73a20..edd39d86 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
@@ -44,6 +44,7 @@ import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.StatementResult;
 import com.google.cloud.spanner.connection.StatementResult.ResultType;
 import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection.NoResult;
@@ -386,7 +387,7 @@ public class BackendConnectionTest {
 
     ExecutionException executionException =
         assertThrows(ExecutionException.class, resultFuture::get);
-    assertSame(executionException.getCause(), error);
+    assertEquals(executionException.getCause(), PGExceptionFactory.toPGException(error));
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java
index 5a523b41..d637d1b5 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/IntermediateStatementTest.java
@@ -38,9 +38,13 @@ import com.google.cloud.spanner.connection.PostgreSQLStatementParser;
 import com.google.cloud.spanner.connection.StatementResult;
 import com.google.cloud.spanner.connection.StatementResult.ResultType;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement.ResultNotReadyBehavior;
 import com.google.common.collect.ImmutableList;
+import java.util.concurrent.Future;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -316,6 +320,24 @@ public class IntermediateStatementTest {
     assertNull(transformDelete("delete from foo hwere col1=2"));
   }
 
+  @Test
+  public void testInterruptedWhileWaitingForResult() throws Exception {
+    when(connectionHandler.getSpannerConnection()).thenReturn(connection);
+    when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata);
+    Future<StatementResult> future = mock(Future.class);
+    when(future.get()).thenThrow(new InterruptedException());
+
+    String sql = "update bar set foo=1";
+    IntermediateStatement statement =
+        new IntermediateStatement(
+            mock(OptionsMetadata.class), parse(sql), Statement.of(sql), connectionHandler);
+    statement.setFutureStatementResult(future);
+    statement.initFutureResult(ResultNotReadyBehavior.BLOCK);
+
+    PGException pgException = statement.getException();
+    assertEquals(SQLState.QueryCanceled, pgException.getSQLState());
+  }
+
   private static Statement transformInsert(String sql) {
     return transformInsertToSelectParams(mock(Connection.class), sql, extractParameters(sql));
   }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
index bd381dba..79c5f1e6 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
@@ -43,6 +43,9 @@ import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.StatementResult;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.ProxyServer;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
@@ -257,7 +260,8 @@ public class StatementTest {
     backendConnection.flush();
 
     assertTrue(intermediateStatement.hasException());
-    assertEquals(thrownException, intermediateStatement.getException());
+    assertEquals(
+        PGExceptionFactory.newPGException("test error"), intermediateStatement.getException());
   }
 
   @Test
@@ -422,8 +426,9 @@ public class StatementTest {
     backendConnection.flush();
 
     assertTrue(intermediateStatement.hasException());
-    SpannerException exception = intermediateStatement.getException();
-    assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
+    PGException exception = intermediateStatement.getException();
+    assertEquals(SQLState.RaiseException, exception.getSQLState());
+    assertEquals("test error", exception.getMessage());
   }
 
   @Test
@@ -487,10 +492,10 @@ public class StatementTest {
 
     backendConnection.flush();
 
-    SpannerException thrown = assertThrows(SpannerException.class, statement::getUpdateCount);
-    assertEquals(ErrorCode.INVALID_ARGUMENT, thrown.getErrorCode());
+    PGException thrown = assertThrows(PGException.class, statement::getUpdateCount);
+    assertEquals(SQLState.DataException, thrown.getSQLState());
     assertEquals(
-        "INVALID_ARGUMENT: Invalid COPY data: Row length mismatched. Expected 2 columns, but only found 1",
+        "Invalid COPY data: Row length mismatched. Expected 2 columns, but only found 1",
         thrown.getMessage());
 
     statement.close();
@@ -634,10 +639,10 @@ public class StatementTest {
 
     backendConnection.flush();
 
-    SpannerException thrown = assertThrows(SpannerException.class, copyStatement::getUpdateCount);
-    assertEquals(ErrorCode.INVALID_ARGUMENT, thrown.getErrorCode());
+    PGException thrown = assertThrows(PGException.class, copyStatement::getUpdateCount);
+    assertEquals(SQLState.DataException, thrown.getSQLState());
     assertEquals(
-        "INVALID_ARGUMENT: Invalid COPY data: Row length mismatched. Expected 2 columns, but only found 1",
+        "Invalid COPY data: Row length mismatched. Expected 2 columns, but only found 1",
         thrown.getMessage());
 
     copyStatement.close();
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyDataReceiverTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyDataReceiverTest.java
index a0658916..d938e566 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyDataReceiverTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/utils/CopyDataReceiverTest.java
@@ -22,9 +22,11 @@ import static org.mockito.Mockito.when;
 
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
-import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.ConnectionStatus;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement;
 import java.io.ByteArrayOutputStream;
@@ -38,18 +40,17 @@ public class CopyDataReceiverTest {
 
   @Test
   public void testCopyStatementWithException() {
-    SpannerException exception =
-        SpannerExceptionFactory.newSpannerException(
-            ErrorCode.INVALID_ARGUMENT, "Invalid copy statement");
+    PGException exception =
+        PGExceptionFactory.newPGException("Invalid copy statement", SQLState.SyntaxError);
     CopyStatement statement = mock(CopyStatement.class);
     when(statement.hasException()).thenReturn(true);
     when(statement.getException()).thenReturn(exception);
     ConnectionHandler connectionHandler = mock(ConnectionHandler.class);
 
     CopyDataReceiver receiver = new CopyDataReceiver(statement, connectionHandler);
-    SpannerException spannerException = assertThrows(SpannerException.class, receiver::handleCopy);
+    PGException pgException = assertThrows(PGException.class, receiver::handleCopy);
 
-    assertSame(exception, spannerException);
+    assertSame(exception, pgException);
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java
index ac776c0b..b6881a67 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/utils/MutationWriterTest.java
@@ -17,6 +17,7 @@ package com.google.cloud.spanner.pgadapter.utils;
 import static com.google.cloud.spanner.pgadapter.utils.MutationWriter.calculateSize;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -33,11 +34,11 @@ import com.google.cloud.Date;
 import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.DatabaseClient;
 import com.google.cloud.spanner.Mutation;
-import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement.Format;
@@ -56,6 +57,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
@@ -73,7 +75,7 @@ public class MutationWriterTest {
 
   @AfterClass
   public static void shutdownExecutor() {
-    executor.shutdown();
+    //    executor.shutdown();
   }
 
   @Test
@@ -207,9 +209,9 @@ public class MutationWriterTest {
             }
           });
 
-      SpannerException exception = assertThrows(SpannerException.class, mutationWriter::call);
+      PGException exception = assertThrows(PGException.class, mutationWriter::call);
       assertEquals(
-          "FAILED_PRECONDITION: Record count: 2 has exceeded the limit: 1.\n"
+          "Record count: 2 has exceeded the limit: 1.\n"
               + "\n"
               + "The number of mutations per record is equal to the number of columns in the record plus the number of indexed columns in the record. The maximum number of mutations in one transaction is 1.\n"
               + "\n"
@@ -314,7 +316,7 @@ public class MutationWriterTest {
             }
           });
 
-      SpannerException exception = assertThrows(SpannerException.class, mutationWriter::call);
+      PGException exception = assertThrows(PGException.class, mutationWriter::call);
       assertTrue(exception.getMessage().contains("Commit size: 40 has exceeded the limit: 30"));
 
       verify(connection, never()).write(anyIterable());
@@ -605,4 +607,46 @@ public class MutationWriterTest {
       assertEquals(String.format("Type: %s", type), 1, mutation.asMap().size());
     }
   }
+
+  @Test
+  public void testWriteAfterClose() throws Exception {
+    Map<String, Type> tableColumns = ImmutableMap.of("number", Type.int64(), "name", Type.string());
+    CSVFormat format =
+        CSVFormat.POSTGRESQL_TEXT
+            .builder()
+            .setHeader(tableColumns.keySet().toArray(new String[0]))
+            .build();
+    SessionState sessionState = new SessionState(mock(OptionsMetadata.class));
+    Connection connection = mock(Connection.class);
+    DatabaseClient databaseClient = mock(DatabaseClient.class);
+    when(connection.getDatabaseClient()).thenReturn(databaseClient);
+    MutationWriter mutationWriter =
+        new MutationWriter(
+            sessionState,
+            CopyTransactionMode.ImplicitAtomic,
+            connection,
+            "numbers",
+            tableColumns,
+            /* indexedColumnsCount = */ 1,
+            Format.TEXT,
+            format,
+            false);
+
+    CountDownLatch latch = new CountDownLatch(1);
+    Future<?> fut =
+        executor.submit(
+            () -> {
+              mutationWriter.addCopyData(
+                  "1\t\"One\"\n2\t\"Two\"\n".getBytes(StandardCharsets.UTF_8));
+              mutationWriter.close();
+              latch.await();
+              mutationWriter.addCopyData(
+                  "1\t\"One\"\n2\t\"Two\"\n".getBytes(StandardCharsets.UTF_8));
+              return null;
+            });
+
+    mutationWriter.call();
+    latch.countDown();
+    assertNull(fut.get());
+  }
 }
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 9b08e5fd..9140d50c 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
@@ -33,11 +33,8 @@ import static org.mockito.Mockito.when;
 
 import com.google.cloud.spanner.DatabaseClient;
 import com.google.cloud.spanner.Dialect;
-import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ReadContext;
 import com.google.cloud.spanner.ResultSet;
-import com.google.cloud.spanner.SpannerException;
-import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractStatementParser;
 import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
@@ -47,6 +44,9 @@ import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.ConnectionStatus;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.QueryMode;
 import com.google.cloud.spanner.pgadapter.ProxyServer;
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.SslMode;
@@ -945,8 +945,8 @@ public class ProtocolTest {
     ByteArrayOutputStream result = new ByteArrayOutputStream();
     DataOutputStream outputStream = new DataOutputStream(result);
 
-    SpannerException testException =
-        SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "test error");
+    PGException testException =
+        PGExceptionFactory.newPGException("test error", SQLState.SyntaxError);
     when(intermediatePortalStatement.hasException()).thenReturn(true);
     when(intermediatePortalStatement.getException()).thenReturn(testException);
     when(connectionHandler.getPortal(anyString())).thenReturn(intermediatePortalStatement);