Repository: GoogleCloudPlatform/pgadapter
Pull request #559 (2022-12-21 17:58:49)

feat: auto-detect npgsql
Adds support for auto-detecting `npgsql` based on the first query that it sends. This feature can also be used to auto-detect other clients in the future.

This also adds support for `pg_proc` and `pg_range` to the `pg_catalog` replacement registry. These will be used for the initial queries that are sent by `npgsql`.

Finally, this removes the parameterized tests for version 1.0 for `npgsql`. These will not be needed, as the version that is currently being returned by PGAdapter is `14.1`. There's no need to support version `1.0` for this driver, as we have never had support for it while PGAdapter returned version `1.0`.

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/559?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) Report
> Merging [#559](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/559?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (728151e) into [postgresql-dialect](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/commit/8762cb3fdc8fbc3e23773f575e9ca96b376041a1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) (8762cb3) will **increase** coverage by `0.01%`.
> The diff coverage is `84.21%`.

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #559      +/-   ##
========================================================
+ Coverage                 88.36%   88.38%   +0.01%     
- Complexity                 2068     2075       +7     
========================================================
  Files                       122      122              
  Lines                      6791     6827      +36     
  Branches                    956      961       +5     
========================================================
+ Hits                       6001     6034      +33     
- Misses                      560      561       +1     
- Partials                    230      232       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `88.38% <84.21%> (+0.01%)` | :arrow_up: |

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/559?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform) | Coverage Δ | |
|---|---|---|
| [...gle/cloud/spanner/pgadapter/ConnectionHandler.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/559/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL0Nvbm5lY3Rpb25IYW5kbGVyLmphdmE=) | `86.94% <66.66%> (+0.33%)` | :arrow_up: |
| [...ud/spanner/pgadapter/utils/ClientAutoDetector.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/559/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3V0aWxzL0NsaWVudEF1dG9EZXRlY3Rvci5qYXZh) | `89.28% <75.00%> (-3.90%)` | :arrow_down: |
| [.../cloud/spanner/pgadapter/statements/PgCatalog.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/559/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvUGdDYXRhbG9nLmphdmE=) | `95.00% <100.00%> (+1.06%)` | :arrow_up: |
| [...d/spanner/pgadapter/wireprotocol/QueryMessage.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/559/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3dpcmVwcm90b2NvbC9RdWVyeU1lc3NhZ2UuamF2YQ==) | `100.00% <100.00%> (ø)` | |

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


Patch:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java b/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
index 5492e44c..176a2f48 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/ConnectionHandler.java
@@ -29,6 +29,7 @@ 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.Connection;
 import com.google.cloud.spanner.connection.ConnectionOptions;
 import com.google.cloud.spanner.connection.ConnectionOptionsHelper;
@@ -45,6 +46,7 @@ import com.google.cloud.spanner.pgadapter.statements.ExtendedQueryProtocolHandle
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePortalStatement;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePreparedStatement;
 import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement;
+import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector;
 import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.WellKnownClient;
 import com.google.cloud.spanner.pgadapter.wireoutput.ErrorResponse;
 import com.google.cloud.spanner.pgadapter.wireoutput.ReadyResponse;
@@ -55,6 +57,7 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableList;
 import com.google.spanner.admin.database.v1.InstanceName;
 import com.google.spanner.v1.DatabaseName;
 import java.io.DataOutputStream;
@@ -118,6 +121,7 @@ public class ConnectionHandler extends Thread {
   private Connection spannerConnection;
   private DatabaseId databaseId;
   private WellKnownClient wellKnownClient;
+  private boolean hasDeterminedClientUsingQuery;
   private ExtendedQueryProtocolHandler extendedQueryProtocolHandler;
   private CopyStatement activeCopyStatement;
 
@@ -702,6 +706,30 @@ public class ConnectionHandler extends Thread {
     this.wellKnownClient = wellKnownClient;
   }
 
+  /**
+   * This is called by the simple {@link
+   * com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage} to give the connection the
+   * opportunity to determine the client that is connected based on the SQL string that is being
+   * executed.
+   */
+  public void maybeDetermineWellKnownClient(Statement statement) {
+    if (!this.hasDeterminedClientUsingQuery
+        && this.wellKnownClient == WellKnownClient.UNSPECIFIED
+        && getServer().getOptions().shouldAutoDetectClient()) {
+      this.wellKnownClient = ClientAutoDetector.detectClient(ImmutableList.of(statement));
+      if (this.wellKnownClient != WellKnownClient.UNSPECIFIED) {
+        logger.log(
+            Level.INFO,
+            () ->
+                String.format(
+                    "Well-known client %s detected for connection %d.",
+                    this.wellKnownClient, getConnectionId()));
+      }
+    }
+    // Make sure that we only try to detect the client once.
+    this.hasDeterminedClientUsingQuery = true;
+  }
+
   /** Status of a {@link ConnectionHandler} */
   public enum ConnectionStatus {
     UNAUTHENTICATED,
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java
index 182c6097..a1d4aac4 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/PgCatalog.java
@@ -32,17 +32,31 @@ import java.util.regex.Pattern;
 @InternalApi
 public class PgCatalog {
   private static final ImmutableMap<TableOrIndexName, TableOrIndexName> TABLE_REPLACEMENTS =
-      ImmutableMap.of(
-          new TableOrIndexName("pg_catalog", "pg_namespace"),
+      ImmutableMap.<TableOrIndexName, TableOrIndexName>builder()
+          .put(
+              new TableOrIndexName("pg_catalog", "pg_namespace"),
+              new TableOrIndexName(null, "pg_namespace"))
+          .put(
               new TableOrIndexName(null, "pg_namespace"),
-          new TableOrIndexName(null, "pg_namespace"), new TableOrIndexName(null, "pg_namespace"),
-          new TableOrIndexName("pg_catalog", "pg_class"), new TableOrIndexName(null, "pg_class"),
-          new TableOrIndexName(null, "pg_class"), new TableOrIndexName(null, "pg_class"),
-          new TableOrIndexName("pg_catalog", "pg_type"), new TableOrIndexName(null, "pg_type"),
-          new TableOrIndexName(null, "pg_type"), new TableOrIndexName(null, "pg_type"),
-          new TableOrIndexName("pg_catalog", "pg_settings"),
-              new TableOrIndexName(null, "pg_settings"),
-          new TableOrIndexName(null, "pg_settings"), new TableOrIndexName(null, "pg_settings"));
+              new TableOrIndexName(null, "pg_namespace"))
+          .put(
+              new TableOrIndexName("pg_catalog", "pg_class"),
+              new TableOrIndexName(null, "pg_class"))
+          .put(new TableOrIndexName(null, "pg_class"), new TableOrIndexName(null, "pg_class"))
+          .put(new TableOrIndexName("pg_catalog", "pg_proc"), new TableOrIndexName(null, "pg_proc"))
+          .put(new TableOrIndexName(null, "pg_proc"), new TableOrIndexName(null, "pg_proc"))
+          .put(
+              new TableOrIndexName("pg_catalog", "pg_range"),
+              new TableOrIndexName(null, "pg_range"))
+          .put(new TableOrIndexName(null, "pg_range"), new TableOrIndexName(null, "pg_range"))
+          .put(new TableOrIndexName("pg_catalog", "pg_type"), new TableOrIndexName(null, "pg_type"))
+          .put(new TableOrIndexName(null, "pg_type"), new TableOrIndexName(null, "pg_type"))
+          .put(
+              new TableOrIndexName("pg_catalog", "pg_settings"),
+              new TableOrIndexName(null, "pg_settings"))
+          .put(new TableOrIndexName(null, "pg_settings"), new TableOrIndexName(null, "pg_settings"))
+          .build();
+
   private static final ImmutableMap<Pattern, String> FUNCTION_REPLACEMENTS =
       ImmutableMap.of(
           Pattern.compile("pg_catalog.pg_table_is_visible\\(.+\\)"), "true",
@@ -53,6 +67,8 @@ public class PgCatalog {
       ImmutableMap.of(
           new TableOrIndexName(null, "pg_namespace"), new PgNamespace(),
           new TableOrIndexName(null, "pg_class"), new PgClass(),
+          new TableOrIndexName(null, "pg_proc"), new PgProc(),
+          new TableOrIndexName(null, "pg_range"), new PgRange(),
           new TableOrIndexName(null, "pg_type"), new PgType(),
           new TableOrIndexName(null, "pg_settings"), new PgSettings());
   private final SessionState sessionState;
@@ -328,4 +344,38 @@ public class PgCatalog {
       return PG_CLASS_CTE;
     }
   }
+
+  private static class PgProc implements PgCatalogTable {
+    private static final String PG_PROC_CTE =
+        "pg_proc as (\n"
+            + "select * from ("
+            + "select 0::bigint as oid, ''::varchar as proname, 0::bigint as pronamespace, 0::bigint as proowner, "
+            + "0::bigint as prolang, 0.0::float8 as procost, 0.0::float8 as prorows, 0::bigint as provariadic, "
+            + "''::varchar as prosupport, ''::varchar as prokind, false::bool as prosecdef, false::bool as proleakproof, "
+            + "false::bool as proisstrict, false::bool as proretset, ''::varchar as provolatile, ''::varchar as proparallel, "
+            + "0::bigint as pronargs, 0::bigint as pronargdefaults, 0::bigint as prorettype, 0::bigint as proargtypes, "
+            + "'{}'::bigint[] as proallargtypes, '{}'::varchar[] as proargmodes, '{}'::text[] as proargnames, "
+            + "''::varchar as proargdefaults, '{}'::bigint[] as protrftypes, ''::text as prosrc, ''::text as probin, "
+            + "''::varchar as prosqlbody, '{}'::text[] as proconfig, '{}'::bigint[] as proacl\n"
+            + ") proc where false)";
+
+    @Override
+    public String getTableExpression() {
+      return PG_PROC_CTE;
+    }
+  }
+
+  private static class PgRange implements PgCatalogTable {
+    private static final String PG_RANGE_CTE =
+        "pg_range as (\n"
+            + "select * from ("
+            + "select 0::bigint as rngtypid, 0::bigint as rngsubtype, 0::bigint as rngmultitypid, "
+            + "0::bigint as rngcollation, 0::bigint as rngsubopc, ''::varchar as rngcanonical, ''::varchar as rngsubdiff\n"
+            + ") range where false)";
+
+    @Override
+    public String getTableExpression() {
+      return PG_RANGE_CTE;
+    }
+  }
 }
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 4d5ecec2..44db4631 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,7 @@
 package com.google.cloud.spanner.pgadapter.utils;
 
 import com.google.api.core.InternalApi;
+import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.statements.local.DjangoGetTableNamesStatement;
 import com.google.cloud.spanner.pgadapter.statements.local.ListDatabasesStatement;
@@ -113,6 +114,27 @@ public class ClientAutoDetector {
         return false;
       }
     },
+    NPGSQL {
+      @Override
+      boolean isClient(List<String> orderedParameterKeys, Map<String, String> parameters) {
+        // npgsql does not send enough unique parameters for it to be auto-detected.
+        return false;
+      }
+
+      @Override
+      boolean isClient(List<Statement> statements) {
+        // The npgsql client always starts with sending a query that contains multiple statements
+        // and that starts with the following prefix.
+        return statements.size() == 1
+            && statements
+                .get(0)
+                .getSql()
+                .startsWith(
+                    "SELECT version();\n"
+                        + "\n"
+                        + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n");
+      }
+    },
     UNSPECIFIED {
       @Override
       boolean isClient(List<String> orderedParameterKeys, Map<String, String> parameters) {
@@ -120,10 +142,21 @@ public class ClientAutoDetector {
         // defaults defined in this enum.
         return true;
       }
+
+      @Override
+      boolean isClient(List<Statement> statements) {
+        // Use UNSPECIFIED as default to prevent null checks everywhere and to ease the use of any
+        // defaults defined in this enum.
+        return true;
+      }
     };
 
     abstract boolean isClient(List<String> orderedParameterKeys, Map<String, String> parameters);
 
+    boolean isClient(List<Statement> statements) {
+      return false;
+    }
+
     public ImmutableList<LocalStatement> getLocalStatements(ConnectionHandler connectionHandler) {
       if (connectionHandler.getServer().getOptions().useDefaultLocalStatements()) {
         return DEFAULT_LOCAL_STATEMENTS;
@@ -151,4 +184,18 @@ public class ClientAutoDetector {
     // The following line should never be reached.
     throw new IllegalStateException("UNSPECIFIED.isClient() should have returned true");
   }
+
+  /**
+   * Returns the {@link WellKnownClient} that the detector thinks is connected to PGAdapter based on
+   * the given list of SQL statements that have been executed.
+   */
+  public static @Nonnull WellKnownClient detectClient(List<Statement> statements) {
+    for (WellKnownClient client : WellKnownClient.values()) {
+      if (client.isClient(statements)) {
+        return client;
+      }
+    }
+    // The following line should never be reached.
+    throw new IllegalStateException("UNSPECIFIED.isClient() should have returned true");
+  }
 }
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 cd39c6dd..00a5dffd 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
@@ -36,6 +36,7 @@ public class QueryMessage extends ControlMessage {
   public QueryMessage(ConnectionHandler connection) throws Exception {
     super(connection);
     this.originalStatement = Statement.of(this.readAll());
+    connection.maybeDetermineWellKnownClient(this.originalStatement);
     this.simpleQueryStatement =
         new SimpleQueryStatement(
             connection.getServer().getOptions(), this.originalStatement, this.connection);


Test patch:
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 f18866d7..e5a5446e 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/ConnectionHandlerTest.java
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.when;
 
 import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler.ConnectionStatus;
 import com.google.cloud.spanner.pgadapter.error.PGException;
@@ -36,6 +37,7 @@ import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.SslMode;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePortalStatement;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePreparedStatement;
+import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.WellKnownClient;
 import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
@@ -528,4 +530,66 @@ public class ConnectionHandlerTest {
 
     assertTrue(connectionHandler.checkValidConnection(false));
   }
+
+  @Test
+  public void testMaybeDetermineWellKnownClient_remainsUnspecifiedForUnknownStatement() {
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    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);
+    ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
+
+    when(options.shouldAutoDetectClient()).thenReturn(true);
+    connectionHandler.setWellKnownClient(WellKnownClient.UNSPECIFIED);
+
+    connectionHandler.maybeDetermineWellKnownClient(Statement.of("select 1"));
+
+    assertEquals(WellKnownClient.UNSPECIFIED, connectionHandler.getWellKnownClient());
+  }
+
+  @Test
+  public void testMaybeDetermineWellKnownClient_changesFromUnspecifiedWithKnownStatement() {
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    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);
+    ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
+
+    when(options.shouldAutoDetectClient()).thenReturn(true);
+    connectionHandler.setWellKnownClient(WellKnownClient.UNSPECIFIED);
+
+    connectionHandler.maybeDetermineWellKnownClient(
+        Statement.of(
+            "SELECT version();\n"
+                + "\n"
+                + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"));
+
+    assertEquals(WellKnownClient.NPGSQL, connectionHandler.getWellKnownClient());
+  }
+
+  @Test
+  public void testMaybeDetermineWellKnownClient_respectsAutoDetectClientSetting() {
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    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);
+    ConnectionHandler connectionHandler = new ConnectionHandler(server, socket);
+
+    when(options.shouldAutoDetectClient()).thenReturn(false);
+    connectionHandler.setWellKnownClient(WellKnownClient.UNSPECIFIED);
+
+    connectionHandler.maybeDetermineWellKnownClient(
+        Statement.of(
+            "SELECT version();\n"
+                + "\n"
+                + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"));
+
+    assertEquals(WellKnownClient.UNSPECIFIED, connectionHandler.getWellKnownClient());
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java
index 1f40ffd6..c117a0a9 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/csharp/AbstractNpgsqlMockServerTest.java
@@ -51,163 +51,17 @@ public abstract class AbstractNpgsqlMockServerTest extends AbstractMockServerTes
       com.google.spanner.v1.ResultSet.newBuilder()
           .addRows(
               ListValue.newBuilder()
-                  .addValues(
-                      Value.newBuilder()
-                          .setStringValue(
-                              "PostgreSQL 13.4 on x86_64-apple-darwin, compiled by Apple clang version 11.0.3 (clang-1103.0.32.59), 64-bit")
-                          .build())
+                  .addValues(Value.newBuilder().setStringValue("PostgreSQL 14.1").build())
                   .build())
           .setMetadata(SELECT_VERSION_METADATA)
           .build();
-  private static final Statement SELECT_TYPES_1_0 =
-      Statement.of(
-          "with pg_namespace as (\n"
-              + "  select case schema_name when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as oid,\n"
-              + "        schema_name as nspname, null as nspowner, null as nspacl\n"
-              + "  from information_schema.schemata\n"
-              + "),\n"
-              + "pg_type as (\n"
-              + "  select 16 as oid, 'bool' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 1 as typlen, true as typbyval, 'b' as typtype, 'B' as typcategory, true as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1000 as typarray, 'boolin' as typinput, 'boolout' as typoutput, 'boolrecv' as typreceive, 'boolsend' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'c' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 17 as oid, 'bytea' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'U' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1001 as typarray, 'byteain' as typinput, 'byteaout' as typoutput, 'bytearecv' as typreceive, 'byteasend' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'x' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 20 as oid, 'int8' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 8 as typlen, true as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1016 as typarray, 'int8in' as typinput, 'int8out' as typoutput, 'int8recv' as typreceive, 'int8send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'd' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 21 as oid, 'int2' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 2 as typlen, true as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, false as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1005 as typarray, 'int2in' as typinput, 'int2out' as typoutput, 'int2recv' as typreceive, 'int2send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 's' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 23 as oid, 'int4' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 4 as typlen, true as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, false as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1007 as typarray, 'int4in' as typinput, 'int4out' as typoutput, 'int4recv' as typreceive, 'int4send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 25 as oid, 'text' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'S' as typcategory, true as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1009 as typarray, 'textin' as typinput, 'textout' as typoutput, 'textrecv' as typreceive, 'textsend' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'x' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 100 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 700 as oid, 'float4' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 4 as typlen, true as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, false as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1021 as typarray, 'float4in' as typinput, 'float4out' as typoutput, 'float4recv' as typreceive, 'float4send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 701 as oid, 'float8' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 8 as typlen, true as typbyval, 'b' as typtype, 'N' as typcategory, true as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1022 as typarray, 'float8in' as typinput, 'float8out' as typoutput, 'float8recv' as typreceive, 'float8send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'd' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 1043 as oid, 'varchar' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'S' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1015 as typarray, 'varcharin' as typinput, 'varcharout' as typoutput, 'varcharrecv' as typreceive, 'varcharsend' as typsend, 'varchartypmodin' as typmodin, 'varchartypmodout' as typmodout, '-' as typanalyze, 'i' as typalign, 'x' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 100 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 1082 as oid, 'date' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 4 as typlen, true as typbyval, 'b' as typtype, 'D' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1182 as typarray, 'date_in' as typinput, 'date_out' as typoutput, 'date_recv' as typreceive, 'date_send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 1114 as oid, 'timestamp' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 8 as typlen, true as typbyval, 'b' as typtype, 'D' as typcategory, false as typispreferred, false as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1115 as typarray, 'timestamp_in' as typinput, 'timestamp_out' as typoutput, 'timestamp_recv' as typreceive, 'timestamp_send' as typsend, 'timestamptypmodin' as typmodin, 'timestamptypmodout' as typmodout, '-' as typanalyze, 'd' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 1184 as oid, 'timestamptz' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, 8 as typlen, true as typbyval, 'b' as typtype, 'D' as typcategory, true as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1185 as typarray, 'timestamptz_in' as typinput, 'timestamptz_out' as typoutput, 'timestamptz_recv' as typreceive, 'timestamptz_send' as typsend, 'timestamptztypmodin' as typmodin, 'timestamptztypmodout' as typmodout, '-' as typanalyze, 'd' as typalign, 'p' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 1700 as oid, 'numeric' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'N' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 1231 as typarray, 'numeric_in' as typinput, 'numeric_out' as typoutput, 'numeric_recv' as typreceive, 'numeric_send' as typsend, 'numerictypmodin' as typmodin, 'numerictypmodout' as typmodout, '-' as typanalyze, 'i' as typalign, 'm' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl union all\n"
-              + "  select 3802 as oid, 'jsonb' as typname, (select oid from pg_namespace where nspname='pg_catalog') as typnamespace, null as typowner, -1 as typlen, false as typbyval, 'b' as typtype, 'U' as typcategory, false as typispreferred, true as typisdefined, ',' as typdelim, 0 as typrelid, 0 as typelem, 3807 as typarray, 'jsonb_in' as typinput, 'jsonb_out' as typoutput, 'jsonb_recv' as typreceive, 'jsonb_send' as typsend, '-' as typmodin, '-' as typmodout, '-' as typanalyze, 'i' as typalign, 'x' as typstorage, false as typnotnull, 0 as typbasetype, -1 as typtypmod, 0 as typndims, 0 as typcollation, null as typdefaultbin, null as typdefault, null as typacl\n"
-              + "),\n"
-              + "pg_class as (\n"
-              + "  select\n"
-              + "  -1 as oid,\n"
-              + "  table_name as relname,\n"
-              + "  case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
-              + "  0 as reltype,\n"
-              + "  0 as reloftype,\n"
-              + "  0 as relowner,\n"
-              + "  1 as relam,\n"
-              + "  0 as relfilenode,\n"
-              + "  0 as reltablespace,\n"
-              + "  0 as relpages,\n"
-              + "  0.0::float8 as reltuples,\n"
-              + "  0 as relallvisible,\n"
-              + "  0 as reltoastrelid,\n"
-              + "  false as relhasindex,\n"
-              + "  false as relisshared,\n"
-              + "  'p' as relpersistence,\n"
-              + "  'r' as relkind,\n"
-              + "  count(*) as relnatts,\n"
-              + "  0 as relchecks,\n"
-              + "  false as relhasrules,\n"
-              + "  false as relhastriggers,\n"
-              + "  false as relhassubclass,\n"
-              + "  false as relrowsecurity,\n"
-              + "  false as relforcerowsecurity,\n"
-              + "  true as relispopulated,\n"
-              + "  'n' as relreplident,\n"
-              + "  false as relispartition,\n"
-              + "  0 as relrewrite,\n"
-              + "  0 as relfrozenxid,\n"
-              + "  0 as relminmxid,\n"
-              + "  '{}'::bigint[] as relacl,\n"
-              + "  '{}'::text[] as reloptions,\n"
-              + "  0 as relpartbound\n"
-              + "from information_schema.tables t\n"
-              + "inner join information_schema.columns using (table_catalog, table_schema, table_name)\n"
-              + "group by t.table_name, t.table_schema\n"
-              + "union all\n"
-              + "select\n"
-              + "    -1 as oid,\n"
-              + "    i.index_name as relname,\n"
-              + "    case table_schema when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as relnamespace,\n"
-              + "    0 as reltype,\n"
-              + "    0 as reloftype,\n"
-              + "    0 as relowner,\n"
-              + "    1 as relam,\n"
-              + "    0 as relfilenode,\n"
-              + "    0 as reltablespace,\n"
-              + "    0 as relpages,\n"
-              + "    0.0::float8 as reltuples,\n"
-              + "    0 as relallvisible,\n"
-              + "    0 as reltoastrelid,\n"
-              + "    false as relhasindex,\n"
-              + "    false as relisshared,\n"
-              + "    'p' as relpersistence,\n"
-              + "    'r' as relkind,\n"
-              + "    count(*) as relnatts,\n"
-              + "    0 as relchecks,\n"
-              + "    false as relhasrules,\n"
-              + "    false as relhastriggers,\n"
-              + "    false as relhassubclass,\n"
-              + "    false as relrowsecurity,\n"
-              + "    false as relforcerowsecurity,\n"
-              + "    true as relispopulated,\n"
-              + "    'n' as relreplident,\n"
-              + "    false as relispartition,\n"
-              + "    0 as relrewrite,\n"
-              + "    0 as relfrozenxid,\n"
-              + "    0 as relminmxid,\n"
-              + "    '{}'::bigint[] as relacl,\n"
-              + "    '{}'::text[] as reloptions,\n"
-              + "    0 as relpartbound\n"
-              + "from information_schema.indexes i\n"
-              + "inner join information_schema.index_columns using (table_catalog, table_schema, table_name)\n"
-              + "group by i.index_name, i.table_schema\n"
-              + ")\n"
-              + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"
-              + "FROM (\n"
-              + "    -- Arrays have typtype=b - this subquery identifies them by their typreceive and converts their typtype to a\n"
-              + "    -- We first do this for the type (innerest-most subquery), and then for its element type\n"
-              + "    -- This also returns the array element, range subtype and domain base type as elemtypoid\n"
-              + "    SELECT\n"
-              + "        typ.oid, typ.typnamespace, typ.typname, typ.typtype, typ.typrelid, typ.typnotnull, typ.relkind,\n"
-              + "        elemtyp.oid AS elemtypoid, elemtyp.typname AS elemtypname, elemcls.relkind AS elemrelkind,\n"
-              + "        CASE WHEN elemproc.proname='array_recv' THEN 'a' ELSE elemtyp.typtype END AS elemtyptype\n"
-              + "    FROM (\n"
-              + "        SELECT typ.oid, typnamespace, typname, typrelid, typnotnull, relkind, typelem AS elemoid,\n"
-              + "            CASE WHEN proc.proname='array_recv' THEN 'a' ELSE typ.typtype END AS typtype,\n"
-              + "            CASE\n"
-              + "                WHEN proc.proname='array_recv' THEN typ.typelem\n"
-              + "                \n"
-              + "                \n"
-              + "                WHEN typ.typtype='d' THEN typ.typbasetype\n"
-              + "            END AS elemtypoid\n"
-              + "        FROM pg_type AS typ\n"
-              + "        LEFT JOIN pg_class AS cls ON (cls.oid = typ.typrelid)\n"
-              + "        LEFT JOIN pg_proc AS proc ON proc.oid = typ.typreceive\n"
-              + "        \n"
-              + "    ) AS typ\n"
-              + "    LEFT JOIN pg_type AS elemtyp ON elemtyp.oid = elemtypoid\n"
-              + "    LEFT JOIN pg_class AS elemcls ON (elemcls.oid = elemtyp.typrelid)\n"
-              + "    LEFT JOIN pg_proc AS elemproc ON elemproc.oid = elemtyp.typreceive\n"
-              + ") AS t\n"
-              + "JOIN pg_namespace AS ns ON (ns.oid = typnamespace)\n"
-              + "WHERE\n"
-              + "    typtype IN ('b', 'r', 'm', 'e', 'd') OR -- Base, range, multirange, enum, domain\n"
-              + "    (typtype = 'c' AND relkind='c') OR -- User-defined free-standing composites (not table composites) by default\n"
-              + "    (typtype = 'p' AND typname IN ('record', 'void')) OR -- Some special supported pseudo-types\n"
-              + "    (typtype = 'a' AND (  -- Array of...\n"
-              + "        elemtyptype IN ('b', 'r', 'm', 'e', 'd') OR -- Array of base, range, multirange, enum, domain\n"
-              + "        (elemtyptype = 'p' AND elemtypname IN ('record', 'void')) OR -- Arrays of special supported pseudo-types\n"
-              + "        (elemtyptype = 'c' AND elemrelkind='c') -- Array of user-defined free-standing composites (not table composites) by default\n"
-              + "    ))\n"
-              + "ORDER BY CASE\n"
-              + "       WHEN typtype IN ('b', 'e', 'p') THEN 0           -- First base types, enums, pseudo-types\n"
-              + "       WHEN typtype = 'r' THEN 1                        -- Ranges after\n"
-              + "       WHEN typtype = 'm' THEN 2                        -- Multiranges after\n"
-              + "       WHEN typtype = 'c' THEN 3                        -- Composites after\n"
-              + "       WHEN typtype = 'd' AND elemtyptype <> 'a' THEN 4 -- Domains over non-arrays after\n"
-              + "       WHEN typtype = 'a' THEN 5                        -- Arrays after\n"
-              + "       WHEN typtype = 'd' AND elemtyptype = 'a' THEN 6  -- Domains over arrays last\n"
-              + "END");
+
   private static final Statement SELECT_TYPES_14_1 =
       Statement.of(
-          "with pg_namespace as (\n"
+          "with pg_range as (\n"
+              + "select * from (select 0::bigint as rngtypid, 0::bigint as rngsubtype, 0::bigint as rngmultitypid, 0::bigint as rngcollation, 0::bigint as rngsubopc, ''::varchar as rngcanonical, ''::varchar as rngsubdiff\n"
+              + ") range where false),\n"
+              + "pg_namespace as (\n"
               + "  select case schema_name when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as oid,\n"
               + "        schema_name as nspname, null as nspowner, null as nspacl\n"
               + "  from information_schema.schemata\n"
@@ -304,7 +158,10 @@ public abstract class AbstractNpgsqlMockServerTest extends AbstractMockServerTes
               + "from information_schema.indexes i\n"
               + "inner join information_schema.index_columns using (table_catalog, table_schema, table_name)\n"
               + "group by i.index_name, i.table_schema\n"
-              + ")\n"
+              + "),\n"
+              + "pg_proc as (\n"
+              + "select * from (select 0::bigint as oid, ''::varchar as proname, 0::bigint as pronamespace, 0::bigint as proowner, 0::bigint as prolang, 0.0::float8 as procost, 0.0::float8 as prorows, 0::bigint as provariadic, ''::varchar as prosupport, ''::varchar as prokind, false::bool as prosecdef, false::bool as proleakproof, false::bool as proisstrict, false::bool as proretset, ''::varchar as provolatile, ''::varchar as proparallel, 0::bigint as pronargs, 0::bigint as pronargdefaults, 0::bigint as prorettype, 0::bigint as proargtypes, '{}'::bigint[] as proallargtypes, '{}'::varchar[] as proargmodes, '{}'::text[] as proargnames, ''::varchar as proargdefaults, '{}'::bigint[] as protrftypes, ''::text as prosrc, ''::text as probin, ''::varchar as prosqlbody, '{}'::text[] as proconfig, '{}'::bigint[] as proacl\n"
+              + ") proc where false)\n"
               + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"
               + "FROM (\n"
               + "    -- Arrays have typtype=b - this subquery identifies them by their typreceive and converts their typtype to a\n"
@@ -712,7 +569,6 @@ public abstract class AbstractNpgsqlMockServerTest extends AbstractMockServerTes
     mockSpanner.putStatementResult(StatementResult.query(SELECT_VERSION, SELECT_VERSION_RESULTSET));
     mockSpanner.putStatementResult(
         StatementResult.query(SELECT_TYPES_14_1, SELECT_TYPES_RESULTSET));
-    mockSpanner.putStatementResult(StatementResult.query(SELECT_TYPES_1_0, SELECT_TYPES_RESULTSET));
     mockSpanner.putStatementResult(
         StatementResult.query(SELECT_ATTRIBUTES, SELECT_ATTRIBUTES_RESULTSET));
     mockSpanner.putStatementResult(
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/csharp/NpgsqlMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/csharp/NpgsqlMockServerTest.java
index ab129356..4928f8fe 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/csharp/NpgsqlMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/csharp/NpgsqlMockServerTest.java
@@ -23,7 +23,7 @@ import com.google.cloud.Date;
 import com.google.cloud.Timestamp;
 import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
 import com.google.cloud.spanner.Statement;
-import com.google.cloud.spanner.pgadapter.golang.GolangTest;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.protobuf.ListValue;
 import com.google.protobuf.Value;
 import com.google.spanner.v1.CommitRequest;
@@ -46,27 +46,29 @@ import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
-@Category(GolangTest.class)
+@Category(DotnetTest.class)
 @RunWith(Parameterized.class)
 public class NpgsqlMockServerTest extends AbstractNpgsqlMockServerTest {
 
-  @Parameter public String pgVersion;
+  @Parameter public String host;
 
-  @Parameters(name = "pgVersion = {0}")
+  @Parameters(name = "host = {0}")
   public static Object[] data() {
-    return new Object[] {"1.0", "14.1"};
+    OptionsMetadata options = new OptionsMetadata(new String[] {"-p p", "-i i"});
+    return options.isDomainSocketEnabled()
+        ? new Object[] {"localhost", "/tmp"}
+        : new Object[] {"localhost"};
   }
 
   private String createConnectionString() {
     return String.format(
-        "Host=localhost;Port=%d;Database=d;SSL Mode=Disable;Options=-c server_version=%s",
-        pgServer.getLocalPort(), pgVersion);
+        "Host=%s;Port=%d;Database=d;SSL Mode=Disable", host, pgServer.getLocalPort());
   }
 
   @Test
   public void testShowServerVersion() throws IOException, InterruptedException {
     String result = execute("TestShowServerVersion", createConnectionString());
-    assertEquals(pgVersion + "\n", result);
+    assertEquals("14.1\n", result);
   }
 
   @Test
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 b3ab055b..8b6fd843 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
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.ProxyServer;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
@@ -243,4 +244,51 @@ public class ClientAutoDetectorTest {
       }
     }
   }
+
+  @Test
+  public void testNpgsql() {
+    assertNotEquals(
+        WellKnownClient.NPGSQL,
+        ClientAutoDetector.detectClient(ImmutableList.of(), ImmutableMap.of()));
+    assertNotEquals(
+        WellKnownClient.NPGSQL,
+        ClientAutoDetector.detectClient(
+            ImmutableList.of("some-param"), ImmutableMap.of("some-param", "some-value")));
+
+    ConnectionHandler connectionHandler = mock(ConnectionHandler.class);
+    ProxyServer server = mock(ProxyServer.class);
+    OptionsMetadata options = mock(OptionsMetadata.class);
+    when(connectionHandler.getServer()).thenReturn(server);
+    when(server.getOptions()).thenReturn(options);
+    for (boolean useDefaultLocalStatements : new boolean[] {true, false}) {
+      when(options.useDefaultLocalStatements()).thenReturn(useDefaultLocalStatements);
+      if (useDefaultLocalStatements) {
+        assertEquals(
+            DEFAULT_LOCAL_STATEMENTS, WellKnownClient.NPGSQL.getLocalStatements(connectionHandler));
+      } else {
+        assertEquals(
+            EMPTY_LOCAL_STATEMENTS, WellKnownClient.NPGSQL.getLocalStatements(connectionHandler));
+      }
+    }
+    assertEquals(
+        WellKnownClient.NPGSQL,
+        ClientAutoDetector.detectClient(
+            ImmutableList.of(
+                Statement.of(
+                    "SELECT version();\n"
+                        + "\n"
+                        + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"))));
+    assertEquals(
+        WellKnownClient.UNSPECIFIED,
+        ClientAutoDetector.detectClient(ImmutableList.of(Statement.of("SELECT version()"))));
+    assertEquals(
+        WellKnownClient.UNSPECIFIED,
+        ClientAutoDetector.detectClient(
+            ImmutableList.of(
+                Statement.of(
+                    "SELECT version();\n"
+                        + "\n"
+                        + "SELECT ns.nspname, t.oid, t.typname, t.typtype, t.typnotnull, t.elemtypoid\n"),
+                Statement.of("SELECT version();"))));
+  }
 }