diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java index 7df115cf07b..6c9988b6f3b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java @@ -16,6 +16,11 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.parseTimeUnit; +import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.toChronoUnit; + +import com.google.api.gax.core.CredentialsProvider; +import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Options.RpcPriority; import com.google.cloud.spanner.SpannerException; @@ -27,16 +32,17 @@ import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.protobuf.Duration; -import com.google.protobuf.util.Durations; import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.RequestOptions.Priority; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.EnumSet; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -172,8 +178,11 @@ public Integer convert(String value) { } } - /** Converter from string to {@link Duration}. */ + /** Converter from string to protobuf {@link Duration}. */ static class DurationConverter implements ClientSideStatementValueConverter { + static final DurationConverter INSTANCE = + new DurationConverter("('(\\d{1,19})(s|ms|us|ns)'|(^\\d{1,19})|NULL)"); + private final Pattern allowedValues; public DurationConverter(String allowedValues) { @@ -193,13 +202,16 @@ public Duration convert(String value) { Matcher matcher = allowedValues.matcher(value); if (matcher.find()) { if (matcher.group(0).equalsIgnoreCase("null")) { - return Durations.fromNanos(0L); + return Duration.ofMillis(0L); } else { - Duration duration = - ReadOnlyStalenessUtil.createDuration( - Long.parseLong(matcher.group(1)), - ReadOnlyStalenessUtil.parseTimeUnit(matcher.group(2))); - if (duration.getSeconds() == 0L && duration.getNanos() == 0) { + Duration duration; + if (matcher.group(3) != null) { + duration = Duration.ofMillis(Long.parseLong(matcher.group(3))); + } else { + ChronoUnit unit = toChronoUnit(parseTimeUnit(matcher.group(2))); + duration = Duration.of(Long.parseLong(matcher.group(1)), unit); + } + if (duration.isZero()) { return null; } return duration; @@ -231,18 +243,14 @@ public Duration convert(String value) { if (matcher.find()) { Duration duration; if (matcher.group(0).equalsIgnoreCase("default")) { - return Durations.fromNanos(0L); + return Duration.ofMillis(0L); } else if (matcher.group(2) == null) { - duration = - ReadOnlyStalenessUtil.createDuration( - Long.parseLong(matcher.group(0)), TimeUnit.MILLISECONDS); + duration = Duration.ofMillis(Long.parseLong(matcher.group(0))); } else { - duration = - ReadOnlyStalenessUtil.createDuration( - Long.parseLong(matcher.group(1)), - ReadOnlyStalenessUtil.parseTimeUnit(matcher.group(2))); + ChronoUnit unit = toChronoUnit(parseTimeUnit(matcher.group(2))); + duration = Duration.of(Long.parseLong(matcher.group(1)), unit); } - if (duration.getSeconds() == 0L && duration.getNanos() == 0) { + if (duration.isZero()) { return null; } return duration; @@ -254,6 +262,10 @@ public Duration convert(String value) { /** Converter from string to possible values for read only staleness ({@link TimestampBound}). */ static class ReadOnlyStalenessConverter implements ClientSideStatementValueConverter { + static final ReadOnlyStalenessConverter INSTANCE = + new ReadOnlyStalenessConverter( + "'((STRONG)|(MIN_READ_TIMESTAMP)[\\t ]+((\\d{4})-(\\d{2})-(\\d{2})([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?)([Zz]|([+-])(\\d{2}):(\\d{2})))|(READ_TIMESTAMP)[\\t ]+((\\d{4})-(\\d{2})-(\\d{2})([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?)([Zz]|([+-])(\\d{2}):(\\d{2})))|(MAX_STALENESS)[\\t ]+((\\d{1,19})(s|ms|us|ns))|(EXACT_STALENESS)[\\t ]+((\\d{1,19})(s|ms|us|ns)))'"); + private final Pattern allowedValues; private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(Mode.class); @@ -297,7 +309,7 @@ public TimestampBound convert(String value) { try { return TimestampBound.ofExactStaleness( Long.parseLong(matcher.group(groupIndex + 2)), - ReadOnlyStalenessUtil.parseTimeUnit(matcher.group(groupIndex + 3))); + parseTimeUnit(matcher.group(groupIndex + 3))); } catch (IllegalArgumentException e) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, e.getMessage()); @@ -306,7 +318,7 @@ public TimestampBound convert(String value) { try { return TimestampBound.ofMaxStaleness( Long.parseLong(matcher.group(groupIndex + 2)), - ReadOnlyStalenessUtil.parseTimeUnit(matcher.group(groupIndex + 3))); + parseTimeUnit(matcher.group(groupIndex + 3))); } catch (IllegalArgumentException e) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, e.getMessage()); @@ -539,7 +551,12 @@ public PgTransactionMode convert(String value) { } } - /** Converter for converting strings to {@link RpcPriority} values. */ + /** + * Converter for converting strings to {@link Priority} values. + * + * @deprecated Use {@link RpcPriorityEnumConverter} instead. + */ + @Deprecated static class RpcPriorityConverter implements ClientSideStatementValueConverter { private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(Priority.class); @@ -569,12 +586,43 @@ public Priority convert(String value) { } } + /** Converter for converting strings to {@link RpcPriority} values. */ + static class RpcPriorityEnumConverter implements ClientSideStatementValueConverter { + static final RpcPriorityEnumConverter INSTANCE = new RpcPriorityEnumConverter(); + + private final CaseInsensitiveEnumMap values = + new CaseInsensitiveEnumMap<>(RpcPriority.class); + + private RpcPriorityEnumConverter() {} + + /** Constructor needed for reflection. */ + public RpcPriorityEnumConverter(String allowedValues) {} + + @Override + public Class getParameterClass() { + return RpcPriority.class; + } + + @Override + public RpcPriority convert(String value) { + if ("null".equalsIgnoreCase(value)) { + return RpcPriority.UNSPECIFIED; + } + return values.get(value); + } + } + /** Converter for converting strings to {@link SavepointSupport} values. */ static class SavepointSupportConverter implements ClientSideStatementValueConverter { + static final SavepointSupportConverter INSTANCE = new SavepointSupportConverter(); + private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(SavepointSupport.class); + private SavepointSupportConverter() {} + + /** Constructor needed for reflection. */ public SavepointSupportConverter(String allowedValues) {} @Override @@ -588,6 +636,30 @@ public SavepointSupport convert(String value) { } } + /** Converter for converting strings to {@link DdlInTransactionMode} values. */ + static class DdlInTransactionModeConverter + implements ClientSideStatementValueConverter { + static final DdlInTransactionModeConverter INSTANCE = new DdlInTransactionModeConverter(); + + private final CaseInsensitiveEnumMap values = + new CaseInsensitiveEnumMap<>(DdlInTransactionMode.class); + + private DdlInTransactionModeConverter() {} + + /** Constructor needed for reflection. */ + public DdlInTransactionModeConverter(String allowedValues) {} + + @Override + public Class getParameterClass() { + return DdlInTransactionMode.class; + } + + @Override + public DdlInTransactionMode convert(String value) { + return values.get(value); + } + } + static class ExplainCommandConverter implements ClientSideStatementValueConverter { @Override public Class getParameterClass() { @@ -648,4 +720,71 @@ public String convert(String filePath) { return filePath; } } + + static class CredentialsProviderConverter + implements ClientSideStatementValueConverter { + static final CredentialsProviderConverter INSTANCE = new CredentialsProviderConverter(); + + private CredentialsProviderConverter() {} + + @Override + public Class getParameterClass() { + return CredentialsProvider.class; + } + + @Override + public CredentialsProvider convert(String credentialsProviderName) { + if (!Strings.isNullOrEmpty(credentialsProviderName)) { + try { + Class clazz = + (Class) Class.forName(credentialsProviderName); + Constructor constructor = clazz.getDeclaredConstructor(); + return constructor.newInstance(); + } catch (ClassNotFoundException classNotFoundException) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Unknown or invalid CredentialsProvider class name: " + credentialsProviderName, + classNotFoundException); + } catch (NoSuchMethodException noSuchMethodException) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Credentials provider " + + credentialsProviderName + + " does not have a public no-arg constructor.", + noSuchMethodException); + } catch (InvocationTargetException + | InstantiationException + | IllegalAccessException exception) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Failed to create an instance of " + + credentialsProviderName + + ": " + + exception.getMessage(), + exception); + } + } + return null; + } + } + + /** Converter for converting strings to {@link Dialect} values. */ + static class DialectConverter implements ClientSideStatementValueConverter { + static final DialectConverter INSTANCE = new DialectConverter(); + + private final CaseInsensitiveEnumMap values = + new CaseInsensitiveEnumMap<>(Dialect.class); + + private DialectConverter() {} + + @Override + public Class getParameterClass() { + return Dialect.class; + } + + @Override + public Dialect convert(String value) { + return values.get(value); + } + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java index 6d171acc3c7..1f918a157c0 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java @@ -18,7 +18,26 @@ import static com.google.cloud.spanner.SpannerApiFutures.get; import static com.google.cloud.spanner.connection.ConnectionPreconditions.checkValidIdentifier; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT_DML_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DATA_BOOST_ENABLED; +import static com.google.cloud.spanner.connection.ConnectionProperties.DDL_IN_TRANSACTION_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DIRECTED_READ; +import static com.google.cloud.spanner.connection.ConnectionProperties.KEEP_TRANSACTION_ALIVE; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONED_PARALLELISM; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_STATISTICS_PACKAGE; +import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_VERSION; +import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.READ_ONLY_STALENESS; import static com.google.cloud.spanner.connection.ConnectionProperties.RETRY_ABORTS_INTERNALLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.RETURN_COMMIT_STATS; +import static com.google.cloud.spanner.connection.ConnectionProperties.RPC_PRIORITY; +import static com.google.cloud.spanner.connection.ConnectionProperties.SAVEPOINT_SUPPORT; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACING_PREFIX; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; @@ -57,8 +76,8 @@ import com.google.cloud.spanner.connection.UnitOfWork.CallType; import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.common.base.Suppliers; import com.google.common.util.concurrent.MoreExecutors; import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; @@ -94,7 +113,6 @@ /** Implementation for {@link Connection}, the generic Spanner connection API (not JDBC). */ class ConnectionImpl implements Connection { private static final String INSTRUMENTATION_SCOPE = "cloud.google.com/java"; - private static final String DEFAULT_TRACING_PREFIX = "CloudSpanner"; private static final String SINGLE_USE_TRANSACTION = "SingleUseTransaction"; private static final String READ_ONLY_TRANSACTION = "ReadOnlyTransaction"; private static final String READ_WRITE_TRANSACTION = "ReadWriteTransaction"; @@ -212,17 +230,11 @@ static UnitOfWorkType of(TransactionMode transactionMode) { private final Spanner spanner; private final Tracer tracer; - private final String tracingPrefix; private final Attributes openTelemetryAttributes; private final DdlClient ddlClient; private final DatabaseClient dbClient; private final BatchClient batchClient; private final ConnectionState connectionState; - private boolean autocommit; - private boolean readOnly; - private boolean returnCommitStats; - private boolean delayTransactionStartUntilFirstWrite; - private boolean keepTransactionAlive; private UnitOfWork currentUnitOfWork = null; /** @@ -240,44 +252,12 @@ static UnitOfWorkType of(TransactionMode transactionMode) { private UnitOfWorkType unitOfWorkType; private final Stack transactionStack = new Stack<>(); private final List transactionRetryListeners = new ArrayList<>(); - private AutocommitDmlMode autocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL; - private TimestampBound readOnlyStaleness = TimestampBound.strong(); - /** - * autoPartitionMode will force this connection to execute all queries as partitioned queries. If - * a query cannot be executed as a partitioned query, for example if it is not partitionable, then - * the query will fail. This mode is intended for integrations with frameworks that should always - * use partitioned queries, and that do not support executing custom SQL statements. This setting - * can be used in combination with the dataBoostEnabled flag to force all queries to use data - * boost. - */ - private boolean autoPartitionMode; - /** - * dataBoostEnabled=true will cause all partitionedQueries to use data boost. All other queries - * and other statements ignore this flag. - */ - private boolean dataBoostEnabled; - /** - * maxPartitions determines the maximum number of partitions that will be used for partitioned - * queries. All other statements ignore this variable. - */ - private int maxPartitions; - /** - * maxPartitionedParallelism determines the maximum number of threads that will be used to execute - * partitions in parallel when executing a partitioned query on this connection. - */ - private int maxPartitionedParallelism; - - private DirectedReadOptions directedReadOptions = null; - private QueryOptions queryOptions = QueryOptions.getDefaultInstance(); - private RpcPriority rpcPriority = null; - private SavepointSupport savepointSupport = SavepointSupport.FAIL_AFTER_ROLLBACK; - private DdlInTransactionMode ddlInTransactionMode; + // The following properties are not 'normal' connection properties, but transient properties that + // are automatically reset after executing a transaction or statement. private String transactionTag; private String statementTag; private boolean excludeTxnFromChangeStreams; - - private Duration maxCommitDelay; private byte[] protoDescriptors; private String protoDescriptorsFilePath; @@ -299,8 +279,6 @@ static UnitOfWorkType of(TransactionMode transactionMode) { .getTracer( INSTRUMENTATION_SCOPE, GaxProperties.getLibraryVersion(spanner.getOptions().getClass())); - this.tracingPrefix = - MoreObjects.firstNonNull(options.getTracingPrefix(), DEFAULT_TRACING_PREFIX); this.openTelemetryAttributes = createOpenTelemetryAttributes(options.getDatabaseId()); if (options.isAutoConfigEmulator()) { EmulatorUtil.maybeCreateInstanceAndDatabase( @@ -329,10 +307,8 @@ static UnitOfWorkType of(TransactionMode transactionMode) { new StatementExecutor(options.isUseVirtualThreads(), Collections.emptyList()); this.spannerPool = Preconditions.checkNotNull(spannerPool); this.options = Preconditions.checkNotNull(options); - this.ddlInTransactionMode = options.getDdlInTransactionMode(); this.spanner = spannerPool.getSpanner(options, this); this.tracer = OpenTelemetry.noop().getTracer(INSTRUMENTATION_SCOPE); - this.tracingPrefix = DEFAULT_TRACING_PREFIX; this.openTelemetryAttributes = Attributes.empty(); this.ddlClient = Preconditions.checkNotNull(ddlClient); this.dbClient = Preconditions.checkNotNull(dbClient); @@ -442,28 +418,32 @@ public void reset() { private void reset(Context context) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); + // TODO: Replace all of these with a resetAll in ConnectionState. this.connectionState.resetValue(RETRY_ABORTS_INTERNALLY, context, /* inTransaction= */ false); - this.readOnly = options.isReadOnly(); - this.autocommit = options.isAutocommit(); - this.queryOptions = - QueryOptions.getDefaultInstance().toBuilder().mergeFrom(options.getQueryOptions()).build(); - this.rpcPriority = options.getRPCPriority(); - this.ddlInTransactionMode = options.getDdlInTransactionMode(); - this.returnCommitStats = options.isReturnCommitStats(); - this.delayTransactionStartUntilFirstWrite = options.isDelayTransactionStartUntilFirstWrite(); - this.keepTransactionAlive = options.isKeepTransactionAlive(); - this.dataBoostEnabled = options.isDataBoostEnabled(); - this.autoPartitionMode = options.isAutoPartitionMode(); - this.maxPartitions = options.getMaxPartitions(); - this.maxPartitionedParallelism = options.getMaxPartitionedParallelism(); - this.maxCommitDelay = options.getMaxCommitDelay(); - - this.autocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL; - this.readOnlyStaleness = TimestampBound.strong(); + this.connectionState.resetValue(AUTOCOMMIT, context, /* inTransaction= */ false); + this.connectionState.resetValue(READONLY, context, /* inTransaction= */ false); + this.connectionState.resetValue(READ_ONLY_STALENESS, context, /* inTransaction= */ false); + this.connectionState.resetValue(OPTIMIZER_VERSION, context, /* inTransaction= */ false); + this.connectionState.resetValue( + OPTIMIZER_STATISTICS_PACKAGE, context, /* inTransaction= */ false); + this.connectionState.resetValue(RPC_PRIORITY, context, /* inTransaction= */ false); + this.connectionState.resetValue(DDL_IN_TRANSACTION_MODE, context, /* inTransaction= */ false); + this.connectionState.resetValue(RETURN_COMMIT_STATS, context, /* inTransaction= */ false); + this.connectionState.resetValue( + DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE, context, /* inTransaction= */ false); + this.connectionState.resetValue(KEEP_TRANSACTION_ALIVE, context, /* inTransaction= */ false); + this.connectionState.resetValue(AUTO_PARTITION_MODE, context, /* inTransaction= */ false); + this.connectionState.resetValue(DATA_BOOST_ENABLED, context, /* inTransaction= */ false); + this.connectionState.resetValue(MAX_PARTITIONS, context, /* inTransaction= */ false); + this.connectionState.resetValue( + MAX_PARTITIONED_PARALLELISM, context, /* inTransaction= */ false); + this.connectionState.resetValue(MAX_COMMIT_DELAY, context, /* inTransaction= */ false); + + this.connectionState.resetValue(AUTOCOMMIT_DML_MODE, context, /* inTransaction= */ false); this.statementTag = null; this.statementTimeout = new StatementExecutor.StatementTimeout(); - this.directedReadOptions = null; - this.savepointSupport = SavepointSupport.FAIL_AFTER_ROLLBACK; + this.connectionState.resetValue(DIRECTED_READ, context, /* inTransaction= */ false); + this.connectionState.resetValue(SAVEPOINT_SUPPORT, context, /* inTransaction= */ false); this.protoDescriptors = null; this.protoDescriptorsFilePath = null; @@ -507,6 +487,15 @@ public boolean isClosed() { return closed; } + private T getConnectionPropertyValue( + com.google.cloud.spanner.connection.ConnectionProperty property) { + return this.connectionState.getValue(property).getValue(); + } + + private void setConnectionPropertyValue(ConnectionProperty property, T value) { + this.connectionState.setValue(property, value, getCurrentContext(), /* inTransaction= */ false); + } + @Override public void setAutocommit(boolean autocommit) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); @@ -521,14 +510,16 @@ public void setAutocommit(boolean autocommit) { "Cannot set autocommit while in a temporary transaction"); ConnectionPreconditions.checkState( !transactionBeginMarked, "Cannot set autocommit when a transaction has begun"); - this.autocommit = autocommit; + setConnectionPropertyValue(AUTOCOMMIT, autocommit); clearLastTransactionAndSetDefaultTransactionOptions(); // Reset the readOnlyStaleness value if it is no longer compatible with the new autocommit // value. - if (!autocommit - && (readOnlyStaleness.getMode() == Mode.MAX_STALENESS - || readOnlyStaleness.getMode() == Mode.MIN_READ_TIMESTAMP)) { - readOnlyStaleness = TimestampBound.strong(); + if (!autocommit) { + TimestampBound readOnlyStaleness = getReadOnlyStaleness(); + if (readOnlyStaleness.getMode() == Mode.MAX_STALENESS + || readOnlyStaleness.getMode() == Mode.MIN_READ_TIMESTAMP) { + setConnectionPropertyValue(READ_ONLY_STALENESS, TimestampBound.strong()); + } } } @@ -539,7 +530,7 @@ public boolean isAutocommit() { } private boolean internalIsAutocommit() { - return this.autocommit; + return getConnectionPropertyValue(AUTOCOMMIT); } @Override @@ -553,14 +544,14 @@ public void setReadOnly(boolean readOnly) { "Cannot set read-only while in a temporary transaction"); ConnectionPreconditions.checkState( !transactionBeginMarked, "Cannot set read-only when a transaction has begun"); - this.readOnly = readOnly; + setConnectionPropertyValue(READONLY, readOnly); clearLastTransactionAndSetDefaultTransactionOptions(); } @Override public boolean isReadOnly() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.readOnly; + return getConnectionPropertyValue(READONLY); } private void clearLastTransactionAndSetDefaultTransactionOptions() { @@ -579,7 +570,7 @@ public void setAutocommitDmlMode(AutocommitDmlMode mode) { "Cannot set autocommit DML mode while not in autocommit mode or while a transaction is active"); ConnectionPreconditions.checkState( !isReadOnly(), "Cannot set autocommit DML mode for a read-only connection"); - this.autocommitDmlMode = mode; + setConnectionPropertyValue(AUTOCOMMIT_DML_MODE, mode); } @Override @@ -587,7 +578,7 @@ public AutocommitDmlMode getAutocommitDmlMode() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState( !isBatchActive(), "Cannot get autocommit DML mode while in a batch"); - return this.autocommitDmlMode; + return getConnectionPropertyValue(AUTOCOMMIT_DML_MODE); } @Override @@ -605,14 +596,14 @@ public void setReadOnlyStaleness(TimestampBound staleness) { isAutocommit() && !inTransaction, "MAX_STALENESS and MIN_READ_TIMESTAMP are only allowed in autocommit mode"); } - this.readOnlyStaleness = staleness; + setConnectionPropertyValue(READ_ONLY_STALENESS, staleness); } @Override public TimestampBound getReadOnlyStaleness() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState(!isBatchActive(), "Cannot get read-only while in a batch"); - return this.readOnlyStaleness; + return getConnectionPropertyValue(READ_ONLY_STALENESS); } @Override @@ -621,57 +612,63 @@ public void setDirectedRead(DirectedReadOptions directedReadOptions) { ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set directed read options when a transaction has been started"); - this.directedReadOptions = directedReadOptions; + setConnectionPropertyValue(DIRECTED_READ, directedReadOptions); } @Override public DirectedReadOptions getDirectedRead() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.directedReadOptions; + return getConnectionPropertyValue(DIRECTED_READ); } @Override public void setOptimizerVersion(String optimizerVersion) { Preconditions.checkNotNull(optimizerVersion); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.queryOptions = queryOptions.toBuilder().setOptimizerVersion(optimizerVersion).build(); + setConnectionPropertyValue(OPTIMIZER_VERSION, optimizerVersion); } @Override public String getOptimizerVersion() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.queryOptions.getOptimizerVersion(); + return getConnectionPropertyValue(OPTIMIZER_VERSION); } @Override public void setOptimizerStatisticsPackage(String optimizerStatisticsPackage) { Preconditions.checkNotNull(optimizerStatisticsPackage); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.queryOptions = - queryOptions.toBuilder().setOptimizerStatisticsPackage(optimizerStatisticsPackage).build(); + setConnectionPropertyValue(OPTIMIZER_STATISTICS_PACKAGE, optimizerStatisticsPackage); } @Override public String getOptimizerStatisticsPackage() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.queryOptions.getOptimizerStatisticsPackage(); + return getConnectionPropertyValue(OPTIMIZER_STATISTICS_PACKAGE); + } + + private QueryOptions buildQueryOptions() { + return QueryOptions.newBuilder() + .setOptimizerVersion(getConnectionPropertyValue(OPTIMIZER_VERSION)) + .setOptimizerStatisticsPackage(getConnectionPropertyValue(OPTIMIZER_STATISTICS_PACKAGE)) + .build(); } @Override public void setRPCPriority(RpcPriority rpcPriority) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.rpcPriority = rpcPriority; + setConnectionPropertyValue(RPC_PRIORITY, rpcPriority); } @Override public RpcPriority getRPCPriority() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.rpcPriority; + return getConnectionPropertyValue(RPC_PRIORITY); } @Override public DdlInTransactionMode getDdlInTransactionMode() { - return this.ddlInTransactionMode; + return getConnectionPropertyValue(DDL_IN_TRANSACTION_MODE); } @Override @@ -681,7 +678,7 @@ public void setDdlInTransactionMode(DdlInTransactionMode ddlInTransactionMode) { !isBatchActive(), "Cannot set DdlInTransactionMode while in a batch"); ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set DdlInTransactionMode while a transaction is active"); - this.ddlInTransactionMode = Preconditions.checkNotNull(ddlInTransactionMode); + setConnectionPropertyValue(DDL_IN_TRANSACTION_MODE, ddlInTransactionMode); } @Override @@ -868,17 +865,13 @@ private void checkSetRetryAbortsInternallyAvailable() { @Override public boolean isRetryAbortsInternally() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.connectionState.getValue(RETRY_ABORTS_INTERNALLY).getValue(); + return getConnectionPropertyValue(RETRY_ABORTS_INTERNALLY); } @Override public void setRetryAbortsInternally(boolean retryAbortsInternally) { checkSetRetryAbortsInternallyAvailable(); - this.connectionState.setValue( - RETRY_ABORTS_INTERNALLY, - retryAbortsInternally, - getCurrentContext(), - /* inTransaction = */ false); + setConnectionPropertyValue(RETRY_ABORTS_INTERNALLY, retryAbortsInternally); } @Override @@ -968,25 +961,25 @@ CommitResponse getCommitResponseOrNull() { @Override public void setReturnCommitStats(boolean returnCommitStats) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.returnCommitStats = returnCommitStats; + setConnectionPropertyValue(RETURN_COMMIT_STATS, returnCommitStats); } @Override public boolean isReturnCommitStats() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.returnCommitStats; + return getConnectionPropertyValue(RETURN_COMMIT_STATS); } @Override public void setMaxCommitDelay(Duration maxCommitDelay) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.maxCommitDelay = maxCommitDelay; + setConnectionPropertyValue(MAX_COMMIT_DELAY, maxCommitDelay); } @Override public Duration getMaxCommitDelay() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.maxCommitDelay; + return getConnectionPropertyValue(MAX_COMMIT_DELAY); } @Override @@ -996,13 +989,14 @@ public void setDelayTransactionStartUntilFirstWrite( ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set DelayTransactionStartUntilFirstWrite while a transaction is active"); - this.delayTransactionStartUntilFirstWrite = delayTransactionStartUntilFirstWrite; + setConnectionPropertyValue( + DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE, delayTransactionStartUntilFirstWrite); } @Override public boolean isDelayTransactionStartUntilFirstWrite() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.delayTransactionStartUntilFirstWrite; + return getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE); } @Override @@ -1010,13 +1004,13 @@ public void setKeepTransactionAlive(boolean keepTransactionAlive) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set KeepTransactionAlive while a transaction is active"); - this.keepTransactionAlive = keepTransactionAlive; + setConnectionPropertyValue(KEEP_TRANSACTION_ALIVE, keepTransactionAlive); } @Override public boolean isKeepTransactionAlive() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.keepTransactionAlive; + return getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE); } /** Resets this connection to its default transaction options. */ @@ -1136,7 +1130,7 @@ private ApiFuture endCurrentTransactionAsync( @Override public SavepointSupport getSavepointSupport() { - return this.savepointSupport; + return getConnectionPropertyValue(SAVEPOINT_SUPPORT); } @Override @@ -1146,12 +1140,13 @@ public void setSavepointSupport(SavepointSupport savepointSupport) { !isBatchActive(), "Cannot set SavepointSupport while in a batch"); ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set SavepointSupport while a transaction is active"); - this.savepointSupport = savepointSupport; + setConnectionPropertyValue(SAVEPOINT_SUPPORT, savepointSupport); } @Override public void savepoint(String name) { ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction"); + SavepointSupport savepointSupport = getSavepointSupport(); ConnectionPreconditions.checkState( savepointSupport.isSavepointCreationAllowed(), "This connection does not allow the creation of savepoints. Current value of SavepointSupport: " @@ -1171,7 +1166,7 @@ public void rollbackToSavepoint(String name) { ConnectionPreconditions.checkState( isTransactionStarted(), "This connection has no active transaction"); getCurrentUnitOfWorkOrStartNewUnitOfWork() - .rollbackToSavepoint(checkValidIdentifier(name), savepointSupport); + .rollbackToSavepoint(checkValidIdentifier(name), getSavepointSupport()); } @Override @@ -1188,7 +1183,7 @@ public StatementResult execute(Statement statement, Set allowedResul private StatementResult internalExecute( Statement statement, @Nullable Set allowedResultTypes) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(statement, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(statement, buildQueryOptions()); checkResultTypeAllowed(parsedStatement, allowedResultTypes); switch (parsedStatement.getType()) { case CLIENT_SIDE: @@ -1267,7 +1262,7 @@ private static ResultType getResultType(ParsedStatement parsedStatement) { public AsyncStatementResult executeAsync(Statement statement) { Preconditions.checkNotNull(statement); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(statement, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(statement, buildQueryOptions()); switch (parsedStatement.getType()) { case CLIENT_SIDE: return AsyncStatementResultImpl.of( @@ -1313,38 +1308,46 @@ public ResultSet analyzeQuery(Statement query, QueryAnalyzeMode queryMode) { @Override public void setDataBoostEnabled(boolean dataBoostEnabled) { - this.dataBoostEnabled = dataBoostEnabled; + setConnectionPropertyValue(DATA_BOOST_ENABLED, dataBoostEnabled); } @Override public boolean isDataBoostEnabled() { - return this.dataBoostEnabled; + return getConnectionPropertyValue(DATA_BOOST_ENABLED); } @Override public void setAutoPartitionMode(boolean autoPartitionMode) { - this.autoPartitionMode = autoPartitionMode; + setConnectionPropertyValue(AUTO_PARTITION_MODE, autoPartitionMode); } + /** + * autoPartitionMode will force this connection to execute all queries as partitioned queries. If + * a query cannot be executed as a partitioned query, for example if it is not partitionable, then + * the query will fail. This mode is intended for integrations with frameworks that should always + * use partitioned queries, and that do not support executing custom SQL statements. This setting + * can be used in combination with the dataBoostEnabled flag to force all queries to use data + * boost. + */ @Override public boolean isAutoPartitionMode() { - return this.autoPartitionMode; + return getConnectionPropertyValue(AUTO_PARTITION_MODE); } @Override public void setMaxPartitions(int maxPartitions) { - this.maxPartitions = maxPartitions; + setConnectionPropertyValue(MAX_PARTITIONS, maxPartitions); } @Override public int getMaxPartitions() { - return this.maxPartitions; + return getConnectionPropertyValue(MAX_PARTITIONS); } @Override public ResultSet partitionQuery( Statement query, PartitionOptions partitionOptions, QueryOption... options) { - ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(query, buildQueryOptions()); if (parsedStatement.getType() != StatementType.QUERY) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, @@ -1365,7 +1368,7 @@ public ResultSet partitionQuery( private PartitionOptions getEffectivePartitionOptions( PartitionOptions callSpecificPartitionOptions) { - if (maxPartitions == 0) { + if (getMaxPartitions() == 0) { if (callSpecificPartitionOptions == null) { return PartitionOptions.newBuilder().build(); } else { @@ -1379,11 +1382,11 @@ private PartitionOptions getEffectivePartitionOptions( if (callSpecificPartitionOptions != null && callSpecificPartitionOptions.getPartitionSizeBytes() > 0L) { return PartitionOptions.newBuilder() - .setMaxPartitions(maxPartitions) + .setMaxPartitions(getMaxPartitions()) .setPartitionSizeBytes(callSpecificPartitionOptions.getPartitionSizeBytes()) .build(); } - return PartitionOptions.newBuilder().setMaxPartitions(maxPartitions).build(); + return PartitionOptions.newBuilder().setMaxPartitions(getMaxPartitions()).build(); } @Override @@ -1398,12 +1401,12 @@ public ResultSet runPartition(String encodedPartitionId) { @Override public void setMaxPartitionedParallelism(int maxThreads) { Preconditions.checkArgument(maxThreads >= 0, "maxThreads must be >=0"); - this.maxPartitionedParallelism = maxThreads; + setConnectionPropertyValue(MAX_PARTITIONED_PARALLELISM, maxThreads); } @Override public int getMaxPartitionedParallelism() { - return this.maxPartitionedParallelism; + return getConnectionPropertyValue(MAX_PARTITIONED_PARALLELISM); } @Override @@ -1417,7 +1420,7 @@ public PartitionedQueryResultSet runPartitionedQuery( } // parallelism=0 means 'dynamically choose based on the number of available processors and the // number of partitions'. - return new MergedResultSet(this, partitionIds, maxPartitionedParallelism); + return new MergedResultSet(this, partitionIds, getMaxPartitionedParallelism()); } /** @@ -1429,7 +1432,7 @@ private ResultSet parseAndExecuteQuery( Preconditions.checkNotNull(query); Preconditions.checkNotNull(analyzeMode); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(query, buildQueryOptions()); if (parsedStatement.isQuery() || parsedStatement.isUpdate()) { switch (parsedStatement.getType()) { case CLIENT_SIDE: @@ -1468,7 +1471,7 @@ private AsyncResultSet parseAndExecuteQueryAsync( CallType callType, Statement query, AnalyzeMode analyzeMode, QueryOption... options) { Preconditions.checkNotNull(query); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(query, buildQueryOptions()); if (parsedStatement.isQuery() || parsedStatement.isUpdate()) { switch (parsedStatement.getType()) { case CLIENT_SIDE: @@ -1683,7 +1686,7 @@ private QueryOption[] concat( } private QueryOption[] mergeDataBoost(QueryOption... options) { - if (this.dataBoostEnabled) { + if (isDataBoostEnabled()) { options = appendQueryOption(options, Options.dataBoostEnabled(true)); } return options; @@ -1699,13 +1702,16 @@ private QueryOption[] mergeQueryStatementTag(QueryOption... options) { private QueryOption[] mergeQueryRequestOptions( ParsedStatement parsedStatement, QueryOption... options) { - if (this.rpcPriority != null) { - options = appendQueryOption(options, Options.priority(this.rpcPriority)); + if (getConnectionPropertyValue(RPC_PRIORITY) != null) { + options = + appendQueryOption(options, Options.priority(getConnectionPropertyValue(RPC_PRIORITY))); } - if (this.directedReadOptions != null - && currentUnitOfWork != null - && currentUnitOfWork.supportsDirectedReads(parsedStatement)) { - options = appendQueryOption(options, Options.directedRead(this.directedReadOptions)); + if (currentUnitOfWork != null + && currentUnitOfWork.supportsDirectedReads(parsedStatement) + && getConnectionPropertyValue(DIRECTED_READ) != null) { + options = + appendQueryOption( + options, Options.directedRead(getConnectionPropertyValue(DIRECTED_READ))); } return options; } @@ -1735,13 +1741,13 @@ private UpdateOption[] mergeUpdateStatementTag(UpdateOption... options) { } private UpdateOption[] mergeUpdateRequestOptions(UpdateOption... options) { - if (this.rpcPriority != null) { + if (getConnectionPropertyValue(RPC_PRIORITY) != null) { // Shortcut for the most common scenario. if (options == null || options.length == 0) { - options = new UpdateOption[] {Options.priority(this.rpcPriority)}; + options = new UpdateOption[] {Options.priority(getConnectionPropertyValue(RPC_PRIORITY))}; } else { options = Arrays.copyOf(options, options.length + 1); - options[options.length - 1] = Options.priority(this.rpcPriority); + options[options.length - 1] = Options.priority(getConnectionPropertyValue(RPC_PRIORITY)); } } return options; @@ -1760,7 +1766,7 @@ private ResultSet internalExecuteQuery( boolean isInternalMetadataQuery = isInternalMetadataQuery(options); QueryOption[] combinedOptions = concat(statement.getOptionsFromHints(), options); UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(isInternalMetadataQuery); - if (autoPartitionMode + if (isAutoPartitionMode() && statement.getType() == StatementType.QUERY && !isInternalMetadataQuery) { return runPartitionedQuery( @@ -1784,7 +1790,7 @@ private AsyncResultSet internalExecuteQueryAsync( || (statement.getType() == StatementType.UPDATE && statement.hasReturningClause()), "Statement must be a query or DML with returning clause."); ConnectionPreconditions.checkState( - !(autoPartitionMode && statement.getType() == StatementType.QUERY), + !(isAutoPartitionMode() && statement.getType() == StatementType.QUERY), "Partitioned queries cannot be executed asynchronously"); boolean isInternalMetadataQuery = isInternalMetadataQuery(options); QueryOption[] combinedOptions = concat(statement.getOptionsFromHints(), options); @@ -1864,7 +1870,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork( createNewUnitOfWork( /* isInternalMetadataQuery = */ false, /* forceSingleUse = */ statementType == StatementType.DDL - && this.ddlInTransactionMode != DdlInTransactionMode.FAIL + && getDdlInTransactionMode() != DdlInTransactionMode.FAIL && !this.transactionBeginMarked, statementType); } @@ -1873,7 +1879,10 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork( private Span createSpanForUnitOfWork(String name) { return tracer - .spanBuilder(this.tracingPrefix + "." + name) + .spanBuilder( + Suppliers.memoize(() -> connectionState.getValue(TRACING_PREFIX).getValue()).get() + + "." + + name) .setAllAttributes(getOpenTelemetryAttributes()) .startSpan(); } @@ -1882,7 +1891,7 @@ void maybeAutoCommitCurrentTransaction(StatementType statementType) { if (this.currentUnitOfWork instanceof ReadWriteTransaction && this.currentUnitOfWork.isActive() && statementType == StatementType.DDL - && this.ddlInTransactionMode == DdlInTransactionMode.AUTO_COMMIT_TRANSACTION) { + && getDdlInTransactionMode() == DdlInTransactionMode.AUTO_COMMIT_TRANSACTION) { commit(); } } @@ -1904,12 +1913,12 @@ UnitOfWork createNewUnitOfWork( .setDdlClient(ddlClient) .setDatabaseClient(dbClient) .setBatchClient(batchClient) - .setReadOnly(isReadOnly()) - .setReadOnlyStaleness(readOnlyStaleness) - .setAutocommitDmlMode(autocommitDmlMode) - .setReturnCommitStats(returnCommitStats) + .setReadOnly(getConnectionPropertyValue(READONLY)) + .setReadOnlyStaleness(getConnectionPropertyValue(READ_ONLY_STALENESS)) + .setAutocommitDmlMode(getConnectionPropertyValue(AUTOCOMMIT_DML_MODE)) + .setReturnCommitStats(getConnectionPropertyValue(RETURN_COMMIT_STATS)) .setExcludeTxnFromChangeStreams(excludeTxnFromChangeStreams) - .setMaxCommitDelay(maxCommitDelay) + .setMaxCommitDelay(getConnectionPropertyValue(MAX_COMMIT_DELAY)) .setStatementTimeout(statementTimeout) .withStatementExecutor(statementExecutor) .setSpan( @@ -1928,30 +1937,30 @@ UnitOfWork createNewUnitOfWork( return ReadOnlyTransaction.newBuilder() .setDatabaseClient(dbClient) .setBatchClient(batchClient) - .setReadOnlyStaleness(readOnlyStaleness) + .setReadOnlyStaleness(getConnectionPropertyValue(READ_ONLY_STALENESS)) .setStatementTimeout(statementTimeout) .withStatementExecutor(statementExecutor) .setTransactionTag(transactionTag) - .setRpcPriority(rpcPriority) + .setRpcPriority(getConnectionPropertyValue(RPC_PRIORITY)) .setSpan(createSpanForUnitOfWork(READ_ONLY_TRANSACTION)) .build(); case READ_WRITE_TRANSACTION: return ReadWriteTransaction.newBuilder() .setUseAutoSavepointsForEmulator(options.useAutoSavepointsForEmulator()) .setDatabaseClient(dbClient) - .setDelayTransactionStartUntilFirstWrite(delayTransactionStartUntilFirstWrite) - .setKeepTransactionAlive(keepTransactionAlive) - .setRetryAbortsInternally( - connectionState.getValue(RETRY_ABORTS_INTERNALLY).getValue()) - .setSavepointSupport(savepointSupport) - .setReturnCommitStats(returnCommitStats) - .setMaxCommitDelay(maxCommitDelay) + .setDelayTransactionStartUntilFirstWrite( + getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE)) + .setKeepTransactionAlive(getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE)) + .setRetryAbortsInternally(getConnectionPropertyValue(RETRY_ABORTS_INTERNALLY)) + .setSavepointSupport(getConnectionPropertyValue(SAVEPOINT_SUPPORT)) + .setReturnCommitStats(getConnectionPropertyValue(RETURN_COMMIT_STATS)) + .setMaxCommitDelay(getConnectionPropertyValue(MAX_COMMIT_DELAY)) .setTransactionRetryListeners(transactionRetryListeners) .setStatementTimeout(statementTimeout) .withStatementExecutor(statementExecutor) .setTransactionTag(transactionTag) .setExcludeTxnFromChangeStreams(excludeTxnFromChangeStreams) - .setRpcPriority(rpcPriority) + .setRpcPriority(getConnectionPropertyValue(RPC_PRIORITY)) .setSpan(createSpanForUnitOfWork(READ_WRITE_TRANSACTION)) .build(); case DML_BATCH: @@ -1964,7 +1973,7 @@ UnitOfWork createNewUnitOfWork( .withStatementExecutor(statementExecutor) .setStatementTag(statementTag) .setExcludeTxnFromChangeStreams(excludeTxnFromChangeStreams) - .setRpcPriority(rpcPriority) + .setRpcPriority(getConnectionPropertyValue(RPC_PRIORITY)) // Use the transaction Span for the DML batch. .setSpan(transactionStack.peek().getSpan()) .build(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 24f68bfec8d..2b8a3990204 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -16,7 +16,39 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_CONFIG_EMULATOR; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.CHANNEL_PROVIDER; +import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_PROVIDER; +import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_URL; +import static com.google.cloud.spanner.connection.ConnectionProperties.DATABASE_ROLE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DATA_BOOST_ENABLED; +import static com.google.cloud.spanner.connection.ConnectionProperties.DIALECT; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_API_TRACING; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_EXTENDED_TRACING; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENCODED_CREDENTIALS; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENDPOINT; +import static com.google.cloud.spanner.connection.ConnectionProperties.LENIENT; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONED_PARALLELISM; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.MIN_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.NUM_CHANNELS; +import static com.google.cloud.spanner.connection.ConnectionProperties.OAUTH_TOKEN; +import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY; import static com.google.cloud.spanner.connection.ConnectionProperties.RETRY_ABORTS_INTERNALLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.RETURN_COMMIT_STATS; +import static com.google.cloud.spanner.connection.ConnectionProperties.ROUTE_TO_LEADER; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACING_PREFIX; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACK_CONNECTION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACK_SESSION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionProperties.USER_AGENT; +import static com.google.cloud.spanner.connection.ConnectionProperties.USE_PLAIN_TEXT; +import static com.google.cloud.spanner.connection.ConnectionProperties.USE_VIRTUAL_GRPC_TRANSPORT_THREADS; +import static com.google.cloud.spanner.connection.ConnectionProperties.USE_VIRTUAL_THREADS; +import static com.google.cloud.spanner.connection.ConnectionPropertyValue.cast; import com.google.api.core.InternalApi; import com.google.api.gax.core.CredentialsProvider; @@ -41,11 +73,8 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; -import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import io.opentelemetry.api.OpenTelemetry; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.time.Duration; import java.util.ArrayList; @@ -90,7 +119,12 @@ */ @InternalApi public class ConnectionOptions { - /** Supported connection properties that can be included in the connection URI. */ + /** + * Supported connection properties that can be included in the connection URI. + * + * @deprecated Replaced by {@link com.google.cloud.spanner.connection.ConnectionProperty}. + */ + @Deprecated public static class ConnectionProperty { private static final String[] BOOLEAN_VALUES = new String[] {"true", "false"}; private final String name; @@ -172,39 +206,39 @@ public String[] getValidValues() { private static final LocalConnectionChecker LOCAL_CONNECTION_CHECKER = new LocalConnectionChecker(); - private static final boolean DEFAULT_USE_PLAIN_TEXT = false; + static final boolean DEFAULT_USE_PLAIN_TEXT = false; static final boolean DEFAULT_AUTOCOMMIT = true; static final boolean DEFAULT_READONLY = false; static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true; static final boolean DEFAULT_USE_VIRTUAL_THREADS = false; static final boolean DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS = false; - private static final String DEFAULT_CREDENTIALS = null; - private static final String DEFAULT_OAUTH_TOKEN = null; - private static final String DEFAULT_MIN_SESSIONS = null; - private static final String DEFAULT_MAX_SESSIONS = null; - private static final String DEFAULT_NUM_CHANNELS = null; + static final String DEFAULT_CREDENTIALS = null; + static final String DEFAULT_OAUTH_TOKEN = null; + static final Integer DEFAULT_MIN_SESSIONS = null; + static final Integer DEFAULT_MAX_SESSIONS = null; + static final Integer DEFAULT_NUM_CHANNELS = null; static final String DEFAULT_ENDPOINT = null; - private static final String DEFAULT_CHANNEL_PROVIDER = null; - private static final String DEFAULT_DATABASE_ROLE = null; - private static final String DEFAULT_USER_AGENT = null; - private static final String DEFAULT_OPTIMIZER_VERSION = ""; - private static final String DEFAULT_OPTIMIZER_STATISTICS_PACKAGE = ""; - private static final RpcPriority DEFAULT_RPC_PRIORITY = null; - private static final DdlInTransactionMode DEFAULT_DDL_IN_TRANSACTION_MODE = + static final String DEFAULT_CHANNEL_PROVIDER = null; + static final String DEFAULT_DATABASE_ROLE = null; + static final String DEFAULT_USER_AGENT = null; + static final String DEFAULT_OPTIMIZER_VERSION = ""; + static final String DEFAULT_OPTIMIZER_STATISTICS_PACKAGE = ""; + static final RpcPriority DEFAULT_RPC_PRIORITY = null; + static final DdlInTransactionMode DEFAULT_DDL_IN_TRANSACTION_MODE = DdlInTransactionMode.ALLOW_IN_EMPTY_TRANSACTION; - private static final boolean DEFAULT_RETURN_COMMIT_STATS = false; - private static final boolean DEFAULT_LENIENT = false; - private static final boolean DEFAULT_ROUTE_TO_LEADER = true; - private static final boolean DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = false; - private static final boolean DEFAULT_KEEP_TRANSACTION_ALIVE = false; - private static final boolean DEFAULT_TRACK_SESSION_LEAKS = true; - private static final boolean DEFAULT_TRACK_CONNECTION_LEAKS = true; - private static final boolean DEFAULT_DATA_BOOST_ENABLED = false; - private static final boolean DEFAULT_AUTO_PARTITION_MODE = false; - private static final int DEFAULT_MAX_PARTITIONS = 0; - private static final int DEFAULT_MAX_PARTITIONED_PARALLELISM = 1; - private static final Boolean DEFAULT_ENABLE_EXTENDED_TRACING = null; - private static final Boolean DEFAULT_ENABLE_API_TRACING = null; + static final boolean DEFAULT_RETURN_COMMIT_STATS = false; + static final boolean DEFAULT_LENIENT = false; + static final boolean DEFAULT_ROUTE_TO_LEADER = true; + static final boolean DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = false; + static final boolean DEFAULT_KEEP_TRANSACTION_ALIVE = false; + static final boolean DEFAULT_TRACK_SESSION_LEAKS = true; + static final boolean DEFAULT_TRACK_CONNECTION_LEAKS = true; + static final boolean DEFAULT_DATA_BOOST_ENABLED = false; + static final boolean DEFAULT_AUTO_PARTITION_MODE = false; + static final int DEFAULT_MAX_PARTITIONS = 0; + static final int DEFAULT_MAX_PARTITIONED_PARALLELISM = 1; + static final Boolean DEFAULT_ENABLE_EXTENDED_TRACING = null; + static final Boolean DEFAULT_ENABLE_API_TRACING = null; private static final String PLAIN_TEXT_PROTOCOL = "http:"; private static final String HOST_PROTOCOL = "https:"; @@ -212,7 +246,7 @@ public String[] getValidValues() { private static final String SPANNER_EMULATOR_HOST_ENV_VAR = "SPANNER_EMULATOR_HOST"; private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010"; /** Use plain text is only for local testing purposes. */ - private static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText"; + static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText"; /** Name of the 'autocommit' connection property. */ public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit"; /** Name of the 'readonly' connection property. */ @@ -255,12 +289,11 @@ public String[] getValidValues() { public static final String ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY = "ENABLE_CHANNEL_PROVIDER"; /** Custom user agent string is only for other Google libraries. */ - private static final String USER_AGENT_PROPERTY_NAME = "userAgent"; + static final String USER_AGENT_PROPERTY_NAME = "userAgent"; /** Query optimizer version to use for a connection. */ - private static final String OPTIMIZER_VERSION_PROPERTY_NAME = "optimizerVersion"; + static final String OPTIMIZER_VERSION_PROPERTY_NAME = "optimizerVersion"; /** Query optimizer statistics package to use for a connection. */ - private static final String OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME = - "optimizerStatisticsPackage"; + static final String OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME = "optimizerStatisticsPackage"; /** Name of the 'lenientMode' connection property. */ public static final String LENIENT_PROPERTY_NAME = "lenient"; /** Name of the 'rpcPriority' connection property. */ @@ -268,7 +301,7 @@ public String[] getValidValues() { public static final String DDL_IN_TRANSACTION_MODE_PROPERTY_NAME = "ddlInTransactionMode"; /** Dialect to use for a connection. */ - private static final String DIALECT_PROPERTY_NAME = "dialect"; + static final String DIALECT_PROPERTY_NAME = "dialect"; /** Name of the 'databaseRole' connection property. */ public static final String DATABASE_ROLE_PROPERTY_NAME = "databaseRole"; /** Name of the 'delay transaction start until first write' property. */ @@ -303,7 +336,12 @@ private static String generateGuardedConnectionPropertyError( systemPropertyName); } - /** All valid connection properties. */ + /** + * All valid connection properties. + * + * @deprecated Replaced by {@link ConnectionProperties#CONNECTION_PROPERTIES} + */ + @Deprecated public static final Set VALID_PROPERTIES = Collections.unmodifiableSet( new HashSet<>( @@ -376,7 +414,8 @@ private static String generateGuardedConnectionPropertyError( "Sets the default query optimizer version to use for this connection."), ConnectionProperty.createStringProperty( OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME, ""), - ConnectionProperty.createBooleanProperty("returnCommitStats", "", false), + ConnectionProperty.createBooleanProperty( + "returnCommitStats", "", DEFAULT_RETURN_COMMIT_STATS), ConnectionProperty.createStringProperty( "maxCommitDelay", "The maximum commit delay in milliseconds that should be applied to commit requests from this connection."), @@ -536,15 +575,12 @@ public static class Builder { private final Map> connectionPropertyValues = new HashMap<>(); private String uri; - private String credentialsUrl; - private String oauthToken; private Credentials credentials; private SessionPoolOptions sessionPoolOptions; private List statementExecutionInterceptors = Collections.emptyList(); private SpannerOptionsConfigurator configurator; private OpenTelemetry openTelemetry; - private String tracingPrefix; private Builder() {} @@ -627,7 +663,9 @@ public Builder setUri(String uri) { Preconditions.checkArgument( isValidUri(uri), "The specified URI is not a valid Cloud Spanner connection URI. Please specify a URI in the format \"cloudspanner:[//host[:port]]/projects/project-id[/instances/instance-id[/databases/database-name]][\\?property-name=property-value[;property-name=property-value]*]?\""); - checkValidProperties(uri); + ConnectionPropertyValue value = + cast(ConnectionProperties.parseValues(uri).get(LENIENT.getKey())); + checkValidProperties(value != null && value.getValue(), uri); this.uri = uri; return this; } @@ -663,7 +701,7 @@ public Builder setSessionPoolOptions(SessionPoolOptions sessionPoolOptions) { * @return this builder */ public Builder setCredentialsUrl(String credentialsUrl) { - this.credentialsUrl = credentialsUrl; + setConnectionPropertyValue(CREDENTIALS_URL, credentialsUrl); return this; } @@ -679,7 +717,7 @@ public Builder setCredentialsUrl(String credentialsUrl) { * @return this builder */ public Builder setOAuthToken(String oauthToken) { - this.oauthToken = oauthToken; + setConnectionPropertyValue(OAUTH_TOKEN, oauthToken); return this; } @@ -707,7 +745,7 @@ public Builder setOpenTelemetry(OpenTelemetry openTelemetry) { } public Builder setTracingPrefix(String tracingPrefix) { - this.tracingPrefix = tracingPrefix; + setConnectionPropertyValue(TRACING_PREFIX, tracingPrefix); return this; } @@ -731,51 +769,16 @@ public static Builder newBuilder() { private final ConnectionState initialConnectionState; private final String uri; private final String warnings; - private final String credentialsUrl; - private final String encodedCredentials; - private final CredentialsProvider credentialsProvider; - private final String oauthToken; private final Credentials fixedCredentials; - private final boolean usePlainText; private final String host; private final String projectId; private final String instanceId; private final String databaseName; private final Credentials credentials; private final SessionPoolOptions sessionPoolOptions; - private final Integer numChannels; - private final String channelProvider; - private final Integer minSessions; - private final Integer maxSessions; - private final String databaseRole; - private final String userAgent; - private final QueryOptions queryOptions; - private final boolean returnCommitStats; - private final Long maxCommitDelay; - private final boolean autoConfigEmulator; - private final Dialect dialect; - private final RpcPriority rpcPriority; - private final DdlInTransactionMode ddlInTransactionMode; - private final boolean delayTransactionStartUntilFirstWrite; - private final boolean keepTransactionAlive; - private final boolean trackSessionLeaks; - private final boolean trackConnectionLeaks; - - private final boolean dataBoostEnabled; - private final boolean autoPartitionMode; - private final int maxPartitions; - private final int maxPartitionedParallelism; - - private final boolean autocommit; - private final boolean readOnly; - private final boolean routeToLeader; - private final boolean useVirtualThreads; - private final boolean useVirtualGrpcTransportThreads; + private final OpenTelemetry openTelemetry; - private final String tracingPrefix; - private final Boolean enableExtendedTracing; - private final Boolean enableApiTracing; private final List statementExecutionInterceptors; private final SpannerOptionsConfigurator configurator; @@ -783,76 +786,79 @@ private ConnectionOptions(Builder builder) { Matcher matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri); Preconditions.checkArgument( matcher.find(), String.format("Invalid connection URI specified: %s", builder.uri)); - this.warnings = checkValidProperties(builder.uri); - this.uri = builder.uri; ImmutableMap> connectionPropertyValues = ImmutableMap.>builder() .putAll(ConnectionProperties.parseValues(builder.uri)) .putAll(builder.connectionPropertyValues) .buildKeepingLast(); - this.credentialsUrl = - builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri); - this.encodedCredentials = parseEncodedCredentials(builder.uri); - this.credentialsProvider = parseCredentialsProvider(builder.uri); - this.oauthToken = - builder.oauthToken != null ? builder.oauthToken : parseOAuthToken(builder.uri); + this.uri = builder.uri; + ConnectionPropertyValue value = cast(connectionPropertyValues.get(LENIENT.getKey())); + this.warnings = checkValidProperties(value != null && value.getValue(), uri); + this.fixedCredentials = builder.credentials; + + this.openTelemetry = builder.openTelemetry; + this.statementExecutionInterceptors = + Collections.unmodifiableList(builder.statementExecutionInterceptors); + this.configurator = builder.configurator; + + // Create the initial connection state from the parsed properties in the connection URL. + this.initialConnectionState = new ConnectionState(connectionPropertyValues); + // Check that at most one of credentials location, encoded credentials, credentials provider and // OUAuth token has been specified in the connection URI. Preconditions.checkArgument( Stream.of( - this.credentialsUrl, - this.encodedCredentials, - this.credentialsProvider, - this.oauthToken) + getInitialConnectionPropertyValue(CREDENTIALS_URL), + getInitialConnectionPropertyValue(ENCODED_CREDENTIALS), + getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER), + getInitialConnectionPropertyValue(OAUTH_TOKEN)) .filter(Objects::nonNull) .count() <= 1, "Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token"); - this.fixedCredentials = builder.credentials; + checkGuardedProperty( + getInitialConnectionPropertyValue(ENCODED_CREDENTIALS), + ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, + ENCODED_CREDENTIALS_PROPERTY_NAME); + checkGuardedProperty( + getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) == null + ? null + : getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getClass().getName(), + ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, + CREDENTIALS_PROVIDER_PROPERTY_NAME); + checkGuardedProperty( + getInitialConnectionPropertyValue(CHANNEL_PROVIDER), + ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY, + CHANNEL_PROVIDER_PROPERTY_NAME); - this.userAgent = parseUserAgent(this.uri); - QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder(); - queryOptionsBuilder.setOptimizerVersion(parseOptimizerVersion(this.uri)); - queryOptionsBuilder.setOptimizerStatisticsPackage(parseOptimizerStatisticsPackage(this.uri)); - this.queryOptions = queryOptionsBuilder.build(); - this.returnCommitStats = parseReturnCommitStats(this.uri); - this.maxCommitDelay = parseMaxCommitDelay(this.uri); - this.autoConfigEmulator = parseAutoConfigEmulator(this.uri); - this.dialect = parseDialect(this.uri); - this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri); + boolean usePlainText = + getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR) + || getInitialConnectionPropertyValue(USE_PLAIN_TEXT); this.host = determineHost( - matcher, parseEndpoint(this.uri), autoConfigEmulator, usePlainText, System.getenv()); - this.rpcPriority = parseRPCPriority(this.uri); - this.ddlInTransactionMode = parseDdlInTransactionMode(this.uri); - this.delayTransactionStartUntilFirstWrite = parseDelayTransactionStartUntilFirstWrite(this.uri); - this.keepTransactionAlive = parseKeepTransactionAlive(this.uri); - this.trackSessionLeaks = parseTrackSessionLeaks(this.uri); - this.trackConnectionLeaks = parseTrackConnectionLeaks(this.uri); - - this.dataBoostEnabled = parseDataBoostEnabled(this.uri); - this.autoPartitionMode = parseAutoPartitionMode(this.uri); - this.maxPartitions = parseMaxPartitions(this.uri); - this.maxPartitionedParallelism = parseMaxPartitionedParallelism(this.uri); - - this.instanceId = matcher.group(Builder.INSTANCE_GROUP); - this.databaseName = matcher.group(Builder.DATABASE_GROUP); + matcher, + getInitialConnectionPropertyValue(ENDPOINT), + getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR), + usePlainText, + System.getenv()); // Using credentials on a plain text connection is not allowed, so if the user has not specified // any credentials and is using a plain text connection, we should not try to get the // credentials from the environment, but default to NoCredentials. if (this.fixedCredentials == null - && this.credentialsUrl == null - && this.encodedCredentials == null - && this.credentialsProvider == null - && this.oauthToken == null - && this.usePlainText) { + && getInitialConnectionPropertyValue(CREDENTIALS_URL) == null + && getInitialConnectionPropertyValue(ENCODED_CREDENTIALS) == null + && getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) == null + && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null + && usePlainText) { this.credentials = NoCredentials.getInstance(); - } else if (this.oauthToken != null) { - this.credentials = new GoogleCredentials(new AccessToken(oauthToken, null)); - } else if (this.credentialsProvider != null) { + } else if (getInitialConnectionPropertyValue(OAUTH_TOKEN) != null) { + this.credentials = + new GoogleCredentials( + new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null)); + } else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) { try { - this.credentials = this.credentialsProvider.getCredentials(); + this.credentials = getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getCredentials(); } catch (IOException exception) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, @@ -861,51 +867,31 @@ private ConnectionOptions(Builder builder) { } } else if (this.fixedCredentials != null) { this.credentials = fixedCredentials; - } else if (this.encodedCredentials != null) { - this.credentials = getCredentialsService().decodeCredentials(this.encodedCredentials); + } else if (getInitialConnectionPropertyValue(ENCODED_CREDENTIALS) != null) { + this.credentials = + getCredentialsService() + .decodeCredentials(getInitialConnectionPropertyValue(ENCODED_CREDENTIALS)); } else { - this.credentials = getCredentialsService().createCredentials(this.credentialsUrl); - } - this.minSessions = - parseIntegerProperty(MIN_SESSIONS_PROPERTY_NAME, parseMinSessions(builder.uri)); - this.maxSessions = - parseIntegerProperty(MAX_SESSIONS_PROPERTY_NAME, parseMaxSessions(builder.uri)); - this.numChannels = - parseIntegerProperty(NUM_CHANNELS_PROPERTY_NAME, parseNumChannels(builder.uri)); - this.channelProvider = parseChannelProvider(builder.uri); - this.databaseRole = parseDatabaseRole(this.uri); - - String projectId = matcher.group(Builder.PROJECT_GROUP); - if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { - projectId = getDefaultProjectId(this.credentials); + this.credentials = + getCredentialsService() + .createCredentials(getInitialConnectionPropertyValue(CREDENTIALS_URL)); } - this.projectId = projectId; - this.autocommit = parseAutocommit(this.uri); - this.readOnly = parseReadOnly(this.uri); - this.routeToLeader = parseRouteToLeader(this.uri); - this.useVirtualThreads = parseUseVirtualThreads(this.uri); - this.useVirtualGrpcTransportThreads = parseUseVirtualGrpcTransportThreads(this.uri); - this.openTelemetry = builder.openTelemetry; - this.tracingPrefix = builder.tracingPrefix; - this.enableExtendedTracing = parseEnableExtendedTracing(this.uri); - this.enableApiTracing = parseEnableApiTracing(this.uri); - this.statementExecutionInterceptors = - Collections.unmodifiableList(builder.statementExecutionInterceptors); - this.configurator = builder.configurator; - - if (this.minSessions != null || this.maxSessions != null || !this.trackSessionLeaks) { + if (getInitialConnectionPropertyValue(MIN_SESSIONS) != null + || getInitialConnectionPropertyValue(MAX_SESSIONS) != null + || !getInitialConnectionPropertyValue(TRACK_SESSION_LEAKS)) { SessionPoolOptions.Builder sessionPoolOptionsBuilder = builder.sessionPoolOptions == null ? SessionPoolOptions.newBuilder() : builder.sessionPoolOptions.toBuilder(); - sessionPoolOptionsBuilder.setTrackStackTraceOfSessionCheckout(this.trackSessionLeaks); + sessionPoolOptionsBuilder.setTrackStackTraceOfSessionCheckout( + getInitialConnectionPropertyValue(TRACK_SESSION_LEAKS)); sessionPoolOptionsBuilder.setAutoDetectDialect(true); - if (this.minSessions != null) { - sessionPoolOptionsBuilder.setMinSessions(this.minSessions); + if (getInitialConnectionPropertyValue(MIN_SESSIONS) != null) { + sessionPoolOptionsBuilder.setMinSessions(getInitialConnectionPropertyValue(MIN_SESSIONS)); } - if (this.maxSessions != null) { - sessionPoolOptionsBuilder.setMaxSessions(this.maxSessions); + if (getInitialConnectionPropertyValue(MAX_SESSIONS) != null) { + sessionPoolOptionsBuilder.setMaxSessions(getInitialConnectionPropertyValue(MAX_SESSIONS)); } this.sessionPoolOptions = sessionPoolOptionsBuilder.build(); } else if (builder.sessionPoolOptions != null) { @@ -913,7 +899,14 @@ private ConnectionOptions(Builder builder) { } else { this.sessionPoolOptions = SessionPoolOptions.newBuilder().setAutoDetectDialect(true).build(); } - this.initialConnectionState = new ConnectionState(connectionPropertyValues); + + String projectId = matcher.group(Builder.PROJECT_GROUP); + if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { + projectId = getDefaultProjectId(this.credentials); + } + this.projectId = projectId; + this.instanceId = matcher.group(Builder.INSTANCE_GROUP); + this.databaseName = matcher.group(Builder.DATABASE_GROUP); } @VisibleForTesting @@ -948,20 +941,6 @@ static String determineHost( return HOST_PROTOCOL + host; } - private static Integer parseIntegerProperty(String propertyName, String value) { - if (value != null) { - try { - return Integer.valueOf(value); - } catch (NumberFormatException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - String.format("Invalid %s value specified: %s", propertyName, value), - e); - } - } - return null; - } - /** * @return an instance of OpenTelemetry. If OpenTelemetry object is not set then null * will be returned. @@ -970,15 +949,6 @@ OpenTelemetry getOpenTelemetry() { return this.openTelemetry; } - /** - * @return The prefix that will be added to all traces that are started by the Connection API. - * This property is used by for example the JDBC driver to make sure all traces start with - * CloudSpannerJdbc. - */ - String getTracingPrefix() { - return this.tracingPrefix; - } - SpannerOptionsConfigurator getConfigurator() { return configurator; } @@ -988,97 +958,6 @@ CredentialsService getCredentialsService() { return CredentialsService.INSTANCE; } - @VisibleForTesting - static boolean parseUsePlainText(String uri) { - String value = parseUriProperty(uri, USE_PLAIN_TEXT_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_USE_PLAIN_TEXT; - } - - @VisibleForTesting - static boolean parseAutocommit(String uri) { - String value = parseUriProperty(uri, AUTOCOMMIT_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_AUTOCOMMIT; - } - - @VisibleForTesting - static boolean parseReadOnly(String uri) { - String value = parseUriProperty(uri, READONLY_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_READONLY; - } - - static boolean parseRouteToLeader(String uri) { - String value = parseUriProperty(uri, ROUTE_TO_LEADER_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_ROUTE_TO_LEADER; - } - - @VisibleForTesting - static boolean parseUseVirtualThreads(String uri) { - String value = parseUriProperty(uri, USE_VIRTUAL_THREADS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_USE_VIRTUAL_THREADS; - } - - @VisibleForTesting - static boolean parseUseVirtualGrpcTransportThreads(String uri) { - String value = parseUriProperty(uri, USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS; - } - - @VisibleForTesting - static @Nullable String parseCredentials(String uri) { - String value = parseUriProperty(uri, CREDENTIALS_PROPERTY_NAME); - return value != null ? value : DEFAULT_CREDENTIALS; - } - - @VisibleForTesting - static @Nullable String parseEncodedCredentials(String uri) { - String encodedCredentials = parseUriProperty(uri, ENCODED_CREDENTIALS_PROPERTY_NAME); - checkGuardedProperty( - encodedCredentials, - ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, - ENCODED_CREDENTIALS_PROPERTY_NAME); - return encodedCredentials; - } - - @VisibleForTesting - static @Nullable CredentialsProvider parseCredentialsProvider(String uri) { - String credentialsProviderName = parseUriProperty(uri, CREDENTIALS_PROVIDER_PROPERTY_NAME); - checkGuardedProperty( - credentialsProviderName, - ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, - CREDENTIALS_PROVIDER_PROPERTY_NAME); - if (!Strings.isNullOrEmpty(credentialsProviderName)) { - try { - Class clazz = - (Class) Class.forName(credentialsProviderName); - Constructor constructor = clazz.getDeclaredConstructor(); - return constructor.newInstance(); - } catch (ClassNotFoundException classNotFoundException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Unknown or invalid CredentialsProvider class name: " + credentialsProviderName, - classNotFoundException); - } catch (NoSuchMethodException noSuchMethodException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Credentials provider " - + credentialsProviderName - + " does not have a public no-arg constructor.", - noSuchMethodException); - } catch (InvocationTargetException - | InstantiationException - | IllegalAccessException exception) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Failed to create an instance of " - + credentialsProviderName - + ": " - + exception.getMessage(), - exception); - } - } - return null; - } - private static void checkGuardedProperty( String value, String systemPropertyName, String connectionPropertyName) { if (!Strings.isNullOrEmpty(value) @@ -1089,213 +968,11 @@ private static void checkGuardedProperty( } } - @VisibleForTesting - static @Nullable String parseOAuthToken(String uri) { - String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME); - return value != null ? value : DEFAULT_OAUTH_TOKEN; - } - - @VisibleForTesting - static String parseMinSessions(String uri) { - String value = parseUriProperty(uri, MIN_SESSIONS_PROPERTY_NAME); - return value != null ? value : DEFAULT_MIN_SESSIONS; - } - - @VisibleForTesting - static String parseMaxSessions(String uri) { - String value = parseUriProperty(uri, MAX_SESSIONS_PROPERTY_NAME); - return value != null ? value : DEFAULT_MAX_SESSIONS; - } - - @VisibleForTesting - static String parseNumChannels(String uri) { - String value = parseUriProperty(uri, NUM_CHANNELS_PROPERTY_NAME); - return value != null ? value : DEFAULT_NUM_CHANNELS; - } - - private static String parseEndpoint(String uri) { - String value = parseUriProperty(uri, ENDPOINT_PROPERTY_NAME); - return value != null ? value : DEFAULT_ENDPOINT; - } - - @VisibleForTesting - static String parseChannelProvider(String uri) { - String value = parseUriProperty(uri, CHANNEL_PROVIDER_PROPERTY_NAME); - checkGuardedProperty( - value, ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY, CHANNEL_PROVIDER_PROPERTY_NAME); - return value != null ? value : DEFAULT_CHANNEL_PROVIDER; - } - - @VisibleForTesting - static String parseDatabaseRole(String uri) { - String value = parseUriProperty(uri, DATABASE_ROLE_PROPERTY_NAME); - return value != null ? value : DEFAULT_DATABASE_ROLE; - } - - @VisibleForTesting - static String parseUserAgent(String uri) { - String value = parseUriProperty(uri, USER_AGENT_PROPERTY_NAME); - return value != null ? value : DEFAULT_USER_AGENT; - } - - @VisibleForTesting - static String parseOptimizerVersion(String uri) { - String value = parseUriProperty(uri, OPTIMIZER_VERSION_PROPERTY_NAME); - return value != null ? value : DEFAULT_OPTIMIZER_VERSION; - } - - @VisibleForTesting - static String parseOptimizerStatisticsPackage(String uri) { - String value = parseUriProperty(uri, OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME); - return value != null ? value : DEFAULT_OPTIMIZER_STATISTICS_PACKAGE; - } - - @VisibleForTesting - static boolean parseReturnCommitStats(String uri) { - String value = parseUriProperty(uri, "returnCommitStats"); - return Boolean.parseBoolean(value); - } - - @VisibleForTesting - static Long parseMaxCommitDelay(String uri) { - String value = parseUriProperty(uri, "maxCommitDelay"); - try { - Long millis = value == null ? null : Long.valueOf(value); - if (millis != null && millis < 0L) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "maxCommitDelay must be >=0"); - } - return millis; - } catch (NumberFormatException numberFormatException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Invalid value for maxCommitDelay: " - + value - + "\n" - + "The value must be a positive integer indicating the number of " - + "milliseconds to use as the max delay."); - } - } - - static boolean parseAutoConfigEmulator(String uri) { - String value = parseUriProperty(uri, "autoConfigEmulator"); - return Boolean.parseBoolean(value); - } - - @VisibleForTesting - static Dialect parseDialect(String uri) { - String value = parseUriProperty(uri, DIALECT_PROPERTY_NAME); - return value != null ? Dialect.valueOf(value.toUpperCase()) : Dialect.GOOGLE_STANDARD_SQL; - } - - @VisibleForTesting - static boolean parseLenient(String uri) { - String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_LENIENT; - } - - @VisibleForTesting - static boolean parseDelayTransactionStartUntilFirstWrite(String uri) { - String value = parseUriProperty(uri, DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME); - return value != null - ? Boolean.parseBoolean(value) - : DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE; - } - - @VisibleForTesting - static boolean parseKeepTransactionAlive(String uri) { - String value = parseUriProperty(uri, KEEP_TRANSACTION_ALIVE_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_KEEP_TRANSACTION_ALIVE; - } - - @VisibleForTesting - static boolean parseTrackSessionLeaks(String uri) { - String value = parseUriProperty(uri, TRACK_SESSION_LEAKS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_TRACK_SESSION_LEAKS; - } - - @VisibleForTesting - static boolean parseTrackConnectionLeaks(String uri) { - String value = parseUriProperty(uri, TRACK_CONNECTION_LEAKS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_TRACK_CONNECTION_LEAKS; - } - - @VisibleForTesting - static boolean parseDataBoostEnabled(String uri) { - String value = parseUriProperty(uri, DATA_BOOST_ENABLED_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_DATA_BOOST_ENABLED; - } - - @VisibleForTesting - static boolean parseAutoPartitionMode(String uri) { - String value = parseUriProperty(uri, AUTO_PARTITION_MODE_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_AUTO_PARTITION_MODE; - } - - @VisibleForTesting - static int parseMaxPartitions(String uri) { - String stringValue = parseUriProperty(uri, MAX_PARTITIONS_PROPERTY_NAME); - if (stringValue == null) { - return DEFAULT_MAX_PARTITIONS; - } - try { - int value = Integer.parseInt(stringValue); - if (value < 0) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "maxPartitions must be >=0"); - } - return value; - } catch (NumberFormatException numberFormatException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "Invalid value for maxPartitions: " + stringValue); - } - } - - @VisibleForTesting - static int parseMaxPartitionedParallelism(String uri) { - String stringValue = parseUriProperty(uri, MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME); - if (stringValue == null) { - return DEFAULT_MAX_PARTITIONED_PARALLELISM; - } - try { - int value = Integer.parseInt(stringValue); - if (value < 0) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "maxPartitionedParallelism must be >=0"); - } - return value; - } catch (NumberFormatException numberFormatException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Invalid value for maxPartitionedParallelism: " + stringValue); - } - } - - @VisibleForTesting - static RpcPriority parseRPCPriority(String uri) { - String value = parseUriProperty(uri, RPC_PRIORITY_NAME); - return value != null ? RpcPriority.valueOf(value) : DEFAULT_RPC_PRIORITY; - } - - @VisibleForTesting - static DdlInTransactionMode parseDdlInTransactionMode(String uri) { - String value = parseUriProperty(uri, DDL_IN_TRANSACTION_MODE_PROPERTY_NAME); - return value != null - ? DdlInTransactionMode.valueOf(value.toUpperCase()) - : DEFAULT_DDL_IN_TRANSACTION_MODE; - } - - @VisibleForTesting - static Boolean parseEnableExtendedTracing(String uri) { - String value = parseUriProperty(uri, ENABLE_EXTENDED_TRACING_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_ENABLE_EXTENDED_TRACING; - } - - @VisibleForTesting - static Boolean parseEnableApiTracing(String uri) { - String value = parseUriProperty(uri, ENABLE_API_TRACING_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_ENABLE_API_TRACING; - } + // @VisibleForTesting + // static boolean parseLenient(String uri) { + // String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME); + // return value != null ? Boolean.parseBoolean(value) : DEFAULT_LENIENT; + // } @VisibleForTesting static String parseUriProperty(String uri, String property) { @@ -1309,10 +986,10 @@ static String parseUriProperty(String uri, String property) { /** Check that only valid properties have been specified. */ @VisibleForTesting - static String checkValidProperties(String uri) { + static String checkValidProperties(boolean lenient, String uri) { String invalidProperties = ""; List properties = parseProperties(uri); - boolean lenient = parseLenient(uri); + // boolean lenient = parseLenient(uri); for (String property : properties) { if (!INTERNAL_VALID_PROPERTIES.contains(ConnectionProperty.createEmptyProperty(property))) { if (invalidProperties.length() > 0) { @@ -1374,13 +1051,18 @@ Map> getInitialConnectionPropertyValues() { return this.initialConnectionState.getAllValues(); } + T getInitialConnectionPropertyValue( + com.google.cloud.spanner.connection.ConnectionProperty property) { + return this.initialConnectionState.getValue(property).getValue(); + } + /** The credentials URL of this {@link ConnectionOptions} */ public String getCredentialsUrl() { - return credentialsUrl; + return getInitialConnectionPropertyValue(CREDENTIALS_URL); } String getOAuthToken() { - return this.oauthToken; + return getInitialConnectionPropertyValue(OAUTH_TOKEN); } Credentials getFixedCredentials() { @@ -1388,7 +1070,7 @@ Credentials getFixedCredentials() { } CredentialsProvider getCredentialsProvider() { - return this.credentialsProvider; + return getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER); } /** The {@link SessionPoolOptions} of this {@link ConnectionOptions}. */ @@ -1402,7 +1084,7 @@ public SessionPoolOptions getSessionPoolOptions() { * database using the same connection settings. */ public Integer getMinSessions() { - return minSessions; + return getInitialConnectionPropertyValue(MIN_SESSIONS); } /** @@ -1411,16 +1093,17 @@ public Integer getMinSessions() { * database using the same connection settings. */ public Integer getMaxSessions() { - return maxSessions; + return getInitialConnectionPropertyValue(MAX_SESSIONS); } /** The number of channels to use for the connection. */ public Integer getNumChannels() { - return numChannels; + return getInitialConnectionPropertyValue(NUM_CHANNELS); } /** Calls the getChannelProvider() method from the supplied class. */ public TransportChannelProvider getChannelProvider() { + String channelProvider = getInitialConnectionPropertyValue(CHANNEL_PROVIDER); if (channelProvider == null) { return null; } @@ -1443,7 +1126,7 @@ public TransportChannelProvider getChannelProvider() { * used to for example restrict the access of a connection to a specific set of tables. */ public String getDatabaseRole() { - return databaseRole; + return getInitialConnectionPropertyValue(DATABASE_ROLE); } /** The host and port number that this {@link ConnectionOptions} will connect to */ @@ -1484,12 +1167,12 @@ public Credentials getCredentials() { /** The initial autocommit value for connections created by this {@link ConnectionOptions} */ public boolean isAutocommit() { - return autocommit; + return getInitialConnectionPropertyValue(AUTOCOMMIT); } /** The initial readonly value for connections created by this {@link ConnectionOptions} */ public boolean isReadOnly() { - return readOnly; + return getInitialConnectionPropertyValue(READONLY); } /** @@ -1497,7 +1180,7 @@ public boolean isReadOnly() { * region. */ public boolean isRouteToLeader() { - return routeToLeader; + return getInitialConnectionPropertyValue(ROUTE_TO_LEADER); } /** @@ -1505,17 +1188,17 @@ public boolean isRouteToLeader() { * ConnectionOptions} */ public boolean isRetryAbortsInternally() { - return this.initialConnectionState.getValue(RETRY_ABORTS_INTERNALLY).getValue(); + return getInitialConnectionPropertyValue(RETRY_ABORTS_INTERNALLY); } /** Whether connections should use virtual threads for connection executors. */ public boolean isUseVirtualThreads() { - return useVirtualThreads; + return getInitialConnectionPropertyValue(USE_VIRTUAL_THREADS); } /** Whether virtual threads should be used for gRPC transport. */ public boolean isUseVirtualGrpcTransportThreads() { - return useVirtualGrpcTransportThreads; + return getInitialConnectionPropertyValue(USE_VIRTUAL_GRPC_TRANSPORT_THREADS); } /** Any warnings that were generated while creating the {@link ConnectionOptions} instance. */ @@ -1526,7 +1209,8 @@ public String getWarnings() { /** Use http instead of https. Only valid for (local) test servers. */ boolean isUsePlainText() { - return usePlainText; + return getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR) + || getInitialConnectionPropertyValue(USE_PLAIN_TEXT); } /** @@ -1534,22 +1218,17 @@ boolean isUsePlainText() { * default JDBC user agent string will be used. */ String getUserAgent() { - return userAgent; - } - - /** The {@link QueryOptions} to use for the connection. */ - QueryOptions getQueryOptions() { - return queryOptions; + return getInitialConnectionPropertyValue(USER_AGENT); } /** Whether connections created by this {@link ConnectionOptions} return commit stats. */ public boolean isReturnCommitStats() { - return returnCommitStats; + return getInitialConnectionPropertyValue(RETURN_COMMIT_STATS); } /** The max_commit_delay that should be applied to commit operations on this connection. */ public Duration getMaxCommitDelay() { - return maxCommitDelay == null ? null : Duration.ofMillis(maxCommitDelay); + return getInitialConnectionPropertyValue(MAX_COMMIT_DELAY); } /** @@ -1559,7 +1238,7 @@ public Duration getMaxCommitDelay() { * emulator instance. */ public boolean isAutoConfigEmulator() { - return autoConfigEmulator; + return getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR); } /** @@ -1569,68 +1248,39 @@ public boolean isAutoConfigEmulator() { boolean useAutoSavepointsForEmulator() { // For now, this option is directly linked to the option autoConfigEmulator=true, which is the // recommended way to configure the emulator for the Connection API. - return autoConfigEmulator; + return getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR); } public Dialect getDialect() { - return dialect; - } - - /** The {@link RpcPriority} to use for the connection. */ - RpcPriority getRPCPriority() { - return rpcPriority; - } - - DdlInTransactionMode getDdlInTransactionMode() { - return this.ddlInTransactionMode; - } - - /** - * Whether connections created by this {@link ConnectionOptions} should delay the actual start of - * a read/write transaction until the first write operation. - */ - boolean isDelayTransactionStartUntilFirstWrite() { - return delayTransactionStartUntilFirstWrite; - } - - /** - * Whether connections created by this {@link ConnectionOptions} should keep read/write - * transactions alive by executing a SELECT 1 once every 10 seconds if no other statements are - * executed. This option should be used with caution, as enabling it can keep transactions alive - * for a very long time, which will hold on to any locks that have been taken by the transaction. - * This option should typically only be enabled for CLI-type applications or other user-input - * applications that might wait for a longer period of time on user input. - */ - boolean isKeepTransactionAlive() { - return keepTransactionAlive; + return getInitialConnectionPropertyValue(DIALECT); } boolean isTrackConnectionLeaks() { - return this.trackConnectionLeaks; + return getInitialConnectionPropertyValue(TRACK_CONNECTION_LEAKS); } boolean isDataBoostEnabled() { - return this.dataBoostEnabled; + return getInitialConnectionPropertyValue(DATA_BOOST_ENABLED); } boolean isAutoPartitionMode() { - return this.autoPartitionMode; + return getInitialConnectionPropertyValue(AUTO_PARTITION_MODE); } int getMaxPartitions() { - return this.maxPartitions; + return getInitialConnectionPropertyValue(MAX_PARTITIONS); } int getMaxPartitionedParallelism() { - return this.maxPartitionedParallelism; + return getInitialConnectionPropertyValue(MAX_PARTITIONED_PARALLELISM); } Boolean isEnableExtendedTracing() { - return this.enableExtendedTracing; + return getInitialConnectionPropertyValue(ENABLE_EXTENDED_TRACING); } Boolean isEnableApiTracing() { - return this.enableApiTracing; + return getInitialConnectionPropertyValue(ENABLE_API_TRACING); } /** Interceptors that should be executed after each statement */ diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java index 48c4aa5f7ee..6a807ef5213 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java @@ -17,18 +17,94 @@ package com.google.cloud.spanner.connection; import static com.google.cloud.spanner.connection.ConnectionOptions.AUTOCOMMIT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_PARTITION_MODE_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.CHANNEL_PROVIDER_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROVIDER_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.DATABASE_ROLE_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.DATA_BOOST_ENABLED_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.DDL_IN_TRANSACTION_MODE_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTOCOMMIT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_PARTITION_MODE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CHANNEL_PROVIDER; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CREDENTIALS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DDL_IN_TRANSACTION_MODE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_API_TRACING; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_EXTENDED_TRACING; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENDPOINT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_KEEP_TRANSACTION_ALIVE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_LENIENT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MAX_PARTITIONED_PARALLELISM; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MAX_PARTITIONS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MAX_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MIN_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_NUM_CHANNELS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_OAUTH_TOKEN; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_OPTIMIZER_STATISTICS_PACKAGE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_OPTIMIZER_VERSION; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_READONLY; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RETRY_ABORTS_INTERNALLY; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RETURN_COMMIT_STATS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ROUTE_TO_LEADER; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RPC_PRIORITY; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_TRACK_CONNECTION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_TRACK_SESSION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USER_AGENT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USE_PLAIN_TEXT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USE_VIRTUAL_THREADS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.DIALECT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_API_TRACING_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_EXTENDED_TRACING_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENCODED_CREDENTIALS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENDPOINT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.KEEP_TRANSACTION_ALIVE_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.LENIENT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MAX_PARTITIONS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MAX_SESSIONS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MIN_SESSIONS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.NUM_CHANNELS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.OAUTH_TOKEN_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.OPTIMIZER_VERSION_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionOptions.READONLY_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionOptions.RETRY_ABORTS_INTERNALLY_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ROUTE_TO_LEADER_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.RPC_PRIORITY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.TRACK_CONNECTION_LEAKS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.TRACK_SESSION_LEAKS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USER_AGENT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USE_PLAIN_TEXT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USE_VIRTUAL_THREADS_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionProperty.castProperty; +import com.google.api.gax.core.CredentialsProvider; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.Options.RpcPriority; +import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.AutocommitDmlModeConverter; import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.BooleanConverter; import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ConnectionStateTypeConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.CredentialsProviderConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DdlInTransactionModeConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DialectConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DurationConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.NonNegativeIntegerConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ReadOnlyStalenessConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.RpcPriorityEnumConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.SavepointSupportConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.StringValueConverter; import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import com.google.cloud.spanner.connection.DirectedReadOptionsUtil.DirectedReadOptionsConverter; import com.google.common.collect.ImmutableMap; +import com.google.spanner.v1.DirectedReadOptions; +import java.time.Duration; import java.util.Map; /** @@ -42,13 +118,200 @@ class ConnectionProperties { static final ConnectionProperty CONNECTION_STATE_TYPE = create( - "connectionStateType", + "connection_state_type", "The type of connection state to use for this connection. Can only be set at start up. " + "If no value is set, then the database dialect default will be used, " + "which is NON_TRANSACTIONAL for GoogleSQL and TRANSACTIONAL for PostgreSQL.", null, ConnectionStateTypeConverter.INSTANCE, Context.STARTUP); + static final ConnectionProperty TRACING_PREFIX = + create( + "tracing_prefix", + "The prefix that will be prepended to all OpenTelemetry traces that are " + + "generated by a Connection.", + "CloudSpanner", + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty LENIENT = + create( + LENIENT_PROPERTY_NAME, + "Silently ignore unknown properties in the connection string/properties (true/false)", + DEFAULT_LENIENT, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENDPOINT = + create( + ENDPOINT_PROPERTY_NAME, + "The endpoint that the JDBC driver should connect to. " + + "The default is the default Spanner production endpoint when autoConfigEmulator=false, " + + "and the default Spanner emulator endpoint (localhost:9010) when autoConfigEmulator=true. " + + "This property takes precedence over any host name at the start of the connection URL.", + DEFAULT_ENDPOINT, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty AUTO_CONFIG_EMULATOR = + create( + "autoConfigEmulator", + "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). " + + "The instance and database in the connection string will automatically be created if these do not yet exist on the emulator. " + + "Add dialect=postgresql to the connection string to make sure that the database that is created uses the PostgreSQL dialect.", + false, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty USE_PLAIN_TEXT = + create( + USE_PLAIN_TEXT_PROPERTY_NAME, + "Use a plain text communication channel (i.e. non-TLS) for communicating with the server (true/false). Set this value to true for communication with the Cloud Spanner emulator.", + DEFAULT_USE_PLAIN_TEXT, + BooleanConverter.INSTANCE, + Context.STARTUP); + + static final ConnectionProperty CREDENTIALS_URL = + create( + CREDENTIALS_PROPERTY_NAME, + "The location of the credentials file to use for this connection. If neither this property or encoded credentials are set, the connection will use the default Google Cloud credentials for the runtime environment.", + DEFAULT_CREDENTIALS, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENCODED_CREDENTIALS = + create( + ENCODED_CREDENTIALS_PROPERTY_NAME, + "Base64-encoded credentials to use for this connection. If neither this property or a credentials location are set, the connection will use the default Google Cloud credentials for the runtime environment.", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty OAUTH_TOKEN = + create( + OAUTH_TOKEN_PROPERTY_NAME, + "A valid pre-existing OAuth token to use for authentication for this connection. Setting this property will take precedence over any value set for a credentials file.", + DEFAULT_OAUTH_TOKEN, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty CREDENTIALS_PROVIDER = + create( + CREDENTIALS_PROVIDER_PROPERTY_NAME, + "The class name of the com.google.api.gax.core.CredentialsProvider implementation that should be used to obtain credentials for connections.", + null, + CredentialsProviderConverter.INSTANCE, + Context.STARTUP); + + static final ConnectionProperty USER_AGENT = + create( + USER_AGENT_PROPERTY_NAME, + "The custom user-agent property name to use when communicating with Cloud Spanner. This property is intended for internal library usage, and should not be set by applications.", + DEFAULT_USER_AGENT, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty DIALECT = + create( + DIALECT_PROPERTY_NAME, + "Sets the dialect to use for new databases that are created by this connection.", + Dialect.GOOGLE_STANDARD_SQL, + DialectConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty TRACK_SESSION_LEAKS = + create( + TRACK_SESSION_LEAKS_PROPERTY_NAME, + "Capture the call stack of the thread that checked out a session of the session pool. This will " + + "pre-create a LeakedSessionException already when a session is checked out. This can be disabled, " + + "for example if a monitoring system logs the pre-created exception. " + + "If disabled, the LeakedSessionException will only be created when an " + + "actual session leak is detected. The stack trace of the exception will " + + "in that case not contain the call stack of when the session was checked out.", + DEFAULT_TRACK_SESSION_LEAKS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty TRACK_CONNECTION_LEAKS = + create( + TRACK_CONNECTION_LEAKS_PROPERTY_NAME, + "Capture the call stack of the thread that created a connection. This will " + + "pre-create a LeakedConnectionException already when a connection is created. " + + "This can be disabled, for example if a monitoring system logs the pre-created exception. " + + "If disabled, the LeakedConnectionException will only be created when an " + + "actual connection leak is detected. The stack trace of the exception will " + + "in that case not contain the call stack of when the connection was created.", + DEFAULT_TRACK_CONNECTION_LEAKS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ROUTE_TO_LEADER = + create( + ROUTE_TO_LEADER_PROPERTY_NAME, + "Should read/write transactions and partitioned DML be routed to leader region (true/false)", + DEFAULT_ROUTE_TO_LEADER, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty USE_VIRTUAL_THREADS = + create( + USE_VIRTUAL_THREADS_PROPERTY_NAME, + "Use a virtual thread instead of a platform thread for each connection (true/false). " + + "This option only has any effect if the application is running on Java 21 or higher. In all other cases, the option is ignored.", + DEFAULT_USE_VIRTUAL_THREADS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty USE_VIRTUAL_GRPC_TRANSPORT_THREADS = + create( + USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME, + "Use a virtual thread instead of a platform thread for the gRPC executor (true/false). " + + "This option only has any effect if the application is running on Java 21 or higher. In all other cases, the option is ignored.", + DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENABLE_EXTENDED_TRACING = + create( + ENABLE_EXTENDED_TRACING_PROPERTY_NAME, + "Include the SQL string in the OpenTelemetry traces that are generated " + + "by this connection. The SQL string is added as the standard OpenTelemetry " + + "attribute 'db.statement'.", + DEFAULT_ENABLE_EXTENDED_TRACING, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENABLE_API_TRACING = + create( + ENABLE_API_TRACING_PROPERTY_NAME, + "Add OpenTelemetry traces for each individual RPC call. Enable this " + + "to get a detailed view of each RPC that is being executed by your application, " + + "or if you want to debug potential latency problems caused by RPCs that are " + + "being retried.", + DEFAULT_ENABLE_API_TRACING, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty MIN_SESSIONS = + create( + MIN_SESSIONS_PROPERTY_NAME, + "The minimum number of sessions in the backing session pool. The default is 100.", + DEFAULT_MIN_SESSIONS, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty MAX_SESSIONS = + create( + MAX_SESSIONS_PROPERTY_NAME, + "The maximum number of sessions in the backing session pool. The default is 400.", + DEFAULT_MAX_SESSIONS, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty NUM_CHANNELS = + create( + NUM_CHANNELS_PROPERTY_NAME, + "The number of gRPC channels to use to communicate with Cloud Spanner. The default is 4.", + DEFAULT_NUM_CHANNELS, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty CHANNEL_PROVIDER = + create( + CHANNEL_PROVIDER_PROPERTY_NAME, + "The name of the channel provider class. The name must reference an implementation of ExternalChannelProvider. If this property is not set, the connection will use the default grpc channel provider.", + DEFAULT_CHANNEL_PROVIDER, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty DATABASE_ROLE = + create( + DATABASE_ROLE_PROPERTY_NAME, + "Sets the database role to use for this connection. The default is privileges assigned to IAM role", + DEFAULT_DATABASE_ROLE, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty AUTOCOMMIT = create( AUTOCOMMIT_PROPERTY_NAME, @@ -72,11 +335,134 @@ class ConnectionProperties { Context.USER); static final ConnectionProperty RETRY_ABORTS_INTERNALLY = create( + // TODO: Add support for synonyms for connection properties. + // retryAbortsInternally / retry_aborts_internally is currently not consistent. + // The connection URL property is retryAbortsInternally. The SET statement assumes + // that the property name is retry_aborts_internally. We should support both to be + // backwards compatible, but the standard should be snake_case. RETRY_ABORTS_INTERNALLY_PROPERTY_NAME, "Should the connection automatically retry Aborted errors (true/false)", DEFAULT_RETRY_ABORTS_INTERNALLY, BooleanConverter.INSTANCE, Context.USER); + static final ConnectionProperty RETURN_COMMIT_STATS = + create( + "returnCommitStats", + "Request that Spanner returns commit statistics for read/write transactions (true/false)", + DEFAULT_RETURN_COMMIT_STATS, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = + create( + DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME, + "Enabling this option will delay the actual start of a read/write transaction until the first write operation is seen in that transaction. " + + "All reads that happen before the first write in a transaction will instead be executed as if the connection was in auto-commit mode. " + + "Enabling this option will make read/write transactions lose their SERIALIZABLE isolation level. Read operations that are executed after " + + "the first write operation in a read/write transaction will be executed using the read/write transaction. Enabling this mode can reduce locking " + + "and improve performance for applications that can handle the lower transaction isolation semantics.", + DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty KEEP_TRANSACTION_ALIVE = + create( + KEEP_TRANSACTION_ALIVE_PROPERTY_NAME, + "Enabling this option will trigger the connection to keep read/write transactions alive by executing a SELECT 1 query once every 10 seconds " + + "if no other statements are being executed. This option should be used with caution, as it can keep transactions alive and hold on to locks " + + "longer than intended. This option should typically be used for CLI-type application that might wait for user input for a longer period of time.", + DEFAULT_KEEP_TRANSACTION_ALIVE, + BooleanConverter.INSTANCE, + Context.USER); + + static final ConnectionProperty READ_ONLY_STALENESS = + create( + "read_only_staleness", + "The read-only staleness to use for read-only transactions and single-use queries.", + TimestampBound.strong(), + ReadOnlyStalenessConverter.INSTANCE, + Context.USER); + static final ConnectionProperty AUTO_PARTITION_MODE = + create( + AUTO_PARTITION_MODE_PROPERTY_NAME, + "Execute all queries on this connection as partitioned queries. " + + "Executing a query that cannot be partitioned will fail. " + + "Executing a query in a read/write transaction will also fail.", + DEFAULT_AUTO_PARTITION_MODE, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty DATA_BOOST_ENABLED = + create( + DATA_BOOST_ENABLED_PROPERTY_NAME, + "Enable data boost for all partitioned queries that are executed by this connection. " + + "This setting is only used for partitioned queries and is ignored by all other statements.", + DEFAULT_DATA_BOOST_ENABLED, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty MAX_PARTITIONS = + create( + MAX_PARTITIONS_PROPERTY_NAME, + "The max partitions hint value to use for partitioned queries. " + + "Use 0 if you do not want to specify a hint.", + DEFAULT_MAX_PARTITIONS, + NonNegativeIntegerConverter.INSTANCE, + Context.USER); + static final ConnectionProperty MAX_PARTITIONED_PARALLELISM = + create( + MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME, + "The max partitions hint value to use for partitioned queries. " + + "Use 0 if you do not want to specify a hint.", + DEFAULT_MAX_PARTITIONED_PARALLELISM, + NonNegativeIntegerConverter.INSTANCE, + Context.USER); + + static final ConnectionProperty DIRECTED_READ = + create( + "directed_read", + "The directed read options to apply to read-only transactions.", + null, + DirectedReadOptionsConverter.INSTANCE, + Context.USER); + static final ConnectionProperty OPTIMIZER_VERSION = + create( + OPTIMIZER_VERSION_PROPERTY_NAME, + "Sets the default query optimizer version to use for this connection.", + DEFAULT_OPTIMIZER_VERSION, + StringValueConverter.INSTANCE, + Context.USER); + static final ConnectionProperty OPTIMIZER_STATISTICS_PACKAGE = + create( + OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME, + "Sets the query optimizer statistics package to use for this connection.", + DEFAULT_OPTIMIZER_STATISTICS_PACKAGE, + StringValueConverter.INSTANCE, + Context.USER); + static final ConnectionProperty RPC_PRIORITY = + create( + RPC_PRIORITY_NAME, + "Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH.", + DEFAULT_RPC_PRIORITY, + RpcPriorityEnumConverter.INSTANCE, + Context.USER); + static final ConnectionProperty SAVEPOINT_SUPPORT = + create( + "savepoint_support", + "Determines the behavior of the connection when savepoints are used.", + SavepointSupport.FAIL_AFTER_ROLLBACK, + SavepointSupportConverter.INSTANCE, + Context.USER); + static final ConnectionProperty DDL_IN_TRANSACTION_MODE = + create( + DDL_IN_TRANSACTION_MODE_PROPERTY_NAME, + "Determines how the connection should handle DDL statements in a read/write transaction.", + DEFAULT_DDL_IN_TRANSACTION_MODE, + DdlInTransactionModeConverter.INSTANCE, + Context.USER); + static final ConnectionProperty MAX_COMMIT_DELAY = + create( + "maxCommitDelay", + "The max delay that Spanner may apply to commit requests to improve throughput.", + null, + DurationConverter.INSTANCE, + Context.USER); static final Map> CONNECTION_PROPERTIES = CONNECTION_PROPERTIES_BUILDER.build(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java index 93415de3cab..2ab641477cf 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java @@ -19,9 +19,9 @@ import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel; -import com.google.protobuf.Duration; import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.RequestOptions.Priority; +import java.time.Duration; /** * The Cloud Spanner JDBC driver supports a number of client side statements that are interpreted by diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java index f99cb764cb2..3b1af4ecc45 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java @@ -103,12 +103,12 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Duration; import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.PlanNode; import com.google.spanner.v1.QueryPlan; import com.google.spanner.v1.RequestOptions; import com.google.spanner.v1.RequestOptions.Priority; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.Map; @@ -218,14 +218,19 @@ public StatementResult statementShowAutocommitDmlMode() { @Override public StatementResult statementSetStatementTimeout(Duration duration) { - if (duration.getSeconds() == 0L && duration.getNanos() == 0) { + if (duration == null || duration.isZero()) { getConnection().clearStatementTimeout(); } else { + com.google.protobuf.Duration protoDuration = + com.google.protobuf.Duration.newBuilder() + .setSeconds(duration.getSeconds()) + .setNanos(duration.getNano()) + .build(); TimeUnit unit = ReadOnlyStalenessUtil.getAppropriateTimeUnit( - new ReadOnlyStalenessUtil.DurationGetter(duration)); + new ReadOnlyStalenessUtil.DurationGetter(protoDuration)); getConnection() - .setStatementTimeout(ReadOnlyStalenessUtil.durationToUnits(duration, unit), unit); + .setStatementTimeout(ReadOnlyStalenessUtil.durationToUnits(protoDuration, unit), unit); } return noResult(SET_STATEMENT_TIMEOUT); } @@ -356,11 +361,7 @@ public StatementResult statementShowReturnCommitStats() { @Override public StatementResult statementSetMaxCommitDelay(Duration duration) { - getConnection() - .setMaxCommitDelay( - duration == null || duration.equals(Duration.getDefaultInstance()) - ? null - : java.time.Duration.ofSeconds(duration.getSeconds(), duration.getNanos())); + getConnection().setMaxCommitDelay(duration == null || duration.isZero() ? null : duration); return noResult(SET_MAX_COMMIT_DELAY); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java index 8b1f8a90199..8b346a08f3d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java @@ -23,6 +23,25 @@ import com.google.spanner.v1.DirectedReadOptions; public class DirectedReadOptionsUtil { + static class DirectedReadOptionsConverter + implements ClientSideStatementValueConverter { + static DirectedReadOptionsConverter INSTANCE = new DirectedReadOptionsConverter(); + + @Override + public Class getParameterClass() { + return DirectedReadOptions.class; + } + + @Override + public DirectedReadOptions convert(String value) { + try { + return parse(value); + } catch (Throwable ignore) { + // ClientSideStatementValueConverters should return null if the value cannot be converted. + return null; + } + } + } /** * Generates a valid JSON string for the given {@link DirectedReadOptions} that can be used with diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyStalenessUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyStalenessUtil.java index dd2b8612ec1..10c8178efb3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyStalenessUtil.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyStalenessUtil.java @@ -27,6 +27,7 @@ import com.google.cloud.spanner.TimestampBound.Mode; import com.google.protobuf.Duration; import com.google.protobuf.util.Durations; +import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; /** @@ -93,6 +94,31 @@ static TimeUnit parseTimeUnit(String unit) { ErrorCode.INVALID_ARGUMENT, "Invalid option for time unit: " + unit); } + /** + * Convert from {@link TimeUnit} to {@link ChronoUnit}. This code is copied from {@link + * TimeUnit#toChronoUnit()}, which is available in Java 9 and higher. + */ + static ChronoUnit toChronoUnit(TimeUnit timeUnit) { + switch (timeUnit) { + case NANOSECONDS: + return ChronoUnit.NANOS; + case MICROSECONDS: + return ChronoUnit.MICROS; + case MILLISECONDS: + return ChronoUnit.MILLIS; + case SECONDS: + return ChronoUnit.SECONDS; + case MINUTES: + return ChronoUnit.MINUTES; + case HOURS: + return ChronoUnit.HOURS; + case DAYS: + return ChronoUnit.DAYS; + default: + throw new IllegalArgumentException(); + } + } + /** * Internal interface that is used to generalize getting a time duration from Cloud Spanner * read-only staleness settings. diff --git a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json index 76ad4e4e921..40bae3a1091 100644 --- a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json +++ b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json @@ -369,7 +369,7 @@ "setStatement": { "propertyName": "STATEMENT_TIMEOUT", "separator": "=", - "allowedValues": "('(\\d{1,19})(s|ms|us|ns)'|NULL)", + "allowedValues": "('(\\d{1,19})(s|ms|us|ns)'|(^\\d{1,19})|NULL)", "converterName": "ClientSideStatementValueConverters$DurationConverter" } }, @@ -489,7 +489,7 @@ "setStatement": { "propertyName": "MAX_COMMIT_DELAY", "separator": "=", - "allowedValues": "('(\\d{1,19})(s|ms|us|ns)'|NULL)", + "allowedValues": "('(\\d{1,19})(s|ms|us|ns)'|(^\\d{1,19})|NULL)", "converterName": "ClientSideStatementValueConverters$DurationConverter" } }, diff --git a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json index cd5f492ca3c..03001458e71 100644 --- a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json +++ b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json @@ -655,7 +655,7 @@ "setStatement": { "propertyName": "SPANNER.MAX_COMMIT_DELAY", "separator": "(?:=|\\s+TO\\s+)", - "allowedValues": "('(\\d{1,19})(s|ms|us|ns)'|NULL)", + "allowedValues": "('(\\d{1,19})(s|ms|us|ns)'|(^\\d{1,19})|NULL)", "converterName": "ClientSideStatementValueConverters$DurationConverter" } }, diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorTest.java index 3e883c377fe..3a5aa1e6d82 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorTest.java @@ -31,7 +31,7 @@ import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.connection.PgTransactionMode.AccessMode; import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel; -import com.google.protobuf.Duration; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; @@ -128,11 +128,11 @@ public void testStatementGetReadTimestamp() { @Test public void testStatementGetStatementTimeout() { - subject.statementSetStatementTimeout(Duration.newBuilder().setSeconds(1L).build()); + subject.statementSetStatementTimeout(Duration.ofSeconds(1L)); when(connection.hasStatementTimeout()).thenReturn(true); subject.statementShowStatementTimeout(); verify(connection, atLeastOnce()).getStatementTimeout(any(TimeUnit.class)); - subject.statementSetStatementTimeout(Duration.getDefaultInstance()); + subject.statementSetStatementTimeout(Duration.ZERO); when(connection.hasStatementTimeout()).thenReturn(false); } @@ -212,7 +212,7 @@ public void testStatementSetOptimizerStatisticsPackage() { @Test public void testStatementSetStatementTimeout() { - subject.statementSetStatementTimeout(Duration.newBuilder().setNanos(100).build()); + subject.statementSetStatementTimeout(Duration.ofNanos(100)); verify(connection).setStatementTimeout(100L, TimeUnit.NANOSECONDS); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java index f4044316fc7..72a8e64ae4c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java @@ -29,7 +29,7 @@ import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; -import com.google.protobuf.Duration; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DurationConverterTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DurationConverterTest.java index 0c26f5b3b76..3a689aa9e1b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DurationConverterTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DurationConverterTest.java @@ -25,7 +25,7 @@ import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.connection.ClientSideStatementImpl.CompileException; import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DurationConverter; -import com.google.protobuf.Duration; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,45 +42,29 @@ public void testConvert() throws CompileException { DurationConverter converter = new DurationConverter(allowedValues); assertThat( converter.convert("'100ms'"), - is( - equalTo( - Duration.newBuilder() - .setNanos((int) TimeUnit.MILLISECONDS.toNanos(100L)) - .build()))); + is(equalTo(Duration.ofNanos((int) TimeUnit.MILLISECONDS.toNanos(100L))))); assertThat(converter.convert("'0ms'"), is(nullValue())); assertThat(converter.convert("'-100ms'"), is(nullValue())); assertThat( - converter.convert("'315576000000000ms'"), - is(equalTo(Duration.newBuilder().setSeconds(315576000000L).build()))); - assertThat( - converter.convert("'1000ms'"), is(equalTo(Duration.newBuilder().setSeconds(1L).build()))); + converter.convert("'315576000000000ms'"), is(equalTo(Duration.ofSeconds(315576000000L)))); + assertThat(converter.convert("'1000ms'"), is(equalTo(Duration.ofSeconds(1L)))); assertThat( converter.convert("'1001ms'"), - is( - equalTo( - Duration.newBuilder() - .setSeconds(1L) - .setNanos((int) TimeUnit.MILLISECONDS.toNanos(1L)) - .build()))); + is(equalTo(Duration.ofSeconds(1L, (int) TimeUnit.MILLISECONDS.toNanos(1L))))); - assertThat(converter.convert("'1ns'"), is(equalTo(Duration.newBuilder().setNanos(1).build()))); - assertThat( - converter.convert("'1us'"), is(equalTo(Duration.newBuilder().setNanos(1000).build()))); - assertThat( - converter.convert("'1ms'"), is(equalTo(Duration.newBuilder().setNanos(1000000).build()))); - assertThat( - converter.convert("'999999999ns'"), - is(equalTo(Duration.newBuilder().setNanos(999999999).build()))); - assertThat( - converter.convert("'1s'"), is(equalTo(Duration.newBuilder().setSeconds(1L).build()))); + assertThat(converter.convert("'1ns'"), is(equalTo(Duration.ofNanos(1)))); + assertThat(converter.convert("'1us'"), is(equalTo(Duration.ofNanos(1000)))); + assertThat(converter.convert("'1ms'"), is(equalTo(Duration.ofNanos(1000000)))); + assertThat(converter.convert("'999999999ns'"), is(equalTo(Duration.ofNanos(999999999)))); + assertThat(converter.convert("'1s'"), is(equalTo(Duration.ofSeconds(1L)))); assertThat(converter.convert("''"), is(nullValue())); assertThat(converter.convert("' '"), is(nullValue())); assertThat(converter.convert("'random string'"), is(nullValue())); - assertThat(converter.convert("null"), is(equalTo(Duration.getDefaultInstance()))); - assertThat(converter.convert("NULL"), is(equalTo(Duration.getDefaultInstance()))); - assertThat(converter.convert("Null"), is(equalTo(Duration.getDefaultInstance()))); + assertThat(converter.convert("null"), is(equalTo(Duration.ZERO))); + assertThat(converter.convert("NULL"), is(equalTo(Duration.ZERO))); + assertThat(converter.convert("Null"), is(equalTo(Duration.ZERO))); assertThat(converter.convert("'null'"), is(nullValue())); assertThat(converter.convert("'NULL'"), is(nullValue())); assertThat(converter.convert("'Null'"), is(nullValue())); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PgDurationConverterTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PgDurationConverterTest.java index b3b0ff1fff5..95bd97962a8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PgDurationConverterTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PgDurationConverterTest.java @@ -23,7 +23,7 @@ import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.connection.ClientSideStatementImpl.CompileException; import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.PgDurationConverter; -import com.google.protobuf.Duration; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,41 +39,33 @@ public void testConvert() throws CompileException { assertNotNull(allowedValues); PgDurationConverter converter = new PgDurationConverter(allowedValues); - assertEquals(Duration.newBuilder().setNanos(1000000).build(), converter.convert("1")); - assertEquals(Duration.newBuilder().setSeconds(1L).build(), converter.convert("1000")); - assertEquals( - Duration.newBuilder().setSeconds(1L).setNanos(1000000).build(), converter.convert("1001")); + assertEquals(Duration.ofNanos(1000000), converter.convert("1")); + assertEquals(Duration.ofSeconds(1L), converter.convert("1000")); + assertEquals(Duration.ofSeconds(1L, 1000000), converter.convert("1001")); assertEquals( - Duration.newBuilder().setNanos((int) TimeUnit.MILLISECONDS.toNanos(100L)).build(), - converter.convert("'100ms'")); + Duration.ofNanos((int) TimeUnit.MILLISECONDS.toNanos(100L)), converter.convert("'100ms'")); assertNull(converter.convert("'0ms'")); assertNull(converter.convert("'-100ms'")); + assertEquals(Duration.ofSeconds(315576000000L), converter.convert("'315576000000000ms'")); + assertEquals(Duration.ofSeconds(1L), converter.convert("'1s'")); assertEquals( - Duration.newBuilder().setSeconds(315576000000L).build(), - converter.convert("'315576000000000ms'")); - assertEquals(Duration.newBuilder().setSeconds(1L).build(), converter.convert("'1s'")); - assertEquals( - Duration.newBuilder() - .setSeconds(1L) - .setNanos((int) TimeUnit.MILLISECONDS.toNanos(1L)) - .build(), + Duration.ofSeconds(1L, (int) TimeUnit.MILLISECONDS.toNanos(1L)), converter.convert("'1001ms'")); - assertEquals(Duration.newBuilder().setNanos(1).build(), converter.convert("'1ns'")); - assertEquals(Duration.newBuilder().setNanos(1000).build(), converter.convert("'1us'")); - assertEquals(Duration.newBuilder().setNanos(1000000).build(), converter.convert("'1ms'")); - assertEquals( - Duration.newBuilder().setNanos(999999999).build(), converter.convert("'999999999ns'")); - assertEquals(Duration.newBuilder().setSeconds(1L).build(), converter.convert("'1s'")); + assertEquals(Duration.ofNanos(1), converter.convert("'1ns'")); + assertEquals(Duration.ofNanos(1000), converter.convert("'1us'")); + assertEquals(Duration.ofNanos(1000000), converter.convert("'1ms'")); + assertEquals(Duration.ofNanos(999999999), converter.convert("'999999999ns'")); + assertEquals(Duration.ofSeconds(1L), converter.convert("'1s'")); assertNull(converter.convert("''")); assertNull(converter.convert("' '")); assertNull(converter.convert("'random string'")); - assertEquals(Duration.getDefaultInstance(), converter.convert("default")); - assertEquals(Duration.getDefaultInstance(), converter.convert("DEFAULT")); - assertEquals(Duration.getDefaultInstance(), converter.convert("Default")); + assertEquals(Duration.ZERO, converter.convert("default")); + assertEquals(Duration.ZERO, converter.convert("DEFAULT")); + assertEquals(Duration.ZERO, converter.convert("Default")); assertNull(converter.convert("'default'")); assertNull(converter.convert("'DEFAULT'")); assertNull(converter.convert("'Default'"));