Skip to content

Commit

Permalink
scylla-macros: introduce SerializeRow derive macro
Browse files Browse the repository at this point in the history
Introduce a derive macro which serializes a struct into bind markers of
a statement.

Unlike the previous ValueList, the new macro takes care to match
the struct fields to bind markers/columns by their names.
  • Loading branch information
piodul committed Dec 9, 2023
1 parent 80a59a9 commit 30a69f8
Show file tree
Hide file tree
Showing 7 changed files with 454 additions and 3 deletions.
11 changes: 10 additions & 1 deletion scylla-cql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ pub mod _macro_internal {
};
pub use crate::macros::*;

pub use crate::types::serialize::row::{
BuiltinSerializationError as BuiltinRowSerializationError,
BuiltinSerializationErrorKind as BuiltinRowSerializationErrorKind,
BuiltinTypeCheckError as BuiltinRowTypeCheckError,
BuiltinTypeCheckErrorKind as BuiltinRowTypeCheckErrorKind, RowSerializationContext,
SerializeRow,
};
pub use crate::types::serialize::value::{
BuiltinSerializationError as BuiltinTypeSerializationError,
BuiltinSerializationErrorKind as BuiltinTypeSerializationErrorKind,
Expand All @@ -29,7 +36,9 @@ pub mod _macro_internal {
UdtSerializationErrorKind, UdtTypeCheckErrorKind,
};
pub use crate::types::serialize::writers::WrittenCellProof;
pub use crate::types::serialize::{CellValueBuilder, CellWriter, SerializationError};
pub use crate::types::serialize::{
CellValueBuilder, CellWriter, RowWriter, SerializationError,
};

pub use crate::frame::response::result::ColumnType;
}
64 changes: 64 additions & 0 deletions scylla-cql/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,70 @@ pub use scylla_macros::ValueList;
/// to either the `scylla` or `scylla-cql` crate.
pub use scylla_macros::SerializeCql;

/// Derive macro for the [`SerializeRow`](crate::types::serialize::row::SerializeRow) trait
/// which serializes given Rust structure into bind markers for a CQL statement.
///
/// At the moment, only structs with named fields are supported. The generated
/// implementation of the trait will match the struct fields to bind markers/columns
/// by name automatically.
///
/// Serialization will fail if there are some bind markers/columns in the statement
/// that don't match to any of the Rust struct fields, _or vice versa_.
///
/// In case of failure, either [`BuiltinTypeCheckError`](crate::types::serialize::row::BuiltinTypeCheckError)
/// or [`BuiltinSerializationError`](crate::types::serialize::row::BuiltinSerializationError)
/// will be returned.
///
/// # Example
///
/// A UDT defined like this:
/// Given a table and a query:
///
/// ```notrust
/// CREATE TABLE ks.my_t (a int PRIMARY KEY, b text, c blob);
/// INSERT INTO ks.my_t (a, b, c) VALUES (?, ?, ?);
/// ```
///
/// ...the values for the query can be serialized using the following struct:
///
/// ```rust
/// # use scylla_cql::macros::SerializeRow;
/// #[derive(SerializeRow)]
/// # #[scylla(crate = scylla_cql)]
/// struct MyValues {
/// a: i32,
/// b: Option<String>,
/// c: Vec<u8>,
/// }
/// ```
///
/// # Attributes
///
/// `#[scylla(crate = crate_name)]`
///
/// By default, the code generated by the derive macro will refer to the items
/// defined by the driver (types, traits, etc.) via the `::scylla` path.
/// For example, it will refer to the [`SerializeRow`](crate::types::serialize::row::SerializeRow) trait
/// using the following path:
///
/// ```rust,ignore
/// use ::scylla::_macro_internal::SerializeRow;
/// ```
///
/// Most users will simply add `scylla` to their dependencies, then use
/// the derive macro and the path above will work. However, there are some
/// niche cases where this path will _not_ work:
///
/// - The `scylla` crate is imported under a different name,
/// - The `scylla` crate is _not imported at all_ - the macro actually
/// is defined in the `scylla-macros` crate and the generated code depends
/// on items defined in `scylla-cql`.
///
/// It's not possible to automatically resolve those issues in the procedural
/// macro itself, so in those cases the user must provide an alternative path
/// to either the `scylla` or `scylla-cql` crate.
pub use scylla_macros::SerializeRow;

// Reexports for derive(IntoUserType)
pub use bytes::{BufMut, Bytes, BytesMut};

Expand Down
168 changes: 167 additions & 1 deletion scylla-cql/src/types/serialize/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,12 @@ mod tests {
use crate::frame::value::{MaybeUnset, SerializedValues, ValueList};
use crate::types::serialize::RowWriter;

use super::{RowSerializationContext, SerializeRow};
use super::{
BuiltinSerializationError, BuiltinSerializationErrorKind, BuiltinTypeCheckError,
BuiltinTypeCheckErrorKind, RowSerializationContext, SerializeCql, SerializeRow,
};

use scylla_macros::SerializeRow;

fn col_spec(name: &str, typ: ColumnType) -> ColumnSpec {
ColumnSpec {
Expand Down Expand Up @@ -672,4 +677,165 @@ mod tests {
);
assert_eq!(typed_data, erased_data);
}

fn do_serialize<T: SerializeRow>(t: T, columns: &[ColumnSpec]) -> Vec<u8> {
let ctx = RowSerializationContext { columns };
let mut ret = Vec::new();
let mut builder = RowWriter::new(&mut ret);
t.serialize(&ctx, &mut builder).unwrap();
ret
}

fn col(name: &str, typ: ColumnType) -> ColumnSpec {
ColumnSpec {
table_spec: TableSpec {
ks_name: "ks".to_string(),
table_name: "tbl".to_string(),
},
name: name.to_string(),
typ,
}
}

// Do not remove. It's not used in tests but we keep it here to check that
// we properly ignore warnings about unused variables, unnecessary `mut`s
// etc. that usually pop up when generating code for empty structs.
#[derive(SerializeRow)]
#[scylla(crate = crate)]
struct TestRowWithNoColumns {}

#[derive(SerializeRow, Debug, PartialEq, Eq, Default)]
#[scylla(crate = crate)]
struct TestRowWithColumnSorting {
a: String,
b: i32,
c: Vec<i64>,
}

#[test]
fn test_row_serialization_with_column_sorting_correct_order() {
let spec = [
col("a", ColumnType::Text),
col("b", ColumnType::Int),
col("c", ColumnType::List(Box::new(ColumnType::BigInt))),
];

let reference = do_serialize(("Ala ma kota", 42i32, vec![1i64, 2i64, 3i64]), &spec);
let row = do_serialize(
TestRowWithColumnSorting {
a: "Ala ma kota".to_owned(),
b: 42,
c: vec![1, 2, 3],
},
&spec,
);

assert_eq!(reference, row);
}

#[test]
fn test_row_serialization_with_column_sorting_incorrect_order() {
// The order of two last columns is swapped
let spec = [
col("a", ColumnType::Text),
col("c", ColumnType::List(Box::new(ColumnType::BigInt))),
col("b", ColumnType::Int),
];

let reference = do_serialize(("Ala ma kota", vec![1i64, 2i64, 3i64], 42i32), &spec);
let row = do_serialize(
TestRowWithColumnSorting {
a: "Ala ma kota".to_owned(),
b: 42,
c: vec![1, 2, 3],
},
&spec,
);

assert_eq!(reference, row);
}

#[test]
fn test_row_serialization_failing_type_check() {
let row = TestRowWithColumnSorting::default();
let mut data = Vec::new();
let mut row_writer = RowWriter::new(&mut data);

let spec_without_c = [
col("a", ColumnType::Text),
col("b", ColumnType::Int),
// Missing column c
];

let ctx = RowSerializationContext {
columns: &spec_without_c,
};
let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut row_writer).unwrap_err();
let err = err.0.downcast_ref::<BuiltinTypeCheckError>().unwrap();
assert!(matches!(
err.kind,
BuiltinTypeCheckErrorKind::ColumnMissingForValue { .. }
));

let spec_duplicate_column = [
col("a", ColumnType::Text),
col("b", ColumnType::Int),
col("c", ColumnType::List(Box::new(ColumnType::BigInt))),
// Unexpected last column
col("d", ColumnType::Counter),
];

let ctx = RowSerializationContext {
columns: &spec_duplicate_column,
};
let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut row_writer).unwrap_err();
let err = err.0.downcast_ref::<BuiltinTypeCheckError>().unwrap();
assert!(matches!(
err.kind,
BuiltinTypeCheckErrorKind::MissingValueForColumn { .. }
));

let spec_wrong_type = [
col("a", ColumnType::Text),
col("b", ColumnType::Int),
col("c", ColumnType::TinyInt), // Wrong type
];

let ctx = RowSerializationContext {
columns: &spec_wrong_type,
};
let err = <_ as SerializeRow>::serialize(&row, &ctx, &mut row_writer).unwrap_err();
let err = err.0.downcast_ref::<BuiltinSerializationError>().unwrap();
assert!(matches!(
err.kind,
BuiltinSerializationErrorKind::ColumnSerializationFailed { .. }
));
}

#[derive(SerializeRow)]
#[scylla(crate = crate)]
struct TestRowWithGenerics<'a, T: SerializeCql> {
a: &'a str,
b: T,
}

#[test]
fn test_row_serialization_with_generics() {
// A minimal smoke test just to test that it works.
fn check_with_type<T: SerializeCql + Copy>(typ: ColumnType, t: T) {
let spec = [col("a", ColumnType::Text), col("b", typ)];
let reference = do_serialize(("Ala ma kota", t), &spec);
let row = do_serialize(
TestRowWithGenerics {
a: "Ala ma kota",
b: t,
},
&spec,
);
assert_eq!(reference, row);
}

check_with_type(ColumnType::Int, 123_i32);
check_with_type(ColumnType::Double, 123_f64);
}
}
9 changes: 9 additions & 0 deletions scylla-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ pub fn serialize_cql_derive(tokens_input: TokenStream) -> TokenStream {
}
}

/// See the documentation for this item in the `scylla` crate.
#[proc_macro_derive(SerializeRow, attributes(scylla))]
pub fn serialize_row_derive(tokens_input: TokenStream) -> TokenStream {
match serialize::row::derive_serialize_row(tokens_input) {
Ok(t) => t.into_token_stream().into(),
Err(e) => e.into_compile_error().into(),
}
}

/// #[derive(FromRow)] derives FromRow for struct
/// Works only on simple structs without generics etc
#[proc_macro_derive(FromRow, attributes(scylla_crate))]
Expand Down
1 change: 1 addition & 0 deletions scylla-macros/src/serialize/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub(crate) mod cql;
pub(crate) mod row;
Loading

0 comments on commit 30a69f8

Please sign in to comment.