Repository: GoogleCloudPlatform/pgadapter
Pull request #662 (2023-02-15 18:42:34)

chore: parse SAVEPOINT commands
Adds support for parsing `SAVEPOINT`, `RELEASE` and `ROLLBACK TO` statements. Executing `ROLLBACK TO` will cause an `Unimplemented` error. The other two statements are silently ignored.

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#662](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (4b86414) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/68d0b14017c8d474d0a9cd44272eb366091edee4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (68d0b14) will **decrease** coverage by `0.03%`.
> The diff coverage is `87.80%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #662      +/-   ##
========================================================
- Coverage                 89.07%   89.05%   -0.03%     
- Complexity                 2192     2223      +31     
========================================================
  Files                       126      129       +3     
  Lines                      7311     7434     +123     
  Branches                   1042     1055      +13     
========================================================
+ Hits                       6512     6620     +108     
- Misses                      559      571      +12     
- Partials                    240      243       +3     
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `89.05% <87.80%> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...d/spanner/pgadapter/wireprotocol/QueryMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9RdWVyeU1lc3NhZ2UuamF2YQ==) | `100.00% <ø> (ø)` | |
| [...anner/pgadapter/statements/SavepointStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvU2F2ZXBvaW50U3RhdGVtZW50LmphdmE=) | `80.76% <80.76%> (ø)` | |
| [...spanner/pgadapter/statements/ReleaseStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvUmVsZWFzZVN0YXRlbWVudC5qYXZh) | `81.48% <81.48%> (ø)` | |
| [...nner/pgadapter/statements/RollbackToStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvUm9sbGJhY2tUb1N0YXRlbWVudC5qYXZh) | `84.84% <84.84%> (ø)` | |
| [...panner/pgadapter/statements/BackendConnection.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvQmFja2VuZENvbm5lY3Rpb24uamF2YQ==) | `93.93% <100.00%> (+0.29%)` | :arrow_up: |
| [...d/spanner/pgadapter/wireprotocol/ParseMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/662?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9QYXJzZU1lc3NhZ2UuamF2YQ==) | `93.15% <100.00%> (+1.08%)` | :arrow_up: |

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


Patch:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
index eca718b7..2a849e74 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
@@ -588,6 +588,67 @@ public class BackendConnection {
     }
   }
 
+  private final class Savepoint extends BufferedStatement<StatementResult> {
+    private final SavepointStatement savepointStatement;
+
+    Savepoint(SavepointStatement savepointStatement) {
+      super(savepointStatement.parsedStatement, savepointStatement.originalStatement);
+      this.savepointStatement = savepointStatement;
+    }
+
+    @Override
+    boolean isUpdate() {
+      return false;
+    }
+
+    @Override
+    void execute() {
+      result.set(NO_RESULT);
+    }
+  }
+
+  private final class Release extends BufferedStatement<StatementResult> {
+    private final ReleaseStatement releaseStatement;
+
+    Release(ReleaseStatement releaseStatement) {
+      super(releaseStatement.parsedStatement, releaseStatement.originalStatement);
+      this.releaseStatement = releaseStatement;
+    }
+
+    @Override
+    boolean isUpdate() {
+      return false;
+    }
+
+    @Override
+    void execute() {
+      result.set(NO_RESULT);
+    }
+  }
+
+  private final class RollbackTo extends BufferedStatement<StatementResult> {
+    private final RollbackToStatement rollbackToStatement;
+
+    RollbackTo(RollbackToStatement rollbackToStatement) {
+      super(rollbackToStatement.parsedStatement, rollbackToStatement.originalStatement);
+      this.rollbackToStatement = rollbackToStatement;
+    }
+
+    @Override
+    boolean isUpdate() {
+      return false;
+    }
+
+    @Override
+    void execute() {
+      throw setAndReturn(
+          result,
+          PGExceptionFactory.newPGException(
+              "Statement 'ROLLBACK [WORK | TRANSACTION] TO [SAVEPOINT] savepoint_name' is not supported",
+              SQLState.FeatureNotSupported));
+    }
+  }
+
   private static final ImmutableMap<String, LocalStatement> EMPTY_LOCAL_STATEMENTS =
       ImmutableMap.of();
   private static final StatementResult NO_RESULT = new NoResult();
@@ -697,6 +758,24 @@ public class BackendConnection {
     return truncate.result;
   }
 
+  public Future<StatementResult> execute(SavepointStatement savepointStatement) {
+    Savepoint savepoint = new Savepoint(savepointStatement);
+    bufferedStatements.add(savepoint);
+    return savepoint.result;
+  }
+
+  public Future<StatementResult> execute(ReleaseStatement releaseStatement) {
+    Release savepoint = new Release(releaseStatement);
+    bufferedStatements.add(savepoint);
+    return savepoint.result;
+  }
+
+  public Future<StatementResult> execute(RollbackToStatement rollbackToStatement) {
+    RollbackTo savepoint = new RollbackTo(rollbackToStatement);
+    bufferedStatements.add(savepoint);
+    return savepoint.result;
+  }
+
   /** Flushes the buffered statements to Spanner. */
   void flush() {
     flush(false);
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/ReleaseStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ReleaseStatement.java
new file mode 100644
index 00000000..2d5ce642
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ReleaseStatement.java
@@ -0,0 +1,122 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SimpleParser.unquoteOrFoldIdentifier;
+
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+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.SimpleParser.TableOrIndexName;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class ReleaseStatement extends IntermediatePortalStatement {
+  static class ParsedReleaseStatement {
+    final String name;
+
+    ParsedReleaseStatement(String name) {
+      this.name = name;
+    }
+  }
+
+  private final ParsedReleaseStatement parsedReleaseStatement;
+
+  public ReleaseStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(
+        name,
+        new IntermediatePreparedStatement(
+            connectionHandler,
+            options,
+            name,
+            NO_PARAMETER_TYPES,
+            parsedStatement,
+            originalStatement),
+        NO_PARAMS,
+        ImmutableList.of(),
+        ImmutableList.of());
+    this.parsedReleaseStatement = parse(originalStatement.getSql());
+  }
+
+  static ParsedReleaseStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    // https://www.postgresql.org/docs/current/sql-release-savepoint.html
+    // RELEASE [ SAVEPOINT ] savepoint_name
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("release")) {
+      throw PGExceptionFactory.newPGException("not a valid RELEASE statement: " + sql);
+    }
+    parser.eatKeyword("savepoint");
+    TableOrIndexName name = parser.readTableOrIndexName();
+    if (name == null || name.schema != null) {
+      throw PGExceptionFactory.newPGException("invalid savepoint name", SQLState.SyntaxError);
+    }
+    String savepointName = unquoteOrFoldIdentifier(name.name);
+    parser.throwIfHasMoreTokens();
+
+    return new ParsedReleaseStatement(savepointName);
+  }
+
+  public String getSavepointName() {
+    return parsedReleaseStatement.name;
+  }
+
+  @Override
+  public String getCommandTag() {
+    return "RELEASE";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    this.executed = true;
+    setFutureStatementResult(backendConnection.execute(this));
+  }
+
+  @Override
+  public Future<StatementResult> describeAsync(BackendConnection backendConnection) {
+    // Return null to indicate that this RELEASE statement does not return any
+    // RowDescriptionResponse.
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public IntermediatePortalStatement createPortal(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    // RELEASE does not support binding any parameters, so we just return the same statement.
+    return this;
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/RollbackToStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/RollbackToStatement.java
new file mode 100644
index 00000000..45b1368b
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/RollbackToStatement.java
@@ -0,0 +1,133 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SimpleParser.unquoteOrFoldIdentifier;
+
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+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.SimpleParser.TableOrIndexName;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class RollbackToStatement extends IntermediatePortalStatement {
+  static class ParsedRollbackToStatement {
+    final String name;
+
+    ParsedRollbackToStatement(String name) {
+      this.name = name;
+    }
+  }
+
+  private final ParsedRollbackToStatement parsedRollbackToStatement;
+
+  public RollbackToStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(
+        name,
+        new IntermediatePreparedStatement(
+            connectionHandler,
+            options,
+            name,
+            NO_PARAMETER_TYPES,
+            parsedStatement,
+            originalStatement),
+        NO_PARAMS,
+        ImmutableList.of(),
+        ImmutableList.of());
+    this.parsedRollbackToStatement = parse(originalStatement.getSql());
+  }
+
+  static ParsedRollbackToStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    // https://www.postgresql.org/docs/current/sql-rollback-to.html
+    // ROLLBACK [ WORK | TRANSACTION ] TO [ SAVEPOINT ] savepoint_name
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("rollback")) {
+      throw PGExceptionFactory.newPGException(
+          "not a valid ROLLBACK [WORK | TRANSACTION] TO statement: " + sql);
+    }
+    if (parser.peekKeyword("work")) {
+      parser.eatKeyword("work");
+    } else if (parser.peekKeyword("transaction")) {
+      parser.eatKeyword("transaction");
+    }
+    if (!parser.eatKeyword("to")) {
+      throw PGExceptionFactory.newPGException(
+          "missing 'TO' keyword in ROLLBACK [WORK | TRANSACTION] TO statement: " + sql);
+    }
+    parser.eatKeyword("savepoint");
+    TableOrIndexName name = parser.readTableOrIndexName();
+    if (name == null || name.schema != null) {
+      throw PGExceptionFactory.newPGException("invalid savepoint name", SQLState.SyntaxError);
+    }
+    String savepointName = unquoteOrFoldIdentifier(name.name);
+    parser.throwIfHasMoreTokens();
+
+    return new ParsedRollbackToStatement(savepointName);
+  }
+
+  public String getSavepointName() {
+    return parsedRollbackToStatement.name;
+  }
+
+  @Override
+  public String getCommandTag() {
+    // The command tag for 'ROLLBACK [WORK | TRANSACTION] TO [SAVEPOINT] is just 'ROLLBACK'.
+    return "ROLLBACK";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    this.executed = true;
+    setFutureStatementResult(backendConnection.execute(this));
+  }
+
+  @Override
+  public Future<StatementResult> describeAsync(BackendConnection backendConnection) {
+    // Return null to indicate that this ROLLBACK statement does not return any
+    // RowDescriptionResponse.
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public IntermediatePortalStatement createPortal(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    // ROLLBACK does not support binding any parameters, so we just return the same statement.
+    return this;
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SavepointStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SavepointStatement.java
new file mode 100644
index 00000000..a2f81e1a
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SavepointStatement.java
@@ -0,0 +1,121 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SimpleParser.unquoteOrFoldIdentifier;
+
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+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.SimpleParser.TableOrIndexName;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class SavepointStatement extends IntermediatePortalStatement {
+  static class ParsedSavepointStatement {
+    final String name;
+
+    ParsedSavepointStatement(String name) {
+      this.name = name;
+    }
+  }
+
+  private final ParsedSavepointStatement parsedSavepointStatement;
+
+  public SavepointStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(
+        name,
+        new IntermediatePreparedStatement(
+            connectionHandler,
+            options,
+            name,
+            NO_PARAMETER_TYPES,
+            parsedStatement,
+            originalStatement),
+        NO_PARAMS,
+        ImmutableList.of(),
+        ImmutableList.of());
+    this.parsedSavepointStatement = parse(originalStatement.getSql());
+  }
+
+  static ParsedSavepointStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    // https://www.postgresql.org/docs/current/sql-savepoint.html
+    // SAVEPOINT savepoint_name
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("savepoint")) {
+      throw PGExceptionFactory.newPGException("not a valid SAVEPOINT statement: " + sql);
+    }
+    TableOrIndexName name = parser.readTableOrIndexName();
+    if (name == null || name.schema != null) {
+      throw PGExceptionFactory.newPGException("invalid savepoint name", SQLState.SyntaxError);
+    }
+    String savepointName = unquoteOrFoldIdentifier(name.name);
+    parser.throwIfHasMoreTokens();
+
+    return new ParsedSavepointStatement(savepointName);
+  }
+
+  public String getSavepointName() {
+    return parsedSavepointStatement.name;
+  }
+
+  @Override
+  public String getCommandTag() {
+    return "SAVEPOINT";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    this.executed = true;
+    setFutureStatementResult(backendConnection.execute(this));
+  }
+
+  @Override
+  public Future<StatementResult> describeAsync(BackendConnection backendConnection) {
+    // Return null to indicate that this SAVEPOINT statement does not return any
+    // RowDescriptionResponse.
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public IntermediatePortalStatement createPortal(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    // SAVEPOINT does not support binding any parameters, so we just return the same statement.
+    return this;
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
index 0eddd36c..bf08103e 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
@@ -19,6 +19,9 @@ import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.COPY;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.DEALLOCATE;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.EXECUTE;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.PREPARE;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.RELEASE;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.ROLLBACK;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.SAVEPOINT;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.TRUNCATE;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.VACUUM;
 
@@ -27,6 +30,7 @@ import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractStatementParser;
 import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection;
 import com.google.cloud.spanner.pgadapter.statements.CopyStatement;
@@ -35,6 +39,9 @@ import com.google.cloud.spanner.pgadapter.statements.ExecuteStatement;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePreparedStatement;
 import com.google.cloud.spanner.pgadapter.statements.InvalidStatement;
 import com.google.cloud.spanner.pgadapter.statements.PrepareStatement;
+import com.google.cloud.spanner.pgadapter.statements.ReleaseStatement;
+import com.google.cloud.spanner.pgadapter.statements.RollbackToStatement;
+import com.google.cloud.spanner.pgadapter.statements.SavepointStatement;
 import com.google.cloud.spanner.pgadapter.statements.TruncateStatement;
 import com.google.cloud.spanner.pgadapter.statements.VacuumStatement;
 import com.google.cloud.spanner.pgadapter.wireoutput.ParseCompleteResponse;
@@ -141,6 +148,30 @@ public class ParseMessage extends AbstractQueryProtocolMessage {
             name,
             parsedStatement,
             originalStatement);
+      } else if (isCommand(SAVEPOINT, originalStatement.getSql())) {
+        return new SavepointStatement(
+            connectionHandler,
+            connectionHandler.getServer().getOptions(),
+            name,
+            parsedStatement,
+            originalStatement);
+      } else if (isCommand(RELEASE, originalStatement.getSql())) {
+        return new ReleaseStatement(
+            connectionHandler,
+            connectionHandler.getServer().getOptions(),
+            name,
+            parsedStatement,
+            originalStatement);
+      } else if (isCommand(ROLLBACK, originalStatement.getSql())
+          && parsedStatement.getType() == StatementType.UNKNOWN) {
+        // ROLLBACK [WORK | TRANSACTION] TO [SAVEPOINT] savepoint_name is not recognized by the
+        // Connection API as any known statement.
+        return new RollbackToStatement(
+            connectionHandler,
+            connectionHandler.getServer().getOptions(),
+            name,
+            parsedStatement,
+            originalStatement);
       } else {
         return new IntermediatePreparedStatement(
             connectionHandler,
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
index 4c189fad..47d805d8 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
@@ -30,7 +30,9 @@ public class QueryMessage extends ControlMessage {
   public static final String DEALLOCATE = "DEALLOCATE";
   public static final String VACUUM = "VACUUM";
   public static final String TRUNCATE = "TRUNCATE";
-
+  public static final String SAVEPOINT = "SAVEPOINT";
+  public static final String RELEASE = "RELEASE";
+  public static final String ROLLBACK = "ROLLBACK";
   private final Statement originalStatement;
   private final SimpleQueryStatement simpleQueryStatement;
 


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 30c1a332..21c19c54 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -92,9 +92,11 @@ import org.junit.runners.Parameterized.Parameters;
 import org.postgresql.PGConnection;
 import org.postgresql.PGStatement;
 import org.postgresql.core.Oid;
+import org.postgresql.jdbc.PSQLSavepoint;
 import org.postgresql.jdbc.PgStatement;
 import org.postgresql.util.PGobject;
 import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
 
 @RunWith(Parameterized.class)
 public class JdbcMockServerTest extends AbstractMockServerTest {
@@ -4033,6 +4035,42 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     assertEquals(sql, request.getStatements(0).getSql());
   }
 
+  @Test
+  public void testUnnamedSavepoint() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.setAutoCommit(false);
+      assertNotNull(connection.setSavepoint());
+    }
+  }
+
+  @Test
+  public void testNamedSavepoint() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.setAutoCommit(false);
+      assertEquals("my-savepoint", connection.setSavepoint("my-savepoint").getSavepointName());
+    }
+  }
+
+  @Test
+  public void testReleaseSavepoint() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.setAutoCommit(false);
+      PSQLSavepoint savepoint = new PSQLSavepoint("my-savepoint");
+      connection.releaseSavepoint(savepoint);
+    }
+  }
+
+  @Test
+  public void testRollbackToSavepoint() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.setAutoCommit(false);
+      PSQLSavepoint savepoint = new PSQLSavepoint("my-savepoint");
+      PSQLException exception =
+          assertThrows(PSQLException.class, () -> connection.rollback(savepoint));
+      assertEquals(PSQLState.NOT_IMPLEMENTED.getState(), exception.getSQLState());
+    }
+  }
+
   @Test
   public void testDescribeAndExecute() throws SQLException {
     String sql = "delete from foo";
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 6cb47f7d..5ed442c8 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
@@ -768,12 +768,9 @@ public class GormMockServerTest extends AbstractMockServerTest {
     mockSpanner.putStatementResult(
         StatementResult.update(Statement.newBuilder(sql).bind("p1").to("2").build(), 1L));
 
-    // 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 25P02)",
-        res);
-    assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
+    assertNull(res);
+    assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/ReleaseStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/ReleaseStatementTest.java
new file mode 100644
index 00000000..af7a8505
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/ReleaseStatementTest.java
@@ -0,0 +1,62 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.ReleaseStatement.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ReleaseStatementTest {
+
+  @Test
+  public void testParse() {
+    assertEquals("foo", parse("release foo").name);
+    assertEquals("foo", parse("release FOO").name);
+    assertEquals("foo", parse("release\tfoo").name);
+    assertEquals("foo", parse("release\nfoo").name);
+    assertEquals("foo", parse("release/*comment*/foo").name);
+    assertEquals("foo", parse("release \"foo\"").name);
+    assertEquals("Foo", parse("release \"Foo\"").name);
+    assertEquals("savepoint", parse("release \"savepoint\"").name);
+    assertEquals("foo", parse("/* this will release a savepoint */ release foo").name);
+
+    assertEquals("foo", parse("release savepoint foo").name);
+    assertEquals("foo", parse("release SAVEPOINT FOO").name);
+    assertEquals("foo", parse("release\tSavepoint\tfoo").name);
+    assertEquals("foo", parse("release\nsavepoint/**/foo").name);
+    assertEquals("foo", parse("release/*comment*/savepoint foo").name);
+    assertEquals("foo", parse("release savepoint \"foo\"").name);
+    assertEquals("Foo", parse("release savepoint \"Foo\"").name);
+    assertEquals("savepoint", parse("release savepoint \"savepoint\"").name);
+    assertEquals("foo", parse("/* this will release a savepoint */ release savepoint foo").name);
+
+    assertThrows(PGException.class, () -> parse("releas my_savepoint"));
+    assertThrows(PGException.class, () -> parse("release"));
+    assertThrows(PGException.class, () -> parse("release my_savepoint foo"));
+    assertThrows(PGException.class, () -> parse("release foo.bar"));
+    assertThrows(PGException.class, () -> parse("release (foo)"));
+
+    assertThrows(PGException.class, () -> parse("release savepoint"));
+    assertThrows(PGException.class, () -> parse("release savepoint my_savepoint foo"));
+    assertThrows(PGException.class, () -> parse("release savepoint foo.bar"));
+    assertThrows(PGException.class, () -> parse("release savepoint (foo)"));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/RollbackToStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/RollbackToStatementTest.java
new file mode 100644
index 00000000..6cca74b8
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/RollbackToStatementTest.java
@@ -0,0 +1,64 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.RollbackToStatement.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RollbackToStatementTest {
+
+  @Test
+  public void testParse() {
+    assertEquals("foo", parse("rollback to foo").name);
+    assertEquals("foo", parse("rollback to FOO").name);
+    assertEquals("foo", parse("rollback\tto\tfoo").name);
+    assertEquals("foo", parse("rollback\nto\nfoo").name);
+    assertEquals("foo", parse("rollback/*comment*/to foo").name);
+    assertEquals("foo", parse("rollback to \"foo\"").name);
+    assertEquals("Foo", parse("rollback to \"Foo\"").name);
+    assertEquals("savepoint", parse("rollback to \"savepoint\"").name);
+    assertEquals("foo", parse("/* this will rollback a savepoint */ rollback to foo").name);
+
+    assertEquals("foo", parse("rollback transaction to foo").name);
+    assertEquals("foo", parse("rollback transaction to SAVEPOINT FOO").name);
+    assertEquals("foo", parse("rollback work to \tSavepoint\tfoo").name);
+    assertEquals("foo", parse("rollback transaction to \nsavepoint/**/foo").name);
+    assertEquals("foo", parse("rollback/*comment*/work to foo").name);
+    assertEquals("foo", parse("rollback work to savepoint \"foo\"").name);
+    assertEquals("Foo", parse("rollback to savepoint \"Foo\"").name);
+    assertEquals("savepoint", parse("rollback to \"savepoint\"").name);
+    assertEquals(
+        "foo", parse("/* this will rollback a savepoint */ rollback to savepoint foo").name);
+
+    assertThrows(PGException.class, () -> parse("rollbak to my_savepoint"));
+    assertThrows(PGException.class, () -> parse("rollback"));
+    assertThrows(PGException.class, () -> parse("rollback to"));
+    assertThrows(PGException.class, () -> parse("rollback to my_savepoint foo"));
+    assertThrows(PGException.class, () -> parse("rollback to foo.bar"));
+    assertThrows(PGException.class, () -> parse("rollback to (foo)"));
+
+    assertThrows(PGException.class, () -> parse("rollback work to savepoint"));
+    assertThrows(PGException.class, () -> parse("rollback transaction to my_savepoint foo"));
+    assertThrows(PGException.class, () -> parse("rollback work to foo.bar"));
+    assertThrows(PGException.class, () -> parse("rollback to savepoint (foo)"));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/SavepointStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SavepointStatementTest.java
new file mode 100644
index 00000000..e68ebb1c
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SavepointStatementTest.java
@@ -0,0 +1,47 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SavepointStatement.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SavepointStatementTest {
+
+  @Test
+  public void testParse() {
+    assertEquals("foo", parse("savepoint foo").name);
+    assertEquals("foo", parse("savepoint FOO").name);
+    assertEquals("foo", parse("savepoint\tfoo").name);
+    assertEquals("foo", parse("savepoint\nfoo").name);
+    assertEquals("foo", parse("savepoint/*comment*/foo").name);
+    assertEquals("foo", parse("savepoint \"foo\"").name);
+    assertEquals("Foo", parse("savepoint \"Foo\"").name);
+    assertEquals("savepoint", parse("savepoint \"savepoint\"").name);
+    assertEquals("foo", parse("/* this will set a savepoint */ savepoint foo").name);
+
+    assertThrows(PGException.class, () -> parse("svepoint foo"));
+    assertThrows(PGException.class, () -> parse("savepoint"));
+    assertThrows(PGException.class, () -> parse("savepoint my_savepoint foo"));
+    assertThrows(PGException.class, () -> parse("savepoint foo.bar"));
+    assertThrows(PGException.class, () -> parse("savepoint (foo)"));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessageTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessageTest.java
new file mode 100644
index 00000000..27744203
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessageTest.java
@@ -0,0 +1,111 @@
+// Copyright 2023 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.wireprotocol;
+
+import static com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage.createStatement;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.ProxyServer;
+import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection;
+import com.google.cloud.spanner.pgadapter.statements.IntermediatePreparedStatement;
+import com.google.cloud.spanner.pgadapter.statements.ReleaseStatement;
+import com.google.cloud.spanner.pgadapter.statements.RollbackToStatement;
+import com.google.cloud.spanner.pgadapter.statements.SavepointStatement;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ParseMessageTest {
+  private static final AbstractStatementParser PARSER =
+      AbstractStatementParser.getInstance(Dialect.POSTGRESQL);
+
+  private static ConnectionHandler connectionHandler;
+
+  @BeforeClass
+  public static void setupMockConnection() {
+    connectionHandler = mock(ConnectionHandler.class);
+    ProxyServer server = mock(ProxyServer.class);
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    ConnectionMetadata connectionMetadata = mock(ConnectionMetadata.class);
+    when(server.getOptions()).thenReturn(options);
+    when(connectionHandler.getServer()).thenReturn(server);
+    when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata);
+  }
+
+  @Test
+  public void testCreateStatementSavepoint() throws Exception {
+    Statement statement = Statement.of("savepoint foo");
+    IntermediatePreparedStatement preparedStatement =
+        createStatement(connectionHandler, "", PARSER.parse(statement), statement, new int[] {});
+    assertEquals(SavepointStatement.class, preparedStatement.getClass());
+    SavepointStatement savepointStatement = (SavepointStatement) preparedStatement;
+    assertEquals("foo", savepointStatement.getSavepointName());
+    assertEquals("SAVEPOINT", savepointStatement.getCommandTag());
+    assertEquals(StatementType.CLIENT_SIDE, savepointStatement.getStatementType());
+    assertNull(savepointStatement.describeAsync(mock(BackendConnection.class)).get());
+  }
+
+  @Test
+  public void testCreateStatementRelease() throws Exception {
+    Statement statement = Statement.of("release foo");
+    IntermediatePreparedStatement preparedStatement =
+        createStatement(connectionHandler, "", PARSER.parse(statement), statement, new int[] {});
+    assertEquals(ReleaseStatement.class, preparedStatement.getClass());
+    ReleaseStatement releaseStatement = (ReleaseStatement) preparedStatement;
+    assertEquals("foo", releaseStatement.getSavepointName());
+    assertEquals("RELEASE", releaseStatement.getCommandTag());
+    assertEquals(StatementType.CLIENT_SIDE, releaseStatement.getStatementType());
+    assertNull(releaseStatement.describeAsync(mock(BackendConnection.class)).get());
+  }
+
+  @Test
+  public void testCreateStatementRollbackTo() throws Exception {
+    Statement statement = Statement.of("rollback to foo");
+    IntermediatePreparedStatement preparedStatement =
+        createStatement(connectionHandler, "", PARSER.parse(statement), statement, new int[] {});
+    assertEquals(RollbackToStatement.class, preparedStatement.getClass());
+    RollbackToStatement rollbackToStatement = (RollbackToStatement) preparedStatement;
+    assertEquals("foo", rollbackToStatement.getSavepointName());
+    assertEquals("ROLLBACK", rollbackToStatement.getCommandTag());
+    assertEquals(StatementType.CLIENT_SIDE, rollbackToStatement.getStatementType());
+    assertNull(rollbackToStatement.describeAsync(mock(BackendConnection.class)).get());
+  }
+
+  @Test
+  public void testCreateStatementRollback() {
+    // Verify that 'rollback' is interpreted as a statement that is not handled by PGAdapter.
+    Statement statement = Statement.of("rollback");
+    ParsedStatement parsedStatement = PARSER.parse(statement);
+    IntermediatePreparedStatement preparedStatement =
+        createStatement(connectionHandler, "", parsedStatement, statement, new int[] {});
+    assertEquals(IntermediatePreparedStatement.class, preparedStatement.getClass());
+    assertEquals(StatementType.CLIENT_SIDE, parsedStatement.getType());
+    assertEquals(ClientSideStatementType.ROLLBACK, parsedStatement.getClientSideStatementType());
+  }
+}