Repository: GoogleCloudPlatform/pgadapter
Pull request #485 (2022-11-14 14:23:49)

docs: document Liquibase Pilot Support
Documents Liquibase Support.


Patch:
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 1bbe6a00..f53d35aa 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -91,6 +91,7 @@ jobs:
         with:
           python-version: '3.9'
       - run: pip install -r ./src/test/python/requirements.txt
+      - run: pip install -r ./src/test/python/pg8000/requirements.txt
       - uses: actions/setup-node@v3
         with:
           node-version: 16
diff --git a/.github/workflows/units.yaml b/.github/workflows/units.yaml
index f43d5908..7f598a0f 100644
--- a/.github/workflows/units.yaml
+++ b/.github/workflows/units.yaml
@@ -26,6 +26,7 @@ jobs:
           python-version: '3.9'
       - run: python --version
       - run: pip install -r ./src/test/python/requirements.txt
+      - run: pip install -r ./src/test/python/pg8000/requirements.txt
       - uses: actions/setup-node@v3
         with:
           node-version: 16
@@ -50,6 +51,7 @@ jobs:
           python-version: '3.9'
       - run: python --version
       - run: pip install -r ./src/test/python/requirements.txt
+      - run: pip install -r ./src/test/python/pg8000/requirements.txt
       - run: mvn -B test
   macos:
     runs-on: macos-latest
@@ -75,6 +77,7 @@ jobs:
           python-version: '3.9'
       - run: python --version
       - run: pip install -r ./src/test/python/requirements.txt
+      - run: pip install -r ./src/test/python/pg8000/requirements.txt
       - uses: actions/setup-dotnet@v3
         with:
           dotnet-version: '6.0.x'
diff --git a/.gitignore b/.gitignore
index d0ffb722..f1509949 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@ target/
 *.lst
 output.txt
 __pycache__
+venv
+.DS_Store
 
 src/test/golang/**/*.h
 src/test/golang/**/*.so
diff --git a/README.md b/README.md
index 0d433c91..37873565 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,8 @@
 PGAdapter is a proxy which translates the PostgreSQL wire-protocol into the
 equivalent for Spanner databases [that use the PostgreSQL interface](https://cloud.google.com/spanner/docs/postgresql-interface).
 
-PGAdapter can be used with the following clients:
+## Drivers and Clients
+PGAdapter can be used with the following drivers and clients:
 1. `psql`: Versions 11, 12, 13 and 14 are supported. See [psql support](docs/psql.md) for more details.
 2. `JDBC`: Versions 42.x and higher are supported. See [JDBC support](docs/jdbc.md) for more details.
 3. `pgx`: Version 4.15 and higher are supported. See [pgx support](docs/pgx.md) for more details.
@@ -11,6 +12,11 @@ PGAdapter can be used with the following clients:
 5. `node-postgres`: Version 8.8.0 and higher have __experimental support__.
    See [node-postgres support](docs/node-postgres.md) for more details.
 
+## Frameworks
+PGAdapter can be used with the following frameworks:
+1. `Liquibase`: Version 4.12.0 and higher have Pilot Support. See [Liquibase support](docs/liquibase.md)
+   for more details. See also [this directory](samples/java/liquibase) for a sample application using `Liquibase`.
+
 ## FAQ
 See [Frequently Asked Questions](docs/faq.md) for answers to frequently asked questions.
 
@@ -264,8 +270,6 @@ Other `psql` meta-commands are __not__ supported.
 ## Limitations
 
 PGAdapter has the following known limitations at this moment:
-- Server side [prepared statements](https://www.postgresql.org/docs/current/sql-prepare.html) are limited to at most 50 parameters.
-- SSL connections are not supported.
 - Only [password authentication](https://www.postgresql.org/docs/current/auth-password.html) using
   the `password` method is supported. All other authentication methods are not supported.
 - The COPY protocol only supports COPY TO|FROM STDOUT|STDIN [BINARY]. COPY TO|FROM <FILE|PROGRAM> is not supported.
diff --git a/docs/jdbc.md b/docs/jdbc.md
index 7db1f37d..09212d84 100644
--- a/docs/jdbc.md
+++ b/docs/jdbc.md
@@ -106,7 +106,3 @@ try (java.sql.Statement statement = connection.createStatement()) {
 ```
 
 ## Limitations
-- Server side [prepared statements](https://www.postgresql.org/docs/current/sql-prepare.html) are limited to at most 50 parameters.
-  Note: This is not the same as `java.sql.PreparedStatement`. A `java.sql.PreparedStatement` will only use
-  a server side prepared statement if it has been executed more at least `prepareThreshold` times.
-  See https://jdbc.postgresql.org/documentation/server-prepare/#activation for more information.
diff --git a/docs/liquibase.md b/docs/liquibase.md
new file mode 100644
index 00000000..5501114c
--- /dev/null
+++ b/docs/liquibase.md
@@ -0,0 +1,56 @@
+# PGAdapter - Liquibase Connection Options
+
+PGAdapter has Pilot Support for [Liquibase](https://www.liquibase.org/) version v4.12.0 and higher.
+
+## Limitations
+Pilot Support means that it is possible to use `Liquibase` with Cloud Spanner PostgreSQL databases,
+but with limitations. This means that porting an existing application from PostgreSQL to Cloud Spanner
+will probably require code changes. See [Limitations](../samples/java/liquibase/README.md#limitations)
+in the `Liquibase` sample directory for a full list of limitations.
+
+## Usage
+
+
+### Start PGAdapter
+First start PGAdapter:
+
+```shell
+export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
+docker pull gcr.io/cloud-spanner-pg-adapter/pgadapter
+docker run \
+  -d -p 5432:5432 \
+  -v ${GOOGLE_APPLICATION_CREDENTIALS}:${GOOGLE_APPLICATION_CREDENTIALS}:ro \
+  -e GOOGLE_APPLICATION_CREDENTIALS \
+  gcr.io/cloud-spanner-pg-adapter/pgadapter \
+  -p my-project -i my-instance \
+  -x
+```
+
+### Create databasechangelog and databasechangeloglock Manually
+The `databasechangeloglock` and `databasechangelog` tables **must** be created manually, as the
+DDL script that is automatically generated by Liquibase will try to use the data type
+`timestamp without time zone`, which is not supported by Cloud Spanner. The DDL script to create
+these tables manually can be found in [create_database_change_log.sql](../samples/java/liquibase/create_database_change_log.sql).
+
+### Connect Liquibase to PGAdapter
+Liquibase will by default use DDL transactions when connecting to PostgreSQL databases. This is not
+supported by Cloud Spanner. Instead, PGAdapter can automatically convert DDL transactions into DDL
+batches. This requires the following option to be set in the JDBC connection URL:
+
+```properties
+url: jdbc:postgresql://localhost:5432/liquibase-test?options=-c%20spanner.ddl_transaction_mode=AutocommitExplicitTransaction
+```
+
+See [liquibase.properties](../samples/java/liquibase/liquibase.properties) for an example connection URL.
+
+### Run Liquibase
+Run Liquibase using for example the Maven plugin:
+
+```shell
+mvn liquibase:validate
+```
+
+## Full Sample and Limitations
+[This directory](../samples/java/liquibase) contains a full sample of how to work with `Liquibase` with
+Cloud Spanner and PGAdapter. The sample readme file also lists the [current limitations](../samples/java/liquibase)
+when working with `Liquibase`.
diff --git a/docs/node-postgres.md b/docs/node-postgres.md
index 505e46a2..c2fbcf5d 100644
--- a/docs/node-postgres.md
+++ b/docs/node-postgres.md
@@ -116,4 +116,3 @@ console.log(res);
 ```
 
 ## Limitations
-- [Prepared statements](https://www.postgresql.org/docs/current/sql-prepare.html) are limited to at most 50 parameters.
diff --git a/docs/pgx.md b/docs/pgx.md
index 5858ed6c..63be7b50 100644
--- a/docs/pgx.md
+++ b/docs/pgx.md
@@ -104,6 +104,3 @@ res := conn.SendBatch(context.Background(), batch)
 ```
 
 ## Limitations
-- Server side [prepared statements](https://www.postgresql.org/docs/current/sql-prepare.html) are limited to at most 50 parameters.
-  `pgx` uses server side prepared statements for all parameterized statements in extended query mode.
-  You can use the [simple query protocol](https://pkg.go.dev/github.com/jackc/pgx/v4#QuerySimpleProtocol) to work around this limitation.
diff --git a/pom.xml b/pom.xml
index 95f62f52..227d6323 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,7 +75,7 @@
     <dependency>
       <groupId>com.google.auto.value</groupId>
       <artifactId>auto-value-annotations</artifactId>
-      <version>1.10</version>
+      <version>1.10.1</version>
     </dependency>
     <dependency>
       <groupId>com.google.cloud</groupId>
diff --git a/samples/golang/gorm/README.md b/samples/golang/gorm/README.md
index d71e5916..22a846f6 100644
--- a/samples/golang/gorm/README.md
+++ b/samples/golang/gorm/README.md
@@ -162,14 +162,3 @@ db, err := gorm.Open(postgres.Open(connectionString), &gorm.Config{
 ### Locking
 Locking clauses, like `clause.Locking{Strength: "UPDATE"}`, are not supported. These are generally speaking also not
 required, as Cloud Spanner uses isolation level `serializable` for read/write transactions.
-
-### Large CreateInBatches
-The `CreateInBatches` function will generate an insert statement in the following form:
-
-```sql
-INSERT INTO my_table (col1, col2, col3)
-VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9), ..., ($x, $y, $z)
-```
-
-PGAdapter currently does not support prepared statements with more than 50 parameters. Either reduce the number of rows
-that are inserted in one batch or use the `SimpleQueryMode` to work around this limitation.
diff --git a/samples/java/hibernate/pom.xml b/samples/java/hibernate/pom.xml
index 254e139d..31e8b50b 100644
--- a/samples/java/hibernate/pom.xml
+++ b/samples/java/hibernate/pom.xml
@@ -24,23 +24,29 @@
     <junixsocket.version>2.5.1</junixsocket.version>
   </properties>
   <dependencies>
+    <!-- [START spanner_hibernate_dependencies] -->
+    <!-- Hibernate for Entity Management -->
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-entitymanager</artifactId>
-      <version>${hibernate.version}</version>
+      <version>5.3.20.Final</version>
     </dependency>
 
+    <!-- Hibernate core dependency -->
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-core</artifactId>
-      <version>${hibernate.version}</version>
+      <version>5.3.20.Final</version>
     </dependency>
 
+    <!-- The Postgresql JDBC driver dependency -->
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
       <version>42.4.3</version>
     </dependency>
+    <!-- [END spanner_hibernate_dependencies] -->
+
     <dependency>
       <groupId>javax.xml.bind</groupId>
       <artifactId>jaxb-api</artifactId>
@@ -58,52 +64,4 @@
     </dependency>
 
   </dependencies>
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>clirr-maven-plugin</artifactId>
-        <executions>
-          <execution>
-            <!-- Disable this plugin for now as it regularly times out when being downloaded -->
-            <phase>none</phase>
-          </execution>
-        </executions>
-        <configuration>
-          <skip>${clirr.skip}</skip>
-        </configuration>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-surefire-plugin</artifactId>
-        <configuration>
-          <forkedProcessExitTimeoutInSeconds>3</forkedProcessExitTimeoutInSeconds>
-          <forkedProcessTimeoutInSeconds>3600</forkedProcessTimeoutInSeconds>
-          <!--suppress UnresolvedMavenProperty -->
-          <argLine>${surefire.jacoco.args}</argLine>
-          <excludedGroups>${excludedTests}</excludedGroups>
-          <reportNameSuffix>sponge_log</reportNameSuffix>
-          <skipTests>${skipUnits}</skipTests>
-          <systemProperties>
-            <property>
-              <name>java.util.logging.config.file</name>
-              <value>src/test/resources/logging.properties</value>
-            </property>
-          </systemProperties>
-        </configuration>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-jar-plugin</artifactId>
-        <configuration>
-          <archive>
-            <manifest>
-              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
-              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
-            </manifest>
-          </archive>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
 </project>
\ No newline at end of file
diff --git a/samples/java/hibernate/src/main/resources/hibernate.properties b/samples/java/hibernate/src/main/resources/hibernate.properties
index ac4ae438..be825c6a 100644
--- a/samples/java/hibernate/src/main/resources/hibernate.properties
+++ b/samples/java/hibernate/src/main/resources/hibernate.properties
@@ -1,5 +1,8 @@
+# [START spanner_hibernate_config]
 hibernate.dialect org.hibernate.dialect.PostgreSQLDialect
 hibernate.connection.driver_class org.postgresql.Driver
+# [END spanner_hibernate_config]
+
 hibernate.connection.url jdbc:postgresql://localhost:5432/test-database
 hibernate.connection.username pratick
 
diff --git a/samples/java/liquibase/README.md b/samples/java/liquibase/README.md
index 0c23d033..a727e60d 100644
--- a/samples/java/liquibase/README.md
+++ b/samples/java/liquibase/README.md
@@ -1,6 +1,6 @@
 # PGAdapter and Liquibase
 
-PGAdapter can be used in combination with Liquibase, but with a number of limitations. This sample
+PGAdapter has Pilot Support for [Liquibase](https://www.liquibase.org/). This sample
 shows the command line arguments and configuration that is needed in order to use Liquibase with
 PGAdapter.
 
diff --git a/samples/java/liquibase/liquibase.properties b/samples/java/liquibase/liquibase.properties
index 000b194a..bb0aae57 100644
--- a/samples/java/liquibase/liquibase.properties
+++ b/samples/java/liquibase/liquibase.properties
@@ -7,4 +7,4 @@ changeLogFile: dbchangelog.xml
 # DDL transactions into DDL batches.
 # See https://github.com/GoogleCloudPlatform/pgadapter/blob/postgresql-dialect/docs/ddl.md for more
 # information.
-url: jdbc:postgresql://localhost:5432/liquibase-test?options=-c%20spanner.ddl_transaction_mode=AutocommitExplicitTransaction%20-c%20server_version=14.1
+url: jdbc:postgresql://localhost:5432/liquibase-test?options=-c%20spanner.ddl_transaction_mode=AutocommitExplicitTransaction
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/DescribeResult.java b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/DescribeResult.java
index 2091f22f..8e43e050 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/DescribeResult.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/DescribeResult.java
@@ -21,19 +21,20 @@ import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.parsers.Parser;
 import com.google.spanner.v1.StructType;
 import java.util.Arrays;
+import javax.annotation.Nullable;
 
 @InternalApi
 public class DescribeResult {
-  private final ResultSet resultSet;
+  @Nullable private final ResultSet resultSet;
   private final int[] parameters;
 
-  public DescribeResult(int[] givenParameterTypes, ResultSet resultMetadata) {
+  public DescribeResult(int[] givenParameterTypes, @Nullable ResultSet resultMetadata) {
     this.resultSet = resultMetadata;
     this.parameters = extractParameters(givenParameterTypes, resultMetadata);
   }
 
-  static int[] extractParameters(int[] givenParameterTypes, ResultSet resultSet) {
-    if (!resultSet.getMetadata().hasUndeclaredParameters()) {
+  static int[] extractParameters(int[] givenParameterTypes, @Nullable ResultSet resultSet) {
+    if (resultSet == null || !resultSet.getMetadata().hasUndeclaredParameters()) {
       return givenParameterTypes;
     }
     return extractParameterTypes(
@@ -79,7 +80,7 @@ public class DescribeResult {
     return parameters;
   }
 
-  public ResultSet getResultSet() {
+  public @Nullable ResultSet getResultSet() {
     return resultSet;
   }
 }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
index f824654e..6abac46f 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
@@ -88,7 +88,7 @@ public class OptionsMetadata {
   }
 
   private static final Logger logger = Logger.getLogger(OptionsMetadata.class.getName());
-  private static final String DEFAULT_SERVER_VERSION = "1.0";
+  private static final String DEFAULT_SERVER_VERSION = "14.1";
   private static final String DEFAULT_USER_AGENT = "pg-adapter";
 
   private static final String OPTION_SERVER_PORT = "s";
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/SendResultSetState.java b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/SendResultSetState.java
index b5ef4570..ad796c51 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/SendResultSetState.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/SendResultSetState.java
@@ -31,7 +31,9 @@ public class SendResultSetState {
   }
 
   public String getCommandAndNumRows() {
-    return getCommandTag() + " " + getNumberOfRowsSent();
+    String command = getCommandTag();
+    command += ("INSERT".equals(command) ? " 0 " : " ") + getNumberOfRowsSent();
+    return command;
   }
 
   public String getCommandTag() {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java
index b26ca6a5..16141cec 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java
@@ -27,7 +27,6 @@ import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
-import java.util.Set;
 import org.postgresql.core.Oid;
 
 /**
@@ -55,27 +54,6 @@ public abstract class Parser<T> {
   protected static final Charset UTF8 = StandardCharsets.UTF_8;
   protected T item;
 
-  /**
-   * Guess the type of a parameter with unspecified type.
-   *
-   * @param item The value to guess the type for
-   * @param formatCode The encoding that is used for the value
-   * @return The {@link Oid} type code that is guessed for the value or {@link Oid#UNSPECIFIED} if
-   *     no type could be guessed.
-   */
-  private static int guessType(Set<Integer> guessTypes, byte[] item, FormatCode formatCode) {
-    if (formatCode == FormatCode.TEXT && item != null) {
-      String value = new String(item, StandardCharsets.UTF_8);
-      if (guessTypes.contains(Oid.TIMESTAMPTZ) && TimestampParser.isTimestamp(value)) {
-        return Oid.TIMESTAMPTZ;
-      }
-      if (guessTypes.contains(Oid.DATE) && DateParser.isDate(value)) {
-        return Oid.DATE;
-      }
-    }
-    return Oid.UNSPECIFIED;
-  }
-
   /**
    * Factory method to create a Parser subtype with a designated type from a byte array.
    *
@@ -111,19 +89,15 @@ public abstract class Parser<T> {
       case Oid.TEXT:
       case Oid.VARCHAR:
         return new StringParser(item, formatCode);
+      case Oid.UUID:
+        return new UuidParser(item, formatCode);
       case Oid.TIMESTAMP:
       case Oid.TIMESTAMPTZ:
         return new TimestampParser(item, formatCode, sessionState);
       case Oid.JSONB:
         return new JsonbParser(item, formatCode);
       case Oid.UNSPECIFIED:
-        // Try to guess the type based on the value. Use an unspecified parser if no type could be
-        // determined.
-        int type = guessType(sessionState.getGuessTypes(), item, formatCode);
-        if (type == Oid.UNSPECIFIED) {
-          return new UnspecifiedParser(item, formatCode);
-        }
-        return create(sessionState, item, type, formatCode);
+        return new UnspecifiedParser(item, formatCode);
       default:
         throw new IllegalArgumentException("Unsupported parameter type: " + oidType);
     }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/UuidParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/UuidParser.java
new file mode 100644
index 00000000..01bebe59
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/UuidParser.java
@@ -0,0 +1,115 @@
+// Copyright 2020 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.parsers;
+
+import com.google.api.core.InternalApi;
+import com.google.cloud.spanner.Statement;
+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 java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import javax.annotation.Nonnull;
+import org.postgresql.util.ByteConverter;
+
+/**
+ * Translate from wire protocol to UUID. This is currently a one-way conversion, as we only accept
+ * UUID as a parameter type. UUIDs are converted to strings.
+ */
+@InternalApi
+public class UuidParser extends Parser<String> {
+
+  UuidParser(byte[] item, FormatCode formatCode) {
+    switch (formatCode) {
+      case TEXT:
+        this.item =
+            item == null ? null : verifyStringValue(new String(item, StandardCharsets.UTF_8));
+        break;
+      case BINARY:
+        this.item = verifyBinaryValue(item);
+        break;
+      default:
+        handleInvalidFormat(formatCode);
+    }
+  }
+
+  static void handleInvalidFormat(FormatCode formatCode) {
+    throw PGException.newBuilder("Unsupported format: " + formatCode.name())
+        .setSQLState(SQLState.InternalError)
+        .setSeverity(Severity.ERROR)
+        .build();
+  }
+
+  @Override
+  public String stringParse() {
+    return this.item;
+  }
+
+  @Override
+  protected byte[] binaryParse() {
+    if (this.item == null) {
+      return null;
+    }
+    return binaryEncode(this.item);
+  }
+
+  static String verifyStringValue(@Nonnull String value) {
+    try {
+      //noinspection ResultOfMethodCallIgnored
+      UUID.fromString(value);
+      return value;
+    } catch (Exception exception) {
+      throw createInvalidUuidValueException(value, exception);
+    }
+  }
+
+  static String verifyBinaryValue(byte[] value) {
+    if (value == null) {
+      return null;
+    }
+    if (value.length != 16) {
+      throw PGException.newBuilder("Invalid UUID binary length: " + value.length)
+          .setSeverity(Severity.ERROR)
+          .setSQLState(SQLState.InvalidParameterValue)
+          .build();
+    }
+    return new UUID(ByteConverter.int8(value, 0), ByteConverter.int8(value, 8)).toString();
+  }
+
+  static byte[] binaryEncode(String value) {
+    try {
+      UUID uuid = UUID.fromString(value);
+      byte[] val = new byte[16];
+      ByteConverter.int8(val, 0, uuid.getMostSignificantBits());
+      ByteConverter.int8(val, 8, uuid.getLeastSignificantBits());
+      return val;
+    } catch (Exception exception) {
+      throw createInvalidUuidValueException(value, exception);
+    }
+  }
+
+  static PGException createInvalidUuidValueException(String value, Exception cause) {
+    return PGException.newBuilder("Invalid UUID: " + value)
+        .setSeverity(Severity.ERROR)
+        .setSQLState(SQLState.InvalidParameterValue)
+        .setCause(cause)
+        .build();
+  }
+
+  @Override
+  public void bind(Statement.Builder statementBuilder, String name) {
+    statementBuilder.bind(name).to(this.item);
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
index 2af43c4d..1c05bb33 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
@@ -16,7 +16,6 @@ package com.google.cloud.spanner.pgadapter.session;
 
 import static com.google.cloud.spanner.pgadapter.session.CopySettings.initCopySettings;
 
-import com.google.api.client.util.Strings;
 import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
@@ -30,7 +29,6 @@ import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSet.Builder;
 import com.google.common.collect.Sets;
 import java.time.ZoneId;
 import java.util.ArrayList;
@@ -45,7 +43,6 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.stream.Collectors;
-import javax.annotation.Nonnull;
 
 /** {@link SessionState} contains all session variables for a connection. */
 @InternalApi
@@ -422,43 +419,6 @@ public class SessionState {
     }
   }
 
-  /**
-   * Returns a set of OIDs that PGAdapter should try to guess if it receives an untyped parameter
-   * value. This is needed because some clients (JDBC) deliberately send parameters without a type
-   * code to force the server to infer the type. This specifically applies to date/timestamp
-   * parameters.
-   */
-  public Set<Integer> getGuessTypes() {
-    PGSetting setting = internalGet(toKey("spanner", "guess_types"), false);
-    if (setting == null || Strings.isNullOrEmpty(setting.getSetting())) {
-      return ImmutableSet.of();
-    }
-    return convertOidListToSet(setting.getSetting());
-  }
-
-  /** Keep a cache of 1 element ready as the setting is not likely to change often. */
-  private final Map<String, Set<Integer>> cachedGuessTypes = new HashMap<>(1);
-
-  Set<Integer> convertOidListToSet(@Nonnull String value) {
-    if (cachedGuessTypes.containsKey(value)) {
-      return cachedGuessTypes.get(value);
-    }
-
-    Builder<Integer> builder = ImmutableSet.builder();
-    String[] oids = value.split(",");
-    for (String oid : oids) {
-      try {
-        builder.add(Integer.valueOf(oid));
-      } catch (Exception ignore) {
-        // ignore invalid oids.
-      }
-    }
-    cachedGuessTypes.clear();
-    cachedGuessTypes.put(value, builder.build());
-
-    return cachedGuessTypes.get(value);
-  }
-
   @SafeVarargs
   static <T> T tryGetFirstNonNull(T defaultResult, Callable<T>... callables) {
     T value;
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 665105e9..b94a7775 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
@@ -237,7 +237,11 @@ public class BackendConnection {
         } else if (statement.getSql().isEmpty()) {
           result.set(NO_RESULT);
         } else if (parsedStatement.isDdl()) {
-          result.set(ddlExecutor.execute(parsedStatement, statement));
+          if (analyze) {
+            result.set(NO_RESULT);
+          } else {
+            result.set(ddlExecutor.execute(parsedStatement, statement));
+          }
         } else {
           // Potentially replace pg_catalog table references with common table expressions.
           updatedStatement =
@@ -277,7 +281,7 @@ public class BackendConnection {
       statement = statementBinder.apply(statement);
       if (analyze) {
         ResultSet resultSet;
-        if (parsedStatement.isUpdate()) {
+        if (parsedStatement.isUpdate() && !parsedStatement.hasReturningClause()) {
           // TODO(#477): Single analyzeUpdate statements that are executed in an implicit
           //     transaction could use a single-use read/write transaction. Replays are not
           //     dangerous for those.
@@ -302,8 +306,10 @@ public class BackendConnection {
           } else {
             resultSet = spannerConnection.analyzeUpdateStatement(statement, QueryAnalyzeMode.PLAN);
           }
-        } else {
+        } else if (parsedStatement.isQuery() || parsedStatement.hasReturningClause()) {
           resultSet = spannerConnection.analyzeQuery(statement, QueryAnalyzeMode.PLAN);
+        } else {
+          return NO_RESULT;
         }
         return new QueryResult(resultSet);
       }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java
index fe508e2e..4d8314f6 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java
@@ -18,6 +18,7 @@ import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
 import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.connection.StatementResult.ResultType;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.cloud.spanner.pgadapter.metadata.DescribeResult;
@@ -102,7 +103,12 @@ public class IntermediatePreparedStatement extends IntermediateStatement {
     this.describeResult =
         Futures.lazyTransform(
             statementResultFuture,
-            result -> new DescribeResult(this.givenParameterDataTypes, result.getResultSet()));
+            result ->
+                new DescribeResult(
+                    this.givenParameterDataTypes,
+                    result.getResultType() == ResultType.RESULT_SET
+                        ? result.getResultSet()
+                        : null));
     this.described = true;
     return statementResultFuture;
   }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java
index 2a96ef03..7aa28a3d 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java
@@ -27,6 +27,7 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 @InternalApi
 public class PgCatalog {
@@ -35,15 +36,22 @@ public class PgCatalog {
           new TableOrIndexName("pg_catalog", "pg_namespace"),
               new TableOrIndexName(null, "pg_namespace"),
           new TableOrIndexName(null, "pg_namespace"), new TableOrIndexName(null, "pg_namespace"),
+          new TableOrIndexName("pg_catalog", "pg_class"), new TableOrIndexName(null, "pg_class"),
+          new TableOrIndexName(null, "pg_class"), new TableOrIndexName(null, "pg_class"),
           new TableOrIndexName("pg_catalog", "pg_type"), new TableOrIndexName(null, "pg_type"),
           new TableOrIndexName(null, "pg_type"), new TableOrIndexName(null, "pg_type"),
           new TableOrIndexName("pg_catalog", "pg_settings"),
               new TableOrIndexName(null, "pg_settings"),
           new TableOrIndexName(null, "pg_settings"), new TableOrIndexName(null, "pg_settings"));
+  private static final ImmutableMap<Pattern, String> FUNCTION_REPLACEMENTS =
+      ImmutableMap.of(
+          Pattern.compile("pg_catalog.pg_table_is_visible\\(.+\\)"), "true",
+          Pattern.compile("pg_table_is_visible\\(.+\\)"), "true");
 
   private final Map<TableOrIndexName, PgCatalogTable> pgCatalogTables =
       ImmutableMap.of(
           new TableOrIndexName(null, "pg_namespace"), new PgNamespace(),
+          new TableOrIndexName(null, "pg_class"), new PgClass(),
           new TableOrIndexName(null, "pg_type"), new PgType(),
           new TableOrIndexName(null, "pg_settings"), new PgSettings());
   private final SessionState sessionState;
@@ -70,9 +78,18 @@ public class PgCatalog {
     return addCommonTableExpressions(replacedTablesStatement.y(), cteBuilder.build());
   }
 
+  static String replaceKnownUnsupportedFunctions(Statement statement) {
+    String sql = statement.getSql();
+    for (Entry<Pattern, String> functionReplacement : FUNCTION_REPLACEMENTS.entrySet()) {
+      sql = functionReplacement.getKey().matcher(sql).replaceAll(functionReplacement.getValue());
+    }
+    return sql;
+  }
+
   static Statement addCommonTableExpressions(
       Statement statement, ImmutableList<String> tableExpressions) {
-    SimpleParser parser = new SimpleParser(statement.getSql());
+    String sql = replaceKnownUnsupportedFunctions(statement);
+    SimpleParser parser = new SimpleParser(sql);
     boolean hadCommonTableExpressions = parser.eatKeyword("with");
     String tableExpressionsSql = String.join(",\n", tableExpressions);
     Statement.Builder builder =
@@ -224,4 +241,90 @@ public class PgCatalog {
       return sessionState.generatePGSettingsCte();
     }
   }
+
+  private static class PgClass implements PgCatalogTable {
+    private static final String PG_CLASS_CTE =
+        "pg_class as (\n"
+            + "  select\n"
+            + "  -1 as oid,\n"
+            + "  table_name as relname,\n"
+            + "  case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+            + "  0 as reltype,\n"
+            + "  0 as reloftype,\n"
+            + "  0 as relowner,\n"
+            + "  1 as relam,\n"
+            + "  0 as relfilenode,\n"
+            + "  0 as reltablespace,\n"
+            + "  0 as relpages,\n"
+            + "  0.0::float8 as reltuples,\n"
+            + "  0 as relallvisible,\n"
+            + "  0 as reltoastrelid,\n"
+            + "  false as relhasindex,\n"
+            + "  false as relisshared,\n"
+            + "  'p' as relpersistence,\n"
+            + "  'r' as relkind,\n"
+            + "  count(*) as relnatts,\n"
+            + "  0 as relchecks,\n"
+            + "  false as relhasrules,\n"
+            + "  false as relhastriggers,\n"
+            + "  false as relhassubclass,\n"
+            + "  false as relrowsecurity,\n"
+            + "  false as relforcerowsecurity,\n"
+            + "  true as relispopulated,\n"
+            + "  'n' as relreplident,\n"
+            + "  false as relispartition,\n"
+            + "  0 as relrewrite,\n"
+            + "  0 as relfrozenxid,\n"
+            + "  0 as relminmxid,\n"
+            + "  '{}'::bigint[] as relacl,\n"
+            + "  '{}'::text[] as reloptions,\n"
+            + "  0 as relpartbound\n"
+            + "from information_schema.tables t\n"
+            + "inner join information_schema.columns using (table_catalog, table_schema, table_name)\n"
+            + "group by t.table_name, t.table_schema\n"
+            + "union all\n"
+            + "select\n"
+            + "    -1 as oid,\n"
+            + "    i.index_name as relname,\n"
+            + "    case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+            + "    0 as reltype,\n"
+            + "    0 as reloftype,\n"
+            + "    0 as relowner,\n"
+            + "    1 as relam,\n"
+            + "    0 as relfilenode,\n"
+            + "    0 as reltablespace,\n"
+            + "    0 as relpages,\n"
+            + "    0.0::float8 as reltuples,\n"
+            + "    0 as relallvisible,\n"
+            + "    0 as reltoastrelid,\n"
+            + "    false as relhasindex,\n"
+            + "    false as relisshared,\n"
+            + "    'p' as relpersistence,\n"
+            + "    'r' as relkind,\n"
+            + "    count(*) as relnatts,\n"
+            + "    0 as relchecks,\n"
+            + "    false as relhasrules,\n"
+            + "    false as relhastriggers,\n"
+            + "    false as relhassubclass,\n"
+            + "    false as relrowsecurity,\n"
+            + "    false as relforcerowsecurity,\n"
+            + "    true as relispopulated,\n"
+            + "    'n' as relreplident,\n"
+            + "    false as relispartition,\n"
+            + "    0 as relrewrite,\n"
+            + "    0 as relfrozenxid,\n"
+            + "    0 as relminmxid,\n"
+            + "    '{}'::bigint[] as relacl,\n"
+            + "    '{}'::text[] as reloptions,\n"
+            + "    0 as relpartbound\n"
+            + "from information_schema.indexes i\n"
+            + "inner join information_schema.index_columns using (table_catalog, table_schema, table_name)\n"
+            + "group by i.index_name, i.table_schema\n"
+            + ")";
+
+    @Override
+    public String getTableExpression() {
+      return PG_CLASS_CTE;
+    }
+  }
 }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java
index 0a3b07a0..e233eb41 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessage.java
@@ -28,6 +28,7 @@ import com.google.cloud.spanner.SpannerOptions;
 import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.ConnectionOptionsHelper;
 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.ConnectionHandler.ConnectionStatus;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.QueryMode;
@@ -243,7 +244,7 @@ public abstract class ControlMessage extends WireMessage {
    *
    * <p>NOTE: This method does not flush the output stream.
    */
-  public void sendSpannerResult(IntermediateStatement statement, QueryMode mode, long maxRows)
+  void sendSpannerResult(IntermediateStatement statement, QueryMode mode, long maxRows)
       throws Exception {
     String command = statement.getCommandTag();
     if (Strings.isNullOrEmpty(command)) {
@@ -253,31 +254,38 @@ public abstract class ControlMessage extends WireMessage {
     if (statement.getStatementResult() == null) {
       return;
     }
-
     switch (statement.getStatementType()) {
       case DDL:
-      case CLIENT_SIDE:
       case UNKNOWN:
         new CommandCompleteResponse(this.outputStream, command).send(false);
         break;
+      case CLIENT_SIDE:
+        if (statement.getStatementResult().getResultType() != ResultType.RESULT_SET) {
+          new CommandCompleteResponse(this.outputStream, command).send(false);
+          break;
+        }
+        // fallthrough to QUERY
       case QUERY:
-        SendResultSetState state = sendResultSet(statement, mode, maxRows);
-        statement.setHasMoreData(state.hasMoreRows());
-        if (state.hasMoreRows()) {
-          new PortalSuspendedResponse(this.outputStream).send(false);
+      case UPDATE:
+        if (statement.getStatementResult().getResultType() == ResultType.RESULT_SET) {
+          SendResultSetState state = sendResultSet(statement, mode, maxRows);
+          statement.setHasMoreData(state.hasMoreRows());
+          if (state.hasMoreRows()) {
+            new PortalSuspendedResponse(this.outputStream).send(false);
+          } else {
+            statement.close();
+            new CommandCompleteResponse(this.outputStream, state.getCommandAndNumRows())
+                .send(false);
+          }
         } else {
-          statement.close();
-          new CommandCompleteResponse(this.outputStream, state.getCommandAndNumRows()).send(false);
+          // For an INSERT command, the tag is INSERT oid rows, where rows is the number of rows
+          // inserted. oid used to be the object ID of the inserted row if rows was 1 and the target
+          // table had OIDs, but OIDs system columns are not supported anymore; therefore oid is
+          // always 0.
+          command += ("INSERT".equals(command) ? " 0 " : " ") + statement.getUpdateCount();
+          new CommandCompleteResponse(this.outputStream, command).send(false);
         }
         break;
-      case UPDATE:
-        // For an INSERT command, the tag is INSERT oid rows, where rows is the number of rows
-        // inserted. oid used to be the object ID of the inserted row if rows was 1 and the target
-        // table had OIDs, but OIDs system columns are not supported anymore; therefore oid is
-        // always 0.
-        command += ("INSERT".equals(command) ? " 0 " : " ") + statement.getUpdateCount();
-        new CommandCompleteResponse(this.outputStream, command).send(false);
-        break;
       default:
         throw new IllegalStateException("Unknown statement type: " + statement.getStatement());
     }
@@ -293,12 +301,13 @@ public abstract class ControlMessage extends WireMessage {
    * @return An adapted representation with specific metadata which PG wire requires.
    * @throws com.google.cloud.spanner.SpannerException if traversing the {@link ResultSet} fails.
    */
-  public SendResultSetState sendResultSet(
+  SendResultSetState sendResultSet(
       IntermediateStatement describedResult, QueryMode mode, long maxRows) throws Exception {
+    StatementResult statementResult = describedResult.getStatementResult();
     Preconditions.checkArgument(
-        describedResult.containsResultSet(), "The statement result must be a result set");
+        statementResult.getResultType() == ResultType.RESULT_SET,
+        "The statement result must be a result set");
     long rows;
-    StatementResult statementResult = describedResult.getStatementResult();
     boolean hasData;
     if (statementResult instanceof PartitionQueryResult) {
       hasData = false;


Test patch:
diff --git a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/HibernateSampleTest.java b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/HibernateSampleTest.java
index 5dbead90..92d0a5fa 100644
--- a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/HibernateSampleTest.java
+++ b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/HibernateSampleTest.java
@@ -261,47 +261,51 @@ public class HibernateSampleTest {
   }
 
   public void executeTest() {
-    System.out.println("Testing Foreign Key");
-    testForeignKey();
-    System.out.println("Foreign Key Test Completed");
-
-    System.out.println("Testing Session Rollback");
-    testSessionRollback();
-    System.out.println("Session Rollback Test Completed");
-
-    System.out.println("Testing Data Insert");
-    testAddingData();
-    System.out.println("Data Insert Test Completed");
-
-    System.out.println("Testing Data Delete");
-    testDeletingData();
-    System.out.println("Data Delete Test Completed");
-
-    System.out.println("Testing One to Many Fetch");
-    testOneToManyData();
-    System.out.println("One To Many Fetch Test Completed");
-
-    System.out.println("Testing HQL List");
-    testHqlList();
-    System.out.println("HQL List Test Completed");
-
-    System.out.println("Testing HQL Update");
-    testHqlUpdate();
-    System.out.println("HQL Update Test Completed");
-
-    System.out.println("Testing JPA List and Update");
-    testJPACriteria();
-    System.out.println("JPA List and Update Test Completed");
-
-    System.out.println("Testing JPA Delete");
-    testJPACriteriaDelete();
-    System.out.println("JPA Delete Test Completed");
-
-    hibernateConfiguration.closeSessionFactory();
+    try {
+      System.out.println("Testing Foreign Key");
+      testForeignKey();
+      System.out.println("Foreign Key Test Completed");
+
+      System.out.println("Testing Session Rollback");
+      testSessionRollback();
+      System.out.println("Session Rollback Test Completed");
+
+      System.out.println("Testing Data Insert");
+      testAddingData();
+      System.out.println("Data Insert Test Completed");
+
+      System.out.println("Testing Data Delete");
+      testDeletingData();
+      System.out.println("Data Delete Test Completed");
+
+      System.out.println("Testing One to Many Fetch");
+      testOneToManyData();
+      System.out.println("One To Many Fetch Test Completed");
+
+      System.out.println("Testing HQL List");
+      testHqlList();
+      System.out.println("HQL List Test Completed");
+
+      System.out.println("Testing HQL Update");
+      testHqlUpdate();
+      System.out.println("HQL Update Test Completed");
+
+      System.out.println("Testing JPA List and Update");
+      testJPACriteria();
+      System.out.println("JPA List and Update Test Completed");
+
+      System.out.println("Testing JPA Delete");
+      testJPACriteriaDelete();
+      System.out.println("JPA Delete Test Completed");
+    } finally {
+      // Make sure we always close the session factory when the test is done. Otherwise, the sample
+      // application might keep non-daemon threads alive and not stop.
+      hibernateConfiguration.closeSessionFactory();
+    }
   }
 
   public static void main(String[] args) {
-    System.out.println("Starting Hibrnate Test");
+    System.out.println("Starting Hibernate Test");
     HibernateSampleTest hibernateSampleTest =
         new HibernateSampleTest(HibernateConfiguration.createHibernateConfiguration());
     hibernateSampleTest.executeTest();
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 73bdfcf9..c870414f 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
@@ -26,6 +26,7 @@ import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType
 import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.PostgreSQLStatementParser;
 import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
 import com.google.cloud.spanner.connection.StatementResult.ResultType;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.error.PGException;
@@ -137,7 +138,11 @@ public class IntermediateStatement {
 
   /** @return True if this is a select statement, false otherwise. */
   public boolean containsResultSet() {
-    return this.parsedStatement.isQuery();
+    return this.parsedStatement.isQuery()
+        || (this.parsedStatement.getType() == StatementType.CLIENT_SIDE
+            && this.parsedStatement.getClientSideStatementType()
+                == ClientSideStatementType.RUN_BATCH)
+        || (this.parsedStatement.isUpdate() && this.parsedStatement.hasReturningClause());
   }
 
   /** @return True if this statement was executed, False otherwise. */
@@ -168,7 +173,8 @@ public class IntermediateStatement {
       case QUERY:
         return -1L;
       case UPDATE:
-        return this.statementResult.getUpdateCount();
+        long res = this.statementResult.getUpdateCount();
+        return Math.max(res, 0L);
       case CLIENT_SIDE:
       case DDL:
       case UNKNOWN:
diff --git a/src/test/golang/pgadapter_pgx_tests/pgx.go b/src/test/golang/pgadapter_pgx_tests/pgx.go
index 48e56c60..cee4fcd7 100644
--- a/src/test/golang/pgadapter_pgx_tests/pgx.go
+++ b/src/test/golang/pgadapter_pgx_tests/pgx.go
@@ -254,6 +254,95 @@ func TestInsertNullsAllDataTypes(connString string) *C.char {
 	return nil
 }
 
+//export TestInsertAllDataTypesReturning
+func TestInsertAllDataTypesReturning(connString string) *C.char {
+	ctx := context.Background()
+	conn, err := pgx.Connect(ctx, connString)
+	if err != nil {
+		return C.CString(err.Error())
+	}
+	defer conn.Close(ctx)
+
+	sql := "INSERT INTO all_types (col_bigint, col_bool, col_bytea, col_float8, col_int, col_numeric, col_timestamptz, col_date, col_varchar, col_jsonb) " +
+		"values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning *"
+	numeric := pgtype.Numeric{}
+	_ = numeric.Set("6.626")
+	timestamptz, _ := time.Parse(time.RFC3339Nano, "2022-03-24T07:39:10.123456789+01:00")
+	date := pgtype.Date{}
+	_ = date.Set("2022-04-02")
+	var row pgx.Row
+	if strings.Contains(connString, "prefer_simple_protocol=true") {
+		// Simple mode will format the date as '2022-04-02 00:00:00Z', which is not supported by the
+		// backend yet.
+		row = conn.QueryRow(ctx, sql, 100, true, []byte("test_bytes"), 3.14, 1, numeric, timestamptz, "2022-04-02", "test_string", "{\"key\": \"value\"}")
+	} else {
+		row = conn.QueryRow(ctx, sql, 100, true, []byte("test_bytes"), 3.14, 1, numeric, timestamptz, date, "test_string", "{\"key\": \"value\"}")
+	}
+	var bigintValue int64
+	var boolValue bool
+	var byteaValue []byte
+	var float8Value float64
+	var intValue int
+	var numericValue pgtype.Numeric // pgx by default maps numeric to string
+	var timestamptzValue time.Time
+	var dateValue time.Time
+	var varcharValue string
+	var jsonbValue string
+
+	err = row.Scan(
+		&bigintValue,
+		&boolValue,
+		&byteaValue,
+		&float8Value,
+		&intValue,
+		&numericValue,
+		&timestamptzValue,
+		&dateValue,
+		&varcharValue,
+		&jsonbValue,
+	)
+	if err != nil {
+		return C.CString(fmt.Sprintf("Failed to execute insert: %v", err.Error()))
+	}
+	if g, w := bigintValue, int64(1); g != w {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	if g, w := boolValue, true; g != w {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	if g, w := byteaValue, []byte("test"); !reflect.DeepEqual(g, w) {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	if g, w := float8Value, 3.14; g != w {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	if g, w := intValue, 100; g != w {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	var wantNumericValue pgtype.Numeric
+	_ = wantNumericValue.Scan("6.626")
+	if g, w := numericValue, wantNumericValue; !reflect.DeepEqual(g, w) {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	wantDateValue, _ := time.Parse("2006-01-02", "2022-03-29")
+	if g, w := dateValue, wantDateValue; !reflect.DeepEqual(g, w) {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	// Encoding the timestamp values as a parameter will truncate it to microsecond precision.
+	wantTimestamptzValue, _ := time.Parse(time.RFC3339Nano, "2022-02-16T13:18:02.123456+00:00")
+	if g, w := timestamptzValue.UTC().String(), wantTimestamptzValue.UTC().String(); g != w {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	if g, w := varcharValue, "test"; g != w {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+	if g, w := jsonbValue, "{\"key\": \"value\"}"; g != w {
+		return C.CString(fmt.Sprintf("value mismatch\n Got: %v\nWant: %v", g, w))
+	}
+
+	return nil
+}
+
 //export TestUpdateAllDataTypes
 func TestUpdateAllDataTypes(connString string) *C.char {
 	ctx := context.Background()
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java
index 3416b332..8d18de14 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/AbortedMockServerTest.java
@@ -92,7 +92,7 @@ public class AbortedMockServerTest extends AbstractMockServerTest {
   }
 
   private String getExpectedInitialApplicationName() {
-    return null;
+    return "PostgreSQL JDBC Driver";
   }
 
   /**
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcDescribeStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcDescribeStatementTest.java
index 98fbf45c..a347bf26 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcDescribeStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcDescribeStatementTest.java
@@ -39,6 +39,8 @@ import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.sql.Types;
 import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -294,6 +296,24 @@ public class ITJdbcDescribeStatementTest implements IntegrationTest {
     }
   }
 
+  @Test
+  public void testMoreThan50Parameters() throws SQLException {
+    String sql =
+        "select * from all_types where "
+            + IntStream.range(0, 51)
+                .mapToObj(i -> "col_varchar=?")
+                .collect(Collectors.joining(" or "));
+    try (Connection connection = DriverManager.getConnection(getConnectionUrl())) {
+      try (PreparedStatement statement = connection.prepareStatement(sql)) {
+        ParameterMetaData metadata = statement.getParameterMetaData();
+        assertEquals(51, metadata.getParameterCount());
+        for (int i = 1; i < metadata.getParameterCount(); i++) {
+          assertEquals(Types.VARCHAR, metadata.getParameterType(i));
+        }
+      }
+    }
+  }
+
   @Test
   public void testDescribeInvalidStatements() throws SQLException {
     for (String sql :
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 e9f1d85e..5bb93078 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import com.google.cloud.ByteArray;
 import com.google.cloud.Date;
@@ -42,6 +43,7 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.DescribeMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ExecuteMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage;
 import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ListValue;
 import com.google.protobuf.Value;
 import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest;
 import com.google.spanner.v1.BeginTransactionRequest;
@@ -66,6 +68,7 @@ import java.sql.DriverManager;
 import java.sql.ParameterMetaData;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.sql.Types;
 import java.time.LocalDate;
@@ -222,6 +225,49 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     assertEquals(0, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
   }
 
+  @Test
+  public void testClientSideStatementWithResultSet() throws SQLException {
+    String sql = "show statement_timeout";
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        assertEquals("0", resultSet.getString("statement_timeout"));
+        assertFalse(resultSet.next());
+      }
+      connection.createStatement().execute("set statement_timeout=6000");
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        assertEquals("6s", resultSet.getString("statement_timeout"));
+        assertFalse(resultSet.next());
+      }
+    }
+
+    // The statement is handled locally and not sent to Cloud Spanner.
+    assertEquals(0, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+  }
+
+  @Test
+  public void testClientSideStatementWithoutResultSet() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (java.sql.Statement statement = connection.createStatement()) {
+        statement.execute("start batch dml");
+        statement.execute(INSERT_STATEMENT.getSql());
+        statement.execute(UPDATE_STATEMENT.getSql());
+        statement.execute("run batch");
+      }
+    }
+    assertEquals(1, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+    ExecuteBatchDmlRequest request =
+        mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).get(0);
+    assertEquals(2, request.getStatementsCount());
+    assertEquals(INSERT_STATEMENT.getSql(), request.getStatements(0).getSql());
+    assertEquals(UPDATE_STATEMENT.getSql(), request.getStatements(1).getSql());
+    assertTrue(request.getTransaction().hasBegin());
+    assertTrue(request.getTransaction().getBegin().hasReadWrite());
+    assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
+  }
+
   @Test
   public void testSelectCurrentSchema() throws SQLException {
     String sql = "SELECT current_schema";
@@ -518,7 +564,21 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
   public void testQueryWithLegacyDateParameter() throws SQLException {
     String jdbcSql = "select col_date from all_types where col_date=?";
     String pgSql = "select col_date from all_types where col_date=$1";
-    mockSpanner.putStatementResult(StatementResult.query(Statement.of(pgSql), ALL_TYPES_RESULTSET));
+    ResultSetMetadata metadata =
+        ALL_TYPES_METADATA
+            .toBuilder()
+            .setUndeclaredParameters(
+                StructType.newBuilder()
+                    .addFields(
+                        Field.newBuilder()
+                            .setName("p1")
+                            .setType(Type.newBuilder().setCode(TypeCode.DATE).build())
+                            .build())
+                    .build())
+            .build();
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(pgSql), ALL_TYPES_RESULTSET.toBuilder().setMetadata(metadata).build()));
     mockSpanner.putStatementResult(
         StatementResult.query(
             Statement.newBuilder(pgSql).bind("p1").to(Date.parseDate("2022-03-29")).build(),
@@ -572,7 +632,21 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
   public void testAutoDescribedStatementsAreReused() throws SQLException {
     String jdbcSql = "select col_date from all_types where col_date=?";
     String pgSql = "select col_date from all_types where col_date=$1";
-    mockSpanner.putStatementResult(StatementResult.query(Statement.of(pgSql), ALL_TYPES_RESULTSET));
+    ResultSetMetadata metadata =
+        ALL_TYPES_METADATA
+            .toBuilder()
+            .setUndeclaredParameters(
+                StructType.newBuilder()
+                    .addFields(
+                        Field.newBuilder()
+                            .setName("p1")
+                            .setType(Type.newBuilder().setCode(TypeCode.DATE).build())
+                            .build())
+                    .build())
+            .build();
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(pgSql), ALL_TYPES_RESULTSET.toBuilder().setMetadata(metadata).build()));
     mockSpanner.putStatementResult(
         StatementResult.query(
             Statement.newBuilder(pgSql).bind("p1").to(Date.parseDate("2022-03-29")).build(),
@@ -617,6 +691,43 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testDescribeDdlStatement() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (PreparedStatement preparedStatement =
+          connection.prepareStatement("create table foo (id bigint primary key, value varchar)")) {
+        ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData();
+        assertEquals(0, parameterMetaData.getParameterCount());
+        assertNull(preparedStatement.getMetaData());
+      }
+    }
+  }
+
+  @Test
+  public void testDescribeClientSideNoResultStatement() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (PreparedStatement preparedStatement = connection.prepareStatement("start batch dml")) {
+        ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData();
+        assertEquals(0, parameterMetaData.getParameterCount());
+        assertNull(preparedStatement.getMetaData());
+      }
+    }
+  }
+
+  @Test
+  public void testDescribeClientSideResultSetStatement() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (PreparedStatement preparedStatement =
+          connection.prepareStatement("show statement_timeout")) {
+        SQLException exception =
+            assertThrows(SQLException.class, preparedStatement::getParameterMetaData);
+        assertEquals(
+            "ERROR: ResultSetMetadata are available only for results that were returned from Cloud Spanner",
+            exception.getMessage());
+      }
+    }
+  }
+
   @Test
   public void testMultipleQueriesInTransaction() throws SQLException {
     String sql = "SELECT 1";
@@ -1592,6 +1703,123 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testPreparedStatementReturning() throws SQLException {
+    String pgSql =
+        "insert into all_types "
+            + "(col_bigint, col_bool, col_bytea, col_float8, col_int, col_numeric, col_timestamptz, col_date, col_varchar, col_jsonb) "
+            + "values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) "
+            + "returning *";
+    String sql =
+        "insert into all_types "
+            + "(col_bigint, col_bool, col_bytea, col_float8, col_int, col_numeric, col_timestamptz, col_date, col_varchar, col_jsonb) "
+            + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
+            + "returning *";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(pgSql),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(
+                    ALL_TYPES_METADATA
+                        .toBuilder()
+                        .setUndeclaredParameters(
+                            createParameterTypesMetadata(
+                                    ImmutableList.of(
+                                        TypeCode.INT64,
+                                        TypeCode.BOOL,
+                                        TypeCode.BYTES,
+                                        TypeCode.FLOAT64,
+                                        TypeCode.INT64,
+                                        TypeCode.NUMERIC,
+                                        TypeCode.TIMESTAMP,
+                                        TypeCode.DATE,
+                                        TypeCode.STRING,
+                                        TypeCode.JSON))
+                                .getUndeclaredParameters())
+                        .build())
+                .setStats(ResultSetStats.newBuilder().build())
+                .build()));
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.newBuilder(pgSql)
+                .bind("p1")
+                .to(1L)
+                .bind("p2")
+                .to(true)
+                .bind("p3")
+                .to(ByteArray.copyFrom("test"))
+                .bind("p4")
+                .to(3.14d)
+                .bind("p5")
+                .to(100L)
+                .bind("p6")
+                .to(com.google.cloud.spanner.Value.pgNumeric("6.626"))
+                .bind("p7")
+                .to(Timestamp.parseTimestamp("2022-02-16T13:18:02.123457000Z"))
+                .bind("p8")
+                .to(Date.parseDate("2022-03-29"))
+                .bind("p9")
+                .to("test")
+                .bind("p10")
+                .to("{\"key\": \"value\"}")
+                .build(),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(ALL_TYPES_METADATA)
+                .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+                .addRows(ALL_TYPES_RESULTSET.getRows(0))
+                .build()));
+
+    OffsetDateTime zonedDateTime =
+        LocalDateTime.of(2022, 2, 16, 13, 18, 2, 123456789).atOffset(ZoneOffset.UTC);
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (PreparedStatement statement = connection.prepareStatement(sql)) {
+        ParameterMetaData parameterMetaData = statement.getParameterMetaData();
+        assertEquals(10, parameterMetaData.getParameterCount());
+        assertEquals(Types.BIGINT, parameterMetaData.getParameterType(1));
+        assertEquals(Types.BIT, parameterMetaData.getParameterType(2));
+        assertEquals(Types.BINARY, parameterMetaData.getParameterType(3));
+        assertEquals(Types.DOUBLE, parameterMetaData.getParameterType(4));
+        assertEquals(Types.BIGINT, parameterMetaData.getParameterType(5));
+        assertEquals(Types.NUMERIC, parameterMetaData.getParameterType(6));
+        assertEquals(Types.TIMESTAMP, parameterMetaData.getParameterType(7));
+        assertEquals(Types.DATE, parameterMetaData.getParameterType(8));
+        assertEquals(Types.VARCHAR, parameterMetaData.getParameterType(9));
+        // TODO: Enable when support for JSONB has been enabled.
+        // assertEquals(Types.OTHER, parameterMetaData.getParameterType(10));
+        ResultSetMetaData metadata = statement.getMetaData();
+        assertEquals(10, metadata.getColumnCount());
+        assertEquals(Types.BIGINT, metadata.getColumnType(1));
+        assertEquals(Types.BIT, metadata.getColumnType(2));
+        assertEquals(Types.BINARY, metadata.getColumnType(3));
+        assertEquals(Types.DOUBLE, metadata.getColumnType(4));
+        assertEquals(Types.BIGINT, metadata.getColumnType(5));
+        assertEquals(Types.NUMERIC, metadata.getColumnType(6));
+        assertEquals(Types.TIMESTAMP, metadata.getColumnType(7));
+        assertEquals(Types.DATE, metadata.getColumnType(8));
+        assertEquals(Types.VARCHAR, metadata.getColumnType(9));
+        // TODO: Enable when support for JSONB has been enabled.
+        // assertEquals(Types.OTHER, metadata.getColumnType(10));
+
+        int index = 0;
+        statement.setLong(++index, 1L);
+        statement.setBoolean(++index, true);
+        statement.setBytes(++index, "test".getBytes(StandardCharsets.UTF_8));
+        statement.setDouble(++index, 3.14d);
+        statement.setInt(++index, 100);
+        statement.setBigDecimal(++index, new BigDecimal("6.626"));
+        statement.setObject(++index, zonedDateTime);
+        statement.setObject(++index, LocalDate.of(2022, 3, 29));
+        statement.setString(++index, "test");
+        statement.setString(++index, "{\"key\": \"value\"}");
+
+        try (ResultSet resultSet = statement.executeQuery()) {
+          assertTrue(resultSet.next());
+          assertFalse(resultSet.next());
+        }
+      }
+    }
+  }
+
   @Test
   public void testCursorSuccess() throws SQLException {
     try (Connection connection = DriverManager.getConnection(createUrl())) {
@@ -2911,6 +3139,111 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testDmlReturning() throws SQLException {
+    String sql = "INSERT INTO test (value) values ('test') RETURNING id";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(sql),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(
+                    createMetadata(ImmutableList.of(TypeCode.INT64), ImmutableList.of("id")))
+                .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(Value.newBuilder().setStringValue("9999").build())
+                        .build())
+                .build()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        assertEquals(9999L, resultSet.getLong(1));
+        assertFalse(resultSet.next());
+      }
+      try (java.sql.Statement statement = connection.createStatement()) {
+        assertTrue(statement.execute(sql));
+        try (ResultSet resultSet = statement.getResultSet()) {
+          assertTrue(resultSet.next());
+          assertEquals(9999L, resultSet.getLong(1));
+          assertFalse(resultSet.next());
+        }
+        assertFalse(statement.getMoreResults());
+        assertEquals(-1, statement.getUpdateCount());
+      }
+    }
+  }
+
+  @Test
+  public void testDmlReturningMultipleRows() throws SQLException {
+    String sql = "UPDATE test SET value='new_value' WHERE value='old_value' RETURNING id";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(sql),
+            com.google.spanner.v1.ResultSet.newBuilder()
+                .setMetadata(
+                    createMetadata(ImmutableList.of(TypeCode.INT64), ImmutableList.of("id")))
+                .setStats(ResultSetStats.newBuilder().setRowCountExact(3L).build())
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(Value.newBuilder().setStringValue("1").build())
+                        .build())
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(Value.newBuilder().setStringValue("2").build())
+                        .build())
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(Value.newBuilder().setStringValue("3").build())
+                        .build())
+                .build()));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        assertEquals(1L, resultSet.getLong(1));
+        assertTrue(resultSet.next());
+        assertEquals(2L, resultSet.getLong(1));
+        assertTrue(resultSet.next());
+        assertEquals(3L, resultSet.getLong(1));
+        assertFalse(resultSet.next());
+      }
+    }
+
+    assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+    ExecuteSqlRequest executeRequest =
+        mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+    assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
+    assertTrue(executeRequest.getTransaction().hasBegin());
+    assertTrue(executeRequest.getTransaction().getBegin().hasReadWrite());
+    assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
+  }
+
+  @Test
+  public void testUUIDParameter() throws SQLException {
+    assumeTrue(pgVersion.equals("14.1"));
+
+    String jdbcSql = "SELECT * FROM all_types WHERE col_uuid=?";
+    String pgSql = "SELECT * FROM all_types WHERE col_uuid=$1";
+    UUID uuid = UUID.randomUUID();
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.newBuilder(pgSql).bind("p1").to(uuid.toString()).build(),
+            ALL_TYPES_RESULTSET));
+
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (PreparedStatement statement = connection.prepareStatement(jdbcSql)) {
+        statement.setObject(1, uuid);
+        try (ResultSet resultSet = statement.executeQuery()) {
+          assertTrue(resultSet.next());
+          assertFalse(resultSet.next());
+        }
+      }
+    }
+
+    assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+  }
+
   private void verifySettingIsNull(Connection connection, String setting) throws SQLException {
     try (ResultSet resultSet =
         connection.createStatement().executeQuery(String.format("show %s", setting))) {
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ServerTest.java
index 584ac56a..9604872c 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ServerTest.java
@@ -94,4 +94,38 @@ public class ServerTest {
       System.setErr(originalErr);
     }
   }
+
+  @Test
+  public void testInvalidKeyStore() {
+    ByteArrayOutputStream outArrayStream = new ByteArrayOutputStream();
+    PrintStream out = new PrintStream(outArrayStream);
+    ByteArrayOutputStream errArrayStream = new ByteArrayOutputStream();
+    PrintStream err = new PrintStream(errArrayStream);
+
+    PrintStream originalOut = System.out;
+    PrintStream originalErr = System.err;
+    String originalKeyStore = System.getProperty("javax.net.ssl.keyStore");
+    System.setOut(out);
+    System.setErr(err);
+    System.setProperty("javax.net.ssl.keyStore", "/path/to/non/existing/file.pfx");
+
+    try {
+      Server.main(new String[] {});
+      assertEquals(
+          "The server could not be started because an error occurred: Key store /path/to/non/existing/file.pfx does not exist\n",
+          errArrayStream.toString());
+      assertTrue(
+          outArrayStream.toString(),
+          outArrayStream
+              .toString()
+              .startsWith(
+                  String.format("-- Starting PGAdapter version %s --", Server.getVersion())));
+    } finally {
+      System.setOut(originalOut);
+      System.setErr(originalErr);
+      if (originalKeyStore != null) {
+        System.setProperty("javax.net.ssl.keyStore", originalKeyStore);
+      }
+    }
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java
index e303a9f9..1f40ffd6 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java
@@ -81,6 +81,83 @@ public abstract class AbstractNpgsqlMockServerTest extends AbstractMockServerTes
               + "  select 1184 as oid, 'timestamptz' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 8 as typlen, true as typbyval, 'b' as typtype, 'D' as typcategory, true as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1185 as typarray, 'timestamptz_in' as typinput, 'timestamptz_out' as typoutput, 'timestamptz_recv' as typreceive, 'timestamptz_send' as typsend, 'timestamptztypmodin' as typmodin, 'timestamptztypmodout' as typmodout, '-' as typanalyze, 'd' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
               + "  select 1700 as oid, 'numeric' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1231 as typarray, 'numeric_in' as typinput, 'numeric_out' as typoutput, 'numeric_recv' as typreceive, 'numeric_send' as typsend, 'numerictypmodin' as typmodin, 'numerictypmodout' as typmodout, '-' as typanalyze, 'i' as typalign, 'm' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
               + "  select 3802 as oid, 'jsonb' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'U' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 3807 as typarray, 'jsonb_in' as typinput, 'jsonb_out' as typoutput, 'jsonb_recv' as typreceive, 'jsonb_send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'x' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl\n"
+              + "),\n"
+              + "pg_class as (\n"
+              + "  select\n"
+              + "  -1 as oid,\n"
+              + "  table_name as relname,\n"
+              + "  case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+              + "  0 as reltype,\n"
+              + "  0 as reloftype,\n"
+              + "  0 as relowner,\n"
+              + "  1 as relam,\n"
+              + "  0 as relfilenode,\n"
+              + "  0 as reltablespace,\n"
+              + "  0 as relpages,\n"
+              + "  0.0::float8 as reltuples,\n"
+              + "  0 as relallvisible,\n"
+              + "  0 as reltoastrelid,\n"
+              + "  false as relhasindex,\n"
+              + "  false as relisshared,\n"
+              + "  'p' as relpersistence,\n"
+              + "  'r' as relkind,\n"
+              + "  count(*) as relnatts,\n"
+              + "  0 as relchecks,\n"
+              + "  false as relhasrules,\n"
+              + "  false as relhastriggers,\n"
+              + "  false as relhassubclass,\n"
+              + "  false as relrowsecurity,\n"
+              + "  false as relforcerowsecurity,\n"
+              + "  true as relispopulated,\n"
+              + "  'n' as relreplident,\n"
+              + "  false as relispartition,\n"
+              + "  0 as relrewrite,\n"
+              + "  0 as relfrozenxid,\n"
+              + "  0 as relminmxid,\n"
+              + "  '{}'::bigint[] as relacl,\n"
+              + "  '{}'::text[] as reloptions,\n"
+              + "  0 as relpartbound\n"
+              + "from information_schema.tables t\n"
+              + "inner join information_schema.columns using (table_catalog, table_schema, table_name)\n"
+              + "group by t.table_name, t.table_schema\n"
+              + "union all\n"
+              + "select\n"
+              + "    -1 as oid,\n"
+              + "    i.index_name as relname,\n"
+              + "    case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+              + "    0 as reltype,\n"
+              + "    0 as reloftype,\n"
+              + "    0 as relowner,\n"
+              + "    1 as relam,\n"
+              + "    0 as relfilenode,\n"
+              + "    0 as reltablespace,\n"
+              + "    0 as relpages,\n"
+              + "    0.0::float8 as reltuples,\n"
+              + "    0 as relallvisible,\n"
+              + "    0 as reltoastrelid,\n"
+              + "    false as relhasindex,\n"
+              + "    false as relisshared,\n"
+              + "    'p' as relpersistence,\n"
+              + "    'r' as relkind,\n"
+              + "    count(*) as relnatts,\n"
+              + "    0 as relchecks,\n"
+              + "    false as relhasrules,\n"
+              + "    false as relhastriggers,\n"
+              + "    false as relhassubclass,\n"
+              + "    false as relrowsecurity,\n"
+              + "    false as relforcerowsecurity,\n"
+              + "    true as relispopulated,\n"
+              + "    'n' as relreplident,\n"
+              + "    false as relispartition,\n"
+              + "    0 as relrewrite,\n"
+              + "    0 as relfrozenxid,\n"
+              + "    0 as relminmxid,\n"
+              + "    '{}'::bigint[] as relacl,\n"
+              + "    '{}'::text[] as reloptions,\n"
+              + "    0 as relpartbound\n"
+              + "from information_schema.indexes i\n"
+              + "inner join information_schema.index_columns using (table_catalog, table_schema, table_name)\n"
+              + "group by i.index_name, i.table_schema\n"
               + ")\n"
               + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"
               + "FROM (\n"
@@ -150,6 +227,83 @@ public abstract class AbstractNpgsqlMockServerTest extends AbstractMockServerTes
               + "  select 1184 as oid, 'timestamptz' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 8 as typlen, true as typbyval, 'b' as typtype, 'D' as typcategory, true as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1185 as typarray, 'timestamptz_in' as typinput, 'timestamptz_out' as typoutput, 'timestamptz_recv' as typreceive, 'timestamptz_send' as typsend, 'timestamptztypmodin' as typmodin, 'timestamptztypmodout' as typmodout, '-' as typanalyze, 'd' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
               + "  select 1700 as oid, 'numeric' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1231 as typarray, 'numeric_in' as typinput, 'numeric_out' as typoutput, 'numeric_recv' as typreceive, 'numeric_send' as typsend, 'numerictypmodin' as typmodin, 'numerictypmodout' as typmodout, '-' as typanalyze, 'i' as typalign, 'm' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
               + "  select 3802 as oid, 'jsonb' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'U' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 3807 as typarray, 'jsonb_in' as typinput, 'jsonb_out' as typoutput, 'jsonb_recv' as typreceive, 'jsonb_send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'x' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl\n"
+              + "),\n"
+              + "pg_class as (\n"
+              + "  select\n"
+              + "  -1 as oid,\n"
+              + "  table_name as relname,\n"
+              + "  case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+              + "  0 as reltype,\n"
+              + "  0 as reloftype,\n"
+              + "  0 as relowner,\n"
+              + "  1 as relam,\n"
+              + "  0 as relfilenode,\n"
+              + "  0 as reltablespace,\n"
+              + "  0 as relpages,\n"
+              + "  0.0::float8 as reltuples,\n"
+              + "  0 as relallvisible,\n"
+              + "  0 as reltoastrelid,\n"
+              + "  false as relhasindex,\n"
+              + "  false as relisshared,\n"
+              + "  'p' as relpersistence,\n"
+              + "  'r' as relkind,\n"
+              + "  count(*) as relnatts,\n"
+              + "  0 as relchecks,\n"
+              + "  false as relhasrules,\n"
+              + "  false as relhastriggers,\n"
+              + "  false as relhassubclass,\n"
+              + "  false as relrowsecurity,\n"
+              + "  false as relforcerowsecurity,\n"
+              + "  true as relispopulated,\n"
+              + "  'n' as relreplident,\n"
+              + "  false as relispartition,\n"
+              + "  0 as relrewrite,\n"
+              + "  0 as relfrozenxid,\n"
+              + "  0 as relminmxid,\n"
+              + "  '{}'::bigint[] as relacl,\n"
+              + "  '{}'::text[] as reloptions,\n"
+              + "  0 as relpartbound\n"
+              + "from information_schema.tables t\n"
+              + "inner join information_schema.columns using (table_catalog, table_schema, table_name)\n"
+              + "group by t.table_name, t.table_schema\n"
+              + "union all\n"
+              + "select\n"
+              + "    -1 as oid,\n"
+              + "    i.index_name as relname,\n"
+              + "    case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+              + "    0 as reltype,\n"
+              + "    0 as reloftype,\n"
+              + "    0 as relowner,\n"
+              + "    1 as relam,\n"
+              + "    0 as relfilenode,\n"
+              + "    0 as reltablespace,\n"
+              + "    0 as relpages,\n"
+              + "    0.0::float8 as reltuples,\n"
+              + "    0 as relallvisible,\n"
+              + "    0 as reltoastrelid,\n"
+              + "    false as relhasindex,\n"
+              + "    false as relisshared,\n"
+              + "    'p' as relpersistence,\n"
+              + "    'r' as relkind,\n"
+              + "    count(*) as relnatts,\n"
+              + "    0 as relchecks,\n"
+              + "    false as relhasrules,\n"
+              + "    false as relhastriggers,\n"
+              + "    false as relhassubclass,\n"
+              + "    false as relrowsecurity,\n"
+              + "    false as relforcerowsecurity,\n"
+              + "    true as relispopulated,\n"
+              + "    'n' as relreplident,\n"
+              + "    false as relispartition,\n"
+              + "    0 as relrewrite,\n"
+              + "    0 as relfrozenxid,\n"
+              + "    0 as relminmxid,\n"
+              + "    '{}'::bigint[] as relacl,\n"
+              + "    '{}'::text[] as reloptions,\n"
+              + "    0 as relpartbound\n"
+              + "from information_schema.indexes i\n"
+              + "inner join information_schema.index_columns using (table_catalog, table_schema, table_name)\n"
+              + "group by i.index_name, i.table_schema\n"
               + ")\n"
               + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"
               + "FROM (\n"
@@ -393,6 +547,83 @@ public abstract class AbstractNpgsqlMockServerTest extends AbstractMockServerTes
               + "  select 1184 as oid, 'timestamptz' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 8 as typlen, true as typbyval, 'b' as typtype, 'D' as typcategory, true as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1185 as typarray, 'timestamptz_in' as typinput, 'timestamptz_out' as typoutput, 'timestamptz_recv' as typreceive, 'timestamptz_send' as typsend, 'timestamptztypmodin' as typmodin, 'timestamptztypmodout' as typmodout, '-' as typanalyze, 'd' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
               + "  select 1700 as oid, 'numeric' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1231 as typarray, 'numeric_in' as typinput, 'numeric_out' as typoutput, 'numeric_recv' as typreceive, 'numeric_send' as typsend, 'numerictypmodin' as typmodin, 'numerictypmodout' as typmodout, '-' as typanalyze, 'i' as typalign, 'm' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
               + "  select 3802 as oid, 'jsonb' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'U' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 3807 as typarray, 'jsonb_in' as typinput, 'jsonb_out' as typoutput, 'jsonb_recv' as typreceive, 'jsonb_send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'x' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl\n"
+              + "),\n"
+              + "pg_class as (\n"
+              + "  select\n"
+              + "  -1 as oid,\n"
+              + "  table_name as relname,\n"
+              + "  case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+              + "  0 as reltype,\n"
+              + "  0 as reloftype,\n"
+              + "  0 as relowner,\n"
+              + "  1 as relam,\n"
+              + "  0 as relfilenode,\n"
+              + "  0 as reltablespace,\n"
+              + "  0 as relpages,\n"
+              + "  0.0::float8 as reltuples,\n"
+              + "  0 as relallvisible,\n"
+              + "  0 as reltoastrelid,\n"
+              + "  false as relhasindex,\n"
+              + "  false as relisshared,\n"
+              + "  'p' as relpersistence,\n"
+              + "  'r' as relkind,\n"
+              + "  count(*) as relnatts,\n"
+              + "  0 as relchecks,\n"
+              + "  false as relhasrules,\n"
+              + "  false as relhastriggers,\n"
+              + "  false as relhassubclass,\n"
+              + "  false as relrowsecurity,\n"
+              + "  false as relforcerowsecurity,\n"
+              + "  true as relispopulated,\n"
+              + "  'n' as relreplident,\n"
+              + "  false as relispartition,\n"
+              + "  0 as relrewrite,\n"
+              + "  0 as relfrozenxid,\n"
+              + "  0 as relminmxid,\n"
+              + "  '{}'::bigint[] as relacl,\n"
+              + "  '{}'::text[] as reloptions,\n"
+              + "  0 as relpartbound\n"
+              + "from information_schema.tables t\n"
+              + "inner join information_schema.columns using (table_catalog, table_schema, table_name)\n"
+              + "group by t.table_name, t.table_schema\n"
+              + "union all\n"
+              + "select\n"
+              + "    -1 as oid,\n"
+              + "    i.index_name as relname,\n"
+              + "    case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
+              + "    0 as reltype,\n"
+              + "    0 as reloftype,\n"
+              + "    0 as relowner,\n"
+              + "    1 as relam,\n"
+              + "    0 as relfilenode,\n"
+              + "    0 as reltablespace,\n"
+              + "    0 as relpages,\n"
+              + "    0.0::float8 as reltuples,\n"
+              + "    0 as relallvisible,\n"
+              + "    0 as reltoastrelid,\n"
+              + "    false as relhasindex,\n"
+              + "    false as relisshared,\n"
+              + "    'p' as relpersistence,\n"
+              + "    'r' as relkind,\n"
+              + "    count(*) as relnatts,\n"
+              + "    0 as relchecks,\n"
+              + "    false as relhasrules,\n"
+              + "    false as relhastriggers,\n"
+              + "    false as relhassubclass,\n"
+              + "    false as relrowsecurity,\n"
+              + "    false as relforcerowsecurity,\n"
+              + "    true as relispopulated,\n"
+              + "    'n' as relreplident,\n"
+              + "    false as relispartition,\n"
+              + "    0 as relrewrite,\n"
+              + "    0 as relfrozenxid,\n"
+              + "    0 as relminmxid,\n"
+              + "    '{}'::bigint[] as relacl,\n"
+              + "    '{}'::text[] as reloptions,\n"
+              + "    0 as relpartbound\n"
+              + "from information_schema.indexes i\n"
+              + "inner join information_schema.index_columns using (table_catalog, table_schema, table_name)\n"
+              + "group by i.index_name, i.table_schema\n"
               + ")\n"
               + "-- Load field definitions for (free-standing) composite types\n"
               + "SELECT typ.oid, att.attname, att.atttypid\n"
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxMockServerTest.java
index bb3c157a..410fd207 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxMockServerTest.java
@@ -401,6 +401,98 @@ public class PgxMockServerTest extends AbstractMockServerTest {
     assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
   }
 
+  @Test
+  public void testInsertAllDataTypesReturning() {
+    String sql =
+        "INSERT INTO all_types "
+            + "(col_bigint, col_bool, col_bytea, col_float8, col_int, col_numeric, col_timestamptz, col_date, col_varchar, col_jsonb) "
+            + "values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning *";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(sql),
+            ResultSet.newBuilder()
+                .setMetadata(
+                    ALL_TYPES_METADATA
+                        .toBuilder()
+                        .setUndeclaredParameters(
+                            createParameterTypesMetadata(
+                                    ImmutableList.of(
+                                        TypeCode.INT64,
+                                        TypeCode.BOOL,
+                                        TypeCode.BYTES,
+                                        TypeCode.FLOAT64,
+                                        TypeCode.INT64,
+                                        TypeCode.NUMERIC,
+                                        TypeCode.TIMESTAMP,
+                                        TypeCode.DATE,
+                                        TypeCode.STRING,
+                                        TypeCode.STRING))
+                                .getUndeclaredParameters()))
+                .setStats(ResultSetStats.newBuilder().build())
+                .build()));
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.newBuilder(sql)
+                .bind("p1")
+                .to(100L)
+                .bind("p2")
+                .to(true)
+                .bind("p3")
+                .to(ByteArray.copyFrom("test_bytes"))
+                .bind("p4")
+                .to(3.14d)
+                .bind("p5")
+                .to(1L)
+                .bind("p6")
+                .to(com.google.cloud.spanner.Value.pgNumeric("6.626"))
+                .bind("p7")
+                .to(Timestamp.parseTimestamp("2022-03-24T06:39:10.123456000Z"))
+                .bind("p8")
+                .to(Date.parseDate("2022-04-02"))
+                .bind("p9")
+                .to("test_string")
+                .bind("p10")
+                .to("{\"key\": \"value\"}")
+                .build(),
+            ResultSet.newBuilder()
+                .setMetadata(
+                    ALL_TYPES_METADATA
+                        .toBuilder()
+                        .setUndeclaredParameters(
+                            createParameterTypesMetadata(
+                                    ImmutableList.of(
+                                        TypeCode.INT64,
+                                        TypeCode.BOOL,
+                                        TypeCode.BYTES,
+                                        TypeCode.FLOAT64,
+                                        TypeCode.INT64,
+                                        TypeCode.NUMERIC,
+                                        TypeCode.TIMESTAMP,
+                                        TypeCode.DATE,
+                                        TypeCode.STRING,
+                                        TypeCode.STRING))
+                                .getUndeclaredParameters()))
+                .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+                .addRows(ALL_TYPES_RESULTSET.getRows(0))
+                .build()));
+
+    String res = pgxTest.TestInsertAllDataTypesReturning(createConnString());
+
+    assertNull(res);
+    List<ExecuteSqlRequest> requests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class);
+    // pgx by default always uses prepared statements. That means that each request is sent two
+    // times to the backend the first time it is executed:
+    // 1. DescribeStatement (parameters)
+    // 2. Execute
+    assertEquals(2, requests.size());
+    ExecuteSqlRequest describeParamsRequest = requests.get(0);
+    assertEquals(sql, describeParamsRequest.getSql());
+    assertEquals(QueryMode.PLAN, describeParamsRequest.getQueryMode());
+    ExecuteSqlRequest executeRequest = requests.get(1);
+    assertEquals(sql, executeRequest.getSql());
+    assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
+  }
+
   @Test
   public void testInsertBatch() {
     String sql =
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxTest.java
index efd9f97a..3521d6cd 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/golang/PgxTest.java
@@ -31,6 +31,8 @@ public interface PgxTest extends Library {
 
   String TestInsertNullsAllDataTypes(GoString connString);
 
+  String TestInsertAllDataTypesReturning(GoString connString);
+
   String TestUpdateAllDataTypes(GoString connString);
 
   String TestPrepareStatement(GoString connString);
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/hibernate/ITHibernateTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/hibernate/ITHibernateTest.java
index dbdc03a7..50e532f5 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/hibernate/ITHibernateTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/hibernate/ITHibernateTest.java
@@ -14,6 +14,8 @@
 
 package com.google.cloud.spanner.pgadapter.hibernate;
 
+import static org.junit.Assert.assertEquals;
+
 import com.google.cloud.spanner.Database;
 import com.google.cloud.spanner.pgadapter.PgAdapterTestEnv;
 import com.google.common.collect.ImmutableList;
@@ -42,7 +44,7 @@ public class ITHibernateTest {
   private static final String HIBERNATE_SAMPLE_DIRECTORY = "samples/java/hibernate";
   private static final String HIBERNATE_PROPERTIES_FILE =
       HIBERNATE_SAMPLE_DIRECTORY + "/src/main/resources/hibernate.properties";
-  private static final String HIBERNATE_DB_CHANGELOG_DDL_FILE =
+  private static final String HIBERNATE_SAMPLE_SCHEMA_FILE =
       HIBERNATE_SAMPLE_DIRECTORY + "/src/main/resources/sample-schema-sql";
   private static final String HIBERNATE_DEFAULT_URL =
       "jdbc:postgresql://localhost:5432/test-database";
@@ -59,9 +61,9 @@ public class ITHibernateTest {
     testEnv.setUp();
     database = testEnv.createDatabase(ImmutableList.of());
     testEnv.startPGAdapterServer(ImmutableList.of());
-    // Create databasechangelog and databasechangeloglock tables.
+    // Create the sample schema.
     StringBuilder builder = new StringBuilder();
-    try (Scanner scanner = new Scanner(new FileReader(HIBERNATE_DB_CHANGELOG_DDL_FILE))) {
+    try (Scanner scanner = new Scanner(new FileReader(HIBERNATE_SAMPLE_SCHEMA_FILE))) {
       while (scanner.hasNextLine()) {
         builder.append(scanner.nextLine()).append("\n");
       }
@@ -93,6 +95,7 @@ public class ITHibernateTest {
     try (FileWriter writer = new FileWriter(HIBERNATE_PROPERTIES_FILE)) {
       LOGGER.info("Using Hibernate properties:\n" + updatesHibernateProperties);
       writer.write(updatesHibernateProperties);
+      writer.flush();
     }
     buildHibernateSample();
   }
@@ -107,7 +110,7 @@ public class ITHibernateTest {
   }
 
   @Test
-  public void testHibernateUpdate() throws IOException, InterruptedException, SQLException {
+  public void testHibernateUpdate() throws IOException, InterruptedException {
     System.out.println("Running hibernate test");
     ImmutableList<String> hibernateCommand =
         ImmutableList.<String>builder()
@@ -123,7 +126,7 @@ public class ITHibernateTest {
   static void buildHibernateSample() throws IOException, InterruptedException {
     System.out.println("Building Hibernate Sample.");
     ImmutableList<String> hibernateCommand =
-        ImmutableList.<String>builder().add("mvn", "clean", "package").build();
+        ImmutableList.<String>builder().add("mvn", "clean", "compile").build();
     runCommand(hibernateCommand);
     System.out.println("Hibernate Sample build complete.");
   }
@@ -145,7 +148,10 @@ public class ITHibernateTest {
       System.out.println("Printing hibernate loadings");
       output = reader.lines().collect(Collectors.joining("\n"));
       errors = errorReader.lines().collect(Collectors.joining("\n"));
-      System.out.println("Hibernate Command. Output: " + output + ". Error: " + errors);
+      System.out.println(output);
     }
+
+    // Verify that there were no errors, and print the error output if there was an error.
+    assertEquals(errors, 0, process.waitFor());
   }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/liquibase/ITLiquibaseTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/liquibase/ITLiquibaseTest.java
index 311e5c46..346785f3 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/liquibase/ITLiquibaseTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/liquibase/ITLiquibaseTest.java
@@ -102,6 +102,7 @@ public class ITLiquibaseTest {
               testEnv.getPGAdapterPort(), database.getId().getDatabase());
       LOGGER.info("Using Liquibase properties:\n" + properties);
       writer.write(properties);
+      writer.flush();
     }
   }
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/parsers/UuidParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/UuidParserTest.java
new file mode 100644
index 00000000..e406b686
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/parsers/UuidParserTest.java
@@ -0,0 +1,149 @@
+// 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.parsers;
+
+import static com.google.cloud.spanner.pgadapter.parsers.UuidParser.binaryEncode;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+
+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 com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode;
+import com.google.cloud.spanner.pgadapter.session.SessionState;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.postgresql.core.Oid;
+import org.postgresql.util.ByteConverter;
+
+@RunWith(JUnit4.class)
+public class UuidParserTest {
+
+  @Test
+  public void testCreate() {
+    assertEquals(
+        UuidParser.class,
+        Parser.create(
+                mock(SessionState.class),
+                UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8),
+                Oid.UUID,
+                FormatCode.TEXT)
+            .getClass());
+    assertEquals(
+        UuidParser.class,
+        Parser.create(
+                mock(SessionState.class),
+                binaryEncode(UUID.randomUUID().toString()),
+                Oid.UUID,
+                FormatCode.BINARY)
+            .getClass());
+  }
+
+  @Test
+  public void testTextToText() {
+    String uuidStringValue = "c852ee2a-7521-4a70-a02f-2b9d0dd9c19a";
+    UuidParser parser =
+        new UuidParser(uuidStringValue.getBytes(StandardCharsets.UTF_8), FormatCode.TEXT);
+    assertEquals(uuidStringValue, parser.stringParse());
+
+    parser = new UuidParser(null, FormatCode.TEXT);
+    assertNull(parser.stringParse());
+  }
+
+  @Test
+  public void testTextToBinary() {
+    String uuidStringValue = "c852ee2a-7521-4a70-a02f-2b9d0dd9c19a";
+    UuidParser parser =
+        new UuidParser(uuidStringValue.getBytes(StandardCharsets.UTF_8), FormatCode.TEXT);
+    UUID uuid = UUID.fromString(uuidStringValue);
+    byte[] bytes = new byte[16];
+    ByteConverter.int8(bytes, 0, uuid.getMostSignificantBits());
+    ByteConverter.int8(bytes, 8, uuid.getLeastSignificantBits());
+    assertArrayEquals(bytes, parser.binaryParse());
+
+    parser = new UuidParser(null, FormatCode.TEXT);
+    assertNull(parser.binaryParse());
+  }
+
+  @Test
+  public void testBinaryToText() {
+    String uuidStringValue = "c852ee2a-7521-4a70-a02f-2b9d0dd9c19a";
+    UUID uuid = UUID.fromString(uuidStringValue);
+    byte[] bytes = new byte[16];
+    ByteConverter.int8(bytes, 0, uuid.getMostSignificantBits());
+    ByteConverter.int8(bytes, 8, uuid.getLeastSignificantBits());
+
+    UuidParser parser = new UuidParser(bytes, FormatCode.BINARY);
+    assertEquals(uuidStringValue, parser.stringParse());
+
+    parser = new UuidParser(null, FormatCode.BINARY);
+    assertNull(parser.stringParse());
+  }
+
+  @Test
+  public void testBinaryToBinary() {
+    String uuidStringValue = "c852ee2a-7521-4a70-a02f-2b9d0dd9c19a";
+    UUID uuid = UUID.fromString(uuidStringValue);
+    byte[] bytes = new byte[16];
+    ByteConverter.int8(bytes, 0, uuid.getMostSignificantBits());
+    ByteConverter.int8(bytes, 8, uuid.getLeastSignificantBits());
+
+    UuidParser parser = new UuidParser(bytes, FormatCode.BINARY);
+    assertArrayEquals(bytes, parser.binaryParse());
+
+    parser = new UuidParser(null, FormatCode.BINARY);
+    assertNull(parser.binaryParse());
+  }
+
+  @Test
+  public void testInvalidBinaryInput() {
+    PGException exception =
+        assertThrows(PGException.class, () -> new UuidParser(new byte[8], FormatCode.BINARY));
+    assertEquals(SQLState.InvalidParameterValue, exception.getSQLState());
+    assertEquals(Severity.ERROR, exception.getSeverity());
+  }
+
+  @Test
+  public void testInvalidTextInput() {
+    PGException exception =
+        assertThrows(
+            PGException.class,
+            () -> new UuidParser("foo".getBytes(StandardCharsets.UTF_8), FormatCode.TEXT));
+    assertEquals(SQLState.InvalidParameterValue, exception.getSQLState());
+    assertEquals(Severity.ERROR, exception.getSeverity());
+  }
+
+  @Test
+  public void testInvalidTextValueForBinaryEncode() {
+    PGException exception = assertThrows(PGException.class, () -> UuidParser.binaryEncode("bar"));
+    assertEquals(SQLState.InvalidParameterValue, exception.getSQLState());
+    assertEquals(Severity.ERROR, exception.getSeverity());
+  }
+
+  @Test
+  public void testHandleInvalidFormatCode() {
+    PGException exception =
+        assertThrows(PGException.class, () -> UuidParser.handleInvalidFormat(FormatCode.TEXT));
+    assertEquals(SQLState.InternalError, exception.getSQLState());
+    assertEquals(Severity.ERROR, exception.getSeverity());
+    assertEquals("Unsupported format: TEXT", exception.getMessage());
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/python/pg8000/Pg8000BasicsTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/python/pg8000/Pg8000BasicsTest.java
new file mode 100644
index 00000000..7fe46b85
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/python/pg8000/Pg8000BasicsTest.java
@@ -0,0 +1,169 @@
+// 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.python.pg8000;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.pgadapter.AbstractMockServerTest;
+import com.google.cloud.spanner.pgadapter.python.PythonTest;
+import com.google.cloud.spanner.pgadapter.wireprotocol.BindMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.DescribeMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ExecuteMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.FlushMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.StartupMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.SyncMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.TerminateMessage;
+import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
+import com.google.common.collect.ImmutableList;
+import com.google.spanner.v1.ExecuteSqlRequest;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Scanner;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+@Category(PythonTest.class)
+public class Pg8000BasicsTest extends AbstractMockServerTest {
+
+  @Parameter public String host;
+
+  @Parameters(name = "host = {0}")
+  public static List<Object[]> data() {
+    return ImmutableList.of(new Object[] {"localhost"}, new Object[] {"/tmp"});
+  }
+
+  static String execute(String script, String host, int port)
+      throws IOException, InterruptedException {
+    String[] runCommand = new String[] {"python3", script, host, Integer.toString(port)};
+    ProcessBuilder builder = new ProcessBuilder();
+    builder.command(runCommand);
+    builder.directory(new File("./src/test/python/pg8000"));
+    Process process = builder.start();
+    Scanner scanner = new Scanner(process.getInputStream());
+    Scanner errorScanner = new Scanner(process.getErrorStream());
+
+    StringBuilder output = new StringBuilder();
+    while (scanner.hasNextLine()) {
+      output.append(scanner.nextLine()).append("\n");
+    }
+    StringBuilder error = new StringBuilder();
+    while (errorScanner.hasNextLine()) {
+      error.append(errorScanner.nextLine()).append("\n");
+    }
+    int result = process.waitFor();
+    assertEquals(error.toString(), 0, result);
+
+    return output.toString();
+  }
+
+  @Test
+  public void testBasicSelect() throws IOException, InterruptedException {
+    String sql = "SELECT 1";
+
+    String actualOutput = execute("select1.py", host, pgServer.getLocalPort());
+    String expectedOutput = "SELECT 1: [1]\n";
+    assertEquals(expectedOutput, actualOutput);
+
+    assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+    ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+    assertEquals(sql, request.getSql());
+    assertTrue(request.getTransaction().hasSingleUse());
+    assertTrue(request.getTransaction().getSingleUse().hasReadOnly());
+
+    List<WireMessage> messages = getWireMessages();
+    assertEquals(4, messages.size());
+    assertEquals(StartupMessage.class, messages.get(0).getClass());
+    assertEquals(QueryMessage.class, messages.get(1).getClass());
+    assertEquals(QueryMessage.class, messages.get(2).getClass());
+    assertEquals(TerminateMessage.class, messages.get(3).getClass());
+  }
+
+  @Test
+  public void testSelectAllTypes() throws IOException, InterruptedException {
+    String sql = "SELECT * FROM all_types";
+    mockSpanner.putStatementResult(StatementResult.query(Statement.of(sql), ALL_TYPES_RESULTSET));
+
+    String actualOutput = execute("select_all_types.py", host, pgServer.getLocalPort());
+    String expectedOutput =
+        "row: [1, True, b'test', 3.14, 100, Decimal('6.626'), "
+            + "datetime.datetime(2022, 2, 16, 13, 18, 2, 123456, tzinfo=tzutc()), datetime.date(2022, 3, 29), "
+            + "'test', {'key': 'value'}]\n";
+    assertEquals(expectedOutput, actualOutput.replace("tzlocal()", "tzutc()"));
+  }
+
+  @Test
+  public void testSelectParameterized() throws IOException, InterruptedException {
+    String sql = "SELECT * FROM all_types WHERE col_bigint=$1";
+    mockSpanner.putStatementResult(StatementResult.query(Statement.of(sql), ALL_TYPES_RESULTSET));
+
+    String actualOutput = execute("select_parameterized.py", host, pgServer.getLocalPort());
+    String expectedOutput =
+        "first execution: [1, True, b'test', 3.14, 100, Decimal('6.626'), "
+            + "datetime.datetime(2022, 2, 16, 13, 18, 2, 123456, tzinfo=tzutc()), datetime.date(2022, 3, 29), "
+            + "'test', {'key': 'value'}]\n"
+            + "second execution: [1, True, b'test', 3.14, 100, Decimal('6.626'), "
+            + "datetime.datetime(2022, 2, 16, 13, 18, 2, 123456, tzinfo=tzutc()), datetime.date(2022, 3, 29), "
+            + "'test', {'key': 'value'}]\n";
+    assertEquals(expectedOutput, actualOutput.replace("tzlocal()", "tzutc()"));
+
+    List<WireMessage> messages = getWireMessages();
+    assertEquals(25, messages.size());
+    // Yes, you read that right. 25 messages to execute the same query twice.
+    // 3 of these messages are not related to executing the queries:
+    // 1. Startup
+    // 2. Execute `set time zone 'utc'`
+    // 3. Terminate
+    int index = 0;
+    assertEquals(StartupMessage.class, messages.get(index++).getClass());
+    assertEquals(QueryMessage.class, messages.get(index++).getClass());
+
+    assertEquals(ParseMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(SyncMessage.class, messages.get(index++).getClass());
+    assertEquals(DescribeMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(SyncMessage.class, messages.get(index++).getClass());
+    assertEquals(BindMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(ExecuteMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(SyncMessage.class, messages.get(index++).getClass());
+
+    assertEquals(ParseMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(SyncMessage.class, messages.get(index++).getClass());
+    assertEquals(DescribeMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(SyncMessage.class, messages.get(index++).getClass());
+    assertEquals(BindMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(ExecuteMessage.class, messages.get(index++).getClass());
+    assertEquals(FlushMessage.class, messages.get(index++).getClass());
+    assertEquals(SyncMessage.class, messages.get(index++).getClass());
+
+    assertEquals(TerminateMessage.class, messages.get(index++).getClass());
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
index 4a48dee6..29bdd306 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
@@ -33,14 +33,12 @@ import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.DdlTransactionMode;
 import com.google.cloud.spanner.pgadapter.statements.PgCatalog;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import java.util.List;
 import java.util.Map;
 import java.util.TimeZone;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.postgresql.core.Oid;
 
 @RunWith(JUnit4.class)
 public class SessionStateTest {
@@ -828,29 +826,6 @@ public class SessionStateTest {
     assertEquals(DdlTransactionMode.Single, state.getDdlTransactionMode());
   }
 
-  @Test
-  public void testGuessTypes_defaultNonJdbc() {
-    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
-    SessionState state = new SessionState(ImmutableMap.of(), optionsMetadata);
-    assertEquals(ImmutableSet.of(), state.getGuessTypes());
-  }
-
-  @Test
-  public void testGuessTypes_defaultJdbc() {
-    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
-    SessionState state = new SessionState(ImmutableMap.of(), optionsMetadata);
-    state.set("spanner", "guess_types", String.format("%d,%d", Oid.TIMESTAMPTZ, Oid.DATE));
-    assertEquals(ImmutableSet.of(Oid.TIMESTAMPTZ, Oid.DATE), state.getGuessTypes());
-  }
-
-  @Test
-  public void testGuessTypes_invalidOids() {
-    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
-    SessionState state = new SessionState(ImmutableMap.of(), optionsMetadata);
-    state.set("spanner", "guess_types", String.format("%d,%d,foo", Oid.TIMESTAMPTZ, Oid.DATE));
-    assertEquals(ImmutableSet.of(Oid.TIMESTAMPTZ, Oid.DATE), state.getGuessTypes());
-  }
-
   @Test
   public void testGetDefaultTimeZone() {
     Map<String, PGSetting> originalSettings = ImmutableMap.copyOf(SessionState.SERVER_SETTINGS);
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 3684b42b..19b87e5b 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
@@ -35,7 +35,6 @@ import com.google.cloud.spanner.DatabaseId;
 import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.ReadContext;
-import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
 import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
@@ -58,7 +57,6 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.primitives.Bytes;
 import java.io.ByteArrayInputStream;
 import java.io.DataInputStream;
@@ -303,7 +301,6 @@ public class StatementTest {
     when(extendedQueryProtocolHandler.getBackendConnection()).thenReturn(backendConnection);
     SessionState sessionState = mock(SessionState.class);
     when(backendConnection.getSessionState()).thenReturn(sessionState);
-    when(sessionState.getGuessTypes()).thenReturn(ImmutableSet.of());
     String sqlStatement = "SELECT * FROM users WHERE age > $2 AND age < $3 AND name = $1";
     int[] parameterDataTypes = new int[] {Oid.VARCHAR, Oid.INT8, Oid.INT4};
 
@@ -357,7 +354,6 @@ public class StatementTest {
     when(extendedQueryProtocolHandler.getBackendConnection()).thenReturn(backendConnection);
     SessionState sessionState = mock(SessionState.class);
     when(backendConnection.getSessionState()).thenReturn(sessionState);
-    when(sessionState.getGuessTypes()).thenReturn(ImmutableSet.of());
 
     String sqlStatement = "SELECT * FROM users WHERE metadata = $1";
     int[] parameterDataTypes = new int[] {Oid.JSON};
@@ -384,8 +380,6 @@ public class StatementTest {
     when(connectionHandler.getSpannerConnection()).thenReturn(connection);
     when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata);
     String sqlStatement = "SELECT * FROM users WHERE name = $1 AND age > $2 AND age < $3";
-    when(connection.analyzeQuery(Statement.of(sqlStatement), QueryAnalyzeMode.PLAN))
-        .thenReturn(resultSet);
 
     int[] parameters = new int[3];
     Arrays.fill(parameters, Oid.INT8);
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ControlMessageTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessageTest.java
similarity index 85%
rename from src/test/java/com/google/cloud/spanner/pgadapter/ControlMessageTest.java
rename to src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessageTest.java
index 976b3d8b..a5221b2c 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ControlMessageTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ControlMessageTest.java
@@ -12,24 +12,27 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.cloud.spanner.pgadapter;
+package com.google.cloud.spanner.pgadapter.wireprotocol;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
 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.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.QueryMode;
+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.metadata.OptionsMetadata.TextFormat;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection.NoResult;
 import com.google.cloud.spanner.pgadapter.statements.BackendConnection.UpdateCount;
 import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement;
-import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.ManuallyCreatedToken;
-import com.google.cloud.spanner.pgadapter.wireprotocol.ExecuteMessage;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
@@ -138,4 +141,19 @@ public final class ControlMessageTest {
     assertEquals(numOfBytes, outputReader.read(bytes, 0, numOfBytes));
     assertEquals(resultMessage, new String(bytes, UTF8));
   }
+
+  @Test
+  public void testSendNoRowsAsResultSetFails() {
+    when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata);
+    IntermediateStatement describedResult = mock(IntermediateStatement.class);
+    StatementResult statementResult = mock(StatementResult.class);
+    when(statementResult.getResultType()).thenReturn(ResultType.NO_RESULT);
+    when(describedResult.getStatementResult()).thenReturn(statementResult);
+
+    ControlMessage message =
+        new DescribeMessage(connectionHandler, ManuallyCreatedToken.MANUALLY_CREATED_TOKEN);
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> message.sendResultSet(describedResult, QueryMode.SIMPLE, 0L));
+  }
 }
diff --git a/src/test/python/pg8000/basics.py b/src/test/python/pg8000/basics.py
new file mode 100644
index 00000000..2c699099
--- /dev/null
+++ b/src/test/python/pg8000/basics.py
@@ -0,0 +1,37 @@
+import pg8000.dbapi
+
+conn = pg8000.dbapi.connect(host="localhost",
+                            port=5433,
+                            database="knut-test-db",
+                            user="test",
+                            password="test")
+cursor = conn.cursor()
+cursor.execute("DELETE FROM test")
+print("delete rowcount = %s" % cursor.rowcount)
+
+cursor = conn.cursor()
+cursor.execute("INSERT INTO test (id, value) VALUES (%s, %s), (%s, %s) RETURNING id, value",
+               (1, "Ender's Game", 2, "Speaker for the Dead"))
+results = cursor.fetchall()
+for row in results:
+  id, value = row
+  print("id = %s, value = %s" % (id, value))
+
+conn.commit()
+
+cursor.execute("SELECT TIMESTAMPTZ '2021-10-10'")
+row = cursor.fetchone()
+print("Timestamp = %s" % row)
+
+conn.close()
+
+pg8000.dbapi.paramstyle = "numeric"
+conn = pg8000.dbapi.connect(host="localhost",
+                            port=5433,
+                            database="knut-test-db",
+                            user="test",
+                            password="test")
+cursor = conn.cursor()
+cursor.execute("SELECT * FROM test WHERE id=:1", (1,))
+row = cursor.fetchone()
+print("Row: %s" % row)
diff --git a/src/test/python/pg8000/connect.py b/src/test/python/pg8000/connect.py
new file mode 100644
index 00000000..9f86e23a
--- /dev/null
+++ b/src/test/python/pg8000/connect.py
@@ -0,0 +1,21 @@
+import pg8000.dbapi
+import sys
+
+
+def create_conn():
+    host = sys.argv[1]
+    port = sys.argv[2]
+    if host.startswith("/"):
+        conn = pg8000.dbapi.connect(unix_sock=host + "/.s.PGSQL." + port,
+                                    database="d",
+                                    user="test",
+                                    password="test")
+    else:
+        conn = pg8000.dbapi.connect(host=host,
+                                    port=port,
+                                    database="d",
+                                    user="test",
+                                    password="test")
+    conn.execute_simple("set time zone 'utc'")
+    return conn
+
diff --git a/src/test/python/pg8000/requirements.txt b/src/test/python/pg8000/requirements.txt
new file mode 100644
index 00000000..c9557c8c
--- /dev/null
+++ b/src/test/python/pg8000/requirements.txt
@@ -0,0 +1,1 @@
+pg8000~=1.29.3
diff --git a/src/test/python/pg8000/select1.py b/src/test/python/pg8000/select1.py
new file mode 100644
index 00000000..40352fed
--- /dev/null
+++ b/src/test/python/pg8000/select1.py
@@ -0,0 +1,10 @@
+import connect
+
+
+with connect.create_conn() as conn:
+    conn.autocommit = True
+
+    cursor = conn.cursor()
+    cursor.execute("SELECT 1")
+    row = cursor.fetchone()
+    print("SELECT 1: %s" % row)
diff --git a/src/test/python/pg8000/select_all_types.py b/src/test/python/pg8000/select_all_types.py
new file mode 100644
index 00000000..bda89b04
--- /dev/null
+++ b/src/test/python/pg8000/select_all_types.py
@@ -0,0 +1,11 @@
+import connect
+
+
+with connect.create_conn() as conn:
+    conn.autocommit = True
+
+    cursor = conn.cursor()
+    cursor.execute("SELECT * FROM all_types")
+    results = cursor.fetchall()
+    for row in results:
+        print("row: %s" % row)
diff --git a/src/test/python/pg8000/select_parameterized.py b/src/test/python/pg8000/select_parameterized.py
new file mode 100644
index 00000000..00a20c97
--- /dev/null
+++ b/src/test/python/pg8000/select_parameterized.py
@@ -0,0 +1,19 @@
+import connect
+import pg8000.dbapi
+
+pg8000.dbapi.paramstyle = "named"
+
+with connect.create_conn() as conn:
+    conn.autocommit = True
+
+    cursor = conn.cursor()
+    cursor.execute("SELECT * FROM all_types WHERE col_bigint=:id", {"id": 1})
+    results = cursor.fetchall()
+    for row in results:
+        print("first execution: %s" % row)
+
+    cursor = conn.cursor()
+    cursor.execute("SELECT * FROM all_types WHERE col_bigint=:id", {"id": 1})
+    results = cursor.fetchall()
+    for row in results:
+        print("second execution: %s" % row)