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 Oct 20, 2023
1 parent 4d35a3c commit d076162
Show file tree
Hide file tree
Showing 7 changed files with 547 additions and 1 deletion.
4 changes: 4 additions & 0 deletions scylla-cql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub mod _macro_internal {
};
pub use crate::macros::*;

pub use crate::types::serialize::row::{
RowSerializationContext, RowSerializationError, RowSerializationErrorKind,
RowTypeCheckError, RowTypeCheckErrorKind, SerializeRow,
};
pub use crate::types::serialize::value::{
SerializeCql, UdtSerializationError, UdtSerializationErrorKind, UdtTypeCheckError,
UdtTypeCheckErrorKind,
Expand Down
62 changes: 62 additions & 0 deletions scylla-cql/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,68 @@ 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 [`RowTypeCheckError`](crate::types::serialize::row::RowTypeCheckError)
/// or [`RowSerializationError`](crate::types::serialize::row::RowSerializationError)
/// will be returned.
///
/// # Example
///
/// A UDT defined like this:
///
/// ```notrust
/// CREATE TYPE ks.my_udt (a int, b text, c blob);
/// ```
///
/// ...can be serialized using the following struct:
///
/// ```rust
/// # use scylla_cql::macros::SerializeRow;
/// #[derive(SerializeRow)]
/// # #[scylla(crate = scylla_cql)]
/// struct MyUdt {
/// 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
221 changes: 221 additions & 0 deletions scylla-cql/src/types/serialize/row.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use std::fmt::Display;
use std::sync::Arc;

use thiserror::Error;

use crate::frame::response::result::ColumnSpec;
use crate::frame::value::ValueList;

Expand Down Expand Up @@ -47,3 +50,221 @@ impl<T: ValueList> SerializeRow for T {
.map_err(|err| Arc::new(err) as SerializationError)
}
}

/// Returned by the code generated by [`SerializeRow`] macro when the types
/// of the bind markers expected by the database do not match the expectations
/// of a Rust struct.
///
/// Returned by the [`SerializeRow::preliminary_type_check`] method from
/// the trait implementation generated by the macro.
#[derive(Debug, Error)]
#[error("Failed to type check Rust struct {rust_name} as a CQL row: {kind}")]
pub struct RowTypeCheckError {
/// Name of the Rust structure that was being serialized.
pub rust_name: String,

/// Detailed infomation about why type checking of the row failed.
pub kind: RowTypeCheckErrorKind,
}

/// Detailed information about why type checking of the row failed.
#[derive(Debug)]
#[non_exhaustive]
pub enum RowTypeCheckErrorKind {
/// There is a Rust struct field that must be serialized but does not
/// match against any of the bind markers.
MissingColumn { column_name: String },

/// There is a bind marker with a column name that does not match against
/// any of the Rust struct fields.
UnexpectedColumn { column_name: String },

/// Failed to type check one of the fields against the expected type
/// of the corresponding bind marker.
ColumnTypeCheckFailed {
column_name: String,
err: SerializationError,
},
}

impl Display for RowTypeCheckErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RowTypeCheckErrorKind::MissingColumn { column_name } => write!(
f,
"no bind marker or column named {column_name} in the statement spec"
),
RowTypeCheckErrorKind::UnexpectedColumn { column_name } => write!(
f,
"the bind marker or column {column_name} does not correspond to any of the rust struct fields"
),
RowTypeCheckErrorKind::ColumnTypeCheckFailed { column_name, err } => {
write!(f, "the bind marker {column_name} failed to type check: {err}")
}
}
}
}

/// Returned by the code generated by [`SerializeRow`] macro when a Rust struct
/// fails to be serialized as bind markers for a statement.
///
/// Returned by the [`SerializeRow::serialize`] method from the trait
/// implementation generated by the macro.
#[derive(Debug, Error)]
#[error("Failed to type check Rust struct {rust_name} as a CQL row: {kind}")]
pub struct RowSerializationError {
/// Name of the Rust structure that was being serialized.
pub rust_name: String,

/// Detailed infomation about why serialization failed.
pub kind: RowSerializationErrorKind,
}

/// Detailed information about why serialization of the row failed.
#[derive(Debug)]
#[non_exhaustive]
pub enum RowSerializationErrorKind {
/// One of the bind markers or columns failed to be serialized.
ColumnSerializationFailed {
column_name: String,
err: SerializationError,
},
}

impl Display for RowSerializationErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RowSerializationErrorKind::ColumnSerializationFailed { column_name, err } => {
write!(
f,
"the bind marker or column {column_name} failed to serialize: {err}"
)
}
}
}
}

#[cfg(test)]
mod tests {
use scylla_macros::SerializeRow;

use super::{RowSerializationContext, RowTypeCheckError, RowTypeCheckErrorKind, SerializeRow};
use crate::frame::response::result::{ColumnSpec, ColumnType, TableSpec};

fn do_serialize<T: SerializeRow>(t: T, columns: &[ColumnSpec]) -> Vec<u8> {
let ctx = RowSerializationContext { columns };
T::preliminary_type_check(&ctx).unwrap();
let mut ret = Vec::new();
t.serialize(&ctx, &mut ret).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)]
#[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 spec_without_c = [
col("a", ColumnType::Text),
col("b", ColumnType::Int),
// Missing column c
];

let ctx = RowSerializationContext {
columns: &spec_without_c,
};
let err = TestRowWithColumnSorting::preliminary_type_check(&ctx).unwrap_err();
let err = err.downcast_ref::<RowTypeCheckError>().unwrap();
assert!(matches!(
err.kind,
RowTypeCheckErrorKind::MissingColumn { .. }
));

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 = TestRowWithColumnSorting::preliminary_type_check(&ctx).unwrap_err();
let err = err.downcast_ref::<RowTypeCheckError>().unwrap();
assert!(matches!(
err.kind,
RowTypeCheckErrorKind::UnexpectedColumn { .. }
));

// TODO: Test case for mismatched field types
// Can't do it without proper SerializeRaw implementation of field types
}
}
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 d076162

Please sign in to comment.