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,132 @@ 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+ List <String > chunks = SPACE_SPLITTER .splitToList (query .trim ().toLowerCase ());
320+ if (chunks .size () < 2 ) {
321+ // Weird query, assuming no result expected
322+ return false ;
323+ }
324+
325+ if (!chunks .get (0 ).equals ("select" )) {
326+ // In case if non-select sneaks in, disable skip metadata for it no result expected.
327+ return false ;
328+ }
329+
330+ if (isStartPresent (query )) {
331+ LOGGER .warn (
332+ "Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
333+ + "These issues may lead to broken deserialization or data corruption. "
334+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
335+ + "though this negatively impacts performance. "
336+ + "To avoid this, consider using a targeted select instead. "
337+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
338+ query );
339+ return false ;
340+ }
341+ // Disable skipping metadata if results contains udt and
342+ for (ColumnDefinition columnDefinition : resultSet ) {
343+ if (containsUDT (columnDefinition .getType ())) {
344+ LOGGER .warn (
345+ "Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
346+ + "These issues may lead to broken deserialization or data corruption. "
347+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
348+ + "though this negatively impacts performance. "
349+ + "To avoid this, consider using regular columns instead of UDT. "
350+ + "Find more mitigation options in description of `advanced.prepared-statements.skip-cql4-metadata-resolve-method` flag" ,
351+ query );
352+ return false ;
353+ }
354+ }
355+ return true ;
356+ }
357+
358+ private static boolean containsUDT (DataType dataType ) {
359+ if (dataType instanceof ContainerType ) {
360+ return containsUDT (((ContainerType ) dataType ).getElementType ());
361+ } else if (dataType instanceof TupleType ) {
362+ for (DataType elementType : ((TupleType ) dataType ).getComponentTypes ()) {
363+ if (containsUDT (elementType )) {
364+ return true ;
365+ }
366+ }
367+ return false ;
368+ } else if (dataType instanceof MapType ) {
369+ return containsUDT (((MapType ) dataType ).getKeyType ())
370+ || containsUDT (((MapType ) dataType ).getValueType ());
371+ }
372+ return dataType instanceof UserDefinedType ;
373+ }
374+
375+ private static boolean isStartPresent (String query ) {
376+ List <String > chunks = SPACE_SPLITTER .splitToList (query .trim ().toLowerCase ());
377+ if (chunks .size () < 2 ) {
378+ // Weird query, assuming no result expected
379+ return false ;
380+ }
381+
382+ if (!chunks .get (0 ).equals ("select" )) {
383+ // In case if non-select sneaks in, disable skip metadata for it no result expected.
384+ return false ;
385+ }
386+
387+ for (String chunk : chunks ) {
388+ if (chunk .equals ("from" )) {
389+ return false ;
390+ }
391+ if (chunk .equals ("*" )) {
392+ return true ;
393+ }
394+ for (String part : COMMA_SPLITTER .split (chunk )) {
395+ if (part .equals ("*" )) {
396+ return true ;
397+ }
398+ }
399+ }
400+ return false ;
401+ }
245402}
0 commit comments