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 );
4961
5062 private final ByteBuffer id ;
5163 private final RepreparePayload repreparePayload ;
@@ -69,6 +81,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
6981 private final Duration timeoutForBoundStatements ;
7082 private final Partitioner partitioner ;
7183 private final boolean isLWT ;
84+ private volatile boolean skipMetadata ;
7285
7386 public DefaultPreparedStatement (
7487 ByteBuffer id ,
@@ -122,6 +135,9 @@ public DefaultPreparedStatement(
122135 this .codecRegistry = codecRegistry ;
123136 this .protocolVersion = protocolVersion ;
124137 this .isLWT = isLWT ;
138+ this .skipMetadata =
139+ resolveSkipMetadata (
140+ query , resultMetadataId , resultSetDefinitions , this .executionProfileForBoundStatements );
125141 }
126142
127143 @ NonNull
@@ -147,6 +163,10 @@ public Partitioner getPartitioner() {
147163 return partitioner ;
148164 }
149165
166+ public boolean isSkipMetadata () {
167+ return skipMetadata ;
168+ }
169+
150170 @ NonNull
151171 @ Override
152172 public List <Integer > getPartitionKeyIndices () {
@@ -172,6 +192,13 @@ public boolean isLWT() {
172192 @ Override
173193 public void setResultMetadata (
174194 @ NonNull ByteBuffer newResultMetadataId , @ NonNull ColumnDefinitions newResultSetDefinitions ) {
195+ this .skipMetadata =
196+ resolveSkipMetadata (
197+ this .getQuery (),
198+ newResultMetadataId ,
199+ newResultSetDefinitions ,
200+ executionProfileForBoundStatements );
201+
175202 this .resultMetadata = new ResultMetadata (newResultMetadataId , newResultSetDefinitions );
176203 }
177204
@@ -242,4 +269,114 @@ private ResultMetadata(ByteBuffer resultMetadataId, ColumnDefinitions resultSetD
242269 this .resultSetDefinitions = resultSetDefinitions ;
243270 }
244271 }
272+
273+ private static boolean resolveSkipMetadata (
274+ String query ,
275+ ByteBuffer resultMetadataId ,
276+ ColumnDefinitions resultSet ,
277+ DriverExecutionProfile executionProfileForBoundStatements ) {
278+ if (resultSet == null || resultSet .size () == 0 ) {
279+ // there is no reason to send this flag, there will be no rows in the response and,
280+ // consequently, no metadata.
281+ return false ;
282+ }
283+ if (resultMetadataId != null && resultMetadataId .capacity () > 0 ) {
284+ // Result metadata ID feature is supported, it makes prepared statement invalidation work
285+ // properly.
286+ // Skip Metadata should be enabled.
287+ // Prepared statement invalidation works perfectly no need to disable skip metadata
288+ return true ;
289+ }
290+
291+ CQL4SkipMetadataResolveMethod resolveMethod = CQL4SkipMetadataResolveMethod .SMART ;
292+
293+ if (executionProfileForBoundStatements != null ) {
294+ String resolveMethodName =
295+ executionProfileForBoundStatements .getString (
296+ DefaultDriverOption .PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD );
297+ try {
298+ resolveMethod = CQL4SkipMetadataResolveMethod .fromValue (resolveMethodName );
299+ } catch (IllegalArgumentException e ) {
300+ LOGGER .warn (
301+ "Property advanced.prepared-statements.skip-cql4-metadata-resolve-method is incorrectly set to `{}`, "
302+ + "available options: smart, enabled, disabled. Defaulting to `SMART`" ,
303+ resolveMethodName );
304+ resolveMethod = CQL4SkipMetadataResolveMethod .SMART ;
305+ }
306+ }
307+
308+ switch (resolveMethod ) {
309+ case ENABLED :
310+ return true ;
311+ case DISABLED :
312+ return false ;
313+ case SMART :
314+ break ;
315+ }
316+
317+ List <String > chunks = Splitter .onPattern ("\\ s+" ).splitToList (query .trim ().toLowerCase ());
318+ if (chunks .size () < 2 ) {
319+ // Weird query, assuming no result expected
320+ return false ;
321+ }
322+
323+ if (!chunks .get (0 ).equals ("select" )) {
324+ // In case if non-select sneaks in, disable skip metadata for it no result expected.
325+ return false ;
326+ }
327+
328+ boolean starPresent = false ;
329+ for (String chunk : chunks ) {
330+ if (chunk .equals ("from" )) {
331+ break ;
332+ }
333+ if (chunk .equals ("*" )) {
334+ starPresent = true ;
335+ }
336+ }
337+
338+ if (starPresent ) {
339+ LOGGER .warn (
340+ "Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
341+ + "These issues may lead to broken deserialization or data corruption. "
342+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
343+ + "though this negatively impacts performance. "
344+ + "To avoid this, consider using a targeted select instead. "
345+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
346+ query );
347+ return false ;
348+ }
349+ // Disable skipping metadata if results contains udt and
350+ for (ColumnDefinition columnDefinition : resultSet ) {
351+ if (containsUDT (columnDefinition .getType ())) {
352+ LOGGER .warn (
353+ "Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
354+ + "These issues may lead to broken deserialization or data corruption. "
355+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
356+ + "though this negatively impacts performance. "
357+ + "To avoid this, consider using regular columns instead of UDT. "
358+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
359+ query );
360+ return false ;
361+ }
362+ }
363+ return true ;
364+ }
365+
366+ private static boolean containsUDT (DataType dataType ) {
367+ if (dataType instanceof ContainerType ) {
368+ return containsUDT (((ContainerType ) dataType ).getElementType ());
369+ } else if (dataType instanceof TupleType ) {
370+ for (DataType elementType : ((TupleType ) dataType ).getComponentTypes ()) {
371+ if (containsUDT (elementType )) {
372+ return true ;
373+ }
374+ }
375+ return false ;
376+ } else if (dataType instanceof MapType ) {
377+ return containsUDT (((MapType ) dataType ).getKeyType ())
378+ || containsUDT (((MapType ) dataType ).getValueType ());
379+ }
380+ return dataType instanceof UserDefinedType ;
381+ }
245382}
0 commit comments