Skip to content

Commit

Permalink
Add flatten attribute to derive SerializeRow
Browse files Browse the repository at this point in the history
Currently only the `match_by_name` flavor is supported. All the needed
structs/traits to make this work are marked as `#[doc(hidden)]` to not
increase the public API surface. Effort was done to not change any of
the existing API.
  • Loading branch information
nrxus committed Dec 8, 2024
1 parent 92fdd71 commit d60d006
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 82 deletions.
5 changes: 3 additions & 2 deletions scylla-cql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ pub mod _macro_internal {
BuiltinSerializationError as BuiltinRowSerializationError,
BuiltinSerializationErrorKind as BuiltinRowSerializationErrorKind,
BuiltinTypeCheckError as BuiltinRowTypeCheckError,
BuiltinTypeCheckErrorKind as BuiltinRowTypeCheckErrorKind, RowSerializationContext,
SerializeRow,
BuiltinTypeCheckErrorKind as BuiltinRowTypeCheckErrorKind,
FieldSerializationStatus as FieldRowSerializationStatus, PartialSerializeRowByName,
RowSerializationContext, SerializeRow, SerializeRowByName,
};
pub use crate::types::serialize::value::{
BuiltinSerializationError as BuiltinTypeSerializationError,
Expand Down
142 changes: 142 additions & 0 deletions scylla-cql/src/types/serialize/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,96 @@ pub trait SerializeRow {
fn is_empty(&self) -> bool;
}

/// How to serialize a row column-by-column
///
/// For now this trait is an implementation detail of `#[derive(SerializeRow)]` when
/// serializing by name
#[doc(hidden)]
pub trait PartialSerializeRowByName {
/// Serializes a single column in the row according to the information in the
/// given context
///
/// It returns whether the column finished the serialization of the struct, did
/// it partially, none of at all, or errored
fn serialize_field(
&mut self,
spec: &ColumnSpec,
writer: &mut RowWriter<'_>,
) -> Result<FieldSerializationStatus, SerializationError>;

/// Checks if there are any missing columns to finish the serialization
fn check_missing(self) -> Result<(), SerializationError>;
}

/// Represents a set of values that can be sent along a CQL statement when serializing by name
///
/// For now this trait is an implementation detail of `#[derive(SerializeRow)]` when
/// serializing by name
#[doc(hidden)]
pub trait SerializeRowByName {
/// A type that can handle serialization of this struct column-by-column
type Partial<'d>: PartialSerializeRowByName
where
Self: 'd;

/// Returns a type that can serialize this row "column-by-column"
fn partial(&self) -> Self::Partial<'_>;

/// Serializes all the fields/columns by name
///
/// Auto-implemented -- do not override
fn serialize_by_name(
&self,
ctx: &RowSerializationContext,
writer: &mut RowWriter<'_>,
) -> Result<(), SerializationError> {
let mut partial = self.partial();

for spec in ctx.columns() {
let serialized = partial.serialize_field(spec, writer).map_err(|err| {
SerializationError::new(BuiltinSerializationError {
rust_name: std::any::type_name::<Self>(),
kind: BuiltinSerializationErrorKind::ColumnSerializationFailed {
name: spec.name().to_owned(),
err,
},
})
})?;

if matches!(serialized, FieldSerializationStatus::NotUsed) {
return Err(SerializationError::new(BuiltinTypeCheckError {
rust_name: std::any::type_name::<Self>(),
kind: BuiltinTypeCheckErrorKind::NoColumnWithName {
name: spec.name().to_owned(),
},
}));
}
}

partial.check_missing()?;

Ok(())
}
}

/// Whether a field used a column to finish its serialization or not
///
/// Used when serializing by name as a single column may not have finished a rust
/// field in the case of a flattened struct
///
/// For now this enum is an implementation detail of `#[derive(SerializeRow)]` when
/// serializing by name
#[derive(Debug)]
#[doc(hidden)]
pub enum FieldSerializationStatus {
/// The column finished the serialization for this field
Done,
/// The column was used but there are other fields not yet serialized
NotDone,
/// The column did not belong to this field
NotUsed,
}

macro_rules! fallback_impl_contents {
() => {
fn serialize(
Expand Down Expand Up @@ -1634,4 +1724,56 @@ pub(crate) mod tests {

assert_eq!(reference, row);
}

#[test]
fn test_row_serialization_nested_structs() {
#[derive(SerializeRow, Debug)]
#[scylla(crate = crate)]
struct InnerColumnsOne {
x: i32,
y: f64,
}

#[derive(SerializeRow, Debug)]
#[scylla(crate = crate)]
struct InnerColumnsTwo {
z: bool,
}

#[derive(SerializeRow, Debug)]
#[scylla(crate = crate)]
struct OuterColumns {
#[scylla(flatten)]
inner_one: InnerColumnsOne,
a: String,
#[scylla(flatten)]
inner_two: InnerColumnsTwo,
}

let spec = [
col("a", ColumnType::Text),
col("x", ColumnType::Int),
col("z", ColumnType::Boolean),
col("y", ColumnType::Double),
];

let value = OuterColumns {
inner_one: InnerColumnsOne { x: 5, y: 1.0 },
a: "something".to_owned(),
inner_two: InnerColumnsTwo { z: true },
};

let reference = do_serialize(
(
&value.a,
&value.inner_one.x,
&value.inner_two.z,
&value.inner_one.y,
),
&spec,
);
let row = do_serialize(value, &spec);

assert_eq!(reference, row);
}
}
Loading

0 comments on commit d60d006

Please sign in to comment.