Skip to content

Commit ff31720

Browse files
authored
Merge pull request #1453 from wprzytula/accept-shorter-encoded-tuples
deserialize/value: Accept shorter encoded tuples
2 parents 0d56f2b + 79ac7b1 commit ff31720

File tree

3 files changed

+135
-13
lines changed

3 files changed

+135
-13
lines changed

scylla-cql/src/deserialize/value.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,17 +1340,26 @@ macro_rules! impl_tuple {
13401340
let mut v = ensure_not_null_frame_slice::<Self>(typ, v)?;
13411341
let ret = (
13421342
$(
1343-
v.read_cql_bytes()
1344-
.map_err(|err| DeserializationError::new(err))
1345-
.and_then(|cql_bytes| <$Ti>::deserialize($idf, cql_bytes))
1346-
.map_err(|err| mk_deser_err::<Self>(
1347-
typ,
1348-
TupleDeserializationErrorKind::FieldDeserializationFailed {
1349-
position: $idx,
1350-
err,
1351-
}
1352-
)
1353-
)?,
1343+
{
1344+
let cql_bytes = if v.is_empty() {
1345+
// Special case: if there are no bytes left to read, consider the tuple element as null.
1346+
// This is needed to handle tuples with less elements than expected
1347+
// (see https://github.com/scylladb/scylla-rust-driver/issues/1452 for context).
1348+
None
1349+
} else {
1350+
v.read_cql_bytes().map_err(|err| DeserializationError::new(err))?
1351+
};
1352+
1353+
<$Ti>::deserialize($idf, cql_bytes)
1354+
.map_err(|err| mk_deser_err::<Self>(
1355+
typ,
1356+
TupleDeserializationErrorKind::FieldDeserializationFailed {
1357+
position: $idx,
1358+
err,
1359+
}
1360+
)
1361+
)?
1362+
},
13541363
)*
13551364
);
13561365
Ok(ret)

scylla/tests/integration/types/cql_collections.rs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::utils::{
2-
DeserializeOwnedValue, PerformDDL, create_new_session_builder, setup_tracing,
3-
unique_keyspace_name,
2+
DeserializeOwnedValue, PerformDDL, SerializeValueWithFakeType, create_new_session_builder,
3+
setup_tracing, unique_keyspace_name,
44
};
55
use scylla::cluster::metadata::NativeType;
66
use scylla::deserialize::value::DeserializeValue;
@@ -254,6 +254,95 @@ async fn test_cql_tuple() {
254254
.unwrap();
255255
}
256256

257+
// Cassandra does not support altering column types starting with version 3.0.11 and 3.10.
258+
// See https://stackoverflow.com/a/76926622 for explanation.
259+
#[cfg_attr(cassandra_tests, ignore)]
260+
#[tokio::test]
261+
async fn test_alter_column_add_field_to_tuple() {
262+
setup_tracing();
263+
let session: Session = connect().await;
264+
265+
let table_name: &str = "test_cql_tuple_alter_tab";
266+
create_table(&session, table_name, "tuple<int, int>").await;
267+
268+
let tuple1: (i32, i32) = (1, 2);
269+
session
270+
.query_unpaged(
271+
format!("INSERT INTO {table_name} (p, val) VALUES (0, ?)"),
272+
&(tuple1,),
273+
)
274+
.await
275+
.unwrap();
276+
277+
// Add a field to the tuple. Existing rows will still have 2 fields in the tuple.
278+
session
279+
.query_unpaged(
280+
format!("ALTER TABLE {table_name} ALTER val TYPE tuple<int, int, text>"),
281+
&(),
282+
)
283+
.await
284+
.unwrap();
285+
286+
// Select a tuple - ScyllaDB will send 2-element tuple.
287+
// Driver should return the third element as null.
288+
let selected_value: (Option<i32>, Option<i32>, Option<String>) = session
289+
.query_unpaged(format!("SELECT val FROM {table_name} WHERE p = 0"), ())
290+
.await
291+
.unwrap()
292+
.into_rows_result()
293+
.unwrap()
294+
.single_row::<((Option<i32>, Option<i32>, Option<String>),)>()
295+
.unwrap()
296+
.0;
297+
298+
assert!(selected_value.2.is_none());
299+
300+
session
301+
.ddl(format!("DROP KEYSPACE {}", session.get_keyspace().unwrap()))
302+
.await
303+
.unwrap();
304+
}
305+
306+
#[tokio::test]
307+
async fn test_cql_tuple_db_repr_shorter_than_metadata() {
308+
use ColumnType::*;
309+
use NativeType::*;
310+
311+
setup_tracing();
312+
let session: Session = connect().await;
313+
314+
{
315+
let table_name: &str = "test_cql_shorter_tuple_tab";
316+
create_table(
317+
&session,
318+
table_name,
319+
"tuple<tuple<int, text, int>, int, tuple<int>>",
320+
)
321+
.await;
322+
323+
// We craft a tuple that is shorter in DB representation than in metadata,
324+
// and also contains another nested tuple with the same property.
325+
// In order to do that, we use SerializeValueWithFakeType to lie about the type
326+
// the value is being serialized to, so that tuple serialization logic does not complain.
327+
let inner_tuple: (i32, String) = (1, "Ala".to_owned());
328+
let inner_tuple_ser =
329+
SerializeValueWithFakeType::new(inner_tuple, Tuple(vec![Native(Int), Native(Text)]));
330+
let tuple: (SerializeValueWithFakeType<_>, i32) = (inner_tuple_ser, 2);
331+
let tuple_ser = SerializeValueWithFakeType::new(
332+
tuple,
333+
Tuple(vec![Tuple(vec![Native(Int), Native(Text)]), Native(Int)]),
334+
);
335+
// The expected deserialized tuple has None for the missing elements.
336+
let tuple_deser = ((1, "Ala".to_owned(), None::<i32>), 2, None::<(i32,)>);
337+
insert_and_select(&session, table_name, &tuple_ser, &tuple_deser).await;
338+
}
339+
340+
session
341+
.ddl(format!("DROP KEYSPACE {}", session.get_keyspace().unwrap()))
342+
.await
343+
.unwrap();
344+
}
345+
257346
// TODO: Remove this ignore when vector type is supported in ScyllaDB
258347
#[cfg_attr(not(cassandra_tests), ignore)]
259348
#[tokio::test]

scylla/tests/integration/utils.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use scylla::client::session::Session;
77
use scylla::client::session_builder::{GenericSessionBuilder, SessionBuilderKind};
88
use scylla::cluster::ClusterState;
99
use scylla::cluster::NodeRef;
10+
use scylla::cluster::metadata::ColumnType;
1011
use scylla::deserialize::value::DeserializeValue;
1112
use scylla::errors::{DbError, ExecutionError, RequestAttemptError};
1213
use scylla::policies::load_balancing::{
@@ -16,6 +17,7 @@ use scylla::policies::retry::{RequestInfo, RetryDecision, RetryPolicy, RetrySess
1617
use scylla::response::query_result::QueryResult;
1718
use scylla::routing::Shard;
1819
use scylla::serialize::row::SerializeRow;
20+
use scylla::serialize::value::SerializeValue;
1921
use scylla::statement::prepared::PreparedStatement;
2022
use scylla::statement::unprepared::Statement;
2123
use std::collections::HashMap;
@@ -455,3 +457,25 @@ pub(crate) async fn execute_unprepared_statement_everywhere(
455457
})
456458
.await
457459
}
460+
461+
pub(crate) struct SerializeValueWithFakeType<'typ, T> {
462+
fake_type: ColumnType<'typ>,
463+
value: T,
464+
}
465+
466+
impl<T: SerializeValue> SerializeValue for SerializeValueWithFakeType<'_, T> {
467+
fn serialize<'b>(
468+
&self,
469+
_typ: &ColumnType,
470+
writer: scylla_cql::serialize::CellWriter<'b>,
471+
) -> Result<scylla::serialize::writers::WrittenCellProof<'b>, scylla::errors::SerializationError>
472+
{
473+
<T as SerializeValue>::serialize(&self.value, &self.fake_type, writer)
474+
}
475+
}
476+
477+
impl<'typ, T> SerializeValueWithFakeType<'typ, T> {
478+
pub(crate) fn new(value: T, fake_type: ColumnType<'typ>) -> Self {
479+
Self { fake_type, value }
480+
}
481+
}

0 commit comments

Comments
 (0)