Skip to content

Commit 7c5e3da

Browse files
authored
feat: analyze update returns param types (#2156)
* feat: analyzeStatement for both DML and queries Add support for `analyzeStatement` that works for both DML and queries. * feat: analyze update returns param types Add the method analyzeUpdateStatement that returns the undeclared parameters in an update statement. This allows connection based APIs to return more metadata for a statement than is currently possible: 1. JDBC should return the parameter data types when PreparedStatement#getMetaData() is called. 2. PGAdapter should return the parameter types when a DescribeStatement message is received. * chore: add clirr differences * chore: address review comments
1 parent 82385b8 commit 7c5e3da

25 files changed

+496
-62
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,19 @@
207207
<className>com/google/cloud/spanner/connection/AbstractStatementParser</className>
208208
<method>boolean checkReturningClauseInternal(java.lang.String)</method>
209209
</difference>
210+
<difference>
211+
<differenceType>7012</differenceType>
212+
<className>com/google/cloud/spanner/ResultSet</className>
213+
<method>com.google.spanner.v1.ResultSetMetadata getMetadata()</method>
214+
</difference>
215+
<difference>
216+
<differenceType>7012</differenceType>
217+
<className>com/google/cloud/spanner/TransactionContext</className>
218+
<method>com.google.cloud.spanner.ResultSet analyzeUpdateStatement(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode, com.google.cloud.spanner.Options$UpdateOption[])</method>
219+
</difference>
220+
<difference>
221+
<differenceType>7012</differenceType>
222+
<className>com/google/cloud/spanner/connection/Connection</className>
223+
<method>com.google.cloud.spanner.ResultSet analyzeUpdateStatement(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode, com.google.cloud.spanner.Options$UpdateOption[])</method>
224+
</difference>
210225
</differences>

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ void onTransactionMetadata(Transaction transaction, boolean shouldIncludeId)
9393
static class GrpcResultSet extends AbstractResultSet<List<Object>> {
9494
private final GrpcValueIterator iterator;
9595
private final Listener listener;
96+
private ResultSetMetadata metadata;
9697
private GrpcStruct currRow;
9798
private SpannerException error;
9899
private ResultSetStats statistics;
@@ -117,7 +118,7 @@ public boolean next() throws SpannerException {
117118
}
118119
try {
119120
if (currRow == null) {
120-
ResultSetMetadata metadata = iterator.getMetadata();
121+
metadata = iterator.getMetadata();
121122
if (metadata.hasTransaction()) {
122123
listener.onTransactionMetadata(
123124
metadata.getTransaction(), iterator.isWithBeginTransaction());
@@ -146,6 +147,12 @@ public ResultSetStats getStats() {
146147
return statistics;
147148
}
148149

150+
@Override
151+
public ResultSetMetadata getMetadata() {
152+
checkState(metadata != null, "next() call required");
153+
return metadata;
154+
}
155+
149156
@Override
150157
public void close() {
151158
listener.onDone(iterator.isWithBeginTransaction());

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.common.collect.ImmutableList;
3030
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
3131
import com.google.common.util.concurrent.MoreExecutors;
32+
import com.google.spanner.v1.ResultSetMetadata;
3233
import com.google.spanner.v1.ResultSetStats;
3334
import java.util.Collection;
3435
import java.util.LinkedList;
@@ -572,6 +573,11 @@ public ResultSetStats getStats() {
572573
return delegateResultSet.get().getStats();
573574
}
574575

576+
@Override
577+
public ResultSetMetadata getMetadata() {
578+
return delegateResultSet.get().getMetadata();
579+
}
580+
575581
@Override
576582
protected void checkValidState() {
577583
synchronized (monitor) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.common.base.Preconditions;
2020
import com.google.common.base.Supplier;
2121
import com.google.common.base.Suppliers;
22+
import com.google.spanner.v1.ResultSetMetadata;
2223
import com.google.spanner.v1.ResultSetStats;
2324

2425
/** Forwarding implementation of ResultSet that forwards all calls to a delegate. */
@@ -76,4 +77,9 @@ public void close() {
7677
public ResultSetStats getStats() {
7778
return delegate.get().getStats();
7879
}
80+
81+
@Override
82+
public ResultSetMetadata getMetadata() {
83+
return delegate.get().getMetadata();
84+
}
7985
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2022 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.spanner.v1.ResultSet;
20+
import com.google.spanner.v1.ResultSetMetadata;
21+
import com.google.spanner.v1.ResultSetStats;
22+
import java.util.List;
23+
import javax.annotation.Nullable;
24+
25+
class NoRowsResultSet extends AbstractResultSet<List<Object>> {
26+
private final ResultSetStats stats;
27+
private final ResultSetMetadata metadata;
28+
29+
NoRowsResultSet(ResultSet resultSet) {
30+
this.stats = resultSet.getStats();
31+
this.metadata = resultSet.getMetadata();
32+
}
33+
34+
@Override
35+
protected GrpcStruct currRow() {
36+
throw SpannerExceptionFactory.newSpannerException(
37+
ErrorCode.FAILED_PRECONDITION, "This result set has no rows");
38+
}
39+
40+
@Override
41+
public boolean next() throws SpannerException {
42+
return false;
43+
}
44+
45+
@Override
46+
public void close() {}
47+
48+
@Nullable
49+
@Override
50+
public ResultSetStats getStats() {
51+
return stats;
52+
}
53+
54+
@Override
55+
public ResultSetMetadata getMetadata() {
56+
return metadata;
57+
}
58+
59+
@Override
60+
public Type getType() {
61+
return Type.struct();
62+
}
63+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner;
1818

1919
import com.google.cloud.spanner.Options.QueryOption;
20+
import com.google.spanner.v1.ResultSetMetadata;
2021
import com.google.spanner.v1.ResultSetStats;
2122
import javax.annotation.Nullable;
2223

@@ -73,4 +74,12 @@ public interface ResultSet extends AutoCloseable, StructReader {
7374
*/
7475
@Nullable
7576
ResultSetStats getStats();
77+
78+
/**
79+
* Returns the {@link ResultSetMetadata} for this {@link ResultSet}. This is method may only be
80+
* called after calling {@link ResultSet#next()} at least once.
81+
*/
82+
default ResultSetMetadata getMetadata() {
83+
throw new UnsupportedOperationException("Method should be overridden");
84+
}
7685
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.common.base.Supplier;
3030
import com.google.common.collect.Lists;
3131
import com.google.common.util.concurrent.ThreadFactoryBuilder;
32+
import com.google.spanner.v1.ResultSetMetadata;
3233
import com.google.spanner.v1.ResultSetStats;
3334
import java.math.BigDecimal;
3435
import java.util.List;
@@ -158,6 +159,12 @@ public ResultSetStats getStats() {
158159
"ResultSetStats are available only for results returned from analyzeQuery() calls");
159160
}
160161

162+
@Override
163+
public ResultSetMetadata getMetadata() {
164+
throw new UnsupportedOperationException(
165+
"ResultSetMetadata are available only for results that were returned from Cloud Spanner");
166+
}
167+
161168
@Override
162169
public int getColumnCount() {
163170
return getType().getStructFields().size();

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,14 @@ public ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
717717
@Override
718718
public ResultSetStats analyzeUpdate(
719719
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
720+
return analyzeUpdateStatement(statement, analyzeMode, options).getStats();
721+
}
722+
723+
@Override
724+
public ResultSet analyzeUpdateStatement(
725+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
720726
try {
721-
return delegate.analyzeUpdate(statement, analyzeMode, options);
727+
return delegate.analyzeUpdateStatement(statement, analyzeMode, options);
722728
} catch (SessionNotFoundException e) {
723729
throw handler.handleSessionNotFound(e);
724730
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,31 @@ default ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
135135
* the statement. {@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PROFILE} executes
136136
* the DML statement, returns the modified row count and execution statistics, and the effects of
137137
* the DML statement will be visible to subsequent operations in the transaction.
138+
*
139+
* @deprecated Use {@link #analyzeUpdateStatement(Statement, QueryAnalyzeMode, UpdateOption...)}
140+
* instead to get both statement plan and parameter metadata
138141
*/
142+
@Deprecated
139143
default ResultSetStats analyzeUpdate(
140144
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
141145
throw new UnsupportedOperationException("method should be overwritten");
142146
}
143147

148+
/**
149+
* Analyzes a DML statement and returns query plan and statement parameter metadata and optionally
150+
* execution statistics information.
151+
*
152+
* <p>{@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PLAN} only returns the plan and
153+
* parameter metadata for the statement. {@link
154+
* com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PROFILE} executes the DML statement,
155+
* returns the modified row count and execution statistics, and the effects of the DML statement
156+
* will be visible to subsequent operations in the transaction.
157+
*/
158+
default ResultSet analyzeUpdateStatement(
159+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
160+
throw new UnsupportedOperationException("method should be overwritten");
161+
}
162+
144163
/**
145164
* Executes a list of DML statements (which can include simple DML statements or DML statements
146165
* with returning clause) in a single request. The statements will be executed in order and the

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

Lines changed: 16 additions & 4 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.ResultSet;
4647
import com.google.spanner.v1.ResultSetStats;
4748
import com.google.spanner.v1.RollbackRequest;
4849
import com.google.spanner.v1.Transaction;
@@ -673,6 +674,17 @@ public ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
673674
@Override
674675
public ResultSetStats analyzeUpdate(
675676
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
677+
return internalAnalyzeStatement(statement, analyzeMode, options).getStats();
678+
}
679+
680+
@Override
681+
public com.google.cloud.spanner.ResultSet analyzeUpdateStatement(
682+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
683+
return new NoRowsResultSet(internalAnalyzeStatement(statement, analyzeMode, options));
684+
}
685+
686+
private ResultSet internalAnalyzeStatement(
687+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
676688
Preconditions.checkNotNull(analyzeMode);
677689
QueryMode queryMode;
678690
switch (analyzeMode) {
@@ -691,12 +703,12 @@ public ResultSetStats analyzeUpdate(
691703

692704
@Override
693705
public long executeUpdate(Statement statement, UpdateOption... options) {
694-
ResultSetStats resultSetStats = internalExecuteUpdate(statement, QueryMode.NORMAL, options);
706+
ResultSet resultSet = internalExecuteUpdate(statement, QueryMode.NORMAL, options);
695707
// For standard DML, using the exact row count.
696-
return resultSetStats.getRowCountExact();
708+
return resultSet.getStats().getRowCountExact();
697709
}
698710

699-
private ResultSetStats internalExecuteUpdate(
711+
private ResultSet internalExecuteUpdate(
700712
Statement statement, QueryMode queryMode, UpdateOption... options) {
701713
beforeReadOrQuery();
702714
final ExecuteSqlRequest.Builder builder =
@@ -716,7 +728,7 @@ private ResultSetStats internalExecuteUpdate(
716728
throw new IllegalArgumentException(
717729
"DML response missing stats possibly due to non-DML statement as input");
718730
}
719-
return resultSet.getStats();
731+
return resultSet;
720732
} catch (Throwable t) {
721733
throw onError(
722734
SpannerExceptionFactory.asSpannerException(t), builder.getTransaction().hasBegin());

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.cloud.spanner.Mutation;
3030
import com.google.cloud.spanner.Options.QueryOption;
3131
import com.google.cloud.spanner.Options.RpcPriority;
32+
import com.google.cloud.spanner.Options.UpdateOption;
3233
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
3334
import com.google.cloud.spanner.ResultSet;
3435
import com.google.cloud.spanner.SpannerBatchUpdateException;
@@ -968,11 +969,30 @@ default RpcPriority getRPCPriority() {
968969
* the statement. {@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PROFILE} executes
969970
* the DML statement, returns the modified row count and execution statistics, and the effects of
970971
* the DML statement will be visible to subsequent operations in the transaction.
972+
*
973+
* @deprecated Use {@link #analyzeUpdateStatement(Statement, QueryAnalyzeMode, UpdateOption...)}
974+
* instead
971975
*/
976+
@Deprecated
972977
default ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeMode) {
973978
throw new UnsupportedOperationException("Not implemented");
974979
}
975980

981+
/**
982+
* Analyzes a DML statement and returns execution plan, undeclared parameters and optionally
983+
* execution statistics information.
984+
*
985+
* <p>{@link com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PLAN} only returns the plan and
986+
* undeclared parameters for the statement. {@link
987+
* com.google.cloud.spanner.ReadContext.QueryAnalyzeMode#PROFILE} also executes the DML statement,
988+
* returns the modified row count and execution statistics, and the effects of the DML statement
989+
* will be visible to subsequent operations in the transaction.
990+
*/
991+
default ResultSet analyzeUpdateStatement(
992+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
993+
throw new UnsupportedOperationException("Not implemented");
994+
}
995+
976996
/**
977997
* Executes the given statement asynchronously as a simple DML statement. If the statement does
978998
* not contain a simple DML statement, the method will throw a {@link SpannerException}. A DML

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,8 @@ public ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeMo
10651065
if (parsedStatement.isUpdate()) {
10661066
switch (parsedStatement.getType()) {
10671067
case UPDATE:
1068-
return get(internalAnalyzeUpdateAsync(parsedStatement, AnalyzeMode.of(analyzeMode)));
1068+
return get(internalAnalyzeUpdateAsync(parsedStatement, AnalyzeMode.of(analyzeMode)))
1069+
.getStats();
10691070
case CLIENT_SIDE:
10701071
case QUERY:
10711072
case DDL:
@@ -1078,6 +1079,27 @@ public ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeMo
10781079
"Statement is not an update statement: " + parsedStatement.getSqlWithoutComments());
10791080
}
10801081

1082+
@Override
1083+
public ResultSet analyzeUpdateStatement(
1084+
Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
1085+
Preconditions.checkNotNull(statement);
1086+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
1087+
ParsedStatement parsedStatement = getStatementParser().parse(statement);
1088+
switch (parsedStatement.getType()) {
1089+
case UPDATE:
1090+
return get(
1091+
internalAnalyzeUpdateAsync(parsedStatement, AnalyzeMode.of(analyzeMode), options));
1092+
case QUERY:
1093+
case CLIENT_SIDE:
1094+
case DDL:
1095+
case UNKNOWN:
1096+
default:
1097+
}
1098+
throw SpannerExceptionFactory.newSpannerException(
1099+
ErrorCode.INVALID_ARGUMENT,
1100+
"Statement is not an update statement: " + parsedStatement.getSqlWithoutComments());
1101+
}
1102+
10811103
@Override
10821104
public long[] executeBatchUpdate(Iterable<Statement> updates) {
10831105
Preconditions.checkNotNull(updates);
@@ -1224,7 +1246,7 @@ private ApiFuture<Long> internalExecuteUpdateAsync(
12241246
update, mergeUpdateRequestOptions(mergeUpdateStatementTag(options)));
12251247
}
12261248

1227-
private ApiFuture<ResultSetStats> internalAnalyzeUpdateAsync(
1249+
private ApiFuture<ResultSet> internalAnalyzeUpdateAsync(
12281250
final ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
12291251
Preconditions.checkArgument(
12301252
update.getType() == StatementType.UPDATE, "Statement must be an update");

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
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;
4241
import com.google.spanner.v1.SpannerGrpc;
4342
import java.util.ArrayList;
4443
import java.util.Arrays;
@@ -203,7 +202,7 @@ public ApiFuture<Long> executeUpdateAsync(ParsedStatement update, UpdateOption..
203202
}
204203

205204
@Override
206-
public ApiFuture<ResultSetStats> analyzeUpdateAsync(
205+
public ApiFuture<ResultSet> analyzeUpdateAsync(
207206
ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
208207
throw SpannerExceptionFactory.newSpannerException(
209208
ErrorCode.FAILED_PRECONDITION, "Analyzing updates is not allowed for DDL batches.");

0 commit comments

Comments
 (0)