Repository: scylladb/scylla-rust-driver
Pull request #294 (2021-09-20 09:35:55)

Add ValueList derive macro
This commit adds a `ValueList` derive macro, which allows passing structs as value lists for queries. Each field in the struct will be used as a value, in the same order as the struct's definition. Example usage:

```rust
    #[derive(scylla::ValueList)]
    struct MyType {
        k: i32,
        my: Option<String>,
    }

    let to_insert = MyType {
        k: 17,
        my: Some("Some string".to_string()),
    };

    session
        .query("INSERT INTO ks.my_type (k, my) VALUES (?, ?)", to_insert)
        .await
        .unwrap();
```

Fixes https://github.com/scylladb/scylla-rust-driver/issues/231

## Pre-review checklist

<!--
    Make sure you took care of the issues on the list.
    Put 'x' into those boxes which apply.
    You can also create the PR now and click on all relevant checkboxes.
    See CONTRIBUTING.md for more details.
-->

- [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.



Patch:
diff --git a/docs/source/queries/values.md b/docs/source/queries/values.md
index b488e607..0818d759 100644
--- a/docs/source/queries/values.md
+++ b/docs/source/queries/values.md
@@ -6,12 +6,13 @@ Each `?` in query text will be filled with the matching value.
 > **Never** pass values by adding strings, this could lead to [SQL Injection](https://en.wikipedia.org/wiki/SQL_injection)
 
 Each list of values to send in a query must implement the trait `ValueList`.  
-By default this can be a slice `&[]` or a tuple `()` (max 16 elements) of values to send.
+By default this can be a slice `&[]`, a tuple `()` (max 16 elements) of values to send,
+or a custom struct which derives from `ValueList`.
 
 A few examples:
 ```rust
 # extern crate scylla;
-# use scylla::Session;
+# use scylla::{Session, ValueList};
 # use std::error::Error;
 # async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
 // Empty slice means that there are no values to send
@@ -30,6 +31,23 @@ session
     .query("INSERT INTO ks.tab (a, b) VALUES(?, ?)", (2_i32, "Some text"))
     .await?;
 
+// Sending an integer and a string using a named struct.
+// The values will be passed in the order from the struct definition
+#[derive(ValueList)]
+struct IntString {
+    first_col: i32,
+    second_col: String,
+}
+
+let int_string = IntString {
+    first_col: 42_i32,
+    second_col: "hello".to_owned(),
+};
+
+session
+    .query("INSERT INTO ks.tab (a, b) VALUES(?, ?)", int_string)
+    .await?;
+
 // Sending a single value as a tuple requires a trailing coma (Rust syntax):
 session.query("INSERT INTO ks.tab (a) VALUES(?)", (2_i32,)).await?;
 
@@ -90,4 +108,4 @@ session
 See the [issue](https://issues.apache.org/jira/browse/CASSANDRA-7304) for more information about `Unset`
 
 ### Other data types
-See [Data Types](../data-types/data-types.md) for instructions on sending other data types
\ No newline at end of file
+See [Data Types](../data-types/data-types.md) for instructions on sending other data types
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index 7aab1fb8..da8653f2 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -79,3 +79,7 @@ path = "speculative-execution.rs"
 [[example]]
 name = "get_by_name"
 path = "get_by_name.rs"
+
+[[example]]
+name = "value_list"
+path = "value_list.rs"
diff --git a/examples/value_list.rs b/examples/value_list.rs
new file mode 100644
index 00000000..084b6912
--- /dev/null
+++ b/examples/value_list.rs
@@ -0,0 +1,44 @@
+use scylla::{Session, SessionBuilder};
+use std::env;
+
+#[tokio::main]
+async fn main() {
+    let uri = env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9042".to_string());
+
+    println!("Connecting to {} ...", uri);
+
+    let session: Session = SessionBuilder::new().known_node(uri).build().await.unwrap();
+
+    session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1}", &[]).await.unwrap();
+
+    session
+        .query(
+            "CREATE TABLE IF NOT EXISTS ks.my_type (k int, my text, primary key (k))",
+            &[],
+        )
+        .await
+        .unwrap();
+
+    #[derive(scylla::ValueList)]
+    struct MyType {
+        k: i32,
+        my: Option<String>,
+    }
+
+    let to_insert = MyType {
+        k: 17,
+        my: Some("Some string".to_string()),
+    };
+
+    session
+        .query("INSERT INTO ks.my_type (k, my) VALUES (?, ?)", to_insert)
+        .await
+        .unwrap();
+
+    let q = session
+        .query("SELECT * FROM ks.my_type", &[])
+        .await
+        .unwrap();
+
+    println!("Q: {:?}", q.rows);
+}
diff --git a/scylla-macros/Cargo.toml b/scylla-macros/Cargo.toml
index 72c7cf18..a0dd98dd 100644
--- a/scylla-macros/Cargo.toml
+++ b/scylla-macros/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "scylla-macros"
-version = "0.1.0"
+version = "0.1.1"
 edition = "2018"
 description = "proc macros for scylla async CQL driver"
 repository = "https://github.com/scylladb/scylla-rust-driver"
diff --git a/scylla-macros/src/lib.rs b/scylla-macros/src/lib.rs
index 27a25ebb..d99a2588 100644
--- a/scylla-macros/src/lib.rs
+++ b/scylla-macros/src/lib.rs
@@ -4,6 +4,7 @@ mod from_row;
 mod from_user_type;
 mod into_user_type;
 mod parser;
+mod value_list;
 
 /// #[derive(FromRow)] derives FromRow for struct
 /// Works only on simple structs without generics etc
@@ -25,3 +26,10 @@ pub fn from_user_type_derive(tokens_input: TokenStream) -> TokenStream {
 pub fn into_user_type_derive(tokens_input: TokenStream) -> TokenStream {
     into_user_type::into_user_type_derive(tokens_input)
 }
+
+/// #[derive(ValueList)] derives ValueList for struct
+/// Works only on simple structs without generics etc
+#[proc_macro_derive(ValueList)]
+pub fn value_list_derive(tokens_input: TokenStream) -> TokenStream {
+    value_list::value_list_derive(tokens_input)
+}
diff --git a/scylla-macros/src/value_list.rs b/scylla-macros/src/value_list.rs
new file mode 100644
index 00000000..8595ba10
--- /dev/null
+++ b/scylla-macros/src/value_list.rs
@@ -0,0 +1,27 @@
+use proc_macro::TokenStream;
+use quote::quote;
+
+/// #[derive(ValueList)] allows to parse a struct as a list of values,
+/// which can be fed to the query directly.
+/// Works only on simple structs without generics etc
+pub fn value_list_derive(tokens_input: TokenStream) -> TokenStream {
+    let (struct_name, struct_fields) =
+        crate::parser::parse_struct_with_named_fields(tokens_input, "ValueList");
+
+    let values_len = struct_fields.named.len();
+    let field_name = struct_fields.named.iter().map(|field| &field.ident);
+    let generated = quote! {
+        impl scylla::frame::value::ValueList for #struct_name {
+            fn serialized(&self) -> scylla::frame::value::SerializedResult {
+                let mut result = scylla::frame::value::SerializedValues::with_capacity(#values_len);
+                #(
+                    result.add_value(&self.#field_name)?;
+                )*
+
+                Ok(std::borrow::Cow::Owned(result))
+            }
+        }
+    };
+
+    TokenStream::from(generated)
+}
diff --git a/scylla/Cargo.toml b/scylla/Cargo.toml
index 5cc4ec1c..2d6cfcd0 100644
--- a/scylla/Cargo.toml
+++ b/scylla/Cargo.toml
@@ -14,7 +14,7 @@ defaults = []
 ssl = ["tokio-openssl", "openssl"]
 
 [dependencies]
-scylla-macros = { version = "0.1.0", path = "../scylla-macros"}
+scylla-macros = { version = "0.1.1", path = "../scylla-macros"}
 byteorder = "1.3.4"
 bytes = "1.0.1"
 futures = "0.3.6"
diff --git a/scylla/src/macros.rs b/scylla/src/macros.rs
index 783e8b54..d8d3d0b2 100644
--- a/scylla/src/macros.rs
+++ b/scylla/src/macros.rs
@@ -10,5 +10,8 @@ pub use scylla_macros::FromUserType;
 /// Works only on simple structs without generics etc
 pub use scylla_macros::IntoUserType;
 
+/// #[derive(ValueList)] allows to pass struct as a list of values for a query
+pub use scylla_macros::ValueList;
+
 // Reexports for derive(IntoUserType)
 pub use bytes::{BufMut, Bytes, BytesMut};


Test patch:
diff --git a/scylla/src/transport/session_test.rs b/scylla/src/transport/session_test.rs
index cb9b1715..b3264305 100644
--- a/scylla/src/transport/session_test.rs
+++ b/scylla/src/transport/session_test.rs
@@ -1,4 +1,6 @@
+use crate as scylla;
 use crate::batch::Batch;
+use crate::cql_to_rust::FromRow;
 use crate::frame::response::result::Row;
 use crate::frame::value::ValueList;
 use crate::query::Query;
@@ -229,6 +231,43 @@ async fn test_prepared_statement() {
         assert!(e.is_none());
         assert_eq!((a, b, c, d), (17, 16, &String::from("I'm prepared!!!"), 7))
     }
+    // Check that ValueList macro works
+    {
+        #[derive(scylla::ValueList, scylla::FromRow, PartialEq, Debug, Clone)]
+        struct ComplexPk {
+            a: i32,
+            b: i32,
+            c: Option<String>,
+            d: i32,
+            e: i32,
+        }
+        let input: ComplexPk = ComplexPk {
+            a: 9,
+            b: 8,
+            c: Some("seven".into()),
+            d: 6,
+            e: 5,
+        };
+        session
+            .query(
+                "INSERT INTO ks.complex_pk (a,b,c,d,e) VALUES (?,?,?,?,?)",
+                input.clone(),
+            )
+            .await
+            .unwrap();
+        let mut rs = session
+            .query(
+                "SELECT * FROM ks.complex_pk WHERE a = 9 and b = 8 and c = 'seven'",
+                &[],
+            )
+            .await
+            .unwrap()
+            .rows
+            .unwrap()
+            .into_typed::<ComplexPk>();
+        let output = rs.next().unwrap().unwrap();
+        assert_eq!(input, output)
+    }
 }
 
 #[tokio::test]