@@ -34,6 +34,7 @@ public class DataRemoteCachePluginTest {
3434 @ Mock TelemetryCounter mockHitCounter ;
3535 @ Mock TelemetryCounter mockMissCounter ;
3636 @ Mock TelemetryCounter mockTotalCallsCounter ;
37+ @ Mock TelemetryCounter mockMalformedHintCounter ;
3738 @ Mock ResultSet mockResult1 ;
3839 @ Mock Statement mockStatement ;
3940 @ Mock ResultSetMetaData mockMetaData ;
@@ -52,6 +53,7 @@ void setUp() throws SQLException {
5253 when (mockTelemetryFactory .createCounter ("remoteCache.cache.hit" )).thenReturn (mockHitCounter );
5354 when (mockTelemetryFactory .createCounter ("remoteCache.cache.miss" )).thenReturn (mockMissCounter );
5455 when (mockTelemetryFactory .createCounter ("remoteCache.cache.totalCalls" )).thenReturn (mockTotalCallsCounter );
56+ when (mockTelemetryFactory .createCounter ("JdbcCacheMalformedQueryHint" )).thenReturn (mockMalformedHintCounter );
5557 when (mockResult1 .getMetaData ()).thenReturn (mockMetaData );
5658 when (mockMetaData .getColumnCount ()).thenReturn (1 );
5759 when (mockMetaData .getColumnLabel (1 )).thenReturn ("fooName" );
@@ -66,36 +68,68 @@ void cleanUp() throws Exception {
6668
6769 @ Test
6870 void test_getTTLFromQueryHint () throws Exception {
69- // Null and empty query string are not cacheable
71+ // Null and empty query hint content are not cacheable
7072 assertNull (plugin .getTtlForQuery (null ));
7173 assertNull (plugin .getTtlForQuery ("" ));
7274 assertNull (plugin .getTtlForQuery (" " ));
73- // Some other query hint
74- assertNull (plugin .getTtlForQuery ("/* cacheNotEnabled */ select * from T" ));
75- // Rule set is empty. All select queries are cached with 300 seconds TTL
76- String selectQuery1 = "cachettl=300s" ;
77- String selectQuery2 = " /* CACHETTL=100s */ SELECT ID from mytable2 " ;
78- String selectQuery3 = "/*CacheTTL=35s*/select * from table3 where ID = 1 and name = 'tom'" ;
79- // Query hints that are not cacheable
80- String selectQueryNoHint = "select * from table4" ;
81- String selectQueryNoCache1 = "no cache" ;
82- String selectQueryNoCache2 = " /* NO CACHE */ SELECT count(*) FROM (select player_id from roster where id = 1 FOR UPDATE) really_long_name_alias" ;
83- String selectQueryNoCache3 = "/* cachettl=300 */ SELECT count(*) FROM (select player_id from roster where id = 1) really_long_name_alias" ;
84-
85- // Non select queries are not cacheable
86- String veryShortQuery = "BEGIN" ;
87- String insertQuery = "/* This is an insert query */ insert into mytable values (1, 2)" ;
88- String updateQuery = "/* Update query */ Update /* Another hint */ mytable set val = 1" ;
89- assertEquals (300 , plugin .getTtlForQuery (selectQuery1 ));
90- assertEquals (100 , plugin .getTtlForQuery (selectQuery2 ));
91- assertEquals (35 , plugin .getTtlForQuery (selectQuery3 ));
92- assertNull (plugin .getTtlForQuery (selectQueryNoHint ));
93- assertNull (plugin .getTtlForQuery (selectQueryNoCache1 ));
94- assertNull (plugin .getTtlForQuery (selectQueryNoCache2 ));
95- assertNull (plugin .getTtlForQuery (selectQueryNoCache3 ));
96- assertNull (plugin .getTtlForQuery (veryShortQuery ));
97- assertNull (plugin .getTtlForQuery (insertQuery ));
98- assertNull (plugin .getTtlForQuery (updateQuery ));
75+ // Valid CACHE_PARAM cases - these are the hint contents after /*+ and before */
76+ assertEquals (300 , plugin .getTtlForQuery ("CACHE_PARAM(ttl=300s)" ));
77+ assertEquals (100 , plugin .getTtlForQuery ("CACHE_PARAM(ttl=100s)" ));
78+ assertEquals (35 , plugin .getTtlForQuery ("CACHE_PARAM(ttl=35s)" ));
79+
80+ // Case insensitive
81+ assertEquals (200 , plugin .getTtlForQuery ("cache_param(ttl=200s)" ));
82+ assertEquals (150 , plugin .getTtlForQuery ("Cache_Param(ttl=150s)" ));
83+ assertEquals (200 , plugin .getTtlForQuery ("cache_param(tTl=200s)" ));
84+ assertEquals (150 , plugin .getTtlForQuery ("Cache_Param(ttl=150S)" ));
85+ assertEquals (200 , plugin .getTtlForQuery ("cache_param(TTL=200S)" ));
86+
87+ // CACHE_PARAM anywhere in hint content (mixed with other hint directives)
88+ assertEquals (250 , plugin .getTtlForQuery ("INDEX(table1 idx1) CACHE_PARAM(ttl=250s)" ));
89+ assertEquals (200 , plugin .getTtlForQuery ("CACHE_PARAM(ttl=200s) USE_NL(t1 t2)" ));
90+ assertEquals (180 , plugin .getTtlForQuery ("FIRST_ROWS(10) CACHE_PARAM(ttl=180s) PARALLEL(4)" ));
91+ assertEquals (200 , plugin .getTtlForQuery ("foo=bar,CACHE_PARAM(ttl=200s),baz=qux" ));
92+
93+ // Whitespace handling
94+ assertEquals (400 , plugin .getTtlForQuery ("CACHE_PARAM( ttl=400s )" ));
95+ assertEquals (500 , plugin .getTtlForQuery ("CACHE_PARAM(ttl = 500s)" ));
96+ assertEquals (200 , plugin .getTtlForQuery ("CACHE_PARAM( ttl = 200s , key = test )" ));
97+
98+ // Invalid cases - no CACHE_PARAM in hint content
99+ assertNull (plugin .getTtlForQuery ("INDEX(table1 idx1)" ));
100+ assertNull (plugin .getTtlForQuery ("FIRST_ROWS(100)" ));
101+ assertNull (plugin .getTtlForQuery ("cachettl=300s" )); // old format
102+ assertNull (plugin .getTtlForQuery ("NO_CACHE" ));
103+
104+ // Missing parentheses
105+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM ttl=300s" ));
106+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(ttl=300s" ));
107+
108+ // Multiple parameters (future-proofing)
109+ assertEquals (300 , plugin .getTtlForQuery ("CACHE_PARAM(ttl=300s, key=test)" ));
110+
111+ // Large TTL values should work
112+ assertEquals (999999 , plugin .getTtlForQuery ("CACHE_PARAM(ttl=999999s)" ));
113+ assertEquals (86400 , plugin .getTtlForQuery ("CACHE_PARAM(ttl=86400s)" )); // 24 hours
114+ }
115+
116+ @ Test
117+ void test_getTTLFromQueryHint_MalformedHints () throws Exception {
118+ // Test malformed cases
119+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM()" ));
120+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(ttl=abc)" ));
121+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(ttl=300)" )); // missing 's'
122+
123+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(ttl=)" ));
124+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(invalid_format)" ));
125+
126+ // Invalid TTL values (negative and zero) does not count toward malformed hints
127+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(ttl=0s)" ));
128+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(ttl=-10s)" ));
129+ assertNull (plugin .getTtlForQuery ("CACHE_PARAM(ttl=-1s)" ));
130+
131+ // Verify counter was incremented 8 times (5 original + 3 new)
132+ verify (mockMalformedHintCounter , times (5 )).inc ();
99133 }
100134
101135 @ Test
@@ -125,7 +159,7 @@ void test_execute_noCachingLongQuery() throws Exception {
125159 when (mockCallable .call ()).thenReturn (mockResult1 );
126160
127161 ResultSet rs = plugin .execute (ResultSet .class , SQLException .class , mockStatement ,
128- methodName , mockCallable , new String []{"/* cacheTTL=30s */ select * from T" + RandomStringUtils .randomAlphanumeric (15990 )});
162+ methodName , mockCallable , new String []{"/* CACHE_PARAM(ttl=20s) */ select * from T" + RandomStringUtils .randomAlphanumeric (15990 )});
129163
130164 // Mock result set containing 1 row
131165 when (mockResult1 .next ()).thenReturn (true , true , false , false );
@@ -153,7 +187,7 @@ void test_execute_cachingMissAndHit() throws Exception {
153187 when (mockResult1 .getObject (1 )).thenReturn ("bar1" );
154188
155189 ResultSet rs = plugin .execute (ResultSet .class , SQLException .class , mockStatement ,
156- methodName , mockCallable , new String []{"/*CACHETTL=100s */ select * from A" });
190+ methodName , mockCallable , new String []{"/*+CACHE_PARAM(ttl=50s) */ select * from A" });
157191
158192 // Cached result set contains 1 row
159193 assertTrue (rs .next ());
@@ -163,7 +197,7 @@ void test_execute_cachingMissAndHit() throws Exception {
163197 byte [] serializedTestResultSet = ((CachedResultSet )rs ).serializeIntoByteArray ();
164198 when (mockCacheConn .readFromCache ("public_user_select * from A" )).thenReturn (serializedTestResultSet );
165199 ResultSet rs2 = plugin .execute (ResultSet .class , SQLException .class , mockStatement ,
166- methodName , mockCallable , new String []{" /* CacheTtl =50s */select * from A" });
200+ methodName , mockCallable , new String []{" /*+CACHE_PARAM(ttl =50s) */select * from A" });
167201
168202 assertTrue (rs2 .next ());
169203 assertEquals ("bar1" , rs2 .getString ("fooName" ));
@@ -172,7 +206,7 @@ void test_execute_cachingMissAndHit() throws Exception {
172206 verify (mockPluginService , times (2 )).isInTransaction ();
173207 verify (mockCacheConn , times (2 )).readFromCache ("public_user_select * from A" );
174208 verify (mockCallable ).call ();
175- verify (mockCacheConn ).writeToCache (eq ("public_user_select * from A" ), any (), eq (100 ));
209+ verify (mockCacheConn ).writeToCache (eq ("public_user_select * from A" ), any (), eq (50 ));
176210 verify (mockTotalCallsCounter , times (2 )).inc ();
177211 verify (mockMissCounter ).inc ();
178212 verify (mockHitCounter ).inc ();
@@ -193,7 +227,7 @@ void test_transaction_cacheQuery() throws Exception {
193227 when (mockResult1 .getObject (1 )).thenReturn ("bar1" );
194228
195229 ResultSet rs = plugin .execute (ResultSet .class , SQLException .class , mockStatement ,
196- methodName , mockCallable , new String []{"/* cacheTTL =300s */ select * from T" });
230+ methodName , mockCallable , new String []{"/*+ CACHE_PARAM(ttl =300s) */ select * from T" });
197231
198232 // Cached result set contains 1 row
199233 assertTrue (rs .next ());
@@ -210,6 +244,66 @@ void test_transaction_cacheQuery() throws Exception {
210244 }
211245
212246 @ Test
247+ void test_transaction_cacheQuery_multiple_query_params () throws Exception {
248+ // Query is cacheable
249+ when (mockPluginService .getCurrentConnection ()).thenReturn (mockConnection );
250+ when (mockPluginService .isInTransaction ()).thenReturn (true );
251+ when (mockConnection .getMetaData ()).thenReturn (mockDbMetadata );
252+ when (mockConnection .getSchema ()).thenReturn ("public" );
253+ when (mockDbMetadata .getUserName ()).thenReturn ("user" );
254+ when (mockCallable .call ()).thenReturn (mockResult1 );
255+
256+ // Result set contains 1 row
257+ when (mockResult1 .next ()).thenReturn (true , false );
258+ when (mockResult1 .getObject (1 )).thenReturn ("bar1" );
259+
260+ ResultSet rs = plugin .execute (ResultSet .class , SQLException .class , mockStatement , methodName , mockCallable , new String []{"/*+ CACHE_PARAM(ttl=300s, otherParam=abc) */ select * from T" });
261+
262+ // Cached result set contains 1 row
263+ assertTrue (rs .next ());
264+ assertEquals ("bar1" , rs .getString ("fooName" ));
265+ assertFalse (rs .next ());
266+ verify (mockPluginService ).getCurrentConnection ();
267+ verify (mockPluginService ).isInTransaction ();
268+ verify (mockCacheConn , never ()).readFromCache (anyString ());
269+ verify (mockCallable ).call ();
270+ verify (mockCacheConn ).writeToCache (eq ("public_user_select * from T" ), any (), eq (300 ));
271+ verify (mockTotalCallsCounter , never ()).inc ();
272+ verify (mockHitCounter , never ()).inc ();
273+ verify (mockMissCounter , never ()).inc ();
274+ }
275+
276+ @ Test
277+ void test_transaction_cacheQuery_multiple_query_hints () throws Exception {// Query is cacheable
278+ when (mockPluginService .getCurrentConnection ()).thenReturn (mockConnection );
279+ when (mockPluginService .isInTransaction ()).thenReturn (true );
280+ when (mockConnection .getMetaData ()).thenReturn (mockDbMetadata );
281+ when (mockConnection .getSchema ()).thenReturn ("public" );
282+ when (mockDbMetadata .getUserName ()).thenReturn ("user" );
283+ when (mockCallable .call ()).thenReturn (mockResult1 );
284+
285+ // Result set contains 1 row
286+ when (mockResult1 .next ()).thenReturn (true , false );
287+ when (mockResult1 .getObject (1 )).thenReturn ("bar1" );
288+
289+ ResultSet rs = plugin .execute (ResultSet .class , SQLException .class , mockStatement ,
290+ methodName , mockCallable , new String []{"/*+ hello CACHE_PARAM(ttl=300s, otherParam=abc) world */ select * from T" });
291+
292+ // Cached result set contains 1 row
293+ assertTrue (rs .next ());
294+ assertEquals ("bar1" , rs .getString ("fooName" ));
295+ assertFalse (rs .next ());
296+ verify (mockPluginService ).getCurrentConnection ();
297+ verify (mockPluginService ).isInTransaction ();
298+ verify (mockCacheConn , never ()).readFromCache (anyString ());
299+ verify (mockCallable ).call ();
300+ verify (mockCacheConn ).writeToCache (eq ("public_user_select * from T" ), any (), eq (300 ));
301+ verify (mockTotalCallsCounter , never ()).inc ();
302+ verify (mockHitCounter , never ()).inc ();
303+ verify (mockMissCounter , never ()).inc ();
304+ }
305+
306+ @ Test
213307 void test_transaction_noCaching () throws Exception {
214308 // Query is not cacheable
215309 when (mockPluginService .isInTransaction ()).thenReturn (true );
0 commit comments