Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserialization API rework #665

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2f57a6d
treewide: get rid of (most) uses of QueryResult::rows
piodul Feb 1, 2023
1dfaf83
types: make write_bytes_opt more generic
piodul Feb 18, 2023
c9f6d47
treewide: rename QueryResult -> Legacy08QueryResult
piodul Mar 14, 2023
10eceb3
scylla-cql/types: introduce new deserialization traits
piodul Mar 15, 2023
b71eabf
result: introduce RawRows
piodul Mar 15, 2023
0585656
transport: introduce new QueryResult
piodul Mar 13, 2023
c6e0876
scylla-macros: add derive macros for Deserialize{Row,Cql}
piodul Jan 31, 2023
36bf20c
{frame,transport}: propagate frame contents as Bytes
piodul Nov 25, 2022
76cfc29
treewide: propagate RawRows/new QueryResult
piodul Mar 13, 2023
ef6d00d
iterator: rename (Typed)RowIterator to Legacy08(Typed)RowIterator
piodul Mar 13, 2023
7221db6
iterator: introduce poll_next_internal
piodul Feb 28, 2023
8a7233e
iterator: introduce ready_some_ok! macro
piodul Mar 13, 2023
f145a07
iterator: introduce poll_next_page
piodul Mar 13, 2023
8243277
iterator: adjust to the new deserialization framework
piodul Mar 14, 2023
a62c909
treewide: rename Session to Legacy08Session
piodul Mar 15, 2023
ecb041c
session: make generic and introduce "session kind" parameter
piodul Mar 16, 2023
9d0b645
session: move query-related methods to a separate block
piodul Mar 16, 2023
b894ae0
session: re-introduce the Session type as an alias
piodul Mar 16, 2023
6aa9a47
session_builder: rename build->build_legacy and then reintroduce
piodul Mar 16, 2023
26be1cd
session: de-genericise internal query/exec functions
piodul Mar 13, 2023
20f81e1
session: return new QueryResult from internal methods
piodul Mar 13, 2023
fb6be07
session: add interface methods for the new deser API
piodul Mar 13, 2023
1edb236
connection: switch to the new deserialization framework
piodul Mar 14, 2023
bf66b03
caching_session: make generic over session APIs
piodul Mar 14, 2023
0a5ad84
caching_session: modernize tests
piodul Mar 14, 2023
201950d
connection: migrate query_iter to new deserialization framework
piodul Mar 14, 2023
cb18f5d
{session,tracing}: switch to the new deserialization framework for tr…
piodul Mar 17, 2023
8e321af
treewide: switch tests to use the new framework
piodul Mar 14, 2023
fea6c7b
examples: adjust to use the new interface
piodul Mar 14, 2023
46317ea
cluster: un-pub Cluster and make it cloneable
piodul Mar 14, 2023
54fd357
session: add methods for creating compatible session with different API
piodul Mar 14, 2023
1a67b5c
documentation: adjust to the new traits
piodul Mar 30, 2023
46b6e4d
docs: add migration guide
piodul Mar 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scylla-cql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub use crate::frame::types::Consistency;

#[doc(hidden)]
pub mod _macro_internal {
pub use crate::frame;
pub use crate::frame::response::cql_to_rust::{
FromCqlVal, FromCqlValError, FromRow, FromRowError,
};
Expand All @@ -20,4 +21,5 @@ pub mod _macro_internal {
SerializedResult, SerializedValues, Value, ValueList, ValueTooBig,
};
pub use crate::macros::*;
pub use crate::types;
}
161 changes: 161 additions & 0 deletions scylla-cql/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,167 @@ pub use scylla_macros::IntoUserType;
/// #[derive(ValueList)] allows to pass struct as a list of values for a query
pub use scylla_macros::ValueList;

/// Derive macro for the `DeserializeCql` trait that generates an implementation
/// which deserializes a User Defined Type with the same layout as the Rust
/// struct.
///
/// At the moment, only structs with named fields are supported.
///
/// This macro properly supports structs with lifetimes, meaning that you can
/// deserialize UDTs with fields that borrow memory from the serialized response.
///
/// # Example
///
/// A UDT defined like this:
///
/// ```notrust
/// CREATE TYPE ks.my_udt (a i32, b text, c blob);
/// ```
///
/// ...can be deserialized using the following struct:
///
/// ```rust
/// # use scylla_cql::macros::DeserializeCql;
/// #[derive(DeserializeCql)]
/// # #[scylla(crate = "scylla_cql")]
/// struct MyUdt<'a> {
/// a: i32,
/// b: Option<String>,
/// c: &'a [u8],
/// }
/// ```
///
/// # Attributes
///
/// The macro supports a number of attributes that customize the generated
/// implementation. Many of the attributes were inspired by procedural macros
/// from `serde` and try to follow the same naming conventions.
///
/// ## Struct attributes
///
/// `#[scylla(crate = "crate_name")]`
///
/// Specify a path to the `scylla` or `scylla-cql` crate to use from the
/// generated code. This attribute should be used if the crate or its API
/// is imported/re-exported under a different name.
wprzytula marked this conversation as resolved.
Show resolved Hide resolved
///
/// `#[scylla(enforce_order)]`
///
/// By default, the generated deserialization code will be insensitive
/// to the UDT field order - when processing a field, it will look it up
/// in the Rust struct with the corresponding field and set it. However,
/// if the UDT field order is known to be the same both in the UDT
/// and the Rust struct, then the `enforce_order` annotation can be used
/// so that a more efficient implementation that does not perform lookups
/// is be generated. The UDT field names will still be checked during the
/// type check phase.
///
/// #[(scylla(no_field_name_verification))]
///
/// This attribute only works when used with `enforce_order`.
///
/// If set, the generated implementation will not verify the UDF field names at
/// all. Because it only works with `enforce_order`, it will deserialize first
/// UDF field into the first struct field, second UDF field into the second
/// struct field and so on. It will still still verify that the UDF field types
/// and struct field types match.
piodul marked this conversation as resolved.
Show resolved Hide resolved
///
/// ## Field attributes
///
/// `#[scylla(skip)]`
///
/// The field will be completely ignored during deserialization and will
/// be initialized with `Default::default()`.
///
/// `#[scylla(default_when_missing)]`
///
/// If the UDT definition does not contain this field, it will be initialized
/// with `Default::default()`. __This attribute has no effect in `enforce_order`
/// mode.__
Comment on lines +90 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make it work with enforce_order?

I imagine that people who use enforce_order for the extra performance boost might need to use it too.
AFAIU the main use of default_when_missing is gracefully handling the ADD FIELD operation on UDTs.
When someone wants to add the field while the app is running they should:

  1. Add a new field to the rust struct, mark it as default_when_missing
  2. Rollout the new updated version of db client
  3. Do the ADD FIELD operation in the database

In such situation it's crucial to be able to use default_when_missing . We shouldn't penalize people who use straight order enforcing by making it impossible for them to handle such changes gracefully.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also if this combination isn't supported I think trying to do both of them at once should generate a compilation error instead of silently ignoring default_when_missing. Ignoring it might lead to some nasty surprises for the users.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, we can support enforce_order + default_when_missing. If a struct field doesn't match the column name, we will initialize that field to Default::default() and will try to initialize the next field with that column.

///
/// `#[scylla(rename = "field_name")`
///
/// By default, the generated implementation will try to match the Rust field
/// to a UDT field with the same name. This attribute allows to match to a
/// UDT field with provided name.
pub use scylla_macros::DeserializeCql;

/// Derive macro for the `DeserializeRow` trait that generates an implementation
/// which deserializes a row with a similar layout to the Rust struct.
///
/// At the moment, only structs with named fields are supported.
///
/// This macro properly supports structs with lifetimes, meaning that you can
/// deserialize columns that borrow memory from the serialized response.
///
/// # Example
///
/// A table defined like this:
///
/// ```notrust
/// CREATE TABLE ks.my_table (a PRIMARY KEY, b text, c blob);
piodul marked this conversation as resolved.
Show resolved Hide resolved
/// ```
///
/// ...can be deserialized using the following struct:
///
/// ```rust
/// # use scylla_cql::macros::DeserializeRow;
/// #[derive(DeserializeRow)]
/// # #[scylla(crate = "scylla_cql")]
/// struct MyUdt<'a> {
piodul marked this conversation as resolved.
Show resolved Hide resolved
/// a: i32,
/// b: Option<String>,
/// c: &'a [u8],
/// }
/// ```
///
/// # Attributes
///
/// The macro supports a number of attributes that customize the generated
/// implementation. Many of the attributes were inspired by procedural macros
/// from `serde` and try to follow the same naming conventions.
///
/// ## Struct attributes
///
/// `#[scylla(crate = "crate_name")]`
///
/// Specify a path to the `scylla` or `scylla-cql` crate to use from the
/// generated code. This attribute should be used if the crate or its API
/// is imported/re-exported under a different name.
///
/// `#[scylla(enforce_order)]`
///
/// By default, the generated deserialization code will be insensitive
/// to the column order - when processing a column, the corresponding Rust field
/// will be looked up and the column will be deserialized based on its type.
/// However, if the column order and the Rust field order is known to be the
/// same, then the `enforce_order` annotation can be used so that a more
/// efficient implementation that does not perform lookups is be generated.
/// The generated code will still check that the column and field names match.
///
/// #[(scylla(no_field_name_verification))]
///
/// This attribute only works when used with `enforce_order`.
///
/// If set, the generated implementation will not verify the column names at
/// all. Because it only works with `enforce_order`, it will deserialize first
/// column into the first field, second column into the second field and so on.
/// It will still still verify that the column types and field types match.
///
/// ## Field attributes
///
/// `#[scylla(skip)]`
///
/// The field will be completely ignored during deserialization and will
/// be initialized with `Default::default()`.
///
/// `#[scylla(rename = "field_name")`
///
/// By default, the generated implementation will try to match the Rust field
/// to a column with the same name. This attribute allows to match to a column
/// with provided name.
wprzytula marked this conversation as resolved.
Show resolved Hide resolved
pub use scylla_macros::DeserializeRow;

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

Expand Down
122 changes: 122 additions & 0 deletions scylla-cql/src/types/deserialize/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ impl_tuple_multiple!(
#[cfg(test)]
mod tests {
use bytes::Bytes;
use scylla_macros::DeserializeRow;

use crate::frame::frame_errors::ParseError;
use crate::frame::response::result::{ColumnSpec, ColumnType, TableSpec};
Expand Down Expand Up @@ -258,6 +259,127 @@ mod tests {
assert!(iter.next().is_none());
}

#[test]
fn test_struct_deserialization_loose_ordering() {
#[derive(DeserializeRow, PartialEq, Eq, Debug)]
#[scylla(crate = "crate")]
struct MyRow<'a> {
a: &'a str,
b: Option<i32>,
#[scylla(skip)]
c: String,
}
wprzytula marked this conversation as resolved.
Show resolved Hide resolved

// Original order of columns
let specs = &[spec("a", ColumnType::Text), spec("b", ColumnType::Int)];
let byts = serialize_cells([val_str("abc"), val_int(123)]);
let row = deserialize::<MyRow<'_>>(specs, &byts).unwrap();
assert_eq!(
row,
MyRow {
a: "abc",
b: Some(123),
c: String::new(),
}
);

// Different order of columns - should still work
let specs = &[spec("b", ColumnType::Int), spec("a", ColumnType::Text)];
let byts = serialize_cells([val_int(123), val_str("abc")]);
let row = deserialize::<MyRow<'_>>(specs, &byts).unwrap();
assert_eq!(
row,
MyRow {
a: "abc",
b: Some(123),
c: String::new(),
}
);

// Missing column
let specs = &[spec("a", ColumnType::Text)];
MyRow::type_check(specs).unwrap_err();

// Wrong column type
let specs = &[spec("a", ColumnType::Int), spec("b", ColumnType::Int)];
MyRow::type_check(specs).unwrap_err();
}

#[test]
fn test_struct_deserialization_strict_ordering() {
#[derive(DeserializeRow, PartialEq, Eq, Debug)]
#[scylla(crate = "crate", enforce_order)]
struct MyRow<'a> {
a: &'a str,
b: Option<i32>,
#[scylla(skip)]
c: String,
}

// Correct order of columns
let specs = &[spec("a", ColumnType::Text), spec("b", ColumnType::Int)];
let byts = serialize_cells([val_str("abc"), val_int(123)]);
let row = deserialize::<MyRow<'_>>(specs, &byts).unwrap();
assert_eq!(
row,
MyRow {
a: "abc",
b: Some(123),
c: String::new(),
}
);

// Wrong order of columns
let specs = &[spec("b", ColumnType::Int), spec("a", ColumnType::Text)];
MyRow::type_check(specs).unwrap_err();

// Missing column
let specs = &[spec("a", ColumnType::Text)];
MyRow::type_check(specs).unwrap_err();

// Wrong column type
let specs = &[spec("a", ColumnType::Int), spec("b", ColumnType::Int)];
MyRow::type_check(specs).unwrap_err();
}

#[test]
fn test_struct_deserialization_no_name_check() {
#[derive(DeserializeRow, PartialEq, Eq, Debug)]
#[scylla(crate = "crate", enforce_order, no_field_name_verification)]
struct MyRow<'a> {
a: &'a str,
b: Option<i32>,
#[scylla(skip)]
c: String,
}

// Correct order of columns
let specs = &[spec("a", ColumnType::Text), spec("b", ColumnType::Int)];
let byts = serialize_cells([val_str("abc"), val_int(123)]);
let row = deserialize::<MyRow<'_>>(specs, &byts).unwrap();
assert_eq!(
row,
MyRow {
a: "abc",
b: Some(123),
c: String::new(),
}
);

// Correct order of columns, but different names - should still succeed
let specs = &[spec("z", ColumnType::Text), spec("x", ColumnType::Int)];
let byts = serialize_cells([val_str("abc"), val_int(123)]);
let row = deserialize::<MyRow<'_>>(specs, &byts).unwrap();
assert_eq!(
row,
MyRow {
a: "abc",
b: Some(123),
c: String::new(),
}
);
}

fn val_int(i: i32) -> Option<Vec<u8>> {
Some(i.to_be_bytes().to_vec())
}
Expand Down
Loading