Repository: GoogleCloudPlatform/pgadapter
Pull request #1409 (2024-02-15 15:38:48)

build(deps): bump junixsocket.version from 2.8.3 to 2.9.0
Bumps `junixsocket.version` from 2.8.3 to 2.9.0.
Updates `com.kohlschutter.junixsocket:junixsocket-core` from 2.8.3 to 2.9.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/kohlschutter/junixsocket/releases">com.kohlschutter.junixsocket:junixsocket-core's releases</a>.</em></p>
<blockquote>
<h2>junixsocket 2.9.0</h2>
<ul>
<li>Add generic socket fallback for FileDescriptors received from other processes</li>
<li>Add &quot;dup&quot;/&quot;dup2&quot; support via FileDescriptorCast.duplicating</li>
<li>Add listen/accept support to AFDatagramSocket, so we can serve SEQPACKETs</li>
<li>Add more SocketException subclasses (such as BrokenPipe-/ConnectionResetSocketException)</li>
<li>Add support to make shutdown-upon-close configurable</li>
<li>Add support for undocumented &quot;ECLOSED&quot; (errno 3417) condition on IBM i PASE</li>
<li>Add test for the &quot;close-during-accept&quot; condition</li>
<li>Fix native library loading for AIX/IBM i on Java 15 and newer</li>
<li>Fix blocking state when using FileDescriptorCast</li>
<li>Fix module-info.java: Don't mark requirements transient (annotations, mysql connector)</li>
<li>Fix TIPC tests on some old environments (which didn't time out)</li>
<li>Fix compilation for z/OS 32-bit</li>
<li>Fix AFServerSocketChannel.getLocalAddress to return AFSocketAddress subclass</li>
<li>Fix unnecessary failures in some tests, error handling in selftest</li>
<li>Fix &quot;force override&quot; path parsing for native library on Windows</li>
<li>Update build/plugin/test/demo dependencies</li>
<li>Update crossclang scripts; no longer requires root to install Xcode components</li>
<li>Improve error handling on broken Java VMs (e.g., IBM Semeru 8.0.7 and older)</li>
<li>Improve demo code, use slf4j-simple for logging</li>
<li>Code cleanup</li>
</ul>
<p>Backwards-incompatible change: Some AF<em>Socket</em> classes are now final or no longer declare constructor exceptions.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/3f00625c53e2f926c422f52e1ceb55d6c4ba0dc9"><code>3f00625</code></a> Set version to 2.9.0</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/6b328a2dd14315bf8bbfce215543ad4e648180f6"><code>6b328a2</code></a> Update README</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/0b3f6d58f6d0e6183ae53e9fe81ffa625235557e"><code>0b3f6d5</code></a> docs: Update changelog</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/c13098ecc3a88158940cd68d5d6b1f4750988c88"><code>c13098e</code></a> site: docs: Update links in sidebar</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/6f710445c1e548da7212f6d341558186f20dce22"><code>6f71044</code></a> github: Update github workflow dependencies</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/dbd9019078775ee64a30ebbf081553bd5c6b50c8"><code>dbd9019</code></a> test: vsock: Suppress timeout exceptions seen on unconfigured systems</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/ed0488b57549952e1c32f9b3cd7342ff3eea3e39"><code>ed0488b</code></a> test: ThroughputTest: poll with timeout</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/b923dad2b6743efd65d4c24dc2625d2134c679c8"><code>b923dad</code></a> ThroughputTest: Catch ClosedChannelException and ignore if necessary</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/5c1ec41ff7712cb7a4c6e0a6ff7c9b562dd50f33"><code>5c1ec41</code></a> test: common: Actually use different AF_UNIX sockets for newTempAddress</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/c2602d348d64311d6652880229df2e8ee51d4726"><code>c2602d3</code></a> native: Implement dup for Windows sockets</li>
<li>Additional commits viewable in <a href="https://github.com/kohlschutter/junixsocket/compare/junixsocket-2.8.3...junixsocket-2.9.0">compare view</a></li>
</ul>
</details>
<br />

Updates `com.kohlschutter.junixsocket:junixsocket-common` from 2.8.3 to 2.9.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/kohlschutter/junixsocket/releases">com.kohlschutter.junixsocket:junixsocket-common's releases</a>.</em></p>
<blockquote>
<h2>junixsocket 2.9.0</h2>
<ul>
<li>Add generic socket fallback for FileDescriptors received from other processes</li>
<li>Add &quot;dup&quot;/&quot;dup2&quot; support via FileDescriptorCast.duplicating</li>
<li>Add listen/accept support to AFDatagramSocket, so we can serve SEQPACKETs</li>
<li>Add more SocketException subclasses (such as BrokenPipe-/ConnectionResetSocketException)</li>
<li>Add support to make shutdown-upon-close configurable</li>
<li>Add support for undocumented &quot;ECLOSED&quot; (errno 3417) condition on IBM i PASE</li>
<li>Add test for the &quot;close-during-accept&quot; condition</li>
<li>Fix native library loading for AIX/IBM i on Java 15 and newer</li>
<li>Fix blocking state when using FileDescriptorCast</li>
<li>Fix module-info.java: Don't mark requirements transient (annotations, mysql connector)</li>
<li>Fix TIPC tests on some old environments (which didn't time out)</li>
<li>Fix compilation for z/OS 32-bit</li>
<li>Fix AFServerSocketChannel.getLocalAddress to return AFSocketAddress subclass</li>
<li>Fix unnecessary failures in some tests, error handling in selftest</li>
<li>Fix &quot;force override&quot; path parsing for native library on Windows</li>
<li>Update build/plugin/test/demo dependencies</li>
<li>Update crossclang scripts; no longer requires root to install Xcode components</li>
<li>Improve error handling on broken Java VMs (e.g., IBM Semeru 8.0.7 and older)</li>
<li>Improve demo code, use slf4j-simple for logging</li>
<li>Code cleanup</li>
</ul>
<p>Backwards-incompatible change: Some AF<em>Socket</em> classes are now final or no longer declare constructor exceptions.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/3f00625c53e2f926c422f52e1ceb55d6c4ba0dc9"><code>3f00625</code></a> Set version to 2.9.0</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/6b328a2dd14315bf8bbfce215543ad4e648180f6"><code>6b328a2</code></a> Update README</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/0b3f6d58f6d0e6183ae53e9fe81ffa625235557e"><code>0b3f6d5</code></a> docs: Update changelog</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/c13098ecc3a88158940cd68d5d6b1f4750988c88"><code>c13098e</code></a> site: docs: Update links in sidebar</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/6f710445c1e548da7212f6d341558186f20dce22"><code>6f71044</code></a> github: Update github workflow dependencies</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/dbd9019078775ee64a30ebbf081553bd5c6b50c8"><code>dbd9019</code></a> test: vsock: Suppress timeout exceptions seen on unconfigured systems</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/ed0488b57549952e1c32f9b3cd7342ff3eea3e39"><code>ed0488b</code></a> test: ThroughputTest: poll with timeout</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/b923dad2b6743efd65d4c24dc2625d2134c679c8"><code>b923dad</code></a> ThroughputTest: Catch ClosedChannelException and ignore if necessary</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/5c1ec41ff7712cb7a4c6e0a6ff7c9b562dd50f33"><code>5c1ec41</code></a> test: common: Actually use different AF_UNIX sockets for newTempAddress</li>
<li><a href="https://github.com/kohlschutter/junixsocket/commit/c2602d348d64311d6652880229df2e8ee51d4726"><code>c2602d3</code></a> native: Implement dup for Windows sockets</li>
<li>Additional commits viewable in <a href="https://github.com/kohlschutter/junixsocket/compare/junixsocket-2.8.3...junixsocket-2.9.0">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/integration-emulator.yaml b/.github/workflows/integration-emulator.yaml
index ad5c1a6a..c2fec81e 100644
--- a/.github/workflows/integration-emulator.yaml
+++ b/.github/workflows/integration-emulator.yaml
@@ -34,13 +34,17 @@ jobs:
         ports:
           - 9010:9010
           - 9020:9020
+    strategy:
+      fail-fast: false
+      matrix:
+        java: [11, 21]
     steps:
       - uses: actions/checkout@v4
       - name: Setup Java
         uses: actions/setup-java@v4
         with:
           distribution: zulu
-          java-version: 17
+          java-version: ${{matrix.java}}
       - run: java -version
       - name: Setup Go
         uses: actions/setup-go@v5
@@ -65,7 +69,7 @@ jobs:
         run: |
           psql -h /pg -U postgres -c "CREATE DATABASE pgadapter"
       - name: Run integration tests
-        run: mvn verify -B -Dclirr.skip=true -DskipUnits=true -DskipITs=false
+        run: mvn -Dpgadapter.use_virtual_threads=true -Dpgadapter.use_virtual_grpc_transport_threads=true verify -B -Dclirr.skip=true -DskipUnits=true -DskipITs=false
         env:
           POSTGRES_HOST: /pg
           POSTGRES_PORT: 5432
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 37bc0add..f9662c78 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -80,7 +80,7 @@ jobs:
         uses: actions/setup-java@v4
         with:
           distribution: zulu
-          java-version: 17
+          java-version: 21
       - run: java -version
       - name: Setup Go
         uses: actions/setup-go@v5
@@ -106,6 +106,8 @@ jobs:
           psql -h /pg -U postgres -c "CREATE DATABASE pgadapter"
       - name: Run unit tests
         run: mvn test -B -Ptest-all
+      - name: Run unit tests with virtual threads
+        run: mvn test -Dpgadapter.use_virtual_threads=true -Dpgadapter.use_virtual_grpc_transport_threads=true -B -Ptest-all
       - name: Auth
         uses: google-github-actions/auth@v2
         with:
diff --git a/.github/workflows/samples.yaml b/.github/workflows/samples.yaml
index 26cef86d..42cc19d7 100644
--- a/.github/workflows/samples.yaml
+++ b/.github/workflows/samples.yaml
@@ -68,3 +68,17 @@ jobs:
         run: |
           npm install
           npm start
+  ruby-samples:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: '3.0'
+          bundler-cache: true
+      - run: ruby --version
+      - name: Run Ruby on Rails ActiveRecord Sample tests
+        working-directory: ./samples/ruby/activerecord
+        run: |
+          bundle install
+          bundle exec rake run
diff --git a/.github/workflows/units.yaml b/.github/workflows/units.yaml
index 2591734a..75b2771e 100644
--- a/.github/workflows/units.yaml
+++ b/.github/workflows/units.yaml
@@ -9,7 +9,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        java: [8, 11, 17]
+        java: [8, 11, 17, 21]
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-java@v4
@@ -32,7 +32,7 @@ jobs:
         with:
           ruby-version: '3.0'
           bundler-cache: true
-      - run: mvn -B test -Ptest-all -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
+      - run: mvn -Dpgadapter.use_virtual_threads=true -Dpgadapter.use_virtual_grpc_transport_threads=true -B test -Ptest-all -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
   windows:
     runs-on: windows-latest
     env:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b725871..e4324558 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,65 @@
 # Changelog
 
+## [0.31.0](https://github.com/GoogleCloudPlatform/pgadapter/compare/v0.30.0...v0.31.0) (2024-02-21)
+
+
+### Features
+
+* support set_config and current_setting ([#1437](https://github.com/GoogleCloudPlatform/pgadapter/issues/1437)) ([59d3250](https://github.com/GoogleCloudPlatform/pgadapter/commit/59d3250467ec988d546082e3fce9e955019ca5b4))
+
+
+### Bug Fixes
+
+* respect the SPANNER_EMULATOR_HOST env var ([#1432](https://github.com/GoogleCloudPlatform/pgadapter/issues/1432)) ([2f059f7](https://github.com/GoogleCloudPlatform/pgadapter/commit/2f059f7dc034a546bd3244923b159cfd2a0259d7))
+
+
+### Dependencies
+
+* remove junit dependency from compile path ([#1423](https://github.com/GoogleCloudPlatform/pgadapter/issues/1423)) ([9c315d1](https://github.com/GoogleCloudPlatform/pgadapter/commit/9c315d1ed41b97ffef69492d21b7939803bdd512))
+
+
+### Documentation
+
+* Ruby ActiveRecord bit-reversed sequence sample ([#1434](https://github.com/GoogleCloudPlatform/pgadapter/issues/1434)) ([176dd15](https://github.com/GoogleCloudPlatform/pgadapter/commit/176dd150f3e5baf5fbd8a37ef7a63edfa49d1a28))
+* run Ruby ActiveRecord sample on emulator ([#1433](https://github.com/GoogleCloudPlatform/pgadapter/issues/1433)) ([cd6c9dc](https://github.com/GoogleCloudPlatform/pgadapter/commit/cd6c9dc9ae3492b9c561ca892cbbf518645be623))
+
+## [0.30.0](https://github.com/GoogleCloudPlatform/pgadapter/compare/v0.29.1...v0.30.0) (2024-02-16)
+
+
+### Features
+
+* add sample for bit-reversed sequence with Hibernate / JPA ([#1387](https://github.com/GoogleCloudPlatform/pgadapter/issues/1387)) ([216efa6](https://github.com/GoogleCloudPlatform/pgadapter/commit/216efa6f4dd20ed322d0c7e4798752089a2dfa4b))
+* bit-reversed sequences in SQLAlchemy ([#1405](https://github.com/GoogleCloudPlatform/pgadapter/issues/1405)) ([6a7d393](https://github.com/GoogleCloudPlatform/pgadapter/commit/6a7d393d5896d0c119c6ae9a4cc8b62dc67ea239))
+* document and test ARRAY mapping for gorm ([#1400](https://github.com/GoogleCloudPlatform/pgadapter/issues/1400)) ([968ac2d](https://github.com/GoogleCloudPlatform/pgadapter/commit/968ac2daca2b07a7056df688a7986e565535f16d))
+* gorm nested transactions ([#1401](https://github.com/GoogleCloudPlatform/pgadapter/issues/1401)) ([2636a49](https://github.com/GoogleCloudPlatform/pgadapter/commit/2636a49415061bb3b0e392d143e78a20dcd5e400))
+* run SQLAlchemy2 sample on emulator ([#1403](https://github.com/GoogleCloudPlatform/pgadapter/issues/1403)) ([542f2dc](https://github.com/GoogleCloudPlatform/pgadapter/commit/542f2dc99ba72242c217078263a1e1dccc4068ad))
+* support bit-reversed sequences in gorm ([#1397](https://github.com/GoogleCloudPlatform/pgadapter/issues/1397)) ([77d2307](https://github.com/GoogleCloudPlatform/pgadapter/commit/77d2307c322bef537a984628e1abbc83ca154f7d))
+* support knex ([#1169](https://github.com/GoogleCloudPlatform/pgadapter/issues/1169)) ([0a78cf6](https://github.com/GoogleCloudPlatform/pgadapter/commit/0a78cf628bd18c60a6678404f4bc6f0c796cf898))
+* support large batches in gorm ([#1399](https://github.com/GoogleCloudPlatform/pgadapter/issues/1399)) ([24e49de](https://github.com/GoogleCloudPlatform/pgadapter/commit/24e49de987abd154ea017f24a794066c9b204410))
+
+
+### Bug Fixes
+
+* allow space between timestamp and timezone ([#1390](https://github.com/GoogleCloudPlatform/pgadapter/issues/1390)) ([9e91973](https://github.com/GoogleCloudPlatform/pgadapter/commit/9e91973a3641e3209ed5479f6c77bf67ec77a954))
+* limit the num bytes to write to length ([#1388](https://github.com/GoogleCloudPlatform/pgadapter/issues/1388)) ([028fdd4](https://github.com/GoogleCloudPlatform/pgadapter/commit/028fdd46d2069bec24b899d9167ed9c02c86d0d1))
+
+
+### Performance Improvements
+
+* support virtual threads on Java 21 and higher ([#1406](https://github.com/GoogleCloudPlatform/pgadapter/issues/1406)) ([ebdf1dd](https://github.com/GoogleCloudPlatform/pgadapter/commit/ebdf1dd624265d237ecf5f1d738dd9862f5ae49d))
+
+
+### Dependencies
+
+* bouncycastle was moved to new Maven coordinates ([#1420](https://github.com/GoogleCloudPlatform/pgadapter/issues/1420)) ([53b95ce](https://github.com/GoogleCloudPlatform/pgadapter/commit/53b95ce27ba88641ea271a857f803bb330526d83))
+* bump Docker images to Java 21 ([#1410](https://github.com/GoogleCloudPlatform/pgadapter/issues/1410)) ([b696e7f](https://github.com/GoogleCloudPlatform/pgadapter/commit/b696e7f8a24386fd38fe2e407b0c6818f8021541))
+
+
+### Documentation
+
+* add sample for using bit-reversed sequence with plain Hibernate ([#1416](https://github.com/GoogleCloudPlatform/pgadapter/issues/1416)) ([ca23785](https://github.com/GoogleCloudPlatform/pgadapter/commit/ca237859625be7a8626cd79416af59baef16a0b7))
+* remove samples for Cloud Run that embed PGAdapter in Docker image ([#1415](https://github.com/GoogleCloudPlatform/pgadapter/issues/1415)) ([d60dc31](https://github.com/GoogleCloudPlatform/pgadapter/commit/d60dc317fbce6cecace1342d9e0bbaa268f131f9))
+
 ## [0.29.1](https://github.com/GoogleCloudPlatform/pgadapter/compare/v0.29.0...v0.29.1) (2024-02-09)
 
 
diff --git a/README.md b/README.md
index 79805c4e..066845a5 100644
--- a/README.md
+++ b/README.md
@@ -111,9 +111,9 @@ Use the `-s` option to specify a different local port than the default 5432 if y
 PostgreSQL running on your local system.
 
 <!--- {x-version-update-start:google-cloud-spanner-pgadapter:released} -->
-You can also download a specific version of the jar. Example (replace `v0.29.1` with the version you want to download):
+You can also download a specific version of the jar. Example (replace `v0.31.0` with the version you want to download):
 ```shell
-VERSION=v0.29.1
+VERSION=v0.31.0
 wget https://storage.googleapis.com/pgadapter-jar-releases/pgadapter-${VERSION}.tar.gz \
   && tar -xzvf pgadapter-${VERSION}.tar.gz
 java -jar pgadapter.jar -p my-project -i my-instance -d my-database
@@ -148,7 +148,7 @@ This option is only available for Java/JVM-based applications.
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-spanner-pgadapter</artifactId>
-  <version>0.29.1</version>
+  <version>0.31.0</version>
 </dependency>
 <!-- [END pgadapter_dependency] -->
 ```
diff --git a/benchmarks/latency-comparison/java/README.md b/benchmarks/latency-comparison/java/README.md
index 54ff3517..8cb6e937 100644
--- a/benchmarks/latency-comparison/java/README.md
+++ b/benchmarks/latency-comparison/java/README.md
@@ -41,3 +41,11 @@ Run a benchmark with 32 parallel clients each executing 5,000 operations:
 ```shell
 mvn clean compile exec:java -Dexec.args="--clients=32 --operations=5000"
 ```
+
+
+Run a benchmark with 32 parallel clients each executing 1 query per second.
+Each client executes 100 queries:
+
+```shell
+mvn clean compile exec:java -Dexec.args="--clients=32 --operations=100 --wait 1000"
+```
diff --git a/benchmarks/latency-comparison/java/pom.xml b/benchmarks/latency-comparison/java/pom.xml
index 87d6990c..5b26eebe 100644
--- a/benchmarks/latency-comparison/java/pom.xml
+++ b/benchmarks/latency-comparison/java/pom.xml
@@ -38,6 +38,25 @@
     <version>1.7.1</version>
   </parent>
 
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>com.google.cloud</groupId>
+        <artifactId>libraries-bom</artifactId>
+        <version>26.33.0</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <groupId>io.opentelemetry</groupId>
+        <artifactId>opentelemetry-bom</artifactId>
+        <version>1.35.0</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
     <dependency>
       <groupId>com.google.auto.value</groupId>
@@ -46,24 +65,17 @@
     </dependency>
     <dependency>
       <groupId>com.google.cloud</groupId>
-      <artifactId>google-cloud-spanner-pgadapter</artifactId>
-      <version>0.29.1</version>
+      <artifactId>google-cloud-spanner-jdbc</artifactId>
     </dependency>
     <dependency>
       <groupId>com.google.cloud</groupId>
-      <artifactId>google-cloud-spanner-jdbc</artifactId>
-      <version>2.15.3</version>
-      <exclusions>
-        <exclusion>
-          <groupId>com.google.cloud</groupId>
-          <artifactId>google-cloud-spanner</artifactId>
-        </exclusion>
-      </exclusions>
+      <artifactId>google-cloud-spanner-pgadapter</artifactId>
+      <version>0.31.0</version>
     </dependency>
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.1</version>
+      <version>42.7.2</version>
       <scope>compile</scope>
     </dependency>
     <dependency>
@@ -82,13 +94,20 @@
       <artifactId>commons-cli</artifactId>
       <version>1.6.0</version>
     </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.13.2</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   <build>
     <plugins>
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>exec-maven-plugin</artifactId>
-        <version>3.1.1</version>
+        <version>3.2.0</version>
         <configuration>
           <mainClass>com.google.cloud.spanner.pgadapter.latency.LatencyBenchmark</mainClass>
           <cleanupDaemonThreads>false</cleanupDaemonThreads>
diff --git a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractJdbcRunner.java b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractJdbcRunner.java
index fe68ea6d..ef6b708f 100644
--- a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractJdbcRunner.java
+++ b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractJdbcRunner.java
@@ -40,12 +40,13 @@ public abstract class AbstractJdbcRunner extends AbstractRunner {
   }
 
   @Override
-  public List<Duration> execute(String sql, int numClients, int numOperations) {
+  public List<Duration> execute(String sql, int numClients, int numOperations, int waitMillis) {
     try {
       List<Future<List<Duration>>> results = new ArrayList<>(numClients);
       ExecutorService service = Executors.newFixedThreadPool(numClients);
       for (int client = 0; client < numClients; client++) {
-        results.add(service.submit(() -> runBenchmark(createUrl(), sql, numOperations)));
+        results.add(
+            service.submit(() -> runBenchmark(createUrl(), sql, numOperations, waitMillis)));
       }
       return collectResults(service, results, numClients, numOperations);
     } catch (Throwable t) {
@@ -55,17 +56,21 @@ public abstract class AbstractJdbcRunner extends AbstractRunner {
 
   protected abstract String createUrl();
 
-  private List<Duration> runBenchmark(String url, String sql, int numOperations) {
+  private List<Duration> runBenchmark(String url, String sql, int numOperations, int waitMillis) {
     List<Duration> results = new ArrayList<>(numOperations);
     try (Connection connection = DriverManager.getConnection(url)) {
       // Execute one query to make sure everything has been warmed up.
       executeQuery(connection, sql);
 
       for (int i = 0; i < numOperations; i++) {
+        randomWait(waitMillis);
         results.add(executeQuery(connection, sql));
+        incOperations();
       }
     } catch (SQLException exception) {
       throw SpannerExceptionFactory.newSpannerException(exception);
+    } catch (InterruptedException interruptedException) {
+      throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
     }
     return results;
   }
diff --git a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractRunner.java b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractRunner.java
index 9ef4d78a..57c5ff95 100644
--- a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractRunner.java
+++ b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/AbstractRunner.java
@@ -19,17 +19,32 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
 
 abstract class AbstractRunner implements BenchmarkRunner {
+  private final AtomicInteger operationCounter = new AtomicInteger();
+
+  protected void incOperations() {
+    operationCounter.incrementAndGet();
+  }
+
   protected List<Duration> collectResults(
       ExecutorService service,
       List<Future<List<Duration>>> results,
       int numClients,
       int numOperations)
       throws Exception {
+    int totalOperations = numClients * numOperations;
     service.shutdown();
+    while (!service.isTerminated()) {
+      //noinspection BusyWait
+      Thread.sleep(1000L);
+      System.out.printf("\r%d/%d", operationCounter.get(), totalOperations);
+    }
+    System.out.println();
     if (!service.awaitTermination(60L, TimeUnit.MINUTES)) {
       throw new TimeoutException();
     }
@@ -39,4 +54,12 @@ abstract class AbstractRunner implements BenchmarkRunner {
     }
     return allResults;
   }
+
+  protected void randomWait(int waitMillis) throws InterruptedException {
+    if (waitMillis <= 0) {
+      return;
+    }
+    int randomMillis = ThreadLocalRandom.current().nextInt(waitMillis * 2);
+    Thread.sleep(randomMillis);
+  }
 }
diff --git a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/BenchmarkRunner.java b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/BenchmarkRunner.java
index d7121b72..eceec2cd 100644
--- a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/BenchmarkRunner.java
+++ b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/BenchmarkRunner.java
@@ -19,5 +19,5 @@ import java.util.List;
 
 public interface BenchmarkRunner {
 
-  List<Duration> execute(String sql, int numClients, int numOperations);
+  List<Duration> execute(String sql, int numClients, int numOperations, int waitMillis);
 }
diff --git a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/JavaClientRunner.java b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/JavaClientRunner.java
index b30ed201..80d33e53 100644
--- a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/JavaClientRunner.java
+++ b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/JavaClientRunner.java
@@ -40,7 +40,7 @@ public class JavaClientRunner extends AbstractRunner {
   }
 
   @Override
-  public List<Duration> execute(String sql, int numClients, int numOperations) {
+  public List<Duration> execute(String sql, int numClients, int numOperations, int waitMillis) {
     SpannerOptions options =
         SpannerOptions.newBuilder().setProjectId(databaseId.getInstanceId().getProject()).build();
     try (Spanner spanner = options.getService()) {
@@ -49,7 +49,8 @@ public class JavaClientRunner extends AbstractRunner {
       List<Future<List<Duration>>> results = new ArrayList<>(numClients);
       ExecutorService service = Executors.newFixedThreadPool(numClients);
       for (int client = 0; client < numClients; client++) {
-        results.add(service.submit(() -> runBenchmark(databaseClient, sql, numOperations)));
+        results.add(
+            service.submit(() -> runBenchmark(databaseClient, sql, numOperations, waitMillis)));
       }
       return collectResults(service, results, numClients, numOperations);
     } catch (Throwable t) {
@@ -58,13 +59,19 @@ public class JavaClientRunner extends AbstractRunner {
   }
 
   private List<Duration> runBenchmark(
-      DatabaseClient databaseClient, String sql, int numOperations) {
+      DatabaseClient databaseClient, String sql, int numOperations, int waitMillis) {
     List<Duration> results = new ArrayList<>(numOperations);
     // Execute one query to make sure everything has been warmed up.
     executeQuery(databaseClient, sql);
 
     for (int i = 0; i < numOperations; i++) {
-      results.add(executeQuery(databaseClient, sql));
+      try {
+        randomWait(waitMillis);
+        results.add(executeQuery(databaseClient, sql));
+        incOperations();
+      } catch (InterruptedException interruptedException) {
+        throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
+      }
     }
     return results;
   }
diff --git a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/LatencyBenchmark.java b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/LatencyBenchmark.java
index 2f8d7ade..fdce36ce 100644
--- a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/LatencyBenchmark.java
+++ b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/LatencyBenchmark.java
@@ -38,27 +38,7 @@ import org.apache.commons.cli.ParseException;
 
 public class LatencyBenchmark {
   public static void main(String[] args) throws ParseException {
-    Options options = new Options();
-    options.addOption("d", "database", true, "The database to use for benchmarking.");
-    options.addOption(
-        "c", "clients", true, "The number of clients that will be executing queries in parallel.");
-    options.addOption(
-        "o",
-        "operations",
-        true,
-        "The number of operations that each client will execute. Defaults to 1000.");
-    options.addOption("skip_pg", false, "Skip PostgreSQL JDBC benchmarks.");
-    options.addOption("skip_jdbc", false, "Skip Cloud Spanner JDBC benchmarks.");
-    options.addOption("skip_spanner", false, "Skip Cloud Spanner client library benchmarks.");
-    options.addOption(
-        "create_results_table",
-        false,
-        "Create the results table in the test database if it does not exist.");
-    options.addOption("s", "store_results", false, "Store results in the test database.");
-    options.addOption("name", true, "Name of this test run");
-    CommandLineParser parser = new DefaultParser();
-    CommandLine cmd = parser.parse(options, args);
-
+    CommandLine cmd = parseCommandLine(args);
     String project = System.getenv("GOOGLE_CLOUD_PROJECT");
     String instance = System.getenv("SPANNER_INSTANCE");
     String database = System.getenv("SPANNER_DATABASE");
@@ -80,6 +60,35 @@ public class LatencyBenchmark {
     benchmark.run(cmd);
   }
 
+  private static CommandLine parseCommandLine(String[] args) throws ParseException {
+    Options options = new Options();
+    options.addOption("d", "database", true, "The database to use for benchmarking.");
+    options.addOption(
+        "c", "clients", true, "The number of clients that will be executing queries in parallel.");
+    options.addOption(
+        "o",
+        "operations",
+        true,
+        "The number of operations that each client will execute. Defaults to 1000.");
+    options.addOption(
+        "w",
+        "wait",
+        true,
+        "The wait time in milliseconds between each query that is executed by each client. Defaults to 0. "
+            + "Set this to for example 1000 to have each client execute 1 query per second.");
+    options.addOption("skip_pg", false, "Skip PostgreSQL JDBC benchmarks.");
+    options.addOption("skip_jdbc", false, "Skip Cloud Spanner JDBC benchmarks.");
+    options.addOption("skip_spanner", false, "Skip Cloud Spanner client library benchmarks.");
+    options.addOption(
+        "create_results_table",
+        false,
+        "Create the results table in the test database if it does not exist.");
+    options.addOption("s", "store_results", false, "Store results in the test database.");
+    options.addOption("name", true, "Name of this test run");
+    CommandLineParser parser = new DefaultParser();
+    return parser.parse(options, args);
+  }
+
   private final DatabaseId databaseId;
 
   LatencyBenchmark(DatabaseId databaseId) {
@@ -92,6 +101,8 @@ public class LatencyBenchmark {
         commandLine.hasOption('c') ? Integer.parseInt(commandLine.getOptionValue('c')) : 16;
     int operations =
         commandLine.hasOption('o') ? Integer.parseInt(commandLine.getOptionValue('o')) : 1000;
+    int waitMillis =
+        commandLine.hasOption('w') ? Integer.parseInt(commandLine.getOptionValue('w')) : 0;
     boolean runPg = !commandLine.hasOption("skip_pg");
     boolean runJdbc = !commandLine.hasOption("skip_jdbc");
     boolean runSpanner = !commandLine.hasOption("skip_spanner");
@@ -102,6 +113,7 @@ public class LatencyBenchmark {
     System.out.printf("Database: %s\n", databaseId);
     System.out.printf("Clients: %d\n", clients);
     System.out.printf("Operations: %d\n", operations);
+    System.out.printf("Wait between queries: %dms\n", waitMillis);
 
     List<Duration> pgJdbcResults = null;
     if (runPg) {
@@ -110,7 +122,10 @@ public class LatencyBenchmark {
       PgJdbcRunner pgJdbcRunner = new PgJdbcRunner(databaseId);
       pgJdbcResults =
           pgJdbcRunner.execute(
-              "select col_varchar from latency_test where col_bigint=?", clients, operations);
+              "select col_varchar from latency_test where col_bigint=?",
+              clients,
+              operations,
+              waitMillis);
     }
 
     List<Duration> jdbcResults = null;
@@ -120,7 +135,10 @@ public class LatencyBenchmark {
       JdbcRunner jdbcRunner = new JdbcRunner(databaseId);
       jdbcResults =
           jdbcRunner.execute(
-              "select col_varchar from latency_test where col_bigint=?", clients, operations);
+              "select col_varchar from latency_test where col_bigint=?",
+              clients,
+              operations,
+              waitMillis);
     }
 
     List<Duration> javaClientResults = null;
@@ -130,7 +148,10 @@ public class LatencyBenchmark {
       JavaClientRunner javaClientRunner = new JavaClientRunner(databaseId);
       javaClientResults =
           javaClientRunner.execute(
-              "select col_varchar from latency_test where col_bigint=$1", clients, operations);
+              "select col_varchar from latency_test where col_bigint=$1",
+              clients,
+              operations,
+              waitMillis);
     }
 
     printResults("PostgreSQL JDBC Driver", pgJdbcResults);
diff --git a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/PgJdbcRunner.java b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/PgJdbcRunner.java
index 578d4913..eb02f258 100644
--- a/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/PgJdbcRunner.java
+++ b/benchmarks/latency-comparison/java/src/main/java/com/google/cloud/spanner/pgadapter/latency/PgJdbcRunner.java
@@ -32,7 +32,7 @@ public class PgJdbcRunner extends AbstractJdbcRunner {
   }
 
   @Override
-  public List<Duration> execute(String sql, int numClients, int numOperations) {
+  public List<Duration> execute(String sql, int numClients, int numOperations, int waitMillis) {
     // Silence the PGAdapter logging.
     Logger root = Logger.getLogger("");
     root.setLevel(Level.WARNING);
@@ -41,16 +41,18 @@ public class PgJdbcRunner extends AbstractJdbcRunner {
     }
     // Start PGAdapter in-process.
     OptionsMetadata options =
-        new OptionsMetadata(
-            new String[] {
-              "-p", databaseId.getInstanceId().getProject(),
-              "-i", databaseId.getInstanceId().getInstance(),
-              "-dir=", "-s=0"
-            });
+        OptionsMetadata.newBuilder()
+            .setProject(databaseId.getInstanceId().getProject())
+            .setInstance(databaseId.getInstanceId().getInstance())
+            .disableUnixDomainSockets()
+            .setPort(0)
+            .useVirtualThreads()
+            .useVirtualGrpcTransportThreads()
+            .build();
     proxyServer = new ProxyServer(options);
     try {
       proxyServer.startServer();
-      return super.execute(sql, numClients, numOperations);
+      return super.execute(sql, numClients, numOperations, waitMillis);
     } catch (Throwable t) {
       throw SpannerExceptionFactory.asSpannerException(t);
     } finally {
diff --git a/benchmarks/ycsb/Dockerfile b/benchmarks/ycsb/Dockerfile
index 43bcf5f5..afeb4a2b 100644
--- a/benchmarks/ycsb/Dockerfile
+++ b/benchmarks/ycsb/Dockerfile
@@ -1,5 +1,5 @@
 # BUILD
-FROM maven:3.8.4-eclipse-temurin-17-alpine AS build
+FROM maven:3-eclipse-temurin-21 AS build
 # Copy over build files to docker image.
 COPY LICENSE ./
 COPY CONTRIBUTING.md ./
@@ -23,10 +23,13 @@ RUN mvn package -Passembly -DskipTests
 FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:455.0.0-slim
 
 RUN apt update && apt -y install postgresql-client
-RUN apt -y install default-jre
 RUN apt -y install wget
 RUN apt -y install python
 
+# Install Java 21 in the Docker image.
+ADD https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz /openjdk.tar.gz
+RUN tar xvfz /openjdk.tar.gz
+
 COPY --from=build target/pgadapter /
 
 ADD https://github.com/brianfrankcooper/YCSB/releases/download/0.17.0/ycsb-0.17.0.tar.gz /ycsb-0.17.0.tar.gz
diff --git a/benchmarks/ycsb/run.sh b/benchmarks/ycsb/run.sh
index dc589fad..489aff72 100644
--- a/benchmarks/ycsb/run.sh
+++ b/benchmarks/ycsb/run.sh
@@ -1,6 +1,9 @@
 #!/bin/bash
 set -euox pipefail
 
+export JAVA_HOME=/jdk-21.0.2
+export PATH=$PATH:$JAVA_HOME/bin
+
 echo "Starting Task #${CLOUD_RUN_TASK_INDEX}, Attempt #${CLOUD_RUN_TASK_ATTEMPT}..."
 EXECUTED_AT=`date +"%Y-%m-%dT%T"`
 
@@ -10,7 +13,8 @@ if [[ "$DATABASES" != *"$SPANNER_DATABASE"* ]]; then
 fi
 SPANNER_PROJECT=$(gcloud --quiet config get project)
 
-java -jar pgadapter.jar -p $SPANNER_PROJECT -i $SPANNER_INSTANCE -enable_otel -r="minSessions=1000;maxSessions=1000;numChannels=20" &
+java --version
+java -jar pgadapter.jar -p $SPANNER_PROJECT -i $SPANNER_INSTANCE -enable_otel -r="minSessions=1000;maxSessions=1000;numChannels=20;useVirtualThreads=true;useVirtualGrpcTransportThreads=true" &
 sleep 6
 export PGDATABASE=$SPANNER_DATABASE
 psql -h localhost -c "CREATE TABLE IF NOT EXISTS usertable (
diff --git a/build/Dockerfile b/build/Dockerfile
index 69ca38f8..b7197650 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -2,7 +2,7 @@
 #                                     BUILD                                    #
 ################################################################################
 
-FROM maven:3.8.6-eclipse-temurin-17 AS build
+FROM maven:3-eclipse-temurin-21 AS build
 
 # Copy over build files to docker image.
 COPY LICENSE ./
@@ -24,7 +24,7 @@ RUN mvn package -Passembly -DskipTests
 #                                   RELEASE                                    #
 ################################################################################
 
-FROM eclipse-temurin:17-jre
+FROM eclipse-temurin:21-jre
 
 COPY --from=build target/pgadapter /home/pgadapter
 COPY --from=build LICENSE /home/pgadapter/
diff --git a/build/distroless/Dockerfile b/build/distroless/Dockerfile
index 885b9459..d8be0894 100644
--- a/build/distroless/Dockerfile
+++ b/build/distroless/Dockerfile
@@ -2,7 +2,7 @@
 #                                     BUILD                                    #
 ################################################################################
 
-FROM maven:3.8.6-eclipse-temurin-17 AS build
+FROM maven:3-eclipse-temurin-21 AS build
 
 # Copy over build files to docker image.
 COPY LICENSE ./
@@ -24,7 +24,7 @@ RUN mvn package -Passembly -DskipTests
 #                                   RELEASE                                    #
 ################################################################################
 
-FROM gcr.io/distroless/java17-debian11:nonroot
+FROM gcr.io/distroless/java21-debian12:nonroot
 
 COPY --from=build --chown=nonroot target/pgadapter /home/pgadapter
 COPY --from=build --chown=nonroot LICENSE /home/pgadapter/
diff --git a/build/emulator/Dockerfile b/build/emulator/Dockerfile
index a0cc286a..61c84178 100644
--- a/build/emulator/Dockerfile
+++ b/build/emulator/Dockerfile
@@ -19,11 +19,6 @@ COPY --from=emulator /licenses.txt.gz /emulator/licenses.txt.gz
 RUN chmod u+x /emulator/gateway_main
 RUN chmod u+x /emulator/emulator_main
 
-# Add the emulator libraries.
-COPY --from=emulator /lib /lib
-COPY --from=emulator /lib64 /lib64
-COPY --from=emulator /usr/lib /usr/lib
-
 # Expose 5432 (PGAdapter PostgreSQL port), 9010 (Emulator gRPC port), and 9020 (Emulator REST port)
 EXPOSE 5432 9010 9020
 
diff --git a/pom.xml b/pom.xml
index f1c258c8..fb6ec224 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,12 +36,12 @@
     </excludedTests>
     <excludedIntegrationTests/>
 
-    <junixsocket.version>2.8.3</junixsocket.version>
+    <junixsocket.version>2.9.0</junixsocket.version>
   </properties>
 
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-spanner-pgadapter</artifactId>
-  <version>0.29.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-pgadapter:current} -->
+  <version>0.31.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-pgadapter:current} -->
   <name>Google Cloud Spanner PostgreSQL Adapter</name>
   <packaging>jar</packaging>
   <description>
@@ -76,7 +76,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.32.0</version>
+        <version>26.33.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -152,7 +152,7 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.1</version>
+      <version>42.7.2</version>
     </dependency>
     <dependency>
       <groupId>com.kohlschutter.junixsocket</groupId>
@@ -169,6 +169,12 @@
       <groupId>com.googlecode.json-simple</groupId>
       <artifactId>json-simple</artifactId>
       <version>1.1.1</version>
+      <exclusions>
+        <exclusion>
+          <groupId>junit</groupId>
+          <artifactId>junit</artifactId>
+        </exclusion>
+      </exclusions>
     </dependency>
     <dependency>
       <groupId>commons-cli</groupId>
@@ -192,8 +198,8 @@
     </dependency>
     <dependency>
       <groupId>org.bouncycastle</groupId>
-      <artifactId>bcpkix-jdk15on</artifactId>
-      <version>1.70</version>
+      <artifactId>bcpkix-jdk18on</artifactId>
+      <version>1.77</version>
     </dependency>
 
     <!-- Test dependencies -->
diff --git a/samples/cloud-run/java/pom.xml b/samples/cloud-run/java/pom.xml
index 7a95d23c..70fec026 100644
--- a/samples/cloud-run/java/pom.xml
+++ b/samples/cloud-run/java/pom.xml
@@ -51,7 +51,7 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.6.0</version>
+      <version>42.7.2</version>
       <scope>compile</scope>
     </dependency>
     <!-- This dependency allows us to use Unix Domain Sockets to connect to PGAdapter/PostgreSQL. -->
diff --git a/samples/golang/cloud-run/.dockerignore b/samples/golang/cloud-run/.dockerignore
deleted file mode 100644
index 4cc30b02..00000000
--- a/samples/golang/cloud-run/.dockerignore
+++ /dev/null
@@ -1,13 +0,0 @@
-# The .dockerignore file excludes files from the container build process.
-#
-# https://docs.docker.com/engine/reference/builder/#dockerignore-file
-
-# Exclude locally vendored dependencies.
-vendor/
-
-# Exclude "build-time" ignore files.
-.dockerignore
-.gcloudignore
-
-# Exclude git history and configuration.
-.gitignore
diff --git a/samples/golang/cloud-run/.gcloudignore b/samples/golang/cloud-run/.gcloudignore
deleted file mode 100644
index 6173d5b6..00000000
--- a/samples/golang/cloud-run/.gcloudignore
+++ /dev/null
@@ -1,12 +0,0 @@
-# The .gcloudignore file excludes file from upload to Cloud Build.
-# If this file is deleted, gcloud will default to .gitignore.
-#
-# https://cloud.google.com/cloud-build/docs/speeding-up-builds#gcloudignore
-# https://cloud.google.com/sdk/gcloud/reference/topic/gcloudignore
-
-# Exclude locally vendored dependencies.
-vendor/
-
-# Exclude git history and configuration.
-.git/
-.gitignore
diff --git a/samples/golang/cloud-run/Dockerfile b/samples/golang/cloud-run/Dockerfile
deleted file mode 100644
index 795115f2..00000000
--- a/samples/golang/cloud-run/Dockerfile
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2023 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    https://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.
-
-# Start with a Docker image that includes a JRE. This is needed for PGAdapter.
-FROM eclipse-temurin:17-jre AS jre
-
-# Use the offical golang image to create a binary.
-FROM golang:1.19-buster as builder
-
-# Create and change to the app directory.
-WORKDIR /app
-
-# Retrieve application dependencies.
-COPY go.* ./
-RUN go mod download
-
-# Copy local code to the container image.
-COPY . ./
-
-# Build the binary.
-RUN go build -v -o server
-
-# Use the official Debian slim image for a lean production container.
-FROM debian:buster-slim
-
-# Copy the JRE into the production image.
-ENV JAVA_HOME=/opt/java/openjdk
-COPY --from=jre $JAVA_HOME $JAVA_HOME
-ENV PATH="${JAVA_HOME}/bin:${PATH}"
-
-# Copy the app-server binary to the production image from the builder stage.
-COPY --from=builder /app/server /app/server
-
-# Add the latest version of PGAdapter to the container.
-ADD https://storage.googleapis.com/pgadapter-jar-releases/pgadapter.tar.gz /pgadapter.tar.gz
-RUN mkdir /pgadapter
-RUN tar -xzf /pgadapter.tar.gz -C /pgadapter
-
-# Copy the startup script that will start both PGAdapter and the app-server.
-COPY ./startup.sh /startup.sh
-RUN chmod +x /startup.sh
-
-ENTRYPOINT ["/bin/bash", "/startup.sh"]
diff --git a/samples/golang/cloud-run/README.md b/samples/golang/cloud-run/README.md
index b7e04c7b..f446a6bf 100644
--- a/samples/golang/cloud-run/README.md
+++ b/samples/golang/cloud-run/README.md
@@ -1,78 +1,4 @@
 # PGAdapter Cloud Run Sample for Go
 
-This sample application shows how to build and deploy a Go application with PGAdapter to Google Cloud Run.
-
-## Build
-
-First build the Docker image locally and tag it. Replace the `IMAGE_URL` in the script with your own repository.
-Make sure that the repository that you are referencing exists.
-See https://cloud.google.com/artifact-registry/docs/repositories/create-repos for more information on how to set up
-Google Cloud artifact repositories.
-
-```shell
-export IMAGE_URL=my-location-docker.pkg.dev/my-project/my-repository/my-image:latest
-docker build . --tag $IMAGE_URL
-```
-
-Example: `export IMAGE_URL=europe-north1-docker.pkg.dev/my-project/sample-test/pgadapter-sample:latest`
-
-Test the Docker image locally:
-
-```shell
-docker run \
-  -p 8080:8080 \
-  -v /path/to/local_credentials.json:/credentials.json:ro \
-  --env GOOGLE_APPLICATION_CREDENTIALS=/credentials.json \
-  --env SPANNER_PROJECT=my-project \
-  --env SPANNER_INSTANCE=my-instance \
-  --env SPANNER_DATABASE=my-database \
-  $IMAGE_URL
-```
-
-Verify that the connection to Cloud Spanner works correctly by opening a new shell and executing:
-
-```shell
-curl http://localhost:8080
-```
-
-## Deploying to Cloud Run
-
-Push the Docker image to Artifact Registry:
-
-```shell
-gcloud auth configure-docker
-docker push $IMAGE_URL
-```
-
-Then deploy the image as a service on Cloud Run:
-
-```shell
-export SERVICE=my-service
-gcloud run deploy $SERVICE \
-  --image $IMAGE_URL \
-  --update-env-vars SPANNER_PROJECT=my-project,SPANNER_INSTANCE=my-instance,SPANNER_DATABASE=my-database
-```
-
-__NOTE__: This example does not specify any credentials for PGAdapter when it is run on Cloud Run. This means that
-PGAdapter will use the default credentials that is used by Cloud Run. This is by default the default compute engine
-service account. See https://cloud.google.com/run/docs/securing/service-identity for more information on how service
-accounts work on Google Cloud Run.
-
-Test the service (replace URL with your actual service URL):
-
-```shell
-curl https://my-service-xyz.run.app
-```
-
-### Authenticated Cloud Run Service
-
-If your Cloud Run service requires authentication, then first add an IAM binding for your own account and include
-an authentication header with the request:
-
-```shell
-gcloud run services add-iam-policy-binding my-service \
-  --member='user:your-email@gmail.com' \
-  --role='roles/run.invoker'
-curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://my-service-xyz.run.app
-```
-
+[The recommended setup for deploying an application written in Go with PGAdapter on Cloud Run is to
+deploy PGAdapter as a side-car proxy.](../../cloud-run/golang)
diff --git a/samples/golang/cloud-run/cloud-run-sample.go b/samples/golang/cloud-run/cloud-run-sample.go
deleted file mode 100644
index 370c26fc..00000000
--- a/samples/golang/cloud-run/cloud-run-sample.go
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
-Copyright 2023 Google LLC
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"context"
-	"fmt"
-	"log"
-	"net/http"
-	"net/url"
-	"os"
-
-	"github.com/jackc/pgx/v5"
-)
-
-var project, instance, database, qualifiedDatabaseName string
-
-// This sample application can be built as a Docker image and deployed on Cloud Run.
-// The Dockerfile defines a Docker image that is based on the PGAdapter Docker image.
-// This Go sample application is added to that base image and the ENTRYPOINT of the
-// Docker image is modified to start both PGAdapter and then this sample application.
-func main() {
-	// TODO(developer): Replace defaults with your project, instance and database if you want to run the sample
-	//                  without having to specify any environment variables.
-	project = getenv("SPANNER_PROJECT", "my-project")
-	instance = getenv("SPANNER_INSTANCE", "my-instance")
-	database = getenv("SPANNER_DATABASE", "my-database")
-	qualifiedDatabaseName = fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, database)
-
-	log.Printf("\nConnecting to %s\n\n", qualifiedDatabaseName)
-
-	log.Print("starting server...")
-	http.HandleFunc("/", handler)
-
-	// Determine port for HTTP service.
-	port := os.Getenv("PORT")
-	if port == "" {
-		port = "8080"
-		log.Printf("defaulting to port %s", port)
-	}
-
-	// Start HTTP server.
-	log.Printf("listening on port %s", port)
-	if err := http.ListenAndServe(":"+port, nil); err != nil {
-		log.Fatal(err)
-	}
-}
-
-func handler(w http.ResponseWriter, r *http.Request) {
-	// Connect to Cloud Spanner through PGAdapter.
-	// Use url.QueryEscape to URL encode the fully qualified database name. This will replace '/' with '%2F'.
-	connString := fmt.Sprintf("postgres://uid:pwd@127.0.0.1:%d/%s?sslmode=disable", 5432, url.QueryEscape(qualifiedDatabaseName))
-	ctx := context.Background()
-	conn, err := pgx.Connect(ctx, connString)
-	if err != nil {
-		fmt.Fprintf(w, "failed to connect to PGAdapter: %v\n", err)
-		return
-	}
-	defer conn.Close(ctx)
-
-	// Execute a query on Cloud Spanner.
-	var greeting string
-	err = conn.QueryRow(ctx, "select 'Hello world!' as hello").Scan(&greeting)
-	if err != nil {
-		fmt.Fprintf(w, "failed to query Cloud Spanner: %v\n", err)
-		return
-	}
-	fmt.Fprintf(w, "\nGreeting from Cloud Spanner PostgreSQL: %v\n\n", greeting)
-}
-
-func getenv(key, def string) string {
-	value := os.Getenv(key)
-	if value == "" {
-		return def
-	}
-	return value
-}
diff --git a/samples/golang/cloud-run/go.mod b/samples/golang/cloud-run/go.mod
deleted file mode 100644
index 2ec034c6..00000000
--- a/samples/golang/cloud-run/go.mod
+++ /dev/null
@@ -1,12 +0,0 @@
-module pgadapter-cloud-run
-
-go 1.19
-
-require github.com/jackc/pgx/v5 v5.3.1
-
-require (
-	github.com/jackc/pgpassfile v1.0.0 // indirect
-	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
-	golang.org/x/crypto v0.17.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
-)
diff --git a/samples/golang/cloud-run/go.sum b/samples/golang/cloud-run/go.sum
deleted file mode 100644
index 0aa66050..00000000
--- a/samples/golang/cloud-run/go.sum
+++ /dev/null
@@ -1,21 +0,0 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
-github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
-github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/samples/golang/cloud-run/startup.sh b/samples/golang/cloud-run/startup.sh
deleted file mode 100644
index a15ad733..00000000
--- a/samples/golang/cloud-run/startup.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-
-# Run both PGAdapter and the web server when the Docker container is started.
-# Start PGAdapter in plain TCP only mode, that is:
-# 1. Do not specify any credentials. This will make sure PGAdapter picks the credentials from the current environment.
-#    You must ensure that the service account that is used by Cloud Run is allowed to access the Cloud Spanner database
-#    that your application is using.
-# 2. Do not specify any specific project, instance or database. This will require the application to specify a fully
-#    qualified database name when connecting to PGAdapter.
-# 3. Set the Unix domain socket directory to the empty string to disable domain sockets. Domain sockets are not
-#    supported on Cloud Run.
-
-# Start PGAdapter in the background.
-java -jar /pgadapter/pgadapter.jar -dir= &
-
-# Start the app server as the main application of the Docker container.
-exec "/app/server"
diff --git a/samples/golang/gorm/go.mod b/samples/golang/gorm/go.mod
index 17eb1932..02eb22d1 100644
--- a/samples/golang/gorm/go.mod
+++ b/samples/golang/gorm/go.mod
@@ -7,8 +7,8 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/jackc/pgtype v1.14.2
 	github.com/shopspring/decimal v1.3.1
-	github.com/testcontainers/testcontainers-go v0.27.0
-	github.com/testcontainers/testcontainers-go/modules/postgres v0.27.0
+	github.com/testcontainers/testcontainers-go v0.28.0
+	github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0
 	gorm.io/datatypes v1.2.0
 	gorm.io/driver/postgres v1.5.6
 	gorm.io/gorm v1.25.7
@@ -26,7 +26,7 @@ require (
 	github.com/containerd/log v0.1.0 // indirect
 	github.com/cpuguy83/dockercfg v0.3.1 // indirect
 	github.com/distribution/reference v0.5.0 // indirect
-	github.com/docker/docker v25.0.1+incompatible // indirect
+	github.com/docker/docker v25.0.2+incompatible // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -39,7 +39,8 @@ require (
 	github.com/jackc/pgio v1.0.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
-	github.com/jackc/pgx/v5 v5.4.3 // indirect
+	github.com/jackc/pgx/v5 v5.5.3 // indirect
+	github.com/jackc/puddle/v2 v2.2.1 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/klauspost/compress v1.17.4 // indirect
@@ -68,6 +69,7 @@ require (
 	golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/oauth2 v0.16.0 // indirect
+	golang.org/x/sync v0.6.0 // indirect
 	golang.org/x/sys v0.16.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/tools v0.17.0 // indirect
diff --git a/samples/golang/gorm/go.sum b/samples/golang/gorm/go.sum
index ebf16b11..5b6917f2 100644
--- a/samples/golang/gorm/go.sum
+++ b/samples/golang/gorm/go.sum
@@ -35,8 +35,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
 github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/docker v25.0.1+incompatible h1:k5TYd5rIVQRSqcTwCID+cyVA0yRg86+Pcrz1ls0/frA=
-github.com/docker/docker v25.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v25.0.2+incompatible h1:/OaKeauroa10K4Nqavw4zlhcDq/WBcPMc5DbjOGgozY=
+github.com/docker/docker v25.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -119,11 +119,13 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9
 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
 github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c h1:Dznn52SgVIVst9UyOT9brctYUgxs+CvVfPaC3jKrA50=
 github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
-github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
-github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
+github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s=
+github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@@ -214,10 +216,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/testcontainers/testcontainers-go v0.27.0 h1:IeIrJN4twonTDuMuBNQdKZ+K97yd7VrmNGu+lDpYcDk=
-github.com/testcontainers/testcontainers-go v0.27.0/go.mod h1:+HgYZcd17GshBUZv9b+jKFJ198heWPQq3KQIp2+N+7U=
-github.com/testcontainers/testcontainers-go/modules/postgres v0.27.0 h1:gbA/HYjBIwOwhE/t4p3kIprfI0qsxCk+YVW7P9XFOus=
-github.com/testcontainers/testcontainers-go/modules/postgres v0.27.0/go.mod h1:VFrFKUUgET2hNXStdtaC7uOIJWviFUrixhKeaVw/4F4=
+github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8=
+github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0 h1:ff0s4JdYIdNAVSi/SrpN2Pdt1f+IjIw3AKjbHau8Un4=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.28.0/go.mod h1:fXgcYpbyrduNdiz2qRZuYkmvqLnEqsjbQiBNYH1ystI=
 github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
 github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
 github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
diff --git a/samples/java/cloud-run/pom.xml b/samples/java/cloud-run/pom.xml
index 65c34370..4fe97b69 100644
--- a/samples/java/cloud-run/pom.xml
+++ b/samples/java/cloud-run/pom.xml
@@ -47,13 +47,13 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.1</version>
+      <version>42.7.2</version>
       <scope>compile</scope>
     </dependency>
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-spanner-pgadapter</artifactId>
-      <version>0.29.1</version>
+      <version>0.31.0</version>
     </dependency>
     <dependency>
       <groupId>com.kohlschutter.junixsocket</groupId>
diff --git a/samples/java/hibernate/README.md b/samples/java/hibernate/README.md
index a88b32e4..a7561ce9 100644
--- a/samples/java/hibernate/README.md
+++ b/samples/java/hibernate/README.md
@@ -3,8 +3,8 @@
 PGAdapter can be used in combination with Hibernate, but with some limitations. This sample shows to use Hibernate with
 PGAdapter.
 
-> __Note__: This sample uses Hibernate directly. There is also a sample for using [Spring Data JPA
-with PGAdapter here](../spring-data-jpa).
+> __Note__: This sample uses Hibernate directly. There is also a sample for using
+> [Spring Data JPA and Spring Boot with PGAdapter here](../spring-data-jpa).
 
 You can run the sample on the Cloud Spanner emulator with this command:
 
@@ -55,7 +55,6 @@ The following limitations are currently known:
 | Limitation             | Workaround                                                                                                                                                                                                                                                                   |
 |------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | Schema updates         | Cloud Spanner does not support the full PostgreSQL DDL dialect. Automated schema updates using `hibernate` are therefore not supported. It is recommended to set the option `hibernate.hbm2ddl.auto=none` (or `spring.jpa.hibernate.ddl-auto=none` if you are using Spring). |
-| Generated primary keys | Cloud Spanner does not support `sequences`. Auto-increment primary key is not supported. Remove auto increment annotation for primary key columns. The recommended type of primary key is a client side generated `UUID` stored as a string.                                 |
 | Pessimistic Locking    | Cloud Spanner does not support `LockMode.UPGRADE` and `LockMode.UPGRADE_NOWAIT` lock modes.                                                                                                                                                                                  |
 
 
@@ -69,11 +68,10 @@ your schema in production. This gives you more control over the changes that are
 you to store schema changes in a code repository. See [Spring Data JPA with PGAdapter here](../spring-data-jpa)
 for an example of how to integrate Liquibase with your application.
 
-### Generated Primary Keys
-`Sequences` are not supported. Hence, auto increment primary key is not supported and should be replaced with primary key definitions that
-are manually assigned. See https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots
-for more information on choosing a good primary key. This sample uses UUIDs that are generated by the client for primary
-keys.
+### Generated Primary Keys - UUIDs
+See https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots for more
+information on choosing a good primary key. Most entities in this sample use UUIDs that are
+generated by the client for primary keys.
 
 ```java
 public class User {
@@ -83,4 +81,65 @@ public class User {
     @GeneratedValue(strategy = GenerationType.UUID)
     private UUID id;
 }
-```
\ No newline at end of file
+```
+
+### Generated Primary Keys - Bit-reversed Sequence
+
+Using a traditional auto-increment primary key with Cloud Spanner is not recommended, because a
+monotonically increasing or decreasing primary key value can create a write-hotspot. This will cause
+all writes to be sent to one server. See https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots
+for more background information.
+
+Cloud Spanner therefore supports bit-reversed sequences. These internally work as traditional
+sequences, but the value that is returned is bit-reversed before being returned to the user. These
+sequences can be used to generate primary keys with JPA / Hibernate.
+
+Note that Hibernate requires sequences to support pooling in order to support efficient batching of
+multiple inserts. Pooling normally requires the sequence to support an increment size larger than 1.
+Bit-reversed sequences can also support pooling, but require a custom ID generator to be used.
+Follow these steps to define an entity that uses a bit-reversed sequence for generating a primary
+key that also supports batching:
+
+1. Add the following dependency to your project:
+
+```xml
+<!-- Add Spanner Hibernate tools for access to the batch compatible bit-reversed sequence generator. -->
+<dependency>
+   <groupId>com.google.cloud</groupId>
+   <artifactId>google-cloud-spanner-hibernate-tools</artifactId>
+   <version>3.1.0</version>
+</dependency>
+```
+
+2. Add the following annotations to your entity:
+
+```java
+  @Id
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ticketSaleId")
+  @GenericGenerator(
+      // This is the name of the generator, not the name of the sequence. This name must correspond
+      // with the name given in the @GeneratedValue above.
+      name = "ticketSaleId",
+      // Use this custom strategy to ensure the use of a bit-reversed sequence that is compatible
+      // with batching multiple inserts. See also
+      // https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#batch.
+      type = PooledBitReversedSequenceStyleGenerator.class,
+      parameters = {
+        // Use a separate sequence name for each entity.
+        // See resources/db.changelog-v1.1.sql file for the sequence definition in the database.
+        @Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "ticket_sale_seq"),
+        // The increment_size is not actually set on the sequence that is created, but is used to
+        // generate a SELECT query that fetches this number of identifiers at once.
+        @Parameter(name = SequenceStyleGenerator.INCREMENT_PARAM, value = "50"),
+        @Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "50000"),
+        // Add any range that should be excluded by the generator if your table already
+        // contains existing values that have been generated by other generators.
+        @Parameter(
+            name = PooledBitReversedSequenceStyleGenerator.EXCLUDE_RANGE_PARAM,
+            value = "[1,1000]"),
+      })
+  public Long id;
+```
+
+See [TicketSale.java](src/main/java/com/google/cloud/postgres/models/TicketSale.java)
+for a working example.
diff --git a/samples/java/hibernate/pom.xml b/samples/java/hibernate/pom.xml
index b64d8248..8c76e86a 100644
--- a/samples/java/hibernate/pom.xml
+++ b/samples/java/hibernate/pom.xml
@@ -21,7 +21,7 @@
       <dependency>
         <groupId>org.testcontainers</groupId>
         <artifactId>testcontainers-bom</artifactId>
-        <version>1.19.5</version>
+        <version>1.19.6</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -33,7 +33,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-spanner-pgadapter</artifactId>
-      <version>0.29.1</version>
+      <version>0.31.0</version>
     </dependency>
     <!-- Hibernate core dependency -->
     <dependency>
@@ -45,7 +45,19 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.1</version>
+      <version>42.7.2</version>
+    </dependency>
+    <!-- Add Spanner Hibernate tools for access to the batch compatible bit-reversed sequence generator. -->
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-spanner-hibernate-tools</artifactId>
+      <version>3.1.0</version>
+    </dependency>
+    <!-- Add Hypersistence Utils for access to additional data type mappings, for example for arrays. -->
+    <dependency>
+      <groupId>io.hypersistence</groupId>
+      <artifactId>hypersistence-utils-hibernate-63</artifactId>
+      <version>3.7.3</version>
     </dependency>
     <!-- Add testcontainers, so we can run a Docker container with PGAdapter+Spanner Emulator -->
     <dependency>
@@ -73,7 +85,7 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>exec-maven-plugin</artifactId>
-        <version>3.1.1</version>
+        <version>3.2.0</version>
         <configuration>
           <mainClass>com.google.cloud.postgres.SampleApplication</mainClass>
           <cleanupDaemonThreads>false</cleanupDaemonThreads>
diff --git a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/Utils.java b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/Utils.java
index 8e072ca7..dc49faa6 100644
--- a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/Utils.java
+++ b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/Utils.java
@@ -17,6 +17,7 @@ package com.google.cloud.postgres;
 import com.google.cloud.postgres.models.Albums;
 import com.google.cloud.postgres.models.Concerts;
 import com.google.cloud.postgres.models.Singers;
+import com.google.cloud.postgres.models.TicketSale;
 import com.google.cloud.postgres.models.Tracks;
 import com.google.cloud.postgres.models.TracksId;
 import com.google.cloud.postgres.models.VenueDescription;
@@ -24,6 +25,9 @@ import com.google.cloud.postgres.models.Venues;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Random;
 
 public class Utils {
@@ -57,6 +61,16 @@ public class Utils {
     concerts.setName("Sunburn");
     concerts.setSingers(singers);
     concerts.setVenues(venues);
+
+    List<TicketSale> ticketSales = new ArrayList<>();
+    TicketSale ticketSale = new TicketSale();
+    ticketSale.setConcert(concerts);
+    ticketSale.setCustomerName("Alice");
+    ticketSale.setPrice(new BigDecimal("99.90"));
+    ticketSale.setSeats(Arrays.asList("A19", "A20", "A21"));
+    ticketSales.add(ticketSale);
+    concerts.setTicketSales(ticketSales);
+
     return concerts;
   }
 
diff --git a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/Concerts.java b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/Concerts.java
index ea556ae6..9c643228 100644
--- a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/Concerts.java
+++ b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/Concerts.java
@@ -15,6 +15,7 @@
 package com.google.cloud.postgres.models;
 
 import com.google.cloud.postgres.CurrentLocalDateTimeGenerator;
+import jakarta.persistence.CascadeType;
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.GeneratedValue;
@@ -22,7 +23,9 @@ import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
 import jakarta.persistence.JoinColumn;
 import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
 import java.time.LocalDateTime;
+import java.util.List;
 import org.hibernate.annotations.GenerationTime;
 import org.hibernate.annotations.GeneratorType;
 
@@ -51,6 +54,9 @@ public class Concerts {
   @JoinColumn(name = "venue_id")
   private Venues venues;
 
+  @OneToMany(cascade = CascadeType.ALL, mappedBy = "concert")
+  private List<TicketSale> ticketSales;
+
   @Column(name = "created_at", columnDefinition = "timestamptz")
   private LocalDateTime createdAt;
 
@@ -106,6 +112,14 @@ public class Concerts {
     this.venues = venues;
   }
 
+  public List<TicketSale> getTicketSales() {
+    return ticketSales;
+  }
+
+  public void setTicketSales(List<TicketSale> ticketSales) {
+    this.ticketSales = ticketSales;
+  }
+
   public LocalDateTime getCreatedAt() {
     return createdAt;
   }
diff --git a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/HibernateConfiguration.java b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/HibernateConfiguration.java
index 5f7285c8..7d72793c 100644
--- a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/HibernateConfiguration.java
+++ b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/HibernateConfiguration.java
@@ -50,6 +50,7 @@ public class HibernateConfiguration {
     configuration.addAnnotatedClass(Venues.class);
     configuration.addAnnotatedClass(Tracks.class);
     configuration.addAnnotatedClass(TracksId.class);
+    configuration.addAnnotatedClass(TicketSale.class);
 
     final SessionFactory sessionFactory = configuration.buildSessionFactory();
 
diff --git a/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/TicketSale.java b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/TicketSale.java
new file mode 100644
index 00000000..cfdc9694
--- /dev/null
+++ b/samples/java/hibernate/src/main/java/com/google/cloud/postgres/models/TicketSale.java
@@ -0,0 +1,144 @@
+// Copyright 2024 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.postgres.models;
+
+import com.google.cloud.spanner.hibernate.PooledBitReversedSequenceStyleGenerator;
+import io.hypersistence.utils.hibernate.type.array.ListArrayType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.math.BigDecimal;
+import java.util.List;
+import org.hibernate.annotations.GenericGenerator;
+import org.hibernate.annotations.Parameter;
+import org.hibernate.annotations.Type;
+import org.hibernate.id.enhanced.SequenceStyleGenerator;
+
+/**
+ * This entity uses a bit-reversed sequence to generate a primary key. The strategy it uses is
+ * compatible with Hibernate batching.
+ *
+ * <p>The ID generator it uses is defined in this dependency:
+ *
+ * <pre>{@code
+ * <dependency>
+ *   <groupId>com.google.cloud</groupId>
+ *   <artifactId>google-cloud-spanner-hibernate-tools</artifactId>
+ * </dependency>
+ * }</pre>
+ *
+ * <p>The table and sequence definition that is used for this entity can be found in the file
+ * resources/sample-schema.sql
+ */
+@Table(name = "ticket_sales")
+@Entity
+public class TicketSale {
+  @Id
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ticketSaleId")
+  @GenericGenerator(
+      // This is the name of the generator, not the name of the sequence. This name must correspond
+      // with the name given in the @GeneratedValue above.
+      name = "ticketSaleId",
+      // Use this custom strategy to ensure the use of a bit-reversed sequence that is compatible
+      // with batching multiple inserts. See also
+      // https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#batch.
+      type = PooledBitReversedSequenceStyleGenerator.class,
+      parameters = {
+        // Use a separate sequence name for each entity.
+        // See resources/db.changelog-v1.1.sql file for the sequence definition in the database.
+        @Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "ticket_sale_seq"),
+        // The increment_size is not actually set on the sequence that is created, but is used to
+        // generate a SELECT query that fetches this number of identifiers at once.
+        @Parameter(name = SequenceStyleGenerator.INCREMENT_PARAM, value = "50"),
+        @Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "50000"),
+        // Add any range that should be excluded by the generator if your table already
+        // contains existing values that have been generated by other generators.
+        @Parameter(
+            name = PooledBitReversedSequenceStyleGenerator.EXCLUDE_RANGE_PARAM,
+            value = "[1,1000]"),
+      })
+  public Long id;
+
+  @ManyToOne(optional = false, fetch = FetchType.LAZY)
+  private Concerts concert;
+
+  @Column(name = "customer_name")
+  private String customerName;
+
+  private BigDecimal price;
+
+  /**
+   * This column is mapped to a text[] column in the database. The {@link ListArrayType} is defined
+   * in the hypersistence-utils-hibernate-63 library. Add the following to your pom.xml to use it:
+   *
+   * <pre>{@code
+   * <dependency>
+   *   <groupId>io.hypersistence</groupId>
+   *   <artifactId>hypersistence-utils-hibernate-63</artifactId>
+   *   <version>3.7.2</version>
+   * </dependency>
+   * }</pre>
+   */
+  @Type(ListArrayType.class)
+  @Column(columnDefinition = "_varchar")
+  private List<String> seats;
+
+  public TicketSale() {}
+
+  public Long getId() {
+    return id;
+  }
+
+  public void setId(Long id) {
+    this.id = id;
+  }
+
+  public Concerts getConcert() {
+    return concert;
+  }
+
+  public void setConcert(Concerts concert) {
+    this.concert = concert;
+  }
+
+  public String getCustomerName() {
+    return customerName;
+  }
+
+  public void setCustomerName(String customerName) {
+    this.customerName = customerName;
+  }
+
+  public BigDecimal getPrice() {
+    return price;
+  }
+
+  public void setPrice(BigDecimal price) {
+    this.price = price;
+  }
+
+  public List<String> getSeats() {
+    return seats;
+  }
+
+  public void setSeats(List<String> seats) {
+    this.seats = seats;
+  }
+}
diff --git a/samples/java/hibernate/src/main/resources/sample-schema.sql b/samples/java/hibernate/src/main/resources/sample-schema.sql
index d39ce909..c8f6833d 100644
--- a/samples/java/hibernate/src/main/resources/sample-schema.sql
+++ b/samples/java/hibernate/src/main/resources/sample-schema.sql
@@ -57,4 +57,23 @@ create table if not exists concerts (
     constraint chk_end_time_after_start_time check (end_time > start_time)
 );
 
+create table ticket_sales (
+    id bigint not null primary key,
+    concert_id       varchar not null,
+    customer_name    varchar not null,
+    price            decimal not null,
+    seats            text[],
+    created_at       timestamptz,
+    updated_at       timestamptz,
+    constraint fk_ticket_sales_concerts foreign key (concert_id) references concerts (id)
+);
+
+-- Create a bit-reversed sequence that will be used to generate identifiers for the ticket_sales table.
+-- See also https://cloud.google.com/spanner/docs/reference/postgresql/data-definition-language#create_sequence
+create sequence ticket_sale_seq
+    bit_reversed_positive
+    skip range 1 1000
+    start counter with 50000
+;
+
 run batch;
\ No newline at end of file
diff --git a/samples/java/jdbc/pom.xml b/samples/java/jdbc/pom.xml
index b2250fa3..238ff63f 100644
--- a/samples/java/jdbc/pom.xml
+++ b/samples/java/jdbc/pom.xml
@@ -39,7 +39,7 @@
       <dependency>
         <groupId>org.testcontainers</groupId>
         <artifactId>testcontainers-bom</artifactId>
-        <version>1.19.5</version>
+        <version>1.19.6</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -51,12 +51,12 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.1</version>
+      <version>42.7.2</version>
     </dependency>
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-spanner-pgadapter</artifactId>
-      <version>0.29.1</version>
+      <version>0.31.0</version>
     </dependency>
     <!-- [END pgadapter_and_jdbc_dependency] -->
     <dependency>
@@ -80,7 +80,7 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>exec-maven-plugin</artifactId>
-        <version>3.1.1</version>
+        <version>3.2.0</version>
         <configuration>
           <mainClass>com.google.cloud.spanner.pgadapter.sample.SampleApplication</mainClass>
           <cleanupDaemonThreads>false</cleanupDaemonThreads>
diff --git a/samples/java/liquibase/pom.xml b/samples/java/liquibase/pom.xml
index 9222bede..4716746a 100644
--- a/samples/java/liquibase/pom.xml
+++ b/samples/java/liquibase/pom.xml
@@ -71,7 +71,7 @@
             <dependency>
               <groupId>org.postgresql</groupId>
               <artifactId>postgresql</artifactId>
-              <version>42.7.1</version>
+              <version>42.7.2</version>
             </dependency>
           </dependencies>
         </plugin>
diff --git a/samples/java/spring-data-jpa/pom.xml b/samples/java/spring-data-jpa/pom.xml
index 42117907..0ae46025 100644
--- a/samples/java/spring-data-jpa/pom.xml
+++ b/samples/java/spring-data-jpa/pom.xml
@@ -31,7 +31,7 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.1</version>
+      <version>42.7.2</version>
     </dependency>
     <!-- Add Jackson for json parsing. This is required for using JSONB columns with Spring Data JPA. -->
     <dependency>
@@ -42,7 +42,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-spanner-pgadapter</artifactId>
-      <version>0.29.1</version>
+      <version>0.31.0</version>
     </dependency>
     <!-- Add Spanner Hibernate tools for access to the batch compatible bit-reversed sequence generator. -->
     <dependency>
@@ -54,7 +54,7 @@
     <dependency>
       <groupId>io.hypersistence</groupId>
       <artifactId>hypersistence-utils-hibernate-63</artifactId>
-      <version>3.7.2</version>
+      <version>3.7.3</version>
     </dependency>
     <dependency>
       <groupId>org.springframework.boot</groupId>
diff --git a/samples/nodejs/cloud-run/.dockerignore b/samples/nodejs/cloud-run/.dockerignore
deleted file mode 100644
index ee269b8b..00000000
--- a/samples/nodejs/cloud-run/.dockerignore
+++ /dev/null
@@ -1,3 +0,0 @@
-Dockerfile
-node_modules
-npm-debug.log
diff --git a/samples/nodejs/cloud-run/.gcloudignore b/samples/nodejs/cloud-run/.gcloudignore
deleted file mode 100644
index 93f13619..00000000
--- a/samples/nodejs/cloud-run/.gcloudignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules
-npm-debug.log
diff --git a/samples/nodejs/cloud-run/Dockerfile b/samples/nodejs/cloud-run/Dockerfile
deleted file mode 100644
index b6ac6e96..00000000
--- a/samples/nodejs/cloud-run/Dockerfile
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright 2023 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     https://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.
-
-# Start with a Docker image that includes a JRE. This is needed for PGAdapter.
-FROM eclipse-temurin:17-jre AS jre
-
-# Use the official lightweight Node.js image.
-FROM node:18-slim
-
-# Copy the JRE into the Node.js image.
-ENV JAVA_HOME=/opt/java/openjdk
-COPY --from=jre $JAVA_HOME $JAVA_HOME
-ENV PATH="${JAVA_HOME}/bin:${PATH}"
-
-# Create and change to the app directory.
-WORKDIR /usr/src/app
-
-# Copy application dependency manifests to the container image.
-COPY package*.json ./
-
-# Install production dependencies.
-RUN npm install --only=production
-
-# Copy local code to the container image.
-COPY . ./
-
-# Add the latest version of PGAdapter to the container.
-ADD https://storage.googleapis.com/pgadapter-jar-releases/pgadapter.tar.gz /pgadapter.tar.gz
-RUN mkdir /pgadapter
-RUN tar -xzf /pgadapter.tar.gz -C /pgadapter
-
-# Copy the startup script that will start both PGAdapter and the web service.
-COPY ./startup.sh /startup.sh
-RUN chmod +x /startup.sh
-
-ENTRYPOINT ["/bin/bash", "/startup.sh"]
diff --git a/samples/nodejs/cloud-run/README.md b/samples/nodejs/cloud-run/README.md
index c914cec5..8fe7df99 100644
--- a/samples/nodejs/cloud-run/README.md
+++ b/samples/nodejs/cloud-run/README.md
@@ -1,77 +1,4 @@
 # PGAdapter Cloud Run Sample for Node.js
 
-This sample application shows how to build and deploy a Node.js application with PGAdapter to Google Cloud Run.
-
-## Build
-
-First build the Docker image locally and tag it. Replace the `IMAGE_URL` in the script with your own repository.
-Make sure that the repository that you are referencing exists.
-See https://cloud.google.com/artifact-registry/docs/repositories/create-repos for more information on how to set up
-Google Cloud artifact repositories.
-
-```shell
-export IMAGE_URL=my-location-docker.pkg.dev/my-project/my-repository/my-image:latest
-docker build . --tag $IMAGE_URL
-```
-
-Example: `export IMAGE_URL=europe-north1-docker.pkg.dev/my-project/sample-test/pgadapter-sample:latest`
-
-Test the Docker image locally:
-
-```shell
-docker run \
-  -p 8080:8080 \
-  -v /path/to/local_credentials.json:/credentials.json:ro \
-  --env GOOGLE_APPLICATION_CREDENTIALS=/credentials.json \
-  --env SPANNER_PROJECT=my-project \
-  --env SPANNER_INSTANCE=my-instance \
-  --env SPANNER_DATABASE=my-database \
-  $IMAGE_URL
-```
-
-Verify that the connection to Cloud Spanner works correctly by opening a new shell and executing:
-
-```shell
-curl http://localhost:8080
-```
-
-## Deploying to Cloud Run
-
-Push the Docker image to Artifact Registry:
-
-```shell
-gcloud auth configure-docker
-docker push $IMAGE_URL
-```
-
-Then deploy the image as a service on Cloud Run:
-
-```shell
-export SERVICE=my-service
-gcloud run deploy $SERVICE \
-  --image $IMAGE_URL \
-  --update-env-vars SPANNER_PROJECT=my-project,SPANNER_INSTANCE=my-instance,SPANNER_DATABASE=my-database
-```
-
-__NOTE__: This example does not specify any credentials for PGAdapter when it is run on Cloud Run. This means that
-PGAdapter will use the default credentials that is used by Cloud Run. This is by default the default compute engine
-service account. See https://cloud.google.com/run/docs/securing/service-identity for more information on how service
-accounts work on Google Cloud Run.
-
-Test the service (replace URL with your actual service URL):
-
-```shell
-curl https://my-service-xyz.run.app
-```
-
-### Authenticated Cloud Run Service
-
-If your Cloud Run service requires authentication, then first add an IAM binding for your own account and include
-an authentication header with the request:
-
-```shell
-gcloud run services add-iam-policy-binding $SERVICE \
-  --member='user:your-email@gmail.com' \
-  --role='roles/run.invoker'
-curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://my-service-xyz.run.app
-```
+[The recommended setup for deploying an application written in Node.js with PGAdapter on Cloud Run is to
+deploy PGAdapter as a side-car proxy.](../../cloud-run/nodejs)
diff --git a/samples/nodejs/cloud-run/index.js b/samples/nodejs/cloud-run/index.js
deleted file mode 100644
index 90f8cc8b..00000000
--- a/samples/nodejs/cloud-run/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const express = require('express');
-const { Client } = require('pg');
-const app = express();
-const client = new Client({
-  host: 'localhost',
-  port: 5432,
-  database: `projects/${process.env.SPANNER_PROJECT}/instances/${process.env.SPANNER_INSTANCE}/databases/${process.env.SPANNER_DATABASE}`,
-});
-const port = parseInt(process.env.PORT) || 8080;
-
-client.connect().then(() => {
-  app.listen(port, () => {
-    console.log(`helloworld: listening on port ${port}`);
-  });
-});
-
-app.get('/', async (req, res) => {
-  // Execute a query on Cloud Spanner that returns a greeting and return that to the user.
-  const greeting = await client.query("select 'Hello world! from Cloud Spanner PostgreSQL' as hello");
-  res.send(greeting.rows[0].hello + '\n');
-});
diff --git a/samples/nodejs/cloud-run/package.json b/samples/nodejs/cloud-run/package.json
deleted file mode 100644
index 26c167a6..00000000
--- a/samples/nodejs/cloud-run/package.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "name": "helloworld",
-  "description": "PGAdapter Hello world sample in Node",
-  "version": "1.0.0",
-  "private": true,
-  "main": "index.js",
-  "scripts": {
-    "start": "node index.js"
-  },
-  "engines": {
-    "node": ">=12.0.0"
-  },
-  "author": "Google LLC",
-  "license": "Apache-2.0",
-  "dependencies": {
-    "express": "^4.17.1",
-    "pg": "^8.11.0"
-  }
-}
diff --git a/samples/nodejs/cloud-run/startup.sh b/samples/nodejs/cloud-run/startup.sh
deleted file mode 100644
index 9fdcb644..00000000
--- a/samples/nodejs/cloud-run/startup.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-
-# Run both PGAdapter and the web server when the Docker container is started.
-# Start PGAdapter in plain TCP only mode, that is:
-# 1. Do not specify any credentials. This will make sure PGAdapter picks the credentials from the current environment.
-#    You must ensure that the service account that is used by Cloud Run is allowed to access the Cloud Spanner database
-#    that your application is using.
-# 2. Do not specify any specific project, instance or database. This will require the application to specify a fully
-#    qualified database name when connecting to PGAdapter.
-# 3. Set the Unix domain socket directory to the empty string to disable domain sockets. Domain sockets are not
-#    supported on Cloud Run.
-
-# Start PGAdapter in the background.
-java -jar /pgadapter/pgadapter.jar -dir= &
-# Give PGAdapter a second to start up.
-sleep 1
-
-# Run the web service on container startup.
-node index.js
diff --git a/samples/python/cloud-run/.dockerignore b/samples/python/cloud-run/.dockerignore
deleted file mode 100644
index 3e4bdd9f..00000000
--- a/samples/python/cloud-run/.dockerignore
+++ /dev/null
@@ -1,7 +0,0 @@
-Dockerfile
-README.md
-*.pyc
-*.pyo
-*.pyd
-__pycache__
-.pytest_cache
diff --git a/samples/python/cloud-run/Dockerfile b/samples/python/cloud-run/Dockerfile
deleted file mode 100644
index bca7fee6..00000000
--- a/samples/python/cloud-run/Dockerfile
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright 2023 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     https://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.
-
-# Start with a Docker image that includes a JRE. This is needed for PGAdapter.
-FROM eclipse-temurin:17-jre AS jre
-
-# Use the official lightweight Python image.
-FROM python:3.11-slim
-
-# Copy the JRE into the Pyton image.
-ENV JAVA_HOME=/opt/java/openjdk
-COPY --from=jre $JAVA_HOME $JAVA_HOME
-ENV PATH="${JAVA_HOME}/bin:${PATH}"
-
-# Allow statements and log messages to immediately appear in the logs
-ENV PYTHONUNBUFFERED True
-
-# Copy local code to the container image.
-ENV APP_HOME /app
-WORKDIR $APP_HOME
-COPY . ./
-
-# Install production dependencies.
-RUN pip install --no-cache-dir -r requirements.txt
-
-# Add the latest version of PGAdapter to the container.
-ADD https://storage.googleapis.com/pgadapter-jar-releases/pgadapter.tar.gz /pgadapter.tar.gz
-RUN mkdir /pgadapter
-RUN tar -xzf /pgadapter.tar.gz -C /pgadapter
-
-# Copy the startup script that will start both PGAdapter and the web service.
-COPY ./startup.sh /startup.sh
-RUN chmod +x /startup.sh
-
-ENTRYPOINT ["/bin/bash", "/startup.sh"]
diff --git a/samples/python/cloud-run/README.md b/samples/python/cloud-run/README.md
index 7f3f0241..65f39b25 100644
--- a/samples/python/cloud-run/README.md
+++ b/samples/python/cloud-run/README.md
@@ -1,78 +1,4 @@
 # PGAdapter Cloud Run Sample for Python
 
-This sample application shows how to build and deploy a Python application with PGAdapter to Google Cloud Run.
-
-## Build
-
-First build the Docker image locally and tag it. Replace the `IMAGE_URL` in the script with your own repository.
-Make sure that the repository that you are referencing exists.
-See https://cloud.google.com/artifact-registry/docs/repositories/create-repos for more information on how to set up
-Google Cloud artifact repositories.
-
-```shell
-export IMAGE_URL=my-location-docker.pkg.dev/my-project/my-repository/my-image:latest
-docker build . --tag $IMAGE_URL
-```
-
-Example: `export IMAGE_URL=europe-north1-docker.pkg.dev/my-project/sample-test/pgadapter-sample:latest`
-
-Test the Docker image locally:
-
-```shell
-docker run \
-  -p 8080:8080 \
-  -v /path/to/local_credentials.json:/credentials.json:ro \
-  --env GOOGLE_APPLICATION_CREDENTIALS=/credentials.json \
-  --env SPANNER_PROJECT=my-project \
-  --env SPANNER_INSTANCE=my-instance \
-  --env SPANNER_DATABASE=my-database \
-  $IMAGE_URL
-```
-
-Verify that the connection to Cloud Spanner works correctly by opening a new shell and executing:
-
-```shell
-curl http://localhost:8080
-```
-
-## Deploying to Cloud Run
-
-Push the Docker image to Artifact Registry:
-
-```shell
-gcloud auth configure-docker
-docker push $IMAGE_URL
-```
-
-Then deploy the image as a service on Cloud Run:
-
-```shell
-export SERVICE=my-service
-gcloud run deploy $SERVICE \
-  --image $IMAGE_URL \
-  --update-env-vars SPANNER_PROJECT=my-project,SPANNER_INSTANCE=my-instance,SPANNER_DATABASE=my-database
-```
-
-__NOTE__: This example does not specify any credentials for PGAdapter when it is run on Cloud Run. This means that
-PGAdapter will use the default credentials that is used by Cloud Run. This is by default the default compute engine
-service account. See https://cloud.google.com/run/docs/securing/service-identity for more information on how service
-accounts work on Google Cloud Run.
-
-Test the service (replace URL with your actual service URL):
-
-```shell
-curl https://my-service-xyz.run.app
-```
-
-### Authenticated Cloud Run Service
-
-If your Cloud Run service requires authentication, then first add an IAM binding for your own account and include
-an authentication header with the request:
-
-```shell
-gcloud run services add-iam-policy-binding $SERVICE \
-  --member='user:your-email@gmail.com' \
-  --role='roles/run.invoker'
-curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://my-service-xyz.run.app
-```
-
+[The recommended setup for deploying an application written in Python with PGAdapter on Cloud Run is to
+deploy PGAdapter as a side-car proxy.](../../cloud-run/python)
diff --git a/samples/python/cloud-run/main.py b/samples/python/cloud-run/main.py
deleted file mode 100644
index 0036525d..00000000
--- a/samples/python/cloud-run/main.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import os
-import psycopg
-from flask import Flask
-
-project = os.environ['SPANNER_PROJECT']
-instance = os.environ['SPANNER_INSTANCE']
-database = os.environ['SPANNER_DATABASE']
-app = Flask(__name__)
-
-
-@app.route("/")
-def hello_world():
-  # Connect to Cloud Spanner using psycopg3.
-  # Note that we use the fully qualified database name to connect to the
-  # database, as the Dockerfile startup script starts PGAdapter without a
-  # default project or instance.
-  with psycopg.connect("host=localhost port={port} "
-                       "dbname=projects/{project}/instances/{instance}/databases/{database} "
-                       "sslmode=disable"
-                           .format(port=5432,
-                                   project=project,
-                                   instance=instance,
-                                   database=database)) as conn:
-    conn.autocommit = True
-    with conn.cursor() as cur:
-      cur.execute("select 'Hello world!' as hello")
-      return "Greeting from Cloud Spanner PostgreSQL: {greeting}\n"\
-        .format(greeting=cur.fetchone()[0])
-
-
-if __name__ == "__main__":
-  app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
diff --git a/samples/python/cloud-run/requirements.txt b/samples/python/cloud-run/requirements.txt
deleted file mode 100644
index 481e8121..00000000
--- a/samples/python/cloud-run/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-psycopg[binary]~=3.1.9
-Flask==2.2.5
-gunicorn==20.1.0
diff --git a/samples/python/cloud-run/startup.sh b/samples/python/cloud-run/startup.sh
deleted file mode 100644
index 1c3da55a..00000000
--- a/samples/python/cloud-run/startup.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-
-# Run both PGAdapter and the web server when the Docker container is started.
-# Start PGAdapter in plain TCP only mode, that is:
-# 1. Do not specify any credentials. This will make sure PGAdapter picks the credentials from the current environment.
-#    You must ensure that the service account that is used by Cloud Run is allowed to access the Cloud Spanner database
-#    that your application is using.
-# 2. Do not specify any specific project, instance or database. This will require the application to specify a fully
-#    qualified database name when connecting to PGAdapter.
-# 3. Set the Unix domain socket directory to the empty string to disable domain sockets. Domain sockets are not
-#    supported on Cloud Run.
-
-# Start PGAdapter in the background.
-java -jar /pgadapter/pgadapter.jar -dir= &
-
-# Run the web service on container startup. Here we use the gunicorn
-# webserver, with one worker process and 8 threads.
-# For environments with multiple CPU cores, increase the number of workers
-# to be equal to the cores available.
-# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
-port="${PORT:-8080}"
-exec gunicorn --bind ":$port" --workers 1 --threads 8 --timeout 0 main:app
diff --git a/samples/python/psycopg3/requirements.txt b/samples/python/psycopg3/requirements.txt
index 824e52f1..7034f464 100644
--- a/samples/python/psycopg3/requirements.txt
+++ b/samples/python/psycopg3/requirements.txt
@@ -1,4 +1,4 @@
 psycopg~=3.1.18
 testcontainers~=3.7.1
 google~=3.0.0
-google.auth~=2.27.0
\ No newline at end of file
+google.auth~=2.28.1
\ No newline at end of file
diff --git a/samples/python/sqlalchemy2-sample/README.md b/samples/python/sqlalchemy2-sample/README.md
index 56233fbc..1ff70a02 100644
--- a/samples/python/sqlalchemy2-sample/README.md
+++ b/samples/python/sqlalchemy2-sample/README.md
@@ -96,7 +96,7 @@ Cloud Spanner supports the following data types in combination with `SQLAlchemy
 | date                                   | Date                    |
 | bytea                                  | LargeBinary             |
 | jsonb                                  | JSONB                   |
-
+| Arrays                                 | Column()                |
 
 ## Limitations
 The following limitations are currently known:
@@ -106,7 +106,6 @@ The following limitations are currently known:
 | Creating and Dropping Tables   | Cloud Spanner does not support the full PostgreSQL DDL dialect. Automated creation of tables using `SQLAlchemy` is therefore not supported.                                                                                                                         |
 | metadata.reflect()             | Cloud Spanner does not support all PostgreSQL `pg_catalog` tables. Using `metadata.reflect()` to get the current objects in the database is therefore not supported.                                                                                                |
 | DDL Transactions               | Cloud Spanner does not support DDL statements in a transaction. Add `?options=-c spanner.ddl_transaction_mode=AutocommitExplicitTransaction` to your connection string to automatically convert DDL transactions to [non-atomic DDL batches](../../../docs/ddl.md). |
-| Generated primary keys         | Manually assign a value to the primary key column in your code. The recommended primary key type is a random UUID. Sequences / SERIAL / IDENTITY columns are currently not supported.                                                                               |
 | INSERT ... ON CONFLICT         | `INSERT ... ON CONFLICT` is not supported.                                                                                                                                                                                                                          |
 | SAVEPOINT                      | Rolling back to a `SAVEPOINT` can fail if the transaction contained at least one query that called a volatile function.                                                                                                                                             |
 | SELECT ... FOR UPDATE          | `SELECT ... FOR UPDATE` is not supported.                                                                                                                                                                                                                           |
@@ -117,21 +116,38 @@ The following limitations are currently known:
 | Other drivers than psycopg 3.x | PGAdapter does not support using SQLAlchemy 2.x with any other drivers than `psycopg 3.x`.                                                                                                                                                                          |
 
 ### Generated Primary Keys
-Generated primary keys are not supported and should be replaced with primary key definitions that
-are manually assigned. See https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots
-for more information on choosing a good primary key. This sample uses random UUIDs that are generated
-by the client and stored as strings for primary keys.
+Generated primary keys can be used in combination with a bit-reversed sequence.
 
-```python
-from uuid import uuid4
+The `TicketSale` model in this sample application uses an auto-generated primary key that is
+generated from a bit-reversed sequence:
+1. See [model.py](model.py) for the model definition.
+2. See [create_data_model.sql](create_data_model.sql) for the sequence and table definition.
+
+See https://cloud.google.com/spanner/docs/primary-key-default-value#bit-reversed-sequence for more
+information on bit-reversed sequences in Cloud Spanner.
+
+#### Example Mapping for Generated Primary Key using a Bit-Reversed Sequence
+
+Python model definition:
 
+```python
 class Singer(Base):
-  id = Column(String, primary_key=True)
+  id = Column(Integer, primary_key=True)
   name = Column(String(100))
 
-singer = Singer(
-  id="{}".format(uuid4()),
-  name="Alice")
+singer = Singer(name="Alice")
+session.add(singer)
+session.commit()
+```
+
+Sequence and table definition:
+
+```sql
+create sequence if not exists singers_seq bit_reversed_positive;
+create table if not exists singers (
+  id   bigint not null primary key default nextval('singers_seq'),
+  name varchar not null
+);
 ```
 
 ### ON CONFLICT Clauses
diff --git a/samples/python/sqlalchemy2-sample/create_data_model.sql b/samples/python/sqlalchemy2-sample/create_data_model.sql
index c8e173b3..7ca401e8 100644
--- a/samples/python/sqlalchemy2-sample/create_data_model.sql
+++ b/samples/python/sqlalchemy2-sample/create_data_model.sql
@@ -61,4 +61,25 @@ create table if not exists concerts (
     constraint chk_end_time_after_start_time check (end_time > start_time)
 );
 
+-- Create a bit-reversed sequence that will be used to generate identifiers for the ticket_sales table.
+-- See also https://cloud.google.com/spanner/docs/reference/postgresql/data-definition-language#create_sequence
+-- Note that the 'bit_reversed_positive' keyword is required for Spanner.
+create sequence if not exists ticket_sale_seq
+    bit_reversed_positive
+    skip range 1 1000
+    start counter with 50000
+;
+
+create table if not exists ticket_sales (
+    id bigint not null primary key default nextval('ticket_sale_seq'),
+    version_id       int not null,
+    concert_id       varchar not null,
+    customer_name    varchar not null,
+    price            decimal not null,
+    seats            text[],
+    created_at       timestamptz,
+    updated_at       timestamptz,
+    constraint fk_ticket_sales_concerts foreign key (concert_id) references concerts (id)
+);
+
 run batch;
diff --git a/samples/python/sqlalchemy2-sample/drop_data_model.sql b/samples/python/sqlalchemy2-sample/drop_data_model.sql
index b57ea2ad..bbc6feea 100644
--- a/samples/python/sqlalchemy2-sample/drop_data_model.sql
+++ b/samples/python/sqlalchemy2-sample/drop_data_model.sql
@@ -1,6 +1,8 @@
 -- Executing the schema drop in a batch will improve execution speed.
 start batch ddl;
 
+drop table if exists ticket_sales;
+drop sequence if exists ticket_sale_seq;
 drop table if exists concerts;
 drop table if exists venues;
 drop table if exists tracks;
diff --git a/samples/python/sqlalchemy2-sample/model.py b/samples/python/sqlalchemy2-sample/model.py
index b920cac0..f49c490f 100644
--- a/samples/python/sqlalchemy2-sample/model.py
+++ b/samples/python/sqlalchemy2-sample/model.py
@@ -16,30 +16,26 @@
 from sqlalchemy import Column, Integer, String, Boolean, LargeBinary, Float, \
   Numeric, DateTime, Date, FetchedValue, ForeignKey, ColumnDefault
 from sqlalchemy.orm import registry, relationship
-from sqlalchemy.dialects.postgresql import JSONB
+from sqlalchemy.dialects.postgresql import JSONB, ARRAY
 from datetime import timezone, datetime
 
 mapper_registry = registry()
 Base = mapper_registry.generate_base()
 
 
-"""
- BaseMixin contains properties that are common to all models in this sample. All
- models use a string column that contains a client-side generated UUID as the
- primary key.
- The created_at and updated_at properties are automatically filled with the
- current client system time when a model is created or updated. 
-"""
-
-
 def format_timestamp(timestamp: datetime) -> str:
   return timestamp.astimezone(timezone.utc).isoformat() if timestamp else None
 
 
+"""
+ BaseMixin contains properties that are common to all models in this sample.
+ 
+ The created_at and updated_at properties are automatically filled with the
+ current client system time when a model is created or updated. 
+"""
 class BaseMixin(object):
   __prepare_statements__ = None
 
-  id = Column(String, primary_key=True)
   version_id = Column(Integer, nullable=False)
   created_at = Column(DateTime(timezone=True),
                       # We need to explicitly format the timestamps with a
@@ -53,7 +49,24 @@ class BaseMixin(object):
   __mapper_args__ = {"version_id_col": version_id}
 
 
-class Singer(BaseMixin, Base):
+"""
+ BaseUuidMixin is the base class for all tables that use a client-side
+ generated UUID as the primary key.
+""" 
+class BaseUuidMixin(BaseMixin):
+  id = Column(String, primary_key=True)
+
+
+"""
+ BaseInt64Mixin is the base class for all tables that use an auto-generated
+ INT64 as the primary key. The value is generated by a bit-reversed sequence
+ in the database.
+"""
+class BaseInt64Mixin(BaseMixin):
+  id = Column(Integer, primary_key=True)
+
+
+class Singer(BaseUuidMixin, Base):
   __tablename__ = "singers"
 
   first_name = Column(String(100))
@@ -82,7 +95,7 @@ class Singer(BaseMixin, Base):
     )
 
 
-class Album(BaseMixin, Base):
+class Album(BaseUuidMixin, Base):
   __tablename__ = "albums"
 
   title = Column(String(200))
@@ -112,7 +125,7 @@ class Album(BaseMixin, Base):
     )
 
 
-class Track(BaseMixin, Base):
+class Track(BaseUuidMixin, Base):
   __tablename__ = "tracks"
 
   id = Column(String, ForeignKey("albums.id"), primary_key=True)
@@ -134,7 +147,7 @@ class Track(BaseMixin, Base):
     )
 
 
-class Venue(BaseMixin, Base):
+class Venue(BaseUuidMixin, Base):
   __tablename__ = "venues"
 
   name = Column(String(200))
@@ -152,7 +165,7 @@ class Venue(BaseMixin, Base):
     )
 
 
-class Concert(BaseMixin, Base):
+class Concert(BaseUuidMixin, Base):
   __tablename__ = "concerts"
 
   name = Column(String(200))
@@ -162,6 +175,7 @@ class Concert(BaseMixin, Base):
   singer = relationship("Singer")
   start_time = Column(DateTime(timezone=True))
   end_time = Column(DateTime(timezone=True))
+  ticket_sales = relationship("TicketSale", back_populates="concert")
 
   def __repr__(self):
     return (
@@ -176,3 +190,33 @@ class Concert(BaseMixin, Base):
       f"updated_at={format_timestamp(self.updated_at)!r}"
       f")"
     )
+
+
+class TicketSale(BaseInt64Mixin, Base):
+  __tablename__ = "ticket_sales"
+
+  concert_id = Column(String, ForeignKey("concerts.id"))
+  concert = relationship("Concert")
+  customer_name = Column(String)
+  price = Column(Numeric)
+  # "seats" has type text[] in the database. By not specifying any type here,
+  # we ensure that SQLAlchemy just sends an untyped string value to the
+  # database. If we were to configure the type to be ARRAY(String) here, then
+  # we would get an error 'Array casting / coercion is not supported', as
+  # SQLAlchemy would try to cast the value to a VARCHAR[].
+  # TODO: Use this mapping when array casting/coercion is supported.
+  #       seats = Column(ARRAY(String))
+  seats = Column()
+
+  def __repr__(self):
+    return (
+      f"ticket_sales("
+      f"id={self.id!r},"
+      f"concert={self.concert!r},"
+      f"customer_name={self.customer_name!r},"
+      f"price={self.price!r},"
+      f"seats={self.seats!r},"
+      f"created_at={format_timestamp(self.created_at)!r},"
+      f"updated_at={format_timestamp(self.updated_at)!r}"
+      f")"
+    )
diff --git a/samples/python/sqlalchemy2-sample/sample.py b/samples/python/sqlalchemy2-sample/sample.py
index 249f3e0b..1685eb40 100644
--- a/samples/python/sqlalchemy2-sample/sample.py
+++ b/samples/python/sqlalchemy2-sample/sample.py
@@ -12,11 +12,12 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 """
+from random import random
 
 from connect import create_test_engine
 from sqlalchemy.orm import Session, joinedload
 from sqlalchemy import text, func, or_, Integer
-from model import Singer, Album, Track, Venue, Concert
+from model import Singer, Album, Track, Venue, Concert, TicketSale
 from util_random_names import random_first_name, random_last_name, \
   random_album_title, random_release_date, random_marketing_budget, \
   random_cover_picture
@@ -143,7 +144,15 @@ def create_venue_and_concert_in_transaction():
       start_time=datetime.fromisoformat("2023-02-01T20:00:00-05:00"),
       end_time=datetime.fromisoformat("2023-02-02T02:00:00-05:00"),
     )
-    session.add_all([venue, concert])
+    # Create a TicketSale. TicketSale uses an auto-generated primary
+    # key, so we do not need to assign it a primary key in code.
+    ticket_sale = TicketSale(
+        concert=concert,
+        customer_name=random_first_name() + " " + random_last_name(),
+        price=random() * 1000,
+        seats=["A19", "A20", "A21"],
+    )
+    session.add_all([venue, concert, ticket_sale])
     session.commit()
     print()
     print("Created Venue and Concert")
@@ -169,6 +178,9 @@ def print_concerts():
                     concert.start_time.astimezone(timezone.utc).isoformat(),
                     concert.singer.full_name,
                     concert.venue.name))
+      for ticket_sale in concert.ticket_sales:
+        print("  Ticket sold to {} for seats {}"
+              .format(ticket_sale.customer_name, ticket_sale.seats))
 
 
 # Prints all albums with a release date before 1980-01-01.
diff --git a/samples/ruby/activerecord/Gemfile b/samples/ruby/activerecord/Gemfile
index 3dd8d918..aac9363c 100644
--- a/samples/ruby/activerecord/Gemfile
+++ b/samples/ruby/activerecord/Gemfile
@@ -2,7 +2,7 @@
 
 source 'https://rubygems.org'
 
-gem 'docker-api'
+gem "testcontainers"
 gem 'pg'
 gem 'rake'
 gem 'activerecord', '~> 7.0'
diff --git a/samples/ruby/activerecord/README.md b/samples/ruby/activerecord/README.md
index ab5a4534..e544f089 100644
--- a/samples/ruby/activerecord/README.md
+++ b/samples/ruby/activerecord/README.md
@@ -12,30 +12,41 @@ to use `ActiveRecord` in general.
 See [Limitations](#limitations) for a full list of known limitations when working with `ActiveRecord`.
 
 ## Running the Sample
-The sample application automatically starts PGAdapter in a Docker container. You must have Docker
-installed on your local system to run the sample. Furthermore, you must configure the following.
+The sample application automatically starts PGAdapter and the Spanner emulator in a Docker
+container. You must have Docker installed on your local system to run the sample.
+
+```shell
+bundle exec rake run
+```
+
+The database is automatically created on the emulator when the application is started. This is
+handled by the `gcr.io/cloud-spanner-pg-adapter/pgadapter-emulator` Docker image. See the
+[PGAdapter emulator documentation](../../../docs/emulator.md) for more information about connecting
+PGAdapter to the emulator.
+
+## Running the Sample on real Cloud Spanner
+You can also run the sample on a real Cloud Spanner PostgreSQL database instead of the emulator.
+The sample application will automatically start PGAdapter in a Docker container and connect to your
+Cloud Spanner database. For this, you need to follow these configuration steps
 
 1. Set the environment variable `GOOGLE_APPLICATION_CREDENTIALS` to point to a valid (service) account file
    that should be used to run the sample.
 2. Modify the `config/database.yml` file so it points to your database.
-3. If you already have PostgreSQL running on your local host on port 5432: Modify the port number in the
-   `database.yml` to for example 5433. This port number will be used to start PGAdapter.
 
 Alternatively, you can also set the following environment variables to configure the database and port number:
 
 ```shell
 GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
-PGPORT=5432
 PGDATABASE=projects/my-project/instances/my-instance/databases/my-database
 ```
 
 Make sure the database that you point the sample to is empty, as the sample application will
 automatically create the required data model in that database.
 
-Run the sample with the following command:
+Run the sample on Cloud Spanner with the following command:
 
 ```shell
-bundle exec rake run
+bundle exec rake run_prod
 ```
 
 
@@ -57,6 +68,8 @@ modifiers for `timestamptz` data types (e.g. `timestamptz(6)`).
 This initialization code is only required for executing migrations. You may omit this from your
 initialization code when you are not running migrations.
 
+This sample application has this initialization code in the `config/environment.rb` file.
+
 ```ruby
 # Make sure that the PostgreSQL-adapter uses timestamptz without any type modifiers.
 ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz
@@ -87,9 +100,9 @@ end
 
 ### database.yml
 The following is the `database.yml` that is used by this sample. It uses the standard
-`PGHOST`, `PGPORT` and `PGDATABASE` environment variables. Note that PGAdapter will
-automatically be started on the value that is set in `PGPORT`. Ensure that you set this
-environment variable to an available port on your system.
+`PGHOST`, `PGPORT` and `PGDATABASE` environment variables. Note that the sample application
+starts PGAdapter automatically on a random port, and the `PGPORT` environment variable is
+automatically updated to use this port.
 
 ```yaml
 default: &default
@@ -123,7 +136,7 @@ The following limitations are currently known:
 |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | Schema Dumper                  | Cloud Spanner does not support all PostgreSQL `pg_catalog` tables. Using `ActiveRecord::SchemaDumper` to get export the current schema is not guaranteed to include all objects in the database.                                                                                                                                                         |
 | DDL Transactions               | Cloud Spanner does not support DDL statements in a transaction. Add `"spanner.ddl_transaction_mode": "AutocommitExplicitTransaction"` to the `variables` section in your `database.yml` file to automatically convert DDL transactions to [non-atomic DDL batches](../../../docs/ddl.md). See [config/database.yml](config/database.yml) for an example. |
-| Generated primary keys         | Manually assign a value to the primary key column in your code. The recommended primary key type is a random UUID. Sequences / SERIAL / IDENTITY columns are currently not supported.                                                                                                                                                                    |
+| Generated primary keys         | The `serial` data type is not supported. Use bit-reversed sequences to auto-generate primary keys.                                                                                                                                                                                                                                                       |
 | Upsert                         | `upsert` and `upsert_all` are not supported.                                                                                                                                                                                                                                                                                                             |
 | SELECT ... FOR UPDATE          | Pessimistic locking / `SELECT ... FOR UPDATE` is not supported.                                                                                                                                                                                                                                                                                          |
 | Transaction isolation level    | Only isolation level `:serializable` is supported.                                                                                                                                                                                                                                                                                                       |
@@ -133,10 +146,36 @@ Dumping the schema of a database is not guaranteed to produce a complete result.
 no workaround for this limitation.
 
 ### Generated Primary Keys
-Generated primary keys are not supported and should be replaced with primary key definitions that
-are manually assigned. See https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots
-for more information on choosing a good primary key. This sample uses random UUIDs that are generated
-by the client and stored as strings for primary keys.
+The `serial` data type is currently not supported.
+Generated primary keys can however be used in combination with bit-reversed sequences.
+
+The [TicketSale](models/ticket_sale.rb) model in this sample uses an auto-generated primary key.
+The [table definition](db/migrate/01_create_tables.rb) for this model looks like this:
+
+```ruby
+execute "create sequence ticket_sale_seq
+         bit_reversed_positive
+         skip range 1 1000
+         start counter with 50000;"
+
+create_table :ticket_sales, id: false, primary_key: :id do |t|
+   t.column :id, "bigint not null primary key default nextval('ticket_sale_seq')"
+   t.references :concert, foreign_key: {primary_key: :concert_id},
+                type: :string, limit: 36, index: false
+   t.string :customer_name
+   t.numeric :price
+   t.string :seats, array: true
+   t.datetime :created_at
+   t.datetime :updated_at
+   t.integer :lock_version, null: false
+end
+```
+
+See https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots
+for more information on choosing a good primary key. Most models in this sample use
+random UUIDs that are generated by the client and stored as strings for primary keys.
+
+Example:
 
 ```ruby
 Singer.create({singer_id: SecureRandom.uuid,
diff --git a/samples/ruby/activerecord/Rakefile b/samples/ruby/activerecord/Rakefile
index e3d84c3c..65618a00 100644
--- a/samples/ruby/activerecord/Rakefile
+++ b/samples/ruby/activerecord/Rakefile
@@ -8,20 +8,35 @@
 
 require_relative 'config/environment'
 require 'sinatra/activerecord/rake'
-require "docker"
+require "testcontainers"
 
 Bundler.require
 
-desc 'Runs a simple ActiveRecord tutorial for Google Cloud Spanner PostgreSQL.'
+desc 'Runs a simple ActiveRecord tutorial for Google Cloud Spanner PostgreSQL on the Spanner emulator.'
 task :run do
+  puts "Starting PGAdapter..."
+  container = create_pgadapter_and_emulator_container
+  # Set the port where PGAdapter is running.
+  ENV["PGPORT"] = container.mapped_port(5432).to_s
+  begin
+    sh 'rake db:migrate'
+    sh 'rake db:seed'
+    sh 'ruby application.rb'
+  ensure
+    container.stop!
+    container.remove
+  end
+end
+
+desc 'Runs a simple ActiveRecord tutorial for Google Cloud Spanner PostgreSQL.'
+task :run_prod do
   credentials = ENV["GOOGLE_APPLICATION_CREDENTIALS"]
   fail "You must set the environment variable GOOGLE_APPLICATION_CREDENTIALS before running the sample." if credentials.nil?
 
   container = create_pgadapter_container(credentials)
+  # Set the port where PGAdapter is running.
+  ENV["PGPORT"] = container.mapped_port(5432).to_s
   begin
-    puts "Starting PGAdapter..."
-    container.start!
-    sleep(2)
     sh 'rake db:migrate'
     sh 'rake db:seed'
     sh 'ruby application.rb'
@@ -32,21 +47,39 @@ task :run do
 end
 
 def create_pgadapter_container(credentials)
-  puts "Downloading PGAdapter image..."
-  Docker::Image.create "fromImage" => "gcr.io/cloud-spanner-pg-adapter/pgadapter:ruby-active-record"
   puts "Creating PGAdapter container..."
-  Docker::Container.create(
-    "Image" => "gcr.io/cloud-spanner-pg-adapter/pgadapter:ruby-active-record",
-    "ExposedPorts" => { "5432/tcp" => {} },
-    "HostConfig" => {
-      "PortBindings" => {
-        "5432/tcp" => [{ "HostPort" => ENV["PGPORT"] || "5432" }]
-      },
-      "Binds" => ["#{credentials}:/credentials.json:ro"],
-    },
-    "Volumes" => { "/credentials.json" => {}},
-    # Start PGAdapter with the given credentials and no default project, instance or database.
-    # This means that the fully qualified database name must be used in the database.yml file.
-    "Cmd" => ["-c", "/credentials.json", "-x"]
-  )
+  container = Testcontainers::DockerContainer
+                .new(image="gcr.io/cloud-spanner-pg-adapter/pgadapter",
+                     image_create_options: {
+                       pull: :always,
+                     })
+                .with_exposed_ports(5432)
+                .with_filesystem_binds("#{credentials}:/credentials.json:ro")
+                .with_command("-c /credentials.json -x")
+  puts "Pulling PGAdapter container image and starting container..."
+  container.start
+  puts "Waiting for PGAdapter to be ready..."
+  container.wait_for_tcp_port(5432)
+  puts "PGAdapter running on port #{container.mapped_port(5432)}"
+  # Sleep 2 seconds to be absolutely sure that PGAdapter has started.
+  sleep(2)
+  container
+end
+
+def create_pgadapter_and_emulator_container
+  puts "Creating PGAdapter+Emulator container..."
+  container = Testcontainers::DockerContainer
+                .new(image="gcr.io/cloud-spanner-pg-adapter/pgadapter-emulator",
+                     image_create_options: {
+                       pull: :always,
+                     })
+                .with_exposed_ports(5432)
+  puts "Pulling PGAdapter+Emulator container image and starting container..."
+  container.start
+  puts "Waiting for PGAdapter to be ready..."
+  container.wait_for_tcp_port(5432)
+  puts "PGAdapter running on port #{container.mapped_port(5432)}"
+  # Sleep 2 seconds to be absolutely sure that PGAdapter has started.
+  sleep(2)
+  container
 end
\ No newline at end of file
diff --git a/samples/ruby/activerecord/application.rb b/samples/ruby/activerecord/application.rb
index 98e7ef15..82fe6f72 100644
--- a/samples/ruby/activerecord/application.rb
+++ b/samples/ruby/activerecord/application.rb
@@ -12,6 +12,7 @@ require_relative 'config/environment'
 require_relative 'models/singer'
 require_relative 'models/album'
 require_relative 'models/concert'
+require_relative 'models/ticket_sale'
 
 # This sample application shows the basic features of
 # ActiveRecord with Google Cloud Spanner PostgreSQL-dialect.
@@ -28,6 +29,9 @@ class Application
 
     # List all singers and related venue information.
     query_concerts
+
+    # Create a ticket_sale record that will be assigned a auto-generated primary key value.
+    create_ticket_sale
   end
 
   def self.query_singers
@@ -88,6 +92,20 @@ class Application
     end
   end
 
+  def self.create_ticket_sale
+    concert = Concert.first
+    ticket_sale = TicketSale.create({
+                                      concert: concert,
+                                      customer_name: "Alice",
+                                      price: 99.90,
+                                      seats: %w[A19 A20 A21],
+                                    })
+    puts ""
+    puts "Created ticket sale for concert '#{concert.name}' for customer #{ticket_sale.customer_name}"
+    puts "The ticket sale was assigned auto-generated key #{ticket_sale.id}"
+    puts ""
+  end
+
 end
 
 Application.run
diff --git a/samples/ruby/activerecord/db/migrate/01_create_tables.rb b/samples/ruby/activerecord/db/migrate/01_create_tables.rb
index 629d96bd..8c5adfc5 100644
--- a/samples/ruby/activerecord/db/migrate/01_create_tables.rb
+++ b/samples/ruby/activerecord/db/migrate/01_create_tables.rb
@@ -63,7 +63,7 @@ class CreateTables < ActiveRecord::Migration[7.0]
       t.integer :lock_version, null: false
     end
 
-    create_table :concerts, id: false, primary_key: :venue_id do |t|
+    create_table :concerts, id: false, primary_key: :concert_id do |t|
       t.string :concert_id, limit: 36, null: false, primary_key: true
       t.references :venue, foreign_key: {primary_key: :venue_id},
                    type: :string, limit: 36, index: false
@@ -78,5 +78,32 @@ class CreateTables < ActiveRecord::Migration[7.0]
       t.check_constraint "end_time > start_time", name: :chk_end_time_after_start_time
     end
 
+    # The ActiveRecord PostgreSQL provider does not know how to create a bit-reversed sequence.
+    # These sequences must therefore be created using a hand-written SQL script.
+    # Bit-reversed sequences can be used to create auto-generated primary key values that are
+    # safe from hot-spotting.
+    # See https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots
+    # for more information on choosing a good primary key.
+    execute "create sequence ticket_sale_seq
+             bit_reversed_positive
+             skip range 1 1000
+             start counter with 50000;"
+
+    # Create a table that uses a bit-reversed sequence to auto-generate primary key values.
+    # The 'id' column definition is specified using a SQL statement to ensure that we get
+    # a column with the right type, and with a default clause that selects a value from the
+    # sequence.
+    create_table :ticket_sales, id: false, primary_key: :id do |t|
+      t.column :id, "bigint not null primary key default nextval('ticket_sale_seq')"
+      t.references :concert, foreign_key: {primary_key: :concert_id},
+                   type: :string, limit: 36, index: false
+      t.string :customer_name
+      t.numeric :price
+      t.string :seats, array: true
+      t.datetime :created_at
+      t.datetime :updated_at
+      t.integer :lock_version, null: false
+    end
+
   end
 end
diff --git a/samples/ruby/activerecord/db/schema.rb b/samples/ruby/activerecord/db/schema.rb
index 402b6703..f8b90c4c 100644
--- a/samples/ruby/activerecord/db/schema.rb
+++ b/samples/ruby/activerecord/db/schema.rb
@@ -10,42 +10,52 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 1) do
-
+ActiveRecord::Schema[7.0].define(version: 1) do
   create_table "albums", primary_key: "album_id", id: { type: :string, limit: 36 }, comment: "", force: :cascade do |t|
     t.string "title"
     t.decimal "marketing_budget"
     t.date "release_date"
     t.binary "cover_picture"
     t.string "singer_id", limit: 36
-    t.datetime "created_at"
-    t.datetime "updated_at"
+    t.datetime "created_at", precision: nil
+    t.datetime "updated_at", precision: nil
     t.bigint "lock_version", null: false
-    t.index [], name: "IDX_albums_singer_id_N_CB7481F91EC719C7"
+    t.index [], name: "IDX_albums_singer_id_N_90CD66D06B92DBCF"
   end
 
   create_table "concerts", primary_key: "concert_id", id: { type: :string, limit: 36 }, comment: "", force: :cascade do |t|
     t.string "venue_id", limit: 36
     t.string "singer_id", limit: 36
     t.string "name"
-    t.datetime "start_time", null: false
-    t.datetime "end_time", null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
+    t.datetime "start_time", precision: nil, null: false
+    t.datetime "end_time", precision: nil, null: false
+    t.datetime "created_at", precision: nil
+    t.datetime "updated_at", precision: nil
     t.bigint "lock_version", null: false
-    t.index [], name: "IDX_concerts_singer_id_N_E69EF1394455F7E2"
-    t.index [], name: "IDX_concerts_venue_id_N_9722D7D8AF4ABB9E"
+    t.index [], name: "IDX_concerts_singer_id_N_E92CCCE963BE681C"
+    t.index [], name: "IDX_concerts_venue_id_N_F08F98ABB662E6D5"
     t.check_constraint nil, name: "chk_end_time_after_start_time"
   end
 
   create_table "singers", primary_key: "singer_id", id: { type: :string, limit: 36 }, comment: "", force: :cascade do |t|
     t.string "first_name", limit: 100
     t.string "last_name", limit: 200, null: false
-    t.string "full_name"
+    t.virtual "full_name", type: :string, as: "COALESCE(concat(first_name, ' '::character varying, last_name), last_name)", stored: true
     t.boolean "active"
-    t.datetime "created_at"
-    t.datetime "updated_at"
+    t.datetime "created_at", precision: nil
+    t.datetime "updated_at", precision: nil
+    t.bigint "lock_version", null: false
+  end
+
+  create_table "ticket_sales", id: :bigint, default: -> { "nextval('ticket_sale_seq'::text)" }, comment: "", force: :cascade do |t|
+    t.string "concert_id", limit: 36
+    t.string "customer_name"
+    t.decimal "price"
+    t.string "seats", array: true
+    t.datetime "created_at", precision: nil
+    t.datetime "updated_at", precision: nil
     t.bigint "lock_version", null: false
+    t.index [], name: "IDX_ticket_sales_concert_id_N_CAE2285D7AEE904D"
   end
 
   create_table "tracks", primary_key: ["album_id", "track_number"], comment: "", force: :cascade do |t|
@@ -53,20 +63,21 @@ ActiveRecord::Schema.define(version: 1) do
     t.bigint "track_number", null: false
     t.string "title", null: false
     t.float "sample_rate", null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
+    t.datetime "created_at", precision: nil
+    t.datetime "updated_at", precision: nil
     t.bigint "lock_version", null: false
   end
 
   create_table "venues", primary_key: "venue_id", id: { type: :string, limit: 36 }, comment: "", force: :cascade do |t|
     t.string "name"
     t.jsonb "description"
-    t.datetime "created_at"
-    t.datetime "updated_at"
+    t.datetime "created_at", precision: nil
+    t.datetime "updated_at", precision: nil
     t.bigint "lock_version", null: false
   end
 
   add_foreign_key "albums", "singers", primary_key: "singer_id"
   add_foreign_key "concerts", "singers", primary_key: "singer_id"
   add_foreign_key "concerts", "venues", primary_key: "venue_id"
+  add_foreign_key "ticket_sales", "concerts", primary_key: "concert_id"
 end
diff --git a/samples/ruby/activerecord/models/concert.rb b/samples/ruby/activerecord/models/concert.rb
index a5c0f57c..032cc08a 100644
--- a/samples/ruby/activerecord/models/concert.rb
+++ b/samples/ruby/activerecord/models/concert.rb
@@ -7,10 +7,13 @@
 # https://opensource.org/licenses/MIT.
 
 require_relative 'singer'
+require_relative 'ticket_sale'
 require_relative 'venue'
 
 # Model for Concert.
 class Concert < ActiveRecord::Base
   belongs_to :singer
   belongs_to :venue
+
+  has_many :ticket_sales
 end
diff --git a/samples/ruby/activerecord/models/ticket_sale.rb b/samples/ruby/activerecord/models/ticket_sale.rb
new file mode 100644
index 00000000..62804861
--- /dev/null
+++ b/samples/ruby/activerecord/models/ticket_sale.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Copyright 2024 Google LLC
+#
+# Use of this source code is governed by an MIT-style
+# license that can be found in the LICENSE file or at
+# https://opensource.org/licenses/MIT.
+
+require_relative 'concert'
+
+# Model for TicketSale.
+class TicketSale < ActiveRecord::Base
+  belongs_to :concert
+end
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java b/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
index 35ff8101..beaa5030 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
@@ -59,6 +59,7 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.SSLMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
@@ -85,7 +86,6 @@ import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.annotation.Nullable;
@@ -97,13 +97,13 @@ import javax.net.ssl.SSLSocketFactory;
  * representation {@link IntermediateStatement} that servers as a middle layer between Postgres and
  * Spanner.
  *
- * <p>Each {@link ConnectionHandler} is also a {@link Thread}. Although a TCP connection does not
- * necessarily need to have its own thread, this makes the implementation more straightforward.
+ * <p>Each {@link ConnectionHandler} is also a {@link Runnable} so it can be handed to a single
+ * handler thread. Although a TCP connection does not necessarily need to have its own thread, this
+ * makes the implementation more straightforward. The handler thread may be a virtual thread.
  */
 @InternalApi
-public class ConnectionHandler extends Thread {
+public class ConnectionHandler implements Runnable {
   private static final Logger logger = Logger.getLogger(ConnectionHandler.class.getName());
-  private static final AtomicLong CONNECTION_HANDLER_ID_GENERATOR = new AtomicLong(0L);
   private static final String CHANNEL_PROVIDER_PROPERTY = "CHANNEL_PROVIDER";
 
   private final ProxyServer server;
@@ -119,6 +119,7 @@ public class ConnectionHandler extends Thread {
   private static final Map<Integer, ConnectionHandler> CONNECTION_HANDLERS =
       new ConcurrentHashMap<>();
   private volatile ConnectionStatus status = ConnectionStatus.UNAUTHENTICATED;
+  private Thread thread;
   private final int connectionId;
   private final int secret;
   // Separate the following from the threat ID generator, since PG connection IDs are maximum
@@ -152,20 +153,37 @@ public class ConnectionHandler extends Thread {
   /** Constructor only for testing. */
   @VisibleForTesting
   ConnectionHandler(ProxyServer server, Socket socket, Connection spannerConnection) {
-    super("ConnectionHandler-" + CONNECTION_HANDLER_ID_GENERATOR.incrementAndGet());
     this.server = server;
     this.socket = socket;
     this.secret = new SecureRandom().nextInt();
     this.connectionId = incrementingConnectionId.incrementAndGet();
     CONNECTION_HANDLERS.put(this.connectionId, this);
-    setDaemon(true);
+    this.spannerConnection = spannerConnection;
+  }
+
+  void start() {
+    Preconditions.checkState(thread != null, "Cannot start a ConnectionHandler without a thread");
+    thread.start();
+  }
+
+  String getName() {
+    Preconditions.checkState(
+        thread != null, "Cannot get the name of a ConnectionHandler without a thread");
+    return thread.getName();
+  }
+
+  Thread getThread() {
+    return this.thread;
+  }
+
+  void setThread(Thread thread) {
+    this.thread = thread;
     logger.log(
         Level.INFO,
         () ->
             String.format(
                 "Connection handler with ID %s created for client %s",
                 getName(), socket.getInetAddress().getHostAddress()));
-    this.spannerConnection = spannerConnection;
   }
 
   void createSSLSocket() throws IOException {
@@ -273,13 +291,14 @@ public class ConnectionHandler extends Thread {
     return uri;
   }
 
-  private static String appendPropertiesToUrl(String url, Properties info) {
+  @VisibleForTesting
+  static String appendPropertiesToUrl(String url, Properties info) {
     if (info == null || info.isEmpty()) {
       return url;
     }
     StringBuilder result = new StringBuilder(url);
     for (Entry<Object, Object> entry : info.entrySet()) {
-      if (entry.getValue() != null && !"".equals(entry.getValue())) {
+      if (!Strings.isNullOrEmpty((String) entry.getValue())) {
         result.append(";").append(entry.getKey()).append("=").append(entry.getValue());
       }
     }
@@ -299,8 +318,8 @@ public class ConnectionHandler extends Thread {
             "Run",
             () ->
                 String.format(
-                    "Connection handler with ID %s starting for client %s",
-                    getName(), socket.getInetAddress().getHostAddress())));
+                    "Connection handler with ID %s starting for client %s with thread %s",
+                    getName(), socket.getInetAddress().getHostAddress(), thread.toString())));
     if (runConnection(false) == RunConnectionState.RESTART_WITH_SSL) {
       logger.log(
           Level.INFO,
@@ -630,7 +649,7 @@ public class ConnectionHandler extends Thread {
     // otherwise)
     try {
       connectionToCancel.getSpannerConnection().cancel();
-      connectionToCancel.interrupt();
+      connectionToCancel.getThread().interrupt();
       return true;
     } catch (Throwable ignore) {
     }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java b/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
index 44d94cd7..ed638ff4 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
@@ -19,6 +19,7 @@ import com.google.api.core.InternalApi;
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.cloud.spanner.ThreadFactoryUtil;
 import com.google.cloud.spanner.connection.SpannerPool;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.QueryMode;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
@@ -41,6 +42,7 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Level;
@@ -81,6 +83,8 @@ public class ProxyServer extends AbstractApiService {
   private final ConcurrentLinkedQueue<WireMessage> debugMessages = new ConcurrentLinkedQueue<>();
   private final AtomicInteger debugMessageCount = new AtomicInteger();
 
+  private final ThreadFactory threadFactory;
+
   /**
    * Instantiates the ProxyServer from CLI-gathered metadata.
    *
@@ -116,6 +120,9 @@ public class ProxyServer extends AbstractApiService {
     this.localPort = optionsMetadata.getProxyPort();
     this.properties = properties;
     this.debugMode = optionsMetadata.isDebugMode();
+    this.threadFactory =
+        ThreadFactoryUtil.createVirtualOrPlatformDaemonThreadFactory(
+            "ConnectionHandler", optionsMetadata.isUseVirtualThreads());
     addConnectionProperties();
   }
 
@@ -329,6 +336,8 @@ public class ProxyServer extends AbstractApiService {
     socket.setTcpNoDelay(true);
     ConnectionHandler handler = new ConnectionHandler(this, socket);
     register(handler);
+    Thread thread = threadFactory.newThread(handler);
+    handler.setThread(thread);
     handler.start();
   }
 
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 e5730088..0bcdb68b 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
@@ -23,6 +23,7 @@ import com.google.cloud.spanner.InstanceId;
 import com.google.cloud.spanner.SessionPoolOptions;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.SpannerOptions;
+import com.google.cloud.spanner.connection.ConnectionOptions;
 import com.google.cloud.spanner.pgadapter.ProxyServer;
 import com.google.cloud.spanner.pgadapter.Server;
 import com.google.common.annotations.VisibleForTesting;
@@ -50,6 +51,15 @@ import org.json.simple.JSONObject;
 
 /** Metadata extractor for CLI. */
 public class OptionsMetadata {
+  /** These system properties can be used to turn on the use of virtual threads. */
+  public static String USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME = "pgadapter.use_virtual_threads";
+
+  public static String USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME =
+      "pgadapter.use_virtual_grpc_transport_threads";
+
+  /** The environment variable used by Spanner to specify the emulator host:port. */
+  private static final String SPANNER_EMULATOR_HOST_ENV_VAR = "SPANNER_EMULATOR_HOST";
+
   static Duration DEFAULT_STARTUP_TIMEOUT = Duration.ofSeconds(30L);
 
   /**
@@ -82,6 +92,8 @@ public class OptionsMetadata {
     private boolean enableOpenTelemetry;
     private Double openTelemetryTraceRatio;
     private boolean skipLocalhostCheck;
+    private boolean useVirtualThreads;
+    private boolean useVirtualGrpcTransportThreads;
     private SslMode sslMode;
     private int port;
     private String unixDomainSocketDirectory;
@@ -255,6 +267,35 @@ public class OptionsMetadata {
       return this;
     }
 
+    /**
+     * Creates a virtual thread for each connection instead of a platform thread. Enabling this
+     * option only has effect if PGAdapter is running on Java 21 or higher. Enabling virtual threads
+     * can reduce memory consumption for PGAdapter instances that have a large number of incoming
+     * connections, and reduces the total number of platform threads that are created.
+     *
+     * <p>See <a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html">Virtual
+     * threads</a> for more information on virtual threads.
+     */
+    public Builder useVirtualThreads() {
+      this.useVirtualThreads = true;
+      return this;
+    }
+
+    /**
+     * Uses virtual threads instead of platform threads for the gRPC executor pool. Enabling this
+     * option only has effect if PGAdapter is running on Java 21 or higher. Enabling virtual threads
+     * for gRPC can reduce memory consumption for PGAdapter instances that execute a large number of
+     * parallel queries or transactions, and reduces the total number of platform threads that are
+     * created.
+     *
+     * <p>See <a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html">Virtual
+     * threads</a> for more information on virtual threads.
+     */
+    public Builder useVirtualGrpcTransportThreads() {
+      this.useVirtualGrpcTransportThreads = true;
+      return this;
+    }
+
     /**
      * PGAdapter by default does not support SSL connections. Call this method to allow or require
      * SSL connections.
@@ -391,7 +432,12 @@ public class OptionsMetadata {
       if (endpoint != null) {
         addOption(args, OPTION_SPANNER_ENDPOINT, endpoint);
       }
-      if (usePlainText || numChannels != null || databaseRole != null || autoConfigEmulator) {
+      if (usePlainText
+          || numChannels != null
+          || databaseRole != null
+          || autoConfigEmulator
+          || useVirtualThreads
+          || useVirtualGrpcTransportThreads) {
         StringBuilder jdbcOptionBuilder = new StringBuilder();
         if (usePlainText) {
           jdbcOptionBuilder.append("usePlainText=true;");
@@ -405,6 +451,16 @@ public class OptionsMetadata {
         if (autoConfigEmulator) {
           jdbcOptionBuilder.append("autoConfigEmulator=true;");
         }
+        if (useVirtualThreads) {
+          jdbcOptionBuilder
+              .append(ConnectionOptions.USE_VIRTUAL_THREADS_PROPERTY_NAME)
+              .append("=true;");
+        }
+        if (useVirtualGrpcTransportThreads) {
+          jdbcOptionBuilder
+              .append(ConnectionOptions.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME)
+              .append("=true;");
+        }
         addOption(args, OPTION_JDBC_PROPERTIES, jdbcOptionBuilder.toString());
       }
       if (debugMode) {
@@ -730,6 +786,18 @@ public class OptionsMetadata {
         }
       }
     }
+    // Set virtual thread properties from system properties if:
+    // 1. The virtual thread property has not been set in the command line arguments.
+    // 2. The corresponding system property has been set.
+    if (properties.get(ConnectionOptions.USE_VIRTUAL_THREADS_PROPERTY_NAME) == null
+        && Boolean.parseBoolean(System.getProperty(USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME))) {
+      properties.put(ConnectionOptions.USE_VIRTUAL_THREADS_PROPERTY_NAME, "true");
+    }
+    if (properties.get(ConnectionOptions.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME) == null
+        && Boolean.parseBoolean(
+            System.getProperty(USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME))) {
+      properties.put(ConnectionOptions.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME, "true");
+    }
     return properties;
   }
 
@@ -876,28 +944,39 @@ public class OptionsMetadata {
     // Check if it is a full database name, or only a database ID.
     DatabaseName databaseName = getDatabaseName(database);
     String host = commandLine.getOptionValue(OPTION_SPANNER_ENDPOINT, "");
+    boolean usePlainText = false;
+    if (Strings.isNullOrEmpty(host)
+        && !Strings.isNullOrEmpty(environment.get(SPANNER_EMULATOR_HOST_ENV_VAR))) {
+      host = environment.get(SPANNER_EMULATOR_HOST_ENV_VAR);
+      usePlainText = true;
+    }
     String endpoint;
     if (host.isEmpty()) {
       endpoint = "cloudspanner:/";
     } else {
       endpoint = "cloudspanner://" + host + "/";
+      String finalHost = host;
       logger.log(
           Level.INFO,
           () ->
               String.format(
                   "PG Adapter will connect to the following Cloud Spanner service endpoint: %s",
-                  host));
+                  finalHost));
     }
 
     // Note that Credentials here is the credentials file, not the actual credentials
     String url = String.format("%s%s;userAgent=%s", endpoint, databaseName, DEFAULT_USER_AGENT);
 
-    if (!shouldAuthenticate() && environment.get("SPANNER_EMULATOR_HOST") == null) {
+    if (!shouldAuthenticate()
+        && Strings.isNullOrEmpty(environment.get(SPANNER_EMULATOR_HOST_ENV_VAR))) {
       String credentials = buildCredentialsFile();
       if (!Strings.isNullOrEmpty(credentials)) {
         url = String.format("%s;credentials=%s", url, credentials);
       }
     }
+    if (usePlainText) {
+      url += ";usePlainText=true";
+    }
 
     return url;
   }
@@ -1266,6 +1345,16 @@ public class OptionsMetadata {
     return this.debugMode;
   }
 
+  public boolean isUseVirtualThreads() {
+    return Boolean.parseBoolean(
+        getPropertyMap().get(ConnectionOptions.USE_VIRTUAL_THREADS_PROPERTY_NAME));
+  }
+
+  public boolean isUseGrpcTransportVirtualThreads() {
+    return Boolean.parseBoolean(
+        getPropertyMap().get(ConnectionOptions.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME));
+  }
+
   public Duration getStartupTimeout() {
     return this.startupTimeout;
   }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SelectCurrentSettingStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SelectCurrentSettingStatement.java
new file mode 100644
index 00000000..2943da29
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SelectCurrentSettingStatement.java
@@ -0,0 +1,135 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.Type;
+import com.google.cloud.spanner.Type.StructField;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.QueryResult;
+import com.google.cloud.spanner.pgadapter.statements.SessionStatementParser.ShowStatement;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.QuotedString;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TableOrIndexName;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Future;
+
+/**
+ * SELECT CURRENT_SETTING(setting_name text [, missing_ok boolean ] ) is translated to the
+ * equivalent SHOW statement.
+ */
+public class SelectCurrentSettingStatement extends IntermediatePortalStatement {
+  private final ShowStatement showStatement;
+
+  public SelectCurrentSettingStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(
+        name,
+        new IntermediatePreparedStatement(
+            connectionHandler,
+            options,
+            name,
+            NO_PARAMETER_TYPES,
+            parsedStatement,
+            originalStatement),
+        NO_PARAMS,
+        ImmutableList.of(),
+        ImmutableList.of());
+    this.showStatement = parse(originalStatement.getSql());
+  }
+
+  static ShowStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    // SELECT CURRENT_SETTING('setting_name'[, missing_ok])
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("select", "current_setting")) {
+      throw PGExceptionFactory.newPGException(
+          "not a valid SELECT CURRENT_SETTING statement: " + sql, SQLState.SyntaxError);
+    }
+    if (!parser.eatToken("(")) {
+      throw PGExceptionFactory.newPGException(
+          "missing '(' for current_setting", SQLState.SyntaxError);
+    }
+    QuotedString setting = parser.readSingleQuotedString();
+    boolean missingOk = false;
+    if (parser.eatToken(",")) {
+      String missingOkString = parser.readKeyword().toLowerCase(Locale.ENGLISH);
+      missingOk = BooleanParser.toBoolean(missingOkString);
+    }
+    if (!parser.eatToken(")")) {
+      throw PGExceptionFactory.newPGException(
+          "missing ')' for current_setting", SQLState.SyntaxError);
+    }
+    parser.throwIfHasMoreTokens();
+
+    try {
+      TableOrIndexName settingName = TableOrIndexName.parse(setting.getValue());
+      return new ShowStatement(settingName, "current_setting", missingOk);
+    } catch (IllegalArgumentException illegalArgumentException) {
+      throw PGExceptionFactory.newPGException(
+          illegalArgumentException.getMessage(), SQLState.SyntaxError);
+    }
+  }
+
+  @Override
+  public String getCommandTag() {
+    return "SELECT";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    this.executed = true;
+    setFutureStatementResult(
+        Futures.immediateFuture(showStatement.execute(backendConnection.getSessionState())));
+  }
+
+  @Override
+  public Future<StatementResult> describeAsync(BackendConnection backendConnection) {
+    ResultSet resultSet =
+        ClientSideResultSet.forRows(
+            Type.struct(StructField.of("current_setting", Type.string())), ImmutableList.of());
+    return Futures.immediateFuture(new QueryResult(resultSet));
+  }
+
+  @Override
+  public IntermediatePortalStatement createPortal(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    return this;
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SelectSetConfigStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SelectSetConfigStatement.java
new file mode 100644
index 00000000..0a949eeb
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SelectSetConfigStatement.java
@@ -0,0 +1,151 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.Type;
+import com.google.cloud.spanner.Type.StructField;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
+import com.google.cloud.spanner.pgadapter.error.SQLState;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.QueryResult;
+import com.google.cloud.spanner.pgadapter.statements.SessionStatementParser.SetStatement;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.QuotedString;
+import com.google.cloud.spanner.pgadapter.statements.SimpleParser.TableOrIndexName;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Future;
+
+/**
+ * SELECT SET_CONFIG(setting_name text, new_value text, is_local boolean) is translated to the
+ * equivalent SET statement.
+ */
+public class SelectSetConfigStatement extends IntermediatePortalStatement {
+  private final SetStatement setStatement;
+
+  public SelectSetConfigStatement(
+      ConnectionHandler connectionHandler,
+      OptionsMetadata options,
+      String name,
+      ParsedStatement parsedStatement,
+      Statement originalStatement) {
+    super(
+        name,
+        new IntermediatePreparedStatement(
+            connectionHandler,
+            options,
+            name,
+            NO_PARAMETER_TYPES,
+            parsedStatement,
+            originalStatement),
+        NO_PARAMS,
+        ImmutableList.of(),
+        ImmutableList.of());
+    this.setStatement = parse(originalStatement.getSql());
+  }
+
+  static SetStatement parse(String sql) {
+    Preconditions.checkNotNull(sql);
+
+    // SELECT SET_CONFIG(setting_name text, new_value text, is_local boolean)
+    SimpleParser parser = new SimpleParser(sql);
+    if (!parser.eatKeyword("select", "set_config")) {
+      throw PGExceptionFactory.newPGException(
+          "not a valid SELECT SET_CONFIG statement: " + sql, SQLState.SyntaxError);
+    }
+    if (!parser.eatToken("(")) {
+      throw PGExceptionFactory.newPGException("missing '(' for set_config", SQLState.SyntaxError);
+    }
+    QuotedString setting = parser.readSingleQuotedString();
+    if (!parser.eatToken(",")) {
+      throw PGExceptionFactory.newPGException(
+          "missing ',' after setting name", SQLState.SyntaxError);
+    }
+    QuotedString value = parser.readSingleQuotedString();
+    if (!parser.eatToken(",")) {
+      throw PGExceptionFactory.newPGException(
+          "missing ',' after setting value", SQLState.SyntaxError);
+    }
+    String localString = parser.readKeyword().toLowerCase(Locale.ENGLISH);
+    boolean local = BooleanParser.toBoolean(localString);
+    if (!parser.eatToken(")")) {
+      throw PGExceptionFactory.newPGException("missing ')' for set_config", SQLState.SyntaxError);
+    }
+    parser.throwIfHasMoreTokens();
+
+    try {
+      TableOrIndexName settingName = TableOrIndexName.parse(setting.getValue());
+      return new SetStatement(local, settingName, value.rawValue);
+    } catch (IllegalArgumentException illegalArgumentException) {
+      throw PGExceptionFactory.newPGException(
+          illegalArgumentException.getMessage(), SQLState.SyntaxError);
+    }
+  }
+
+  @Override
+  public String getCommandTag() {
+    return "SELECT";
+  }
+
+  @Override
+  public StatementType getStatementType() {
+    return StatementType.CLIENT_SIDE;
+  }
+
+  @Override
+  public void executeAsync(BackendConnection backendConnection) {
+    this.executed = true;
+    try {
+      setStatement.execute(backendConnection.getSessionState());
+    } catch (Throwable throwable) {
+      setFutureStatementResult(Futures.immediateFailedFuture(throwable));
+      return;
+    }
+    setFutureStatementResult(
+        Futures.immediateFuture(
+            new QueryResult(
+                ClientSideResultSet.forRows(
+                    Type.struct(StructField.of("set_config", Type.string())),
+                    ImmutableList.of(
+                        Struct.newBuilder().set("set_config").to(setStatement.value).build())))));
+  }
+
+  @Override
+  public Future<StatementResult> describeAsync(BackendConnection backendConnection) {
+    ResultSet resultSet =
+        ClientSideResultSet.forRows(
+            Type.struct(StructField.of("set_config", Type.string())), ImmutableList.of());
+    return Futures.immediateFuture(new QueryResult(resultSet));
+  }
+
+  @Override
+  public IntermediatePortalStatement createPortal(
+      String name,
+      byte[][] parameters,
+      List<Short> parameterFormatCodes,
+      List<Short> resultFormatCodes) {
+    return this;
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java
index 2bfc7a0a..63ca472a 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SessionStatementParser.java
@@ -172,26 +172,37 @@ public class SessionStatementParser {
   }
 
   static class ShowStatement extends SessionStatement {
+    final String header;
+    final boolean missingOk;
 
     static ShowStatement createShowAll() {
       return new ShowStatement(null);
     }
 
     ShowStatement(TableOrIndexName name) {
+      this(name, null, false);
+    }
+
+    ShowStatement(TableOrIndexName name, String header, boolean missingOk) {
       super(name);
+      this.header = header;
+      this.missingOk = missingOk;
     }
 
     @Override
     public StatementResult execute(SessionState sessionState) {
       if (name != null) {
+        String value;
+        if (missingOk) {
+          PGSetting pgSetting = sessionState.tryGet(extension, name);
+          value = pgSetting == null ? null : pgSetting.getSetting();
+        } else {
+          value = sessionState.get(extension, name).getSetting();
+        }
         return new QueryResult(
             ClientSideResultSet.forRows(
                 Type.struct(StructField.of(getKey(), Type.string())),
-                ImmutableList.of(
-                    Struct.newBuilder()
-                        .set(getKey())
-                        .to(sessionState.get(extension, name).getSetting())
-                        .build())));
+                ImmutableList.of(Struct.newBuilder().set(getKey()).to(value).build())));
       }
       return new QueryResult(
           ClientSideResultSet.forRows(
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java
index eac80132..f926e086 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/SimpleParser.java
@@ -960,18 +960,22 @@ public class SimpleParser {
     return pos > startPos;
   }
 
+  @Nonnull
   QuotedString readSingleQuotedString() {
     return readQuotedString(SINGLE_QUOTE, false);
   }
 
+  @Nonnull
   QuotedString readDoubleQuotedString() {
     return readQuotedString(DOUBLE_QUOTE, false);
   }
 
+  @Nonnull
   QuotedString readQuotedString(char quote) {
     return readQuotedString(quote, false);
   }
 
+  @Nonnull
   QuotedString readQuotedString(char quote, boolean alwaysEscaped) {
     skipWhitespaces();
     if (pos >= sql.length()) {
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
index 5ce27fdf..f2c32e5f 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/MutationWriter.java
@@ -14,6 +14,8 @@
 
 package com.google.cloud.spanner.pgadapter.utils;
 
+import static com.google.cloud.spanner.ThreadFactoryUtil.createVirtualOrPlatformDaemonThreadFactory;
+
 import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutures;
 import com.google.api.core.InternalApi;
@@ -64,6 +66,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
@@ -97,6 +100,9 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
 
   private static final Logger logger = Logger.getLogger(MutationWriter.class.getName());
 
+  private static final ThreadFactory THREAD_FACTORY =
+      createVirtualOrPlatformDaemonThreadFactory("copy-worker", true);
+
   private final CopyTransactionMode transactionMode;
   private long rowCount;
   private final Connection connection;
@@ -117,7 +123,7 @@ public class MutationWriter implements Callable<StatementResult>, Closeable {
   private final AtomicBoolean rollback = new AtomicBoolean(false);
   private final CountDownLatch closedLatch = new CountDownLatch(1);
   private final ListeningExecutorService executorService =
-      MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+      MoreExecutors.listeningDecorator(Executors.newCachedThreadPool(THREAD_FACTORY));
 
   private final Object lock = new Object();
 
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
index 08a4edaf..a10a767a 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java
@@ -26,6 +26,8 @@ import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.PREPA
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.RELEASE;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.ROLLBACK;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.SAVEPOINT;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.SELECT_CURRENT_SETTING;
+import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.SELECT_SET_CONFIG;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.SHOW_DATABASE_DDL;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.TRUNCATE;
 import static com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage.VACUUM;
@@ -51,6 +53,8 @@ import com.google.cloud.spanner.pgadapter.statements.PrepareStatement;
 import com.google.cloud.spanner.pgadapter.statements.ReleaseStatement;
 import com.google.cloud.spanner.pgadapter.statements.RollbackToStatement;
 import com.google.cloud.spanner.pgadapter.statements.SavepointStatement;
+import com.google.cloud.spanner.pgadapter.statements.SelectCurrentSettingStatement;
+import com.google.cloud.spanner.pgadapter.statements.SelectSetConfigStatement;
 import com.google.cloud.spanner.pgadapter.statements.ShowDatabaseDdlStatement;
 import com.google.cloud.spanner.pgadapter.statements.TruncateStatement;
 import com.google.cloud.spanner.pgadapter.statements.VacuumStatement;
@@ -217,6 +221,20 @@ public class ParseMessage extends AbstractQueryProtocolMessage {
             name,
             parsedStatement,
             originalStatement);
+      } else if (isCommand(SELECT_CURRENT_SETTING, originalStatement.getSql())) {
+        return new SelectCurrentSettingStatement(
+            connectionHandler,
+            connectionHandler.getServer().getOptions(),
+            name,
+            parsedStatement,
+            originalStatement);
+      } else if (isCommand(SELECT_SET_CONFIG, originalStatement.getSql())) {
+        return new SelectSetConfigStatement(
+            connectionHandler,
+            connectionHandler.getServer().getOptions(),
+            name,
+            parsedStatement,
+            originalStatement);
       } else {
         return new IntermediatePreparedStatement(
             connectionHandler,
@@ -238,11 +256,15 @@ public class ParseMessage extends AbstractQueryProtocolMessage {
   }
 
   @Override
-  void buffer(BackendConnection backendConnection) {
+  void buffer(BackendConnection backendConnection) throws Exception {
     if (!Strings.isNullOrEmpty(this.name) && this.connection.hasStatement(this.name)) {
       throw new IllegalStateException("Must close statement before reusing name.");
     }
-    this.connection.registerStatement(this.name, this.statement);
+    if (this.statement.hasException()) {
+      handleError(statement.getException());
+    } else {
+      this.connection.registerStatement(this.name, this.statement);
+    }
   }
 
   @Override
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
index 0af8d90d..454288a5 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/QueryMessage.java
@@ -40,6 +40,10 @@ public class QueryMessage extends ControlMessage {
   public static final String CLOSE = "CLOSE";
   public static final ImmutableList<String> SHOW_DATABASE_DDL =
       ImmutableList.of("SHOW", "DATABASE", "DDL");
+  public static final ImmutableList<String> SELECT_CURRENT_SETTING =
+      ImmutableList.of("SELECT", "CURRENT_SETTING");
+  public static final ImmutableList<String> SELECT_SET_CONFIG =
+      ImmutableList.of("SELECT", "SET_CONFIG");
   private final Statement originalStatement;
   private final SimpleQueryStatement simpleQueryStatement;
 
diff --git a/versions.txt b/versions.txt
index abb87c16..adc38d06 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-spanner-pgadapter:0.29.1:0.29.2-SNAPSHOT
+google-cloud-spanner-pgadapter:0.31.0:0.31.1-SNAPSHOT


Test patch:
diff --git a/.github/workflows/benchmark-tests.yml b/.github/workflows/benchmark-tests.yml
new file mode 100644
index 00000000..31b68465
--- /dev/null
+++ b/.github/workflows/benchmark-tests.yml
@@ -0,0 +1,20 @@
+on:
+  push:
+    branches:
+      - postgresql-dialect
+  pull_request:
+  workflow_dispatch:
+name: benchmark-tests
+jobs:
+  java-benchmarks:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup Java
+        uses: actions/setup-java@v4
+        with:
+          distribution: zulu
+          java-version: 17
+      - name: Run Java Latency Benchmark tests
+        working-directory: ./benchmarks/latency-comparison/java
+        run: mvn test -B
diff --git a/benchmarks/latency-comparison/java/src/test/java/com/google/cloud/spanner/pgadapter/latency/RunBenchmarkTest.java b/benchmarks/latency-comparison/java/src/test/java/com/google/cloud/spanner/pgadapter/latency/RunBenchmarkTest.java
new file mode 100644
index 00000000..ea920687
--- /dev/null
+++ b/benchmarks/latency-comparison/java/src/test/java/com/google/cloud/spanner/pgadapter/latency/RunBenchmarkTest.java
@@ -0,0 +1,60 @@
+// Copyright 2024 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.latency;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Scanner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RunBenchmarkTest {
+
+  @Test
+  public void testRunBenchmark() throws Exception {
+    ProcessBuilder builder = new ProcessBuilder();
+    builder.command(
+        "mvn",
+        "clean",
+        "compile",
+        "exec:java",
+        "-Dexec.args=--clients=16 --operations=1000 -skip_pg -skip_jdbc -skip_spanner");
+    builder.environment().put("GOOGLE_CLOUD_PROJECT", "test-project");
+    builder.environment().put("SPANNER_INSTANCE", "test-instance");
+    builder.environment().put("SPANNER_DATABASE", "test-database");
+
+    Process process = builder.start();
+    InputStream inputStream = process.getInputStream();
+    InputStream errorStream = process.getErrorStream();
+    int res = process.waitFor();
+    String output = readAll(inputStream);
+    String errors = readAll(errorStream);
+    assertEquals(output + "\n" + errors, 0, res);
+  }
+
+  static String readAll(InputStream inputStream) {
+    StringBuilder result = new StringBuilder();
+    try (Scanner scanner = new Scanner(new InputStreamReader(inputStream))) {
+      while (scanner.hasNextLine()) {
+        result.append(scanner.nextLine()).append("\n");
+      }
+    }
+    return result.toString();
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java
index 71a160f0..16102ac9 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/AbstractMockServerTest.java
@@ -173,7 +173,7 @@ public abstract class AbstractMockServerTest {
       Statement.of(
           "with "
               + PG_TYPE_PREFIX
-              + "\nSELECT pg_type.oid, typname   FROM pg_type   LEFT   JOIN (select ns.oid as nspoid, ns.nspname, r.r           from pg_namespace as ns           join ( select 1 as r, 'public' as nspname ) as r          using ( nspname )        ) as sp     ON sp.nspoid = typnamespace  WHERE typname = 'jsonb'  ORDER BY sp.r, pg_type.oid DESC LIMIT 1");
+              + "\nSELECT pg_type.oid, typname   FROM pg_type   LEFT   JOIN (select ns.oid as nspoid, ns.nspname, r.r           from pg_namespace as ns           join ( select 1 as r, 'public' as nspname ) as r          using ( nspname )        ) as sp     ON sp.nspoid = typnamespace  WHERE typname = ('jsonb')  ORDER BY sp.r, pg_type.oid DESC LIMIT 1");
 
   protected static final ResultSet SELECT_JSONB_TYPE_BY_NAME_RESULT_SET =
       ResultSet.newBuilder()
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
index 83d20e8f..36b89b8b 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
@@ -14,21 +14,33 @@
 
 package com.google.cloud.spanner.pgadapter;
 
+import static com.google.cloud.spanner.pgadapter.ConnectionHandler.appendPropertiesToUrl;
 import static com.google.cloud.spanner.pgadapter.ConnectionHandler.buildConnectionURL;
+import static com.google.cloud.spanner.pgadapter.ConnectionHandler.listDatabasesOrInstances;
+import static com.google.cloud.spanner.pgadapter.EmulatedPsqlMockServerTest.newStatusResourceNotFoundException;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.google.api.core.SettableApiFuture;
+import com.google.api.gax.paging.Pages;
 import com.google.cloud.NoCredentials;
+import com.google.cloud.spanner.DatabaseAdminClient;
 import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.Spanner;
 import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.SpannerException.ResourceNotFoundException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
@@ -48,6 +60,9 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.spanner.v1.DatabaseName;
+import io.grpc.StatusRuntimeException;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
 import java.io.EOFException;
@@ -56,8 +71,10 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.InetAddress;
 import java.net.Socket;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.Properties;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,8 +87,7 @@ public class ConnectionHandlerTest {
   public void testRegisterStatement() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     IntermediatePreparedStatement statement = mock(IntermediatePreparedStatement.class);
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
@@ -88,8 +104,7 @@ public class ConnectionHandlerTest {
   public void testCloseUnknownStatement() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
 
@@ -100,8 +115,7 @@ public class ConnectionHandlerTest {
   public void testCloseAll() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     IntermediatePreparedStatement statement1 = mock(IntermediatePreparedStatement.class);
     IntermediatePreparedStatement statement2 = mock(IntermediatePreparedStatement.class);
 
@@ -119,8 +133,7 @@ public class ConnectionHandlerTest {
   public void testTerminateClosesSocket() throws IOException {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
 
@@ -133,8 +146,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
     when(socket.isClosed()).thenReturn(false, true);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
 
@@ -150,12 +162,12 @@ public class ConnectionHandlerTest {
   public void testTerminateHandlesCloseError() throws IOException {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     // IOException should be handled internally in terminate().
     doThrow(new IOException("test exception")).when(socket).close();
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
+    connection.setThread(mock(Thread.class));
 
     connection.terminate();
     verify(socket).close();
@@ -165,8 +177,7 @@ public class ConnectionHandlerTest {
   public void testTerminateClosesAllPortals() throws Exception {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     IntermediatePortalStatement portal1 = mock(IntermediatePortalStatement.class);
     IntermediatePortalStatement portal2 = mock(IntermediatePortalStatement.class);
 
@@ -184,8 +195,7 @@ public class ConnectionHandlerTest {
   public void testTerminateIgnoresPortalCloseError() throws Exception {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     IntermediatePortalStatement portal1 = mock(IntermediatePortalStatement.class);
     IntermediatePortalStatement portal2 = mock(IntermediatePortalStatement.class);
     doThrow(new Exception("test")).when(portal2).close();
@@ -204,8 +214,7 @@ public class ConnectionHandlerTest {
   public void testGetPortal() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     IntermediatePortalStatement portal1 = mock(IntermediatePortalStatement.class);
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
@@ -218,8 +227,7 @@ public class ConnectionHandlerTest {
   public void testGetUnknownPortal() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
     PGException exception =
@@ -231,8 +239,7 @@ public class ConnectionHandlerTest {
   public void testClosePortal() throws Exception {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     IntermediatePortalStatement portal1 = mock(IntermediatePortalStatement.class);
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
@@ -246,8 +253,7 @@ public class ConnectionHandlerTest {
   public void testCloseUnknownPortal() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
 
     ConnectionHandler connection = new ConnectionHandler(server, socket);
     PGException exception =
@@ -259,8 +265,7 @@ public class ConnectionHandlerTest {
   public void testHandleMessages_NonFatalException() throws Exception {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     DataOutputStream dataOutputStream = new DataOutputStream(new ByteArrayOutputStream());
     ConnectionMetadata connectionMetadata = mock(ConnectionMetadata.class);
     when(connectionMetadata.getOutputStream()).thenReturn(dataOutputStream);
@@ -278,6 +283,7 @@ public class ConnectionHandlerTest {
             return connectionMetadata;
           }
         };
+    connection.setThread(mock(Thread.class));
 
     connection.setMessageState(message);
     connection.handleMessages();
@@ -289,8 +295,7 @@ public class ConnectionHandlerTest {
   public void testHandleMessages_FatalException() throws Exception {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     DataOutputStream dataOutputStream = new DataOutputStream(new ByteArrayOutputStream());
     ConnectionMetadata connectionMetadata = mock(ConnectionMetadata.class);
     when(connectionMetadata.getOutputStream()).thenReturn(dataOutputStream);
@@ -304,6 +309,7 @@ public class ConnectionHandlerTest {
             return connectionMetadata;
           }
         };
+    connection.setThread(mock(Thread.class));
 
     connection.setMessageState(message);
     connection.handleMessages();
@@ -315,15 +321,14 @@ public class ConnectionHandlerTest {
   public void testCancelActiveStatement() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
-    when(address.getHostAddress()).thenReturn("address1");
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     Connection spannerConnection = mock(Connection.class);
 
     ConnectionHandler connectionHandler =
         new ConnectionHandler(server, socket, mock(Connection.class));
     ConnectionHandler connectionHandlerToCancel =
         new ConnectionHandler(server, socket, spannerConnection);
+    connectionHandlerToCancel.setThread(mock(Thread.class));
 
     // Cancelling yourself is not allowed.
     assertFalse(
@@ -354,8 +359,7 @@ public class ConnectionHandlerTest {
   public void testRestartConnectionWithSsl_CreatesSslSocket() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     AtomicBoolean calledCreateSSLSocket = new AtomicBoolean();
     ConnectionHandler connection =
         new ConnectionHandler(server, socket) {
@@ -364,6 +368,7 @@ public class ConnectionHandlerTest {
             calledCreateSSLSocket.set(true);
           }
         };
+    connection.setThread(mock(Thread.class));
     connection.restartConnectionWithSsl();
 
     assertTrue(calledCreateSSLSocket.get());
@@ -373,8 +378,7 @@ public class ConnectionHandlerTest {
   public void testRestartConnectionWithSsl_SslSocketCreationFailureIsConvertedToPGException() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connection =
         new ConnectionHandler(server, socket) {
           @Override
@@ -391,8 +395,7 @@ public class ConnectionHandlerTest {
       testRestartConnectionWithSsl_SslSocketCreationFailureIsConvertedToPGExceptionWithMessage() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connection =
         new ConnectionHandler(server, socket) {
           @Override
@@ -408,8 +411,7 @@ public class ConnectionHandlerTest {
   public void testRestartConnectionWithSsl_SendsPGException() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
 
     ConnectionHandler connection =
         new ConnectionHandler(server, socket) {
@@ -432,8 +434,7 @@ public class ConnectionHandlerTest {
   public void testRestartConnectionWithSsl_IgnoresExceptionErrorHandling() {
     ProxyServer server = mock(ProxyServer.class);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
 
     ConnectionHandler connection =
         new ConnectionHandler(server, socket) {
@@ -463,8 +464,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler =
         new ConnectionHandler(server, socket) {
           @Override
@@ -473,11 +473,7 @@ public class ConnectionHandlerTest {
           }
         };
 
-    when(address.isLoopbackAddress()).thenReturn(true);
     assertTrue(connectionHandler.checkValidConnection(false));
-
-    when(address.isLoopbackAddress()).thenReturn(false);
-    assertFalse(connectionHandler.checkValidConnection(false));
   }
 
   @Test
@@ -486,8 +482,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler =
         new ConnectionHandler(server, socket) {
           @Override
@@ -496,11 +491,7 @@ public class ConnectionHandlerTest {
           }
         };
 
-    when(address.isAnyLocalAddress()).thenReturn(true);
     assertTrue(connectionHandler.checkValidConnection(false));
-
-    when(address.isAnyLocalAddress()).thenReturn(false);
-    assertFalse(connectionHandler.checkValidConnection(false));
   }
 
   @Test
@@ -509,8 +500,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler =
         new ConnectionHandler(server, socket) {
           @Override
@@ -518,15 +508,11 @@ public class ConnectionHandlerTest {
             return new ConnectionMetadata(mock(InputStream.class), mock(OutputStream.class));
           }
         };
+    connectionHandler.setThread(mock(Thread.class));
 
     when(options.getSslMode()).thenReturn(SslMode.Enable);
-    when(address.isLoopbackAddress()).thenReturn(true);
     assertTrue(connectionHandler.checkValidConnection(false));
 
-    assertTrue(connectionHandler.checkValidConnection(true));
-    when(address.isLoopbackAddress()).thenReturn(false);
-    assertTrue(connectionHandler.checkValidConnection(true));
-
     when(options.getSslMode()).thenReturn(SslMode.Require);
     assertFalse(connectionHandler.checkValidConnection(false));
     assertTrue(connectionHandler.checkValidConnection(true));
@@ -539,8 +525,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
 
     assertTrue(connectionHandler.checkValidConnection(false));
@@ -552,8 +537,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
 
     when(options.shouldAutoDetectClient()).thenReturn(true);
@@ -570,8 +554,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
 
     when(options.shouldAutoDetectClient()).thenReturn(true);
@@ -592,8 +575,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
 
     when(options.shouldAutoDetectClient()).thenReturn(false);
@@ -614,8 +596,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
 
     when(options.shouldAutoDetectClient()).thenReturn(true);
@@ -640,8 +621,7 @@ public class ConnectionHandlerTest {
     ProxyServer server = mock(ProxyServer.class);
     when(server.getOptions()).thenReturn(options);
     Socket socket = mock(Socket.class);
-    InetAddress address = mock(InetAddress.class);
-    when(socket.getInetAddress()).thenReturn(address);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
     ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
 
     when(options.shouldAutoDetectClient()).thenReturn(true);
@@ -670,6 +650,12 @@ public class ConnectionHandlerTest {
 
   @Test
   public void testBuildConnectionUrl() {
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME) == null);
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME)
+            == null);
+
     OptionsMetadata options =
         OptionsMetadata.newBuilder().setCredentials(NoCredentials.getInstance()).build();
     // Check that the dialect is included in the connection URL. This is required to support the
@@ -748,6 +734,155 @@ public class ConnectionHandlerTest {
         System.setProperty("ENABLE_CHANNEL_PROVIDER", currentEnableChannelProvider);
       }
     }
+
+    // Verify that the virtual threads properties are carried over to the connection URL.
+    assertEquals(
+        "cloudspanner:/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter;useVirtualThreads=true;dialect=postgresql",
+        buildConnectionURL(
+            "projects/my-project/instances/my-instance/databases/my-database",
+            options,
+            buildProperties(ImmutableMap.of("useVirtualThreads", "true"))));
+    assertEquals(
+        "cloudspanner:/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter;useVirtualGrpcTransportThreads=true;dialect=postgresql",
+        buildConnectionURL(
+            "projects/my-project/instances/my-instance/databases/my-database",
+            options,
+            buildProperties(ImmutableMap.of("useVirtualGrpcTransportThreads", "true"))));
+
+    runWithSystemProperty(
+        OptionsMetadata.USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME,
+        "true",
+        () -> {
+          OptionsMetadata optionsWithSystemProperty =
+              OptionsMetadata.newBuilder().setCredentials(NoCredentials.getInstance()).build();
+          assertEquals(
+              "cloudspanner:/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter;useVirtualThreads=true;dialect=postgresql",
+              buildConnectionURL(
+                  "projects/my-project/instances/my-instance/databases/my-database",
+                  optionsWithSystemProperty,
+                  buildProperties(optionsWithSystemProperty.getPropertyMap())));
+        });
+    runWithSystemProperty(
+        OptionsMetadata.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME,
+        "true",
+        () -> {
+          OptionsMetadata optionsWithSystemProperty =
+              OptionsMetadata.newBuilder().setCredentials(NoCredentials.getInstance()).build();
+          assertEquals(
+              "cloudspanner:/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter;useVirtualGrpcTransportThreads=true;dialect=postgresql",
+              buildConnectionURL(
+                  "projects/my-project/instances/my-instance/databases/my-database",
+                  optionsWithSystemProperty,
+                  buildProperties(optionsWithSystemProperty.getPropertyMap())));
+        });
+  }
+
+  @Test
+  public void testGetName() {
+    ProxyServer server = mock(ProxyServer.class);
+    Socket socket = mock(Socket.class);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
+    Connection connection = mock(Connection.class);
+    ConnectionHandler connectionHandler = new ConnectionHandler(server, socket, connection);
+
+    assertThrows(IllegalStateException.class, connectionHandler::getName);
+
+    Thread thread = new Thread(() -> {}, "test-thread");
+    connectionHandler.setThread(thread);
+    assertEquals("test-thread", connectionHandler.getName());
+  }
+
+  @Test
+  public void testStart() throws Exception {
+    ProxyServer server = mock(ProxyServer.class);
+    Socket socket = mock(Socket.class);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getLoopbackAddress());
+    Connection connection = mock(Connection.class);
+    ConnectionHandler connectionHandler = new ConnectionHandler(server, socket, connection);
+
+    assertThrows(IllegalStateException.class, connectionHandler::start);
+
+    SettableApiFuture<Boolean> started = SettableApiFuture.create();
+    Thread thread = new Thread(() -> started.set(true), "test-thread");
+    connectionHandler.setThread(thread);
+
+    connectionHandler.start();
+    assertTrue(started.get(10L, TimeUnit.SECONDS));
+  }
+
+  @Test
+  public void testAppendPropertiesToUrl() {
+    assertEquals(
+        "cloudspanner:/project/p/instances/i/databases/d",
+        appendPropertiesToUrl("cloudspanner:/project/p/instances/i/databases/d", new Properties()));
+    assertEquals(
+        "cloudspanner:/project/p/instances/i/databases/d",
+        appendPropertiesToUrl("cloudspanner:/project/p/instances/i/databases/d", null));
+    assertEquals(
+        "cloudspanner:/project/p/instances/i/databases/d",
+        appendPropertiesToUrl(
+            "cloudspanner:/project/p/instances/i/databases/d",
+            buildProperties(ImmutableMap.of("key", ""))));
+    assertEquals(
+        "cloudspanner:/project/p/instances/i/databases/d;key=value",
+        appendPropertiesToUrl(
+            "cloudspanner:/project/p/instances/i/databases/d",
+            buildProperties(ImmutableMap.of("key", "value"))));
+  }
+
+  @Test
+  public void testCheckValidConnection_External() throws Exception {
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    ProxyServer server = mock(ProxyServer.class);
+    when(server.getOptions()).thenReturn(options);
+    Socket socket = mock(Socket.class);
+    when(socket.getInetAddress()).thenReturn(InetAddress.getByName("google.com"));
+    Connection connection = mock(Connection.class);
+
+    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+    ConnectionMetadata connectionMetadata =
+        new ConnectionMetadata(new ByteArrayInputStream(new byte[0]), bos);
+
+    ConnectionHandler connectionHandler =
+        new ConnectionHandler(server, socket, connection) {
+          @Override
+          public ConnectionMetadata getConnectionMetadata() {
+            return connectionMetadata;
+          }
+        };
+    connectionHandler.setThread(new Thread(() -> {}, "test-thread"));
+
+    assertFalse(connectionHandler.checkValidConnection(false));
+    byte[] bytes = bos.toByteArray();
+    assertEquals(74, bytes.length);
+    assertEquals('E', bytes[0]); // Error
+    // The total length of the Error message is 68.
+    assertArrayEquals(new byte[] {0, 0, 0, 68}, Arrays.copyOfRange(bytes, 1, 5));
+    // The next message is a Terminate ('X') message.
+    assertEquals('X', bytes[69]);
+  }
+
+  @Test
+  public void testListDatabasesOrInstances() {
+    Spanner spanner = mock(Spanner.class);
+    DatabaseAdminClient adminClient = mock(DatabaseAdminClient.class);
+    when(spanner.getDatabaseAdminClient()).thenReturn(adminClient);
+    when(adminClient.listDatabases(eq("my-instance"), any())).thenReturn(Pages.empty());
+    StatusRuntimeException exception =
+        newStatusResourceNotFoundException(
+            "test-database",
+            "type.googleapis.com/google.spanner.admin.database.v1.Database",
+            "projects/my-project/instances/my-instance/databases/test-database");
+    ResourceNotFoundException resourceNotFoundException =
+        (ResourceNotFoundException) SpannerExceptionFactory.asSpannerException(exception);
+    assertEquals(
+        "Database projects/my-project/instances/my-instance/databases/test-database not found.\n"
+            + "\n"
+            + "These PostgreSQL databases are available on instance projects/my-project/instances/my-instance:\n",
+        listDatabasesOrInstances(
+            resourceNotFoundException,
+            DatabaseName.of("my-project", "my-instance", "test-database"),
+            spanner));
   }
 
   static Properties buildProperties(Map<String, String> map) {
@@ -755,4 +890,22 @@ public class ConnectionHandlerTest {
     properties.putAll(map);
     return properties;
   }
+
+  void runWithSystemProperty(String property, String value, Runnable runnable) {
+    String currentValue = System.getProperty(property);
+    try {
+      if (value == null) {
+        System.clearProperty(property);
+      } else {
+        System.setProperty(property, value);
+      }
+      runnable.run();
+    } finally {
+      if (currentValue == null) {
+        System.clearProperty(property);
+      } else {
+        System.setProperty(property, currentValue);
+      }
+    }
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/CursorMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/CursorMockServerTest.java
index 07ba17ce..9523b332 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/CursorMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/CursorMockServerTest.java
@@ -196,7 +196,7 @@ public class CursorMockServerTest extends AbstractMockServerTest {
     int totalRows = 0;
     mockSpanner.putStatementResult(
         StatementResult.query(
-            Statement.of("select * from random where some_col='some-value'"),
+            Statement.of("select * from random where some_col=('some-value')"),
             new RandomResultSetGenerator(numRows).generate()));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
index eb3606a9..5ec2f0c1 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/EmulatedPsqlMockServerTest.java
@@ -99,11 +99,16 @@ public class EmulatedPsqlMockServerTest extends AbstractMockServerTest {
    * recognize the connection as a psql connection.
    */
   private String createUrl(String database) {
+    return createUrl(database, "psql");
+  }
+
+  private String createUrl(String database, String applicationName) {
     return String.format(
-        "jdbc:postgresql://localhost:%d/%s?preferQueryMode=simple&%s=psql&%s=090000",
+        "jdbc:postgresql://localhost:%d/%s?preferQueryMode=simple&%s=%s&%s=090000",
         pgServer.getLocalPort(),
         database,
         PGProperty.APPLICATION_NAME.getName(),
+        applicationName,
         PGProperty.ASSUME_MIN_SERVER_VERSION.getName());
   }
 
@@ -210,37 +215,61 @@ public class EmulatedPsqlMockServerTest extends AbstractMockServerTest {
 
   @Test
   public void testConnectToNonExistingInstance() {
+    for (String appName : new String[] {"psql", "other-app"}) {
+      try {
+        mockSpanner.setExecuteStreamingSqlExecutionTime(
+            SimulatedExecutionTime.ofStickyException(
+                newStatusResourceNotFoundException(
+                    "i",
+                    "type.googleapis.com/google.spanner.admin.instance.v1.Instance",
+                    "projects/p/instances/i")));
+        // The Connection API calls listInstanceConfigs(..) once first when the connection is a
+        // localhost connection. It does so to verify that the connection is valid and to quickly
+        // return an error if someone is for example trying to connect to the emulator while the
+        // emulator is not running. This does not happen when you connect to a remote host. We
+        // therefore need to add a response for the listInstanceConfigs as well.
+        mockInstanceAdmin.addResponse(ListInstanceConfigsResponse.getDefaultInstance());
+        mockInstanceAdmin.addResponse(
+            ListInstancesResponse.newBuilder()
+                .addInstances(
+                    Instance.newBuilder()
+                        .setConfig("projects/p/instanceConfigs/ic")
+                        .setName("projects/p/instances/i")
+                        .build())
+                .build());
+
+        SQLException exception =
+            assertThrows(
+                SQLException.class,
+                () -> DriverManager.getConnection(createUrl("non-existing-db", appName)));
+        assertTrue(exception.getMessage(), exception.getMessage().contains("NOT_FOUND"));
+
+        boolean isPsql = "psql".equals(appName);
+        assertEquals(
+            exception.getMessage(),
+            isPsql,
+            exception.getMessage().contains("These instances are available in project p:"));
+        assertEquals(
+            exception.getMessage(),
+            isPsql,
+            exception.getMessage().contains("\tprojects/p/instances/i\n"));
+      } finally {
+        closeSpannerPool(true);
+      }
+    }
+  }
+
+  @Test
+  public void testConnectFailed() {
     try {
       mockSpanner.setExecuteStreamingSqlExecutionTime(
           SimulatedExecutionTime.ofStickyException(
-              newStatusResourceNotFoundException(
-                  "i",
-                  "type.googleapis.com/google.spanner.admin.instance.v1.Instance",
-                  "projects/p/instances/i")));
-      // The Connection API calls listInstanceConfigs(..) once first when the connection is a
-      // localhost connection. It does so to verify that the connection is valid and to quickly
-      // return an error if someone is for example trying to connect to the emulator while the
-      // emulator is not running. This does not happen when you connect to a remote host. We
-      // therefore need to add a response for the listInstanceConfigs as well.
-      mockInstanceAdmin.addResponse(ListInstanceConfigsResponse.getDefaultInstance());
-      mockInstanceAdmin.addResponse(
-          ListInstancesResponse.newBuilder()
-              .addInstances(
-                  Instance.newBuilder()
-                      .setConfig("projects/p/instanceConfigs/ic")
-                      .setName("projects/p/instances/i")
-                      .build())
-              .build());
-
+              Status.INVALID_ARGUMENT.withDescription("test error").asRuntimeException()));
       SQLException exception =
           assertThrows(
               SQLException.class, () -> DriverManager.getConnection(createUrl("non-existing-db")));
-      assertTrue(exception.getMessage(), exception.getMessage().contains("NOT_FOUND"));
-      assertTrue(
-          exception.getMessage(),
-          exception.getMessage().contains("These instances are available in project p:"));
-      assertTrue(
-          exception.getMessage(), exception.getMessage().contains("\tprojects/p/instances/i\n"));
+      assertTrue(exception.getMessage(), exception.getMessage().contains("test error"));
+
     } finally {
       closeSpannerPool(true);
     }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java
index 8d5365da..d050b210 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java
@@ -318,7 +318,12 @@ public class ITJdbcTest implements IntegrationTest {
           statement.setBytes(++index, "test".getBytes(StandardCharsets.UTF_8));
         }
         statement.setDouble(++index, 3.14d);
-        statement.setInt(++index, 1);
+        // TODO: Remove when Spangres supports casting to int4
+        if (isSimpleMode) {
+          statement.setLong(++index, 1);
+        } else {
+          statement.setInt(++index, 1);
+        }
         statement.setBigDecimal(++index, new BigDecimal("3.14"));
         statement.setTimestamp(
             ++index, Timestamp.parseTimestamp("2022-01-27T17:51:30+01:00").toSqlTimestamp());
@@ -382,9 +387,12 @@ public class ITJdbcTest implements IntegrationTest {
           assertArrayEquals(
               new String[] {"string1", null, "string2"},
               (String[]) resultSet.getArray(++index).getArray());
-          assertArrayEquals(
-              new String[] {"{\"key\": \"value1\"}", null, "{\"key\": \"value2\"}"},
-              (String[]) resultSet.getArray(++index).getArray());
+          // TODO: Remove when Spangres supports casting to int4
+          if (!isSimpleMode) {
+            assertArrayEquals(
+                new String[] {"{\"key\": \"value1\"}", null, "{\"key\": \"value2\"}"},
+                (String[]) resultSet.getArray(++index).getArray());
+          }
 
           assertFalse(resultSet.next());
         }
@@ -412,7 +420,12 @@ public class ITJdbcTest implements IntegrationTest {
           statement.setBytes(++index, "bytes_test".getBytes(StandardCharsets.UTF_8));
         }
         statement.setDouble(++index, 10.1);
-        statement.setInt(++index, 100);
+        // TODO: Remove when the emulator supports casting to int4
+        if (isSimpleMode) {
+          statement.setLong(++index, 100);
+        } else {
+          statement.setInt(++index, 100);
+        }
         statement.setBigDecimal(++index, new BigDecimal("6.626"));
         statement.setTimestamp(
             ++index, Timestamp.parseTimestamp("2022-02-11T13:45:00.123456+01:00").toSqlTimestamp());
@@ -438,7 +451,11 @@ public class ITJdbcTest implements IntegrationTest {
         }
         statement.setArray(
             ++index, connection.createArrayOf("float8", new Double[] {3.14d, null, -99.8}));
-        statement.setArray(++index, connection.createArrayOf("int", new Integer[] {-1, null, -2}));
+        // TODO: Remove when Spangres supports casting to int4
+        statement.setArray(
+            ++index,
+            connection.createArrayOf(
+                isSimpleMode ? "bigint" : "int", new Integer[] {-1, null, -2}));
         statement.setArray(
             ++index,
             connection.createArrayOf(
@@ -461,15 +478,28 @@ public class ITJdbcTest implements IntegrationTest {
         statement.setArray(
             ++index,
             connection.createArrayOf("varchar", new String[] {"string1", null, "string2"}));
-        statement.setArray(
-            ++index,
-            connection.createArrayOf(
-                "jsonb",
-                new String[] {
-                  "{\"key1\": \"value1\", \"key2\": \"value2\"}",
-                  null,
-                  "{\"key1\": \"value3\", \"key2\": \"value4\"}"
-                }));
+        // TODO: Remove when the emulator supports casting to int4
+        if (!isSimpleMode) {
+          statement.setArray(
+              ++index,
+              connection.createArrayOf(
+                  "jsonb",
+                  new String[] {
+                    "{\"key1\": \"value1\", \"key2\": \"value2\"}",
+                    null,
+                    "{\"key1\": \"value3\", \"key2\": \"value4\"}"
+                  }));
+        } else {
+          statement.setArray(
+              ++index,
+              connection.createArrayOf(
+                  "varchar",
+                  new String[] {
+                    "{\"key1\": \"value1\", \"key2\": \"value2\"}",
+                    null,
+                    "{\"key1\": \"value3\", \"key2\": \"value4\"}"
+                  }));
+        }
 
         assertEquals(1, statement.executeUpdate());
       }
@@ -545,13 +575,16 @@ public class ITJdbcTest implements IntegrationTest {
         assertArrayEquals(
             new String[] {"string1", null, "string2"},
             (String[]) resultSet.getArray(++index).getArray());
-        assertArrayEquals(
-            new String[] {
-              "{\"key1\": \"value1\", \"key2\": \"value2\"}",
-              null,
-              "{\"key1\": \"value3\", \"key2\": \"value4\"}"
-            },
-            (String[]) resultSet.getArray(++index).getArray());
+        // TODO: Remove when Spangres supports casting to int4
+        if (!isSimpleMode) {
+          assertArrayEquals(
+              new String[] {
+                "{\"key1\": \"value1\", \"key2\": \"value2\"}",
+                null,
+                "{\"key1\": \"value3\", \"key2\": \"value4\"}"
+              },
+              (String[]) resultSet.getArray(++index).getArray());
+        }
 
         assertFalse(resultSet.next());
       }
@@ -583,7 +616,12 @@ public class ITJdbcTest implements IntegrationTest {
           statement.setBytes(++index, "updated".getBytes(StandardCharsets.UTF_8));
         }
         statement.setDouble(++index, 3.14d * 2d);
-        statement.setInt(++index, 2);
+        // TODO: Remove when Spangres supports casting to int4
+        if (isSimpleMode) {
+          statement.setLong(++index, 2);
+        } else {
+          statement.setInt(++index, 2);
+        }
         statement.setBigDecimal(++index, new BigDecimal("10.0"));
         // Note that PostgreSQL does not support nanosecond precision, so the JDBC driver therefore
         // truncates this value before it is sent to PG.
@@ -727,7 +765,7 @@ public class ITJdbcTest implements IntegrationTest {
             assertThrows(PSQLException.class, preparedStatement::executeQuery);
         if (preferQueryMode.equals("simple")) {
           assertEquals(
-              "ERROR: relation \"non_existing_table\" does not exist - Statement: 'select * from non_existing_table where id=1'",
+              "ERROR: relation \"non_existing_table\" does not exist - Statement: 'select * from non_existing_table where id=('1'::int8)'",
               exception.getMessage());
         } else {
           assertEquals(
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
index b5dc15f9..0d47a4d7 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITPsqlTest.java
@@ -927,6 +927,8 @@ public class ITPsqlTest implements IntegrationTest {
           ZoneId.of("Asia/Amman"),
           // Iran observed DST in 1978. Not all databases agree on this.
           ZoneId.of("Asia/Tehran"),
+          // Changed the use of DST multiple times. Changed offset on 2024-02-29.
+          ZoneId.of("Asia/Almaty"),
           // Rankin_Inlet and Resolute did not observe DST in 1970-1979, but not all databases
           // agree.
           ZoneId.of("America/Rankin_Inlet"),
@@ -942,6 +944,9 @@ public class ITPsqlTest implements IntegrationTest {
           // Nuuk stopped using DST in 2023. This is unknown to older JDKs.
           ZoneId.of("America/Nuuk"),
           ZoneId.of("America/Godthab"),
+          // Antarctica has multiple differences between databases.
+          ZoneId.of("Antarctica/Vostok"),
+          ZoneId.of("Antarctica/Casey"),
           // Egypt has started using DST again from 2023.
           ZoneId.of("Egypt"));
 
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 89d23906..3b1ca19e 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -4232,6 +4232,106 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testSelectSetConfigTimezone() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (ResultSet resultSet =
+          connection
+              .createStatement()
+              .executeQuery("select set_config('timezone', 'ist', false)")) {
+        assertTrue(resultSet.next());
+        assertEquals("ist", resultSet.getString("set_config"));
+        assertFalse(resultSet.next());
+      }
+      verifySettingValue(connection, "timezone", "Asia/Kolkata");
+    }
+  }
+
+  @Test
+  public void testSelectSetConfigInvalidTimezone() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      PSQLException exception =
+          assertThrows(
+              PSQLException.class,
+              () ->
+                  connection
+                      .createStatement()
+                      .executeQuery(
+                          "select set_config('timezone', 'non-existent-timezone', false)"));
+      assertNotNull(exception.getServerErrorMessage());
+      assertEquals(
+          exception.getServerErrorMessage().getSQLState(), SQLState.RaiseException.toString());
+    }
+  }
+
+  @Test
+  public void testSelectCurrentSettingTimezone() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'IST'");
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("select current_setting('timezone')")) {
+        assertTrue(resultSet.next());
+        assertEquals("Asia/Kolkata", resultSet.getString("current_setting"));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  public void testSelectCurrentSettingInvalidName() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      PSQLException exception =
+          assertThrows(
+              PSQLException.class,
+              () ->
+                  connection
+                      .createStatement()
+                      .executeQuery("select current_setting('invalid-setting-name')"));
+      assertEquals(SQLState.SyntaxError.toString(), exception.getSQLState());
+    }
+  }
+
+  @Test
+  public void testSelectCurrentSettingMissingNotOk() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      PSQLException exception =
+          assertThrows(
+              PSQLException.class,
+              () ->
+                  connection
+                      .createStatement()
+                      .executeQuery("select current_setting('non_existing_setting')"));
+      assertEquals(SQLState.RaiseException.toString(), exception.getSQLState());
+    }
+  }
+
+  @Test
+  public void testSelectCurrentSettingMissingOk_settingIsMissing() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (ResultSet resultSet =
+          connection
+              .createStatement()
+              .executeQuery("select current_setting('non_existing_setting', true)")) {
+        assertTrue(resultSet.next());
+        assertNull(resultSet.getString("current_setting"));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  public void testSelectCurrentSettingMissingOk_settingIsPresent() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'IST'");
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("select current_setting('timezone', true)")) {
+        assertTrue(resultSet.next());
+        assertEquals("Asia/Kolkata", resultSet.getString("current_setting"));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
   @Test
   public void testSetNames() throws SQLException {
     try (Connection connection = DriverManager.getConnection(createUrl())) {
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
index 8eca2385..d4a95591 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcSimpleModeMockServerTest.java
@@ -48,7 +48,6 @@ import java.util.List;
 import java.util.TimeZone;
 import java.util.stream.Collectors;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -396,15 +395,17 @@ public class JdbcSimpleModeMockServerTest extends AbstractMockServerTest {
             .timeToString(java.sql.Timestamp.from(Instant.from(zonedDateTime)), true);
 
     String pgSql =
-        "select col_bigint, col_bool, col_bytea, col_float8, col_numeric, col_timestamptz, col_varchar, col_jsonb "
-            + "from all_types "
-            + "where col_bigint=1 "
-            + "and col_bool='TRUE' "
-            + "and col_float8=3.14 "
-            + "and col_numeric=6.626 "
-            + String.format("and col_timestamptz='%s' ", timestampString)
-            + "and col_varchar='test' "
-            + "and col_jsonb='{\"key\": \"value\"}'";
+        String.format(
+            "select col_bigint, col_bool, col_bytea, col_float8, col_numeric, col_timestamptz, col_varchar, col_jsonb "
+                + "from all_types "
+                + "where col_bigint=('1'::int8) "
+                + "and col_bool=('TRUE') "
+                + "and col_float8=('3.14'::double precision) "
+                + "and col_numeric=('6.626'::numeric) "
+                + "and col_timestamptz=('%s') "
+                + "and col_varchar=('test') "
+                + "and col_jsonb=('{\"key\": \"value\"}')",
+            timestampString);
     String jdbcSql =
         "select col_bigint, col_bool, col_bytea, col_float8, col_numeric, col_timestamptz, col_varchar, col_jsonb "
             + "from all_types "
@@ -419,6 +420,7 @@ public class JdbcSimpleModeMockServerTest extends AbstractMockServerTest {
         StatementResult.query(com.google.cloud.spanner.Statement.of(pgSql), ALL_TYPES_RESULTSET));
 
     try (Connection connection = DriverManager.getConnection(createUrl())) {
+      connection.createStatement().execute("set time zone 'utc'");
       try (PreparedStatement preparedStatement = connection.prepareStatement(jdbcSql)) {
         int index = 0;
         preparedStatement.setLong(++index, 1L);
@@ -757,7 +759,6 @@ public class JdbcSimpleModeMockServerTest extends AbstractMockServerTest {
     assertEquals(0, requests.size());
   }
 
-  @Ignore("https://github.com/pgjdbc/pgjdbc/issues/3007")
   @Test
   public void testImplicitBatchOfClientSideStatements() throws SQLException {
     String sql = "set statement_timeout = '10s'; " + "show statement_timeout; ";
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadataTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadataTest.java
index 5bd82644..8d76a12c 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadataTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadataTest.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import com.google.auth.oauth2.AccessToken;
 import com.google.auth.oauth2.OAuth2Credentials;
@@ -123,6 +124,12 @@ public class OptionsMetadataTest {
 
   @Test
   public void testBuildConnectionUrlWithFullPath() {
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME) == null);
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME)
+            == null);
+
     assertEquals(
         "cloudspanner:/projects/test-project/instances/test-instance/databases/test-database;userAgent=pg-adapter;credentials=credentials.json",
         new OptionsMetadata(new String[] {"-c", "credentials.json"})
@@ -157,6 +164,12 @@ public class OptionsMetadataTest {
 
   @Test
   public void testBuildConnectionUrlWithDefaultProjectId() {
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME) == null);
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME)
+            == null);
+
     OptionsMetadata useDefaultProjectIdOptions =
         new OptionsMetadata(new String[] {"-i", "test-instance", "-c", "credentials.json"}) {
           @Override
@@ -182,6 +195,12 @@ public class OptionsMetadataTest {
 
   @Test
   public void testBuildConnectionUrlWithDefaultCredentials() {
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME) == null);
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME)
+            == null);
+
     OptionsMetadata useDefaultCredentials =
         new OptionsMetadata(new String[] {"-p", "test-project", "-i", "test-instance"}) {
           @Override
@@ -299,6 +318,12 @@ public class OptionsMetadataTest {
 
   @Test
   public void testBuilder() {
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_THREADS_SYSTEM_PROPERTY_NAME) == null);
+    assumeTrue(
+        System.getProperty(OptionsMetadata.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_SYSTEM_PROPERTY_NAME)
+            == null);
+
     assertFalse(
         OptionsMetadata.newBuilder()
             .setProject("my-project")
@@ -398,6 +423,41 @@ public class OptionsMetadataTest {
             .build()
             .getPropertyMap()
             .get("usePlainText"));
+    assertNull(
+        OptionsMetadata.newBuilder()
+            .setCredentials(NoCredentials.getInstance())
+            .build()
+            .getPropertyMap()
+            .get("useVirtualThreads"));
+    assertEquals(
+        "true",
+        OptionsMetadata.newBuilder()
+            .useVirtualThreads()
+            .setCredentials(NoCredentials.getInstance())
+            .build()
+            .getPropertyMap()
+            .get("useVirtualThreads"));
+    assertNull(
+        OptionsMetadata.newBuilder()
+            .setCredentials(NoCredentials.getInstance())
+            .build()
+            .getPropertyMap()
+            .get("useVirtualGrpcTransportThreads"));
+    assertEquals(
+        "true",
+        OptionsMetadata.newBuilder()
+            .useVirtualGrpcTransportThreads()
+            .setCredentials(NoCredentials.getInstance())
+            .build()
+            .getPropertyMap()
+            .get("useVirtualGrpcTransportThreads"));
+    assertFalse(OptionsMetadata.newBuilder().build().isUseGrpcTransportVirtualThreads());
+    assertTrue(
+        OptionsMetadata.newBuilder()
+            .useVirtualGrpcTransportThreads()
+            .build()
+            .isUseGrpcTransportVirtualThreads());
+
     assertEquals(
         DdlTransactionMode.Batch,
         OptionsMetadata.newBuilder()
@@ -530,13 +590,13 @@ public class OptionsMetadataTest {
   @Test
   public void testBuildConnectionUrlWithEmulator() {
     assertEquals(
-        "cloudspanner:/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter",
+        "cloudspanner://localhost:9010/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter;usePlainText=true",
         OptionsMetadata.newBuilder()
             .setEnvironment(ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9010"))
             .build()
             .buildConnectionURL("projects/my-project/instances/my-instance/databases/my-database"));
     assertEquals(
-        "cloudspanner:/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter",
+        "cloudspanner://localhost:9010/projects/my-project/instances/my-instance/databases/my-database;userAgent=pg-adapter;usePlainText=true",
         OptionsMetadata.newBuilder()
             .setRequireAuthentication()
             .setEnvironment(ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9010"))
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/python/sqlalchemy2/SqlAlchemy2SampleTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/python/sqlalchemy2/SqlAlchemy2SampleTest.java
index 647a6a06..fbec6888 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/python/sqlalchemy2/SqlAlchemy2SampleTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/python/sqlalchemy2/SqlAlchemy2SampleTest.java
@@ -343,11 +343,31 @@ public class SqlAlchemy2SampleTest extends AbstractMockServerTest {
                         .addAllValues(singerValues)
                         .build())
                 .build()));
+    String ticketSalesSql =
+        "SELECT ticket_sales.concert_id AS ticket_sales_concert_id, ticket_sales.customer_name AS ticket_sales_customer_name, ticket_sales.price AS ticket_sales_price, ticket_sales.seats AS ticket_sales_seats, ticket_sales.id AS ticket_sales_id, ticket_sales.version_id AS ticket_sales_version_id, ticket_sales.created_at AS ticket_sales_created_at, ticket_sales.updated_at AS ticket_sales_updated_at \n"
+            + "FROM ticket_sales \n"
+            + "WHERE $1::VARCHAR = ticket_sales.concert_id";
+    mockSpanner.putPartialStatementResult(
+        StatementResult.query(
+            Statement.of(ticketSalesSql),
+            ResultSet.newBuilder()
+                .setMetadata(createTicketSalesMetadata("ticket_sales_"))
+                .addRows(
+                    createTicketSaleRow(
+                        1L,
+                        "c1",
+                        "Alice",
+                        "99.99",
+                        Timestamp.parseTimestamp("2022-12-02T17:30:00Z"),
+                        Timestamp.parseTimestamp("2022-12-02T17:30:00Z")))
+                .build()));
+
     String output =
         execute(SAMPLE_DIR, "test_print_concerts.py", "localhost", pgServer.getLocalPort());
     assertEquals(
         "\n"
-            + "Concert 'Avenue Park Open' starting at 2023-02-02T01:00:00+00:00 with Pete Allison will be held at Avenue Park\n",
+            + "Concert 'Avenue Park Open' starting at 2023-02-02T01:00:00+00:00 with Pete Allison will be held at Avenue Park\n"
+            + "  Ticket sold to Alice for seats ['A10']\n",
         output);
   }
 
@@ -420,6 +440,97 @@ public class SqlAlchemy2SampleTest extends AbstractMockServerTest {
             Statement.of(
                 "INSERT INTO concerts (name, venue_id, singer_id, start_time, end_time, id, version_id, created_at, updated_at) "),
             1L));
+    String insertTicketSaleSql =
+        "INSERT INTO ticket_sales (concert_id, customer_name, price, seats, version_id, created_at, updated_at) "
+            + "VALUES ($1::VARCHAR, $2::VARCHAR, $3, $4, $5::INTEGER, $6::TIMESTAMP WITH TIME ZONE, $7::TIMESTAMP WITH TIME ZONE) "
+            + "RETURNING ticket_sales.id";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(insertTicketSaleSql),
+            ResultSet.newBuilder()
+                .setMetadata(
+                    ResultSetMetadata.newBuilder()
+                        .setRowType(
+                            StructType.newBuilder()
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("id")
+                                        .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                                        .build())
+                                .build())
+                        .setUndeclaredParameters(
+                            StructType.newBuilder()
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p1")
+                                        .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p2")
+                                        .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p3")
+                                        .setType(
+                                            Type.newBuilder().setCode(TypeCode.NUMERIC).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p4")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.STRING)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p5")
+                                        .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p6")
+                                        .setType(
+                                            Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p7")
+                                        .setType(
+                                            Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+                                        .build())
+                                .build())
+                        .build())
+                .setStats(ResultSetStats.newBuilder().build())
+                .build()));
+    mockSpanner.putPartialStatementResult(
+        StatementResult.query(
+            Statement.of(
+                "INSERT INTO ticket_sales (concert_id, customer_name, price, seats, version_id, created_at, updated_at) "),
+            ResultSet.newBuilder()
+                .setMetadata(
+                    ResultSetMetadata.newBuilder()
+                        .setRowType(
+                            StructType.newBuilder()
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("id")
+                                        .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                                        .build())
+                                .build())
+                        .build())
+                .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+                .addRows(
+                    ListValue.newBuilder()
+                        .addValues(Value.newBuilder().setStringValue("1").build())
+                        .build())
+                .build()));
 
     String output =
         execute(
@@ -1087,6 +1198,7 @@ public class SqlAlchemy2SampleTest extends AbstractMockServerTest {
         output);
   }
 
+  @Ignore("Requires support for array casting/coercion")
   @Test
   public void testCreateDataModel() throws Exception {
     String checkTableExistsSql =
@@ -1228,6 +1340,27 @@ public class SqlAlchemy2SampleTest extends AbstractMockServerTest {
             ResultSet.newBuilder()
                 .setMetadata(createMetadata(ImmutableList.of(TypeCode.STRING)))
                 .build()));
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.newBuilder(checkTableExistsSql)
+                .bind("p1")
+                .to("ticket_sales")
+                .bind("p2")
+                .to("r")
+                .bind("p3")
+                .to("p")
+                .bind("p4")
+                .to("f")
+                .bind("p5")
+                .to("v")
+                .bind("p6")
+                .to("m")
+                .bind("p7")
+                .to("pg_catalog")
+                .build(),
+            ResultSet.newBuilder()
+                .setMetadata(createMetadata(ImmutableList.of(TypeCode.STRING)))
+                .build()));
     addDdlResponseToSpannerAdmin();
 
     String actualOutput =
@@ -1241,7 +1374,7 @@ public class SqlAlchemy2SampleTest extends AbstractMockServerTest {
             .map(req -> (UpdateDatabaseDdlRequest) req)
             .collect(Collectors.toList());
     assertEquals(1, requests.size());
-    assertEquals(5, requests.get(0).getStatementsCount());
+    assertEquals(6, requests.get(0).getStatementsCount());
     assertEquals(
         "CREATE TABLE singers (\n"
             + "\tfirst_name VARCHAR(100), \n"
@@ -1312,6 +1445,20 @@ public class SqlAlchemy2SampleTest extends AbstractMockServerTest {
             + "\tFOREIGN KEY(id) REFERENCES albums (id)\n"
             + ")",
         requests.get(0).getStatements(4));
+    assertEquals(
+        "CREATE TABLE ticket_sales (\n"
+            + "\tconcert_id VARCHAR, \n"
+            + "\tcustomer_name VARCHAR, \n"
+            + "\tprice NUMERIC, \n"
+            + "\tseats VARCHAR[], \n"
+            + "\tid SERIAL NOT NULL, \n"
+            + "\tversion_id INTEGER NOT NULL, \n"
+            + "\tcreated_at TIMESTAMP WITH TIME ZONE, \n"
+            + "\tupdated_at TIMESTAMP WITH TIME ZONE, \n"
+            + "\tPRIMARY KEY (id), \n"
+            + "\tFOREIGN KEY(concert_id) REFERENCES concerts (id)\n"
+            + ")",
+        requests.get(0).getStatements(5));
   }
 
   @Ignore("Uses too much pg_catalog tables")
@@ -1573,6 +1720,88 @@ public class SqlAlchemy2SampleTest extends AbstractMockServerTest {
         .build();
   }
 
+  static ResultSetMetadata createTicketSalesMetadata(String prefix) {
+    return ResultSetMetadata.newBuilder()
+        .setRowType(
+            StructType.newBuilder()
+                .addFields(
+                    Field.newBuilder()
+                        .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+                        .setName(prefix + "concert_id")
+                        .build())
+                .addFields(
+                    Field.newBuilder()
+                        .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+                        .setName(prefix + "customer_name")
+                        .build())
+                .addFields(
+                    Field.newBuilder()
+                        .setType(
+                            Type.newBuilder()
+                                .setCode(TypeCode.NUMERIC)
+                                .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC)
+                                .build())
+                        .setName(prefix + "price")
+                        .build())
+                .addFields(
+                    Field.newBuilder()
+                        .setType(
+                            Type.newBuilder()
+                                .setCode(TypeCode.ARRAY)
+                                .setArrayElementType(
+                                    Type.newBuilder().setCode(TypeCode.STRING).build())
+                                .build())
+                        .setName(prefix + "seats")
+                        .build())
+                .addFields(
+                    Field.newBuilder()
+                        .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                        .setName(prefix + "id")
+                        .build())
+                .addFields(
+                    Field.newBuilder()
+                        .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                        .setName(prefix + "version_id")
+                        .build())
+                .addFields(
+                    Field.newBuilder()
+                        .setType(Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+                        .setName(prefix + "created_at")
+                        .build())
+                .addFields(
+                    Field.newBuilder()
+                        .setType(Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+                        .setName(prefix + "updated_at")
+                        .build())
+                .build())
+        .build();
+  }
+
+  static ListValue createTicketSaleRow(
+      long id,
+      String concertId,
+      String customerName,
+      String price,
+      Timestamp createdAt,
+      Timestamp updatedAt) {
+    return ListValue.newBuilder()
+        .addValues(Value.newBuilder().setStringValue(concertId).build())
+        .addValues(Value.newBuilder().setStringValue(customerName).build())
+        .addValues(Value.newBuilder().setStringValue(price).build())
+        .addValues(
+            Value.newBuilder()
+                .setListValue(
+                    ListValue.newBuilder()
+                        .addValues(Value.newBuilder().setStringValue("A10").build())
+                        .build())
+                .build())
+        .addValues(Value.newBuilder().setStringValue(String.valueOf(id)).build())
+        .addValues(Value.newBuilder().setStringValue("1").build())
+        .addValues(Value.newBuilder().setStringValue(createdAt.toString()).build())
+        .addValues(Value.newBuilder().setStringValue(updatedAt.toString()).build())
+        .build();
+  }
+
   static ResultSetMetadata createConcertsMetadata(String prefix) {
     return ResultSetMetadata.newBuilder()
         .setRowType(
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/SelectCurrentSettingStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SelectCurrentSettingStatementTest.java
new file mode 100644
index 00000000..6dfbd328
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SelectCurrentSettingStatementTest.java
@@ -0,0 +1,55 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SelectCurrentSettingStatement.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SelectCurrentSettingStatementTest {
+
+  @Test
+  public void testParse() {
+    assertEquals("foo", parse("select current_setting('foo')").name);
+    assertFalse(parse("select current_setting('foo')").missingOk);
+    assertFalse(parse("select current_setting('foo', off)").missingOk);
+    assertTrue(parse("select current_setting('foo', t)").missingOk);
+
+    assertEquals(
+        "spanner", parse("select current_setting('spanner.autocommit_dml_mode')").extension);
+    assertEquals(
+        "autocommit_dml_mode", parse("select current_setting('spanner.autocommit_dml_mode')").name);
+
+    assertThrows(PGException.class, () -> parse("select"));
+    assertThrows(PGException.class, () -> parse("select foo"));
+    assertThrows(PGException.class, () -> parse("select current_setting"));
+    assertThrows(PGException.class, () -> parse("select current_setting ("));
+    assertThrows(PGException.class, () -> parse("select current_setting ('foo', 'bar')"));
+    assertThrows(PGException.class, () -> parse("select current_setting ('foo', true, true)"));
+    assertThrows(PGException.class, () -> parse("select current_setting ('foo', true"));
+    assertThrows(PGException.class, () -> parse("select current_setting ('foo', true, 1)"));
+    assertThrows(PGException.class, () -> parse("select current_setting ('foo', true), 1"));
+    assertThrows(PGException.class, () -> parse("select current_setting ('foo.bar.baz', true)"));
+    assertThrows(PGException.class, () -> parse("select current_setting('foo-bar')"));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/SelectSetConfigStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SelectSetConfigStatementTest.java
new file mode 100644
index 00000000..71be39f3
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/SelectSetConfigStatementTest.java
@@ -0,0 +1,63 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static com.google.cloud.spanner.pgadapter.statements.SelectSetConfigStatement.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.spanner.pgadapter.error.PGException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SelectSetConfigStatementTest {
+
+  @Test
+  public void testParse() {
+    assertEquals("foo", parse("select set_config('foo', 'bar', false)").name);
+    assertEquals("bar", parse("select set_config('foo', 'bar', false)").value);
+    assertFalse(parse("select set_config('foo', 'bar', false)").local);
+    assertTrue(parse("select set_config('foo', 'bar', 1)").local);
+
+    assertEquals(
+        "spanner",
+        parse("select set_config('spanner.autocommit_dml_mode', 'partitioned_non_atomic', false)")
+            .extension);
+    assertEquals(
+        "autocommit_dml_mode",
+        parse("select set_config('spanner.autocommit_dml_mode', 'partitioned_non_atomic', false)")
+            .name);
+    assertEquals(
+        "partitioned_non_atomic",
+        parse("select set_config('spanner.autocommit_dml_mode', 'partitioned_non_atomic', false)")
+            .value);
+
+    assertThrows(PGException.class, () -> parse("select"));
+    assertThrows(PGException.class, () -> parse("select foo"));
+    assertThrows(PGException.class, () -> parse("select set_config"));
+    assertThrows(PGException.class, () -> parse("select set_config ("));
+    assertThrows(PGException.class, () -> parse("select set_config ('foo')"));
+    assertThrows(PGException.class, () -> parse("select set_config ('foo', 'bar')"));
+    assertThrows(PGException.class, () -> parse("select set_config ('foo', true, true)"));
+    assertThrows(PGException.class, () -> parse("select set_config ('foo', 'bar', true"));
+    assertThrows(PGException.class, () -> parse("select set_config ('foo', 'bar', true, 1)"));
+    assertThrows(PGException.class, () -> parse("select set_config ('foo', 'bar', true), 1"));
+    assertThrows(PGException.class, () -> parse("select set_config ('foo.bar.baz', 'bar', true)"));
+  }
+}