Repository: GoogleCloudPlatform/pgadapter
Pull request #1045 (2023-09-13 15:47:24)

feat: translate Aborted to SerializationFailure errors
Cloud Spanner returns Aborted if a transaction failed due to conflicts or for other reasons need to be retried. PostgreSQL returns serialization_failure in such cases. Both error codes indicate to the client that the transaction should be retried.

See also
https://www.postgresql.org/docs/current/mvcc-serialization-failure-handling.html and https://www.postgresql.org/docs/current/errcodes-appendix.html.

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

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect    #1045      +/-   ##
========================================================
- Coverage                 90.29%   88.55%   -1.74%     
+ Complexity                 2555     2506      -49     
========================================================
  Files                       141      141              
  Lines                      8508     8517       +9     
  Branches                   1264     1265       +1     
========================================================
- Hits                       7682     7542     -140     
- Misses                      561      675     +114     
- Partials                    265      300      +35     
```

| [Flag](https://app.codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/1045/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [all_tests](https://app.codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/1045/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | `88.55% <100.00%> (-1.74%)` | :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.

| [Files Changed](https://app.codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/1045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...ud/spanner/pgadapter/error/PGExceptionFactory.java](https://app.codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/1045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL2Vycm9yL1BHRXhjZXB0aW9uRmFjdG9yeS5qYXZh) | `95.55% <100.00%> (-4.45%)` | :arrow_down: |
| [...google/cloud/spanner/pgadapter/error/SQLState.java](https://app.codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/1045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL2Vycm9yL1NRTFN0YXRlLmphdmE=) | `99.45% <100.00%> (+0.01%)` | :arrow_up: |

... and [4 files with indirect coverage changes](https://app.codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/1045/indirect-changes?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/PGExceptionFactory.java b/src/main/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactory.java
index 381322fd..c0ec3625 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
@@ -74,6 +74,13 @@ public class PGExceptionFactory {
 
   /** Converts the given {@link SpannerException} to a {@link PGException}. */
   public static PGException toPGException(SpannerException spannerException) {
+    if (spannerException.getErrorCode() == ErrorCode.ABORTED) {
+      return PGException.newBuilder(extractMessage(spannerException))
+          // This is the equivalent of an Aborted transaction error in Cloud Spanner.
+          // A client should be prepared to retry this.
+          .setSQLState(SQLState.SerializationFailure)
+          .build();
+    }
     if (spannerException.getErrorCode() == ErrorCode.INVALID_ARGUMENT
         && RELATION_NOT_FOUND_PATTERN.matcher(spannerException.getMessage()).find()) {
       return PGException.newBuilder(extractMessage(spannerException))
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 86f6a91d..a019918e 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
@@ -156,6 +156,13 @@ public enum SQLState {
   SavepointException("3B000"),
   InvalidSavepointSpecification("3B001"),
 
+  // Class 40 — Transaction Rollback
+  TransactionRollback("40000"),
+  TransactionIntegrityConstraintViolation("40002"),
+  SerializationFailure("40001"),
+  StatementCompletionUnknown("40003"),
+  DeadlockDetected("40P01"),
+
   // Class 42 — Syntax Error or Access Rule Violation
   SyntaxErrorOrAccessRuleViolation("42000"),
   SyntaxError("42601"),


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
index 69d4f847..27088aff 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -5058,6 +5058,31 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testTransactionAbortedByCloudSpanner() throws SQLException {
+    String sql = "SELECT * FROM random";
+    RandomResultSetGenerator generator = new RandomResultSetGenerator(5, Dialect.POSTGRESQL);
+    mockSpanner.putStatementResult(StatementResult.query(Statement.of(sql), generator.generate()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.setAutoCommit(false);
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        while (resultSet.next()) {
+          // just consume the results.
+        }
+      }
+      // Change the results to force any retry of the transaction to fail.
+      mockSpanner.putStatementResult(
+          StatementResult.query(Statement.of(sql), generator.generate()));
+      mockSpanner.abortNextStatement();
+      PSQLException exception = assertThrows(PSQLException.class, connection::commit);
+      assertNotNull(exception.getServerErrorMessage());
+      assertEquals(
+          SQLState.SerializationFailure.toString(),
+          exception.getServerErrorMessage().getSQLState());
+    }
+  }
+
   @Ignore("Only used for manual performance testing")
   @Test
   public void testBasePerformance() throws SQLException {
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactoryTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactoryTest.java
index c504e617..727b57c0 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactoryTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/error/PGExceptionFactoryTest.java
@@ -52,6 +52,12 @@ public class PGExceptionFactoryTest {
                     /* cause= */ null,
                     GrpcStatusCode.of(Code.NOT_FOUND),
                     /* retryable= */ false))));
+    assertEquals(
+        SQLState.SerializationFailure,
+        toPGException(
+                SpannerExceptionFactory.newSpannerException(
+                    ErrorCode.ABORTED, "The transaction was aborted"))
+            .getSQLState());
   }
 
   @Test