Skip to content

Commit 73ccc95

Browse files
Mizuchimeta-codesync[bot]
authored andcommitted
Fix bug when ensuring union
Summary: # Problem A long-standing bug affected Thrift unions used as optional fields. When ensuring different fields in a union, the patch logic would incorrectly remove the entire union from its parent struct. For example: ``` union Bar { 1: i32 field_1; 2: i32 field_2; } struct Foo { 1: optional Bar bar; } ``` If you performed: ``` Foo.patch<ident::bar>().ensure<ident::field_1>(10); Foo.patch<ident::bar>().ensure<ident::field_2>(20); ``` The patch would incorrectly translate to: ``` Foo.remove<ident::bar>(); ``` # Why? The union's ensure semantic clears all fields before ensuring a new one. E.g., ``` MyUnion u; u.field_1().ensure(); u.field_2().ensure(); EXPECT_FALSE(u.field_1().has_value()); ``` In this case we ensured `field_1` first. When ensuring `field_2`, the existing code saw previously we ensured `field_1`, thus it will first clear all fields, then ensure field_2. In another word, ``` Foo.patch<ident::bar>().ensure<ident::field_2>(20); ``` this will be translated to ``` auto& patch = Foo.patch<ident::bar>(); patch.clear(); patch.ensure<ident::field_2>(20); ``` The problem is that `Foo.patch<ident::bar>().clear()` has a different meaning when `bar` is an optional field: it means removing the field from the parent struct. Thus the patch would be equivalent to ``` Foo.remove<ident::bar>(); ``` which is not the intended behavior. # Fix The patch changes the behavior so that ensuring a different field in a union now uses assign instead of clear. e.g., if `field_1` is ensured previously, the following operation ``` Foo.patch<ident::bar>().ensure<ident::field_2>(20); ``` is now translated to: ``` Foo.patch<ident::bar>().assign<ident::field_2>(20); ``` # Another problem To avoid similar problems, we should never translate other patch operations into `clear`, since `clear` will remove the data from the parent struct, which is not what we want. I found that we also translate `assign` operation in union to `clear`, e.g., ``` MyUnionPatch patch; patch.assign(foo); patch.patchIfSet<Id>(); ``` when we saw `patchIfSet`, we will call `ensurePatchable` which translate `patch.assign(foo)` into ``` patch.clear(); patch.ensure(foo); ``` This is also problematic (see unit-test for concrete example). This diff changed so that it will be translated to the following code instead ``` patch.patch<Id>().assign(active field in foo); ``` Which is basically an ensure + assign. Reviewed By: praihan Differential Revision: D88342407 fbshipit-source-id: 082e4022ec192c0982c25905ead34043de9700df
1 parent 62dc4f3 commit 73ccc95

1 file changed

Lines changed: 49 additions & 5 deletions

File tree

third-party/thrift/src/thrift/lib/cpp2/op/detail/StructPatch.h

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,21 @@ class BaseEnsurePatch : public BaseClearPatch<Patch, Derived> {
345345
/// Ensures the given field is set.
346346
template <typename Id>
347347
void ensure() {
348+
if constexpr (is_thrift_union_v<T>) {
349+
ensurePatchable();
350+
if (data_.assign() ||
351+
(!ensures<Id>() && !apache::thrift::empty(*data_.ensure()))) {
352+
// If we are ensuring a different field than we previously ensured,
353+
// we should assign this field.
354+
constexpr bool kIsIOBufPtr =
355+
std::is_same_v<FieldType<Id>, std::unique_ptr<folly::IOBuf>>;
356+
if constexpr (kIsIOBufPtr) {
357+
return Base::derived().template assign<Id>(folly::IOBuf{});
358+
} else {
359+
return Base::derived().template assign<Id>(FieldType<Id>{});
360+
}
361+
}
362+
}
348363
// Ensuring non-optional field to intrinsic default is allowed since we
349364
// might want to ensure field in case the field doesn't exist in dynamic
350365
// value. (e.g., Terse field with default value. Without ensuring it first,
@@ -355,6 +370,16 @@ class BaseEnsurePatch : public BaseClearPatch<Patch, Derived> {
355370
template <typename Id, typename U = FieldType<Id>>
356371
std::enable_if_t<type::is_optional_or_union_field_v<T, Id>> ensure(
357372
U&& defaultVal) {
373+
// TODO: simplify the logic by combining with the `ensure()` method above.
374+
if constexpr (is_thrift_union_v<T>) {
375+
ensurePatchable();
376+
if (data_.assign() ||
377+
(!ensures<Id>() && !apache::thrift::empty(*data_.ensure()))) {
378+
// If we are ensuring a different field than we previously ensured,
379+
// we should assign this field.
380+
return Base::derived().template assign<Id>(std::forward<U>(defaultVal));
381+
}
382+
}
358383
if (maybeEnsure<Id>()) {
359384
if (patchPrior<Id>().toThrift().clear().value() &&
360385
!is_thrift_union_v<T>) {
@@ -591,12 +616,31 @@ class BaseEnsurePatch : public BaseClearPatch<Patch, Derived> {
591616
// operation so that we can have sub-patches.
592617
template <typename U = T>
593618
if_union_patch<U> ensurePatchable() {
594-
if (data_.assign().has_value()) {
595-
data_.clear() = true;
596-
*data_.ensure() = std::move(*data_.assign());
597-
// Unset assign.
598-
data_.assign().reset();
619+
if (!data_.assign().has_value()) {
620+
return;
599621
}
622+
623+
if (data_.assign()->getType() == T::Type::__EMPTY__) {
624+
// If we assigned patch to an empty union, we can't turn it into ensure +
625+
// patch. In this case we can just keep it as it is since all field
626+
// patches would be no-op unless we ensure them later.
627+
return;
628+
}
629+
630+
auto tmp = std::move(*data_.assign());
631+
op::visit_union_with_tag(
632+
tmp,
633+
[&](auto tag, auto& field) {
634+
using Id = folly::type_list_element_t<0, decltype(tag)>;
635+
using Tag = op::get_type_tag<T, Id>;
636+
Base::reset();
637+
FieldPatchAssigner<Tag>{}(patch<Id>(), std::move(field));
638+
},
639+
[] {
640+
throw std::logic_error(
641+
"This should never happen. "
642+
"We just handled the __EMPTY__ case.");
643+
});
600644
}
601645

602646
// For Thrift Struct, we always ensure the patch is patchable.

0 commit comments

Comments
 (0)