Skip to content

Commit 29c3051

Browse files
committed
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
1 parent 4cf5261 commit 29c3051

File tree

9 files changed

+2433
-772
lines changed

9 files changed

+2433
-772
lines changed

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

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,28 @@
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;
35+
import com.google.common.base.Suppliers;
3436
import com.google.common.cache.Cache;
3537
import com.google.common.cache.CacheBuilder;
3638
import com.google.common.cache.CacheStats;
3739
import com.google.common.cache.Weigher;
3840
import com.google.common.collect.ImmutableMap;
3941
import com.google.common.collect.ImmutableSet;
4042
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
43+
import java.nio.CharBuffer;
4144
import java.util.Collection;
4245
import java.util.Collections;
4346
import java.util.HashMap;
4447
import java.util.Map;
4548
import java.util.Objects;
4649
import java.util.Set;
4750
import java.util.concurrent.Callable;
51+
import java.util.function.Supplier;
4852
import java.util.logging.Level;
4953
import java.util.logging.Logger;
5054
import javax.annotation.Nullable;
@@ -179,24 +183,24 @@ public static class ParsedStatement {
179183
private final StatementType type;
180184
private final ClientSideStatementImpl clientSideStatement;
181185
private final Statement statement;
182-
private final String sqlWithoutComments;
183-
private final boolean returningClause;
186+
private final Supplier<String> sqlWithoutComments;
187+
private final Supplier<Boolean> returningClause;
184188
private final ReadQueryUpdateTransactionOption[] optionsFromHints;
185189

186190
private static ParsedStatement clientSideStatement(
187191
ClientSideStatementImpl clientSideStatement,
188192
Statement statement,
189-
String sqlWithoutComments) {
193+
Supplier<String> sqlWithoutComments) {
190194
return new ParsedStatement(clientSideStatement, statement, sqlWithoutComments);
191195
}
192196

193-
private static ParsedStatement ddl(Statement statement, String sqlWithoutComments) {
197+
private static ParsedStatement ddl(Statement statement, Supplier<String> sqlWithoutComments) {
194198
return new ParsedStatement(StatementType.DDL, statement, sqlWithoutComments);
195199
}
196200

197201
private static ParsedStatement query(
198202
Statement statement,
199-
String sqlWithoutComments,
203+
Supplier<String> sqlWithoutComments,
200204
QueryOptions defaultQueryOptions,
201205
ReadQueryUpdateTransactionOption[] optionsFromHints) {
202206
return new ParsedStatement(
@@ -205,57 +209,66 @@ private static ParsedStatement query(
205209
statement,
206210
sqlWithoutComments,
207211
defaultQueryOptions,
208-
false,
212+
Suppliers.ofInstance(false),
209213
optionsFromHints);
210214
}
211215

212216
private static ParsedStatement update(
213217
Statement statement,
214-
String sqlWithoutComments,
215-
boolean returningClause,
218+
Supplier<String> sqlWithoutComments,
219+
Supplier<Boolean> returningClause,
216220
ReadQueryUpdateTransactionOption[] optionsFromHints) {
217221
return new ParsedStatement(
218222
StatementType.UPDATE, statement, sqlWithoutComments, returningClause, optionsFromHints);
219223
}
220224

221-
private static ParsedStatement unknown(Statement statement, String sqlWithoutComments) {
225+
private static ParsedStatement unknown(
226+
Statement statement, Supplier<String> sqlWithoutComments) {
222227
return new ParsedStatement(StatementType.UNKNOWN, statement, sqlWithoutComments);
223228
}
224229

225230
private ParsedStatement(
226231
ClientSideStatementImpl clientSideStatement,
227232
Statement statement,
228-
String sqlWithoutComments) {
233+
Supplier<String> sqlWithoutComments) {
229234
Preconditions.checkNotNull(clientSideStatement);
230235
Preconditions.checkNotNull(statement);
231236
this.type = StatementType.CLIENT_SIDE;
232237
this.clientSideStatement = clientSideStatement;
233238
this.statement = statement;
234-
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
235-
this.returningClause = false;
239+
this.sqlWithoutComments = sqlWithoutComments;
240+
this.returningClause = Suppliers.ofInstance(false);
236241
this.optionsFromHints = EMPTY_OPTIONS;
237242
}
238243

239244
private ParsedStatement(
240245
StatementType type,
241246
Statement statement,
242-
String sqlWithoutComments,
243-
boolean returningClause,
247+
Supplier<String> sqlWithoutComments,
248+
Supplier<Boolean> returningClause,
244249
ReadQueryUpdateTransactionOption[] optionsFromHints) {
245250
this(type, null, statement, sqlWithoutComments, null, returningClause, optionsFromHints);
246251
}
247252

248-
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
249-
this(type, null, statement, sqlWithoutComments, null, false, EMPTY_OPTIONS);
253+
private ParsedStatement(
254+
StatementType type, Statement statement, Supplier<String> sqlWithoutComments) {
255+
this(
256+
type,
257+
null,
258+
statement,
259+
sqlWithoutComments,
260+
null,
261+
Suppliers.ofInstance(false),
262+
EMPTY_OPTIONS);
250263
}
251264

252265
private ParsedStatement(
253266
StatementType type,
254267
ClientSideStatementImpl clientSideStatement,
255268
Statement statement,
256-
String sqlWithoutComments,
269+
Supplier<String> sqlWithoutComments,
257270
QueryOptions defaultQueryOptions,
258-
boolean returningClause,
271+
Supplier<Boolean> returningClause,
259272
ReadQueryUpdateTransactionOption[] optionsFromHints) {
260273
Preconditions.checkNotNull(type);
261274
this.type = type;
@@ -315,7 +328,7 @@ public StatementType getType() {
315328
/** @return whether the statement has a returning clause or not. */
316329
@InternalApi
317330
public boolean hasReturningClause() {
318-
return this.returningClause;
331+
return this.returningClause.get();
319332
}
320333

321334
@InternalApi
@@ -413,7 +426,7 @@ Statement mergeQueryOptions(Statement statement, QueryOptions defaultQueryOption
413426
/** @return the SQL statement with all comments removed from the SQL string. */
414427
@InternalApi
415428
public String getSqlWithoutComments() {
416-
return sqlWithoutComments;
429+
return sqlWithoutComments.get();
417430
}
418431

419432
ClientSideStatement getClientSideStatement() {
@@ -464,7 +477,7 @@ private static boolean isRecordStatementCacheStats() {
464477
// We do length*2 because Java uses 2 bytes for each char.
465478
.weigher(
466479
(Weigher<String, ParsedStatement>)
467-
(key, value) -> 2 * key.length() + 2 * value.sqlWithoutComments.length())
480+
(key, value) -> 2 * key.length() + 2 * value.statement.getSql().length())
468481
.concurrencyLevel(Runtime.getRuntime().availableProcessors());
469482
if (isRecordStatementCacheStats()) {
470483
cacheBuilder.recordStats();
@@ -511,28 +524,56 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
511524
return parsedStatement.copy(statement, defaultQueryOptions);
512525
}
513526

514-
private ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
515-
StatementHintParser statementHintParser =
516-
new StatementHintParser(getDialect(), statement.getSql());
527+
ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
528+
String sql = statement.getSql();
529+
StatementHintParser statementHintParser = new StatementHintParser(getDialect(), sql);
517530
ReadQueryUpdateTransactionOption[] optionsFromHints = EMPTY_OPTIONS;
518531
if (statementHintParser.hasStatementHints()
519532
&& !statementHintParser.getClientSideStatementHints().isEmpty()) {
520533
statement =
521534
statement.toBuilder().replace(statementHintParser.getSqlWithoutClientSideHints()).build();
522535
optionsFromHints = convertHintsToOptions(statementHintParser.getClientSideStatementHints());
523536
}
524-
String sql = removeCommentsAndTrim(statement.getSql());
525-
ClientSideStatementImpl client = parseClientSideStatement(sql);
537+
// Create a supplier that will actually remove all comments and hints from the SQL string to be
538+
// backwards compatible with anything that really needs the SQL string without comments.
539+
Supplier<String> sqlWithoutCommentsSupplier =
540+
Suppliers.memoize(() -> removeCommentsAndTrim(sql));
541+
542+
// Get rid of any spaces/comments at the start of the string.
543+
SimpleParser simpleParser = new SimpleParser(getDialect(), sql);
544+
simpleParser.skipWhitespaces();
545+
// Create a wrapper around the SQL string from the point after the first whitespace.
546+
CharBuffer charBuffer = CharBuffer.wrap(sql, simpleParser.getPos(), sql.length());
547+
ClientSideStatementImpl client = parseClientSideStatement(charBuffer);
548+
526549
if (client != null) {
527-
return ParsedStatement.clientSideStatement(client, statement, sql);
528-
} else if (isQuery(sql)) {
529-
return ParsedStatement.query(statement, sql, defaultQueryOptions, optionsFromHints);
530-
} else if (isUpdateStatement(sql)) {
531-
return ParsedStatement.update(statement, sql, checkReturningClause(sql), optionsFromHints);
532-
} else if (isDdlStatement(sql)) {
533-
return ParsedStatement.ddl(statement, sql);
550+
return ParsedStatement.clientSideStatement(client, statement, sqlWithoutCommentsSupplier);
551+
} else {
552+
Result keywordResult = simpleParser.eatNextKeyword();
553+
if (keywordResult.isValid()) {
554+
String keyword = keywordResult.getValue().toUpperCase();
555+
if (keywordResult.isInParenthesis()) {
556+
if (SELECT_STATEMENTS_ALLOWING_PRECEDING_BRACKETS.contains(keyword)) {
557+
return ParsedStatement.query(
558+
statement, sqlWithoutCommentsSupplier, defaultQueryOptions, optionsFromHints);
559+
}
560+
} else {
561+
if (selectStatements.contains(keyword)) {
562+
return ParsedStatement.query(
563+
statement, sqlWithoutCommentsSupplier, defaultQueryOptions, optionsFromHints);
564+
} else if (dmlStatements.contains(keyword)) {
565+
return ParsedStatement.update(
566+
statement,
567+
sqlWithoutCommentsSupplier,
568+
Suppliers.memoize(() -> checkReturningClause(sqlWithoutCommentsSupplier.get())),
569+
optionsFromHints);
570+
} else if (ddlStatements.contains(keyword)) {
571+
return ParsedStatement.ddl(statement, sqlWithoutCommentsSupplier);
572+
}
573+
}
574+
}
534575
}
535-
return ParsedStatement.unknown(statement, sql);
576+
return ParsedStatement.unknown(statement, sqlWithoutCommentsSupplier);
536577
}
537578

538579
/**
@@ -546,7 +587,7 @@ private ParsedStatement internalParse(Statement statement, QueryOptions defaultQ
546587
* statement.
547588
*/
548589
@VisibleForTesting
549-
ClientSideStatementImpl parseClientSideStatement(String sql) {
590+
ClientSideStatementImpl parseClientSideStatement(CharSequence sql) {
550591
for (ClientSideStatementImpl css : statements) {
551592
if (css.matches(sql)) {
552593
return css;

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

Lines changed: 1 addition & 1 deletion
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
}

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

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,31 @@
2828

2929
/** A very simple token-based parser for extracting relevant information from SQL strings. */
3030
class SimpleParser {
31+
static final char DOLLAR = '$';
32+
static final char BACKSLASH = '\\';
33+
3134
/**
3235
* An immutable result from a parse action indicating whether the parse action was successful, and
3336
* if so, what the value was.
3437
*/
3538
static class Result {
36-
static final Result NOT_FOUND = new Result(null);
39+
static final Result NOT_FOUND = new Result(null, false);
3740

3841
static Result found(String value) {
39-
return new Result(Preconditions.checkNotNull(value));
42+
return new Result(Preconditions.checkNotNull(value), false);
43+
}
44+
45+
static Result found(String value, boolean inParenthesis) {
46+
return new Result(Preconditions.checkNotNull(value), inParenthesis);
4047
}
4148

4249
private final String value;
4350

44-
private Result(String value) {
51+
private final boolean inParenthesis;
52+
53+
private Result(String value, boolean inParenthesis) {
4554
this.value = value;
55+
this.inParenthesis = inParenthesis;
4656
}
4757

4858
@Override
@@ -55,7 +65,8 @@ public boolean equals(Object o) {
5565
if (!(o instanceof Result)) {
5666
return false;
5767
}
58-
return Objects.equals(this.value, ((Result) o).value);
68+
return Objects.equals(this.value, ((Result) o).value)
69+
&& Objects.equals(this.inParenthesis, ((Result) o).inParenthesis);
5970
}
6071

6172
@Override
@@ -73,6 +84,10 @@ boolean isValid() {
7384
String getValue() {
7485
return this.value;
7586
}
87+
88+
boolean isInParenthesis() {
89+
return this.inParenthesis;
90+
}
7691
}
7792

7893
// TODO: Replace this with a direct reference to the dialect, and move the isXYZSupported methods
@@ -81,6 +96,8 @@ String getValue() {
8196

8297
private final String sql;
8398

99+
private final int length;
100+
84101
private final boolean treatHintCommentsAsTokens;
85102

86103
private int pos;
@@ -100,6 +117,7 @@ String getValue() {
100117
!(treatHintCommentsAsTokens && dialect != Dialect.POSTGRESQL),
101118
"treatHintCommentsAsTokens can only be enabled for PostgreSQL");
102119
this.sql = sql;
120+
this.length = sql.length();
103121
this.pos = pos;
104122
this.statementParser = AbstractStatementParser.getInstance(dialect);
105123
this.treatHintCommentsAsTokens = treatHintCommentsAsTokens;
@@ -117,12 +135,50 @@ int getPos() {
117135
return this.pos;
118136
}
119137

138+
void skipHint() {
139+
// We don't need to do anything special for PostgreSQL, as hints in PostgreSQL are inside
140+
// comments.
141+
if (getDialect() == Dialect.GOOGLE_STANDARD_SQL && eatTokens('@', '{')) {
142+
while (pos < length && !eatToken('}')) {
143+
pos += statementParser.skip(sql, pos, /*result=*/ null);
144+
}
145+
}
146+
}
147+
148+
Result eatNextKeyword() {
149+
skipHint();
150+
boolean inParenthesis = false;
151+
while (pos < length && eatToken('(')) {
152+
inParenthesis = true;
153+
}
154+
return eatKeyword(inParenthesis);
155+
}
156+
120157
/** Returns true if this parser has more tokens. Advances the position to the first next token. */
121158
boolean hasMoreTokens() {
122159
skipWhitespaces();
123160
return pos < sql.length();
124161
}
125162

163+
/** Eats and returns the keyword at the current position. */
164+
Result eatKeyword() {
165+
return eatKeyword(false);
166+
}
167+
168+
Result eatKeyword(boolean inParenthesis) {
169+
if (!hasMoreTokens()) {
170+
return Result.NOT_FOUND;
171+
}
172+
if (!Character.isLetter(sql.charAt(pos))) {
173+
return Result.NOT_FOUND;
174+
}
175+
int startPos = pos;
176+
while (pos < length && Character.isLetter(sql.charAt(pos))) {
177+
pos++;
178+
}
179+
return Result.found(sql.substring(startPos, pos), inParenthesis);
180+
}
181+
126182
/**
127183
* Eats and returns the identifier at the current position. This implementation does not support
128184
* quoted identifiers.

0 commit comments

Comments
 (0)