scylladb / scylla-rust-driver

Async CQL driver for Rust, optimized for ScyllaDB!
Apache License 2.0
576 stars 100 forks source link

IntoRow macro #231

Closed Jasperav closed 3 years ago

Jasperav commented 3 years ago

I see there is a derive macro 'FromRow', but no 'IntoRow' (https://github.com/scylladb/scylla-rust-driver/blob/main/scylla-macros/src/lib.rs). Is there an easy to to transform a struct to a row? I would expect this should work with a certain derive macro:


use std::env;
use scylla::{Session, SessionBuilder};
use scylla::cql_to_rust::FromCqlVal; // <-- I get a compile error without this import, although import shouldn't be needed IMO

#[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(Debug, scylla::IntoUserType, scylla::FromUserType)]
    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();
}

But it fails with this error: thread 'main' panicked at 'calledResult::unwrap()on anErrvalue: DbError(Invalid, "Invalid amount of bind variables: expected 2 received 1")', src\main.rs:36:10

psarna commented 3 years ago

Pardon me, I somehow missed this long-standing issue... thanks @Jasperav for pinging it in context of #128. And right, the error message you see here is absolutely expected, because (to_insert,) is a tuple with one element, while you defined two bind markers in the query (... VALUES (?, ?)). The right way of passing the struct as separate parameters should be

    session
        .query("INSERT INTO ks.my_type (k, my) VALUES (?, ?)", to_insert)
        .await
        .unwrap();

... except it of course doesn't work now, because MyType does not implement ValueList trait (which tuple does, that's why it's possible to pass a tuple here). So the fix should be to add a macro which allows generating ValueList trait implementation for a given struct. I'll take a look at it.

psarna commented 3 years ago

For reference, that's how to enable passing this particular struct as a list of bound values:

use scylla::cql_to_rust::FromCqlVal; // <-- I get a compile error without this import, although import shouldn't be needed IMO
use scylla::frame::value::{SerializedResult, SerializedValues, ValueList};
use scylla::{Session, SessionBuilder};
use std::borrow::Cow;
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(Debug, scylla::IntoUserType, scylla::FromUserType)]
    struct MyType {
        k: i32,
        my: Option<String>,
    }

    impl ValueList for MyType {
        fn serialized(&self) -> SerializedResult {
            let mut result = SerializedValues::with_capacity(2);
            result.add_value(&self.k)?;
            result.add_value(&self.my)?;

            Ok(Cow::Owned(result))
        }
    }

    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);
}

Given that example, the next thing to do is to forge it into a derive macro which generates such implementation.