2727import com .google .cloud .spanner .SpannerExceptionFactory ;
2828import com .google .cloud .spanner .Statement ;
2929import com .google .cloud .spanner .connection .AbstractBaseUnitOfWork .InterceptorsUsage ;
30+ import com .google .cloud .spanner .connection .SimpleParser .Result ;
3031import com .google .cloud .spanner .connection .StatementResult .ClientSideStatementType ;
3132import com .google .cloud .spanner .connection .UnitOfWork .CallType ;
3233import com .google .common .annotations .VisibleForTesting ;
3334import com .google .common .base .Preconditions ;
3435import com .google .common .base .Splitter ;
36+ import com .google .common .base .Suppliers ;
3537import com .google .common .cache .Cache ;
3638import com .google .common .cache .CacheBuilder ;
3739import com .google .common .cache .CacheStats ;
3840import com .google .common .cache .Weigher ;
3941import com .google .common .collect .ImmutableMap ;
4042import com .google .common .collect .ImmutableSet ;
4143import com .google .spanner .v1 .ExecuteSqlRequest .QueryOptions ;
44+ import java .nio .CharBuffer ;
4245import java .util .Collection ;
4346import java .util .Collections ;
4447import java .util .HashMap ;
4750import java .util .Objects ;
4851import java .util .Set ;
4952import java .util .concurrent .Callable ;
53+ import java .util .function .Supplier ;
5054import java .util .logging .Level ;
5155import java .util .logging .Logger ;
5256import 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 ("@" )) {
0 commit comments