Repository: scylladb/scylla-rust-driver
Pull request #324 (2021-10-28 21:11:20)

Fix `CqlValue`'s tuple representation
## Description

It turnes out, that Scylla/Cassandra allows for `NULL` values as tuple elements. Our declaration of `CqlValue` (`Vec<CqlValue>`) lacks support for this, so any attempts to parse a response that contains tuple with `NULL` element result in error.

This PR changes tuple's representation to `Vec<Option<CqlValue>>`.

## Pre-review checklist

- [x] I have split my patch into logically separate commits.
- [x] All commit messages clearly explain what they change and why.
- [x] I added relevant tests for new features and bug fixes.
- [x] All commits compile, pass static checks and pass test.
- [x] PR description sums up the changes and reasons why they should be introduced.
- [x] I added appropriate `Fixes:` annotations to PR description.

Fixes: #321


Patch:
diff --git a/scylla/src/frame/response/cql_to_rust.rs b/scylla/src/frame/response/cql_to_rust.rs
index 788dd6ec..1c6e3a3f 100644
--- a/scylla/src/frame/response/cql_to_rust.rs
+++ b/scylla/src/frame/response/cql_to_rust.rs
@@ -186,7 +186,7 @@ macro_rules! impl_tuple_from_cql {
     ( $($Ti:tt),+ ) => {
         impl<$($Ti),+> FromCqlVal<CqlValue> for ($($Ti,)+)
         where
-            $($Ti: FromCqlVal<CqlValue>),+
+            $($Ti: FromCqlVal<Option<CqlValue>>),+
         {
             fn from_cql(cql_val: CqlValue) -> Result<Self, FromCqlValError> {
                 let tuple_fields = match cql_val {
diff --git a/scylla/src/frame/response/result.rs b/scylla/src/frame/response/result.rs
index 6cbbe89c..beac805c 100644
--- a/scylla/src/frame/response/result.rs
+++ b/scylla/src/frame/response/result.rs
@@ -106,7 +106,7 @@ pub enum CqlValue {
     /// Nanoseconds since midnight
     Time(Duration),
     Timeuuid(Uuid),
-    Tuple(Vec<CqlValue>),
+    Tuple(Vec<Option<CqlValue>>),
     Uuid(Uuid),
     Varint(BigInt),
 }
@@ -743,9 +743,12 @@ fn deser_cql_value(typ: &ColumnType, buf: &mut &[u8]) -> StdResult<CqlValue, Par
         Tuple(type_names) => {
             let mut res = Vec::with_capacity(type_names.len());
             for type_name in type_names {
-                let mut b = types::read_bytes(buf)?;
-                res.push(deser_cql_value(type_name, &mut b)?);
+                match types::read_bytes_opt(buf)? {
+                    Some(mut b) => res.push(Some(deser_cql_value(type_name, &mut b)?)),
+                    None => res.push(None),
+                };
             }
+
             CqlValue::Tuple(res)
         }
     })


Test patch:
diff --git a/scylla/src/frame/cql_collections_test.rs b/scylla/src/frame/cql_collections_test.rs
index 3014b7fd..0968bda9 100644
--- a/scylla/src/frame/cql_collections_test.rs
+++ b/scylla/src/frame/cql_collections_test.rs
@@ -162,7 +162,10 @@ async fn test_cql_collections() {
         .await.unwrap();
 
     let duration1 = (3, "hours");
-    let duration2 = CqlValue::Tuple(vec![CqlValue::Int(2), CqlValue::Text("minutes".into())]);
+    let duration2 = CqlValue::Tuple(vec![
+        Some(CqlValue::Int(2)),
+        Some(CqlValue::Text("minutes".into())),
+    ]);
     session
         .query(
             "INSERT INTO ks.durations (event, duration) VALUES ('ev1', ?)",
@@ -177,14 +180,21 @@ async fn test_cql_collections() {
         )
         .await
         .unwrap();
+    session
+        .query(
+            "INSERT INTO ks.durations (event, duration) VALUES ('ev3', (4, null))",
+            &[],
+        )
+        .await
+        .unwrap();
 
-    let mut received_elements: Vec<(i32, String)> = session
+    let mut received_elements: Vec<(i32, Option<String>)> = session
         .query("SELECT duration FROM ks.durations", &[])
         .await
         .unwrap()
         .rows
         .unwrap()
-        .into_typed::<((i32, String),)>()
+        .into_typed::<((i32, Option<String>),)>()
         .map(Result::unwrap)
         .map(|(x,)| x)
         .collect();
@@ -192,6 +202,10 @@ async fn test_cql_collections() {
 
     assert_eq!(
         received_elements,
-        vec![(2, "minutes".into()), (3, "hours".into())]
+        vec![
+            (2, Some("minutes".into())),
+            (3, Some("hours".into())),
+            (4, None)
+        ]
     );
 }