From 9330d4124f0aed825891b96307f41012908eb5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Tue, 25 Jun 2024 18:14:56 +0200 Subject: [PATCH] DeserializeValue: `default_when_null` attribute support By default, if DB provides null value in serialized data and the corresponding Rust type expects non-null value, deserialization fails. Instead, if `default_when_null` is specified for a field, deserialization yields `Default::default()`. This is important in production: when added a field to a UDT, the existing UDT instances have the new field filled with null, even if the data represented makes no sense to allow nulls. By using `default_when_null`, clients can handle such situation without using `Option` in their Rust struct. --- scylla-macros/src/deserialize/value.rs | 39 ++++++++++++++++++++++++-- scylla/src/macros.rs | 5 ++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/scylla-macros/src/deserialize/value.rs b/scylla-macros/src/deserialize/value.rs index f79b93cddc..4e0fa8b2f9 100644 --- a/scylla-macros/src/deserialize/value.rs +++ b/scylla-macros/src/deserialize/value.rs @@ -56,6 +56,12 @@ struct Field { #[darling(rename = "allow_missing")] default_when_missing: bool, + // If true, then - if this field is present among UDT fields metadata + // but at the same time missing from serialized data or set to null + // - it will be initialized to Default::default(). + #[darling(default)] + default_when_null: bool, + // If set, then deserializes from the UDT field with this particular name // instead of the Rust field name. #[darling(default)] @@ -367,6 +373,7 @@ impl<'sd> DeserializeAssumeOrderGenerator<'sd> { let deserializer = field.deserialize_target(); let constraint_lifetime = self.0.constraint_lifetime(); let default_when_missing = field.default_when_missing; + let default_when_null = field.default_when_null; let skip_name_checks = self.0.attrs.skip_name_checks; let deserialize: syn::Expr = parse_quote! { @@ -380,6 +387,20 @@ impl<'sd> DeserializeAssumeOrderGenerator<'sd> { ))? }; + let maybe_default_deserialize: syn::Expr = if default_when_null { + parse_quote! { + if value.is_none() { + ::std::default::Default::default() + } else { + #deserialize + } + } + } else { + parse_quote! { + #deserialize + } + }; + // Action performed in case of field name mismatch. let name_mismatch: syn::Expr = if default_when_missing { parse_quote! { @@ -406,12 +427,12 @@ impl<'sd> DeserializeAssumeOrderGenerator<'sd> { let maybe_name_check_and_deserialize_or_save: syn::Expr = if skip_name_checks { parse_quote! { - #deserialize + #maybe_default_deserialize } } else { parse_quote! { if #cql_name_literal == cql_field_name { - #deserialize + #maybe_default_deserialize } else { #name_mismatch } @@ -723,6 +744,18 @@ impl<'sd> DeserializeUnorderedGenerator<'sd> { ))? }; + let deserialize_action: syn::Expr = if field.default_when_null { + parse_quote! { + if value.is_some() { + #do_deserialize + } else { + ::std::default::Default::default() + } + } + } else { + do_deserialize + }; + parse_quote! { { assert!( @@ -738,7 +771,7 @@ impl<'sd> DeserializeUnorderedGenerator<'sd> { let value = value.flatten(); #deserialize_field = ::std::option::Option::Some( - #do_deserialize + #deserialize_action ); } } diff --git a/scylla/src/macros.rs b/scylla/src/macros.rs index f8f787f714..bc8a550a4b 100644 --- a/scylla/src/macros.rs +++ b/scylla/src/macros.rs @@ -340,6 +340,11 @@ pub use scylla_cql::macros::SerializeRow; /// If the UDT definition does not contain this field, it will be initialized /// with `Default::default()`. /// +/// `#[scylla(default_when_null)]` +/// +/// If the value of the field received from DB is null, the field will be +/// initialized with `Default::default()`. +/// /// `#[scylla(rename = "field_name")` /// /// By default, the generated implementation will try to match the Rust field