Skip to content

Commit aac20be

Browse files
feat: support DML with Returning clause in Connection API (#1978)
* feat: incorporate dml with returning clause * feat: changes * feat: change handling of AsyncResultSet. * fix: lint * doc: add comments * fix: lint * test: add tests for executeBatchUpdate * test: import fix * test: remove redundant import * test: add abort tests for dml returning * test: add pg dml returning tests * feat: change error statement * doc: add doc for dml with returning clause usage * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java Co-authored-by: Knut Olav Løite <[email protected]> * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java Co-authored-by: Knut Olav Løite <[email protected]> * fix: incorporate review comments * test: add more test cases * test: add todo * test: add separate abort tests for dml returning * fix: add try-with-resources block around ResultSet * feat: enhancement by adding a pre-check * feat: changes * test: delete unnecessary test * test: add few more tests to PG parser * feat: method doc update * test: nit fixes * feat: handle another corner case * test: add another test * clirr fixes * revert env for integration tests * remove comments * skip returning tests in emulator * fix: linting Co-authored-by: Knut Olav Løite <[email protected]>
1 parent 4840580 commit aac20be

13 files changed

+1150
-47
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,9 @@
202202
<className>com/google/cloud/spanner/DatabaseClient</className>
203203
<method>java.lang.String getDatabaseRole()</method>
204204
</difference>
205+
<difference>
206+
<differenceType>7013</differenceType>
207+
<className>com/google/cloud/spanner/connection/AbstractStatementParser</className>
208+
<method>boolean checkReturningClauseInternal(java.lang.String)</method>
209+
</difference>
205210
</differences>

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

+45-6
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public static class ParsedStatement {
140140
private final ClientSideStatementImpl clientSideStatement;
141141
private final Statement statement;
142142
private final String sqlWithoutComments;
143+
private final boolean returningClause;
143144

144145
private static ParsedStatement clientSideStatement(
145146
ClientSideStatementImpl clientSideStatement,
@@ -155,11 +156,13 @@ private static ParsedStatement ddl(Statement statement, String sqlWithoutComment
155156
private static ParsedStatement query(
156157
Statement statement, String sqlWithoutComments, QueryOptions defaultQueryOptions) {
157158
return new ParsedStatement(
158-
StatementType.QUERY, statement, sqlWithoutComments, defaultQueryOptions);
159+
StatementType.QUERY, statement, sqlWithoutComments, defaultQueryOptions, false);
159160
}
160161

161-
private static ParsedStatement update(Statement statement, String sqlWithoutComments) {
162-
return new ParsedStatement(StatementType.UPDATE, statement, sqlWithoutComments);
162+
private static ParsedStatement update(
163+
Statement statement, String sqlWithoutComments, boolean returningClause) {
164+
return new ParsedStatement(
165+
StatementType.UPDATE, statement, sqlWithoutComments, returningClause);
163166
}
164167

165168
private static ParsedStatement unknown(Statement statement, String sqlWithoutComments) {
@@ -176,23 +179,34 @@ private ParsedStatement(
176179
this.clientSideStatement = clientSideStatement;
177180
this.statement = statement;
178181
this.sqlWithoutComments = sqlWithoutComments;
182+
this.returningClause = false;
183+
}
184+
185+
private ParsedStatement(
186+
StatementType type,
187+
Statement statement,
188+
String sqlWithoutComments,
189+
boolean returningClause) {
190+
this(type, statement, sqlWithoutComments, null, returningClause);
179191
}
180192

181193
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
182-
this(type, statement, sqlWithoutComments, null);
194+
this(type, statement, sqlWithoutComments, null, false);
183195
}
184196

185197
private ParsedStatement(
186198
StatementType type,
187199
Statement statement,
188200
String sqlWithoutComments,
189-
QueryOptions defaultQueryOptions) {
201+
QueryOptions defaultQueryOptions,
202+
boolean returningClause) {
190203
Preconditions.checkNotNull(type);
191204
Preconditions.checkNotNull(statement);
192205
this.type = type;
193206
this.clientSideStatement = null;
194207
this.statement = mergeQueryOptions(statement, defaultQueryOptions);
195208
this.sqlWithoutComments = sqlWithoutComments;
209+
this.returningClause = returningClause;
196210
}
197211

198212
@Override
@@ -219,6 +233,12 @@ public StatementType getType() {
219233
return type;
220234
}
221235

236+
/** Returns whether the statement has a returning clause or not. * */
237+
@InternalApi
238+
public boolean hasReturningClause() {
239+
return this.returningClause;
240+
}
241+
222242
/**
223243
* Returns true if the statement is a query that will return a {@link
224244
* com.google.cloud.spanner.ResultSet}.
@@ -355,7 +375,7 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
355375
} else if (isQuery(sql)) {
356376
return ParsedStatement.query(statement, sql, defaultQueryOptions);
357377
} else if (isUpdateStatement(sql)) {
358-
return ParsedStatement.update(statement, sql);
378+
return ParsedStatement.update(statement, sql, checkReturningClause(sql));
359379
} else if (isDdlStatement(sql)) {
360380
return ParsedStatement.ddl(statement, sql);
361381
}
@@ -460,6 +480,10 @@ private boolean statementStartsWith(String sql, Iterable<String> checkStatements
460480
static final char SLASH = '/';
461481
static final char ASTERISK = '*';
462482
static final char DOLLAR = '$';
483+
static final char SPACE = ' ';
484+
static final char CLOSE_PARENTHESIS = ')';
485+
static final char COMMA = ',';
486+
static final char UNDERSCORE = '_';
463487

464488
/**
465489
* Removes comments from and trims the given sql statement using the dialect of this parser.
@@ -522,4 +546,19 @@ static int countOccurrencesOf(char c, String string) {
522546
}
523547
return res;
524548
}
549+
550+
/**
551+
* Checks if the given SQL string contains a Returning clause. This method is used only in case of
552+
* a DML statement.
553+
*
554+
* @param sql The sql string without comments that has to be evaluated.
555+
* @return A boolean indicating whether the sql string has a Returning clause or not.
556+
*/
557+
@InternalApi
558+
protected abstract boolean checkReturningClauseInternal(String sql);
559+
560+
@InternalApi
561+
public boolean checkReturningClause(String sql) {
562+
return checkReturningClauseInternal(sql);
563+
}
525564
}

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

+29-24
Original file line numberDiff line numberDiff line change
@@ -856,8 +856,8 @@ default RpcPriority getRPCPriority() {
856856
* state. The returned value depends on the type of statement:
857857
*
858858
* <ul>
859-
* <li>Queries will return a {@link ResultSet}
860-
* <li>DML statements will return an update count
859+
* <li>Queries and DML statements with returning clause will return a {@link ResultSet}.
860+
* <li>Simple DML statements will return an update count
861861
* <li>DDL statements will return a {@link ResultType#NO_RESULT}
862862
* <li>Connection and transaction statements (SET AUTOCOMMIT=TRUE|FALSE, SHOW AUTOCOMMIT, SET
863863
* TRANSACTION READ ONLY, etc) will return either a {@link ResultSet} or {@link
@@ -874,9 +874,9 @@ default RpcPriority getRPCPriority() {
874874
* state asynchronously. The returned value depends on the type of statement:
875875
*
876876
* <ul>
877-
* <li>Queries will return an {@link AsyncResultSet}
878-
* <li>DML statements will return an {@link ApiFuture} with an update count that is done when
879-
* the DML statement has been applied successfully, or that throws an {@link
877+
* <li>Queries and DML statements with returning clause will return an {@link AsyncResultSet}.
878+
* <li>Simple DML statements will return an {@link ApiFuture} with an update count that is done
879+
* when the DML statement has been applied successfully, or that throws an {@link
880880
* ExecutionException} if the DML statement failed.
881881
* <li>DDL statements will return an {@link ApiFuture} containing a {@link Void} that is done
882882
* when the DDL statement has been applied successfully, or that throws an {@link
@@ -894,31 +894,33 @@ default RpcPriority getRPCPriority() {
894894
AsyncStatementResult executeAsync(Statement statement);
895895

896896
/**
897-
* Executes the given statement as a query and returns the result as a {@link ResultSet}. This
898-
* method blocks and waits for a response from Spanner. If the statement does not contain a valid
899-
* query, the method will throw a {@link SpannerException}.
897+
* Executes the given statement (a query or a DML statement with returning clause) and returns the
898+
* result as a {@link ResultSet}. This method blocks and waits for a response from Spanner. If the
899+
* statement does not contain a valid query or a DML statement with returning clause, the method
900+
* will throw a {@link SpannerException}.
900901
*
901-
* @param query The query statement to execute
902+
* @param query The query statement or DML statement with returning clause to execute
902903
* @param options the options to configure the query
903-
* @return a {@link ResultSet} with the results of the query
904+
* @return a {@link ResultSet} with the results of the statement
904905
*/
905906
ResultSet executeQuery(Statement query, QueryOption... options);
906907

907908
/**
908-
* Executes the given statement asynchronously as a query and returns the result as an {@link
909-
* AsyncResultSet}. This method is guaranteed to be non-blocking. If the statement does not
910-
* contain a valid query, the method will throw a {@link SpannerException}.
909+
* Executes the given statement (a query or a DML statement with returning clause) asynchronously
910+
* and returns the result as an {@link AsyncResultSet}. This method is guaranteed to be
911+
* non-blocking. If the statement does not contain a valid query or a DML statement with returning
912+
* clause, the method will throw a {@link SpannerException}.
911913
*
912914
* <p>See {@link AsyncResultSet#setCallback(java.util.concurrent.Executor,
913915
* com.google.cloud.spanner.AsyncResultSet.ReadyCallback)} for more information on how to consume
914-
* the results of the query asynchronously.
916+
* the results of the statement asynchronously.
915917
*
916918
* <p>It is also possible to consume the returned {@link AsyncResultSet} in the same way as a
917919
* normal {@link ResultSet}, i.e. in a while-loop calling {@link AsyncResultSet#next()}.
918920
*
919-
* @param query The query statement to execute
921+
* @param query The query statement or DML statement with returning clause to execute
920922
* @param options the options to configure the query
921-
* @return an {@link AsyncResultSet} with the results of the query
923+
* @return an {@link AsyncResultSet} with the results of the statement
922924
*/
923925
AsyncResultSet executeQueryAsync(Statement query, QueryOption... options);
924926

@@ -951,8 +953,8 @@ default RpcPriority getRPCPriority() {
951953
ResultSet analyzeQuery(Statement query, QueryAnalyzeMode queryMode);
952954

953955
/**
954-
* Executes the given statement as a DML statement. If the statement does not contain a valid DML
955-
* statement, the method will throw a {@link SpannerException}.
956+
* Executes the given statement as a simple DML statement. If the statement does not contain a
957+
* valid DML statement, the method will throw a {@link SpannerException}.
956958
*
957959
* @param update The update statement to execute
958960
* @return the number of records that were inserted/updated/deleted by this statement
@@ -972,8 +974,9 @@ default ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeM
972974
}
973975

974976
/**
975-
* Executes the given statement asynchronously as a DML statement. If the statement does not
976-
* contain a valid DML statement, the method will throw a {@link SpannerException}.
977+
* Executes the given statement asynchronously as a simple DML statement. If the statement does
978+
* not contain a simple DML statement, the method will throw a {@link SpannerException}. A DML
979+
* statement with returning clause will throw a {@link SpannerException}.
977980
*
978981
* <p>This method is guaranteed to be non-blocking.
979982
*
@@ -984,8 +987,9 @@ default ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeM
984987
ApiFuture<Long> executeUpdateAsync(Statement update);
985988

986989
/**
987-
* Executes a list of DML statements in a single request. The statements will be executed in order
988-
* and the semantics is the same as if each statement is executed by {@link
990+
* Executes a list of DML statements (can be simple DML statements or DML statements with
991+
* returning clause) in a single request. The statements will be executed in order and the
992+
* semantics is the same as if each statement is executed by {@link
989993
* Connection#executeUpdate(Statement)} in a loop. This method returns an array of long integers,
990994
* each representing the number of rows modified by each statement.
991995
*
@@ -1006,8 +1010,9 @@ default ResultSetStats analyzeUpdate(Statement update, QueryAnalyzeMode analyzeM
10061010
long[] executeBatchUpdate(Iterable<Statement> updates);
10071011

10081012
/**
1009-
* Executes a list of DML statements in a single request. The statements will be executed in order
1010-
* and the semantics is the same as if each statement is executed by {@link
1013+
* Executes a list of DML statements (can be simple DML statements or DML statements with
1014+
* returning clause) in a single request. The statements will be executed in order and the
1015+
* semantics is the same as if each statement is executed by {@link
10111016
* Connection#executeUpdate(Statement)} in a loop. This method returns an {@link ApiFuture} that
10121017
* contains an array of long integers, each representing the number of rows modified by each
10131018
* statement.

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

+59-7
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,9 @@ public StatementResult execute(Statement statement) {
853853
case QUERY:
854854
return StatementResultImpl.of(internalExecuteQuery(parsedStatement, AnalyzeMode.NONE));
855855
case UPDATE:
856+
if (parsedStatement.hasReturningClause()) {
857+
return StatementResultImpl.of(internalExecuteQuery(parsedStatement, AnalyzeMode.NONE));
858+
}
856859
return StatementResultImpl.of(get(internalExecuteUpdateAsync(parsedStatement)));
857860
case DDL:
858861
get(executeDdlAsync(parsedStatement));
@@ -881,6 +884,10 @@ public AsyncStatementResult executeAsync(Statement statement) {
881884
return AsyncStatementResultImpl.of(
882885
internalExecuteQueryAsync(parsedStatement, AnalyzeMode.NONE));
883886
case UPDATE:
887+
if (parsedStatement.hasReturningClause()) {
888+
return AsyncStatementResultImpl.of(
889+
internalExecuteQueryAsync(parsedStatement, AnalyzeMode.NONE));
890+
}
884891
return AsyncStatementResultImpl.of(internalExecuteUpdateAsync(parsedStatement));
885892
case DDL:
886893
return AsyncStatementResultImpl.noResult(executeDdlAsync(parsedStatement));
@@ -918,7 +925,7 @@ private ResultSet parseAndExecuteQuery(
918925
Preconditions.checkNotNull(analyzeMode);
919926
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
920927
ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions);
921-
if (parsedStatement.isQuery()) {
928+
if (parsedStatement.isQuery() || parsedStatement.isUpdate()) {
922929
switch (parsedStatement.getType()) {
923930
case CLIENT_SIDE:
924931
return parsedStatement
@@ -928,22 +935,36 @@ private ResultSet parseAndExecuteQuery(
928935
case QUERY:
929936
return internalExecuteQuery(parsedStatement, analyzeMode, options);
930937
case UPDATE:
938+
if (parsedStatement.hasReturningClause()) {
939+
// Cannot execute DML statement with returning clause in read-only mode or in
940+
// READ_ONLY_TRANSACTION transaction mode.
941+
if (this.isReadOnly()
942+
|| (this.isInTransaction()
943+
&& this.getTransactionMode() == TransactionMode.READ_ONLY_TRANSACTION)) {
944+
throw SpannerExceptionFactory.newSpannerException(
945+
ErrorCode.FAILED_PRECONDITION,
946+
"DML statement with returning clause cannot be executed in read-only mode: "
947+
+ parsedStatement.getSqlWithoutComments());
948+
}
949+
return internalExecuteQuery(parsedStatement, analyzeMode, options);
950+
}
931951
case DDL:
932952
case UNKNOWN:
933953
default:
934954
}
935955
}
936956
throw SpannerExceptionFactory.newSpannerException(
937957
ErrorCode.INVALID_ARGUMENT,
938-
"Statement is not a query: " + parsedStatement.getSqlWithoutComments());
958+
"Statement is not a query or DML with returning clause: "
959+
+ parsedStatement.getSqlWithoutComments());
939960
}
940961

941962
private AsyncResultSet parseAndExecuteQueryAsync(
942963
Statement query, AnalyzeMode analyzeMode, QueryOption... options) {
943964
Preconditions.checkNotNull(query);
944965
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
945966
ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions);
946-
if (parsedStatement.isQuery()) {
967+
if (parsedStatement.isQuery() || parsedStatement.isUpdate()) {
947968
switch (parsedStatement.getType()) {
948969
case CLIENT_SIDE:
949970
return ResultSets.toAsyncResultSet(
@@ -956,14 +977,28 @@ private AsyncResultSet parseAndExecuteQueryAsync(
956977
case QUERY:
957978
return internalExecuteQueryAsync(parsedStatement, analyzeMode, options);
958979
case UPDATE:
980+
if (parsedStatement.hasReturningClause()) {
981+
// Cannot execute DML statement with returning clause in read-only mode or in
982+
// READ_ONLY_TRANSACTION transaction mode.
983+
if (this.isReadOnly()
984+
|| (this.isInTransaction()
985+
&& this.getTransactionMode() == TransactionMode.READ_ONLY_TRANSACTION)) {
986+
throw SpannerExceptionFactory.newSpannerException(
987+
ErrorCode.FAILED_PRECONDITION,
988+
"DML statement with returning clause cannot be executed in read-only mode: "
989+
+ parsedStatement.getSqlWithoutComments());
990+
}
991+
return internalExecuteQueryAsync(parsedStatement, analyzeMode, options);
992+
}
959993
case DDL:
960994
case UNKNOWN:
961995
default:
962996
}
963997
}
964998
throw SpannerExceptionFactory.newSpannerException(
965999
ErrorCode.INVALID_ARGUMENT,
966-
"Statement is not a query: " + parsedStatement.getSqlWithoutComments());
1000+
"Statement is not a query or DML with returning clause: "
1001+
+ parsedStatement.getSqlWithoutComments());
9671002
}
9681003

9691004
@Override
@@ -974,6 +1009,13 @@ public long executeUpdate(Statement update) {
9741009
if (parsedStatement.isUpdate()) {
9751010
switch (parsedStatement.getType()) {
9761011
case UPDATE:
1012+
if (parsedStatement.hasReturningClause()) {
1013+
throw SpannerExceptionFactory.newSpannerException(
1014+
ErrorCode.FAILED_PRECONDITION,
1015+
"DML statement with returning clause cannot be executed using executeUpdate: "
1016+
+ parsedStatement.getSqlWithoutComments()
1017+
+ ". Please use executeQuery instead.");
1018+
}
9771019
return get(internalExecuteUpdateAsync(parsedStatement));
9781020
case CLIENT_SIDE:
9791021
case QUERY:
@@ -995,6 +1037,13 @@ public ApiFuture<Long> executeUpdateAsync(Statement update) {
9951037
if (parsedStatement.isUpdate()) {
9961038
switch (parsedStatement.getType()) {
9971039
case UPDATE:
1040+
if (parsedStatement.hasReturningClause()) {
1041+
throw SpannerExceptionFactory.newSpannerException(
1042+
ErrorCode.FAILED_PRECONDITION,
1043+
"DML statement with returning clause cannot be executed using executeUpdateAsync: "
1044+
+ parsedStatement.getSqlWithoutComments()
1045+
+ ". Please use executeQueryAsync instead.");
1046+
}
9981047
return internalExecuteUpdateAsync(parsedStatement);
9991048
case CLIENT_SIDE:
10001049
case QUERY:
@@ -1141,8 +1190,9 @@ private ResultSet internalExecuteQuery(
11411190
final QueryOption... options) {
11421191
Preconditions.checkArgument(
11431192
statement.getType() == StatementType.QUERY
1144-
|| (statement.getType() == StatementType.UPDATE && analyzeMode != AnalyzeMode.NONE),
1145-
"Statement must either be a query or a DML mode with analyzeMode!=NONE");
1193+
|| (statement.getType() == StatementType.UPDATE
1194+
&& (analyzeMode != AnalyzeMode.NONE || statement.hasReturningClause())),
1195+
"Statement must either be a query or a DML mode with analyzeMode!=NONE or returning clause");
11461196
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
11471197
return get(
11481198
transaction.executeQueryAsync(
@@ -1154,7 +1204,9 @@ private AsyncResultSet internalExecuteQueryAsync(
11541204
final AnalyzeMode analyzeMode,
11551205
final QueryOption... options) {
11561206
Preconditions.checkArgument(
1157-
statement.getType() == StatementType.QUERY, "Statement must be a query");
1207+
(statement.getType() == StatementType.QUERY)
1208+
|| (statement.getType() == StatementType.UPDATE && statement.hasReturningClause()),
1209+
"Statement must be a query or DML with returning clause.");
11581210
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
11591211
return ResultSets.toAsyncResultSet(
11601212
transaction.executeQueryAsync(

0 commit comments

Comments
 (0)