2323 */
2424package com .datastax .oss .driver .internal .core .cql ;
2525
26+ import com .datastax .oss .driver .api .core .CQL4SkipMetadataResolveMethod ;
2627import com .datastax .oss .driver .api .core .ConsistencyLevel ;
2728import com .datastax .oss .driver .api .core .CqlIdentifier ;
2829import com .datastax .oss .driver .api .core .ProtocolVersion ;
30+ import com .datastax .oss .driver .api .core .config .DefaultDriverOption ;
2931import com .datastax .oss .driver .api .core .config .DriverExecutionProfile ;
3032import com .datastax .oss .driver .api .core .cql .BoundStatement ;
3133import com .datastax .oss .driver .api .core .cql .BoundStatementBuilder ;
34+ import com .datastax .oss .driver .api .core .cql .ColumnDefinition ;
3235import com .datastax .oss .driver .api .core .cql .ColumnDefinitions ;
3336import com .datastax .oss .driver .api .core .cql .PreparedStatement ;
3437import com .datastax .oss .driver .api .core .cql .Statement ;
3538import com .datastax .oss .driver .api .core .metadata .token .Partitioner ;
3639import 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 ;
3745import com .datastax .oss .driver .api .core .type .codec .registry .CodecRegistry ;
3846import com .datastax .oss .driver .internal .core .data .ValuesHelper ;
3947import com .datastax .oss .driver .internal .core .session .RepreparePayload ;
48+ import com .datastax .oss .driver .shaded .guava .common .base .Splitter ;
4049import edu .umd .cs .findbugs .annotations .NonNull ;
4150import java .nio .ByteBuffer ;
4251import java .time .Duration ;
4352import java .util .List ;
4453import java .util .Map ;
4554import net .jcip .annotations .ThreadSafe ;
55+ import org .slf4j .Logger ;
56+ import org .slf4j .LoggerFactory ;
4657
4758@ ThreadSafe
4859public 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 ("," );
4963
5064 private final ByteBuffer id ;
5165 private final RepreparePayload repreparePayload ;
@@ -69,6 +83,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
6983 private final Duration timeoutForBoundStatements ;
7084 private final Partitioner partitioner ;
7185 private final boolean isLWT ;
86+ private volatile boolean skipMetadata ;
7287
7388 public DefaultPreparedStatement (
7489 ByteBuffer id ,
@@ -122,6 +137,9 @@ public DefaultPreparedStatement(
122137 this .codecRegistry = codecRegistry ;
123138 this .protocolVersion = protocolVersion ;
124139 this .isLWT = isLWT ;
140+ this .skipMetadata =
141+ resolveSkipMetadata (
142+ query , resultMetadataId , resultSetDefinitions , this .executionProfileForBoundStatements );
125143 }
126144
127145 @ NonNull
@@ -147,6 +165,10 @@ public Partitioner getPartitioner() {
147165 return partitioner ;
148166 }
149167
168+ public boolean isSkipMetadata () {
169+ return skipMetadata ;
170+ }
171+
150172 @ NonNull
151173 @ Override
152174 public List <Integer > getPartitionKeyIndices () {
@@ -172,6 +194,13 @@ public boolean isLWT() {
172194 @ Override
173195 public void setResultMetadata (
174196 @ NonNull ByteBuffer newResultMetadataId , @ NonNull ColumnDefinitions newResultSetDefinitions ) {
197+ this .skipMetadata =
198+ resolveSkipMetadata (
199+ this .getQuery (),
200+ newResultMetadataId ,
201+ newResultSetDefinitions ,
202+ executionProfileForBoundStatements );
203+
175204 this .resultMetadata = new ResultMetadata (newResultMetadataId , newResultSetDefinitions );
176205 }
177206
@@ -242,4 +271,121 @@ private ResultMetadata(ByteBuffer resultMetadataId, ColumnDefinitions resultSetD
242271 this .resultSetDefinitions = resultSetDefinitions ;
243272 }
244273 }
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+ }
245391}
0 commit comments