Skip to content

Commit 48828df

Browse files
authored
feat: support client-side hints for tags and priority (#3005)
Supports including a hint in the SQL string to set a statement tag or an RPC priority for a single SQL statement in the Connection API. This makes it easier to use these features from frameworks and tools that only support SQL statements. Replaces #2978
1 parent 5419491 commit 48828df

11 files changed

+1303
-124
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ public String getSql() {
178178
return sql;
179179
}
180180

181+
/** Returns a copy of this statement with the SQL string replaced by the given SQL string. */
182+
public Statement withReplacedSql(String sql) {
183+
return new Statement(sql, this.parameters, this.queryOptions);
184+
}
185+
181186
/** Returns the {@link QueryOptions} that will be used with this {@link Statement}. */
182187
public QueryOptions getQueryOptions() {
183188
return queryOptions;

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

+79-62
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package com.google.cloud.spanner.connection;
1818

19+
import static com.google.cloud.spanner.connection.SimpleParser.isValidIdentifierChar;
20+
import static com.google.cloud.spanner.connection.StatementHintParser.convertHintsToOptions;
21+
1922
import com.google.api.core.InternalApi;
2023
import com.google.cloud.spanner.Dialect;
2124
import com.google.cloud.spanner.ErrorCode;
25+
import com.google.cloud.spanner.Options.ReadQueryUpdateTransactionOption;
2226
import com.google.cloud.spanner.SpannerException;
2327
import com.google.cloud.spanner.SpannerExceptionFactory;
2428
import com.google.cloud.spanner.Statement;
@@ -169,6 +173,7 @@ public static class ParsedStatement {
169173
private final Statement statement;
170174
private final String sqlWithoutComments;
171175
private final boolean returningClause;
176+
private final ReadQueryUpdateTransactionOption[] optionsFromHints;
172177

173178
private static ParsedStatement clientSideStatement(
174179
ClientSideStatementImpl clientSideStatement,
@@ -182,15 +187,27 @@ private static ParsedStatement ddl(Statement statement, String sqlWithoutComment
182187
}
183188

184189
private static ParsedStatement query(
185-
Statement statement, String sqlWithoutComments, QueryOptions defaultQueryOptions) {
190+
Statement statement,
191+
String sqlWithoutComments,
192+
QueryOptions defaultQueryOptions,
193+
ReadQueryUpdateTransactionOption[] optionsFromHints) {
186194
return new ParsedStatement(
187-
StatementType.QUERY, null, statement, sqlWithoutComments, defaultQueryOptions, false);
195+
StatementType.QUERY,
196+
null,
197+
statement,
198+
sqlWithoutComments,
199+
defaultQueryOptions,
200+
false,
201+
optionsFromHints);
188202
}
189203

190204
private static ParsedStatement update(
191-
Statement statement, String sqlWithoutComments, boolean returningClause) {
205+
Statement statement,
206+
String sqlWithoutComments,
207+
boolean returningClause,
208+
ReadQueryUpdateTransactionOption[] optionsFromHints) {
192209
return new ParsedStatement(
193-
StatementType.UPDATE, statement, sqlWithoutComments, returningClause);
210+
StatementType.UPDATE, statement, sqlWithoutComments, returningClause, optionsFromHints);
194211
}
195212

196213
private static ParsedStatement unknown(Statement statement, String sqlWithoutComments) {
@@ -208,18 +225,20 @@ private ParsedStatement(
208225
this.statement = statement;
209226
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
210227
this.returningClause = false;
228+
this.optionsFromHints = EMPTY_OPTIONS;
211229
}
212230

213231
private ParsedStatement(
214232
StatementType type,
215233
Statement statement,
216234
String sqlWithoutComments,
217-
boolean returningClause) {
218-
this(type, null, statement, sqlWithoutComments, null, returningClause);
235+
boolean returningClause,
236+
ReadQueryUpdateTransactionOption[] optionsFromHints) {
237+
this(type, null, statement, sqlWithoutComments, null, returningClause, optionsFromHints);
219238
}
220239

221240
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
222-
this(type, null, statement, sqlWithoutComments, null, false);
241+
this(type, null, statement, sqlWithoutComments, null, false, EMPTY_OPTIONS);
223242
}
224243

225244
private ParsedStatement(
@@ -228,33 +247,37 @@ private ParsedStatement(
228247
Statement statement,
229248
String sqlWithoutComments,
230249
QueryOptions defaultQueryOptions,
231-
boolean returningClause) {
250+
boolean returningClause,
251+
ReadQueryUpdateTransactionOption[] optionsFromHints) {
232252
Preconditions.checkNotNull(type);
233253
this.type = type;
234254
this.clientSideStatement = clientSideStatement;
235255
this.statement = statement == null ? null : mergeQueryOptions(statement, defaultQueryOptions);
236256
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
237257
this.returningClause = returningClause;
258+
this.optionsFromHints = optionsFromHints;
238259
}
239260

240261
private ParsedStatement copy(Statement statement, QueryOptions defaultQueryOptions) {
241262
return new ParsedStatement(
242263
this.type,
243264
this.clientSideStatement,
244-
statement,
265+
statement.withReplacedSql(this.statement.getSql()),
245266
this.sqlWithoutComments,
246267
defaultQueryOptions,
247-
this.returningClause);
268+
this.returningClause,
269+
this.optionsFromHints);
248270
}
249271

250272
private ParsedStatement forCache() {
251273
return new ParsedStatement(
252274
this.type,
253275
this.clientSideStatement,
254-
null,
276+
Statement.of(this.statement.getSql()),
255277
this.sqlWithoutComments,
256278
null,
257-
this.returningClause);
279+
this.returningClause,
280+
this.optionsFromHints);
258281
}
259282

260283
@Override
@@ -287,6 +310,11 @@ public boolean hasReturningClause() {
287310
return this.returningClause;
288311
}
289312

313+
@InternalApi
314+
public ReadQueryUpdateTransactionOption[] getOptionsFromHints() {
315+
return this.optionsFromHints;
316+
}
317+
290318
/**
291319
* @return true if the statement is a query that will return a {@link
292320
* com.google.cloud.spanner.ResultSet}.
@@ -480,14 +508,23 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
480508
}
481509

482510
private ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
511+
StatementHintParser statementHintParser =
512+
new StatementHintParser(getDialect(), statement.getSql());
513+
ReadQueryUpdateTransactionOption[] optionsFromHints = EMPTY_OPTIONS;
514+
if (statementHintParser.hasStatementHints()
515+
&& !statementHintParser.getClientSideStatementHints().isEmpty()) {
516+
statement =
517+
statement.toBuilder().replace(statementHintParser.getSqlWithoutClientSideHints()).build();
518+
optionsFromHints = convertHintsToOptions(statementHintParser.getClientSideStatementHints());
519+
}
483520
String sql = removeCommentsAndTrim(statement.getSql());
484521
ClientSideStatementImpl client = parseClientSideStatement(sql);
485522
if (client != null) {
486523
return ParsedStatement.clientSideStatement(client, statement, sql);
487524
} else if (isQuery(sql)) {
488-
return ParsedStatement.query(statement, sql, defaultQueryOptions);
525+
return ParsedStatement.query(statement, sql, defaultQueryOptions, optionsFromHints);
489526
} else if (isUpdateStatement(sql)) {
490-
return ParsedStatement.update(statement, sql, checkReturningClause(sql));
527+
return ParsedStatement.update(statement, sql, checkReturningClause(sql), optionsFromHints);
491528
} else if (isDdlStatement(sql)) {
492529
return ParsedStatement.ddl(statement, sql);
493530
}
@@ -621,6 +658,10 @@ public String removeCommentsAndTrim(String sql) {
621658
/** Removes any statement hints at the beginning of the statement. */
622659
abstract String removeStatementHint(String sql);
623660

661+
@VisibleForTesting
662+
static final ReadQueryUpdateTransactionOption[] EMPTY_OPTIONS =
663+
new ReadQueryUpdateTransactionOption[0];
664+
624665
/** Parameter information with positional parameters translated to named parameters. */
625666
@InternalApi
626667
public static class ParametersInfo {
@@ -697,9 +738,10 @@ public boolean checkReturningClause(String sql) {
697738
return checkReturningClauseInternal(sql);
698739
}
699740

741+
abstract Dialect getDialect();
742+
700743
/**
701-
* <<<<<<< HEAD Returns true if this dialect supports nested comments. ======= <<<<<<< HEAD
702-
* Returns true if this dialect supports nested comments. >>>>>>> main
744+
* Returns true if this dialect supports nested comments.
703745
*
704746
* <ul>
705747
* <li>This method should return false for dialects that consider this to be a valid comment:
@@ -757,18 +799,6 @@ public boolean checkReturningClause(String sql) {
757799
/** Returns the query parameter prefix that should be used for this dialect. */
758800
abstract String getQueryParameterPrefix();
759801

760-
/**
761-
* Returns true for characters that can be used as the first character in unquoted identifiers.
762-
*/
763-
boolean isValidIdentifierFirstChar(char c) {
764-
return Character.isLetter(c) || c == UNDERSCORE;
765-
}
766-
767-
/** Returns true for characters that can be used in unquoted identifiers. */
768-
boolean isValidIdentifierChar(char c) {
769-
return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == DOLLAR;
770-
}
771-
772802
/** Reads a dollar-quoted string literal from position index in the given sql string. */
773803
String parseDollarQuotedString(String sql, int index) {
774804
// Look ahead to the next dollar sign (if any). Everything in between is the quote tag.
@@ -812,9 +842,9 @@ int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
812842
} else if (currentChar == HYPHEN
813843
&& sql.length() > (currentIndex + 1)
814844
&& sql.charAt(currentIndex + 1) == HYPHEN) {
815-
return skipSingleLineComment(sql, currentIndex, result);
845+
return skipSingleLineComment(sql, /* prefixLength = */ 2, currentIndex, result);
816846
} else if (currentChar == DASH && supportsHashSingleLineComments()) {
817-
return skipSingleLineComment(sql, currentIndex, result);
847+
return skipSingleLineComment(sql, /* prefixLength = */ 1, currentIndex, result);
818848
} else if (currentChar == SLASH
819849
&& sql.length() > (currentIndex + 1)
820850
&& sql.charAt(currentIndex + 1) == ASTERISK) {
@@ -826,44 +856,31 @@ int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
826856
}
827857

828858
/** Skips a single-line comment from startIndex and adds it to result if result is not null. */
829-
static int skipSingleLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
830-
int endIndex = sql.indexOf('\n', startIndex + 2);
831-
if (endIndex == -1) {
832-
endIndex = sql.length();
833-
} else {
834-
// Include the newline character.
835-
endIndex++;
859+
int skipSingleLineComment(
860+
String sql, int prefixLength, int startIndex, @Nullable StringBuilder result) {
861+
return skipSingleLineComment(getDialect(), sql, prefixLength, startIndex, result);
862+
}
863+
864+
static int skipSingleLineComment(
865+
Dialect dialect,
866+
String sql,
867+
int prefixLength,
868+
int startIndex,
869+
@Nullable StringBuilder result) {
870+
SimpleParser simpleParser = new SimpleParser(dialect, sql, startIndex, false);
871+
if (simpleParser.skipSingleLineComment(prefixLength)) {
872+
appendIfNotNull(result, sql.substring(startIndex, simpleParser.getPos()));
836873
}
837-
appendIfNotNull(result, sql.substring(startIndex, endIndex));
838-
return endIndex;
874+
return simpleParser.getPos();
839875
}
840876

841877
/** Skips a multi-line comment from startIndex and adds it to result if result is not null. */
842878
int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
843-
// Current position is start + '/*'.length().
844-
int pos = startIndex + 2;
845-
// PostgreSQL allows comments to be nested. That is, the following is allowed:
846-
// '/* test /* inner comment */ still a comment */'
847-
int level = 1;
848-
while (pos < sql.length()) {
849-
if (supportsNestedComments()
850-
&& sql.charAt(pos) == SLASH
851-
&& sql.length() > (pos + 1)
852-
&& sql.charAt(pos + 1) == ASTERISK) {
853-
level++;
854-
}
855-
if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) {
856-
level--;
857-
if (level == 0) {
858-
pos += 2;
859-
appendIfNotNull(result, sql.substring(startIndex, pos));
860-
return pos;
861-
}
862-
}
863-
pos++;
879+
SimpleParser simpleParser = new SimpleParser(getDialect(), sql, startIndex, false);
880+
if (simpleParser.skipMultiLineComment()) {
881+
appendIfNotNull(result, sql.substring(startIndex, simpleParser.getPos()));
864882
}
865-
appendIfNotNull(result, sql.substring(startIndex));
866-
return sql.length();
883+
return simpleParser.getPos();
867884
}
868885

869886
/** Skips a quoted string from startIndex. */

0 commit comments

Comments
 (0)