Skip to content

Commit 74184aa

Browse files
committed
feat: add allow_missing in the struct_field
add allow_missing with macro DeserializeRow, add allow_missing in the struct attributes to handle partial deserialization and make it easier instead of defining on each struct field
1 parent c622e73 commit 74184aa

File tree

7 files changed

+204
-20
lines changed

7 files changed

+204
-20
lines changed

scylla-cql/src/deserialize/row_tests.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ fn test_struct_deserialization_loose_ordering() {
114114
d: i32,
115115
#[scylla(default_when_null)]
116116
e: &'a str,
117+
#[scylla(allow_missing)]
118+
f: &'a str,
117119
}
118120

119121
// Original order of columns
@@ -133,6 +135,7 @@ fn test_struct_deserialization_loose_ordering() {
133135
c: String::new(),
134136
d: 0,
135137
e: "def",
138+
f: "",
136139
}
137140
);
138141

@@ -153,6 +156,7 @@ fn test_struct_deserialization_loose_ordering() {
153156
c: String::new(),
154157
d: 0,
155158
e: "",
159+
f: "",
156160
}
157161
);
158162

@@ -178,6 +182,90 @@ fn test_struct_deserialization_loose_ordering() {
178182
MyRow::type_check(specs).unwrap_err();
179183
}
180184

185+
#[test]
186+
fn test_struct_deserialization_loose_ordering_allow_missing() {
187+
#[derive(DeserializeRow, PartialEq, Eq, Debug)]
188+
#[scylla(crate = "crate")]
189+
#[scylla(allow_missing)]
190+
struct MyRow<'a> {
191+
a: &'a str,
192+
b: Option<i32>,
193+
#[scylla(skip)]
194+
c: String,
195+
#[scylla(default_when_null)]
196+
d: i32,
197+
#[scylla(default_when_null)]
198+
e: &'a str,
199+
f: &'a str,
200+
g: &'a str,
201+
}
202+
203+
// Original order of columns
204+
let specs = &[
205+
spec("a", ColumnType::Native(NativeType::Text)),
206+
spec("b", ColumnType::Native(NativeType::Int)),
207+
spec("d", ColumnType::Native(NativeType::Int)),
208+
spec("e", ColumnType::Native(NativeType::Text)),
209+
];
210+
let byts = serialize_cells([val_str("abc"), val_int(123), None, val_str("def")]);
211+
let row = deserialize::<MyRow<'_>>(specs, &byts).unwrap();
212+
assert_eq!(
213+
row,
214+
MyRow {
215+
a: "abc",
216+
b: Some(123),
217+
c: String::new(),
218+
d: 0,
219+
e: "def",
220+
f: "",
221+
g: "",
222+
}
223+
);
224+
225+
// Different order of columns - should still work
226+
let specs = &[
227+
spec("e", ColumnType::Native(NativeType::Text)),
228+
spec("b", ColumnType::Native(NativeType::Int)),
229+
spec("d", ColumnType::Native(NativeType::Int)),
230+
spec("a", ColumnType::Native(NativeType::Text)),
231+
];
232+
let byts = serialize_cells([None, val_int(123), None, val_str("abc")]);
233+
let row = deserialize::<MyRow<'_>>(specs, &byts).unwrap();
234+
assert_eq!(
235+
row,
236+
MyRow {
237+
a: "abc",
238+
b: Some(123),
239+
c: String::new(),
240+
d: 0,
241+
e: "",
242+
f: "",
243+
g: "",
244+
}
245+
);
246+
247+
// Missing column
248+
let specs = &[
249+
spec("a", ColumnType::Native(NativeType::Text)),
250+
spec("e", ColumnType::Native(NativeType::Text)),
251+
];
252+
MyRow::type_check(specs).unwrap();
253+
254+
// Missing both default_when_null column
255+
let specs = &[
256+
spec("a", ColumnType::Native(NativeType::Text)),
257+
spec("b", ColumnType::Native(NativeType::Int)),
258+
];
259+
MyRow::type_check(specs).unwrap();
260+
261+
// Wrong column type
262+
let specs = &[
263+
spec("a", ColumnType::Native(NativeType::Int)),
264+
spec("b", ColumnType::Native(NativeType::Int)),
265+
];
266+
MyRow::type_check(specs).unwrap_err();
267+
}
268+
181269
#[test]
182270
fn test_struct_deserialization_strict_ordering() {
183271
#[derive(DeserializeRow, PartialEq, Eq, Debug)]

scylla-macros/src/deserialize/row.rs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ struct StructAttrs {
2424
// This annotation only works if `enforce_order` is specified.
2525
#[darling(default)]
2626
skip_name_checks: bool,
27+
28+
// If true, then - if this field is missing from the UDT fields metadata
29+
// - it will be initialized to Default::default().
30+
// currently only supported with Flavor::MatchByName
31+
#[darling(default)]
32+
#[darling(rename = "allow_missing")]
33+
default_when_missing: bool,
2734
}
2835

2936
impl DeserializeCommonStructAttrs for StructAttrs {
@@ -51,6 +58,13 @@ struct Field {
5158
#[darling(default)]
5259
default_when_null: bool,
5360

61+
// If true, then - if this field is missing from the UDT fields metadata
62+
// - it will be initialized to Default::default().
63+
// currently only supported with Flavor::MatchByName
64+
#[darling(default)]
65+
#[darling(rename = "allow_missing")]
66+
default_when_missing: bool,
67+
5468
ident: Option<syn::Ident>,
5569
ty: syn::Type,
5670
}
@@ -135,7 +149,7 @@ fn validate_attrs(attrs: &StructAttrs, fields: &[Field]) -> Result<(), darling::
135149
impl Field {
136150
// Returns whether this field is mandatory for deserialization.
137151
fn is_required(&self) -> bool {
138-
!self.skip
152+
!self.skip && !self.default_when_missing
139153
}
140154

141155
// The name of the column corresponding to this Rust struct field
@@ -209,13 +223,7 @@ impl TypeCheckAssumeOrderGenerator<'_> {
209223
let macro_internal = self.0.struct_attrs().macro_internal_path();
210224
let (frame_lifetime, metadata_lifetime) = self.0.constraint_lifetimes();
211225

212-
let required_fields_iter = || {
213-
self.0
214-
.fields()
215-
.iter()
216-
.enumerate()
217-
.filter(|(_, f)| f.is_required())
218-
};
226+
let required_fields_iter = || self.0.fields().iter().enumerate().filter(|(_, f)| !f.skip);
219227
let required_fields_count = required_fields_iter().count();
220228
let required_fields_idents: Vec<_> = (0..required_fields_count)
221229
.map(|i| quote::format_ident!("f_{}", i))
@@ -394,7 +402,7 @@ impl TypeCheckUnorderedGenerator<'_> {
394402
let visited_flag = Self::visited_flag_variable(field);
395403
let typ = field.deserialize_target();
396404
let cql_name_literal = field.cql_name_literal();
397-
let decrement_if_required: Option::<syn::Stmt> = field.is_required().then(|| parse_quote! {
405+
let decrement_if_required: Option::<syn::Stmt> = (!self.0.attrs.default_when_missing && field.is_required()).then(|| parse_quote! {
398406
remaining_required_fields -= 1;
399407
});
400408

@@ -467,7 +475,11 @@ impl TypeCheckUnorderedGenerator<'_> {
467475
.iter()
468476
.filter(|f| !f.skip)
469477
.map(|f| f.cql_name_literal());
470-
let field_count_lit = fields.iter().filter(|f| f.is_required()).count();
478+
let field_count_lit = if self.0.attrs.default_when_missing {
479+
0
480+
} else {
481+
fields.iter().filter(|f| f.is_required()).count()
482+
};
471483

472484
parse_quote! {
473485
fn type_check(
@@ -541,11 +553,18 @@ impl DeserializeUnorderedGenerator<'_> {
541553

542554
let deserialize_field = Self::deserialize_field_variable(field);
543555
let cql_name_literal = field.cql_name_literal();
544-
parse_quote! {
545-
#deserialize_field.unwrap_or_else(|| ::std::panic!(
546-
"column {} missing in DB row - type check should have prevented this!",
547-
#cql_name_literal
548-
))
556+
if self.0.attrs.default_when_missing || field.default_when_missing {
557+
// Generate Default::default if the field was missing
558+
parse_quote! {
559+
#deserialize_field.unwrap_or_default()
560+
}
561+
} else {
562+
parse_quote! {
563+
#deserialize_field.unwrap_or_else(|| ::std::panic!(
564+
"column {} missing in DB row - type check should have prevented this!",
565+
#cql_name_literal
566+
))
567+
}
549568
}
550569
}
551570

scylla-macros/src/deserialize/value.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ struct StructAttrs {
3131
// they will be ignored. With true, an error will be raised.
3232
#[darling(default)]
3333
forbid_excess_udt_fields: bool,
34+
35+
// If true, then - if this field is missing from the UDT fields metadata
36+
// - it will be initialized to Default::default().
37+
#[darling(default)]
38+
#[darling(rename = "allow_missing")]
39+
default_when_missing: bool,
3440
}
3541

3642
impl DeserializeCommonStructAttrs for StructAttrs {
@@ -229,7 +235,7 @@ impl TypeCheckAssumeOrderGenerator<'_> {
229235
let (frame_lifetime, metadata_lifetime) = self.0.constraint_lifetimes();
230236
let rust_field_name = field.cql_name_literal();
231237
let rust_field_typ = field.deserialize_target();
232-
let default_when_missing = field.default_when_missing;
238+
let default_when_missing = self.0.attrs.default_when_missing || field.default_when_missing;
233239
let skip_name_checks = self.0.attrs.skip_name_checks;
234240

235241
// Action performed in case of field name mismatch.
@@ -593,8 +599,8 @@ impl TypeCheckUnorderedGenerator<'_> {
593599
let visited_flag = Self::visited_flag_variable(field);
594600
let typ = field.deserialize_target();
595601
let cql_name_literal = field.cql_name_literal();
596-
let decrement_if_required: Option<syn::Stmt> = field
597-
.is_required()
602+
let decrement_if_required: Option<syn::Stmt> = (!self.0.attrs.default_when_missing && field
603+
.is_required())
598604
.then(|| parse_quote! {remaining_required_cql_fields -= 1;});
599605

600606
parse_quote! {
@@ -659,7 +665,12 @@ impl TypeCheckUnorderedGenerator<'_> {
659665
.iter()
660666
.filter(|f| !f.skip)
661667
.map(|f| f.cql_name_literal());
662-
let required_cql_field_count = rust_fields.iter().filter(|f| f.is_required()).count();
668+
let required_cql_field_count = if self.0.attrs.default_when_missing {
669+
0
670+
} else {
671+
rust_fields.iter().filter(|f| f.is_required()).count()
672+
};
673+
663674
let required_cql_field_count_lit =
664675
syn::LitInt::new(&required_cql_field_count.to_string(), Span::call_site());
665676
let extract_cql_fields_expr = self.0.generate_extract_fields_from_type(parse_quote!(typ));
@@ -746,7 +757,7 @@ impl DeserializeUnorderedGenerator<'_> {
746757
}
747758

748759
let deserialize_field = Self::deserialize_field_variable(field);
749-
if field.default_when_missing {
760+
if self.0.attrs.default_when_missing || field.default_when_missing {
750761
// Generate Default::default if the field was missing
751762
parse_quote! {
752763
#deserialize_field.unwrap_or_default()

scylla-macros/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,11 @@ mod deserialize;
378378
/// column into the first field, second column into the second field and so on.
379379
/// It will still still verify that the column types and field types match.
380380
///
381+
/// #[scylla(allow_missing)]
382+
///
383+
/// if set, implementation will not fail if some columns are missing.
384+
/// Instead, it will initialize the field with `Default::default()`.
385+
///
381386
/// ## Field attributes
382387
///
383388
/// `#[scylla(skip)]`
@@ -395,6 +400,11 @@ mod deserialize;
395400
/// By default, the generated implementation will try to match the Rust field
396401
/// to a column with the same name. This attribute allows to match to a column
397402
/// with provided name.
403+
///
404+
/// #[scylla(allow_missing)]
405+
///
406+
/// if set, implementation will not fail if some columns are missing.
407+
/// Instead, it will initialize the field with `Default::default()`.
398408
#[proc_macro_derive(DeserializeRow, attributes(scylla))]
399409
pub fn deserialize_row_derive(tokens_input: TokenStream) -> TokenStream {
400410
match deserialize::row::deserialize_row_derive(tokens_input) {
@@ -501,6 +511,11 @@ pub fn deserialize_row_derive(tokens_input: TokenStream) -> TokenStream {
501511
/// If more strictness is desired, this flag makes sure that no excess fields
502512
/// are present and forces error in case there are some.
503513
///
514+
/// `#[scylla(allow_missing)]`
515+
///
516+
/// If the value of the field received from DB is null, the field will be
517+
/// initialized with `Default::default()`.
518+
///
504519
/// ## Field attributes
505520
///
506521
/// `#[scylla(skip)]`

scylla-macros/src/serialize/row.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ struct Attributes {
2222
// This annotation only works if `enforce_order` flavor is specified.
2323
#[darling(default)]
2424
skip_name_checks: bool,
25+
26+
// Used for deserialization only. Ignored in serialization.
27+
#[darling(default)]
28+
#[darling(rename = "allow_missing")]
29+
_default_when_missing: bool,
2530
}
2631

2732
impl Attributes {
@@ -70,6 +75,11 @@ struct FieldAttributes {
7075
#[darling(default)]
7176
#[darling(rename = "default_when_null")]
7277
_default_when_null: bool,
78+
79+
// Used for deserialization only. Ignored in serialization.
80+
#[darling(default)]
81+
#[darling(rename = "allow_missing")]
82+
_default_when_missing: bool,
7383
}
7484

7585
struct Context {

scylla-macros/src/serialize/value.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ struct Attributes {
3030
// the DB will interpret them as NULLs anyway.
3131
#[darling(default)]
3232
forbid_excess_udt_fields: bool,
33+
34+
// Used for deserialization only. Ignored in serialization.
35+
#[darling(default)]
36+
#[darling(rename = "allow_missing")]
37+
_default_when_missing: bool,
3338
}
3439

3540
impl Attributes {

scylla/tests/integration/macros/hygiene.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,24 @@ macro_rules! test_crate {
242242
g: ::core::primitive::i32,
243243
}
244244

245+
// Test attributes for value struct with ordered flavor
246+
#[derive(
247+
_scylla::DeserializeValue, _scylla::SerializeValue, PartialEq, Debug,
248+
)]
249+
#[scylla(crate = _scylla, flavor = "enforce_order")]
250+
#[scylla(allow_missing)]
251+
struct TestStructOrderedAllowedMissing {
252+
a: ::core::primitive::i32,
253+
b: ::core::primitive::i32,
254+
#[scylla(default_when_null)]
255+
c: ::core::primitive::i32,
256+
#[scylla(skip)]
257+
d: ::core::primitive::i32,
258+
#[scylla(rename = "f")]
259+
e: ::core::primitive::i32,
260+
g: ::core::primitive::i32,
261+
}
262+
245263
// Test attributes for value struct with strict ordered flavor
246264
#[derive(
247265
_scylla::DeserializeValue, _scylla::SerializeValue, PartialEq, Debug,
@@ -314,6 +332,24 @@ macro_rules! test_crate {
314332
c: ::core::primitive::i32,
315333
#[scylla(default_when_null)]
316334
d: ::core::primitive::i32,
335+
#[scylla(allow_missing)]
336+
e: ::core::primitive::i32,
337+
}
338+
// Test attributes for row struct with name flavor
339+
#[derive(
340+
_scylla::DeserializeRow, _scylla::SerializeRow, PartialEq, Debug,
341+
)]
342+
#[scylla(crate = _scylla)]
343+
#[scylla(allow_missing)]
344+
struct TestRowByNameWithMissing {
345+
#[scylla(skip)]
346+
a: ::core::primitive::i32,
347+
#[scylla(rename = "f")]
348+
b: ::core::primitive::i32,
349+
c: ::core::primitive::i32,
350+
#[scylla(default_when_null)]
351+
d: ::core::primitive::i32,
352+
e: ::core::primitive::i32,
317353
}
318354

319355
// Test attributes for row struct with ordered flavor

0 commit comments

Comments
 (0)