Skip to content

Commit b04ea80

Browse files
olavloitesgorse12cloud-java-bot
authored
feat: add option to indicate that a statement is the last in a transaction (#3647)
* feat: add option to indicate that a statement is the last in a transaction (#3644) * feat: Add LastStatement DML option * Removing debugging changes --------- Co-authored-by: Shirdon Gorse <[email protected]> * feat: set last_statement for autocommit statements Automatically sets the last_statement option to true for DML statements that are executed in autocommit=true mode. * fix: add hashCode implementation Add a hashCode implementation that ensures that it is in sync with the equals method. * chore: generate libraries at Fri Feb 14 08:41:45 UTC 2025 * fix: extract error details from SpannerException --------- Co-authored-by: Shirdon Gorse <[email protected]> Co-authored-by: cloud-java-bot <[email protected]>
1 parent 1d7af0c commit b04ea80

11 files changed

+338
-9
lines changed

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

+6
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

+53
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

+3
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/connection/SingleUseTransaction.java

+34-4
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

+37
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/OptionsTest.java

+18
Original file line numberDiff line numberDiff line change
@@ -789,4 +789,22 @@ public void updateOptionsExcludeTxnFromChangeStreams() {
789789
assertNull(option3.withExcludeTxnFromChangeStreams());
790790
assertThat(option3.toString()).doesNotContain("withExcludeTxnFromChangeStreams: true");
791791
}
792+
793+
@Test
794+
public void testLastStatement() {
795+
Options option1 = Options.fromUpdateOptions(Options.lastStatement());
796+
Options option2 = Options.fromUpdateOptions(Options.lastStatement());
797+
Options option3 = Options.fromUpdateOptions();
798+
799+
assertEquals(option1, option2);
800+
assertEquals(option1.hashCode(), option2.hashCode());
801+
assertNotEquals(option1, option3);
802+
assertNotEquals(option1.hashCode(), option3.hashCode());
803+
804+
assertTrue(option1.isLastStatement());
805+
assertThat(option1.toString()).contains("lastStatement: true");
806+
807+
assertNull(option3.isLastStatement());
808+
assertThat(option3.toString()).doesNotContain("lastStatement: true");
809+
}
792810
}

0 commit comments

Comments
 (0)