24
24
import static com .datastax .driver .core .ProtocolVersion .V4 ;
25
25
26
26
import com .datastax .driver .core .policies .RetryPolicy ;
27
+ import com .google .common .base .Splitter ;
27
28
import com .google .common .collect .ImmutableMap ;
28
29
import java .nio .ByteBuffer ;
29
30
import java .util .List ;
30
31
import java .util .Map ;
32
+ import org .slf4j .Logger ;
33
+ import org .slf4j .LoggerFactory ;
31
34
32
35
public class DefaultPreparedStatement implements PreparedStatement {
33
-
36
+ private static final Logger LOGGER = LoggerFactory . getLogger ( DefaultPreparedStatement . class );
34
37
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 ("," );
35
40
36
41
final PreparedId preparedId ;
37
42
@@ -50,6 +55,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
50
55
volatile RetryPolicy retryPolicy ;
51
56
volatile ImmutableMap <String , ByteBuffer > outgoingPayload ;
52
57
volatile Boolean idempotent ;
58
+ volatile boolean skipMetadata ;
53
59
54
60
private DefaultPreparedStatement (
55
61
PreparedId id ,
@@ -66,6 +72,7 @@ private DefaultPreparedStatement(
66
72
this .cluster = cluster ;
67
73
this .isLWT = isLWT ;
68
74
this .partitioner = partitioner ;
75
+ this .skipMetadata = this .calculateSkipMetadata ();
69
76
}
70
77
71
78
static DefaultPreparedStatement fromMessage (
@@ -172,6 +179,62 @@ private static Token.Factory partitioner(ColumnDefinitions defs, Cluster cluster
172
179
return null ;
173
180
}
174
181
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
+
175
238
@ Override
176
239
public ColumnDefinitions getVariables () {
177
240
return preparedId .boundValuesMetadata .variables ;
@@ -315,4 +378,44 @@ public Boolean isIdempotent() {
315
378
public boolean isLWT () {
316
379
return isLWT ;
317
380
}
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
+ }
318
421
}
0 commit comments