Skip to content

Commit 5a168f1

Browse files
Merge branch 'main' into mux-partitioned-ops-fallback
2 parents 2130f52 + b04ea80 commit 5a168f1

22 files changed

+445
-26
lines changed

.github/workflows/hermetic_library_generation.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
with:
3838
fetch-depth: 0
3939
token: ${{ secrets.CLOUD_JAVA_BOT_TOKEN }}
40-
- uses: googleapis/sdk-platform-java/.github/scripts@v2.52.0
40+
- uses: googleapis/sdk-platform-java/.github/scripts@v2.53.0
4141
if: env.SHOULD_RUN == 'true'
4242
with:
4343
base_ref: ${{ github.base_ref }}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file:
1919
<dependency>
2020
<groupId>com.google.cloud</groupId>
2121
<artifactId>libraries-bom</artifactId>
22-
<version>26.53.0</version>
22+
<version>26.54.0</version>
2323
<type>pom</type>
2424
<scope>import</scope>
2525
</dependency>
@@ -49,7 +49,7 @@ If you are using Maven without the BOM, add this to your dependencies:
4949
If you are using Gradle 5.x or later, add this to your dependencies:
5050

5151
```Groovy
52-
implementation platform('com.google.cloud:libraries-bom:26.53.0')
52+
implementation platform('com.google.cloud:libraries-bom:26.54.0')
5353
5454
implementation 'com.google.cloud:google-cloud-spanner'
5555
```

generation_config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
gapic_generator_version: 2.52.0
2-
googleapis_commitish: 3cf61b2df20eace09e6336c23f9e08859c0d87ae
3-
libraries_bom_version: 26.53.0
1+
gapic_generator_version: 2.53.0
2+
googleapis_commitish: 9605bff3d36fbdb1227b26bce68258c5f00815e4
3+
libraries_bom_version: 26.54.0
44
libraries:
55
- api_shortname: spanner
66
name_pretty: Cloud Spanner

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,9 @@ ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder(
698698
if (!isReadOnly()) {
699699
builder.setSeqno(getSeqNo());
700700
}
701+
if (options.hasLastStatement()) {
702+
builder.setLastStatement(options.isLastStatement());
703+
}
701704
builder.setQueryOptions(buildQueryOptions(statement.getQueryOptions()));
702705
builder.setRequestOptions(buildRequestOptions(options));
703706
return builder;
@@ -743,6 +746,9 @@ ExecuteBatchDmlRequest.Builder getExecuteBatchDmlRequestBuilder(
743746
if (selector != null) {
744747
builder.setTransaction(selector);
745748
}
749+
if (options.hasLastStatement()) {
750+
builder.setLastStatements(options.isLastStatement());
751+
}
746752
builder.setSeqno(getSeqNo());
747753
builder.setRequestOptions(buildRequestOptions(options));
748754
return builder;

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ public interface ReadQueryUpdateTransactionOption
108108
/** Marker interface to mark options applicable to Update and Write operations */
109109
public interface UpdateTransactionOption extends UpdateOption, TransactionOption {}
110110

111+
/** Marker interface for options that can be used with both executeQuery and executeUpdate. */
112+
public interface QueryUpdateOption extends QueryOption, UpdateOption {}
113+
111114
/**
112115
* Marker interface to mark options applicable to Create, Update and Delete operations in admin
113116
* API.
@@ -236,6 +239,20 @@ public static DataBoostQueryOption dataBoostEnabled(Boolean dataBoostEnabled) {
236239
return new DataBoostQueryOption(dataBoostEnabled);
237240
}
238241

242+
/**
243+
* If set to true, this option marks the end of the transaction. The transaction should be
244+
* committed or aborted after this statement executes, and attempts to execute any other requests
245+
* against this transaction (including reads and queries) will be rejected. Mixing mutations with
246+
* statements that are marked as the last statement is not allowed.
247+
*
248+
* <p>For DML statements, setting this option may cause some error reporting to be deferred until
249+
* commit time (e.g. validation of unique constraints). Given this, successful execution of a DML
250+
* statement should not be assumed until the transaction commits.
251+
*/
252+
public static QueryUpdateOption lastStatement() {
253+
return new LastStatementUpdateOption();
254+
}
255+
239256
/**
240257
* Specifying this will cause the list operation to start fetching the record from this onwards.
241258
*/
@@ -494,6 +511,7 @@ void appendToOptions(Options options) {
494511
private DecodeMode decodeMode;
495512
private RpcOrderBy orderBy;
496513
private RpcLockHint lockHint;
514+
private Boolean lastStatement;
497515

498516
// Construction is via factory methods below.
499517
private Options() {}
@@ -630,6 +648,14 @@ OrderBy orderBy() {
630648
return orderBy == null ? null : orderBy.proto;
631649
}
632650

651+
boolean hasLastStatement() {
652+
return lastStatement != null;
653+
}
654+
655+
Boolean isLastStatement() {
656+
return lastStatement;
657+
}
658+
633659
boolean hasLockHint() {
634660
return lockHint != null;
635661
}
@@ -694,6 +720,9 @@ public String toString() {
694720
if (orderBy != null) {
695721
b.append("orderBy: ").append(orderBy).append(' ');
696722
}
723+
if (lastStatement != null) {
724+
b.append("lastStatement: ").append(lastStatement).append(' ');
725+
}
697726
if (lockHint != null) {
698727
b.append("lockHint: ").append(lockHint).append(' ');
699728
}
@@ -737,6 +766,7 @@ public boolean equals(Object o) {
737766
&& Objects.equals(dataBoostEnabled(), that.dataBoostEnabled())
738767
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
739768
&& Objects.equals(orderBy(), that.orderBy())
769+
&& Objects.equals(isLastStatement(), that.isLastStatement())
740770
&& Objects.equals(lockHint(), that.lockHint());
741771
}
742772

@@ -797,6 +827,9 @@ public int hashCode() {
797827
if (orderBy != null) {
798828
result = 31 * result + orderBy.hashCode();
799829
}
830+
if (lastStatement != null) {
831+
result = 31 * result + lastStatement.hashCode();
832+
}
800833
if (lockHint != null) {
801834
result = 31 * result + lockHint.hashCode();
802835
}
@@ -965,4 +998,24 @@ public boolean equals(Object o) {
965998
return Objects.equals(filter, ((FilterOption) o).filter);
966999
}
9671000
}
1001+
1002+
static final class LastStatementUpdateOption extends InternalOption implements QueryUpdateOption {
1003+
1004+
LastStatementUpdateOption() {}
1005+
1006+
@Override
1007+
void appendToOptions(Options options) {
1008+
options.lastStatement = true;
1009+
}
1010+
1011+
@Override
1012+
public int hashCode() {
1013+
return LastStatementUpdateOption.class.hashCode();
1014+
}
1015+
1016+
@Override
1017+
public boolean equals(Object o) {
1018+
return o instanceof LastStatementUpdateOption;
1019+
}
1020+
}
9681021
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ static ErrorDetails extractErrorDetails(Throwable cause) {
265265
if (cause instanceof ApiException) {
266266
return ((ApiException) cause).getErrorDetails();
267267
}
268+
if (cause instanceof SpannerException) {
269+
return ((SpannerException) cause).getErrorDetails();
270+
}
268271
prevCause = cause;
269272
cause = cause.getCause();
270273
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ public void commit() {
8080
} catch (SpannerException e2) {
8181
txnState = TransactionState.COMMIT_FAILED;
8282
throw e2;
83+
} finally {
84+
// At this point, if the TransactionState is not ABORTED, then the transaction has reached an
85+
// end state.
86+
// We can safely call close() to release resources.
87+
if (getState() != TransactionState.ABORTED) {
88+
close();
89+
}
8390
}
8491
}
8592

@@ -92,6 +99,9 @@ public void rollback() {
9299
txn.rollback();
93100
} finally {
94101
txnState = TransactionState.ROLLED_BACK;
102+
// At this point, the TransactionState is ROLLED_BACK which is an end state.
103+
// We can safely call close() to release resources.
104+
close();
95105
}
96106
}
97107

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.cloud.spanner.Mutation;
3535
import com.google.cloud.spanner.Options;
3636
import com.google.cloud.spanner.Options.QueryOption;
37+
import com.google.cloud.spanner.Options.QueryUpdateOption;
3738
import com.google.cloud.spanner.Options.UpdateOption;
3839
import com.google.cloud.spanner.PartitionOptions;
3940
import com.google.cloud.spanner.ReadOnlyTransaction;
@@ -298,7 +299,8 @@ private ApiFuture<ResultSet> executeDmlReturningAsync(
298299
writeTransaction.run(
299300
transaction ->
300301
DirectExecuteResultSet.ofResultSet(
301-
transaction.executeQuery(update.getStatement(), options)));
302+
transaction.executeQuery(
303+
update.getStatement(), appendLastStatement(options))));
302304
state = UnitOfWorkState.COMMITTED;
303305
return resultSet;
304306
} catch (Throwable t) {
@@ -554,11 +556,15 @@ private ApiFuture<Tuple<Long, ResultSet>> executeTransactionalUpdateAsync(
554556
transaction -> {
555557
if (analyzeMode == AnalyzeMode.NONE) {
556558
return Tuple.of(
557-
transaction.executeUpdate(update.getStatement(), options), null);
559+
transaction.executeUpdate(
560+
update.getStatement(), appendLastStatement(options)),
561+
null);
558562
}
559563
ResultSet resultSet =
560564
transaction.analyzeUpdateStatement(
561-
update.getStatement(), analyzeMode.getQueryAnalyzeMode(), options);
565+
update.getStatement(),
566+
analyzeMode.getQueryAnalyzeMode(),
567+
appendLastStatement(options));
562568
return Tuple.of(null, resultSet);
563569
});
564570
state = UnitOfWorkState.COMMITTED;
@@ -582,6 +588,29 @@ private ApiFuture<Tuple<Long, ResultSet>> executeTransactionalUpdateAsync(
582588
return transactionalResult;
583589
}
584590

591+
private static final QueryUpdateOption[] LAST_STATEMENT_OPTIONS =
592+
new QueryUpdateOption[] {Options.lastStatement()};
593+
594+
private static UpdateOption[] appendLastStatement(UpdateOption[] options) {
595+
if (options.length == 0) {
596+
return LAST_STATEMENT_OPTIONS;
597+
}
598+
UpdateOption[] result = new UpdateOption[options.length + 1];
599+
System.arraycopy(options, 0, result, 0, options.length);
600+
result[result.length - 1] = LAST_STATEMENT_OPTIONS[0];
601+
return result;
602+
}
603+
604+
private static QueryOption[] appendLastStatement(QueryOption[] options) {
605+
if (options.length == 0) {
606+
return LAST_STATEMENT_OPTIONS;
607+
}
608+
QueryOption[] result = new QueryOption[options.length + 1];
609+
System.arraycopy(options, 0, result, 0, options.length);
610+
result[result.length - 1] = LAST_STATEMENT_OPTIONS[0];
611+
return result;
612+
}
613+
585614
/**
586615
* Adds a callback to the given future that retries the update statement using Partitioned DML if
587616
* the original statement fails with a {@link TransactionMutationLimitExceededException}.
@@ -719,7 +748,8 @@ private ApiFuture<long[]> executeTransactionalBatchUpdateAsync(
719748
try {
720749
long[] res =
721750
transaction.batchUpdate(
722-
Iterables.transform(updates, ParsedStatement::getStatement), options);
751+
Iterables.transform(updates, ParsedStatement::getStatement),
752+
appendLastStatement(options));
723753
state = UnitOfWorkState.COMMITTED;
724754
return res;
725755
} catch (Throwable t) {

google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
2122
import static org.junit.Assert.assertTrue;
2223
import static org.mockito.Mockito.mock;
2324
import static org.mockito.Mockito.when;
@@ -266,6 +267,42 @@ public void testGetExecuteBatchDmlRequestBuilderWithPriority() {
266267
assertEquals(Priority.PRIORITY_LOW, request.getRequestOptions().getPriority());
267268
}
268269

270+
@Test
271+
public void testExecuteSqlLastStatement() {
272+
assertFalse(
273+
context
274+
.getExecuteSqlRequestBuilder(
275+
Statement.of("insert into test (id) values (1)"),
276+
QueryMode.NORMAL,
277+
Options.fromUpdateOptions(),
278+
false)
279+
.getLastStatement());
280+
assertTrue(
281+
context
282+
.getExecuteSqlRequestBuilder(
283+
Statement.of("insert into test (id) values (1)"),
284+
QueryMode.NORMAL,
285+
Options.fromUpdateOptions(Options.lastStatement()),
286+
false)
287+
.getLastStatement());
288+
}
289+
290+
@Test
291+
public void testExecuteBatchDmlLastStatement() {
292+
assertFalse(
293+
context
294+
.getExecuteBatchDmlRequestBuilder(
295+
Collections.singleton(Statement.of("insert into test (id) values (1)")),
296+
Options.fromUpdateOptions())
297+
.getLastStatements());
298+
assertTrue(
299+
context
300+
.getExecuteBatchDmlRequestBuilder(
301+
Collections.singleton(Statement.of("insert into test (id) values (1)")),
302+
Options.fromUpdateOptions(Options.lastStatement()))
303+
.getLastStatements());
304+
}
305+
269306
public void executeSqlRequestBuilderWithRequestOptions() {
270307
ExecuteSqlRequest request =
271308
context

google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,51 @@ public void clearRequests() {
6060
@Test
6161
public void testAsyncRunner_doesNotReturnCommitTimestampBeforeCommit() {
6262
AsyncRunner runner = client().runAsync();
63-
IllegalStateException e =
64-
assertThrows(IllegalStateException.class, () -> runner.getCommitTimestamp());
65-
assertTrue(e.getMessage().contains("runAsync() has not yet been called"));
63+
if (isMultiplexedSessionsEnabledForRW()) {
64+
Throwable e = assertThrows(Throwable.class, () -> runner.getCommitTimestamp().get());
65+
// If the error occurs within the future, it gets wrapped in an ExecutionException.
66+
// This happens when DelayedAsyncRunner is invoked while the multiplexed session is not yet
67+
// created.
68+
// If the error occurs before the future is created, it may throw an IllegalStateException
69+
// instead.
70+
assertTrue(e instanceof ExecutionException || e instanceof IllegalStateException);
71+
if (e instanceof ExecutionException) {
72+
Throwable cause = e.getCause();
73+
assertTrue(cause instanceof IllegalStateException);
74+
assertTrue(cause.getMessage().contains("runAsync() has not yet been called"));
75+
} else {
76+
assertTrue(e.getMessage().contains("runAsync() has not yet been called"));
77+
}
78+
} else {
79+
IllegalStateException e =
80+
assertThrows(IllegalStateException.class, () -> runner.getCommitTimestamp());
81+
assertTrue(e.getMessage().contains("runAsync() has not yet been called"));
82+
}
6683
}
6784

6885
@Test
6986
public void testAsyncRunner_doesNotReturnCommitResponseBeforeCommit() {
7087
AsyncRunner runner = client().runAsync();
71-
IllegalStateException e =
72-
assertThrows(IllegalStateException.class, () -> runner.getCommitResponse());
73-
assertTrue(e.getMessage().contains("runAsync() has not yet been called"));
88+
if (isMultiplexedSessionsEnabledForRW()) {
89+
Throwable e = assertThrows(Throwable.class, () -> runner.getCommitResponse().get());
90+
// If the error occurs within the future, it gets wrapped in an ExecutionException.
91+
// This happens when DelayedAsyncRunner is invoked while the multiplexed session is not yet
92+
// created.
93+
// If the error occurs before the future is created, it may throw an IllegalStateException
94+
// instead.
95+
assertTrue(e instanceof ExecutionException || e instanceof IllegalStateException);
96+
if (e instanceof ExecutionException) {
97+
Throwable cause = e.getCause();
98+
assertTrue(cause instanceof IllegalStateException);
99+
assertTrue(cause.getMessage().contains("runAsync() has not yet been called"));
100+
} else {
101+
assertTrue(e.getMessage().contains("runAsync() has not yet been called"));
102+
}
103+
} else {
104+
IllegalStateException e =
105+
assertThrows(IllegalStateException.class, () -> runner.getCommitResponse());
106+
assertTrue(e.getMessage().contains("runAsync() has not yet been called"));
107+
}
74108
}
75109

76110
@Test
@@ -558,7 +592,9 @@ public void closeTransactionBeforeEndOfAsyncQuery() throws Exception {
558592
// Wait until at least one row has been fetched. At that moment there should be one session
559593
// checked out.
560594
dataReceived.await();
561-
assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1);
595+
if (!isMultiplexedSessionsEnabledForRW()) {
596+
assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1);
597+
}
562598
assertThat(res.isDone()).isFalse();
563599
dataChecked.countDown();
564600
// Get the data from the transaction.

0 commit comments

Comments
 (0)