27
27
import com .google .cloud .spanner .SpannerExceptionFactory ;
28
28
import com .google .cloud .spanner .Statement ;
29
29
import com .google .cloud .spanner .connection .AbstractBaseUnitOfWork .InterceptorsUsage ;
30
+ import com .google .cloud .spanner .connection .SimpleParser .Result ;
30
31
import com .google .cloud .spanner .connection .StatementResult .ClientSideStatementType ;
31
32
import com .google .cloud .spanner .connection .UnitOfWork .CallType ;
32
33
import com .google .common .annotations .VisibleForTesting ;
33
34
import com .google .common .base .Preconditions ;
34
35
import com .google .common .base .Splitter ;
36
+ import com .google .common .base .Suppliers ;
35
37
import com .google .common .cache .Cache ;
36
38
import com .google .common .cache .CacheBuilder ;
37
39
import com .google .common .cache .CacheStats ;
38
40
import com .google .common .cache .Weigher ;
39
41
import com .google .common .collect .ImmutableMap ;
40
42
import com .google .common .collect .ImmutableSet ;
41
43
import com .google .spanner .v1 .ExecuteSqlRequest .QueryOptions ;
44
+ import java .nio .CharBuffer ;
42
45
import java .util .Collection ;
43
46
import java .util .Collections ;
44
47
import java .util .HashMap ;
47
50
import java .util .Objects ;
48
51
import java .util .Set ;
49
52
import java .util .concurrent .Callable ;
53
+ import java .util .function .Supplier ;
50
54
import java .util .logging .Level ;
51
55
import java .util .logging .Logger ;
52
56
import javax .annotation .Nullable ;
@@ -181,24 +185,24 @@ public static class ParsedStatement {
181
185
private final StatementType type ;
182
186
private final ClientSideStatementImpl clientSideStatement ;
183
187
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 ;
186
190
private final ReadQueryUpdateTransactionOption [] optionsFromHints ;
187
191
188
192
private static ParsedStatement clientSideStatement (
189
193
ClientSideStatementImpl clientSideStatement ,
190
194
Statement statement ,
191
- String sqlWithoutComments ) {
195
+ Supplier < String > sqlWithoutComments ) {
192
196
return new ParsedStatement (clientSideStatement , statement , sqlWithoutComments );
193
197
}
194
198
195
- private static ParsedStatement ddl (Statement statement , String sqlWithoutComments ) {
199
+ private static ParsedStatement ddl (Statement statement , Supplier < String > sqlWithoutComments ) {
196
200
return new ParsedStatement (StatementType .DDL , statement , sqlWithoutComments );
197
201
}
198
202
199
203
private static ParsedStatement query (
200
204
Statement statement ,
201
- String sqlWithoutComments ,
205
+ Supplier < String > sqlWithoutComments ,
202
206
QueryOptions defaultQueryOptions ,
203
207
ReadQueryUpdateTransactionOption [] optionsFromHints ) {
204
208
return new ParsedStatement (
@@ -207,57 +211,66 @@ private static ParsedStatement query(
207
211
statement ,
208
212
sqlWithoutComments ,
209
213
defaultQueryOptions ,
210
- false ,
214
+ Suppliers . ofInstance ( false ) ,
211
215
optionsFromHints );
212
216
}
213
217
214
218
private static ParsedStatement update (
215
219
Statement statement ,
216
- String sqlWithoutComments ,
217
- boolean returningClause ,
220
+ Supplier < String > sqlWithoutComments ,
221
+ Supplier < Boolean > returningClause ,
218
222
ReadQueryUpdateTransactionOption [] optionsFromHints ) {
219
223
return new ParsedStatement (
220
224
StatementType .UPDATE , statement , sqlWithoutComments , returningClause , optionsFromHints );
221
225
}
222
226
223
- private static ParsedStatement unknown (Statement statement , String sqlWithoutComments ) {
227
+ private static ParsedStatement unknown (
228
+ Statement statement , Supplier <String > sqlWithoutComments ) {
224
229
return new ParsedStatement (StatementType .UNKNOWN , statement , sqlWithoutComments );
225
230
}
226
231
227
232
private ParsedStatement (
228
233
ClientSideStatementImpl clientSideStatement ,
229
234
Statement statement ,
230
- String sqlWithoutComments ) {
235
+ Supplier < String > sqlWithoutComments ) {
231
236
Preconditions .checkNotNull (clientSideStatement );
232
237
Preconditions .checkNotNull (statement );
233
238
this .type = StatementType .CLIENT_SIDE ;
234
239
this .clientSideStatement = clientSideStatement ;
235
240
this .statement = statement ;
236
- this .sqlWithoutComments = Preconditions . checkNotNull ( sqlWithoutComments ) ;
237
- this .returningClause = false ;
241
+ this .sqlWithoutComments = sqlWithoutComments ;
242
+ this .returningClause = Suppliers . ofInstance ( false ) ;
238
243
this .optionsFromHints = EMPTY_OPTIONS ;
239
244
}
240
245
241
246
private ParsedStatement (
242
247
StatementType type ,
243
248
Statement statement ,
244
- String sqlWithoutComments ,
245
- boolean returningClause ,
249
+ Supplier < String > sqlWithoutComments ,
250
+ Supplier < Boolean > returningClause ,
246
251
ReadQueryUpdateTransactionOption [] optionsFromHints ) {
247
252
this (type , null , statement , sqlWithoutComments , null , returningClause , optionsFromHints );
248
253
}
249
254
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 );
252
265
}
253
266
254
267
private ParsedStatement (
255
268
StatementType type ,
256
269
ClientSideStatementImpl clientSideStatement ,
257
270
Statement statement ,
258
- String sqlWithoutComments ,
271
+ Supplier < String > sqlWithoutComments ,
259
272
QueryOptions defaultQueryOptions ,
260
- boolean returningClause ,
273
+ Supplier < Boolean > returningClause ,
261
274
ReadQueryUpdateTransactionOption [] optionsFromHints ) {
262
275
Preconditions .checkNotNull (type );
263
276
this .type = type ;
@@ -317,7 +330,7 @@ public StatementType getType() {
317
330
/** @return whether the statement has a returning clause or not. */
318
331
@ InternalApi
319
332
public boolean hasReturningClause () {
320
- return this .returningClause ;
333
+ return this .returningClause . get () ;
321
334
}
322
335
323
336
@ InternalApi
@@ -415,7 +428,7 @@ Statement mergeQueryOptions(Statement statement, QueryOptions defaultQueryOption
415
428
/** @return the SQL statement with all comments removed from the SQL string. */
416
429
@ InternalApi
417
430
public String getSqlWithoutComments () {
418
- return sqlWithoutComments ;
431
+ return sqlWithoutComments . get () ;
419
432
}
420
433
421
434
ClientSideStatement getClientSideStatement () {
@@ -466,7 +479,7 @@ private static boolean isRecordStatementCacheStats() {
466
479
// We do length*2 because Java uses 2 bytes for each char.
467
480
.weigher (
468
481
(Weigher <String , ParsedStatement >)
469
- (key , value ) -> 2 * key .length () + 2 * value .sqlWithoutComments .length ())
482
+ (key , value ) -> 2 * key .length () + 2 * value .statement . getSql () .length ())
470
483
.concurrencyLevel (Runtime .getRuntime ().availableProcessors ());
471
484
if (isRecordStatementCacheStats ()) {
472
485
cacheBuilder .recordStats ();
@@ -514,32 +527,61 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
514
527
}
515
528
516
529
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 );
519
532
ReadQueryUpdateTransactionOption [] optionsFromHints = EMPTY_OPTIONS ;
520
533
if (statementHintParser .hasStatementHints ()
521
534
&& !statementHintParser .getClientSideStatementHints ().isEmpty ()) {
522
535
statement =
523
536
statement .toBuilder ().replace (statementHintParser .getSqlWithoutClientSideHints ()).build ();
524
537
optionsFromHints = convertHintsToOptions (statementHintParser .getClientSideStatementHints ());
525
538
}
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
+
529
551
if (client != null ) {
530
- return ParsedStatement .clientSideStatement (client , statement , sql );
552
+ return ParsedStatement .clientSideStatement (client , statement , sqlWithoutCommentsSupplier );
531
553
} 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
+ }
540
581
}
541
582
}
542
- return ParsedStatement .unknown (statement , sql );
583
+ // Fallthrough: Return an unknown statement.
584
+ return ParsedStatement .unknown (statement , sqlWithoutCommentsSupplier );
543
585
}
544
586
545
587
/**
@@ -553,7 +595,7 @@ ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOpti
553
595
* statement.
554
596
*/
555
597
@ VisibleForTesting
556
- ClientSideStatementImpl parseClientSideStatement (String sql ) {
598
+ ClientSideStatementImpl parseClientSideStatement (CharSequence sql ) {
557
599
for (ClientSideStatementImpl css : statements ) {
558
600
if (css .matches (sql )) {
559
601
return css ;
@@ -570,8 +612,10 @@ ClientSideStatementImpl parseClientSideStatement(String sql) {
570
612
* @param sql The statement to check (without any comments).
571
613
* @return <code>true</code> if the statement is a DDL statement (i.e. starts with 'CREATE',
572
614
* 'ALTER' or 'DROP').
615
+ * @deprecated Use {@link #parse(Statement)} instead
573
616
*/
574
617
@ InternalApi
618
+ @ Deprecated
575
619
public boolean isDdlStatement (String sql ) {
576
620
return statementStartsWith (sql , ddlStatements );
577
621
}
@@ -583,8 +627,10 @@ public boolean isDdlStatement(String sql) {
583
627
*
584
628
* @param sql The statement to check (without any comments).
585
629
* @return <code>true</code> if the statement is a SELECT statement (i.e. starts with 'SELECT').
630
+ * @deprecated Use {@link #parse(Statement)} instead
586
631
*/
587
632
@ InternalApi
633
+ @ Deprecated
588
634
public boolean isQuery (String sql ) {
589
635
// Skip any query hints at the beginning of the query.
590
636
// 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) {
607
653
* @param sql The statement to check (without any comments).
608
654
* @return <code>true</code> if the statement is a DML update statement (i.e. starts with
609
655
* 'INSERT', 'UPDATE' or 'DELETE').
656
+ * @deprecated Use {@link #parse(Statement)} instead
610
657
*/
611
658
@ InternalApi
659
+ @ Deprecated
612
660
public boolean isUpdateStatement (String sql ) {
613
661
// Skip any query hints at the beginning of the query.
614
662
if (sql .startsWith ("@" )) {
0 commit comments