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 ;
35+ import com .google .common .base .Suppliers ;
3436import com .google .common .cache .Cache ;
3537import com .google .common .cache .CacheBuilder ;
3638import com .google .common .cache .CacheStats ;
3739import com .google .common .cache .Weigher ;
3840import com .google .common .collect .ImmutableMap ;
3941import com .google .common .collect .ImmutableSet ;
4042import com .google .spanner .v1 .ExecuteSqlRequest .QueryOptions ;
43+ import java .nio .CharBuffer ;
4144import java .util .Collection ;
4245import java .util .Collections ;
4346import java .util .HashMap ;
4447import java .util .Map ;
4548import java .util .Objects ;
4649import java .util .Set ;
4750import java .util .concurrent .Callable ;
51+ import java .util .function .Supplier ;
4852import java .util .logging .Level ;
4953import java .util .logging .Logger ;
5054import 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,62 @@ 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+ // Find the first keyword in the SQL statement.
553+ Result keywordResult = simpleParser .eatNextKeyword ();
554+ if (keywordResult .isValid ()) {
555+ // Determine the statement type based on the first keyword.
556+ String keyword = keywordResult .getValue ().toUpperCase ();
557+ if (keywordResult .isInParenthesis ()) {
558+ // If the first keyword is inside one or more parentheses, then only a subset of all
559+ // keywords are allowed.
560+ if (SELECT_STATEMENTS_ALLOWING_PRECEDING_BRACKETS .contains (keyword )) {
561+ return ParsedStatement .query (
562+ statement , sqlWithoutCommentsSupplier , defaultQueryOptions , optionsFromHints );
563+ }
564+ } else {
565+ if (selectStatements .contains (keyword )) {
566+ return ParsedStatement .query (
567+ statement , sqlWithoutCommentsSupplier , defaultQueryOptions , optionsFromHints );
568+ } else if (dmlStatements .contains (keyword )) {
569+ return ParsedStatement .update (
570+ statement ,
571+ sqlWithoutCommentsSupplier ,
572+ // TODO: Make the returning clause check work without removing comments
573+ Suppliers .memoize (() -> checkReturningClause (sqlWithoutCommentsSupplier .get ())),
574+ optionsFromHints );
575+ } else if (ddlStatements .contains (keyword )) {
576+ return ParsedStatement .ddl (statement , sqlWithoutCommentsSupplier );
577+ }
578+ }
579+ }
534580 }
535- return ParsedStatement .unknown (statement , sql );
581+ // Fallthrough: Return an unknown statement.
582+ return ParsedStatement .unknown (statement , sqlWithoutCommentsSupplier );
536583 }
537584
538585 /**
@@ -546,7 +593,7 @@ private ParsedStatement internalParse(Statement statement, QueryOptions defaultQ
546593 * statement.
547594 */
548595 @ VisibleForTesting
549- ClientSideStatementImpl parseClientSideStatement (String sql ) {
596+ ClientSideStatementImpl parseClientSideStatement (CharSequence sql ) {
550597 for (ClientSideStatementImpl css : statements ) {
551598 if (css .matches (sql )) {
552599 return css ;
@@ -563,8 +610,10 @@ ClientSideStatementImpl parseClientSideStatement(String sql) {
563610 * @param sql The statement to check (without any comments).
564611 * @return <code>true</code> if the statement is a DDL statement (i.e. starts with 'CREATE',
565612 * 'ALTER' or 'DROP').
613+ * @deprecated Use {@link #parse(Statement)} instead
566614 */
567615 @ InternalApi
616+ @ Deprecated
568617 public boolean isDdlStatement (String sql ) {
569618 return statementStartsWith (sql , ddlStatements );
570619 }
@@ -576,8 +625,10 @@ public boolean isDdlStatement(String sql) {
576625 *
577626 * @param sql The statement to check (without any comments).
578627 * @return <code>true</code> if the statement is a SELECT statement (i.e. starts with 'SELECT').
628+ * @deprecated Use {@link #parse(Statement)} instead
579629 */
580630 @ InternalApi
631+ @ Deprecated
581632 public boolean isQuery (String sql ) {
582633 // Skip any query hints at the beginning of the query.
583634 // We only do this if we actually know that it starts with a hint to prevent unnecessary
@@ -600,8 +651,10 @@ public boolean isQuery(String sql) {
600651 * @param sql The statement to check (without any comments).
601652 * @return <code>true</code> if the statement is a DML update statement (i.e. starts with
602653 * 'INSERT', 'UPDATE' or 'DELETE').
654+ * @deprecated Use {@link #parse(Statement)} instead
603655 */
604656 @ InternalApi
657+ @ Deprecated
605658 public boolean isUpdateStatement (String sql ) {
606659 // Skip any query hints at the beginning of the query.
607660 if (sql .startsWith ("@" )) {
0 commit comments