Repository: GoogleCloudPlatform/pgadapter
Pull request #334 (2022-08-24 13:33:20)

feat: add ddl_transaction_mode and replace_pg_catalog_tables settings
Adds the ddl_transaction_mode and replace_pg_catalog_tables settings
from a purely server setting to one that can be set in session state.
This makes the features a lot more usable, as they can be set on a
per-session basis, and also set in for example the connection string.
This will for example enable the use of DDL transaction mode Explicit
for Liquibase.

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

```diff
@@                   Coverage Diff                    @@
##             postgresql-dialect     #334      +/-   ##
========================================================
- Coverage                 83.95%   83.73%   -0.23%     
- Complexity                 1593     1597       +4     
========================================================
  Files                       114      114              
  Lines                      5417     5446      +29     
  Branches                    727      730       +3     
========================================================
+ Hits                       4548     4560      +12     
- Misses                      643      654      +11     
- Partials                    226      232       +6     
```

| Flag | Coverage Δ | |
|---|---|---|
| all_tests | `83.73% <57.57%> (-0.23%)` | :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/334?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/session/PGSetting.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/334/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3Nlc3Npb24vUEdTZXR0aW5nLmphdmE=) | `92.54% <0.00%> (-0.58%)` | :arrow_down: |
| [.../cloud/spanner/pgadapter/session/SessionState.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/334/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3Nlc3Npb24vU2Vzc2lvblN0YXRlLmphdmE=) | `86.56% <56.66%> (-12.48%)` | :arrow_down: |
| [...panner/pgadapter/statements/BackendConnection.java](https://codecov.io/gh/GoogleCloudPlatform/pgadapter/pull/334/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleCloudPlatform#diff-c3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3NwYW5uZXIvcGdhZGFwdGVyL3N0YXRlbWVudHMvQmFja2VuZENvbm5lY3Rpb24uamF2YQ==) | `90.86% <100.00%> (+0.19%)` | :arrow_up: |

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


Patch:
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java b/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
index 48e21f56..840d8a07 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/session/PGSetting.java
@@ -19,6 +19,7 @@ import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -160,7 +161,8 @@ public class PGSetting {
     this.enumVals = null;
   }
 
-  private PGSetting(
+  @VisibleForTesting
+  PGSetting(
       String extension,
       String name,
       String category,
@@ -450,4 +452,8 @@ public class PGSetting {
   public String getResetVal() {
     return resetVal;
   }
+
+  public String getBootVal() {
+    return bootVal;
+  }
 }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
index 35240efb..d48c1e27 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/session/SessionState.java
@@ -19,6 +19,10 @@ import com.google.cloud.spanner.ErrorCode;
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.SpannerExceptionFactory;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.DdlTransactionMode;
+import com.google.cloud.spanner.pgadapter.parsers.BooleanParser;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -31,6 +35,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.stream.Collectors;
 
 /** {@link SessionState} contains all session variables for a connection. */
@@ -57,7 +62,9 @@ public class SessionState {
           "server_version_num",
           "TimeZone",
           "transaction_isolation",
-          "transaction_read_only");
+          "transaction_read_only",
+          "spanner.ddl_transaction_mode",
+          "spanner.replace_pg_catalog_tables");
 
   private static final Map<String, PGSetting> SERVER_SETTINGS = new HashMap<>();
 
@@ -75,12 +82,29 @@ public class SessionState {
   private Map<String, PGSetting> localSettings;
 
   public SessionState(OptionsMetadata options) {
-    this.settings = new HashMap<>(SERVER_SETTINGS.size());
-    for (Entry<String, PGSetting> entry : SERVER_SETTINGS.entrySet()) {
+    this(SERVER_SETTINGS, options);
+  }
+
+  @VisibleForTesting
+  SessionState(Map<String, PGSetting> serverSettings, OptionsMetadata options) {
+    this.settings = new HashMap<>(serverSettings.size());
+    for (Entry<String, PGSetting> entry : serverSettings.entrySet()) {
       this.settings.put(entry.getKey(), entry.getValue().copy());
     }
-    this.settings.get("server_version").initSettingValue(options.getServerVersion());
-    this.settings.get("server_version_num").initSettingValue(options.getServerVersionNum());
+    initSettingValue("server_version", options.getServerVersion());
+    initSettingValue("server_version_num", options.getServerVersionNum());
+    initSettingValue(
+        "spanner.ddl_transaction_mode",
+        MoreObjects.firstNonNull(options.getDdlTransactionMode(), DdlTransactionMode.Batch).name());
+    initSettingValue(
+        "spanner.replace_pg_catalog_tables", Boolean.toString(options.replacePgCatalogTables()));
+  }
+
+  void initSettingValue(String key, String value) {
+    PGSetting setting = this.settings.get(key);
+    if (setting != null) {
+      setting.initSettingValue(value);
+    }
   }
 
   /**
@@ -205,10 +229,10 @@ public class SessionState {
 
   /** Returns the current value of the specified setting. */
   public PGSetting get(String extension, String name) {
-    return internalGet(toKey(extension, name));
+    return internalGet(toKey(extension, name), true);
   }
 
-  private PGSetting internalGet(String key) {
+  private PGSetting internalGet(String key, boolean throwForUnknownParam) {
     if (localSettings != null && localSettings.containsKey(key)) {
       return localSettings.get(key);
     }
@@ -218,7 +242,10 @@ public class SessionState {
     if (settings.containsKey(key)) {
       return settings.get(key);
     }
-    throw unknownParamError(key);
+    if (throwForUnknownParam) {
+      throw unknownParamError(key);
+    }
+    return null;
   }
 
   /** Returns all settings and their current values. */
@@ -237,7 +264,7 @@ public class SessionState {
                     ? Collections.emptySet()
                     : transactionSettings.keySet()));
     for (String key : keys) {
-      result.add(internalGet(key));
+      result.add(internalGet(key, true));
     }
     result.sort(Comparator.comparing(PGSetting::getCasePreservingKey));
     return result;
@@ -277,4 +304,49 @@ public class SessionState {
     this.localSettings = null;
     this.transactionSettings = null;
   }
+
+  /** Returns the current setting for replacing pg_catalog tables with common table expressions. */
+  public boolean isReplacePgCatalogTables() {
+    PGSetting setting = internalGet(toKey("spanner", "replace_pg_catalog_tables"), false);
+    if (setting == null) {
+      return true;
+    }
+    return tryGetFirstNonNull(
+        true,
+        () -> BooleanParser.toBoolean(setting.getSetting()),
+        () -> BooleanParser.toBoolean(setting.getResetVal()),
+        () -> BooleanParser.toBoolean(setting.getBootVal()));
+  }
+
+  /** Returns the {@link DdlTransactionMode} that is used for this connection at this time. */
+  public DdlTransactionMode getDdlTransactionMode() {
+    PGSetting setting = internalGet(toKey("spanner", "ddl_transaction_mode"), false);
+    if (setting == null) {
+      return DdlTransactionMode.Batch;
+    }
+    return tryGetFirstNonNull(
+        DdlTransactionMode.Batch,
+        () -> DdlTransactionMode.valueOf(setting.getSetting()),
+        () -> DdlTransactionMode.valueOf(setting.getResetVal()),
+        () -> DdlTransactionMode.valueOf(setting.getBootVal()));
+  }
+
+  @SafeVarargs
+  static <T> T tryGetFirstNonNull(T defaultResult, Callable<T>... callables) {
+    T value;
+    for (Callable<T> callable : callables) {
+      if ((value = tryGet(callable)) != null) {
+        return value;
+      }
+    }
+    return defaultResult;
+  }
+
+  static <T> T tryGet(Callable<T> callable) {
+    try {
+      return callable.call();
+    } catch (Throwable ignored) {
+      return null;
+    }
+  }
 }
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 445dea5d..5c1d7cc6 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
@@ -205,7 +205,7 @@ public class BackendConnection {
         } else {
           // Potentially replace pg_catalog table references with common table expressions.
           Statement updatedStatement =
-              optionsMetadata.replacePgCatalogTables()
+              sessionState.isReplacePgCatalogTables()
                   ? pgCatalog.replacePgCatalogTables(statement)
                   : statement;
           result.set(spannerConnection.execute(updatedStatement));
@@ -381,7 +381,6 @@ public class BackendConnection {
   private final Connection spannerConnection;
   private final DatabaseId databaseId;
   private final DdlExecutor ddlExecutor;
-  private final OptionsMetadata optionsMetadata;
 
   /**
    * Creates a PG backend connection that uses the given Spanner {@link Connection} and {@link
@@ -397,7 +396,6 @@ public class BackendConnection {
     this.spannerConnection = spannerConnection;
     this.databaseId = databaseId;
     this.ddlExecutor = new DdlExecutor(databaseId, this);
-    this.optionsMetadata = optionsMetadata;
     if (localStatements.isEmpty()) {
       this.localStatements = EMPTY_LOCAL_STATEMENTS;
     } else {
@@ -510,11 +508,6 @@ public class BackendConnection {
     return this.databaseId.getDatabase();
   }
 
-  /** Returns the options that are used for this connection. */
-  public OptionsMetadata getOptionsMetadata() {
-    return this.optionsMetadata;
-  }
-
   /**
    * Flushes all buffered statements to Cloud Spanner and commits/rolls back the transaction at the
    * end if isSync=true.
@@ -655,7 +648,7 @@ public class BackendConnection {
    * allowed.
    */
   private void prepareExecuteDdl(BufferedStatement<?> bufferedStatement) {
-    DdlTransactionMode ddlTransactionMode = this.optionsMetadata.getDdlTransactionMode();
+    DdlTransactionMode ddlTransactionMode = sessionState.getDdlTransactionMode();
     try {
       // Single statements are simpler to check, so we do that in a separate check.
       if (bufferedStatements.size() == 1) {
diff --git a/src/main/resources/com/google/cloud/spanner/pgadapter/session/pg_settings.txt b/src/main/resources/com/google/cloud/spanner/pgadapter/session/pg_settings.txt
index d2a6fe1a..6b5e0acb 100644
--- a/src/main/resources/com/google/cloud/spanner/pgadapter/session/pg_settings.txt
+++ b/src/main/resources/com/google/cloud/spanner/pgadapter/session/pg_settings.txt
@@ -343,3 +343,5 @@ work_mem	4096	kB	Resource Usage / Memory	Sets the maximum memory to be used for
 xmlbinary	base64	\N	Client Connection Defaults / Statement Behavior	Sets how binary values are to be encoded in XML.	\N	user	enum	default	\N	\N	{base64,hex}	base64	base64	\N	\N	f
 xmloption	content	\N	Client Connection Defaults / Statement Behavior	Sets whether XML data in implicit parsing and serialization operations is to be considered as documents or content fragments.	\N	user	enum	default	\N	\N	{content,document}	content	content	\N	\N	f
 zero_damaged_pages	off	\N	Developer Options	Continues processing past damaged page headers.	Detection of a damaged page header normally causes PostgreSQL to report an error, aborting the current transaction. Setting zero_damaged_pages to true causes the system to instead report a warning, zero out the damaged page, and continue processing. This behavior will destroy data, namely all the rows on the damaged page.	superuser	bool	default	\N	\N	\N	off	off	\N	\N	f
+spanner.ddl_transaction_mode	Batch	\N	PGAdapter Options	DDL transaction mode.	\N	user	enum	default	\N	\N	{Single,Batch,AutocommitImplicitTransaction,AutocommitExplicitTransaction}	Batch	Batch	\N	\N	f
+spanner.replace_pg_catalog_tables	on	\N	PGAdapter Options	Automatically replace pg_catalog table references with Common Table Expressions.	\N	user	bool	default	\N	\N	\N	on	on	\N	\N	f


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
index d1a4c30b..61876660 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java
@@ -2022,7 +2022,7 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
           }
           count++;
         }
-        assertEquals(345, count);
+        assertEquals(347, count);
       }
     }
   }
@@ -2176,6 +2176,59 @@ public class JdbcMockServerTest extends AbstractMockServerTest {
     }
   }
 
+  @Test
+  public void testDefaultReplacePgCatalogTables() throws SQLException {
+    try (Connection connection = DriverManager.getConnection(createUrl())) {
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("show spanner.replace_pg_catalog_tables")) {
+        assertTrue(resultSet.next());
+        assertTrue(resultSet.getBoolean(1));
+        assertFalse(resultSet.next());
+      }
+      mockSpanner.putStatementResult(
+          StatementResult.query(
+              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"
+                      + "select * from pg_namespace"),
+              SELECT1_RESULTSET));
+      // Just verify that this works, we don't care about the result.
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("select * from pg_catalog.pg_namespace")) {
+        //noinspection StatementWithEmptyBody
+        while (resultSet.next()) {}
+      }
+    }
+  }
+
+  @Test
+  public void testReplacePgCatalogTablesOff() throws SQLException {
+    try (Connection connection =
+        DriverManager.getConnection(
+            createUrl() + "?options=-c%20spanner.replace_pg_catalog_tables=off")) {
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("show spanner.replace_pg_catalog_tables")) {
+        assertTrue(resultSet.next());
+        assertFalse(resultSet.getBoolean(1));
+        assertFalse(resultSet.next());
+      }
+
+      // The query will now not be modified by PGAdapter before it is sent to Cloud Spanner.
+      mockSpanner.putStatementResult(
+          StatementResult.query(
+              Statement.of("select * from pg_catalog.pg_namespace"), SELECT1_RESULTSET));
+      // Just verify that this works, we don't care about the result.
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery("select * from pg_catalog.pg_namespace")) {
+        //noinspection StatementWithEmptyBody
+        while (resultSet.next()) {}
+      }
+    }
+  }
+
   private void verifySettingIsNull(Connection connection, String setting) throws SQLException {
     try (ResultSet resultSet =
         connection.createStatement().executeQuery(String.format("show %s", setting))) {
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
index b702be9f..333d93ee 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/session/SessionStateTest.java
@@ -14,16 +14,23 @@
 
 package com.google.cloud.spanner.pgadapter.session;
 
+import static com.google.cloud.spanner.pgadapter.session.SessionState.tryGet;
+import static com.google.cloud.spanner.pgadapter.session.SessionState.tryGetFirstNonNull;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.cloud.spanner.SpannerException;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
+import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.DdlTransactionMode;
 import com.google.cloud.spanner.pgadapter.statements.PgCatalog;
+import com.google.common.collect.ImmutableMap;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -149,7 +156,7 @@ public class SessionStateTest {
   public void testGetAll() {
     SessionState state = new SessionState(mock(OptionsMetadata.class));
     List<PGSetting> allSettings = state.getAll();
-    assertEquals(345, allSettings.size());
+    assertEquals(347, allSettings.size());
   }
 
   @Test
@@ -187,7 +194,7 @@ public class SessionStateTest {
     state.setLocal("spanner", "custom_local_setting", "value2");
 
     List<PGSetting> allSettings = state.getAll();
-    assertEquals(347, allSettings.size());
+    assertEquals(349, allSettings.size());
 
     PGSetting applicationName =
         allSettings.stream()
@@ -418,6 +425,10 @@ public class SessionStateTest {
         + "union all\n"
         + "select 'server_version_num' as name, null as setting, null as unit, 'Preset Options' as category, null as short_desc, null as extra_desc, 'internal' as context, 'integer' as vartype, '140001' as min_val, '140001' as max_val, null::text[] as enumvals, '140001' as boot_val, '140001' as reset_val, 'default' as source, null as sourcefile, null::bigint as sourceline, 'f'::boolean as pending_restart\n"
         + "union all\n"
+        + "select 'spanner.ddl_transaction_mode' as name, 'Batch' as setting, null as unit, 'PGAdapter Options' as category, null as short_desc, null as extra_desc, 'user' as context, 'enum' as vartype, null as min_val, null as max_val, '{\"Single\", \"Batch\", \"AutocommitImplicitTransaction\", \"AutocommitExplicitTransaction\"}'::text[] as enumvals, 'Batch' as boot_val, 'Batch' as reset_val, 'default' as source, null as sourcefile, null::bigint as sourceline, 'f'::boolean as pending_restart\n"
+        + "union all\n"
+        + "select 'spanner.replace_pg_catalog_tables' as name, 'false' as setting, null as unit, 'PGAdapter Options' as category, null as short_desc, null as extra_desc, 'user' as context, 'bool' as vartype, null as min_val, null as max_val, null::text[] as enumvals, 'on' as boot_val, 'on' as reset_val, 'default' as source, null as sourcefile, null::bigint as sourceline, 'f'::boolean as pending_restart\n"
+        + "union all\n"
         + "select 'transaction_isolation' as name, 'serializable' as setting, null as unit, 'Client Connection Defaults / Statement Behavior' as category, null as short_desc, null as extra_desc, 'user' as context, 'enum' as vartype, null as min_val, null as max_val, '{\"serializable\", \"repeatable read\", \"read committed\", \"read uncommitted\"}'::text[] as enumvals, 'serializable' as boot_val, 'serializable' as reset_val, 'override' as source, null as sourcefile, null::bigint as sourceline, 'f'::boolean as pending_restart\n"
         + "union all\n"
         + "select 'transaction_read_only' as name, 'off' as setting, null as unit, 'Client Connection Defaults / Statement Behavior' as category, null as short_desc, null as extra_desc, 'user' as context, 'bool' as vartype, null as min_val, null as max_val, null::text[] as enumvals, 'off' as boot_val, 'off' as reset_val, 'override' as source, null as sourcefile, null::bigint as sourceline, 'f'::boolean as pending_restart\n"
@@ -536,4 +547,207 @@ public class SessionStateTest {
             + ",\n/* This comment is preserved */  foo as (select * from bar)\nselect * from pg_settings",
         withSessionState.getSql());
   }
+
+  @Test
+  public void testTryGet() {
+    assertNull(
+        tryGet(
+            () -> {
+              throw new IllegalArgumentException();
+            }));
+    assertEquals("test", tryGet(() -> "test"));
+  }
+
+  @Test
+  public void testTryGetFirstNonNull() {
+    assertEquals("default", tryGetFirstNonNull("default"));
+    assertEquals("test", tryGetFirstNonNull("default", () -> "test"));
+    assertEquals("test", tryGetFirstNonNull("default", () -> null, () -> "test"));
+    assertEquals("default", tryGetFirstNonNull("default", () -> null, () -> null));
+  }
+
+  @Test
+  public void testIsReplacePgCatalogTables_noDefault() {
+    SessionState state = new SessionState(ImmutableMap.of(), mock(OptionsMetadata.class));
+    assertTrue(state.isReplacePgCatalogTables());
+  }
+
+  @Test
+  public void testIsReplacePgCatalogTables_defaultFalse() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    when(optionsMetadata.replacePgCatalogTables()).thenReturn(false);
+    SessionState state =
+        new SessionState(
+            ImmutableMap.of(
+                "spanner.replace_pg_catalog_tables",
+                new PGSetting("spanner", "replace_pg_catalog_tables")),
+            optionsMetadata);
+    assertFalse(state.isReplacePgCatalogTables());
+  }
+
+  @Test
+  public void testIsReplacePgCatalogTables_defaultTrue() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    when(optionsMetadata.replacePgCatalogTables()).thenReturn(true);
+    SessionState state =
+        new SessionState(
+            ImmutableMap.of(
+                "spanner.replace_pg_catalog_tables",
+                new PGSetting("spanner", "replace_pg_catalog_tables")),
+            optionsMetadata);
+    assertTrue(state.isReplacePgCatalogTables());
+  }
+
+  @Test
+  public void testIsReplacePgCatalogTables_resetVal() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    PGSetting setting =
+        new PGSetting(
+            "spanner",
+            "replace_pg_catalog_tables",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            /* resetVal = */ "off",
+            null,
+            null,
+            false);
+    SessionState state =
+        new SessionState(
+            ImmutableMap.of("spanner.replace_pg_catalog_tables", setting), optionsMetadata);
+    state.set("spanner", "replace_pg_catalog_tables", null);
+
+    assertFalse(state.isReplacePgCatalogTables());
+  }
+
+  @Test
+  public void testIsReplacePgCatalogTables_bootVal() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    PGSetting setting =
+        new PGSetting(
+            "spanner",
+            "replace_pg_catalog_tables",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            /* bootVal = */ "off",
+            null,
+            null,
+            null,
+            false);
+    SessionState state =
+        new SessionState(
+            ImmutableMap.of("spanner.replace_pg_catalog_tables", setting), optionsMetadata);
+    state.set("spanner", "replace_pg_catalog_tables", null);
+
+    assertFalse(state.isReplacePgCatalogTables());
+  }
+
+  @Test
+  public void testDdlTransactionMode_noDefault() {
+    SessionState state = new SessionState(ImmutableMap.of(), mock(OptionsMetadata.class));
+    assertEquals(DdlTransactionMode.Batch, state.getDdlTransactionMode());
+  }
+
+  @Test
+  public void testDdlTransactionMode_defaultSingle() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    when(optionsMetadata.getDdlTransactionMode()).thenReturn(DdlTransactionMode.Single);
+    SessionState state =
+        new SessionState(
+            ImmutableMap.of(
+                "spanner.ddl_transaction_mode", new PGSetting("spanner", "ddl_transaction_mode")),
+            optionsMetadata);
+    assertEquals(DdlTransactionMode.Single, state.getDdlTransactionMode());
+  }
+
+  @Test
+  public void testDdlTransactionMode_defaultExplicit() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    when(optionsMetadata.getDdlTransactionMode())
+        .thenReturn(DdlTransactionMode.AutocommitExplicitTransaction);
+    SessionState state =
+        new SessionState(
+            ImmutableMap.of(
+                "spanner.ddl_transaction_mode", new PGSetting("spanner", "ddl_transaction_mode")),
+            optionsMetadata);
+    assertEquals(DdlTransactionMode.AutocommitExplicitTransaction, state.getDdlTransactionMode());
+  }
+
+  @Test
+  public void testDdlTransactionMode_resetVal() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    PGSetting setting =
+        new PGSetting(
+            "spanner",
+            "ddl_transaction_mode",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            /* resetVal = */ "Single",
+            null,
+            null,
+            false);
+    SessionState state =
+        new SessionState(ImmutableMap.of("spanner.ddl_transaction_mode", setting), optionsMetadata);
+    state.set("spanner", "ddl_transaction_mode", null);
+
+    assertEquals(DdlTransactionMode.Single, state.getDdlTransactionMode());
+  }
+
+  @Test
+  public void testDdlTransactionMode_bootVal() {
+    OptionsMetadata optionsMetadata = mock(OptionsMetadata.class);
+    PGSetting setting =
+        new PGSetting(
+            "spanner",
+            "ddl_transaction_mode",
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            /* bootVal = */ "Single",
+            null,
+            null,
+            null,
+            false);
+    SessionState state =
+        new SessionState(ImmutableMap.of("spanner.ddl_transaction_mode", setting), optionsMetadata);
+    state.set("spanner", "ddl_transaction_mode", null);
+
+    assertEquals(DdlTransactionMode.Single, state.getDdlTransactionMode());
+  }
 }