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 <shirdon@google.com> * 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 <shirdon@google.com> Co-authored-by: cloud-java-bot <cloud-java-bot@google.com>
1 parent 1d7af0c commit b04ea80

11 files changed

+338
-9
lines changed

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

Lines changed: 18 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy