2525import com .google .cloud .spanner .connection .StatementResult .ClientSideStatementType ;
2626import com .google .common .annotations .VisibleForTesting ;
2727import com .google .common .base .Preconditions ;
28+ import com .google .common .cache .Cache ;
29+ import com .google .common .cache .CacheBuilder ;
30+ import com .google .common .cache .CacheStats ;
31+ import com .google .common .cache .Weigher ;
2832import com .google .common .collect .ImmutableMap ;
2933import com .google .common .collect .ImmutableSet ;
3034import com .google .spanner .v1 .ExecuteSqlRequest .QueryOptions ;
@@ -59,6 +63,13 @@ public abstract class AbstractStatementParser {
5963 Dialect .POSTGRESQL ,
6064 PostgreSQLStatementParser .class );
6165
66+ @ VisibleForTesting
67+ static void resetParsers () {
68+ synchronized (lock ) {
69+ INSTANCES .clear ();
70+ }
71+ }
72+
6273 /**
6374 * Get an instance of {@link AbstractStatementParser} for the specified dialect.
6475 *
@@ -171,7 +182,7 @@ private static ParsedStatement ddl(Statement statement, String sqlWithoutComment
171182 private static ParsedStatement query (
172183 Statement statement , String sqlWithoutComments , QueryOptions defaultQueryOptions ) {
173184 return new ParsedStatement (
174- StatementType .QUERY , statement , sqlWithoutComments , defaultQueryOptions , false );
185+ StatementType .QUERY , null , statement , sqlWithoutComments , defaultQueryOptions , false );
175186 }
176187
177188 private static ParsedStatement update (
@@ -193,7 +204,7 @@ private ParsedStatement(
193204 this .type = StatementType .CLIENT_SIDE ;
194205 this .clientSideStatement = clientSideStatement ;
195206 this .statement = statement ;
196- this .sqlWithoutComments = sqlWithoutComments ;
207+ this .sqlWithoutComments = Preconditions . checkNotNull ( sqlWithoutComments ) ;
197208 this .returningClause = false ;
198209 }
199210
@@ -202,28 +213,48 @@ private ParsedStatement(
202213 Statement statement ,
203214 String sqlWithoutComments ,
204215 boolean returningClause ) {
205- this (type , statement , sqlWithoutComments , null , returningClause );
216+ this (type , null , statement , sqlWithoutComments , null , returningClause );
206217 }
207218
208219 private ParsedStatement (StatementType type , Statement statement , String sqlWithoutComments ) {
209- this (type , statement , sqlWithoutComments , null , false );
220+ this (type , null , statement , sqlWithoutComments , null , false );
210221 }
211222
212223 private ParsedStatement (
213224 StatementType type ,
225+ ClientSideStatementImpl clientSideStatement ,
214226 Statement statement ,
215227 String sqlWithoutComments ,
216228 QueryOptions defaultQueryOptions ,
217229 boolean returningClause ) {
218230 Preconditions .checkNotNull (type );
219- Preconditions .checkNotNull (statement );
220231 this .type = type ;
221- this .clientSideStatement = null ;
222- this .statement = mergeQueryOptions (statement , defaultQueryOptions );
223- this .sqlWithoutComments = sqlWithoutComments ;
232+ this .clientSideStatement = clientSideStatement ;
233+ this .statement = statement == null ? null : mergeQueryOptions (statement , defaultQueryOptions );
234+ this .sqlWithoutComments = Preconditions . checkNotNull ( sqlWithoutComments ) ;
224235 this .returningClause = returningClause ;
225236 }
226237
238+ private ParsedStatement copy (Statement statement , QueryOptions defaultQueryOptions ) {
239+ return new ParsedStatement (
240+ this .type ,
241+ this .clientSideStatement ,
242+ statement ,
243+ this .sqlWithoutComments ,
244+ defaultQueryOptions ,
245+ this .returningClause );
246+ }
247+
248+ private ParsedStatement forCache () {
249+ return new ParsedStatement (
250+ this .type ,
251+ this .clientSideStatement ,
252+ null ,
253+ this .sqlWithoutComments ,
254+ null ,
255+ this .returningClause );
256+ }
257+
227258 @ Override
228259 public int hashCode () {
229260 return Objects .hash (
@@ -361,8 +392,58 @@ ClientSideStatement getClientSideStatement() {
361392 static final Set <String > dmlStatements = ImmutableSet .of ("INSERT" , "UPDATE" , "DELETE" );
362393 private final Set <ClientSideStatementImpl > statements ;
363394
395+ /** The default maximum size of the statement cache in Mb. */
396+ public static final int DEFAULT_MAX_STATEMENT_CACHE_SIZE_MB = 5 ;
397+
398+ private static int getMaxStatementCacheSize () {
399+ String stringValue = System .getProperty ("spanner.statement_cache_size_mb" );
400+ if (stringValue == null ) {
401+ return DEFAULT_MAX_STATEMENT_CACHE_SIZE_MB ;
402+ }
403+ int value = 0 ;
404+ try {
405+ value = Integer .parseInt (stringValue );
406+ } catch (NumberFormatException ignore ) {
407+ }
408+ return Math .max (value , 0 );
409+ }
410+
411+ private static boolean isRecordStatementCacheStats () {
412+ return "true"
413+ .equalsIgnoreCase (System .getProperty ("spanner.record_statement_cache_stats" , "false" ));
414+ }
415+
416+ /**
417+ * Cache for parsed statements. This prevents statements that are executed multiple times by the
418+ * application to be parsed over and over again. The default maximum size is 5Mb.
419+ */
420+ private final Cache <String , ParsedStatement > statementCache ;
421+
364422 AbstractStatementParser (Set <ClientSideStatementImpl > statements ) {
365423 this .statements = Collections .unmodifiableSet (statements );
424+ int maxCacheSize = getMaxStatementCacheSize ();
425+ if (maxCacheSize > 0 ) {
426+ CacheBuilder <String , ParsedStatement > cacheBuilder =
427+ CacheBuilder .newBuilder ()
428+ // Set the max size to (approx) 5MB (by default).
429+ .maximumWeight (maxCacheSize * 1024L * 1024L )
430+ // We do length*2 because Java uses 2 bytes for each char.
431+ .weigher (
432+ (Weigher <String , ParsedStatement >)
433+ (key , value ) -> 2 * key .length () + 2 * value .sqlWithoutComments .length ())
434+ .concurrencyLevel (Runtime .getRuntime ().availableProcessors ());
435+ if (isRecordStatementCacheStats ()) {
436+ cacheBuilder .recordStats ();
437+ }
438+ this .statementCache = cacheBuilder .build ();
439+ } else {
440+ this .statementCache = null ;
441+ }
442+ }
443+
444+ @ VisibleForTesting
445+ CacheStats getStatementCacheStats () {
446+ return statementCache == null ? null : statementCache .stats ();
366447 }
367448
368449 @ VisibleForTesting
@@ -383,6 +464,20 @@ public ParsedStatement parse(Statement statement) {
383464 }
384465
385466 ParsedStatement parse (Statement statement , QueryOptions defaultQueryOptions ) {
467+ if (statementCache == null ) {
468+ return internalParse (statement , defaultQueryOptions );
469+ }
470+
471+ ParsedStatement parsedStatement = statementCache .getIfPresent (statement .getSql ());
472+ if (parsedStatement == null ) {
473+ parsedStatement = internalParse (statement , null );
474+ statementCache .put (statement .getSql (), parsedStatement .forCache ());
475+ return parsedStatement ;
476+ }
477+ return parsedStatement .copy (statement , defaultQueryOptions );
478+ }
479+
480+ private ParsedStatement internalParse (Statement statement , QueryOptions defaultQueryOptions ) {
386481 String sql = removeCommentsAndTrim (statement .getSql ());
387482 ClientSideStatementImpl client = parseClientSideStatement (sql );
388483 if (client != null ) {
0 commit comments