Skip to content

Commit e8abf33

Browse files
authored
feat: automatically set default sequence kind in JDBC and PGAdapter (#3658)
* feat: automatically set default sequence kind in JDBC and PGAdapter Add a connection property that automatically sets the default sequence kind for a database if a DDL statement fails due to no default sequence kind having been set. This allows tools like PGAdapter and ORMs that use JDBC to automatically use auto_increment columns without the need to add an ALTER DATABASE statement. * chore: fix clirr failures * test: fix integration test * fix: correctly retry if a failure happens in the middle
1 parent 8d1423f commit e8abf33

14 files changed

+563
-95
lines changed

google-cloud-spanner/clirr-ignored-differences.xml

+12
Original file line numberDiff line numberDiff line change
@@ -828,4 +828,16 @@
828828
<className>com/google/cloud/spanner/SpannerOptions$SpannerEnvironment</className>
829829
<method>com.google.auth.oauth2.GoogleCredentials getDefaultExternalHostCredentials()</method>
830830
</difference>
831+
832+
<!-- Default sequence kind -->
833+
<difference>
834+
<differenceType>7012</differenceType>
835+
<className>com/google/cloud/spanner/connection/Connection</className>
836+
<method>void setDefaultSequenceKind(java.lang.String)</method>
837+
</difference>
838+
<difference>
839+
<differenceType>7012</differenceType>
840+
<className>com/google/cloud/spanner/connection/Connection</className>
841+
<method>java.lang.String getDefaultSequenceKind()</method>
842+
</difference>
831843
</differences>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner;
18+
19+
import com.google.api.gax.rpc.ApiException;
20+
import java.util.regex.Pattern;
21+
import javax.annotation.Nullable;
22+
23+
/**
24+
* Exception thrown by Spanner when a DDL statement failed because no default sequence kind has been
25+
* configured for a database.
26+
*/
27+
public class MissingDefaultSequenceKindException extends SpannerException {
28+
private static final long serialVersionUID = 1L;
29+
30+
private static final Pattern PATTERN =
31+
Pattern.compile(
32+
"The sequence kind of an identity column .+ is not specified\\. Please specify the sequence kind explicitly or set the database option `default_sequence_kind`\\.");
33+
34+
/** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */
35+
MissingDefaultSequenceKindException(
36+
DoNotConstructDirectly token,
37+
ErrorCode errorCode,
38+
String message,
39+
Throwable cause,
40+
@Nullable ApiException apiException) {
41+
super(token, errorCode, /*retryable = */ false, message, cause, apiException);
42+
}
43+
44+
static boolean isMissingDefaultSequenceKindException(Throwable cause) {
45+
if (cause == null
46+
|| cause.getMessage() == null
47+
|| !PATTERN.matcher(cause.getMessage()).find()) {
48+
return false;
49+
}
50+
return true;
51+
}
52+
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import static com.google.cloud.spanner.MissingDefaultSequenceKindException.isMissingDefaultSequenceKindException;
1920
import static com.google.cloud.spanner.TransactionMutationLimitExceededException.isTransactionMutationLimitException;
2021

2122
import com.google.api.gax.grpc.GrpcStatusCode;
@@ -336,6 +337,9 @@ static SpannerException newSpannerExceptionPreformatted(
336337
return new TransactionMutationLimitExceededException(
337338
token, code, message, cause, apiException);
338339
}
340+
if (isMissingDefaultSequenceKindException(apiException)) {
341+
return new MissingDefaultSequenceKindException(token, code, message, cause, apiException);
342+
}
339343
// Fall through to the default.
340344
default:
341345
return new SpannerException(

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java

+12
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,18 @@ interface TransactionCallable<T> {
862862
/** Sets how the connection should behave if a DDL statement is executed during a transaction. */
863863
void setDdlInTransactionMode(DdlInTransactionMode ddlInTransactionMode);
864864

865+
/**
866+
* Returns the default sequence kind that will be set for this database if a DDL statement is
867+
* executed that uses auto_increment or serial.
868+
*/
869+
String getDefaultSequenceKind();
870+
871+
/**
872+
* Sets the default sequence kind that will be set for this database if a DDL statement is
873+
* executed that uses auto_increment or serial.
874+
*/
875+
void setDefaultSequenceKind(String defaultSequenceKind);
876+
865877
/**
866878
* Creates a savepoint with the given name.
867879
*

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

+13-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE;
2828
import static com.google.cloud.spanner.connection.ConnectionProperties.DATA_BOOST_ENABLED;
2929
import static com.google.cloud.spanner.connection.ConnectionProperties.DDL_IN_TRANSACTION_MODE;
30+
import static com.google.cloud.spanner.connection.ConnectionProperties.DEFAULT_SEQUENCE_KIND;
3031
import static com.google.cloud.spanner.connection.ConnectionProperties.DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
3132
import static com.google.cloud.spanner.connection.ConnectionProperties.DIRECTED_READ;
3233
import static com.google.cloud.spanner.connection.ConnectionProperties.KEEP_TRANSACTION_ALIVE;
@@ -761,6 +762,16 @@ public void setDdlInTransactionMode(DdlInTransactionMode ddlInTransactionMode) {
761762
setConnectionPropertyValue(DDL_IN_TRANSACTION_MODE, ddlInTransactionMode);
762763
}
763764

765+
@Override
766+
public String getDefaultSequenceKind() {
767+
return getConnectionPropertyValue(DEFAULT_SEQUENCE_KIND);
768+
}
769+
770+
@Override
771+
public void setDefaultSequenceKind(String defaultSequenceKind) {
772+
setConnectionPropertyValue(DEFAULT_SEQUENCE_KIND, defaultSequenceKind);
773+
}
774+
764775
@Override
765776
public void setStatementTimeout(long timeout, TimeUnit unit) {
766777
Preconditions.checkArgument(timeout > 0L, "Zero or negative timeout values are not allowed");
@@ -2152,13 +2163,9 @@ UnitOfWork createNewUnitOfWork(
21522163
.setDdlClient(ddlClient)
21532164
.setDatabaseClient(dbClient)
21542165
.setBatchClient(batchClient)
2155-
.setReadOnly(getConnectionPropertyValue(READONLY))
2156-
.setReadOnlyStaleness(getConnectionPropertyValue(READ_ONLY_STALENESS))
2157-
.setAutocommitDmlMode(getConnectionPropertyValue(AUTOCOMMIT_DML_MODE))
2166+
.setConnectionState(connectionState)
21582167
.setTransactionRetryListeners(transactionRetryListeners)
2159-
.setReturnCommitStats(getConnectionPropertyValue(RETURN_COMMIT_STATS))
21602168
.setExcludeTxnFromChangeStreams(excludeTxnFromChangeStreams)
2161-
.setMaxCommitDelay(getConnectionPropertyValue(MAX_COMMIT_DELAY))
21622169
.setStatementTimeout(statementTimeout)
21632170
.withStatementExecutor(statementExecutor)
21642171
.setSpan(
@@ -2230,6 +2237,7 @@ UnitOfWork createNewUnitOfWork(
22302237
.withStatementExecutor(statementExecutor)
22312238
.setSpan(createSpanForUnitOfWork(DDL_BATCH))
22322239
.setProtoDescriptors(getProtoDescriptors())
2240+
.setConnectionState(connectionState)
22332241
.build();
22342242
default:
22352243
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

+2
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ public String[] getValidValues() {
242242
static final RpcPriority DEFAULT_RPC_PRIORITY = null;
243243
static final DdlInTransactionMode DEFAULT_DDL_IN_TRANSACTION_MODE =
244244
DdlInTransactionMode.ALLOW_IN_EMPTY_TRANSACTION;
245+
static final String DEFAULT_DEFAULT_SEQUENCE_KIND = null;
245246
static final boolean DEFAULT_RETURN_COMMIT_STATS = false;
246247
static final boolean DEFAULT_LENIENT = false;
247248
static final boolean DEFAULT_ROUTE_TO_LEADER = true;
@@ -324,6 +325,7 @@ public String[] getValidValues() {
324325
public static final String RPC_PRIORITY_NAME = "rpcPriority";
325326

326327
public static final String DDL_IN_TRANSACTION_MODE_PROPERTY_NAME = "ddlInTransactionMode";
328+
public static final String DEFAULT_SEQUENCE_KIND_PROPERTY_NAME = "defaultSequenceKind";
327329
/** Dialect to use for a connection. */
328330
static final String DIALECT_PROPERTY_NAME = "dialect";
329331
/** Name of the 'databaseRole' connection property. */

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java

+15-10
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE;
4242
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED;
4343
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DDL_IN_TRANSACTION_MODE;
44+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DEFAULT_SEQUENCE_KIND;
4445
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
4546
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_API_TRACING;
4647
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_END_TO_END_TRACING;
@@ -61,6 +62,7 @@
6162
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RETURN_COMMIT_STATS;
6263
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ROUTE_TO_LEADER;
6364
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RPC_PRIORITY;
65+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_SEQUENCE_KIND_PROPERTY_NAME;
6466
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_TRACK_CONNECTION_LEAKS;
6567
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_TRACK_SESSION_LEAKS;
6668
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USER_AGENT;
@@ -531,6 +533,15 @@ public class ConnectionProperties {
531533
DdlInTransactionMode.values(),
532534
DdlInTransactionModeConverter.INSTANCE,
533535
Context.USER);
536+
static final ConnectionProperty<String> DEFAULT_SEQUENCE_KIND =
537+
create(
538+
DEFAULT_SEQUENCE_KIND_PROPERTY_NAME,
539+
"The default sequence kind that should be used for the database. "
540+
+ "This property is only used when a DDL statement that requires a default "
541+
+ "sequence kind is executed on this connection.",
542+
DEFAULT_DEFAULT_SEQUENCE_KIND,
543+
StringValueConverter.INSTANCE,
544+
Context.USER);
534545
static final ConnectionProperty<Duration> MAX_COMMIT_DELAY =
535546
create(
536547
"maxCommitDelay",
@@ -615,16 +626,10 @@ private static <T> ConnectionProperty<T> create(
615626
T[] validValues,
616627
ClientSideStatementValueConverter<T> converter,
617628
Context context) {
618-
try {
619-
ConnectionProperty<T> property =
620-
ConnectionProperty.create(
621-
name, description, defaultValue, validValues, converter, context);
622-
CONNECTION_PROPERTIES_BUILDER.put(property.getKey(), property);
623-
return property;
624-
} catch (Throwable t) {
625-
t.printStackTrace();
626-
}
627-
return null;
629+
ConnectionProperty<T> property =
630+
ConnectionProperty.create(name, description, defaultValue, validValues, converter, context);
631+
CONNECTION_PROPERTIES_BUILDER.put(property.getKey(), property);
632+
return property;
628633
}
629634

630635
/** Parse the connection properties that can be found in the given connection URL. */

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java

+26-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.connection;
1818

1919
import static com.google.cloud.spanner.connection.AbstractStatementParser.RUN_BATCH_STATEMENT;
20+
import static com.google.cloud.spanner.connection.ConnectionProperties.DEFAULT_SEQUENCE_KIND;
2021

2122
import com.google.api.core.ApiFuture;
2223
import com.google.api.core.ApiFutures;
@@ -45,6 +46,7 @@
4546
import java.util.Arrays;
4647
import java.util.List;
4748
import java.util.concurrent.Callable;
49+
import java.util.concurrent.atomic.AtomicReference;
4850
import javax.annotation.Nonnull;
4951

5052
/**
@@ -61,11 +63,13 @@ class DdlBatch extends AbstractBaseUnitOfWork {
6163
private final List<String> statements = new ArrayList<>();
6264
private UnitOfWorkState state = UnitOfWorkState.STARTED;
6365
private final byte[] protoDescriptors;
66+
private final ConnectionState connectionState;
6467

6568
static class Builder extends AbstractBaseUnitOfWork.Builder<Builder, DdlBatch> {
6669
private DdlClient ddlClient;
6770
private DatabaseClient dbClient;
6871
private byte[] protoDescriptors;
72+
private ConnectionState connectionState;
6973

7074
private Builder() {}
7175

@@ -86,6 +90,11 @@ Builder setProtoDescriptors(byte[] protoDescriptors) {
8690
return this;
8791
}
8892

93+
Builder setConnectionState(ConnectionState connectionState) {
94+
this.connectionState = connectionState;
95+
return this;
96+
}
97+
8998
@Override
9099
DdlBatch build() {
91100
Preconditions.checkState(ddlClient != null, "No DdlClient specified");
@@ -103,6 +112,7 @@ private DdlBatch(Builder builder) {
103112
this.ddlClient = builder.ddlClient;
104113
this.dbClient = builder.dbClient;
105114
this.protoDescriptors = builder.protoDescriptors;
115+
this.connectionState = Preconditions.checkNotNull(builder.connectionState);
106116
}
107117

108118
@Override
@@ -235,17 +245,28 @@ public ApiFuture<long[]> runBatchAsync(CallType callType) {
235245
Callable<long[]> callable =
236246
() -> {
237247
try {
238-
OperationFuture<Void, UpdateDatabaseDdlMetadata> operation =
239-
ddlClient.executeDdl(statements, protoDescriptors);
248+
AtomicReference<OperationFuture<Void, UpdateDatabaseDdlMetadata>> operationReference =
249+
new AtomicReference<>();
240250
try {
241-
// Wait until the operation has finished.
242-
getWithStatementTimeout(operation, RUN_BATCH_STATEMENT);
251+
ddlClient.runWithRetryForMissingDefaultSequenceKind(
252+
restartIndex -> {
253+
OperationFuture<Void, UpdateDatabaseDdlMetadata> operation =
254+
ddlClient.executeDdl(
255+
statements.subList(restartIndex, statements.size()),
256+
protoDescriptors);
257+
operationReference.set(operation);
258+
// Wait until the operation has finished.
259+
getWithStatementTimeout(operation, RUN_BATCH_STATEMENT);
260+
},
261+
connectionState.getValue(DEFAULT_SEQUENCE_KIND).getValue(),
262+
dbClient.getDialect(),
263+
operationReference);
243264
long[] updateCounts = new long[statements.size()];
244265
Arrays.fill(updateCounts, 1L);
245266
state = UnitOfWorkState.RAN;
246267
return updateCounts;
247268
} catch (SpannerException e) {
248-
long[] updateCounts = extractUpdateCounts(operation);
269+
long[] updateCounts = extractUpdateCounts(operationReference.get());
249270
throw SpannerExceptionFactory.newSpannerBatchUpdateException(
250271
e.getErrorCode(), e.getMessage(), updateCounts);
251272
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlClient.java

+47
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@
2222
import com.google.cloud.spanner.DatabaseId;
2323
import com.google.cloud.spanner.Dialect;
2424
import com.google.cloud.spanner.ErrorCode;
25+
import com.google.cloud.spanner.MissingDefaultSequenceKindException;
26+
import com.google.cloud.spanner.SpannerException;
2527
import com.google.cloud.spanner.SpannerExceptionFactory;
2628
import com.google.common.base.Preconditions;
2729
import com.google.common.base.Strings;
2830
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
2931
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
3032
import java.util.Collections;
3133
import java.util.List;
34+
import java.util.concurrent.ExecutionException;
35+
import java.util.concurrent.atomic.AtomicReference;
36+
import java.util.function.Consumer;
3237

3338
/**
3439
* Convenience class for executing Data Definition Language statements on transactions that support
@@ -131,4 +136,46 @@ static boolean isCreateDatabaseStatement(String statement) {
131136
&& tokens[0].equalsIgnoreCase("CREATE")
132137
&& tokens[1].equalsIgnoreCase("DATABASE");
133138
}
139+
140+
void runWithRetryForMissingDefaultSequenceKind(
141+
Consumer<Integer> runnable,
142+
String defaultSequenceKind,
143+
Dialect dialect,
144+
AtomicReference<OperationFuture<Void, UpdateDatabaseDdlMetadata>> operationReference) {
145+
try {
146+
runnable.accept(0);
147+
} catch (Throwable t) {
148+
SpannerException spannerException = SpannerExceptionFactory.asSpannerException(t);
149+
if (!Strings.isNullOrEmpty(defaultSequenceKind)
150+
&& spannerException instanceof MissingDefaultSequenceKindException) {
151+
setDefaultSequenceKind(defaultSequenceKind, dialect);
152+
int restartIndex = 0;
153+
if (operationReference.get() != null) {
154+
try {
155+
UpdateDatabaseDdlMetadata metadata = operationReference.get().getMetadata().get();
156+
restartIndex = metadata.getCommitTimestampsCount();
157+
} catch (Throwable ignore) {
158+
}
159+
}
160+
runnable.accept(restartIndex);
161+
return;
162+
}
163+
throw t;
164+
}
165+
}
166+
167+
private void setDefaultSequenceKind(String defaultSequenceKind, Dialect dialect) {
168+
String ddl =
169+
dialect == Dialect.POSTGRESQL
170+
? "alter database \"%s\" set spanner.default_sequence_kind = '%s'"
171+
: "alter database `%s` set options (default_sequence_kind='%s')";
172+
ddl = String.format(ddl, databaseName, defaultSequenceKind);
173+
try {
174+
executeDdl(ddl, null).get();
175+
} catch (ExecutionException executionException) {
176+
throw SpannerExceptionFactory.asSpannerException(executionException.getCause());
177+
} catch (InterruptedException interruptedException) {
178+
throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
179+
}
180+
}
134181
}

0 commit comments

Comments
 (0)