Repository: GoogleCloudPlatform/pgadapter
Pull request #215 (2022-06-17 15:23:06)

feat: support \l meta command
Adds support for the `\l` psql meta command to list all databases on the same 'server'. The meta command behaves as follows in PGAdapter:
1. If PGAdapter has been started with `-d` (a fixed database) it will only show the current database, as no other database can be 'reached' by using `\c`.
2. If `-d` has not been specified, PGAdapter shows all the databases on the same instance as the current connection.
3. The databases in step 2 are shown with only the database id if the current connection is to the default instance of this instance of PGAdapter. Otherwise, the fully qualified name is shown.

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#215](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (f15847d) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/1392a7af55a40f58ca287bf0ec537994b097f807?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (1392a7a) will **decrease** coverage by `1.71%`.
> The diff coverage is `16.66%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #215      +/-   ##
========================================================
- Coverage                 79.73%   78.02%   -1.72%     
- Complexity                 1041     1042       +1     
========================================================
  Files                        91       92       +1     
  Lines                      3543     3631      +88     
  Branches                    423      428       +5     
========================================================
+ Hits                       2825     2833       +8     
- Misses                      553      631      +78     
- Partials                    165      167       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `78.02% <16.66%> (-1.72%)` | :arrow_down: |

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

| [Impacted Files](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...ud/spanner/connection/ConnectionOptionsHelper.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvY29ubmVjdGlvbi9Db25uZWN0aW9uT3B0aW9uc0hlbHBlci5qYXZh) | `33.33% <0.00%> (-16.67%)` | :arrow_down: |
| [...ud/spanner/pgadapter/metadata/OptionsMetadata.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL21ldGFkYXRhL09wdGlvbnNNZXRhZGF0YS5qYXZh) | `79.22% <0.00%> (-3.97%)` | :arrow_down: |
| [...apter/statements/local/ListDatabasesStatement.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvbG9jYWwvTGlzdERhdGFiYXNlc1N0YXRlbWVudC5qYXZh) | `0.00% <0.00%> (ø)` | |
| [...panner/pgadapter/statements/BackendConnection.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvQmFja2VuZENvbm5lY3Rpb24uamF2YQ==) | `86.08% <23.80%> (-6.34%)` | :arrow_down: |
| [...ud/spanner/pgadapter/utils/ClientAutoDetector.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL0NsaWVudEF1dG9EZXRlY3Rvci5qYXZh) | `93.33% <66.66%> (-2.97%)` | :arrow_down: |
| [...gle/cloud/spanner/pgadapter/ConnectionHandler.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL0Nvbm5lY3Rpb25IYW5kbGVyLmphdmE=) | `76.38% <75.00%> (-0.14%)` | :arrow_down: |
| [...ogle/cloud/spanner/pgadapter/commands/Command.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL2NvbW1hbmRzL0NvbW1hbmQuamF2YQ==) | `100.00% <100.00%> (ø)` | |
| [.../cloud/spanner/pgadapter/commands/ListCommand.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL2NvbW1hbmRzL0xpc3RDb21tYW5kLmphdmE=) | `100.00% <100.00%> (ø)` | |
| [...apter/statements/ExtendedQueryProtocolHandler.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvRXh0ZW5kZWRRdWVyeVByb3RvY29sSGFuZGxlci5qYXZh) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform). Last update [1392a7a...f15847d](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/215?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform).


Patch:
diff --git a/.ci/evaluate-with-psql.sh b/.ci/evaluate-with-psql.sh
index 1699b3b4..5a14e708 100644
--- a/.ci/evaluate-with-psql.sh
+++ b/.ci/evaluate-with-psql.sh
@@ -7,6 +7,7 @@ sed -i "s/testdb_e2e_psql_v../$GOOGLE_CLOUD_DATABASE_WITH_VERSION/" .ci/e2e-expe
 sed -i "s/testdb_e2e_psql_v../$GOOGLE_CLOUD_DATABASE_WITH_VERSION/" .ci/e2e-expected/backslash-di-param.txt
 sed -i "s/testdb_e2e_psql_v../$GOOGLE_CLOUD_DATABASE_WITH_VERSION/" .ci/e2e-expected/backslash-dn.txt
 sed -i "s/testdb_e2e_psql_v../$GOOGLE_CLOUD_DATABASE_WITH_VERSION/" .ci/e2e-expected/backslash-dn-param.txt
+sed -i "s/testdb_e2e_psql_v../$GOOGLE_CLOUD_DATABASE_WITH_VERSION/" .ci/e2e-expected/backslash-l.txt
 # remove --- hyphenated formatting lines for diff comparison
 sed -i "s/---[-]*//g" .ci/e2e-expected/backslash-dt.txt
 sed -i "s/---[-]*//g" .ci/e2e-expected/backslash-dt-param.txt
@@ -14,6 +15,7 @@ sed -i "s/---[-]*//g" .ci/e2e-expected/backslash-di.txt
 sed -i "s/---[-]*//g" .ci/e2e-expected/backslash-di-param.txt
 sed -i "s/---[-]*//g" .ci/e2e-expected/backslash-dn.txt
 sed -i "s/---[-]*//g" .ci/e2e-expected/backslash-dn-param.txt
+sed -i "s/---[-]*//g" .ci/e2e-expected/backslash-l.txt
 
 echo "------Test \"select 1::int8\"------"
 echo "select 1::int8;" | /usr/lib/postgresql/"${PSQL_VERSION}"/bin/psql -h /tmp -p 4242 -d "${GOOGLE_CLOUD_DATABASE_WITH_VERSION}" > .ci/e2e-result/select-one.txt
@@ -75,6 +77,14 @@ sed -i "s/---[-]*//g" .ci/e2e-result/backslash-dn-param.txt
 diff -i -w -s .ci/e2e-result/backslash-dn-param.txt .ci/e2e-expected/backslash-dn-param.txt
 RETURN_CODE=$((${RETURN_CODE}||$?))
 
+echo "------Test \"\\l\"------"
+echo "\l" | /usr/lib/postgresql/"${PSQL_VERSION}"/bin/psql -h localhost -p 4242 -d "${GOOGLE_CLOUD_DATABASE_WITH_VERSION}" > .ci/e2e-result/backslash-l.txt
+# ignore effective_timestamp
+sed -i -E 's/[0-9]{16,}//' .ci/e2e-result/backslash-l.txt
+sed -i "s/---[-]*//g" .ci/e2e-result/backslash-l.txt
+diff -i -w -s .ci/e2e-result/backslash-l.txt .ci/e2e-expected/backslash-l.txt
+RETURN_CODE=$((${RETURN_CODE}||$?))
+
 echo "------Test \"-c option dml batching\"------"
 /usr/lib/postgresql/"${PSQL_VERSION}"/bin/psql -h localhost -p 4242 -d "${GOOGLE_CLOUD_DATABASE_WITH_VERSION}" -c "$(cat .ci/e2e-batching/dml-batch.txt)" > .ci/e2e-result/dml-batching.txt
 diff -i -w -s .ci/e2e-result/dml-batching.txt .ci/e2e-expected/dml-batching.txt
diff --git a/.github/release-please.yml b/.github/release-please.yml
index fbc85207..bdafab20 100644
--- a/.github/release-please.yml
+++ b/.github/release-please.yml
@@ -2,9 +2,4 @@ primaryBranch: postgresql-dialect
 releaseType: java-yoshi
 bumpMinorPreMajor: true
 handleGHRelease: true
-extraFiles: [
-  {
-    type: "generic",
-    path: "README.md",
-  }
-]
+extraFiles: ["README.md"]
diff --git a/README.md b/README.md
index 2f170e93..254dc014 100644
--- a/README.md
+++ b/README.md
@@ -50,14 +50,14 @@ java -jar pgadapter.jar -p my-project -i my-instance -d my-database
 Use the `-s` option to specify a different local port than the default 5432 if you already have
 PostgreSQL running on your local system.
 
-<!--- x-release-please-start-version -->
-You can also download a specific version of the jar. Example (replace `v0.5.0` with the version you want to download):
+<!--- {x-version-update-start:google-cloud-spanner-pgadapter:released} -->
+You can also download a specific version of the jar. Example (replace `v0.5.1` with the version you want to download):
 ```shell
-VERSION=v0.5.0
+VERSION=v0.5.1
 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
 ```
-<!--- x-release-please-end -->
+<!--- {x-version-update-end} -->
 
 See [Options](#Options) for an explanation of all further options.
 
@@ -81,15 +81,15 @@ This option is only available for Java/JVM-based applications.
 
 1. Add `google-cloud-spanner-pgadapter` as a dependency to your project by adding this to your `pom.xml` file:
 
-<!--- x-release-please-start-version -->
+<!--- {x-version-update-start:google-cloud-spanner-pgadapter:released} -->
 ```xml
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-spanner-pgadapter</artifactId>
-  <version>0.5.0</version>
+  <version>0.5.1</version>
 </dependency>
 ```
-<!--- x-release-please-end -->
+<!--- {x-version-update-end} -->
 
 
 2. Build a server using the `com.google.cloud.spanner.pgadapter.ProxyServer` class:
diff --git a/pom.xml b/pom.xml
index a4e6f0b5..c43209f8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,7 +35,7 @@
 
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-spanner-pgadapter</artifactId>
-  <version>0.5.1</version><!-- {x-version-update:google-cloud-spanner-pgadapter:current} -->
+  <version>0.5.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-pgadapter:current} -->
   <name>Google Cloud Spanner PostgreSQL Adapter</name>
   <packaging>jar</packaging>
   <description>
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 2dc7fa2f..abd0eb43 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
@@ -18,6 +18,7 @@ import com.google.api.core.InternalApi;
 import com.google.auth.oauth2.GoogleCredentials;
 import com.google.cloud.spanner.Database;
 import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseId;
 import com.google.cloud.spanner.DatabaseNotFoundException;
 import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.ErrorCode;
@@ -98,6 +99,7 @@ public class ConnectionHandler extends Thread {
   private ConnectionMetadata connectionMetadata;
   private WireMessage message;
   private Connection spannerConnection;
+  private DatabaseId databaseId;
   private WellKnownClient wellKnownClient;
   private ExtendedQueryProtocolHandler extendedQueryProtocolHandler;
 
@@ -149,7 +151,8 @@ public class ConnectionHandler extends Thread {
       connectionOptionsBuilder =
           ConnectionOptionsHelper.setCredentials(connectionOptionsBuilder, credentials);
     }
-    Connection spannerConnection = connectionOptionsBuilder.build().getConnection();
+    ConnectionOptions connectionOptions = connectionOptionsBuilder.build();
+    Connection spannerConnection = connectionOptions.getConnection();
     try {
       // Note: Calling getDialect() will cause a SpannerException if the connection itself is
       // invalid, for example as a result of the credentials being wrong.
@@ -186,6 +189,7 @@ public class ConnectionHandler extends Thread {
       throw e;
     }
     this.spannerConnection = spannerConnection;
+    this.databaseId = connectionOptions.getDatabaseId();
     this.extendedQueryProtocolHandler = new ExtendedQueryProtocolHandler(this);
   }
 
@@ -473,6 +477,10 @@ public class ConnectionHandler extends Thread {
     return this.spannerConnection;
   }
 
+  public DatabaseId getDatabaseId() {
+    return this.databaseId;
+  }
+
   public int getConnectionId() {
     if (this.connectionId == 0) {
       this.connectionId = ConnectionHandler.incrementingConnectionId.incrementAndGet();
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/commands/Command.java b/src/main/java/com/google/cloud/spanner/pgadapter/commands/Command.java
index 9e39c68c..6a491edb 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/commands/Command.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/commands/Command.java
@@ -68,8 +68,7 @@ public abstract class Command {
       commandList.add(new DynamicCommand(sql, currentMetadata));
     }
 
-    commandList.addAll(
-        Arrays.asList(new ListCommand(sql, connection), new InvalidMetaCommand(sql)));
+    commandList.addAll(Arrays.asList(new ListCommand(sql), new InvalidMetaCommand(sql)));
 
     return commandList;
   }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/commands/ListCommand.java b/src/main/java/com/google/cloud/spanner/pgadapter/commands/ListCommand.java
index 9c017ca7..1284c7be 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/commands/ListCommand.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/commands/ListCommand.java
@@ -15,14 +15,10 @@
 package com.google.cloud.spanner.pgadapter.commands;
 
 import com.google.api.core.InternalApi;
-import com.google.cloud.spanner.connection.Connection;
+import com.google.cloud.spanner.pgadapter.statements.local.ListDatabasesStatement;
 import java.util.regex.Pattern;
 
-/**
- * This command is equivalent to the PSQL \l and \l <param> meta-commands. The original expectation
- * would be to print database and database attributes, however As we do not have easy access to
- * those, we just print the current database name.
- */
+/** This command is equivalent to the PSQL \l and \l <param> meta-commands. */
 @InternalApi
 public class ListCommand extends Command {
 
@@ -35,13 +31,8 @@ public class ListCommand extends Command {
               + "FROM pg_catalog\\.pg_database d\n.*\n?"
               + "ORDER BY 1;?$");
 
-  private static final String OUTPUT_QUERY = "SELECT '%s' AS Name;";
-
-  private final Connection connection;
-
-  ListCommand(String sql, Connection connection) {
+  ListCommand(String sql) {
     super(sql);
-    this.connection = connection;
   }
 
   @Override
@@ -51,7 +42,6 @@ public class ListCommand extends Command {
 
   @Override
   public String translate() {
-    // TODO: Pass in the database name, or add it to the Connection API surface and get it here.
-    return String.format(OUTPUT_QUERY, "");
+    return ListDatabasesStatement.LIST_DATABASES_SQL;
   }
 }
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 aec2ce4b..94024025 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
@@ -15,7 +15,9 @@
 package com.google.cloud.spanner.pgadapter.metadata;
 
 import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.spanner.DatabaseId;
 import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.InstanceId;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.SpannerOptions;
 import com.google.cloud.spanner.pgadapter.Server;
@@ -658,6 +660,31 @@ public class OptionsMetadata {
     return this.defaultConnectionUrl != null;
   }
 
+  /** Returns the id of the default database or null if no default has been selected. */
+  public DatabaseId getDefaultDatabaseId() {
+    return this.hasDefaultConnectionUrl()
+        ? DatabaseId.of(
+            commandLine.getOptionValue(OPTION_PROJECT_ID),
+            commandLine.getOptionValue(OPTION_INSTANCE_ID),
+            commandLine.getOptionValue(OPTION_DATABASE_NAME))
+        : null;
+  }
+
+  /** Returns true if these options contain a default instance id. */
+  public boolean hasDefaultInstanceId() {
+    return commandLine.hasOption(OPTION_PROJECT_ID) && commandLine.hasOption(OPTION_INSTANCE_ID);
+  }
+
+  /** Returns the id of the default instance or null if no default has been selected. */
+  public InstanceId getDefaultInstanceId() {
+    if (hasDefaultInstanceId()) {
+      return InstanceId.of(
+          commandLine.getOptionValue(OPTION_PROJECT_ID),
+          commandLine.getOptionValue(OPTION_INSTANCE_ID));
+    }
+    return null;
+  }
+
   /**
    * Returns the default connection URL that is used by the server. If a default connection URL has
    * been set, the database parameter in a connection request will be ignored, and the database in
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
index 7aa29a05..d233d994 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
@@ -27,13 +27,18 @@ import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.StatementResult;
 import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.DdlTransactionMode;
+import com.google.cloud.spanner.pgadapter.statements.local.LocalStatement;
 import com.google.cloud.spanner.pgadapter.wireoutput.ReadyResponse;
 import com.google.cloud.spanner.pgadapter.wireoutput.ReadyResponse.Status;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.SettableFuture;
+import java.util.AbstractMap.SimpleImmutableEntry;
 import java.util.LinkedList;
 import java.util.Objects;
 import java.util.concurrent.Future;
+import java.util.stream.Collectors;
 
 /**
  * This class emulates a backend PostgreSQL connection. Statements are buffered in memory until a
@@ -120,7 +125,11 @@ public class BackendConnection {
         //  SELECT statements, then we should create a read-only transaction. Also, if a transaction
         //  block always ends with a ROLLBACK, PGAdapter should skip the entire execution of that
         //  block.
-        if (connectionState == ConnectionState.ABORTED
+        if (!localStatements.isEmpty() && localStatements.containsKey(statement.getSql())) {
+          result.set(
+              Objects.requireNonNull(localStatements.get(statement.getSql()))
+                  .execute(spannerConnection));
+        } else if (connectionState == ConnectionState.ABORTED
             && !spannerConnection.isInTransaction()
             && (isRollback(parsedStatement) || isCommit(parsedStatement))) {
           result.set(ROLLBACK_RESULT);
@@ -145,10 +154,13 @@ public class BackendConnection {
     }
   }
 
+  private static final ImmutableMap<String, LocalStatement> EMPTY_LOCAL_STATEMENTS =
+      ImmutableMap.of();
   private static final StatementResult NO_RESULT = new NoResult();
   private static final StatementResult ROLLBACK_RESULT = new NoResult("ROLLBACK");
   private static final Statement ROLLBACK = Statement.of("ROLLBACK");
 
+  private final ImmutableMap<String, LocalStatement> localStatements;
   private ConnectionState connectionState = ConnectionState.IDLE;
   private TransactionMode transactionMode = TransactionMode.IMPLICIT;
   private final LinkedList<BufferedStatement<?>> bufferedStatements = new LinkedList<>();
@@ -159,9 +171,22 @@ public class BackendConnection {
    * Creates a PG backend connection that uses the given Spanner {@link Connection} and {@link
    * DdlTransactionMode}.
    */
-  BackendConnection(Connection spannerConnection, DdlTransactionMode ddlTransactionMode) {
+  BackendConnection(
+      Connection spannerConnection,
+      DdlTransactionMode ddlTransactionMode,
+      ImmutableList<LocalStatement> localStatements) {
     this.spannerConnection = spannerConnection;
     this.ddlTransactionMode = ddlTransactionMode;
+    //noinspection UnstableApiUsage
+    this.localStatements =
+        localStatements.isEmpty()
+            ? EMPTY_LOCAL_STATEMENTS
+            : ImmutableMap.copyOf(
+                localStatements.stream()
+                    .map(
+                        localStatement ->
+                            new SimpleImmutableEntry<>(localStatement.getSql(), localStatement))
+                    .collect(Collectors.toList()));
   }
 
   /** Returns the current connection state. */
@@ -572,4 +597,33 @@ public class BackendConnection {
       return updateCount;
     }
   }
+
+  @InternalApi
+  public static final class QueryResult implements StatementResult {
+    private final ResultSet resultSet;
+
+    public QueryResult(ResultSet resultSet) {
+      this.resultSet = resultSet;
+    }
+
+    @Override
+    public ResultType getResultType() {
+      return ResultType.RESULT_SET;
+    }
+
+    @Override
+    public ClientSideStatementType getClientSideStatementType() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ResultSet getResultSet() {
+      return resultSet;
+    }
+
+    @Override
+    public Long getUpdateCount() {
+      throw new UnsupportedOperationException();
+    }
+  }
 }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java
index 7d6afc25..64b69afa 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java
@@ -14,9 +14,13 @@
 
 package com.google.cloud.spanner.pgadapter.statements;
 
+import static com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.EMPTY_LOCAL_STATEMENTS;
+
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.statements.local.LocalStatement;
 import com.google.cloud.spanner.pgadapter.wireprotocol.AbstractQueryProtocolMessage;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
@@ -31,10 +35,15 @@ public class ExtendedQueryProtocolHandler {
 
   /** Creates an {@link ExtendedQueryProtocolHandler} for the given connection. */
   public ExtendedQueryProtocolHandler(ConnectionHandler connectionHandler) {
+    ImmutableList<LocalStatement> localStatements =
+        connectionHandler.getWellKnownClient() == null
+            ? EMPTY_LOCAL_STATEMENTS
+            : connectionHandler.getWellKnownClient().getLocalStatements(connectionHandler);
     this.backendConnection =
         new BackendConnection(
             connectionHandler.getSpannerConnection(),
-            connectionHandler.getServer().getOptions().getDdlTransactionMode());
+            connectionHandler.getServer().getOptions().getDdlTransactionMode(),
+            localStatements);
   }
 
   /** Constructor only intended for testing. */
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/local/ListDatabasesStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/local/ListDatabasesStatement.java
new file mode 100644
index 00000000..2f304223
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/local/ListDatabasesStatement.java
@@ -0,0 +1,138 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements.local;
+
+import com.google.api.core.InternalApi;
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.InstanceId;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.ResultSets;
+import com.google.cloud.spanner.Spanner;
+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.Connection;
+import com.google.cloud.spanner.connection.ConnectionOptionsHelper;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.QueryResult;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+@InternalApi
+public class ListDatabasesStatement implements LocalStatement {
+  public static final String LIST_DATABASES_SQL = "select reachable databases";
+  private final ConnectionHandler connectionHandler;
+
+  public ListDatabasesStatement(ConnectionHandler connectionHandler) {
+    this.connectionHandler = Preconditions.checkNotNull(connectionHandler);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof ListDatabasesStatement)) {
+      return false;
+    }
+    return Objects.equals(
+        this.connectionHandler, ((ListDatabasesStatement) other).connectionHandler);
+  }
+
+  @Override
+  public int hashCode() {
+    return this.connectionHandler.hashCode();
+  }
+
+  @Override
+  public String getSql() {
+    return LIST_DATABASES_SQL;
+  }
+
+  @Override
+  public StatementResult execute(Connection connection) {
+    Spanner spanner = ConnectionOptionsHelper.getSpanner(connection);
+    InstanceId defaultInstanceId =
+        connectionHandler.getServer().getOptions().getDefaultInstanceId();
+    ResultSet resultSet =
+        ResultSets.forRows(
+            Type.struct(
+                StructField.of("Name", Type.string()),
+                StructField.of("Owner", Type.string()),
+                StructField.of("Encoding", Type.string()),
+                StructField.of("Collate", Type.string()),
+                StructField.of("Ctype", Type.string()),
+                StructField.of("Access privileges", Type.string())),
+            listDatabases(spanner).stream()
+                .map(
+                    database ->
+                        Struct.newBuilder()
+                            .set("Name")
+                            .to(
+                                database.getId().getInstanceId().equals(defaultInstanceId)
+                                    ? database.getId().getDatabase()
+                                    : database.getId().toString())
+                            .set("Owner")
+                            .to("")
+                            .set("Encoding")
+                            .to("UTF8")
+                            .set("Collate")
+                            .to("")
+                            .set("Ctype")
+                            .to("")
+                            .set("Access privileges")
+                            .to("")
+                            .build())
+                .sorted(Comparator.comparing(row -> row.getString("Name")))
+                .collect(Collectors.toList()));
+    return new QueryResult(resultSet);
+  }
+
+  /**
+   * Returns the databases that are:
+   *
+   * <ol>
+   *   <li>The same as the current database
+   *   <li>On the default instance of this instance of PGAdapter
+   *   <li>On the same instance as the current connection
+   * </ol>
+   */
+  private List<Database> listDatabases(Spanner spanner) {
+    DatabaseAdminClient databaseAdminClient = spanner.getDatabaseAdminClient();
+    if (connectionHandler.getServer().getOptions().hasDefaultConnectionUrl()) {
+      // Only return the current database, as that is the only one that is reachable.
+      return ImmutableList.of(
+          databaseAdminClient
+              .newDatabaseBuilder(connectionHandler.getServer().getOptions().getDefaultDatabaseId())
+              .setDialect(Dialect.POSTGRESQL)
+              .build());
+    }
+    // Return all the databases on the same instance as the database that we are currently connected
+    // to.
+    return StreamSupport.stream(
+            databaseAdminClient
+                .listDatabases(connectionHandler.getDatabaseId().getInstanceId().getInstance())
+                .iterateAll()
+                .spliterator(),
+            false)
+        .filter(database -> database.getDialect() == Dialect.POSTGRESQL)
+        .collect(Collectors.toList());
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/local/LocalStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/local/LocalStatement.java
new file mode 100644
index 00000000..7a5e3760
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/local/LocalStatement.java
@@ -0,0 +1,30 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements.local;
+
+import com.google.cloud.spanner.connection.Connection;
+import com.google.cloud.spanner.connection.StatementResult;
+
+/**
+ * Interface for statements that are handled locally in PGAdapter instead of being sent to
+ * Connection API.
+ */
+public interface LocalStatement {
+  /** Returns the static SQL string associated with this local statement. */
+  String getSql();
+
+  /** Executes the local statement and returns the result. */
+  StatementResult execute(Connection connection);
+}
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetector.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetector.java
index e373290c..404d9661 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetector.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetector.java
@@ -15,6 +15,10 @@
 package com.google.cloud.spanner.pgadapter.utils;
 
 import com.google.api.core.InternalApi;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.statements.local.ListDatabasesStatement;
+import com.google.cloud.spanner.pgadapter.statements.local.LocalStatement;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import java.util.Map;
 
@@ -24,14 +28,22 @@ import java.util.Map;
  */
 @InternalApi
 public class ClientAutoDetector {
+  public static final ImmutableList<LocalStatement> EMPTY_LOCAL_STATEMENTS = ImmutableList.of();
+
   public enum WellKnownClient {
     PSQL {
+
       @Override
       boolean isClient(List<String> orderedParameterKeys, Map<String, String> parameters) {
         // PSQL makes it easy for us, as it sends its own name in the application_name parameter.
         return parameters.containsKey("application_name")
             && parameters.get("application_name").equals("psql");
       }
+
+      @Override
+      public ImmutableList<LocalStatement> getLocalStatements(ConnectionHandler connectionHandler) {
+        return ImmutableList.of(new ListDatabasesStatement(connectionHandler));
+      }
     },
     JDBC {
       @Override
@@ -76,6 +88,10 @@ public class ClientAutoDetector {
     };
 
     abstract boolean isClient(List<String> orderedParameterKeys, Map<String, String> parameters);
+
+    public ImmutableList<LocalStatement> getLocalStatements(ConnectionHandler connectionHandler) {
+      return EMPTY_LOCAL_STATEMENTS;
+    }
   }
 
   /**
diff --git a/versions.txt b/versions.txt
index 5b4405ba..5b66947a 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-spanner-pgadapter:0.5.1:0.5.1
+google-cloud-spanner-pgadapter:0.5.1:0.5.2-SNAPSHOT


Test patch:
diff --git a/.ci/e2e-expected/backslash-l.txt b/.ci/e2e-expected/backslash-l.txt
new file mode 100644
index 00000000..6232f89c
--- /dev/null
+++ b/.ci/e2e-expected/backslash-l.txt
@@ -0,0 +1,6 @@
+                               List of databases
+  Name | Owner | Encoding | Collate | Ctype | Access privileges
++++++
+ testdb_e2e_psql_v?? |       | UTF8     |         |       |
+(1 row)
+
diff --git a/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsHelperTest.java b/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsHelperTest.java
new file mode 100644
index 00000000..532f7788
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsHelperTest.java
@@ -0,0 +1,58 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.connection;
+
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.spanner.Spanner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ConnectionOptionsHelperTest {
+
+  public static Connection setupMockConnection(Spanner spanner) {
+    ConnectionImpl connection = mock(ConnectionImpl.class);
+    when(connection.getSpanner()).thenReturn(spanner);
+
+    return connection;
+  }
+
+  @Test
+  public void testSetCredentials() {
+    ConnectionOptions.Builder builder = mock(ConnectionOptions.Builder.class);
+    GoogleCredentials credentials = mock(GoogleCredentials.class);
+
+    ConnectionOptionsHelper.setCredentials(builder, credentials);
+
+    verify(builder).setCredentials(credentials);
+  }
+
+  @Test
+  public void testGetSpanner() {
+    ConnectionImpl connection = mock(ConnectionImpl.class);
+    Spanner expected = mock(Spanner.class);
+    when(connection.getSpanner()).thenReturn(expected);
+
+    Spanner got = ConnectionOptionsHelper.getSpanner(connection);
+
+    assertSame(expected, got);
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
new file mode 100644
index 00000000..d4e7fe94
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
@@ -0,0 +1,106 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
+import com.google.cloud.spanner.connection.Connection;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.connection.StatementResult.ResultType;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.DdlTransactionMode;
+import com.google.cloud.spanner.pgadapter.statements.BackendConnection.QueryResult;
+import com.google.cloud.spanner.pgadapter.statements.local.ListDatabasesStatement;
+import com.google.cloud.spanner.pgadapter.statements.local.LocalStatement;
+import com.google.common.collect.ImmutableList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class BackendConnectionTest {
+
+  @Test
+  public void testExecuteLocalStatement() throws ExecutionException, InterruptedException {
+    Connection connection = mock(Connection.class);
+    StatementResult listDatabasesResult = mock(StatementResult.class);
+    when(listDatabasesResult.getResultType()).thenReturn(ResultType.RESULT_SET);
+    ListDatabasesStatement listDatabasesStatement = mock(ListDatabasesStatement.class);
+    when(listDatabasesStatement.getSql()).thenReturn(ListDatabasesStatement.LIST_DATABASES_SQL);
+    when(listDatabasesStatement.execute(connection)).thenReturn(listDatabasesResult);
+    ImmutableList<LocalStatement> localStatements = ImmutableList.of(listDatabasesStatement);
+    ParsedStatement parsedListDatabasesStatement = mock(ParsedStatement.class);
+    when(parsedListDatabasesStatement.getSqlWithoutComments())
+        .thenReturn(ListDatabasesStatement.LIST_DATABASES_SQL);
+
+    BackendConnection backendConnection =
+        new BackendConnection(connection, DdlTransactionMode.Batch, localStatements);
+    Future<StatementResult> resultFuture =
+        backendConnection.execute(
+            parsedListDatabasesStatement, Statement.of(ListDatabasesStatement.LIST_DATABASES_SQL));
+    backendConnection.flush();
+
+    verify(listDatabasesStatement).execute(connection);
+    assertTrue(resultFuture.isDone());
+    assertEquals(listDatabasesResult, resultFuture.get());
+  }
+
+  @Test
+  public void testExecuteOtherStatementWithLocalStatements()
+      throws ExecutionException, InterruptedException {
+    Connection connection = mock(Connection.class);
+    ListDatabasesStatement listDatabasesStatement = mock(ListDatabasesStatement.class);
+    when(listDatabasesStatement.getSql()).thenReturn(ListDatabasesStatement.LIST_DATABASES_SQL);
+    ImmutableList<LocalStatement> localStatements = ImmutableList.of(listDatabasesStatement);
+
+    StatementResult statementResult = mock(StatementResult.class);
+    when(statementResult.getResultType()).thenReturn(ResultType.RESULT_SET);
+    ParsedStatement parsedStatement = mock(ParsedStatement.class);
+    String sql = "SELECT * FROM foo";
+    when(parsedStatement.getSqlWithoutComments()).thenReturn(sql);
+    Statement statement = Statement.of(sql);
+    when(connection.execute(statement)).thenReturn(statementResult);
+
+    BackendConnection backendConnection =
+        new BackendConnection(connection, DdlTransactionMode.Batch, localStatements);
+    Future<StatementResult> resultFuture = backendConnection.execute(parsedStatement, statement);
+    backendConnection.flush();
+
+    verify(listDatabasesStatement, never()).execute(connection);
+    assertTrue(resultFuture.isDone());
+    assertEquals(statementResult, resultFuture.get());
+  }
+
+  @Test
+  public void testQueryResult() {
+    ResultSet resultSet = mock(ResultSet.class);
+    QueryResult queryResult = new QueryResult(resultSet);
+
+    assertEquals(ResultType.RESULT_SET, queryResult.getResultType());
+    assertEquals(resultSet, queryResult.getResultSet());
+    assertThrows(UnsupportedOperationException.class, queryResult::getClientSideStatementType);
+    assertThrows(UnsupportedOperationException.class, queryResult::getUpdateCount);
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/PSQLTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/PSQLTest.java
index b46ac2ce..09a88bcc 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/PSQLTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/PSQLTest.java
@@ -25,6 +25,7 @@ import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.ProxyServer;
 import com.google.cloud.spanner.pgadapter.metadata.CommandMetadataParser;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.statements.local.ListDatabasesStatement;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.JSONParser;
 import org.junit.Before;
@@ -283,7 +284,7 @@ public class PSQLTest {
             + "       pg_catalog.array_to_string(d.datacl, '\\n') AS \"Access privileges\"\n"
             + "FROM pg_catalog.pg_database d\n"
             + "ORDER BY 1;";
-    String expected = "SELECT '' AS Name";
+    String expected = ListDatabasesStatement.LIST_DATABASES_SQL;
 
     assertEquals(expected, translate(sql));
   }
@@ -299,7 +300,7 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_database d\n"
             + "WHERE d.datname OPERATOR(pg_catalog.~) '^(users)$'\n"
             + "ORDER BY 1;";
-    String expected = "SELECT '' AS Name";
+    String expected = ListDatabasesStatement.LIST_DATABASES_SQL;
 
     assertEquals(expected, translate(sql));
   }
@@ -315,10 +316,8 @@ public class PSQLTest {
             + "FROM pg_catalog.pg_database d\n"
             + "WHERE d.datname OPERATOR(pg_catalog.~) '^(users)$'\n"
             + "ORDER BY 1;";
-    String expected = "SELECT '' AS Name";
+    String expected = ListDatabasesStatement.LIST_DATABASES_SQL;
 
-    // TODO: Add Connection#getDatabase() to Connection API and test here what happens if that
-    // method throws an exception.
     assertEquals(expected, translate(sql));
   }
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
index 175e138e..0344746d 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
@@ -14,6 +14,7 @@
 
 package com.google.cloud.spanner.pgadapter.statements;
 
+import static com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.EMPTY_LOCAL_STATEMENTS;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -154,7 +155,7 @@ public class StatementTest {
         new IntermediatePortalStatement(
             connectionHandler, options, "", parse(sql), Statement.of(sql));
     BackendConnection backendConnection =
-        new BackendConnection(connection, DdlTransactionMode.Batch);
+        new BackendConnection(connection, DdlTransactionMode.Batch, EMPTY_LOCAL_STATEMENTS);
 
     assertFalse(intermediateStatement.isExecuted());
     assertEquals("UPDATE", intermediateStatement.getCommand());
@@ -226,7 +227,7 @@ public class StatementTest {
         new IntermediatePortalStatement(
             connectionHandler, options, "", parse(sql), Statement.of(sql));
     BackendConnection backendConnection =
-        new BackendConnection(connection, DdlTransactionMode.Batch);
+        new BackendConnection(connection, DdlTransactionMode.Batch, EMPTY_LOCAL_STATEMENTS);
 
     intermediateStatement.executeAsync(backendConnection);
     backendConnection.flush();
@@ -251,7 +252,7 @@ public class StatementTest {
             .to(30)
             .build();
     BackendConnection backendConnection =
-        new BackendConnection(connection, DdlTransactionMode.Batch);
+        new BackendConnection(connection, DdlTransactionMode.Batch, EMPTY_LOCAL_STATEMENTS);
 
     IntermediatePreparedStatement intermediateStatement =
         new IntermediatePreparedStatement(
@@ -318,7 +319,7 @@ public class StatementTest {
         new IntermediatePortalStatement(
             connectionHandler, options, "", parse(sqlStatement), Statement.of(sqlStatement));
     BackendConnection backendConnection =
-        new BackendConnection(connection, DdlTransactionMode.Batch);
+        new BackendConnection(connection, DdlTransactionMode.Batch, EMPTY_LOCAL_STATEMENTS);
 
     intermediateStatement.describeAsync(backendConnection);
     backendConnection.flush();
@@ -362,7 +363,7 @@ public class StatementTest {
         new IntermediatePortalStatement(
             connectionHandler, options, "", parse(sqlStatement), Statement.of(sqlStatement));
     BackendConnection backendConnection =
-        new BackendConnection(connection, DdlTransactionMode.Batch);
+        new BackendConnection(connection, DdlTransactionMode.Batch, EMPTY_LOCAL_STATEMENTS);
 
     when(connection.execute(Statement.of(sqlStatement)))
         .thenThrow(
@@ -462,7 +463,7 @@ public class StatementTest {
         new IntermediatePortalStatement(
             connectionHandler, options, "", parse(sql), Statement.of(sql));
     BackendConnection backendConnection =
-        new BackendConnection(connection, DdlTransactionMode.Batch);
+        new BackendConnection(connection, DdlTransactionMode.Batch, EMPTY_LOCAL_STATEMENTS);
 
     intermediateStatement.executeAsync(backendConnection);
 
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/local/ListDatabasesStatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/local/ListDatabasesStatementTest.java
new file mode 100644
index 00000000..f58a0f39
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/local/ListDatabasesStatementTest.java
@@ -0,0 +1,252 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.cloud.spanner.pgadapter.statements.local;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.api.gax.paging.Page;
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.InstanceId;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.connection.Connection;
+import com.google.cloud.spanner.connection.ConnectionOptionsHelperTest;
+import com.google.cloud.spanner.connection.StatementResult;
+import com.google.cloud.spanner.connection.StatementResult.ResultType;
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.ProxyServer;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ListDatabasesStatementTest {
+
+  @Test
+  public void testGetSql() {
+    ConnectionHandler connectionHandler = mock(ConnectionHandler.class);
+    ListDatabasesStatement statement = new ListDatabasesStatement(connectionHandler);
+
+    assertEquals(ListDatabasesStatement.LIST_DATABASES_SQL, statement.getSql());
+  }
+
+  @Test
+  public void testEquals() {
+    ConnectionHandler connectionHandler = mock(ConnectionHandler.class);
+    ListDatabasesStatement statement1 = new ListDatabasesStatement(connectionHandler);
+    ListDatabasesStatement statement2 = new ListDatabasesStatement(connectionHandler);
+    ListDatabasesStatement statement3 = new ListDatabasesStatement(mock(ConnectionHandler.class));
+
+    assertEquals(statement1, statement2);
+    assertNotEquals(statement1, statement3);
+    assertNotEquals(statement1, null);
+  }
+
+  @Test
+  public void testHashCode() {
+    ConnectionHandler connectionHandler1 = mock(ConnectionHandler.class);
+    ConnectionHandler connectionHandler2 = mock(ConnectionHandler.class);
+    ListDatabasesStatement statement1 = new ListDatabasesStatement(connectionHandler1);
+    ListDatabasesStatement statement2 = new ListDatabasesStatement(connectionHandler1);
+    ListDatabasesStatement statement3 = new ListDatabasesStatement(connectionHandler2);
+
+    assertEquals(statement1.hashCode(), statement2.hashCode());
+    assertNotEquals(statement1.hashCode(), statement3.hashCode());
+  }
+
+  @Test
+  public void testExecuteNoDefault() {
+    Spanner spanner = mock(Spanner.class);
+    DatabaseAdminClient databaseAdminClient = mock(DatabaseAdminClient.class);
+    when(spanner.getDatabaseAdminClient()).thenReturn(databaseAdminClient);
+    Connection connection = ConnectionOptionsHelperTest.setupMockConnection(spanner);
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    DatabaseId currentDatabaseId = DatabaseId.of("my-project", "my-instance", "my-database");
+    ConnectionHandler connectionHandler = setupMockConnectionHandler(options, currentDatabaseId);
+
+    // Setup results for databaseAdminClient.listDatabases(currentInstanceId);
+    Database currentDatabase = mock(Database.class);
+    when(currentDatabase.getId()).thenReturn(currentDatabaseId);
+    when(currentDatabase.getDialect()).thenReturn(Dialect.POSTGRESQL);
+    Database otherPgDatabase = mock(Database.class);
+    when(otherPgDatabase.getId())
+        .thenReturn(DatabaseId.of("my-project", "my-instance", "other-pg-database"));
+    when(otherPgDatabase.getDialect()).thenReturn(Dialect.POSTGRESQL);
+    Database otherGsqlDatabase = mock(Database.class);
+    when(otherGsqlDatabase.getId())
+        .thenReturn(DatabaseId.of("my-project", "my-instance", "other-gsql-database"));
+    when(otherGsqlDatabase.getDialect()).thenReturn(Dialect.GOOGLE_STANDARD_SQL);
+    Page<Database> page =
+        createMockPage(ImmutableList.of(otherPgDatabase, otherGsqlDatabase, currentDatabase));
+    when(databaseAdminClient.listDatabases(currentDatabaseId.getInstanceId().getInstance()))
+        .thenReturn(page);
+
+    ListDatabasesStatement statement = new ListDatabasesStatement(connectionHandler);
+    StatementResult result = statement.execute(connection);
+
+    assertEquals(ResultType.RESULT_SET, result.getResultType());
+    ResultSet resultSet = result.getResultSet();
+    assertTrue(resultSet.next());
+    assertEquals(currentDatabaseId.toString(), resultSet.getString("Name"));
+    assertTrue(resultSet.next());
+    assertEquals(otherPgDatabase.getId().toString(), resultSet.getString("Name"));
+    assertFalse(resultSet.next());
+  }
+
+  @Test
+  public void testExecuteWithDefaultInstance() {
+    Spanner spanner = mock(Spanner.class);
+    DatabaseAdminClient databaseAdminClient = mock(DatabaseAdminClient.class);
+    when(spanner.getDatabaseAdminClient()).thenReturn(databaseAdminClient);
+    Connection connection = ConnectionOptionsHelperTest.setupMockConnection(spanner);
+    DatabaseId currentDatabaseId = DatabaseId.of("my-project", "my-instance", "my-database");
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    when(options.hasDefaultInstanceId()).thenReturn(true);
+    when(options.getDefaultInstanceId()).thenReturn(currentDatabaseId.getInstanceId());
+    ConnectionHandler connectionHandler = setupMockConnectionHandler(options, currentDatabaseId);
+
+    // Setup results for databaseAdminClient.listDatabases(currentInstanceId);
+    Database currentDatabase = mock(Database.class);
+    when(currentDatabase.getId()).thenReturn(currentDatabaseId);
+    when(currentDatabase.getDialect()).thenReturn(Dialect.POSTGRESQL);
+    Database otherPgDatabase = mock(Database.class);
+    when(otherPgDatabase.getId())
+        .thenReturn(DatabaseId.of("my-project", "my-instance", "other-pg-database"));
+    when(otherPgDatabase.getDialect()).thenReturn(Dialect.POSTGRESQL);
+    Database otherGsqlDatabase = mock(Database.class);
+    when(otherGsqlDatabase.getId())
+        .thenReturn(DatabaseId.of("my-project", "my-instance", "other-gsql-database"));
+    when(otherGsqlDatabase.getDialect()).thenReturn(Dialect.GOOGLE_STANDARD_SQL);
+    Page<Database> page =
+        createMockPage(ImmutableList.of(otherPgDatabase, otherGsqlDatabase, currentDatabase));
+    when(databaseAdminClient.listDatabases(currentDatabaseId.getInstanceId().getInstance()))
+        .thenReturn(page);
+
+    ListDatabasesStatement statement = new ListDatabasesStatement(connectionHandler);
+    StatementResult result = statement.execute(connection);
+
+    assertEquals(ResultType.RESULT_SET, result.getResultType());
+    ResultSet resultSet = result.getResultSet();
+    assertTrue(resultSet.next());
+    assertEquals(currentDatabaseId.getDatabase(), resultSet.getString("Name"));
+    assertTrue(resultSet.next());
+    assertEquals(otherPgDatabase.getId().getDatabase(), resultSet.getString("Name"));
+    assertFalse(resultSet.next());
+  }
+
+  @Test
+  public void testExecuteWithDefaultInstanceWhileConnectedToDifferentInstance() {
+    Spanner spanner = mock(Spanner.class);
+    DatabaseAdminClient databaseAdminClient = mock(DatabaseAdminClient.class);
+    when(spanner.getDatabaseAdminClient()).thenReturn(databaseAdminClient);
+    Connection connection = ConnectionOptionsHelperTest.setupMockConnection(spanner);
+    DatabaseId currentDatabaseId = DatabaseId.of("my-project", "my-instance", "my-database");
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    when(options.hasDefaultInstanceId()).thenReturn(true);
+    when(options.getDefaultInstanceId())
+        .thenReturn(InstanceId.of("my-project", "default-instance"));
+    ConnectionHandler connectionHandler = setupMockConnectionHandler(options, currentDatabaseId);
+
+    // Setup results for databaseAdminClient.listDatabases(currentInstanceId);
+    Database currentDatabase = mock(Database.class);
+    when(currentDatabase.getId()).thenReturn(currentDatabaseId);
+    when(currentDatabase.getDialect()).thenReturn(Dialect.POSTGRESQL);
+    Database otherPgDatabase = mock(Database.class);
+    when(otherPgDatabase.getId())
+        .thenReturn(DatabaseId.of("my-project", "my-instance", "other-pg-database"));
+    when(otherPgDatabase.getDialect()).thenReturn(Dialect.POSTGRESQL);
+    Database otherGsqlDatabase = mock(Database.class);
+    when(otherGsqlDatabase.getId())
+        .thenReturn(DatabaseId.of("my-project", "my-instance", "other-gsql-database"));
+    when(otherGsqlDatabase.getDialect()).thenReturn(Dialect.GOOGLE_STANDARD_SQL);
+    Page<Database> page =
+        createMockPage(ImmutableList.of(otherPgDatabase, otherGsqlDatabase, currentDatabase));
+    when(databaseAdminClient.listDatabases(currentDatabaseId.getInstanceId().getInstance()))
+        .thenReturn(page);
+
+    ListDatabasesStatement statement = new ListDatabasesStatement(connectionHandler);
+    StatementResult result = statement.execute(connection);
+
+    assertEquals(ResultType.RESULT_SET, result.getResultType());
+    ResultSet resultSet = result.getResultSet();
+    assertTrue(resultSet.next());
+    assertEquals(currentDatabaseId.toString(), resultSet.getString("Name"));
+    assertTrue(resultSet.next());
+    assertEquals(otherPgDatabase.getId().toString(), resultSet.getString("Name"));
+    assertFalse(resultSet.next());
+  }
+
+  @Test
+  public void testExecuteWithDefaultDatabase() {
+    Spanner spanner = mock(Spanner.class);
+    DatabaseAdminClient databaseAdminClient = mock(DatabaseAdminClient.class);
+    when(spanner.getDatabaseAdminClient()).thenReturn(databaseAdminClient);
+    Connection connection = ConnectionOptionsHelperTest.setupMockConnection(spanner);
+    DatabaseId currentDatabaseId = DatabaseId.of("my-project", "my-instance", "my-database");
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    when(options.hasDefaultConnectionUrl()).thenReturn(true);
+    when(options.getDefaultDatabaseId()).thenReturn(currentDatabaseId);
+    when(options.hasDefaultInstanceId()).thenReturn(true);
+    when(options.getDefaultInstanceId()).thenReturn(currentDatabaseId.getInstanceId());
+    ConnectionHandler connectionHandler = setupMockConnectionHandler(options, currentDatabaseId);
+
+    // Setup results for databaseAdminClient.listDatabases(currentInstanceId);
+    Database currentDatabase = mock(Database.class);
+    when(currentDatabase.getId()).thenReturn(currentDatabaseId);
+    when(currentDatabase.getDialect()).thenReturn(Dialect.POSTGRESQL);
+    Database.Builder databaseBuilder = mock(Database.Builder.class);
+    when(databaseBuilder.setDialect(Dialect.POSTGRESQL)).thenReturn(databaseBuilder);
+    when(databaseBuilder.build()).thenReturn(currentDatabase);
+    when(databaseAdminClient.newDatabaseBuilder(currentDatabaseId)).thenReturn(databaseBuilder);
+
+    ListDatabasesStatement statement = new ListDatabasesStatement(connectionHandler);
+    StatementResult result = statement.execute(connection);
+
+    assertEquals(ResultType.RESULT_SET, result.getResultType());
+    ResultSet resultSet = result.getResultSet();
+    assertTrue(resultSet.next());
+    assertEquals(currentDatabaseId.getDatabase(), resultSet.getString("Name"));
+    assertFalse(resultSet.next());
+  }
+
+  private ConnectionHandler setupMockConnectionHandler(
+      OptionsMetadata options, DatabaseId databaseId) {
+    ProxyServer server = mock(ProxyServer.class);
+    ConnectionHandler connectionHandler = mock(ConnectionHandler.class);
+    when(connectionHandler.getServer()).thenReturn(server);
+    when(connectionHandler.getDatabaseId()).thenReturn(databaseId);
+    when(server.getOptions()).thenReturn(options);
+
+    return connectionHandler;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Page<Database> createMockPage(Iterable<Database> databases) {
+    Page<Database> result = mock(Page.class);
+    when(result.iterateAll()).thenReturn(databases);
+
+    return result;
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetectorTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetectorTest.java
index b101546d..b5c94e88 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetectorTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/utils/ClientAutoDetectorTest.java
@@ -14,9 +14,13 @@
 
 package com.google.cloud.spanner.pgadapter.utils;
 
+import static com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.EMPTY_LOCAL_STATEMENTS;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.mock;
 
+import com.google.cloud.spanner.pgadapter.ConnectionHandler;
+import com.google.cloud.spanner.pgadapter.statements.local.ListDatabasesStatement;
 import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.WellKnownClient;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -143,6 +147,10 @@ public class ClientAutoDetectorTest {
                   "DateStyle", "ISO",
                   "TimeZone", "some-time-zone")));
     }
+
+    assertEquals(
+        EMPTY_LOCAL_STATEMENTS,
+        WellKnownClient.JDBC.getLocalStatements(mock(ConnectionHandler.class)));
   }
 
   @Test
@@ -182,6 +190,11 @@ public class ClientAutoDetectorTest {
         WellKnownClient.PSQL,
         ClientAutoDetector.detectClient(
             ImmutableList.of("application_name"), ImmutableMap.of("application_name", "PSQL")));
+
+    ConnectionHandler connectionHandler = mock(ConnectionHandler.class);
+    assertEquals(
+        ImmutableList.of(new ListDatabasesStatement(connectionHandler)),
+        WellKnownClient.PSQL.getLocalStatements(connectionHandler));
   }
 
   @Test
@@ -193,5 +206,9 @@ public class ClientAutoDetectorTest {
         WellKnownClient.PGX,
         ClientAutoDetector.detectClient(
             ImmutableList.of("some-param"), ImmutableMap.of("some-param", "some-value")));
+
+    assertEquals(
+        EMPTY_LOCAL_STATEMENTS,
+        WellKnownClient.PGX.getLocalStatements(mock(ConnectionHandler.class)));
   }
 }