Skip to content

Commit d358cb9

Browse files
perf: qualify statements without removing comments (googleapis#3810)
* perf: qualify statements without removing comments Determine the type of statement without first removing all comments and hints. This prevents the creation of new strings and stepping through the entire SQL string for each statement that is not found in the statement cache. Benchmark Mode Cnt Score Error Units StatementParserBenchmark.isQueryTest thrpt 5 547904.501 ± 1970.170 ops/s StatementParserBenchmark.longDmlTest thrpt 5 114806.782 ± 826.881 ops/s StatementParserBenchmark.longQueryTest thrpt 5 112666.992 ± 700.783 ops/s * chore: generate libraries at Fri Apr 11 05:56:44 UTC 2025 --------- Co-authored-by: cloud-java-bot <[email protected]>
1 parent c5a2045 commit d358cb9

File tree

8 files changed

+2363
-774
lines changed

8 files changed

+2363
-774
lines changed

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

+85-37
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,21 @@
2727
import com.google.cloud.spanner.SpannerExceptionFactory;
2828
import com.google.cloud.spanner.Statement;
2929
import com.google.cloud.spanner.connection.AbstractBaseUnitOfWork.InterceptorsUsage;
30+
import com.google.cloud.spanner.connection.SimpleParser.Result;
3031
import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
3132
import com.google.cloud.spanner.connection.UnitOfWork.CallType;
3233
import com.google.common.annotations.VisibleForTesting;
3334
import com.google.common.base.Preconditions;
3435
import com.google.common.base.Splitter;
36+
import com.google.common.base.Suppliers;
3537
import com.google.common.cache.Cache;
3638
import com.google.common.cache.CacheBuilder;
3739
import com.google.common.cache.CacheStats;
3840
import com.google.common.cache.Weigher;
3941
import com.google.common.collect.ImmutableMap;
4042
import com.google.common.collect.ImmutableSet;
4143
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
44+
import java.nio.CharBuffer;
4245
import java.util.Collection;
4346
import java.util.Collections;
4447
import java.util.HashMap;
@@ -47,6 +50,7 @@
4750
import java.util.Objects;
4851
import java.util.Set;
4952
import java.util.concurrent.Callable;
53+
import java.util.function.Supplier;
5054
import java.util.logging.Level;
5155
import java.util.logging.Logger;
5256
import javax.annotation.Nullable;
@@ -181,24 +185,24 @@ public static class ParsedStatement {
181185
private final StatementType type;
182186
private final ClientSideStatementImpl clientSideStatement;
183187
private final Statement statement;
184-
private final String sqlWithoutComments;
185-
private final boolean returningClause;
188+
private final Supplier<String> sqlWithoutComments;
189+
private final Supplier<Boolean> returningClause;
186190
private final ReadQueryUpdateTransactionOption[] optionsFromHints;
187191

188192
private static ParsedStatement clientSideStatement(
189193
ClientSideStatementImpl clientSideStatement,
190194
Statement statement,
191-
String sqlWithoutComments) {
195+
Supplier<String> sqlWithoutComments) {
192196
return new ParsedStatement(clientSideStatement, statement, sqlWithoutComments);
193197
}
194198

195-
private static ParsedStatement ddl(Statement statement, String sqlWithoutComments) {
199+
private static ParsedStatement ddl(Statement statement, Supplier<String> sqlWithoutComments) {
196200
return new ParsedStatement(StatementType.DDL, statement, sqlWithoutComments);
197201
}
198202

199203
private static ParsedStatement query(
200204
Statement statement,
201-
String sqlWithoutComments,
205+
Supplier<String> sqlWithoutComments,
202206
QueryOptions defaultQueryOptions,
203207
ReadQueryUpdateTransactionOption[] optionsFromHints) {
204208
return new ParsedStatement(
@@ -207,57 +211,66 @@ private static ParsedStatement query(
207211
statement,
208212
sqlWithoutComments,
209213
defaultQueryOptions,
210-
false,
214+
Suppliers.ofInstance(false),
211215
optionsFromHints);
212216
}
213217

214218
private static ParsedStatement update(
215219
Statement statement,
216-
String sqlWithoutComments,
217-
boolean returningClause,
220+
Supplier<String> sqlWithoutComments,
221+
Supplier<Boolean> returningClause,
218222
ReadQueryUpdateTransactionOption[] optionsFromHints) {
219223
return new ParsedStatement(
220224
StatementType.UPDATE, statement, sqlWithoutComments, returningClause, optionsFromHints);
221225
}
222226

223-
private static ParsedStatement unknown(Statement statement, String sqlWithoutComments) {
227+
private static ParsedStatement unknown(
228+
Statement statement, Supplier<String> sqlWithoutComments) {
224229
return new ParsedStatement(StatementType.UNKNOWN, statement, sqlWithoutComments);
225230
}
226231

227232
private ParsedStatement(
228233
ClientSideStatementImpl clientSideStatement,
229234
Statement statement,
230-
String sqlWithoutComments) {
235+
Supplier<String> sqlWithoutComments) {
231236
Preconditions.checkNotNull(clientSideStatement);
232237
Preconditions.checkNotNull(statement);
233238
this.type = StatementType.CLIENT_SIDE;
234239
this.clientSideStatement = clientSideStatement;
235240
this.statement = statement;
236-
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
237-
this.returningClause = false;
241+
this.sqlWithoutComments = sqlWithoutComments;
242+
this.returningClause = Suppliers.ofInstance(false);
238243
this.optionsFromHints = EMPTY_OPTIONS;
239244
}
240245

241246
private ParsedStatement(
242247
StatementType type,
243248
Statement statement,
244-
String sqlWithoutComments,
245-
boolean returningClause,
249+
Supplier<String> sqlWithoutComments,
250+
Supplier<Boolean> returningClause,
246251
ReadQueryUpdateTransactionOption[] optionsFromHints) {
247252
this(type, null, statement, sqlWithoutComments, null, returningClause, optionsFromHints);
248253
}
249254

250-
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
251-
this(type, null, statement, sqlWithoutComments, null, false, EMPTY_OPTIONS);
255+
private ParsedStatement(
256+
StatementType type, Statement statement, Supplier<String> sqlWithoutComments) {
257+
this(
258+
type,
259+
null,
260+
statement,
261+
sqlWithoutComments,
262+
null,
263+
Suppliers.ofInstance(false),
264+
EMPTY_OPTIONS);
252265
}
253266

254267
private ParsedStatement(
255268
StatementType type,
256269
ClientSideStatementImpl clientSideStatement,
257270
Statement statement,
258-
String sqlWithoutComments,
271+
Supplier<String> sqlWithoutComments,
259272
QueryOptions defaultQueryOptions,
260-
boolean returningClause,
273+
Supplier<Boolean> returningClause,
261274
ReadQueryUpdateTransactionOption[] optionsFromHints) {
262275
Preconditions.checkNotNull(type);
263276
this.type = type;
@@ -317,7 +330,7 @@ public StatementType getType() {
317330
/** @return whether the statement has a returning clause or not. */
318331
@InternalApi
319332
public boolean hasReturningClause() {
320-
return this.returningClause;
333+
return this.returningClause.get();
321334
}
322335

323336
@InternalApi
@@ -415,7 +428,7 @@ Statement mergeQueryOptions(Statement statement, QueryOptions defaultQueryOption
415428
/** @return the SQL statement with all comments removed from the SQL string. */
416429
@InternalApi
417430
public String getSqlWithoutComments() {
418-
return sqlWithoutComments;
431+
return sqlWithoutComments.get();
419432
}
420433

421434
ClientSideStatement getClientSideStatement() {
@@ -466,7 +479,7 @@ private static boolean isRecordStatementCacheStats() {
466479
// We do length*2 because Java uses 2 bytes for each char.
467480
.weigher(
468481
(Weigher<String, ParsedStatement>)
469-
(key, value) -> 2 * key.length() + 2 * value.sqlWithoutComments.length())
482+
(key, value) -> 2 * key.length() + 2 * value.statement.getSql().length())
470483
.concurrencyLevel(Runtime.getRuntime().availableProcessors());
471484
if (isRecordStatementCacheStats()) {
472485
cacheBuilder.recordStats();
@@ -514,32 +527,61 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
514527
}
515528

516529
ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
517-
StatementHintParser statementHintParser =
518-
new StatementHintParser(getDialect(), statement.getSql());
530+
String sql = statement.getSql();
531+
StatementHintParser statementHintParser = new StatementHintParser(getDialect(), sql);
519532
ReadQueryUpdateTransactionOption[] optionsFromHints = EMPTY_OPTIONS;
520533
if (statementHintParser.hasStatementHints()
521534
&& !statementHintParser.getClientSideStatementHints().isEmpty()) {
522535
statement =
523536
statement.toBuilder().replace(statementHintParser.getSqlWithoutClientSideHints()).build();
524537
optionsFromHints = convertHintsToOptions(statementHintParser.getClientSideStatementHints());
525538
}
526-
// TODO: Qualify statements without removing comments first.
527-
String sql = removeCommentsAndTrim(statement.getSql());
528-
ClientSideStatementImpl client = parseClientSideStatement(sql);
539+
// Create a supplier that will actually remove all comments and hints from the SQL string to be
540+
// backwards compatible with anything that really needs the SQL string without comments.
541+
Supplier<String> sqlWithoutCommentsSupplier =
542+
Suppliers.memoize(() -> removeCommentsAndTrim(sql));
543+
544+
// Get rid of any spaces/comments at the start of the string.
545+
SimpleParser simpleParser = new SimpleParser(getDialect(), sql);
546+
simpleParser.skipWhitespaces();
547+
// Create a wrapper around the SQL string from the point after the first whitespace.
548+
CharBuffer charBuffer = CharBuffer.wrap(sql, simpleParser.getPos(), sql.length());
549+
ClientSideStatementImpl client = parseClientSideStatement(charBuffer);
550+
529551
if (client != null) {
530-
return ParsedStatement.clientSideStatement(client, statement, sql);
552+
return ParsedStatement.clientSideStatement(client, statement, sqlWithoutCommentsSupplier);
531553
} else {
532-
String sqlWithoutHints =
533-
!sql.isEmpty() && sql.charAt(0) == '@' ? removeStatementHint(sql) : sql;
534-
if (isQuery(sqlWithoutHints)) {
535-
return ParsedStatement.query(statement, sql, defaultQueryOptions, optionsFromHints);
536-
} else if (isUpdateStatement(sqlWithoutHints)) {
537-
return ParsedStatement.update(statement, sql, checkReturningClause(sql), optionsFromHints);
538-
} else if (isDdlStatement(sqlWithoutHints)) {
539-
return ParsedStatement.ddl(statement, sql);
554+
// Find the first keyword in the SQL statement.
555+
Result keywordResult = simpleParser.eatNextKeyword();
556+
if (keywordResult.isValid()) {
557+
// Determine the statement type based on the first keyword.
558+
String keyword = keywordResult.getValue().toUpperCase();
559+
if (keywordResult.isInParenthesis()) {
560+
// If the first keyword is inside one or more parentheses, then only a subset of all
561+
// keywords are allowed.
562+
if (SELECT_STATEMENTS_ALLOWING_PRECEDING_BRACKETS.contains(keyword)) {
563+
return ParsedStatement.query(
564+
statement, sqlWithoutCommentsSupplier, defaultQueryOptions, optionsFromHints);
565+
}
566+
} else {
567+
if (selectStatements.contains(keyword)) {
568+
return ParsedStatement.query(
569+
statement, sqlWithoutCommentsSupplier, defaultQueryOptions, optionsFromHints);
570+
} else if (dmlStatements.contains(keyword)) {
571+
return ParsedStatement.update(
572+
statement,
573+
sqlWithoutCommentsSupplier,
574+
// TODO: Make the returning clause check work without removing comments
575+
Suppliers.memoize(() -> checkReturningClause(sqlWithoutCommentsSupplier.get())),
576+
optionsFromHints);
577+
} else if (ddlStatements.contains(keyword)) {
578+
return ParsedStatement.ddl(statement, sqlWithoutCommentsSupplier);
579+
}
580+
}
540581
}
541582
}
542-
return ParsedStatement.unknown(statement, sql);
583+
// Fallthrough: Return an unknown statement.
584+
return ParsedStatement.unknown(statement, sqlWithoutCommentsSupplier);
543585
}
544586

545587
/**
@@ -553,7 +595,7 @@ ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOpti
553595
* statement.
554596
*/
555597
@VisibleForTesting
556-
ClientSideStatementImpl parseClientSideStatement(String sql) {
598+
ClientSideStatementImpl parseClientSideStatement(CharSequence sql) {
557599
for (ClientSideStatementImpl css : statements) {
558600
if (css.matches(sql)) {
559601
return css;
@@ -570,8 +612,10 @@ ClientSideStatementImpl parseClientSideStatement(String sql) {
570612
* @param sql The statement to check (without any comments).
571613
* @return <code>true</code> if the statement is a DDL statement (i.e. starts with 'CREATE',
572614
* 'ALTER' or 'DROP').
615+
* @deprecated Use {@link #parse(Statement)} instead
573616
*/
574617
@InternalApi
618+
@Deprecated
575619
public boolean isDdlStatement(String sql) {
576620
return statementStartsWith(sql, ddlStatements);
577621
}
@@ -583,8 +627,10 @@ public boolean isDdlStatement(String sql) {
583627
*
584628
* @param sql The statement to check (without any comments).
585629
* @return <code>true</code> if the statement is a SELECT statement (i.e. starts with 'SELECT').
630+
* @deprecated Use {@link #parse(Statement)} instead
586631
*/
587632
@InternalApi
633+
@Deprecated
588634
public boolean isQuery(String sql) {
589635
// Skip any query hints at the beginning of the query.
590636
// We only do this if we actually know that it starts with a hint to prevent unnecessary
@@ -607,8 +653,10 @@ public boolean isQuery(String sql) {
607653
* @param sql The statement to check (without any comments).
608654
* @return <code>true</code> if the statement is a DML update statement (i.e. starts with
609655
* 'INSERT', 'UPDATE' or 'DELETE').
656+
* @deprecated Use {@link #parse(Statement)} instead
610657
*/
611658
@InternalApi
659+
@Deprecated
612660
public boolean isUpdateStatement(String sql) {
613661
// Skip any query hints at the beginning of the query.
614662
if (sql.startsWith("@")) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public ClientSideStatementType getStatementType() {
193193
return statementType;
194194
}
195195

196-
boolean matches(String statement) {
196+
boolean matches(CharSequence statement) {
197197
Preconditions.checkState(pattern != null, "This statement has not been compiled");
198198
return pattern.matcher(statement).matches();
199199
}

0 commit comments

Comments
 (0)