Skip to content

Commit 67e73b9

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 3da897d commit 67e73b9

File tree

8 files changed

+205
-20
lines changed

8 files changed

+205
-20
lines changed

examples/user-defined-type.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ async fn main() -> Result<()> {
3232
// Define custom struct that matches User Defined Type created earlier
3333
// wrapping field in Option will gracefully handle null field values
3434
#[derive(Debug, DeserializeValue, SerializeValue)]
35+
#[scylla(allow_missing)]
3536
struct MyType {
3637
int_val: i32,
3738
text_val: Option<String>,

scylla-cql/src/deserialize/row_tests.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ fn test_struct_deserialization_loose_ordering() {
105105
d: i32,
106106
#[scylla(default_when_null)]
107107
e: &'a str,
108+
#[scylla(allow_missing)]
109+
f: &'a str,
108110
}
109111

110112
// Original order of columns
@@ -124,6 +126,7 @@ fn test_struct_deserialization_loose_ordering() {
124126
c: String::new(),
125127
d: 0,
126128
e: "def",
129+
f: "",
127130
}
128131
);
129132

@@ -144,6 +147,7 @@ fn test_struct_deserialization_loose_ordering() {
144147
c: String::new(),
145148
d: 0,
146149
e: "",
150+
f: "",
147151
}
148152
);
149153

@@ -169,6 +173,90 @@ fn test_struct_deserialization_loose_ordering() {
169173
MyRow::type_check(specs).unwrap_err();
170174
}
171175

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

242+
// Test attributes for value struct with ordered flavor
243+
#[derive(
244+
_scylla::DeserializeValue, _scylla::SerializeValue, PartialEq, Debug,
245+
)]
246+
#[scylla(crate = _scylla, flavor = "enforce_order")]
247+
#[scylla(allow_missing)]
248+
struct TestStructOrderedAllowedMissing {
249+
a: ::core::primitive::i32,
250+
b: ::core::primitive::i32,
251+
#[scylla(default_when_null)]
252+
c: ::core::primitive::i32,
253+
#[scylla(skip)]
254+
d: ::core::primitive::i32,
255+
#[scylla(rename = "f")]
256+
e: ::core::primitive::i32,
257+
g: ::core::primitive::i32,
258+
}
259+
242260
// Test attributes for value struct with strict ordered flavor
243261
#[derive(
244262
_scylla::DeserializeValue, _scylla::SerializeValue, PartialEq, Debug,
@@ -304,6 +322,24 @@ macro_rules! test_crate {
304322
c: ::core::primitive::i32,
305323
#[scylla(default_when_null)]
306324
d: ::core::primitive::i32,
325+
#[scylla(allow_missing)]
326+
e: ::core::primitive::i32,
327+
}
328+
// Test attributes for row struct with name flavor
329+
#[derive(
330+
_scylla::DeserializeRow, _scylla::SerializeRow, PartialEq, Debug,
331+
)]
332+
#[scylla(crate = _scylla)]
333+
#[scylla(allow_missing)]
334+
struct TestRowByNameWithMissing {
335+
#[scylla(skip)]
336+
a: ::core::primitive::i32,
337+
#[scylla(rename = "f")]
338+
b: ::core::primitive::i32,
339+
c: ::core::primitive::i32,
340+
#[scylla(default_when_null)]
341+
d: ::core::primitive::i32,
342+
e: ::core::primitive::i32,
307343
}
308344

309345
// Test attributes for row struct with ordered flavor

0 commit comments

Comments
 (0)