23
23
*/
24
24
package com .datastax .oss .driver .internal .core .cql ;
25
25
26
+ import com .datastax .oss .driver .api .core .CQL4SkipMetadataResolveMethod ;
26
27
import com .datastax .oss .driver .api .core .ConsistencyLevel ;
27
28
import com .datastax .oss .driver .api .core .CqlIdentifier ;
28
29
import com .datastax .oss .driver .api .core .ProtocolVersion ;
30
+ import com .datastax .oss .driver .api .core .config .DefaultDriverOption ;
29
31
import com .datastax .oss .driver .api .core .config .DriverExecutionProfile ;
30
32
import com .datastax .oss .driver .api .core .cql .BoundStatement ;
31
33
import com .datastax .oss .driver .api .core .cql .BoundStatementBuilder ;
34
+ import com .datastax .oss .driver .api .core .cql .ColumnDefinition ;
32
35
import com .datastax .oss .driver .api .core .cql .ColumnDefinitions ;
33
36
import com .datastax .oss .driver .api .core .cql .PreparedStatement ;
34
37
import com .datastax .oss .driver .api .core .cql .Statement ;
35
38
import com .datastax .oss .driver .api .core .metadata .token .Partitioner ;
36
39
import com .datastax .oss .driver .api .core .metadata .token .Token ;
40
+ import com .datastax .oss .driver .api .core .type .ContainerType ;
41
+ import com .datastax .oss .driver .api .core .type .DataType ;
42
+ import com .datastax .oss .driver .api .core .type .MapType ;
43
+ import com .datastax .oss .driver .api .core .type .TupleType ;
44
+ import com .datastax .oss .driver .api .core .type .UserDefinedType ;
37
45
import com .datastax .oss .driver .api .core .type .codec .registry .CodecRegistry ;
38
46
import com .datastax .oss .driver .internal .core .data .ValuesHelper ;
39
47
import com .datastax .oss .driver .internal .core .session .RepreparePayload ;
48
+ import com .datastax .oss .driver .shaded .guava .common .base .Splitter ;
40
49
import edu .umd .cs .findbugs .annotations .NonNull ;
41
50
import java .nio .ByteBuffer ;
42
51
import java .time .Duration ;
43
52
import java .util .List ;
44
53
import java .util .Map ;
45
54
import net .jcip .annotations .ThreadSafe ;
55
+ import org .slf4j .Logger ;
56
+ import org .slf4j .LoggerFactory ;
46
57
47
58
@ ThreadSafe
48
59
public class DefaultPreparedStatement implements PreparedStatement {
60
+ private static final Logger LOGGER = LoggerFactory .getLogger (DefaultPreparedStatement .class );
61
+ private static final Splitter SPACE_SPLITTER = Splitter .onPattern ("\\ s+" );
62
+ private static final Splitter COMMA_SPLITTER = Splitter .onPattern ("," );
49
63
50
64
private final ByteBuffer id ;
51
65
private final RepreparePayload repreparePayload ;
@@ -69,6 +83,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
69
83
private final Duration timeoutForBoundStatements ;
70
84
private final Partitioner partitioner ;
71
85
private final boolean isLWT ;
86
+ private volatile boolean skipMetadata ;
72
87
73
88
public DefaultPreparedStatement (
74
89
ByteBuffer id ,
@@ -122,6 +137,9 @@ public DefaultPreparedStatement(
122
137
this .codecRegistry = codecRegistry ;
123
138
this .protocolVersion = protocolVersion ;
124
139
this .isLWT = isLWT ;
140
+ this .skipMetadata =
141
+ resolveSkipMetadata (
142
+ query , resultMetadataId , resultSetDefinitions , this .executionProfileForBoundStatements );
125
143
}
126
144
127
145
@ NonNull
@@ -147,6 +165,10 @@ public Partitioner getPartitioner() {
147
165
return partitioner ;
148
166
}
149
167
168
+ public boolean isSkipMetadata () {
169
+ return skipMetadata ;
170
+ }
171
+
150
172
@ NonNull
151
173
@ Override
152
174
public List <Integer > getPartitionKeyIndices () {
@@ -172,6 +194,13 @@ public boolean isLWT() {
172
194
@ Override
173
195
public void setResultMetadata (
174
196
@ NonNull ByteBuffer newResultMetadataId , @ NonNull ColumnDefinitions newResultSetDefinitions ) {
197
+ this .skipMetadata =
198
+ resolveSkipMetadata (
199
+ this .getQuery (),
200
+ newResultMetadataId ,
201
+ newResultSetDefinitions ,
202
+ executionProfileForBoundStatements );
203
+
175
204
this .resultMetadata = new ResultMetadata (newResultMetadataId , newResultSetDefinitions );
176
205
}
177
206
@@ -242,4 +271,121 @@ private ResultMetadata(ByteBuffer resultMetadataId, ColumnDefinitions resultSetD
242
271
this .resultSetDefinitions = resultSetDefinitions ;
243
272
}
244
273
}
274
+
275
+ private static boolean resolveSkipMetadata (
276
+ String query ,
277
+ ByteBuffer resultMetadataId ,
278
+ ColumnDefinitions resultSet ,
279
+ DriverExecutionProfile executionProfileForBoundStatements ) {
280
+ if (resultSet == null || resultSet .size () == 0 ) {
281
+ // there is no reason to send this flag, there will be no rows in the response and,
282
+ // consequently, no metadata.
283
+ return false ;
284
+ }
285
+ if (resultMetadataId != null && resultMetadataId .capacity () > 0 ) {
286
+ // Result metadata ID feature is supported, it makes prepared statement invalidation work
287
+ // properly.
288
+ // Skip Metadata should be enabled.
289
+ // Prepared statement invalidation works perfectly no need to disable skip metadata
290
+ return true ;
291
+ }
292
+
293
+ CQL4SkipMetadataResolveMethod resolveMethod = CQL4SkipMetadataResolveMethod .SMART ;
294
+
295
+ if (executionProfileForBoundStatements != null ) {
296
+ String resolveMethodName =
297
+ executionProfileForBoundStatements .getString (
298
+ DefaultDriverOption .PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD );
299
+ try {
300
+ resolveMethod = CQL4SkipMetadataResolveMethod .fromValue (resolveMethodName );
301
+ } catch (IllegalArgumentException e ) {
302
+ LOGGER .warn (
303
+ "Property advanced.prepared-statements.skip-cql4-metadata-resolve-method is incorrectly set to `{}`, "
304
+ + "available options: smart, enabled, disabled. Defaulting to `SMART`" ,
305
+ resolveMethodName );
306
+ resolveMethod = CQL4SkipMetadataResolveMethod .SMART ;
307
+ }
308
+ }
309
+
310
+ switch (resolveMethod ) {
311
+ case ENABLED :
312
+ return true ;
313
+ case DISABLED :
314
+ return false ;
315
+ case SMART :
316
+ break ;
317
+ }
318
+
319
+ if (isWildcardSelect (query )) {
320
+ LOGGER .warn (
321
+ "Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
322
+ + "These issues may lead to broken deserialization or data corruption. "
323
+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
324
+ + "though this negatively impacts performance. "
325
+ + "To avoid this, consider using a targeted select instead. "
326
+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
327
+ query );
328
+ return false ;
329
+ }
330
+ // Disable skipping metadata if results contains udt and
331
+ for (ColumnDefinition columnDefinition : resultSet ) {
332
+ if (containsUDT (columnDefinition .getType ())) {
333
+ LOGGER .warn (
334
+ "Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
335
+ + "These issues may lead to broken deserialization or data corruption. "
336
+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
337
+ + "though this negatively impacts performance. "
338
+ + "To avoid this, consider using regular columns instead of UDT. "
339
+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
340
+ query );
341
+ return false ;
342
+ }
343
+ }
344
+ return true ;
345
+ }
346
+
347
+ private static boolean containsUDT (DataType dataType ) {
348
+ if (dataType instanceof ContainerType ) {
349
+ return containsUDT (((ContainerType ) dataType ).getElementType ());
350
+ } else if (dataType instanceof TupleType ) {
351
+ for (DataType elementType : ((TupleType ) dataType ).getComponentTypes ()) {
352
+ if (containsUDT (elementType )) {
353
+ return true ;
354
+ }
355
+ }
356
+ return false ;
357
+ } else if (dataType instanceof MapType ) {
358
+ return containsUDT (((MapType ) dataType ).getKeyType ())
359
+ || containsUDT (((MapType ) dataType ).getValueType ());
360
+ }
361
+ return dataType instanceof UserDefinedType ;
362
+ }
363
+
364
+ private static boolean isWildcardSelect (String query ) {
365
+ List <String > chunks = SPACE_SPLITTER .splitToList (query .trim ().toLowerCase ());
366
+ if (chunks .size () < 2 ) {
367
+ // Weird query, assuming no result expected
368
+ return false ;
369
+ }
370
+
371
+ if (!chunks .get (0 ).equals ("select" )) {
372
+ // In case if non-select sneaks in, disable skip metadata for it no result expected.
373
+ return false ;
374
+ }
375
+
376
+ for (String chunk : chunks ) {
377
+ if (chunk .equals ("from" )) {
378
+ return false ;
379
+ }
380
+ if (chunk .equals ("*" )) {
381
+ return true ;
382
+ }
383
+ for (String part : COMMA_SPLITTER .split (chunk )) {
384
+ if (part .equals ("*" )) {
385
+ return true ;
386
+ }
387
+ }
388
+ }
389
+ return false ;
390
+ }
245
391
}
0 commit comments