diff --git a/java-spanner/google-cloud-spanner/clirr-ignored-differences.xml b/java-spanner/google-cloud-spanner/clirr-ignored-differences.xml
index cdcb343a6a01..6ae30aeebe3f 100644
--- a/java-spanner/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/java-spanner/google-cloud-spanner/clirr-ignored-differences.xml
@@ -1156,4 +1156,10 @@
com/google/cloud/spanner/spi/v1/ChannelFinder
* findServer(*)
+
+
+ 7012
+ com/google/cloud/spanner/DatabaseClient
+ com.google.spanner.v1.TransactionOptions$IsolationLevel getDatabaseDefaultIsolationLevel()
+
diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java
index 0293c7248d79..c73e38be1d7b 100644
--- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java
+++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java
@@ -16,6 +16,7 @@
package com.google.cloud.spanner;
+import com.google.api.core.InternalApi;
import com.google.api.gax.rpc.ServerStream;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Options.ReadOnlyTransactionOption;
@@ -42,6 +43,23 @@ default Dialect getDialect() {
throw new UnsupportedOperationException("method should be overwritten");
}
+ /**
+ * Returns the default transaction isolation level that is configured for the database with the
+ * {@code default_transaction_isolation} database option. Returns {@link
+ * IsolationLevel#ISOLATION_LEVEL_UNSPECIFIED} if the option has not been set, and {@link
+ * IsolationLevel#UNRECOGNIZED} if the option is set to a value that is not known to this version
+ * of the client.
+ *
+ *
The option is read once, together with the dialect of the database, and is cached for the
+ * lifetime of this client. Changes to the option after it has been read are not reflected.
+ *
+ * @return the default transaction isolation level configured for the database.
+ */
+ @InternalApi
+ default IsolationLevel getDatabaseDefaultIsolationLevel() {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
/**
* Returns the {@link DatabaseRole} used by the client connection. The database role that is used
* determines the access permissions that a connection has. This can for example be used to create
diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
index 66eea4a882ef..bbc282f7703a 100644
--- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
+++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
@@ -27,6 +27,7 @@
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.spanner.v1.BatchWriteResponse;
+import com.google.spanner.v1.TransactionOptions.IsolationLevel;
import io.opentelemetry.api.common.Attributes;
import java.util.HashMap;
import java.util.Map;
@@ -80,6 +81,11 @@ public Dialect getDialect() {
return this.multiplexedSessionDatabaseClient.getDialect();
}
+ @Override
+ public IsolationLevel getDatabaseDefaultIsolationLevel() {
+ return this.multiplexedSessionDatabaseClient.getDatabaseDefaultIsolationLevel();
+ }
+
private final AbstractLazyInitializer statementFactorySupplier =
new AbstractLazyInitializer() {
@Override
diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseMetadata.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseMetadata.java
new file mode 100644
index 000000000000..fd0e9aa624d7
--- /dev/null
+++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseMetadata.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2026 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;
+
+import com.google.spanner.v1.TransactionOptions.IsolationLevel;
+import java.util.Locale;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/** Database options that are read from {@code information_schema.database_options}. */
+@NullMarked
+final class DatabaseMetadata {
+ static final String DATABASE_DIALECT_OPTION = "database_dialect";
+ static final String DEFAULT_TRANSACTION_ISOLATION_OPTION = "default_transaction_isolation";
+
+ // The values of default_transaction_isolation. Spanner accepts any case when the option is set,
+ // and stores and returns these lower-case values for both GoogleSQL and PostgreSQL databases.
+ static final String SERIALIZABLE_VALUE = "serializable";
+ static final String REPEATABLE_READ_VALUE = "repeatable read";
+
+ /** Reads all the options in {@link DatabaseMetadata} with a single query. */
+ static final Statement STATEMENT =
+ Statement.of(
+ String.format(
+ "select option_name, option_value from information_schema.database_options "
+ + "where option_name in ('%s', '%s')",
+ DATABASE_DIALECT_OPTION, DEFAULT_TRANSACTION_ISOLATION_OPTION));
+
+ private final String dialect;
+ private final IsolationLevel defaultIsolationLevel;
+
+ private DatabaseMetadata(String dialect, IsolationLevel defaultIsolationLevel) {
+ this.dialect = dialect;
+ this.defaultIsolationLevel = defaultIsolationLevel;
+ }
+
+ /**
+ * Creates a {@link DatabaseMetadata} from the result of {@link #STATEMENT}. Options that are not
+ * in the result have not been set.
+ */
+ static DatabaseMetadata fromResultSet(ResultSet resultSet) {
+ String dialect = Dialect.GOOGLE_STANDARD_SQL.name();
+ IsolationLevel defaultIsolationLevel = IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED;
+ while (resultSet.next()) {
+ switch (resultSet.getString(0)) {
+ case DATABASE_DIALECT_OPTION:
+ dialect = resultSet.getString(1);
+ break;
+ case DEFAULT_TRANSACTION_ISOLATION_OPTION:
+ defaultIsolationLevel =
+ parseIsolationLevel(resultSet.isNull(1) ? null : resultSet.getString(1));
+ break;
+ default:
+ break;
+ }
+ }
+ return new DatabaseMetadata(dialect, defaultIsolationLevel);
+ }
+
+ /**
+ * Parses the value of the {@code default_transaction_isolation} option. {@code null} means that
+ * the option has not been set and returns {@link IsolationLevel#ISOLATION_LEVEL_UNSPECIFIED}.
+ * Values that are not known to this version of the client return {@link
+ * IsolationLevel#UNRECOGNIZED}.
+ */
+ private static IsolationLevel parseIsolationLevel(@Nullable String optionValue) {
+ if (optionValue == null) {
+ return IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED;
+ }
+ switch (optionValue.toLowerCase(Locale.ROOT)) {
+ case SERIALIZABLE_VALUE:
+ return IsolationLevel.SERIALIZABLE;
+ case REPEATABLE_READ_VALUE:
+ return IsolationLevel.REPEATABLE_READ;
+ default:
+ return IsolationLevel.UNRECOGNIZED;
+ }
+ }
+
+ Dialect getDialect() {
+ return Dialect.fromName(dialect);
+ }
+
+ IsolationLevel getDefaultIsolationLevel() {
+ return defaultIsolationLevel;
+ }
+}
diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java
index 03558aded997..b4c95a0309dc 100644
--- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java
+++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java
@@ -33,6 +33,7 @@
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Futures;
import com.google.spanner.v1.BatchWriteResponse;
+import com.google.spanner.v1.TransactionOptions.IsolationLevel;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
@@ -68,14 +69,6 @@ final class MultiplexedSessionDatabaseClient extends AbstractMultiplexedSessionD
*/
private static final int MAX_INITIAL_CREATE_SESSION_ATTEMPTS = 10;
- @VisibleForTesting
- static final Statement DETERMINE_DIALECT_STATEMENT =
- Statement.newBuilder(
- "select option_value "
- + "from information_schema.database_options "
- + "where option_name='database_dialect'")
- .build();
-
/**
* Represents a single transaction on a multiplexed session. This can be both a single-use or
* multi-use transaction, and both read/write or read-only transaction. This can be compared to a
@@ -534,17 +527,18 @@ private int getSingleUseChannelHint() {
}
}
- private final AbstractLazyInitializer dialectSupplier =
- new AbstractLazyInitializer() {
+ /**
+ * Reads the {@link DatabaseMetadata} with a single query the first time that it is needed, and
+ * caches it for the lifetime of this client. The metadata is not refreshed when the multiplexed
+ * session is replaced.
+ */
+ private final AbstractLazyInitializer databaseMetadataSupplier =
+ new AbstractLazyInitializer() {
@Override
- protected Dialect initialize() {
- try (ResultSet dialectResultSet = singleUse().executeQuery(DETERMINE_DIALECT_STATEMENT)) {
- if (dialectResultSet.next()) {
- return Dialect.fromName(dialectResultSet.getString(0));
- }
+ protected DatabaseMetadata initialize() {
+ try (ResultSet resultSet = singleUse().executeQuery(DatabaseMetadata.STATEMENT)) {
+ return DatabaseMetadata.fromResultSet(resultSet);
}
- // This should not really happen, but it is the safest fallback value.
- return Dialect.GOOGLE_STANDARD_SQL;
}
@Override
@@ -556,7 +550,7 @@ protected boolean shouldCacheError(Exception e) {
@Override
public Dialect getDialect() {
try {
- return dialectSupplier.get();
+ return databaseMetadataSupplier.get().getDialect();
} catch (Exception exception) {
throw SpannerExceptionFactory.asSpannerException(exception);
}
@@ -564,7 +558,16 @@ public Dialect getDialect() {
Future getDialectAsync() {
try {
- return MAINTAINER_SERVICE.submit(dialectSupplier::get);
+ return MAINTAINER_SERVICE.submit(() -> databaseMetadataSupplier.get().getDialect());
+ } catch (Exception exception) {
+ throw SpannerExceptionFactory.asSpannerException(exception);
+ }
+ }
+
+ @Override
+ public IsolationLevel getDatabaseDefaultIsolationLevel() {
+ try {
+ return databaseMetadataSupplier.get().getDefaultIsolationLevel();
} catch (Exception exception) {
throw SpannerExceptionFactory.asSpannerException(exception);
}
diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
index 02a3b4b80172..11d962af320e 100644
--- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
+++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
@@ -3212,7 +3212,7 @@ public void testGetDialect_RecoversAfterTransientCreateSessionFailure() throws E
public void testGetDialectAndStatementFactory_RecoverAfterTransientQueryFailure() {
mockSpanner.putStatementResult(
StatementResult.exception(
- MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT,
+ DatabaseMetadata.STATEMENT,
Status.DEADLINE_EXCEEDED
.withDescription("Transient dialect query timeout")
.asRuntimeException()));
@@ -3245,7 +3245,7 @@ public void testGetDialectAndStatementFactory_RecoverAfterTransientQueryFailure(
public void testGetDialect_CachesNonRetryableQueryFailure() {
mockSpanner.putStatementResult(
StatementResult.exception(
- MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT,
+ DatabaseMetadata.STATEMENT,
Status.PERMISSION_DENIED
.withDescription("Permission denied on information_schema")
.asRuntimeException()));
@@ -3263,6 +3263,56 @@ public void testGetDialect_CachesNonRetryableQueryFailure() {
}
}
+ @Test
+ public void testGetDatabaseDefaultIsolationLevelDefault() {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
+ assertEquals(
+ IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, client.getDatabaseDefaultIsolationLevel());
+ }
+
+ @Test
+ public void testGetDatabaseDefaultIsolationLevelRepeatableRead() {
+ mockSpanner.putStatementResult(
+ StatementResult.detectDialectResult(Dialect.POSTGRESQL, IsolationLevel.REPEATABLE_READ));
+ try {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
+ assertEquals(Dialect.POSTGRESQL, client.getDialect());
+ assertEquals(IsolationLevel.REPEATABLE_READ, client.getDatabaseDefaultIsolationLevel());
+ } finally {
+ mockSpanner.putStatementResult(
+ StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL));
+ }
+ }
+
+ @Test
+ public void testGetDialectAndDefaultIsolationLevelCachedWithSingleQuery() {
+ mockSpanner.putStatementResult(
+ StatementResult.detectDialectResult(Dialect.POSTGRESQL, IsolationLevel.REPEATABLE_READ));
+ mockSpanner.setIncludeDetermineDialectStatementInRequests(true);
+ try {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
+ assertEquals(IsolationLevel.REPEATABLE_READ, client.getDatabaseDefaultIsolationLevel());
+ assertEquals(Dialect.POSTGRESQL, client.getDialect());
+ assertEquals(IsolationLevel.REPEATABLE_READ, client.getDatabaseDefaultIsolationLevel());
+ assertEquals(Dialect.POSTGRESQL, client.getDialect());
+
+ assertEquals(1L, countDatabaseMetadataQueries());
+ } finally {
+ mockSpanner.setIncludeDetermineDialectStatementInRequests(false);
+ mockSpanner.putStatementResult(
+ StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL));
+ }
+ }
+
+ private long countDatabaseMetadataQueries() {
+ return mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
+ .filter(request -> request.getSql().equals(DatabaseMetadata.STATEMENT.getSql()))
+ .count();
+ }
+
@Test
public void testUntypedNullParameters() {
Statement statement =
diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseMetadataTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseMetadataTest.java
new file mode 100644
index 000000000000..c9f6378ab561
--- /dev/null
+++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseMetadataTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2026 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.spanner.Type.StructField;
+import com.google.spanner.v1.TransactionOptions.IsolationLevel;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DatabaseMetadataTest {
+ private static final Type OPTION_TYPE =
+ Type.struct(
+ StructField.of("option_name", Type.string()),
+ StructField.of("option_value", Type.string()));
+
+ private static Struct option(String name, String value) {
+ return Struct.newBuilder().set("option_name").to(name).set("option_value").to(value).build();
+ }
+
+ private static DatabaseMetadata fromOptions(Struct... options) {
+ try (ResultSet resultSet = ResultSets.forRows(OPTION_TYPE, Arrays.asList(options))) {
+ return DatabaseMetadata.fromResultSet(resultSet);
+ }
+ }
+
+ @Test
+ public void testFromResultSetWithoutOptionsReturnsDefaults() {
+ DatabaseMetadata metadata = fromOptions();
+
+ assertEquals(Dialect.GOOGLE_STANDARD_SQL, metadata.getDialect());
+ assertEquals(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, metadata.getDefaultIsolationLevel());
+ }
+
+ @Test
+ public void testFromResultSetReadsAllOptions() {
+ DatabaseMetadata metadata =
+ fromOptions(
+ option(DatabaseMetadata.DATABASE_DIALECT_OPTION, "POSTGRESQL"),
+ option(DatabaseMetadata.DEFAULT_TRANSACTION_ISOLATION_OPTION, "repeatable read"));
+
+ assertEquals(Dialect.POSTGRESQL, metadata.getDialect());
+ assertEquals(IsolationLevel.REPEATABLE_READ, metadata.getDefaultIsolationLevel());
+ }
+
+ @Test
+ public void testFromResultSetWithSerializable() {
+ DatabaseMetadata metadata =
+ fromOptions(option(DatabaseMetadata.DEFAULT_TRANSACTION_ISOLATION_OPTION, "serializable"));
+
+ assertEquals(IsolationLevel.SERIALIZABLE, metadata.getDefaultIsolationLevel());
+ }
+
+ @Test
+ public void testFromResultSetWithNullIsolationLevelReturnsDefault() {
+ DatabaseMetadata metadata =
+ fromOptions(option(DatabaseMetadata.DEFAULT_TRANSACTION_ISOLATION_OPTION, null));
+
+ assertEquals(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, metadata.getDefaultIsolationLevel());
+ }
+
+ @Test
+ public void testFromResultSetWithUnknownIsolationLevel() {
+ DatabaseMetadata metadata =
+ fromOptions(
+ option(DatabaseMetadata.DEFAULT_TRANSACTION_ISOLATION_OPTION, "read committed"));
+
+ assertEquals(IsolationLevel.UNRECOGNIZED, metadata.getDefaultIsolationLevel());
+ }
+
+ @Test
+ public void testFromResultSetWithUnknownDialect() {
+ DatabaseMetadata metadata =
+ fromOptions(
+ option(DatabaseMetadata.DATABASE_DIALECT_OPTION, "UNKNOWN_DIALECT"),
+ option(DatabaseMetadata.DEFAULT_TRANSACTION_ISOLATION_OPTION, "repeatable read"));
+
+ assertThrows(IllegalArgumentException.class, metadata::getDialect);
+ // The other options can still be read.
+ assertEquals(IsolationLevel.REPEATABLE_READ, metadata.getDefaultIsolationLevel());
+ }
+}
diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
index a598e64045fa..e5d9299e4c96 100644
--- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
+++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
@@ -72,6 +72,7 @@
import com.google.spanner.v1.StructType.Field;
import com.google.spanner.v1.Transaction;
import com.google.spanner.v1.TransactionOptions;
+import com.google.spanner.v1.TransactionOptions.IsolationLevel;
import com.google.spanner.v1.TransactionOptions.ModeCase;
import com.google.spanner.v1.TransactionOptions.ReadWrite;
import com.google.spanner.v1.TransactionSelector;
@@ -94,6 +95,7 @@
import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -319,8 +321,45 @@ public static StatementResult exception(Statement statement, StatusRuntimeExcept
/** Creates a result for the query that detects the dialect that is used for the database. */
public static StatementResult detectDialectResult(Dialect resultDialect) {
- return StatementResult.query(
- MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT,
+ return detectDialectResult(resultDialect, IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED);
+ }
+
+ /**
+ * Creates a result for the query that detects the dialect and default transaction isolation
+ * level of the database. The isolation level is returned in the same format as Spanner. {@link
+ * IsolationLevel#ISOLATION_LEVEL_UNSPECIFIED} means that the database option has not been set.
+ */
+ public static StatementResult detectDialectResult(
+ Dialect resultDialect, IsolationLevel defaultIsolationLevel) {
+ Map options = new LinkedHashMap<>();
+ options.put(DatabaseMetadata.DATABASE_DIALECT_OPTION, resultDialect.toString());
+ switch (defaultIsolationLevel) {
+ case ISOLATION_LEVEL_UNSPECIFIED:
+ break;
+ case SERIALIZABLE:
+ options.put(
+ DatabaseMetadata.DEFAULT_TRANSACTION_ISOLATION_OPTION,
+ DatabaseMetadata.SERIALIZABLE_VALUE);
+ break;
+ case REPEATABLE_READ:
+ options.put(
+ DatabaseMetadata.DEFAULT_TRANSACTION_ISOLATION_OPTION,
+ DatabaseMetadata.REPEATABLE_READ_VALUE);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported isolation level: " + defaultIsolationLevel);
+ }
+ return detectDatabaseMetadataResult(options);
+ }
+
+ /**
+ * Creates a result for the query that reads the database options that are cached by the client.
+ * The result contains one row for each option in the given map. Options that are not in the map
+ * are treated as not set.
+ */
+ private static StatementResult detectDatabaseMetadataResult(Map options) {
+ ResultSet.Builder builder =
ResultSet.newBuilder()
.setMetadata(
ResultSetMetadata.newBuilder()
@@ -328,19 +367,28 @@ public static StatementResult detectDialectResult(Dialect resultDialect) {
StructType.newBuilder()
.addFields(
Field.newBuilder()
- .setName("DIALECT")
+ .setName("option_name")
+ .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+ .build())
+ .addFields(
+ Field.newBuilder()
+ .setName("option_value")
.setType(Type.newBuilder().setCode(TypeCode.STRING).build())
.build())
.build())
- .build())
- .addRows(
- ListValue.newBuilder()
- .addValues(
- com.google.protobuf.Value.newBuilder()
- .setStringValue(resultDialect.toString())
- .build())
- .build())
- .build());
+ .build());
+ for (Entry option : options.entrySet()) {
+ builder.addRows(
+ ListValue.newBuilder()
+ .addValues(
+ com.google.protobuf.Value.newBuilder().setStringValue(option.getKey()).build())
+ .addValues(
+ com.google.protobuf.Value.newBuilder()
+ .setStringValue(option.getValue())
+ .build())
+ .build());
+ }
+ return StatementResult.query(DatabaseMetadata.STATEMENT, builder.build());
}
private static class KeepLastElementDeque extends LinkedList {
@@ -610,10 +658,9 @@ private static void checkStreamException(
private double abortProbability = 0.0010D;
/**
- * Flip this switch to true if you want the {@link
- * MultiplexedSessionDatabaseClient#DETERMINE_DIALECT_STATEMENT} statement to be included in the
- * recorded requests on the mock server. It is ignored by default to prevent tests that do not
- * expect this request to suddenly start failing.
+ * Flip this switch to true if you want the {@link DatabaseMetadata#STATEMENT} statement to be
+ * included in the recorded requests on the mock server. It is ignored by default to prevent tests
+ * that do not expect this request to suddenly start failing.
*/
private boolean includeDetermineDialectStatementInRequests = false;
@@ -777,9 +824,8 @@ public void setAbortProbability(double probability) {
}
/**
- * Set this to true if you want the {@link
- * MultiplexedSessionDatabaseClient#DETERMINE_DIALECT_STATEMENT} statement to be included in the
- * recorded requests on the mock server. It is ignored by default to prevent tests that do not
+ * Set this to true if you want the {@link DatabaseMetadata#STATEMENT} statement to be included in
+ * the recorded requests on the mock server. It is ignored by default to prevent tests that do not
* expect this request to suddenly start failing.
*/
public void setIncludeDetermineDialectStatementInRequests(boolean include) {
@@ -1283,9 +1329,7 @@ public void executeBatchDml(
public void executeStreamingSql(
ExecuteSqlRequest request, StreamObserver responseObserver) {
if (includeDetermineDialectStatementInRequests
- || !request
- .getSql()
- .equals(MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT.getSql())) {
+ || !request.getSql().equals(DatabaseMetadata.STATEMENT.getSql())) {
maybeFreezeAndRecordRequest(request);
}
Preconditions.checkNotNull(request.getSession());
diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseDefaultIsolationLevelTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseDefaultIsolationLevelTest.java
new file mode 100644
index 000000000000..e6fed9087e65
--- /dev/null
+++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseDefaultIsolationLevelTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2026 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.it;
+
+import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeFalse;
+
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+import com.google.spanner.v1.TransactionOptions.IsolationLevel;
+import java.util.Collections;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Integration tests for {@link DatabaseClient#getDatabaseDefaultIsolationLevel()}. */
+@Category(ParallelIntegrationTest.class)
+@RunWith(Parameterized.class)
+public class ITDatabaseDefaultIsolationLevelTest {
+ @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
+
+ @Test
+ public void testDefaultIsolationLevelWhenOptionIsNotSet() {
+ RemoteSpannerHelper testHelper = env.getTestHelper();
+ Database database = testHelper.createTestDatabase(dialect, Collections.emptyList());
+ DatabaseClient client = testHelper.getDatabaseClient(database);
+
+ assertEquals(
+ IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, client.getDatabaseDefaultIsolationLevel());
+ assertEquals(dialect, client.getDialect());
+ }
+
+ @Test
+ public void testDefaultIsolationLevelRepeatableRead() throws Exception {
+ assumeFalse(
+ "The emulator does not support the default_transaction_isolation option",
+ isUsingEmulator());
+ RemoteSpannerHelper testHelper = env.getTestHelper();
+ Database database = testHelper.createTestDatabase(dialect, Collections.emptyList());
+ String databaseId = database.getId().getDatabase();
+ String alterDatabase =
+ dialect == Dialect.POSTGRESQL
+ ? "ALTER DATABASE "
+ + databaseId
+ + " SET default_transaction_isolation TO 'repeatable read'"
+ : "ALTER DATABASE "
+ + databaseId
+ + " SET OPTIONS (default_transaction_isolation = 'repeatable read')";
+ testHelper
+ .getClient()
+ .getDatabaseAdminClient()
+ .updateDatabaseDdl(
+ testHelper.getInstanceId().getInstance(),
+ databaseId,
+ Collections.singletonList(alterDatabase),
+ null)
+ .get();
+
+ // The client is created after the option has been set, so it reads the new value.
+ DatabaseClient client = testHelper.getDatabaseClient(database);
+ assertEquals(IsolationLevel.REPEATABLE_READ, client.getDatabaseDefaultIsolationLevel());
+ assertEquals(dialect, client.getDialect());
+ }
+}