Skip to content

Commit 941684a

Browse files
committed
macros: SerializeValue supports #[allow_missing]
The `#[allow_missing]` attibute is intented to allow a flexible transition period when schema is altered. Namely, if a UDT is extended with a new field and the transition begins with migrating clients to the new schema (extending the Rust struct with a new field), then server may still have the old schema and provide metadata that is missing the new field. In such case, `#[allow_missing]` can be attached to the new Rust struct's field and this way handle the situation: - in deserialization, the field missing from DB metadata will be default-initialized, - in serialization, such a field will be ignored and not serialized. This commit adds support for this attribute: for parsing it using `darling`, and for its semantics in both `match_by_name` and `enforce_order` flavors. Also, corresponding (doc)tests for attribute sanity verification are added, fully based on those from DeserializeValue.
1 parent 66b0172 commit 941684a

File tree

2 files changed

+113
-13
lines changed

2 files changed

+113
-13
lines changed

scylla-cql/src/types/serialize/value.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,45 @@ mod doctests {
15401540
/// }
15411541
/// ```
15421542
fn _test_udt_bad_attributes_rename_collision_with_another_rename() {}
1543+
1544+
/// ```compile_fail
1545+
///
1546+
/// #[derive(scylla_macros::SerializeValue)]
1547+
/// #[scylla(crate = scylla_cql, flavor = "enforce_order", skip_name_checks)]
1548+
/// struct TestUdt {
1549+
/// a: i32,
1550+
/// #[scylla(allow_missing)]
1551+
/// b: bool,
1552+
/// c: String,
1553+
/// }
1554+
/// ```
1555+
fn _test_udt_bad_attributes_name_skip_name_checks_limitations_on_allow_missing() {}
1556+
1557+
/// ```
1558+
///
1559+
/// #[derive(scylla_macros::SerializeValue)]
1560+
/// #[scylla(crate = scylla_cql, flavor = "enforce_order", skip_name_checks)]
1561+
/// struct TestUdt {
1562+
/// a: i32,
1563+
/// #[scylla(allow_missing)]
1564+
/// b: bool,
1565+
/// #[scylla(allow_missing)]
1566+
/// c: String,
1567+
/// }
1568+
/// ```
1569+
fn _test_udt_good_attributes_name_skip_name_checks_limitations_on_allow_missing() {}
1570+
1571+
/// ```
1572+
/// #[derive(scylla_macros::SerializeValue)]
1573+
/// #[scylla(crate = scylla_cql)]
1574+
/// struct TestUdt {
1575+
/// a: i32,
1576+
/// #[scylla(allow_missing)]
1577+
/// b: bool,
1578+
/// c: String,
1579+
/// }
1580+
/// ```
1581+
fn _test_udt_unordered_flavour_no_limitations_on_allow_missing() {}
15431582
}
15441583

15451584
#[cfg(test)]

scylla-macros/src/serialize/value.rs

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ impl Field {
5151
None => self.ident.to_string(),
5252
}
5353
}
54+
55+
// Returns whether this field must be serialized (can't be ignored in case
56+
// that there is no corresponding field in UDT).
57+
fn is_required(&self) -> bool {
58+
!self.attrs.skip && !self.attrs.ignore_missing
59+
}
5460
}
5561

5662
#[derive(FromAttributes)]
@@ -60,6 +66,12 @@ struct FieldAttributes {
6066

6167
#[darling(default)]
6268
skip: bool,
69+
70+
// If true, then - if this field is missing from the UDT fields metadata
71+
// - it will be ignored during serialization.
72+
#[darling(default)]
73+
#[darling(rename = "allow_missing")]
74+
ignore_missing: bool,
6375
}
6476

6577
struct Context {
@@ -125,6 +137,28 @@ impl Context {
125137
errors.push(err);
126138
}
127139

140+
// When name checks are skipped, fields with `allow_missing` are only
141+
// permitted at the end of the struct, i.e. no field without
142+
// `allow_missing` and `skip` is allowed to be after any field
143+
// with `allow_missing`.
144+
let invalid_default_when_missing_field = self
145+
.fields
146+
.iter()
147+
.rev()
148+
// Skip the whole suffix of <allow_missing> and <skip>.
149+
.skip_while(|field| !field.is_required())
150+
// skip_while finished either because the iterator is empty or it found a field without both <allow_missing> and <skip>.
151+
// In either case, there aren't allowed to be any more fields with `allow_missing`.
152+
.find(|field| field.attrs.ignore_missing);
153+
if let Some(invalid) = invalid_default_when_missing_field {
154+
let error =
155+
darling::Error::custom(
156+
"when `skip_name_checks` is on, fields with `allow_missing` are only permitted at the end of the struct, \
157+
i.e. no field without `allow_missing` and `skip` is allowed to be after any field with `allow_missing`."
158+
).with_span(&invalid.ident);
159+
errors.push(error);
160+
}
161+
128162
// `rename` annotations don't make sense with skipped name checks
129163
for field in self.fields.iter() {
130164
if field.attrs.rename.is_some() {
@@ -230,6 +264,8 @@ impl<'a> Generator for FieldSortingGenerator<'a> {
230264
.iter()
231265
.map(|f| f.field_name())
232266
.collect::<Vec<_>>();
267+
let rust_field_ignore_missing_flags =
268+
self.ctx.fields.iter().map(|f| f.attrs.ignore_missing);
233269
let udt_field_names = rust_field_names.clone(); // For now, it's the same
234270
let field_types = self.ctx.fields.iter().map(|f| &f.ty).collect::<Vec<_>>();
235271

@@ -278,15 +314,32 @@ impl<'a> Generator for FieldSortingGenerator<'a> {
278314
.generate_udt_type_match(parse_quote!(#crate_path::UdtTypeCheckErrorKind::NotUdt)),
279315
);
280316

317+
fn make_visited_flag_ident(field_name: &str) -> syn::Ident {
318+
syn::Ident::new(&format!("visited_flag_{}", field_name), Span::call_site())
319+
}
320+
281321
// Generate a "visited" flag for each field
282322
let visited_flag_names = rust_field_names
283323
.iter()
284-
.map(|s| syn::Ident::new(&format!("visited_flag_{}", s), Span::call_site()))
324+
.map(|s| make_visited_flag_ident(s))
285325
.collect::<Vec<_>>();
286326
statements.extend::<Vec<_>>(parse_quote! {
287327
#(let mut #visited_flag_names = false;)*
288328
});
289329

330+
// An iterator over names of Rust fields that can't be ignored
331+
// (i.e., if UDT misses a corresponding field, an error should be raised).
332+
let nonignorable_rust_field_names = self
333+
.ctx
334+
.fields
335+
.iter()
336+
.filter(|f| !f.attrs.ignore_missing)
337+
.map(|f| f.field_name());
338+
// An iterator over visited flags of Rust fields that can't be ignored
339+
// (i.e., if UDT misses a corresponding field, an error should be raised).
340+
let nonignorable_visited_flag_names =
341+
nonignorable_rust_field_names.map(|s| make_visited_flag_ident(&s));
342+
290343
// Generate a variable that counts down visited fields.
291344
let field_count = self.ctx.fields.len();
292345
statements.push(parse_quote! {
@@ -340,19 +393,19 @@ impl<'a> Generator for FieldSortingGenerator<'a> {
340393
});
341394

342395
// Finally, check that all fields were consumed.
343-
// If there are some missing fields, return an error
396+
// If there are some missing fields that don't have the `#[allow_missing]`
397+
// attribute on them, return an error.
344398
statements.push(parse_quote! {
345399
if remaining_count > 0 {
346400
#(
347-
if !#visited_flag_names {
401+
if !#nonignorable_visited_flag_names && !#rust_field_ignore_missing_flags {
348402
return ::std::result::Result::Err(mk_typck_err(
349403
#crate_path::UdtTypeCheckErrorKind::ValueMissingForUdtField {
350404
field_name: <_ as ::std::string::ToString>::to_string(#rust_field_names),
351405
}
352406
));
353407
}
354408
)*
355-
::std::unreachable!()
356409
}
357410
});
358411

@@ -404,25 +457,29 @@ impl<'a> Generator for FieldOrderedGenerator<'a> {
404457
let mut builder = #crate_path::CellWriter::into_value_builder(writer);
405458
});
406459

407-
// Create an iterator over fields
460+
// Create a peekable iterator over fields.
408461
statements.push(parse_quote! {
409-
let mut field_iter = field_types.iter();
462+
let mut field_iter = field_types.iter().peekable();
410463
});
411464

412465
// Serialize each field
413466
for field in self.ctx.fields.iter() {
414467
let rust_field_ident = &field.ident;
415468
let rust_field_name = field.field_name();
469+
let field_can_be_ignored = field.attrs.ignore_missing;
416470
let typ = &field.ty;
417471
let name_check_expression: syn::Expr = if !self.ctx.attributes.skip_name_checks {
418472
parse_quote! { field_name == #rust_field_name }
419473
} else {
420474
parse_quote! { true }
421475
};
422476
statements.push(parse_quote! {
423-
match field_iter.next() {
477+
match field_iter.peek() {
424478
Some((field_name, typ)) => {
425479
if #name_check_expression {
480+
// Advance the iterator.
481+
field_iter.next();
482+
426483
let sub_builder = #crate_path::CellValueBuilder::make_sub_writer(&mut builder);
427484
match <#typ as #crate_path::SerializeValue>::serialize(&self.#rust_field_ident, typ, sub_builder) {
428485
Ok(_proof) => {},
@@ -435,21 +492,25 @@ impl<'a> Generator for FieldOrderedGenerator<'a> {
435492
));
436493
}
437494
}
438-
} else {
495+
} else if !#field_can_be_ignored {
439496
return ::std::result::Result::Err(mk_typck_err(
440497
#crate_path::UdtTypeCheckErrorKind::FieldNameMismatch {
441498
rust_field_name: <_ as ::std::string::ToString>::to_string(#rust_field_name),
442499
db_field_name: <_ as ::std::clone::Clone>::clone(field_name).into_owned(),
443500
}
444501
));
445502
}
503+
// Else simply ignore the field.
446504
}
447505
None => {
448-
return ::std::result::Result::Err(mk_typck_err(
449-
#crate_path::UdtTypeCheckErrorKind::ValueMissingForUdtField {
450-
field_name: <_ as ::std::string::ToString>::to_string(#rust_field_name),
451-
}
452-
));
506+
if !#field_can_be_ignored {
507+
return ::std::result::Result::Err(mk_typck_err(
508+
#crate_path::UdtTypeCheckErrorKind::ValueMissingForUdtField {
509+
field_name: <_ as ::std::string::ToString>::to_string(#rust_field_name),
510+
}
511+
));
512+
}
513+
// Else the field is ignored and we continue with other fields.
453514
}
454515
}
455516
});

0 commit comments

Comments
 (0)