2424import static com .datastax .driver .core .ProtocolVersion .V4 ;
2525
2626import com .datastax .driver .core .policies .RetryPolicy ;
27+ import com .google .common .base .Splitter ;
2728import com .google .common .collect .ImmutableMap ;
2829import java .nio .ByteBuffer ;
2930import java .util .List ;
3031import java .util .Map ;
32+ import org .slf4j .Logger ;
33+ import org .slf4j .LoggerFactory ;
3134
3235public class DefaultPreparedStatement implements PreparedStatement {
33-
36+ private static final Logger LOGGER = LoggerFactory . getLogger ( DefaultPreparedStatement . class );
3437 private static final String SCYLLA_CDC_LOG_SUFFIX = "_scylla_cdc_log" ;
38+ private static final Splitter SPACE_SPLITTER = Splitter .onPattern ("\\ s+" );
39+ private static final Splitter COMMA_SPLITTER = Splitter .onPattern ("," );
3540
3641 final PreparedId preparedId ;
3742
@@ -50,6 +55,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
5055 volatile RetryPolicy retryPolicy ;
5156 volatile ImmutableMap <String , ByteBuffer > outgoingPayload ;
5257 volatile Boolean idempotent ;
58+ volatile boolean skipMetadata ;
5359
5460 private DefaultPreparedStatement (
5561 PreparedId id ,
@@ -66,6 +72,7 @@ private DefaultPreparedStatement(
6672 this .cluster = cluster ;
6773 this .isLWT = isLWT ;
6874 this .partitioner = partitioner ;
75+ this .skipMetadata = this .calculateSkipMetadata ();
6976 }
7077
7178 static DefaultPreparedStatement fromMessage (
@@ -172,6 +179,62 @@ private static Token.Factory partitioner(ColumnDefinitions defs, Cluster cluster
172179 return null ;
173180 }
174181
182+ private boolean calculateSkipMetadata () {
183+ if (cluster .manager .protocolVersion () == ProtocolVersion .V1
184+ || preparedId .resultSetMetadata .variables == null ) {
185+ // CQL1 does not support it.
186+ // If no rows returned there is no reason to send this flag, consequently, no metadata.
187+ return false ;
188+ }
189+
190+ if (preparedId .resultSetMetadata .id != null
191+ && preparedId .resultSetMetadata .id .bytes .length > 0 ) {
192+ // It is CQL 5 or higher.
193+ // Prepared statement invalidation works perfectly no need to disable skip metadata
194+ return true ;
195+ }
196+
197+ switch (cluster .getConfiguration ().getQueryOptions ().getSkipCQL4MetadataResolveMethod ()) {
198+ case ENABLED :
199+ return true ;
200+ case DISABLED :
201+ return false ;
202+ }
203+
204+ if (isWildcardSelect (query )) {
205+ LOGGER .warn (
206+ "Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
207+ + "These issues may lead to broken deserialization or data corruption. "
208+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
209+ + "though this negatively impacts performance. "
210+ + "To avoid this, consider using a targeted select instead. "
211+ + "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
212+ + "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption." ,
213+ query );
214+ return false ;
215+ }
216+ // Disable skipping metadata if results contains udt and
217+ for (ColumnDefinitions .Definition columnDefinition : preparedId .resultSetMetadata .variables ) {
218+ if (containsUDT (columnDefinition .getType ())) {
219+ LOGGER .warn (
220+ "Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
221+ + "These issues may lead to broken deserialization or data corruption. "
222+ + "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
223+ + "though this negatively impacts performance. "
224+ + "To avoid this, consider using a targeted select instead. "
225+ + "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
226+ + "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption." ,
227+ query );
228+ return false ;
229+ }
230+ }
231+ return true ;
232+ }
233+
234+ public boolean isSkipMetadata () {
235+ return skipMetadata ;
236+ }
237+
175238 @ Override
176239 public ColumnDefinitions getVariables () {
177240 return preparedId .boundValuesMetadata .variables ;
@@ -315,4 +378,44 @@ public Boolean isIdempotent() {
315378 public boolean isLWT () {
316379 return isLWT ;
317380 }
381+
382+ private static boolean containsUDT (DataType dataType ) {
383+ if (dataType .isCollection ()) {
384+ for (DataType elementType : dataType .getTypeArguments ()) {
385+ if (containsUDT (elementType )) {
386+ return true ;
387+ }
388+ }
389+ return false ;
390+ }
391+ return dataType instanceof UserType ;
392+ }
393+
394+ private static boolean isWildcardSelect (String query ) {
395+ List <String > chunks = SPACE_SPLITTER .splitToList (query .trim ().toLowerCase ());
396+ if (chunks .size () < 2 ) {
397+ // Weird query, assuming no result expected
398+ return false ;
399+ }
400+
401+ if (!chunks .get (0 ).equals ("select" )) {
402+ // In case if non-select sneaks in, disable skip metadata for it no result expected.
403+ return false ;
404+ }
405+
406+ for (String chunk : chunks ) {
407+ if (chunk .equals ("from" )) {
408+ return false ;
409+ }
410+ if (chunk .equals ("*" )) {
411+ return true ;
412+ }
413+ for (String part : COMMA_SPLITTER .split (chunk )) {
414+ if (part .equals ("*" )) {
415+ return true ;
416+ }
417+ }
418+ }
419+ return false ;
420+ }
318421}
0 commit comments