Skip to content

Commit 2d8cfa4

Browse files
feat: support analyzeUpdate (#1867)
* feat: support analyzeUpdate Adds support for analyzeUpdate for DML statements, similarly to analyzeQuery for DQL statements. Executing a DML statement in PLAN mode only returns the query plan, but does not actually execute the statement. Executing a DML statement in PROFILE mode executes the statement and returns the query plan and execution statistics. Fixes #1866 * test: fix integration test * build: add ignored differences to clirr * fix: plan returns zero row count * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent c7629d8 commit 2d8cfa4

17 files changed

+562
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ If you are using Maven without 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:25.2.0')
52+
implementation platform('com.google.cloud:libraries-bom:25.3.0')
5353
5454
implementation 'com.google.cloud:google-cloud-spanner'
5555
```

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,15 @@
7070
<className>com/google/cloud/spanner/DatabaseAdminClient</className>
7171
<method>com.google.api.gax.longrunning.OperationFuture createDatabase(java.lang.String, java.lang.String, com.google.cloud.spanner.Dialect, java.lang.Iterable)</method>
7272
</difference>
73+
<!-- Support analyzeUpdate -->
74+
<difference>
75+
<differenceType>7012</differenceType>
76+
<className>com/google/cloud/spanner/TransactionContext</className>
77+
<method>com.google.spanner.v1.ResultSetStats analyzeUpdate(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode, com.google.cloud.spanner.Options$UpdateOption[])</method>
78+
</difference>
79+
<difference>
80+
<differenceType>7012</differenceType>
81+
<className>com/google/cloud/spanner/connection/Connection</className>
82+
<method>com.google.spanner.v1.ResultSetStats analyzeUpdate(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode)</method>
83+
</difference>
7384
</differences>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ long executeStreamingPartitionedUpdate(
9595
resumeToken = rs.getResumeToken();
9696
}
9797
if (rs.hasStats()) {
98-
foundStats = true;
98+
foundStats = rs.getStats().hasRowCountLowerBound();
9999
updateCount += rs.getStats().getRowCountLowerBound();
100100
}
101101
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import com.google.common.util.concurrent.MoreExecutors;
6868
import com.google.common.util.concurrent.SettableFuture;
6969
import com.google.protobuf.Empty;
70+
import com.google.spanner.v1.ResultSetStats;
7071
import io.opencensus.common.Scope;
7172
import io.opencensus.metrics.DerivedLongCumulative;
7273
import io.opencensus.metrics.DerivedLongGauge;
@@ -713,6 +714,16 @@ public ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
713714
return delegate.bufferAsync(mutations);
714715
}
715716

717+
@Override
718+
public ResultSetStats analyzeUpdate(
719+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
720+
try {
721+
return delegate.analyzeUpdate(statement, analyzeMode, options);
722+
} catch (SessionNotFoundException e) {
723+
throw handler.handleSessionNotFound(e);
724+
}
725+
}
726+
716727
@Override
717728
public long executeUpdate(Statement statement, UpdateOption... options) {
718729
try {

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

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

1919
import com.google.api.core.ApiFuture;
2020
import com.google.cloud.spanner.Options.UpdateOption;
21+
import com.google.spanner.v1.ResultSetStats;
2122

2223
/**
2324
* Context for a single attempt of a locking read-write transaction. This type of transaction is the
@@ -126,6 +127,19 @@ default ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
126127
*/
127128
ApiFuture<Long> executeUpdateAsync(Statement statement, UpdateOption... options);
128129

130+
/**
131+
* Analyzes a DML statement and returns query plan and/or execution statistics information.
132+
*
133+
* <p>{@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PLAN} only returns the plan for
134+
* the statement. {@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PROFILE} executes
135+
* the DML statement, returns the modified row count and execution statistics, and the effects of
136+
* the DML statement will be visible to subsequent operations in the transaction.
137+
*/
138+
default ResultSetStats analyzeUpdate(
139+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
140+
throw new UnsupportedOperationException("method should be overwritten");
141+
}
142+
129143
/**
130144
* Executes a list of DML statements in a single request. The statements will be executed in order
131145
* and the semantics is the same as if each statement is executed by {@code executeUpdate} in a

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import com.google.spanner.v1.ExecuteSqlRequest;
4444
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
4545
import com.google.spanner.v1.RequestOptions;
46+
import com.google.spanner.v1.ResultSetStats;
4647
import com.google.spanner.v1.RollbackRequest;
4748
import com.google.spanner.v1.Transaction;
4849
import com.google.spanner.v1.TransactionOptions;
@@ -669,13 +670,39 @@ public ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
669670
return ApiFutures.immediateFuture(null);
670671
}
671672

673+
@Override
674+
public ResultSetStats analyzeUpdate(
675+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
676+
Preconditions.checkNotNull(analyzeMode);
677+
QueryMode queryMode;
678+
switch (analyzeMode) {
679+
case PLAN:
680+
queryMode = QueryMode.PLAN;
681+
break;
682+
case PROFILE:
683+
queryMode = QueryMode.PROFILE;
684+
break;
685+
default:
686+
throw SpannerExceptionFactory.newSpannerException(
687+
ErrorCode.INVALID_ARGUMENT, "Unknown analyze mode: " + analyzeMode);
688+
}
689+
return internalExecuteUpdate(statement, queryMode, options);
690+
}
691+
672692
@Override
673693
public long executeUpdate(Statement statement, UpdateOption... options) {
694+
ResultSetStats resultSetStats = internalExecuteUpdate(statement, QueryMode.NORMAL, options);
695+
// For standard DML, using the exact row count.
696+
return resultSetStats.getRowCountExact();
697+
}
698+
699+
private ResultSetStats internalExecuteUpdate(
700+
Statement statement, QueryMode queryMode, UpdateOption... options) {
674701
beforeReadOrQuery();
675702
final ExecuteSqlRequest.Builder builder =
676703
getExecuteSqlRequestBuilder(
677704
statement,
678-
QueryMode.NORMAL,
705+
queryMode,
679706
Options.fromUpdateOptions(options),
680707
/* withTransactionSelector = */ true);
681708
try {
@@ -689,8 +716,7 @@ public long executeUpdate(Statement statement, UpdateOption... options) {
689716
throw new IllegalArgumentException(
690717
"DML response missing stats possibly due to non-DML statement as input");
691718
}
692-
// For standard DML, using the exact row count.
693-
return resultSet.getStats().getRowCountExact();
719+
return resultSet.getStats();
694720
} catch (Throwable t) {
695721
throw onError(
696722
SpannerExceptionFactory.asSpannerException(t), builder.getTransaction().hasBegin());

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.google.cloud.spanner.TimestampBound;
3838
import com.google.cloud.spanner.connection.StatementResult.ResultType;
3939
import com.google.spanner.v1.ExecuteBatchDmlRequest;
40+
import com.google.spanner.v1.ResultSetStats;
4041
import java.util.Iterator;
4142
import java.util.concurrent.ExecutionException;
4243
import java.util.concurrent.TimeUnit;
@@ -922,7 +923,8 @@ default RpcPriority getRPCPriority() {
922923
AsyncResultSet executeQueryAsync(Statement query, QueryOption... options);
923924

924925
/**
925-
* Analyzes a query and returns query plan and/or query execution statistics information.
926+
* Analyzes a query or a DML statement and returns query plan and/or query execution statistics
927+
* information.
926928
*
927929
* <p>The query plan and query statistics information is contained in {@link
928930
* com.google.spanner.v1.ResultSetStats} that can be accessed by calling {@link
@@ -957,6 +959,18 @@ default RpcPriority getRPCPriority() {
957959
*/
958960
long executeUpdate(Statement update);
959961

962+
/**
963+
* Analyzes a DML statement and returns query plan and/or execution statistics information.
964+
*
965+
* <p>{@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PLAN} only returns the plan for
966+
* the statement. {@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PROFILE} executes
967+
* the DML statement, returns the modified row count and execution statistics, and the effects of
968+
* the DML statement will be visible to subsequent operations in the transaction.
969+
*/
970+
default ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeMode) {
971+
throw new UnsupportedOperationException("Not implemented");
972+
}
973+
960974
/**
961975
* Executes the given statement asynchronously as a DML statement. If the statement does not
962976
* contain a valid DML statement, the method will throw a {@link SpannerException}.

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import com.google.common.base.Preconditions;
4949
import com.google.common.util.concurrent.MoreExecutors;
5050
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
51+
import com.google.spanner.v1.ResultSetStats;
5152
import java.util.ArrayList;
5253
import java.util.Arrays;
5354
import java.util.Collections;
@@ -293,6 +294,9 @@ public void close() {
293294
public ApiFuture<Void> closeAsync() {
294295
if (!isClosed()) {
295296
List<ApiFuture<Void>> futures = new ArrayList<>();
297+
if (isBatchActive()) {
298+
abortBatch();
299+
}
296300
if (isTransactionStarted()) {
297301
futures.add(rollbackAsync());
298302
}
@@ -970,6 +974,7 @@ public long executeUpdate(Statement update) {
970974
"Statement is not an update statement: " + parsedStatement.getSqlWithoutComments());
971975
}
972976

977+
@Override
973978
public ApiFuture<Long> executeUpdateAsync(Statement update) {
974979
Preconditions.checkNotNull(update);
975980
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
@@ -990,6 +995,27 @@ public ApiFuture<Long> executeUpdateAsync(Statement update) {
990995
"Statement is not an update statement: " + parsedStatement.getSqlWithoutComments());
991996
}
992997

998+
@Override
999+
public ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeMode) {
1000+
Preconditions.checkNotNull(update);
1001+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
1002+
ParsedStatement parsedStatement = getStatementParser().parse(update);
1003+
if (parsedStatement.isUpdate()) {
1004+
switch (parsedStatement.getType()) {
1005+
case UPDATE:
1006+
return get(internalAnalyzeUpdateAsync(parsedStatement, AnalyzeMode.of(analyzeMode)));
1007+
case CLIENT_SIDE:
1008+
case QUERY:
1009+
case DDL:
1010+
case UNKNOWN:
1011+
default:
1012+
}
1013+
}
1014+
throw SpannerExceptionFactory.newSpannerException(
1015+
ErrorCode.INVALID_ARGUMENT,
1016+
"Statement is not an update statement: " + parsedStatement.getSqlWithoutComments());
1017+
}
1018+
9931019
@Override
9941020
public long[] executeBatchUpdate(Iterable<Statement> updates) {
9951021
Preconditions.checkNotNull(updates);
@@ -1101,7 +1127,9 @@ private ResultSet internalExecuteQuery(
11011127
final AnalyzeMode analyzeMode,
11021128
final QueryOption... options) {
11031129
Preconditions.checkArgument(
1104-
statement.getType() == StatementType.QUERY, "Statement must be a query");
1130+
statement.getType() == StatementType.QUERY
1131+
|| (statement.getType() == StatementType.UPDATE && analyzeMode != AnalyzeMode.NONE),
1132+
"Statement must either be a query or a DML mode with analyzeMode!=NONE");
11051133
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
11061134
return get(
11071135
transaction.executeQueryAsync(
@@ -1131,6 +1159,15 @@ private ApiFuture<Long> internalExecuteUpdateAsync(
11311159
update, mergeUpdateRequestOptions(mergeUpdateStatementTag(options)));
11321160
}
11331161

1162+
private ApiFuture<ResultSetStats> internalAnalyzeUpdateAsync(
1163+
final ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
1164+
Preconditions.checkArgument(
1165+
update.getType() == StatementType.UPDATE, "Statement must be an update");
1166+
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
1167+
return transaction.analyzeUpdateAsync(
1168+
update, analyzeMode, mergeUpdateRequestOptions(mergeUpdateStatementTag(options)));
1169+
}
1170+
11341171
private ApiFuture<long[]> internalExecuteBatchUpdateAsync(
11351172
List<ParsedStatement> updates, UpdateOption... options) {
11361173
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.google.common.base.Preconditions;
3939
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc;
4040
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
41+
import com.google.spanner.v1.ResultSetStats;
4142
import com.google.spanner.v1.SpannerGrpc;
4243
import java.util.ArrayList;
4344
import java.util.Arrays;
@@ -201,6 +202,13 @@ public ApiFuture<Long> executeUpdateAsync(ParsedStatement update, UpdateOption..
201202
ErrorCode.FAILED_PRECONDITION, "Executing updates is not allowed for DDL batches.");
202203
}
203204

205+
@Override
206+
public ApiFuture<ResultSetStats> analyzeUpdateAsync(
207+
ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
208+
throw SpannerExceptionFactory.newSpannerException(
209+
ErrorCode.FAILED_PRECONDITION, "Analyzing updates is not allowed for DDL batches.");
210+
}
211+
204212
@Override
205213
public ApiFuture<long[]> executeBatchUpdateAsync(
206214
Iterable<ParsedStatement> updates, UpdateOption... options) {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
3434
import com.google.common.base.Preconditions;
3535
import com.google.common.util.concurrent.MoreExecutors;
36+
import com.google.spanner.v1.ResultSetStats;
3637
import java.util.ArrayList;
3738
import java.util.List;
3839

@@ -161,6 +162,13 @@ public ApiFuture<Long> executeUpdateAsync(ParsedStatement update, UpdateOption..
161162
return ApiFutures.immediateFuture(-1L);
162163
}
163164

165+
@Override
166+
public ApiFuture<ResultSetStats> analyzeUpdateAsync(
167+
ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
168+
throw SpannerExceptionFactory.newSpannerException(
169+
ErrorCode.FAILED_PRECONDITION, "Analyzing updates is not allowed for DML batches.");
170+
}
171+
164172
@Override
165173
public ApiFuture<long[]> executeBatchUpdateAsync(
166174
Iterable<ParsedStatement> updates, UpdateOption... options) {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.cloud.spanner.TimestampBound;
3131
import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
3232
import com.google.common.base.Preconditions;
33+
import com.google.spanner.v1.ResultSetStats;
3334

3435
/**
3536
* Transaction that is used when a {@link Connection} is in read-only mode or when the transaction
@@ -163,6 +164,14 @@ public ApiFuture<Long> executeUpdateAsync(ParsedStatement update, UpdateOption..
163164
"Update statements are not allowed for read-only transactions");
164165
}
165166

167+
@Override
168+
public ApiFuture<ResultSetStats> analyzeUpdateAsync(
169+
ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
170+
throw SpannerExceptionFactory.newSpannerException(
171+
ErrorCode.FAILED_PRECONDITION,
172+
"Analyzing updates is not allowed for read-only transactions");
173+
}
174+
166175
@Override
167176
public ApiFuture<long[]> executeBatchUpdateAsync(
168177
Iterable<ParsedStatement> updates, UpdateOption... options) {

0 commit comments

Comments
 (0)