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+" );
4962
5063 private final ByteBuffer id ;
5164 private final RepreparePayload repreparePayload ;
@@ -69,6 +82,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
6982 private final Duration timeoutForBoundStatements ;
7083 private final Partitioner partitioner ;
7184 private final boolean isLWT ;
85+ private volatile boolean skipMetadata ;
7286
7387 public DefaultPreparedStatement (
7488 ByteBuffer id ,
@@ -122,6 +136,9 @@ public DefaultPreparedStatement(
122136 this .codecRegistry = codecRegistry ;
123137 this .protocolVersion = protocolVersion ;
124138 this .isLWT = isLWT ;
139+ this .skipMetadata =
140+ resolveSkipMetadata (
141+ query , resultMetadataId , resultSetDefinitions , this .executionProfileForBoundStatements );
125142 }
126143
127144 @ NonNull
@@ -147,6 +164,10 @@ public Partitioner getPartitioner() {
147164 return partitioner ;
148165 }
149166
167+ public boolean isSkipMetadata () {
168+ return skipMetadata ;
169+ }
170+
150171 @ NonNull
151172 @ Override
152173 public List <Integer > getPartitionKeyIndices () {
@@ -172,6 +193,13 @@ public boolean isLWT() {
172193 @ Override
173194 public void setResultMetadata (
174195 @ NonNull ByteBuffer newResultMetadataId , @ NonNull ColumnDefinitions newResultSetDefinitions ) {
196+ this .skipMetadata =
197+ resolveSkipMetadata (
198+ this .getQuery (),
199+ newResultMetadataId ,
200+ newResultSetDefinitions ,
201+ executionProfileForBoundStatements );
202+
175203 this .resultMetadata = new ResultMetadata (newResultMetadataId , newResultSetDefinitions );
176204 }
177205
@@ -242,4 +270,132 @@ private ResultMetadata(ByteBuffer resultMetadataId, ColumnDefinitions resultSetD
242270 this .resultSetDefinitions = resultSetDefinitions ;
243271 }
244272 }
273+
274+ private static boolean resolveSkipMetadata (
275+ String query ,
276+ ByteBuffer resultMetadataId ,
277+ ColumnDefinitions resultSet ,
278+ DriverExecutionProfile executionProfileForBoundStatements ) {
279+ if (resultSet == null || resultSet .size () == 0 ) {
280+ // there is no reason to send this flag, there will be no rows in the response and,
281+ // consequently, no metadata.
282+ return false ;
283+ }
284+ if (resultMetadataId != null && resultMetadataId .capacity () > 0 ) {
285+ // Result metadata ID feature is supported, it makes prepared statement invalidation work
286+ // properly.
287+ // Skip Metadata should be enabled.
288+ // Prepared statement invalidation works perfectly no need to disable skip metadata
289+ return true ;
290+ }
291+
292+ CQL4SkipMetadataResolveMethod resolveMethod = CQL4SkipMetadataResolveMethod .SMART ;
293+
294+ if (executionProfileForBoundStatements != null ) {
295+ String resolveMethodName =
296+ executionProfileForBoundStatements .getString (
297+ DefaultDriverOption .PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD );
298+ try {
299+ resolveMethod = CQL4SkipMetadataResolveMethod .fromValue (resolveMethodName );
300+ } catch (IllegalArgumentException e ) {
301+ LOGGER .warn (
302+ "Property advanced.prepared-statements.skip-cql4-metadata-resolve-method is incorrectly set to `{}`, "
303+ + "available options: smart, enabled, disabled. Defaulting to `SMART`" ,
304+ resolveMethodName );
305+ resolveMethod = CQL4SkipMetadataResolveMethod .SMART ;
306+ }
307+ }
308+
309+ switch (resolveMethod ) {
310+ case ENABLED :
311+ return true ;
312+ case DISABLED :
313+ return false ;
314+ case SMART :
315+ break ;
316+ }
317+
318+ List <String > chunks = SPACE_SPLITTER .splitToList (query .trim ().toLowerCase ());
319+ if (chunks .size () < 2 ) {
320+ // Weird query, assuming no result expected
321+ return false ;
322+ }
323+
324+ if (!chunks .get (0 ).equals ("select" )) {
325+ // In case if non-select sneaks in, disable skip metadata for it no result expected.
326+ return false ;
327+ }
328+
329+ if (isStartPresent (query )) {
330+ LOGGER .warn (
331+ "Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
332+ + "These issues may lead to broken deserialization or data corruption. "
333+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
334+ + "though this negatively impacts performance. "
335+ + "To avoid this, consider using a targeted select instead. "
336+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
337+ query );
338+ return false ;
339+ }
340+ // Disable skipping metadata if results contains udt and
341+ for (ColumnDefinition columnDefinition : resultSet ) {
342+ if (containsUDT (columnDefinition .getType ())) {
343+ LOGGER .warn (
344+ "Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
345+ + "These issues may lead to broken deserialization or data corruption. "
346+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
347+ + "though this negatively impacts performance. "
348+ + "To avoid this, consider using regular columns instead of UDT. "
349+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
350+ query );
351+ return false ;
352+ }
353+ }
354+ return true ;
355+ }
356+
357+ private static boolean containsUDT (DataType dataType ) {
358+ if (dataType instanceof ContainerType ) {
359+ return containsUDT (((ContainerType ) dataType ).getElementType ());
360+ } else if (dataType instanceof TupleType ) {
361+ for (DataType elementType : ((TupleType ) dataType ).getComponentTypes ()) {
362+ if (containsUDT (elementType )) {
363+ return true ;
364+ }
365+ }
366+ return false ;
367+ } else if (dataType instanceof MapType ) {
368+ return containsUDT (((MapType ) dataType ).getKeyType ())
369+ || containsUDT (((MapType ) dataType ).getValueType ());
370+ }
371+ return dataType instanceof UserDefinedType ;
372+ }
373+
374+ private static boolean isStartPresent (String query ) {
375+ List <String > chunks = SPACE_SPLITTER .splitToList (query .trim ().toLowerCase ());
376+ if (chunks .size () < 2 ) {
377+ // Weird query, assuming no result expected
378+ return false ;
379+ }
380+
381+ if (!chunks .get (0 ).equals ("select" )) {
382+ // In case if non-select sneaks in, disable skip metadata for it no result expected.
383+ return false ;
384+ }
385+
386+ for (String chunk : chunks ) {
387+ if (chunk .equals ("from" )) {
388+ return false ;
389+ }
390+ if (chunk .equals ("*" )) {
391+ return true ;
392+ }
393+ for (String part : chunk .split ("," )) {
394+ if (part .equals ("*" )) {
395+ return true ;
396+ }
397+ }
398+ }
399+ return false ;
400+ }
245401}
0 commit comments