diff --git a/src/realm/dictionary.cpp b/src/realm/dictionary.cpp index 4b2a3191dda..3e020ef7e3a 100644 --- a/src/realm/dictionary.cpp +++ b/src/realm/dictionary.cpp @@ -800,23 +800,16 @@ bool Dictionary::replace_link(ObjLink old_link, ObjLink replace_link) return false; } -void Dictionary::remove_backlinks(CascadeState& state) const +bool Dictionary::remove_backlinks(CascadeState& state) const { size_t sz = size(); + bool recurse = false; for (size_t ndx = 0; ndx < sz; ndx++) { - auto val = m_values->get(ndx); - if (val.is_type(type_TypedLink)) { - Base::remove_backlink(m_col_key, val.get_link(), state); - } - else if (val.is_type(type_Dictionary)) { - auto key = do_get_key(ndx); - get_dictionary(key.get_string())->remove_backlinks(state); - } - else if (val.is_type(type_List)) { - auto key = do_get_key(ndx); - get_list(key.get_string())->remove_backlinks(state); + if (clear_backlink(ndx, state)) { + recurse = true; } } + return recurse; } size_t Dictionary::find_first(Mixed value) const @@ -827,16 +820,12 @@ size_t Dictionary::find_first(Mixed value) const void Dictionary::clear() { if (size() > 0) { - Replication* repl = get_replication(); - bool recurse = false; - CascadeState cascade_state(CascadeState::Mode::Strong); - if (repl) { + if (Replication* repl = get_replication()) { repl->dictionary_clear(*this); } - for (auto&& elem : *this) { - if (clear_backlink(elem.second, cascade_state)) - recurse = true; - } + CascadeState cascade_state(CascadeState::Mode::Strong); + bool recurse = remove_backlinks(cascade_state); + // Just destroy the whole cluster m_dictionary_top->destroy_deep(); m_dictionary_top.reset(); @@ -958,10 +947,9 @@ Mixed Dictionary::do_get(size_t ndx) const void Dictionary::do_erase(size_t ndx, Mixed key) { - auto old_value = m_values->get(ndx); - CascadeState cascade_state(CascadeState::Mode::Strong); - bool recurse = clear_backlink(old_value, cascade_state); + bool recurse = clear_backlink(ndx, cascade_state); + if (recurse) _impl::TableFriend::remove_recursive(*get_table_unchecked(), cascade_state); // Throws @@ -971,7 +959,6 @@ void Dictionary::do_erase(size_t ndx, Mixed key) m_keys->erase(ndx); m_values->erase(ndx); - bump_content_version(); } @@ -996,11 +983,20 @@ std::pair Dictionary::do_get_pair(size_t ndx) const return {do_get_key(ndx), do_get(ndx)}; } -bool Dictionary::clear_backlink(Mixed value, CascadeState& state) const +bool Dictionary::clear_backlink(size_t ndx, CascadeState& state) const { + auto value = m_values->get(ndx); if (value.is_type(type_TypedLink)) { return Base::remove_backlink(m_col_key, value.get_link(), state); } + if (value.is_type(type_Dictionary)) { + auto key = do_get_key(ndx); + return get_dictionary(key.get_string())->remove_backlinks(state); + } + if (value.is_type(type_List)) { + auto key = do_get_key(ndx); + return get_list(key.get_string())->remove_backlinks(state); + } return false; } diff --git a/src/realm/dictionary.hpp b/src/realm/dictionary.hpp index 79679f206b0..a7bdc96c100 100644 --- a/src/realm/dictionary.hpp +++ b/src/realm/dictionary.hpp @@ -137,7 +137,7 @@ class Dictionary final : public CollectionBaseImpl, public Colle void nullify(size_t); bool nullify(ObjLink target_link); bool replace_link(ObjLink old_link, ObjLink replace_link); - void remove_backlinks(CascadeState& state) const; + bool remove_backlinks(CascadeState& state) const; size_t find_first(Mixed value) const; void clear() final; @@ -242,7 +242,7 @@ class Dictionary final : public CollectionBaseImpl, public Colle size_t do_find_key(Mixed key) const noexcept; std::pair find_impl(Mixed key) const noexcept; std::pair do_get_pair(size_t ndx) const; - bool clear_backlink(Mixed value, CascadeState& state) const; + bool clear_backlink(size_t ndx, CascadeState& state) const; void align_indices(std::vector& indices) const; void swap_content(Array& fields1, Array& fields2, size_t index1, size_t index2); diff --git a/src/realm/list.cpp b/src/realm/list.cpp index 1f469e70207..e8a42750d96 100644 --- a/src/realm/list.cpp +++ b/src/realm/list.cpp @@ -418,9 +418,14 @@ void Lst::clear() if (Replication* repl = Base::get_replication()) { repl->list_clear(*this); } - size_t ndx = size(); - while (ndx--) { - do_remove(ndx); + CascadeState state; + bool recurse = remove_backlinks(state); + + m_tree->clear(); + + if (recurse) { + auto table = get_table_unchecked(); + _impl::TableFriend::remove_recursive(*table, state); // Throws } bump_content_version(); } @@ -573,34 +578,14 @@ void Lst::do_insert(size_t ndx, Mixed value) void Lst::do_remove(size_t ndx) { - Mixed old_value = m_tree->get(ndx); - if (old_value.is_type(type_TypedLink, type_Dictionary, type_List)) { - - bool recurse = false; - CascadeState state; - if (old_value.is_type(type_TypedLink)) { - auto old_link = old_value.get(); - if (old_link.get_obj_key().is_unresolved()) { - state.m_mode = CascadeState::Mode::All; - } - recurse = Base::remove_backlink(m_col_key, old_link, state); - } - else if (old_value.is_type(type_List)) { - get_list(ndx)->remove_backlinks(state); - } - else if (old_value.is_type(type_Dictionary)) { - get_dictionary(ndx)->remove_backlinks(state); - } + CascadeState state; + bool recurse = clear_backlink(ndx, state); - m_tree->erase(ndx); + m_tree->erase(ndx); - if (recurse) { - auto table = get_table_unchecked(); - _impl::TableFriend::remove_recursive(*table, state); // Throws - } - } - else { - m_tree->erase(ndx); + if (recurse) { + auto table = get_table_unchecked(); + _impl::TableFriend::remove_recursive(*table, state); // Throws } } @@ -822,21 +807,37 @@ bool Lst::replace_link(ObjLink old_link, ObjLink replace_link) return false; } -void Lst::remove_backlinks(CascadeState& state) const +bool Lst::clear_backlink(size_t ndx, CascadeState& state) const { - size_t sz = size(); - for (size_t ndx = 0; ndx < sz; ndx++) { - Mixed val = m_tree->get(ndx); - if (val.is_type(type_TypedLink)) { - Base::remove_backlink(m_col_key, val.get_link(), state); + Mixed value = m_tree->get(ndx); + if (value.is_type(type_TypedLink, type_Dictionary, type_List)) { + if (value.is_type(type_TypedLink)) { + auto link = value.get(); + if (link.get_obj_key().is_unresolved()) { + state.m_mode = CascadeState::Mode::All; + } + return Base::remove_backlink(m_col_key, link, state); } - else if (val.is_type(type_List)) { - get_list(ndx)->remove_backlinks(state); + else if (value.is_type(type_List)) { + return get_list(ndx)->remove_backlinks(state); } - else if (val.is_type(type_Dictionary)) { - get_dictionary(ndx)->remove_backlinks(state); + else if (value.is_type(type_Dictionary)) { + return get_dictionary(ndx)->remove_backlinks(state); + } + } + return false; +} + +bool Lst::remove_backlinks(CascadeState& state) const +{ + size_t sz = size(); + bool recurse = false; + for (size_t ndx = 0; ndx < sz; ndx++) { + if (clear_backlink(ndx, state)) { + recurse = true; } } + return recurse; } bool Lst::update_if_needed() const diff --git a/src/realm/list.hpp b/src/realm/list.hpp index d21dd636d14..7ca14d06973 100644 --- a/src/realm/list.hpp +++ b/src/realm/list.hpp @@ -490,7 +490,7 @@ class Lst final : public CollectionBaseImpl, public CollectionPa bool nullify(ObjLink); bool replace_link(ObjLink old_link, ObjLink replace_link); - void remove_backlinks(CascadeState& state) const; + bool remove_backlinks(CascadeState& state) const; TableRef get_table() const noexcept override { return get_obj().get_table(); @@ -548,6 +548,7 @@ class Lst final : public CollectionBaseImpl, public CollectionPa return unresolved_to_null(m_tree->get(ndx)); } + bool clear_backlink(size_t ndx, CascadeState& state) const; }; // Specialization of Lst: diff --git a/src/realm/obj.cpp b/src/realm/obj.cpp index 2e637e964f3..a592b4f36ae 100644 --- a/src/realm/obj.cpp +++ b/src/realm/obj.cpp @@ -211,6 +211,11 @@ bool Obj::compare_values(Mixed val1, Mixed val2, ColKey ck, Obj other, StringDat Lst lst2(other, other.get_column_key(col_name)); return compare_list_in_mixed(lst1, lst2, ck, other, col_name); } + else if (type == type_Set) { + Set set1(*this, ck); + Set set2(other, other.get_column_key(col_name)); + return set1 == set2; + } else if (type == type_Dictionary) { Dictionary dict1(*this, ck); Dictionary dict2(other, other.get_column_key(col_name)); @@ -1145,11 +1150,11 @@ Obj& Obj::set(ColKey col_key, Mixed value, bool is_default) } else if (old_value.is_type(type_Dictionary)) { Dictionary dict(*this, col_key); - dict.remove_backlinks(state); + recurse = dict.remove_backlinks(state); } else if (old_value.is_type(type_List)) { Lst list(*this, col_key); - list.remove_backlinks(state); + recurse = list.remove_backlinks(state); } if (value.is_type(type_TypedLink)) { @@ -2028,16 +2033,25 @@ CollectionPtr Obj::get_collection_by_stable_path(const StablePath& path) const while (level < path.size()) { auto& index = path[level]; auto get_ref = [&]() -> std::pair { + Mixed ref; + PathElement path_elem; if (collection->get_collection_type() == CollectionType::List) { auto list_of_mixed = dynamic_cast*>(collection.get()); size_t ndx = list_of_mixed->find_index(index); - return {list_of_mixed->get(ndx), PathElement(ndx)}; + if (ndx != realm::not_found) { + ref = list_of_mixed->get(ndx); + path_elem = ndx; + } } else { auto dict = dynamic_cast(collection.get()); size_t ndx = dict->find_index(index); - return {dict->get_any(ndx), PathElement(dict->get_key(ndx).get_string())}; + if (ndx != realm::not_found) { + ref = dict->get_any(ndx); + path_elem = dict->get_key(ndx).get_string(); + } } + return {ref, path_elem}; }; auto [ref, path_elem] = get_ref(); if (ref.is_type(type_List)) { diff --git a/src/realm/object_converter.cpp b/src/realm/object_converter.cpp index 0b41d969117..3c420038dab 100644 --- a/src/realm/object_converter.cpp +++ b/src/realm/object_converter.cpp @@ -23,11 +23,10 @@ #include #include - namespace realm::converters { // Takes two lists, src and dst, and makes dst equal src. src is unchanged. -void InterRealmValueConverter::copy_list(const Obj& src_obj, Obj& dst_obj, bool* update_out) +void InterRealmValueConverter::copy_list(const LstBase& src, LstBase& dst, bool* update_out) const { // The two arrays are compared by finding the longest common prefix and // suffix. The middle section differs between them and is made equal by @@ -37,25 +36,23 @@ void InterRealmValueConverter::copy_list(const Obj& src_obj, Obj& dst_obj, bool* // src = abcdefghi // dst = abcxyhi // The common prefix is abc. The common suffix is hi. xy is replaced by defg. - LstBasePtr src = src_obj.get_listbase_ptr(m_src_col); - LstBasePtr dst = dst_obj.get_listbase_ptr(m_dst_col); bool updated = false; - size_t len_src = src->size(); - size_t len_dst = dst->size(); + size_t len_src = src.size(); + size_t len_dst = dst.size(); size_t len_min = std::min(len_src, len_dst); size_t ndx = 0; size_t suffix_len = 0; - while (ndx < len_min && cmp_src_to_dst(src->get_any(ndx), dst->get_any(ndx), nullptr, update_out) == 0) { + while (ndx < len_min && cmp_src_to_dst(src.get_any(ndx), dst.get_any(ndx), nullptr, update_out) == 0) { ndx++; } size_t suffix_len_max = len_min - ndx; while (suffix_len < suffix_len_max && - cmp_src_to_dst(src->get_any(len_src - 1 - suffix_len), dst->get_any(len_dst - 1 - suffix_len), nullptr, + cmp_src_to_dst(src.get_any(len_src - 1 - suffix_len), dst.get_any(len_dst - 1 - suffix_len), nullptr, update_out) == 0) { suffix_len++; } @@ -64,15 +61,15 @@ void InterRealmValueConverter::copy_list(const Obj& src_obj, Obj& dst_obj, bool* for (size_t i = 0; i < len_min; i++) { InterRealmValueConverter::ConversionResult converted_src; - if (cmp_src_to_dst(src->get_any(ndx), dst->get_any(ndx), &converted_src, update_out)) { + if (cmp_src_to_dst(src.get_any(ndx), dst.get_any(ndx), &converted_src, update_out)) { if (converted_src.requires_new_embedded_object) { - auto lnklist = dynamic_cast(dst.get()); + auto lnklist = dynamic_cast(&dst); REALM_ASSERT(lnklist); // this is the only type of list that supports embedded objects Obj embedded = lnklist->create_and_set_linked_object(ndx); track_new_embedded(converted_src.src_embedded_to_check, embedded); } else { - dst->set_any(ndx, converted_src.converted_value); + dst.set_any(ndx, converted_src.converted_value); } updated = true; } @@ -82,15 +79,15 @@ void InterRealmValueConverter::copy_list(const Obj& src_obj, Obj& dst_obj, bool* // New elements must be inserted in dst. while (len_dst < len_src) { InterRealmValueConverter::ConversionResult converted_src; - cmp_src_to_dst(src->get_any(ndx), Mixed{}, &converted_src, update_out); + cmp_src_to_dst(src.get_any(ndx), Mixed{}, &converted_src, update_out); if (converted_src.requires_new_embedded_object) { - auto lnklist = dynamic_cast(dst.get()); + auto lnklist = dynamic_cast(&dst); REALM_ASSERT(lnklist); // this is the only type of list that supports embedded objects Obj embedded = lnklist->create_and_insert_linked_object(ndx); track_new_embedded(converted_src.src_embedded_to_check, embedded); } else { - dst->insert_any(ndx, converted_src.converted_value); + dst.insert_any(ndx, converted_src.converted_value); } len_dst++; ndx++; @@ -98,27 +95,24 @@ void InterRealmValueConverter::copy_list(const Obj& src_obj, Obj& dst_obj, bool* } // Excess elements must be removed from ll_dst. if (len_dst > len_src) { - dst->remove(len_src - suffix_len, len_dst - suffix_len); + dst.remove(len_src - suffix_len, len_dst - suffix_len); updated = true; } - REALM_ASSERT(dst->size() == len_src); + REALM_ASSERT(dst.size() == len_src); if (updated && update_out) { *update_out = updated; } } -void InterRealmValueConverter::copy_set(const Obj& src_obj, Obj& dst_obj, bool* update_out) +void InterRealmValueConverter::copy_set(const SetBase& src, SetBase& dst, bool* update_out) const { - SetBasePtr src = src_obj.get_setbase_ptr(m_src_col); - SetBasePtr dst = dst_obj.get_setbase_ptr(m_dst_col); - std::vector sorted_src, sorted_dst, to_insert, to_delete; constexpr bool ascending = true; // the implementation could be storing elements in sorted order, but // we don't assume that here. - src->sort(sorted_src, ascending); - dst->sort(sorted_dst, ascending); + src.sort(sorted_src, ascending); + dst.sort(sorted_dst, ascending); size_t dst_ndx = 0; size_t src_ndx = 0; @@ -132,11 +126,11 @@ void InterRealmValueConverter::copy_set(const Obj& src_obj, Obj& dst_obj, bool* break; } size_t ndx_in_src = sorted_src[src_ndx]; - Mixed src_val = src->get_any(ndx_in_src); + Mixed src_val = src.get_any(ndx_in_src); while (dst_ndx < sorted_dst.size()) { size_t ndx_in_dst = sorted_dst[dst_ndx]; - int cmp = cmp_src_to_dst(src_val, dst->get_any(ndx_in_dst), nullptr, update_out); + int cmp = cmp_src_to_dst(src_val, dst.get_any(ndx_in_dst), nullptr, update_out); if (cmp == 0) { // equal: advance both src and dst ++dst_ndx; @@ -163,14 +157,14 @@ void InterRealmValueConverter::copy_set(const Obj& src_obj, Obj& dst_obj, bool* std::sort(to_delete.begin(), to_delete.end()); for (auto it = to_delete.rbegin(); it != to_delete.rend(); ++it) { - dst->erase_any(dst->get_any(*it)); + dst.erase_any(dst.get_any(*it)); } for (auto ndx : to_insert) { InterRealmValueConverter::ConversionResult converted_src; - cmp_src_to_dst(src->get_any(ndx), Mixed{}, &converted_src, update_out); + cmp_src_to_dst(src.get_any(ndx), Mixed{}, &converted_src, update_out); // we do not support a set of embedded objects REALM_ASSERT(!converted_src.requires_new_embedded_object); - dst->insert_any(converted_src.converted_value); + dst.insert_any(converted_src.converted_value); } if (update_out && (to_delete.size() || to_insert.size())) { @@ -178,11 +172,8 @@ void InterRealmValueConverter::copy_set(const Obj& src_obj, Obj& dst_obj, bool* } } -void InterRealmValueConverter::copy_dictionary(const Obj& src_obj, Obj& dst_obj, bool* update_out) +void InterRealmValueConverter::copy_dictionary(const Dictionary& src, Dictionary& dst, bool* update_out) const { - Dictionary src = src_obj.get_dictionary(m_src_col); - Dictionary dst = dst_obj.get_dictionary(m_dst_col); - std::vector to_insert, to_delete; size_t dst_ndx = 0; @@ -253,29 +244,335 @@ void InterRealmValueConverter::copy_dictionary(const Obj& src_obj, Obj& dst_obj, void InterRealmValueConverter::copy_value(const Obj& src_obj, Obj& dst_obj, bool* update_out) { if (m_src_col.is_list()) { - copy_list(src_obj, dst_obj, update_out); + LstBasePtr src = src_obj.get_listbase_ptr(m_src_col); + LstBasePtr dst = dst_obj.get_listbase_ptr(m_dst_col); + copy_list(*src, *dst, update_out); } else if (m_src_col.is_dictionary()) { - copy_dictionary(src_obj, dst_obj, update_out); + Dictionary src = src_obj.get_dictionary(m_src_col); + Dictionary dst = dst_obj.get_dictionary(m_dst_col); + copy_dictionary(src, dst, update_out); } else if (m_src_col.is_set()) { - copy_set(src_obj, dst_obj, update_out); + SetBasePtr src = src_obj.get_setbase_ptr(m_src_col); + SetBasePtr dst = dst_obj.get_setbase_ptr(m_dst_col); + copy_set(*src, *dst, update_out); } else { REALM_ASSERT(!m_src_col.is_collection()); - InterRealmValueConverter::ConversionResult converted_src; - if (cmp_src_to_dst(src_obj.get_any(m_src_col), dst_obj.get_any(m_dst_col), &converted_src, update_out)) { - if (converted_src.requires_new_embedded_object) { - Obj new_embedded = dst_obj.create_and_set_linked_object(m_dst_col); - track_new_embedded(converted_src.src_embedded_to_check, new_embedded); + // nested collections + auto src_mixed = src_obj.get_any(m_src_col); + if (src_mixed.is_type(type_List)) { + dst_obj.set_collection(m_dst_col, CollectionType::List); + Lst src_list{src_obj, m_src_col}; + Lst dst_list{dst_obj, m_dst_col}; + handle_list_in_mixed(src_list, dst_list); + } + else if (src_mixed.is_type(type_Set)) { + dst_obj.set_collection(m_dst_col, CollectionType::Set); + realm::Set src_set{src_obj, m_src_col}; + realm::Set dst_set{dst_obj, m_dst_col}; + // sets cannot be nested, so we just need to copy the values + copy_set(src_set, dst_set, nullptr); + } + else if (src_mixed.is_type(type_Dictionary)) { + dst_obj.set_collection(m_dst_col, CollectionType::Dictionary); + Dictionary src_dict{src_obj, m_src_col}; + Dictionary dst_dict{dst_obj, m_dst_col}; + handle_dictionary_in_mixed(src_dict, dst_dict); + } + else { + InterRealmValueConverter::ConversionResult converted_src; + auto dst_mixed = dst_obj.get_any(m_dst_col); + if (cmp_src_to_dst(src_mixed, dst_mixed, &converted_src, update_out)) { + if (converted_src.requires_new_embedded_object) { + Obj new_embedded = dst_obj.create_and_set_linked_object(m_dst_col); + track_new_embedded(converted_src.src_embedded_to_check, new_embedded); + } + else { + dst_obj.set_any(m_dst_col, converted_src.converted_value); + } } - else { - dst_obj.set_any(m_dst_col, converted_src.converted_value); + } + } +} + +// +// Handle collections in mixed. A collection can have N nested levels (expect for Sets). And these levels can be +// nested in arbitrary way (eg a List within a Dictionary or viceversa). In order to try to merge server changes with +// client changes, the algorithm needs to go throw each single element in the collection, check its type and perform +// the most appropriate action in order to miminize the number of notificiations triggered. +// +void InterRealmValueConverter::handle_list_in_mixed(const Lst& src_list, Lst& dst_list) const +{ + int sz = (int)std::min(src_list.size(), dst_list.size()); + int left = 0; + int right = (int)sz - 1; + + // find fist not matching element from beginning + while (left < sz) { + auto src_any = src_list.get_any(left); + auto dst_any = dst_list.get_any(left); + if (src_any != dst_any) + break; + if (is_collection(src_any) && !check_matching_list(src_list, dst_list, left, to_collection_type(src_any))) + break; + left += 1; + } + + // find first not matching element from end + while (right >= 0) { + auto src_any = src_list.get_any(right); + auto dst_any = dst_list.get_any(right); + if (src_any != dst_any) + break; + if (is_collection(src_any) && !check_matching_list(src_list, dst_list, right, to_collection_type(src_any))) + break; + right -= 1; + } + + // Replace all different elements in [left, right] + while (left <= right) { + auto src_any = src_list.get_any(left); + auto dst_any = dst_list.get_any(left); + + if (is_collection(src_any)) { + auto coll_type = to_collection_type(src_any); + + if (!dst_any.is_type(src_any.get_type())) { + // Mixed vs Collection + dst_list.set_collection(left, coll_type); + copy_list_in_mixed(src_list, dst_list, left, coll_type); + } + else if (!check_matching_list(src_list, dst_list, left, coll_type)) { + // Collection vs Collection + dst_list.set_any(left, src_any); + copy_list_in_mixed(src_list, dst_list, left, coll_type); } } + else if (dst_any != src_any) { + // Mixed vs Mixed + dst_list.set_any(left, src_any); + } + left += 1; + } + + // remove dst elements not present in src + if (dst_list.size() > src_list.size()) { + auto dst_size = dst_list.size(); + auto src_size = src_list.size(); + while (dst_size > src_size) + dst_list.remove(--dst_size); + } + + // append remainig src into dst + for (size_t i = dst_list.size(); i < src_list.size(); ++i) { + auto src_any = src_list.get(i); + if (is_collection(src_any)) { + auto coll_type = to_collection_type(src_any); + dst_list.insert_collection(i, coll_type); + copy_list_in_mixed(src_list, dst_list, i, coll_type); + } + else { + dst_list.insert_any(i, src_any); + } } } +void InterRealmValueConverter::handle_dictionary_in_mixed(Dictionary& src_dictionary, + Dictionary& dst_dictionary) const +{ + std::vector to_insert, to_delete; + size_t src_ndx = 0, dst_ndx = 0; + while (src_ndx < src_dictionary.size() && dst_ndx < dst_dictionary.size()) { + const auto [key_src, src_any] = src_dictionary.get_pair(src_ndx); + const auto [key_dst, dst_any] = dst_dictionary.get_pair(dst_ndx); + + auto cmp = key_src.compare(key_dst); + if (cmp == 0) { + if (src_any != dst_any) { + to_insert.push_back(src_ndx); + } + else if (is_collection(src_any) && + !check_matching_dictionary(src_dictionary, dst_dictionary, key_src.get_string(), + to_collection_type(src_any))) { + to_insert.push_back(src_ndx); + } + src_ndx += 1; + dst_ndx += 1; + } + else if (cmp < 0) { + to_insert.push_back(src_ndx); + src_ndx += 1; + } + else { + to_delete.push_back(dst_ndx); + dst_ndx += 1; + } + } + + // append src to dst + while (src_ndx < src_dictionary.size()) { + to_insert.push_back(src_ndx); + src_ndx += 1; + } + + // delete everything that did not match passed src.size() + while (dst_ndx < dst_dictionary.size()) { + to_delete.push_back(dst_ndx); + dst_ndx += 1; + } + + // delete all the non matching keys + while (!to_delete.empty()) { + dst_dictionary.erase(dst_dictionary.begin() + to_delete.back()); + to_delete.pop_back(); + } + + // insert into dst + for (const auto pos : to_insert) { + const auto [key, any] = src_dictionary.get_pair(pos); + if (is_collection(any)) { + auto coll_type = to_collection_type(any); + dst_dictionary.insert_collection(key.get_string(), coll_type); + copy_dictionary_in_mixed(src_dictionary, dst_dictionary, key.get_string(), coll_type); + } + else { + dst_dictionary.insert(key, any); + } + } +} + +bool InterRealmValueConverter::check_matching_list(const Lst& src_list, Lst& dst_list, size_t ndx, + CollectionType type) const +{ + + if (type == CollectionType::List) { + auto nested_src_list = src_list.get_list(ndx); + auto nested_dst_list = dst_list.get_list(ndx); + auto size_src = nested_src_list->size(); + auto size_dst = nested_dst_list->size(); + if (size_src != size_dst) + return false; + for (size_t i = 0; i < size_src; ++i) { + auto src_mixed = nested_src_list->get_any(i); + auto dst_mixed = nested_dst_list->get_any(i); + if (src_mixed != dst_mixed) + return false; + } + } + else if (type == CollectionType::Dictionary) { + auto nested_src_dictionary = src_list.get_dictionary(ndx); + auto nested_dst_dictionary = dst_list.get_dictionary(ndx); + auto size_src = nested_src_dictionary->size(); + auto size_dst = nested_dst_dictionary->size(); + if (size_src != size_dst) + return false; + for (size_t i = 0; i < size_src; ++i) { + auto [src_key, src_mixed] = nested_src_dictionary->get_pair(i); + auto [dst_key, dst_mixed] = nested_dst_dictionary->get_pair(i); + if (src_key != dst_key) + return false; + if (src_mixed != dst_mixed) + return false; + } + } + return true; +} + +bool InterRealmValueConverter::check_matching_dictionary(const Dictionary& src_dictionary, + const Dictionary& dst_dictionary, StringData key, + CollectionType type) const +{ + if (type == CollectionType::List) { + auto n_src_list = src_dictionary.get_list(key); + auto n_dst_list = dst_dictionary.get_list(key); + auto size_src = n_src_list->size(); + auto size_dst = n_dst_list->size(); + if (size_src != size_dst) + return false; + for (size_t i = 0; i < size_src; ++i) { + auto src_mixed = n_src_list->get_any(i); + auto dst_mixed = n_dst_list->get_any(i); + if (src_mixed != dst_mixed) + return false; + } + } + else if (type == CollectionType::Dictionary) { + auto n_src_dictionary = src_dictionary.get_dictionary(key); + auto n_dst_dictionary = dst_dictionary.get_dictionary(key); + auto size_src = n_src_dictionary->size(); + auto size_dst = n_dst_dictionary->size(); + if (size_src != size_dst) + return false; + for (size_t i = 0; i < size_src; ++i) { + auto [src_key, src_mixed] = n_src_dictionary->get_pair(i); + auto [dst_key, dst_mixed] = n_dst_dictionary->get_pair(i); + if (src_key != dst_key) + return false; + if (src_mixed != dst_mixed) + return false; + } + } + return true; +} + +void InterRealmValueConverter::copy_list_in_mixed(const Lst& src_list, Lst& dst_list, size_t ndx, + CollectionType type) const +{ + if (type == CollectionType::List) { + auto n_src_list = src_list.get_list(ndx); + auto n_dst_list = dst_list.get_list(ndx); + handle_list_in_mixed(*n_src_list, *n_dst_list); + } + else if (type == CollectionType::Set) { + auto n_src_set = src_list.get_set(ndx); + auto n_dst_set = dst_list.get_set(ndx); + copy_set(*n_src_set, *n_dst_set, nullptr); + } + else if (type == CollectionType::Dictionary) { + auto n_src_dict = src_list.get_dictionary(ndx); + auto n_dst_dict = dst_list.get_dictionary(ndx); + handle_dictionary_in_mixed(*n_src_dict, *n_dst_dict); + } +} + +void InterRealmValueConverter::copy_dictionary_in_mixed(const Dictionary& src_dictionary, Dictionary& dst_dictionary, + StringData key, CollectionType type) const +{ + if (type == CollectionType::List) { + auto n_src_list = src_dictionary.get_list(key); + auto n_dst_list = dst_dictionary.get_list(key); + handle_list_in_mixed(*n_src_list, *n_dst_list); + } + else if (type == CollectionType::Set) { + auto n_src_set = src_dictionary.get_set(key); + auto n_dst_set = dst_dictionary.get_set(key); + copy_set(*n_src_set, *n_dst_set, nullptr); + } + else if (type == CollectionType::Dictionary) { + auto n_src_dictionary = src_dictionary.get_dictionary(key); + auto n_dst_dictionary = dst_dictionary.get_dictionary(key); + handle_dictionary_in_mixed(*n_src_dictionary, *n_dst_dictionary); + } +} + +bool InterRealmValueConverter::is_collection(Mixed mixed) const +{ + return mixed.is_type(type_List, type_Set, type_Dictionary); +} + +CollectionType InterRealmValueConverter::to_collection_type(Mixed mixed) const +{ + const auto mixed_type = mixed.get_type(); + if (mixed_type == type_List) + return CollectionType::List; + if (mixed_type == type_Set) + return CollectionType::Set; + if (mixed_type == type_Dictionary) + return CollectionType::Dictionary; + REALM_UNREACHABLE(); +} // If an embedded object is encountered, add it to a list of embedded objects to process. // This relies on the property that embedded objects only have one incoming link @@ -327,7 +624,7 @@ InterRealmValueConverter::InterRealmValueConverter(ConstTableRef src_table, ColK } } -void InterRealmValueConverter::track_new_embedded(const Obj& src, const Obj& dst) +void InterRealmValueConverter::track_new_embedded(const Obj& src, const Obj& dst) const { m_embedded_converter->track(src, dst); } @@ -335,7 +632,7 @@ void InterRealmValueConverter::track_new_embedded(const Obj& src, const Obj& dst // convert `src` to the destination Realm and compare that value with `dst` // If `converted_src_out` is provided, it will be set to the converted src value int InterRealmValueConverter::cmp_src_to_dst(Mixed src, Mixed dst, ConversionResult* converted_src_out, - bool* did_update_out) + bool* did_update_out) const { int cmp = 0; Mixed converted_src; diff --git a/src/realm/object_converter.hpp b/src/realm/object_converter.hpp index caec6c4bbc4..89ec1378536 100644 --- a/src/realm/object_converter.hpp +++ b/src/realm/object_converter.hpp @@ -39,7 +39,7 @@ struct EmbeddedObjectConverter { struct InterRealmValueConverter { InterRealmValueConverter(ConstTableRef src_table, ColKey src_col, ConstTableRef dst_table, ColKey dst_col, EmbeddedObjectConverter* ec); - void track_new_embedded(const Obj& src, const Obj& dst); + void track_new_embedded(const Obj& src, const Obj& dst) const; struct ConversionResult { Mixed converted_value; bool requires_new_embedded_object = false; @@ -49,13 +49,24 @@ struct InterRealmValueConverter { // convert `src` to the destination Realm and compare that value with `dst` // If `converted_src_out` is provided, it will be set to the converted src value int cmp_src_to_dst(Mixed src, Mixed dst, ConversionResult* converted_src_out = nullptr, - bool* did_update_out = nullptr); + bool* did_update_out = nullptr) const; void copy_value(const Obj& src_obj, Obj& dst_obj, bool* update_out); private: - void copy_list(const Obj& src_obj, Obj& dst_obj, bool* update_out); - void copy_set(const Obj& src_obj, Obj& dst_obj, bool* update_out); - void copy_dictionary(const Obj& src_obj, Obj& dst_obj, bool* update_out); + void copy_list(const LstBase& src_obj, LstBase& dst_obj, bool* update_out) const; + void copy_set(const SetBase& src_obj, SetBase& dst_obj, bool* update_out) const; + void copy_dictionary(const Dictionary& src_obj, Dictionary& dst_obj, bool* update_out) const; + // collection in mixed. + void handle_list_in_mixed(const Lst& src_list, Lst& dst_list) const; + void handle_dictionary_in_mixed(Dictionary& src_dict, Dictionary& dst_dict) const; + void copy_list_in_mixed(const Lst& src_list, Lst& dst_list, size_t ndx, CollectionType type) const; + void copy_dictionary_in_mixed(const Dictionary& src_list, Dictionary& dst_list, StringData ndx, + CollectionType type) const; + bool check_matching_list(const Lst& src_list, Lst& dst_list, size_t ndx, CollectionType type) const; + bool check_matching_dictionary(const Dictionary& src_list, const Dictionary& dst_list, StringData key, + CollectionType type) const; + bool is_collection(Mixed) const; + CollectionType to_collection_type(Mixed) const; TableRef m_dst_link_table; ConstTableRef m_src_table; diff --git a/src/realm/sync/instruction_applier.cpp b/src/realm/sync/instruction_applier.cpp index 98f29bcdc28..ab2cdc8f6c9 100644 --- a/src/realm/sync/instruction_applier.cpp +++ b/src/realm/sync/instruction_applier.cpp @@ -1538,20 +1538,22 @@ InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resol else { if (list.get_data_type() == type_Mixed) { auto& mixed_list = static_cast&>(list); - auto val = mixed_list.get(index); + if (index < mixed_list.size()) { + auto val = mixed_list.get(index); - if (val.is_type(type_Dictionary)) { - if (auto pfield = mpark::get_if(&*m_it_begin)) { - Dictionary d(mixed_list, mixed_list.get_key(index)); - ++m_it_begin; - return resolve_dictionary_element(d, *pfield); + if (val.is_type(type_Dictionary)) { + if (auto pfield = mpark::get_if(&*m_it_begin)) { + Dictionary d(mixed_list, mixed_list.get_key(index)); + ++m_it_begin; + return resolve_dictionary_element(d, *pfield); + } } - } - if (val.is_type(type_List)) { - if (auto pindex = mpark::get_if(&*m_it_begin)) { - Lst l(mixed_list, mixed_list.get_key(index)); - ++m_it_begin; - return resolve_list_element(l, *pindex); + if (val.is_type(type_List)) { + if (auto pindex = mpark::get_if(&*m_it_begin)) { + Lst l(mixed_list, mixed_list.get_key(index)); + ++m_it_begin; + return resolve_list_element(l, *pindex); + } } } } diff --git a/src/realm/sync/noinst/client_reset.cpp b/src/realm/sync/noinst/client_reset.cpp index 04abcff8b06..739375c4416 100644 --- a/src/realm/sync/noinst/client_reset.cpp +++ b/src/realm/sync/noinst/client_reset.cpp @@ -294,7 +294,7 @@ void transfer_group(const Transaction& group_src, Transaction& group_dst, util:: // column preexists in dest, make sure the types match if (col_key.get_type() != col_key_dst.get_type()) { throw ClientResetFailed(util::format( - "Incompatable column type change detected during client reset for '%1.%2' (%3 vs %4)", + "Incompatible column type change detected during client reset for '%1.%2' (%3 vs %4)", table_name, col_name, col_key.get_type(), col_key_dst.get_type())); } ColumnAttrMask src_col_attrs = col_key.get_attrs(); diff --git a/src/realm/sync/noinst/client_reset_recovery.cpp b/src/realm/sync/noinst/client_reset_recovery.cpp index 4d90783da61..f7efd78ba4e 100644 --- a/src/realm/sync/noinst/client_reset_recovery.cpp +++ b/src/realm/sync/noinst/client_reset_recovery.cpp @@ -547,7 +547,46 @@ bool RecoverLocalChangesetsHandler::resolve_path(ListPath& path, Obj remote_obj, return false; } } - else { // single link to embedded object + else if (col.get_type() == col_type_Mixed) { + StringData col_name = remote_obj.get_table()->get_column_name(col); + auto local_any = local_obj.get_any(col_name); + auto remote_any = remote_obj.get_any(col); + + if (local_any.is_type(type_List) && remote_any.is_type(type_List)) { + ++it; + if (it == path.end()) { + auto local_col = local_obj.get_table()->get_column_key(col_name); + Lst local_list{local_obj, local_col}; + Lst remote_list{remote_obj, col}; + callback(remote_list, local_list); + return true; + } + else { + // same as above. + REALM_UNREACHABLE(); + } + } + else if (local_any.is_type(type_Dictionary) && remote_any.is_type(type_Dictionary)) { + ++it; + REALM_ASSERT(it != path.end()); + REALM_ASSERT(it->type == ListPath::Element::Type::InternKey); + StringData col_name = remote_obj.get_table()->get_column_name(col); + auto local_col = local_obj.get_table()->get_column_key(col_name); + Dictionary remote_dict{remote_obj, col}; + Dictionary local_dict{local_obj, local_col}; + StringData dict_key = m_intern_keys.get_key(it->intern_key); + if (remote_dict.contains(dict_key) && local_dict.contains(dict_key)) { + remote_obj = remote_dict.get_object(dict_key); + local_obj = local_dict.get_object(dict_key); + ++it; + } + else { + return false; + } + } + } + else { + // single link to embedded object // Neither embedded object sets nor Mixed(TypedLink) to embedded objects are supported. REALM_ASSERT_EX(!col.is_collection(), col); REALM_ASSERT_EX(col.get_type() == col_type_Link, col); diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index 1fcf09c5de4..727e1404c9e 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -2971,6 +2971,7 @@ TEST_CASE("client reset with embedded object", "[sync][pbs][client reset][embedd {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "EmbeddedObject"}, {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable, "EmbeddedObject"}, + {"any_mixed", PropertyType::Mixed | PropertyType::Nullable}, }}, {"EmbeddedObject", ObjectSchema::ObjectType::Embedded, @@ -3995,7 +3996,7 @@ TEST_CASE("client reset with embedded object", "[sync][pbs][client reset][embedd Obj obj = get_top_object(realm); auto dict = obj.get_dictionary("embedded_dict"); auto embedded = dict.get_object(key); - REQUIRE(!!embedded); + REQUIRE(embedded); embedded.add_int("int_value", addition); return TopLevelContent::get_from(obj); }; @@ -4144,3 +4145,1674 @@ TEST_CASE("client reset with embedded object", "[sync][pbs][client reset][embedd } } } + +TEST_CASE("client reset with nested collection", "[client reset][local][nested collection]") { + + if (!util::EventLoop::has_implementation()) + return; + + // remove this check once sync is ready + if (!realm::sync::SYNC_SUPPORTS_NESTED_COLLECTIONS) + return; + + TestSyncManager init_sync_manager; + SyncTestFile config(init_sync_manager.app(), "default"); + config.cache = false; + config.automatic_change_notifications = false; + ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover); + CAPTURE(test_mode); + config.sync_config->client_resync_mode = test_mode; + + ObjectSchema shared_class = {"object", + { + {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, + {"value", PropertyType::Int}, + }}; + + config.schema = Schema{shared_class, + {"TopLevel", + { + {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, + {"any_mixed", PropertyType::Mixed | PropertyType::Nullable}, + }}}; + + SECTION("add nested collection locally") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = Schema{shared_class}; + + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset->make_local_changes([&](SharedRealm local) { + advance_and_notify(*local); + TableRef table = get_table(*local, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{local, obj, col}; + list.insert_collection(0, CollectionType::List); + auto nlist = list.get_list(0); + nlist.add(Mixed{10}); + nlist.add(Mixed{"Test"}); + REQUIRE(table->size() == 1); + }); + if (test_mode == ClientResyncMode::DiscardLocal) { + REQUIRE_THROWS_WITH(test_reset->run(), "Client reset cannot recover when classes have been removed: " + "{TopLevel}"); + } + else { + test_reset + ->on_post_reset([&](SharedRealm local) { + advance_and_notify(*local); + TableRef table = get_table(*local, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local, obj, col}; + REQUIRE(list.size() == 1); + auto nlist = list.get_list(0); + REQUIRE(nlist.size() == 2); + REQUIRE(nlist.get_any(0).get_int() == 10); + REQUIRE(nlist.get_any(1).get_string() == "Test"); + }) + ->run(); + } + } + SECTION("server adds nested collection. List of nested collections") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + config.schema = Schema{shared_class}; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + + test_reset + ->make_remote_changes([&](SharedRealm remote) { + advance_and_notify(*remote); + TableRef table = get_table(*remote, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + // List + obj.set_collection(col, CollectionType::List); + List list{remote, obj, col}; + // primitive type + list.add(Mixed{42}); + // List> + list.insert_collection(1, CollectionType::List); + auto nlist = list.get_list(1); + nlist.add(Mixed{10}); + nlist.add(Mixed{"Test"}); + // List + list.insert_collection(2, CollectionType::Dictionary); + auto n_dict = list.get_dictionary(2); + n_dict.insert("Test", Mixed{"10"}); + n_dict.insert("Test1", Mixed{10}); + // List> + list.insert_collection(3, CollectionType::Set); + auto n_set = list.get_set(3); + n_set.insert(Mixed{"Hello"}); + n_set.insert(Mixed{"World"}); + REQUIRE(list.size() == 4); + REQUIRE(table->size() == 1); + }) + ->on_post_reset([&](SharedRealm local) { + advance_and_notify(*local); + TableRef table = get_table(*local, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local, obj, col}; + REQUIRE(list.size() == 4); + auto mixed = list.get_any(0); + REQUIRE(mixed.get_int() == 42); + auto nlist = list.get_list(1); + REQUIRE(nlist.size() == 2); + REQUIRE(nlist.get_any(0).get_int() == 10); + REQUIRE(nlist.get_any(1).get_string() == "Test"); + auto n_dict = list.get_dictionary(2); + REQUIRE(n_dict.size() == 2); + REQUIRE(n_dict.get("Test").get_string() == "10"); + REQUIRE(n_dict.get("Test1").get_int() == 10); + auto n_set = list.get_set(3); + REQUIRE(n_set.size() == 2); + REQUIRE(n_set.find_any("Hello") == 0); + REQUIRE(n_set.find_any("World") == 1); + }) + ->run(); + } + SECTION("server adds nested collection. Dictionary of nested collections") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + config.schema = Schema{shared_class}; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->make_remote_changes([&](SharedRealm remote) { + advance_and_notify(*remote); + TableRef table = get_table(*remote, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + // List + obj.set_collection(col, CollectionType::Dictionary); + object_store::Dictionary dict{remote, obj, col}; + // primitive type + dict.insert("Scalar", Mixed{42}); + // Dictionary> + dict.insert_collection("List", CollectionType::List); + auto nlist = dict.get_list("List"); + nlist.add(Mixed{10}); + nlist.add(Mixed{"Test"}); + // Dictionary + dict.insert_collection("Dict", CollectionType::Dictionary); + auto n_dict = dict.get_dictionary("Dict"); + n_dict.insert("Test", Mixed{"10"}); + n_dict.insert("Test1", Mixed{10}); + // List> + dict.insert_collection("Set", CollectionType::Set); + auto n_set = dict.get_set("Set"); + n_set.insert(Mixed{"Hello"}); + n_set.insert(Mixed{"World"}); + REQUIRE(dict.size() == 4); + REQUIRE(table->size() == 1); + }) + ->on_post_reset([&](SharedRealm local) { + advance_and_notify(*local); + TableRef table = get_table(*local, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dict{local, obj, col}; + REQUIRE(dict.size() == 4); + auto mixed = dict.get_any("Scalar"); + REQUIRE(mixed.get_int() == 42); + auto nlist = dict.get_list("List"); + REQUIRE(nlist.size() == 2); + REQUIRE(nlist.get_any(0).get_int() == 10); + REQUIRE(nlist.get_any(1).get_string() == "Test"); + auto n_dict = dict.get_dictionary("Dict"); + REQUIRE(n_dict.size() == 2); + REQUIRE(n_dict.get("Test").get_string() == "10"); + REQUIRE(n_dict.get("Test1").get_int() == 10); + auto n_set = dict.get_set("Set"); + REQUIRE(n_set.size() == 2); + REQUIRE(n_set.find_any("Hello") == 0); + REQUIRE(n_set.find_any("World") == 1); + }) + ->run(); + } + SECTION("add nested collection both locally and remotely List vs Set") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->make_local_changes([&](SharedRealm local) { + advance_and_notify(*local); + auto table = get_table(*local, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{local, obj, col}; + list.insert(0, Mixed{30}); + REQUIRE(list.size() == 1); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + auto table = get_table(*remote_realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::Set); + object_store::Set set{remote_realm, obj, col}; + set.insert(Mixed{40}); + REQUIRE(set.size() == 1); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + if (test_mode == ClientResyncMode::DiscardLocal) { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Set set{local_realm, obj, col}; + REQUIRE(set.size() == 1); + REQUIRE(set.get_any(0).get_int() == 40); + } + else { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + REQUIRE(list.get_any(0).get_int() == 30); + } + }) + ->run(); + } + SECTION("add nested collection both locally and remotely List vs Dictionary") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->make_local_changes([&](SharedRealm local) { + advance_and_notify(*local); + auto table = get_table(*local, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{local, obj, col}; + list.insert(0, Mixed{30}); + REQUIRE(list.size() == 1); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + auto table = get_table(*remote_realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::Dictionary); + object_store::Dictionary dict{remote_realm, obj, col}; + dict.insert("Test", Mixed{40}); + REQUIRE(dict.size() == 1); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + if (test_mode == ClientResyncMode::DiscardLocal) { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 1); + REQUIRE(dictionary.get_any("Test").get_int() == 40); + } + else { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + REQUIRE(list.get_any(0) == 30); + } + }) + ->run(); + } + SECTION("add nested collection both locally and remotely. Nesting levels mismatch List vs Dictionary") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->make_local_changes([&](SharedRealm local) { + advance_and_notify(*local); + auto table = get_table(*local, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{local, obj, col}; + list.insert_collection(0, CollectionType::Dictionary); + auto dict = list.get_dictionary(0); + dict.insert("Test", Mixed{30}); + REQUIRE(list.size() == 1); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + auto table = get_table(*remote_realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{remote_realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto nlist = list.get_list(0); + nlist.insert(0, Mixed{30}); + REQUIRE(nlist.size() == 1); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + if (test_mode == ClientResyncMode::DiscardLocal) { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + auto nlist = list.get_list(0); + REQUIRE(nlist.size() == 1); + REQUIRE(nlist.get(0).get_int() == 30); + } + else { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto n_dict = list.get_dictionary(0); + REQUIRE(n_dict.size() == 1); + REQUIRE(n_dict.get("Test").get_int() == 30); + auto n_list = list.get_list(1); + REQUIRE(n_list.size() == 1); + REQUIRE(n_list.get_any(0) == 30); + } + }) + ->run(); + } + SECTION("add nested collection both locally and remotely. Collections matched. Merge collections if not discard " + "local") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->make_local_changes([&](SharedRealm local) { + advance_and_notify(*local); + auto table = get_table(*local, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{local, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + list.insert_collection(1, CollectionType::Dictionary); + auto dict = list.get_dictionary(1); + dict.insert("Test", Mixed{10}); + REQUIRE(list.size() == 2); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + auto table = get_table(*remote_realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{remote_realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{40}); + list.insert_collection(1, CollectionType::Dictionary); + auto dict = list.get_dictionary(1); + dict.insert("Test1", Mixed{11}); + REQUIRE(list.size() == 2); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + if (test_mode == ClientResyncMode::DiscardLocal) { + REQUIRE(list.size() == 2); + auto n_list = list.get_list(0); + REQUIRE(n_list.get_any(0).get_int() == 40); + auto n_dict = list.get_dictionary(1); + REQUIRE(n_dict.size() == 1); + REQUIRE(n_dict.get("Test1").get_int() == 11); + } + else { + REQUIRE(list.size() == 4); + auto n_list = list.get_list(0); + REQUIRE(n_list.size() == 1); + REQUIRE(n_list.get_any(0).get_int() == 30); + auto n_dict = list.get_dictionary(1); + REQUIRE(n_dict.size() == 1); + REQUIRE(n_dict.get("Test").get_int() == 10); + auto n_list1 = list.get_list(2); + REQUIRE(n_list1.size() == 1); + REQUIRE(n_list1.get_any(0).get_int() == 40); + auto n_dict1 = list.get_dictionary(3); + REQUIRE(n_dict1.size() == 1); + REQUIRE(n_dict1.get("Test1").get_int() == 11); + } + }) + ->run(); + } + SECTION("add nested collection both locally and remotely. Collections matched. Mix collections with values") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->make_local_changes([&](SharedRealm local) { + advance_and_notify(*local); + auto table = get_table(*local, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{local, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + list.insert_collection(1, CollectionType::Dictionary); + auto dict = list.get_dictionary(1); + dict.insert("Test", Mixed{10}); + list.insert(0, Mixed{2}); // this shifts all the other collections by 1 + REQUIRE(list.size() == 3); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + auto table = get_table(*remote_realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{remote_realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{40}); + list.insert_collection(1, CollectionType::Dictionary); + auto dict = list.get_dictionary(1); + dict.insert("Test1", Mixed{11}); + list.insert(0, Mixed{30}); // this shifts all the other collections by 1 + REQUIRE(list.size() == 3); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + if (test_mode == ClientResyncMode::DiscardLocal) { + REQUIRE(list.size() == 3); + REQUIRE(list.get_any(0).get_int() == 30); + auto n_list = list.get_list(1); + REQUIRE(n_list.get_any(0).get_int() == 40); + auto n_dict = list.get_dictionary(2); + REQUIRE(n_dict.size() == 1); + REQUIRE(n_dict.get("Test1").get_int() == 11); + } + else { + // local + REQUIRE(list.size() == 6); + REQUIRE(list.get_any(0).get_int() == 2); + auto n_list = list.get_list(1); + REQUIRE(n_list.size() == 1); + REQUIRE(n_list.get_any(0).get_int() == 30); + auto n_dict = list.get_dictionary(2); + REQUIRE(n_dict.size() == 1); + REQUIRE(n_dict.get("Test").get_int() == 10); + // remote + REQUIRE(list.get_any(3).get_int() == 30); + auto n_list1 = list.get_list(4); + REQUIRE(n_list1.size() == 1); + REQUIRE(n_list1.get_any(0).get_int() == 40); + auto n_dict1 = list.get_dictionary(5); + REQUIRE(n_dict1.size() == 1); + REQUIRE(n_dict1.get("Test1").get_int() == 11); + } + }) + ->run(); + } + SECTION("add nested collection both locally and remotely. Collections do not match") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->make_local_changes([&](SharedRealm local) { + advance_and_notify(*local); + auto table = get_table(*local, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{local, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + auto table = get_table(*remote_realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::Dictionary); + object_store::Dictionary dict{remote_realm, obj, col}; + dict.insert_collection("List", CollectionType::List); + auto n_list = dict.get_list("List"); + n_list.insert(0, Mixed{30}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + object_store::Dictionary dict{local_realm, obj, col}; + REQUIRE(dict.size() == 1); + auto n_list = dict.get_list("List"); + REQUIRE(n_list.size() == 1); + REQUIRE(n_list.get_any(0).get_int() == 30); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + auto n_list = list.get_list(0); + REQUIRE(n_list.size() == 1); + REQUIRE(n_list.get_any(0).get_int() == 30); + } + }) + ->run(); + } + SECTION("delete collection remotely and add locally. Collections do not match") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + list.insert_collection(1, CollectionType::List); + n_list = list.get_list(1); + n_list.insert(0, Mixed{31}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{50}); + REQUIRE(list.size() == 3); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + REQUIRE(list.size() == 2); + list.remove(0); + auto n_list = list.get_list(0); + REQUIRE(n_list.get_any(0).get_int() == 31); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + auto n_list = list.get_list(0); + REQUIRE(n_list.get_any(0).get_int() == 31); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto n_list1 = list.get_list(0); + auto n_list2 = list.get_list(1); + REQUIRE(n_list1.size() == 1); + REQUIRE(n_list2.size() == 1); + REQUIRE(n_list1.get_any(0).get_int() == 50); + REQUIRE(n_list2.get_any(0).get_int() == 31); + } + }) + ->run(); + } + SECTION("delete collection remotely and add locally same index.") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{50}); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + REQUIRE(list.size() == 1); + list.remove(0); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + if (test_mode == ClientResyncMode::DiscardLocal) { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 0); + } + else { + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + auto nlist = list.get_list(0); + REQUIRE(nlist.size() == 1); + REQUIRE(nlist.get_any(0).get_int() == 50); + } + }) + ->run(); + } + SECTION("shift collection remotely and locally") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{50}); + list.insert_collection(0, CollectionType::List); // shift + auto n_list1 = list.get_list(0); + n_list1.insert(0, Mixed{150}); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + auto n_list = list.get_list(0); + n_list.insert(1, Mixed{100}); + list.insert_collection(0, CollectionType::List); // shift + auto n_list1 = list.get_list(0); + n_list1.insert(0, Mixed{42}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto n_list = list.get_list(0); + auto n_list1 = list.get_list(1); + REQUIRE(n_list.size() == 1); + REQUIRE(n_list1.size() == 2); + REQUIRE(n_list1.get_any(0).get_int() == 30); + REQUIRE(n_list1.get_any(1).get_int() == 100); + REQUIRE(n_list.get_any(0).get_int() == 42); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 3); + auto n_list = list.get_list(0); + auto n_list1 = list.get_list(1); + auto n_list2 = list.get_list(2); + REQUIRE(n_list.size() == 1); + REQUIRE(n_list1.size() == 2); + REQUIRE(n_list2.size() == 2); + REQUIRE(n_list.get_any(0).get_int() == 150); + REQUIRE(n_list1.get_any(0).get_int() == 50); + REQUIRE(n_list1.get_any(1).get_int() == 42); + REQUIRE(n_list2.get_any(0).get_int() == 30); + REQUIRE(n_list2.get_any(1).get_int() == 100); + } + }) + ->run(); + } + SECTION("delete collection locally (list). Local should win") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + list.remove(0); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + list.add(Mixed{10}); + REQUIRE(list.size() == 2); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto n_list1 = list.get_list(0); + auto mixed = list.get_any(1); + REQUIRE(n_list1.size() == 1); + REQUIRE(mixed.get_int() == 10); + REQUIRE(n_list1.get_any(0).get_int() == 30); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 0); + } + }) + ->run(); + } + SECTION("move collection locally (list). Local should win") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto n_list = list.get_list(0); + n_list.insert(0, Mixed{30}); + n_list.insert(1, Mixed{10}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + auto nlist = list.get_list(0); + nlist.move(0, 1); // move value 30 in pos 1. + REQUIRE(nlist.size() == 2); + REQUIRE(nlist.get_any(0).get_int() == 10); + REQUIRE(nlist.get_any(1).get_int() == 30); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + REQUIRE(list.size() == 1); + auto nlist = list.get_list(0); + REQUIRE(nlist.size() == 2); + nlist.add(Mixed{2}); + REQUIRE(nlist.size() == 3); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + // local state is preserved + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + auto nlist = list.get_list(0); + REQUIRE(nlist.size() == 3); + REQUIRE(nlist.get_any(0).get_int() == 30); + REQUIRE(nlist.get_any(1).get_int() == 10); + REQUIRE(nlist.get_any(2).get_int() == 2); + } + else { + // local change wins + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + auto nlist = list.get_list(0); + REQUIRE(nlist.size() == 2); + REQUIRE(nlist.get_any(0).get_int() == 10); + REQUIRE(nlist.get_any(1).get_int() == 30); + } + }) + ->run(); + } + SECTION("delete collection locally (dictionary). Local should win") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::Dictionary); + object_store::Dictionary dictionary{realm, obj, col}; + dictionary.insert_collection("Test", CollectionType::Dictionary); + auto n_dictionary = dictionary.get_dictionary("Test"); + n_dictionary.insert("Val", 30); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 1); + dictionary.erase("Test"); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dictionary{remote_realm, obj, col}; + REQUIRE(dictionary.size() == 1); + auto n_dictionary = dictionary.get_dictionary("Test"); + n_dictionary.insert("Val1", 31); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 1); + auto n_dictionary = dictionary.get_dictionary("Test"); + REQUIRE(n_dictionary.get_any("Val").get_int() == 30); + REQUIRE(n_dictionary.get_any("Val1").get_int() == 31); + } + else { + // local change wins + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 0); + } + }) + ->run(); + } + // testing copying logic for nested collections + SECTION("Verify copy logic for collections in mixed.") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::List); + list.insert_collection(1, CollectionType::Dictionary); + auto nlist = list.get_list(0); + auto ndict = list.get_dictionary(1); + nlist.add(Mixed{1}); + nlist.add(Mixed{"Test"}); + ndict.insert("Int", Mixed(3)); + ndict.insert("String", Mixed("Test")); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto nlist = list.get_list(0); + nlist.add(Mixed{4}); + auto ndict = list.get_dictionary(1); + ndict.insert("Int2", 6); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + REQUIRE(list.size() == 2); + auto nlist = list.get_list(0); + nlist.add(Mixed{7}); + auto ndict = list.get_dictionary(1); + ndict.insert("Int3", Mixed{9}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + // db must be equal to remote + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto nlist = list.get_list(0); + auto ndict = list.get_dictionary(1); + REQUIRE(nlist.size() == 3); + REQUIRE(ndict.size() == 3); + REQUIRE(nlist.get_any(0).get_int() == 1); + REQUIRE(nlist.get_any(1).get_string() == "Test"); + REQUIRE(nlist.get_any(2).get_int() == 7); + REQUIRE(ndict.get_any("Int").get_int() == 3); + REQUIRE(ndict.get_any("String").get_string() == "Test"); + REQUIRE(ndict.get_any("Int3").get_int() == 9); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto nlist = list.get_list(0); + auto ndict = list.get_dictionary(1); + REQUIRE(nlist.size() == 4); + REQUIRE(ndict.size() == 4); + REQUIRE(nlist.get_any(0).get_int() == 1); + REQUIRE(nlist.get_any(1).get_string() == "Test"); + REQUIRE(nlist.get_any(2).get_int() == 4); + REQUIRE(nlist.get_any(3).get_int() == 7); + REQUIRE(ndict.get_any("Int").get_int() == 3); + REQUIRE(ndict.get_any("String").get_string() == "Test"); + REQUIRE(ndict.get_any("Int2").get_int() == 6); + REQUIRE(ndict.get_any("Int3").get_int() == 9); + } + }) + ->run(); + } + SECTION("Verify copy logic for collections in mixed. Mismatch at index i") { + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + list.insert_collection(0, CollectionType::List); + auto nlist = list.get_list(0); + nlist.add(Mixed{"Local"}); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + list.insert_collection(0, CollectionType::Dictionary); + auto ndict = list.get_dictionary(0); + ndict.insert("Test", Mixed{"Remote"}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + if (test_mode == ClientResyncMode::DiscardLocal) { + // db must be equal to remote + List list{local_realm, obj, col}; + REQUIRE(list.size() == 1); + auto ndict = list.get_dictionary(0); + REQUIRE(ndict.size() == 1); + REQUIRE(ndict.get_any("Test").get_string() == "Remote"); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + auto nlist = list.get_list(0); + auto ndict = list.get_dictionary(1); + REQUIRE(ndict.get_any("Test").get_string() == "Remote"); + REQUIRE(nlist.get_any(0).get_string() == "Local"); + } + }) + ->run(); + } + SECTION("Verify copy and notification logic for List and scalar types") { + Results results; + Object object; + List list_listener, nlist_setup_listener, nlist_local_listener; + CollectionChangeSet list_changes, nlist_setup_changes, nlist_local_changes; + NotificationToken list_token, nlist_setup_token, nlist_local_token; + + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::List); + list.add(Mixed{"Setup"}); + auto nlist = list.get_list(0); + nlist.add(Mixed{"Setup"}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + list.insert_collection(0, CollectionType::List); + list.add(Mixed{"Local"}); + auto nlist = list.get_list(0); + nlist.add(Mixed{"Local"}); + }) + ->on_post_local_changes([&](SharedRealm realm) { + TableRef table = get_table(*realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + list_listener = List{realm, obj, col}; + REQUIRE(list_listener.size() == 4); + list_token = list_listener.add_notification_callback([&](CollectionChangeSet changes) { + list_changes = std::move(changes); + }); + auto nlist_setup = list_listener.get_list(1); + REQUIRE(nlist_setup.size() == 1); + REQUIRE(nlist_setup.get_any(0) == Mixed{"Setup"}); + nlist_setup_listener = nlist_setup; + nlist_setup_token = nlist_setup_listener.add_notification_callback([&](CollectionChangeSet changes) { + nlist_setup_changes = std::move(changes); + }); + auto nlist_local = list_listener.get_list(0); + REQUIRE(nlist_local.size() == 1); + REQUIRE(nlist_local.get_any(0) == Mixed{"Local"}); + nlist_local_listener = nlist_local; + nlist_local_token = nlist_local_listener.add_notification_callback([&](CollectionChangeSet changes) { + nlist_local_changes = std::move(changes); + }); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + REQUIRE(list.size() == 2); + list.insert_collection(0, CollectionType::List); + list.add(Mixed{"Remote"}); + auto nlist = list.get_list(0); + nlist.add(Mixed{"Remote"}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + + if (test_mode == ClientResyncMode::DiscardLocal) { + // db must be equal to remote + List list{local_realm, obj, col}; + REQUIRE(list.size() == 4); + auto nlist_remote = list.get_list(0); + auto nlist_setup = list.get_list(1); + auto mixed_setup = list.get_any(2); + auto mixed_remote = list.get_any(3); + REQUIRE(nlist_remote.size() == 1); + REQUIRE(nlist_setup.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(nlist_remote.get_any(0).get_string() == "Remote"); + REQUIRE(nlist_setup.get_any(0).get_string() == "Setup"); + REQUIRE(list_listener.is_valid()); + REQUIRE_INDICES(list_changes.deletions); // old nested collection deleted + REQUIRE_INDICES(list_changes.insertions); // new nested collection inserted + REQUIRE_INDICES(list_changes.modifications, 0, + 3); // replace Local with Remote at position 0 and 3 + REQUIRE(!nlist_local_changes.collection_root_was_deleted); // original local collection deleted + REQUIRE(!nlist_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(nlist_setup_changes.insertions); // there are no new insertions or deletions + REQUIRE_INDICES(nlist_setup_changes.deletions); + REQUIRE_INDICES(nlist_setup_changes.modifications); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 6); + auto nlist_local = list.get_list(0); + auto nlist_remote = list.get_list(1); + auto nlist_setup = list.get_list(2); + auto mixed_local = list.get_any(3); + auto mixed_setup = list.get_any(4); + auto mixed_remote = list.get_any(5); + // local, remote changes are kept + REQUIRE(nlist_remote.size() == 1); + REQUIRE(nlist_setup.size() == 1); + REQUIRE(nlist_local.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(mixed_local.get_string() == "Local"); + REQUIRE(nlist_remote.get_any(0).get_string() == "Remote"); + REQUIRE(nlist_local.get_any(0).get_string() == "Local"); + REQUIRE(nlist_setup.get_any(0).get_string() == "Setup"); + // notifications + REQUIRE(list_listener.is_valid()); + // src is [ [Local],[Remote],[Setup], Local, Setup, Remote ] + // dst is [ [Local], [Setup], Setup, Local] + // no deletions + REQUIRE_INDICES(list_changes.deletions); + // inserted "Setup" and "Remote" at the end + REQUIRE_INDICES(list_changes.insertions, 4, 5); + // changed [Setup] ==> [Remote] and Setup ==> [Setup] + REQUIRE_INDICES(list_changes.modifications, 1, 2); + REQUIRE(!nlist_local_changes.collection_root_was_deleted); + REQUIRE_INDICES(nlist_local_changes.insertions); + REQUIRE_INDICES(nlist_local_changes.deletions); + REQUIRE(!nlist_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(nlist_setup_changes.insertions); + REQUIRE_INDICES(nlist_setup_changes.deletions); + } + }) + ->run(); + } + SECTION("Verify copy and notification logic for Dictionary and scalar types") { + Results results; + Object object; + object_store::Dictionary dictionary_listener; + List nlist_setup_listener, nlist_local_listener; + CollectionChangeSet dictionary_changes, nlist_setup_changes, nlist_local_changes; + NotificationToken dictionary_token, nlist_setup_token, nlist_local_token; + + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::Dictionary); + object_store::Dictionary dictionary{realm, obj, col}; + dictionary.insert_collection("[Setup]", CollectionType::List); + dictionary.insert("Setup", Mixed{"Setup"}); + auto nlist = dictionary.get_list("[Setup]"); + nlist.add(Mixed{"Setup"}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 2); + dictionary.insert_collection("[Local]", CollectionType::List); + dictionary.insert("Local", Mixed{"Local"}); + auto nlist = dictionary.get_list("[Local]"); + nlist.add(Mixed{"Local"}); + }) + ->on_post_local_changes([&](SharedRealm realm) { + TableRef table = get_table(*realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + dictionary_listener = object_store::Dictionary{realm, obj, col}; + REQUIRE(dictionary_listener.size() == 4); + dictionary_token = dictionary_listener.add_notification_callback([&](CollectionChangeSet changes) { + dictionary_changes = std::move(changes); + }); + auto nlist_setup = dictionary_listener.get_list("[Setup]"); + REQUIRE(nlist_setup.size() == 1); + REQUIRE(nlist_setup.get_any(0) == Mixed{"Setup"}); + nlist_setup_listener = nlist_setup; + nlist_setup_token = nlist_setup_listener.add_notification_callback([&](CollectionChangeSet changes) { + nlist_setup_changes = std::move(changes); + }); + auto nlist_local = dictionary_listener.get_list("[Local]"); + REQUIRE(nlist_local.size() == 1); + REQUIRE(nlist_local.get_any(0) == Mixed{"Local"}); + nlist_local_listener = nlist_local; + nlist_local_token = nlist_local_listener.add_notification_callback([&](CollectionChangeSet changes) { + nlist_local_changes = std::move(changes); + }); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dictionary{remote_realm, obj, col}; + REQUIRE(dictionary.size() == 2); + dictionary.insert_collection("[Remote]", CollectionType::List); + dictionary.insert("Remote", Mixed{"Remote"}); + auto nlist = dictionary.get_list("[Remote]"); + nlist.add(Mixed{"Remote"}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + + if (test_mode == ClientResyncMode::DiscardLocal) { + // db must be equal to remote + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 4); + auto nlist_remote = dictionary.get_list("[Remote]"); + auto nlist_setup = dictionary.get_list("[Setup]"); + auto mixed_setup = dictionary.get_any("Setup"); + auto mixed_remote = dictionary.get_any("Remote"); + REQUIRE(nlist_remote.size() == 1); + REQUIRE(nlist_setup.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(nlist_remote.get_any(0).get_string() == "Remote"); + REQUIRE(nlist_setup.get_any(0).get_string() == "Setup"); + REQUIRE(dictionary_listener.is_valid()); + REQUIRE_INDICES(dictionary_changes.deletions, 0, 2); // remove [Local], Local + REQUIRE_INDICES(dictionary_changes.insertions, 0, 2); // insert [Remote], Remote + REQUIRE_INDICES( + dictionary_changes.modifications); // replace Local with Remote at position 0 and 3 + REQUIRE(nlist_local_changes.collection_root_was_deleted); // local list is deleted + REQUIRE(!nlist_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(nlist_setup_changes.insertions); // there are no new insertions or deletions + REQUIRE_INDICES(nlist_setup_changes.deletions); + REQUIRE_INDICES(nlist_setup_changes.modifications); + } + else { + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 6); + auto nlist_local = dictionary.get_list("[Local]"); + auto nlist_remote = dictionary.get_list("[Remote]"); + auto nlist_setup = dictionary.get_list("[Setup]"); + auto mixed_local = dictionary.get_any("Local"); + auto mixed_setup = dictionary.get_any("Setup"); + auto mixed_remote = dictionary.get_any("Remote"); + // local, remote changes are kept + REQUIRE(nlist_remote.size() == 1); + REQUIRE(nlist_setup.size() == 1); + REQUIRE(nlist_local.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(mixed_local.get_string() == "Local"); + REQUIRE(nlist_remote.get_any(0).get_string() == "Remote"); + REQUIRE(nlist_local.get_any(0).get_string() == "Local"); + REQUIRE(nlist_setup.get_any(0).get_string() == "Setup"); + // notifications + REQUIRE(dictionary_listener.is_valid()); + // src is [ [Local],[Remote],[Setup], Local, Setup, Remote ] + // dst is [ [Local], [Setup], Setup, Local] + // no deletions + REQUIRE_INDICES(dictionary_changes.deletions); + // inserted "[Remote]" and "Remote" + REQUIRE_INDICES(dictionary_changes.insertions, 1, 4); + REQUIRE_INDICES(dictionary_changes.modifications); + REQUIRE(!nlist_local_changes.collection_root_was_deleted); + REQUIRE_INDICES(nlist_local_changes.insertions); + REQUIRE_INDICES(nlist_local_changes.deletions); + REQUIRE(!nlist_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(nlist_setup_changes.insertions); + REQUIRE_INDICES(nlist_setup_changes.deletions); + } + }) + ->run(); + } + SECTION("Verify copy and notification logic for List and scalar types") { + Results results; + Object object; + List list_listener; + object_store::Dictionary ndictionary_setup_listener, ndictionary_local_listener; + CollectionChangeSet list_changes, ndictionary_setup_changes, ndictionary_local_changes; + NotificationToken list_token, ndictionary_setup_token, ndictionary_local_token; + + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::List); + List list{realm, obj, col}; + list.insert_collection(0, CollectionType::Dictionary); + list.add(Mixed{"Setup"}); + auto ndictionary = list.get_dictionary(0); + ndictionary.insert("Key", Mixed{"Setup"}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{local_realm, obj, col}; + REQUIRE(list.size() == 2); + list.insert_collection(0, CollectionType::Dictionary); + list.add(Mixed{"Local"}); + auto ndictionary = list.get_dictionary(0); + ndictionary.insert("Key", Mixed{"Local"}); + }) + ->on_post_local_changes([&](SharedRealm realm) { + TableRef table = get_table(*realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + list_listener = List{realm, obj, col}; + REQUIRE(list_listener.size() == 4); + list_token = list_listener.add_notification_callback([&](CollectionChangeSet changes) { + list_changes = std::move(changes); + }); + auto ndictionary_setup = list_listener.get_dictionary(1); + REQUIRE(ndictionary_setup.size() == 1); + REQUIRE(ndictionary_setup.get_any("Key") == Mixed{"Setup"}); + ndictionary_setup_listener = ndictionary_setup; + ndictionary_setup_token = + ndictionary_setup_listener.add_notification_callback([&](CollectionChangeSet changes) { + ndictionary_setup_changes = std::move(changes); + }); + auto ndictionary_local = list_listener.get_dictionary(0); + REQUIRE(ndictionary_local.size() == 1); + REQUIRE(ndictionary_local.get_any("Key") == Mixed{"Local"}); + ndictionary_local_listener = ndictionary_local; + ndictionary_local_token = + ndictionary_local_listener.add_notification_callback([&](CollectionChangeSet changes) { + ndictionary_local_changes = std::move(changes); + }); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + List list{remote_realm, obj, col}; + REQUIRE(list.size() == 2); + list.insert_collection(0, CollectionType::Dictionary); + list.add(Mixed{"Remote"}); + auto ndictionary = list.get_dictionary(0); + ndictionary.insert("Key", Mixed{"Remote"}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + + if (test_mode == ClientResyncMode::DiscardLocal) { + // db must be equal to remote + List list{local_realm, obj, col}; + REQUIRE(list.size() == 4); + auto ndictionary_remote = list.get_dictionary(0); + auto ndictionary_setup = list.get_dictionary(1); + auto mixed_setup = list.get_any(2); + auto mixed_remote = list.get_any(3); + REQUIRE(ndictionary_remote.size() == 1); + REQUIRE(ndictionary_setup.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(ndictionary_remote.get_any("Key").get_string() == "Remote"); + REQUIRE(ndictionary_setup.get_any("Key").get_string() == "Setup"); + REQUIRE(list_listener.is_valid()); + REQUIRE_INDICES(list_changes.deletions); // old nested collection deleted + REQUIRE_INDICES(list_changes.insertions); // new nested collection inserted + REQUIRE_INDICES(list_changes.modifications, 0, + 3); // replace Local with Remote at position 0 and 3 + REQUIRE( + !ndictionary_local_changes.collection_root_was_deleted); // original local collection deleted + REQUIRE(!ndictionary_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(ndictionary_setup_changes.insertions); // there are no new insertions or deletions + REQUIRE_INDICES(ndictionary_setup_changes.deletions); + REQUIRE_INDICES(ndictionary_setup_changes.modifications); + } + else { + List list{local_realm, obj, col}; + REQUIRE(list.size() == 6); + auto ndictionary_local = list.get_dictionary(0); + auto ndictionary_remote = list.get_dictionary(1); + auto ndictionary_setup = list.get_dictionary(2); + auto mixed_local = list.get_any(3); + auto mixed_setup = list.get_any(4); + auto mixed_remote = list.get_any(5); + // local, remote changes are kept + REQUIRE(ndictionary_remote.size() == 1); + REQUIRE(ndictionary_setup.size() == 1); + REQUIRE(ndictionary_local.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(mixed_local.get_string() == "Local"); + REQUIRE(ndictionary_remote.get_any("Key").get_string() == "Remote"); + REQUIRE(ndictionary_local.get_any("Key").get_string() == "Local"); + REQUIRE(ndictionary_setup.get_any("Key").get_string() == "Setup"); + // notifications + REQUIRE(list_listener.is_valid()); + // src is [ [Local],[Remote],[Setup], Local, Setup, Remote ] + // dst is [ [Local], [Setup], Setup, Local] + // no deletions + REQUIRE_INDICES(list_changes.deletions); + // inserted "Setup" and "Remote" at the end + REQUIRE_INDICES(list_changes.insertions, 4, 5); + // changed [Setup] ==> [Remote] and Setup ==> [Setup] + REQUIRE_INDICES(list_changes.modifications, 1, 2); + REQUIRE(!ndictionary_local_changes.collection_root_was_deleted); + REQUIRE_INDICES(ndictionary_local_changes.insertions); + REQUIRE_INDICES(ndictionary_local_changes.deletions); + REQUIRE(!ndictionary_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(ndictionary_setup_changes.insertions); + REQUIRE_INDICES(ndictionary_setup_changes.deletions); + } + }) + ->run(); + } + SECTION("Verify copy and notification logic for Dictionary and scalar types") { + Results results; + Object object; + object_store::Dictionary dictionary_listener, ndictionary_setup_listener, ndictionary_local_listener; + CollectionChangeSet dictionary_changes, ndictionary_setup_changes, ndictionary_local_changes; + NotificationToken dictionary_token, ndictionary_setup_token, ndictionary_local_token; + + ObjectId pk_val = ObjectId::gen(); + SyncTestFile config2(init_sync_manager.app(), "default"); + config2.schema = config.schema; + auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); + test_reset + ->setup([&](SharedRealm realm) { + auto table = get_table(*realm, "TopLevel"); + auto obj = table->create_object_with_primary_key(pk_val); + auto col = table->get_column_key("any_mixed"); + obj.set_collection(col, CollectionType::Dictionary); + object_store::Dictionary dictionary{realm, obj, col}; + dictionary.insert_collection("", CollectionType::Dictionary); + dictionary.insert("Key-Setup", Mixed{"Setup"}); + auto ndictionary = dictionary.get_dictionary(""); + ndictionary.insert("Key", Mixed{"Setup"}); + }) + ->make_local_changes([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dictionary{local_realm, obj, col}; + dictionary.insert_collection("", CollectionType::Dictionary); + dictionary.insert("Key-Local", Mixed{"Local"}); + auto ndictionary = dictionary.get_dictionary(""); + ndictionary.insert("Key", Mixed{"Local"}); + }) + ->on_post_local_changes([&](SharedRealm realm) { + TableRef table = get_table(*realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + dictionary_listener = object_store::Dictionary{realm, obj, col}; + REQUIRE(dictionary_listener.size() == 4); + dictionary_token = dictionary_listener.add_notification_callback([&](CollectionChangeSet changes) { + dictionary_changes = std::move(changes); + }); + auto ndictionary_setup = dictionary_listener.get_dictionary(""); + REQUIRE(ndictionary_setup.size() == 1); + REQUIRE(ndictionary_setup.get_any("Key") == Mixed{"Setup"}); + ndictionary_setup_listener = ndictionary_setup; + ndictionary_setup_token = + ndictionary_setup_listener.add_notification_callback([&](CollectionChangeSet changes) { + ndictionary_setup_changes = std::move(changes); + }); + auto ndictionary_local = dictionary_listener.get_dictionary(""); + REQUIRE(ndictionary_local.size() == 1); + REQUIRE(ndictionary_local.get_any("Key") == Mixed{"Local"}); + ndictionary_local_listener = ndictionary_local; + ndictionary_local_token = + ndictionary_local_listener.add_notification_callback([&](CollectionChangeSet changes) { + ndictionary_local_changes = std::move(changes); + }); + }) + ->make_remote_changes([&](SharedRealm remote_realm) { + advance_and_notify(*remote_realm); + TableRef table = get_table(*remote_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + object_store::Dictionary dictionary{remote_realm, obj, col}; + REQUIRE(dictionary.size() == 2); + dictionary.insert_collection("", CollectionType::Dictionary); + dictionary.insert("Key-Remote", Mixed{"Remote"}); + auto ndictionary = dictionary.get_dictionary(""); + ndictionary.insert("Key", Mixed{"Remote"}); + }) + ->on_post_reset([&](SharedRealm local_realm) { + advance_and_notify(*local_realm); + TableRef table = get_table(*local_realm, "TopLevel"); + REQUIRE(table->size() == 1); + auto obj = table->get_object(0); + auto col = table->get_column_key("any_mixed"); + + if (test_mode == ClientResyncMode::DiscardLocal) { + // db must be equal to remote + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 4); + auto ndictionary_remote = dictionary.get_dictionary(""); + auto ndictionary_setup = dictionary.get_dictionary(""); + auto mixed_setup = dictionary.get_any("Key-Setup"); + auto mixed_remote = dictionary.get_any("Key-Remote"); + REQUIRE(ndictionary_remote.size() == 1); + REQUIRE(ndictionary_setup.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(ndictionary_remote.get_any("Key").get_string() == "Remote"); + REQUIRE(ndictionary_setup.get_any("Key").get_string() == "Setup"); + REQUIRE(dictionary_listener.is_valid()); + REQUIRE_INDICES(dictionary_changes.deletions, 0, 2); + REQUIRE_INDICES(dictionary_changes.insertions, 0, 2); + REQUIRE_INDICES(dictionary_changes.modifications); + REQUIRE(ndictionary_local_changes.collection_root_was_deleted); + REQUIRE(!ndictionary_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(ndictionary_setup_changes.insertions); + REQUIRE_INDICES(ndictionary_setup_changes.deletions); + REQUIRE_INDICES(ndictionary_setup_changes.modifications); + } + else { + object_store::Dictionary dictionary{local_realm, obj, col}; + REQUIRE(dictionary.size() == 6); + auto ndictionary_local = dictionary.get_dictionary(""); + auto ndictionary_remote = dictionary.get_dictionary(""); + auto ndictionary_setup = dictionary.get_dictionary(""); + auto mixed_local = dictionary.get_any("Key-Local"); + auto mixed_setup = dictionary.get_any("Key-Setup"); + auto mixed_remote = dictionary.get_any("Key-Remote"); + // local, remote changes are kept + REQUIRE(ndictionary_remote.size() == 1); + REQUIRE(ndictionary_setup.size() == 1); + REQUIRE(ndictionary_local.size() == 1); + REQUIRE(mixed_setup.get_string() == "Setup"); + REQUIRE(mixed_remote.get_string() == "Remote"); + REQUIRE(mixed_local.get_string() == "Local"); + REQUIRE(ndictionary_remote.get_any("Key").get_string() == "Remote"); + REQUIRE(ndictionary_local.get_any("Key").get_string() == "Local"); + REQUIRE(ndictionary_setup.get_any("Key").get_string() == "Setup"); + // notifications + REQUIRE(dictionary_listener.is_valid()); + // src is [ [Local],[Remote],[Setup], Local, Setup, Remote ] + // dst is [ [Local], [Setup], Setup, Local] + // no deletions + REQUIRE_INDICES(dictionary_changes.deletions); + REQUIRE_INDICES(dictionary_changes.insertions, 1, 4); + REQUIRE_INDICES(dictionary_changes.modifications); + REQUIRE(!ndictionary_local_changes.collection_root_was_deleted); + REQUIRE_INDICES(ndictionary_local_changes.insertions); + REQUIRE_INDICES(ndictionary_local_changes.deletions); + REQUIRE(!ndictionary_setup_changes.collection_root_was_deleted); + REQUIRE_INDICES(ndictionary_setup_changes.insertions); + REQUIRE_INDICES(ndictionary_setup_changes.deletions); + } + }) + ->run(); + } +} diff --git a/test/test_list.cpp b/test/test_list.cpp index e540eb7450a..be9d12b487f 100644 --- a/test/test_list.cpp +++ b/test/test_list.cpp @@ -832,43 +832,60 @@ TEST(List_NestedCollection_Links) auto list_col = origin->add_column_list(type_Mixed, "any_list"); auto any_col = origin->add_column(type_Mixed, "any"); - Obj o = origin->create_object(); Obj target_obj1 = target->create_object(); Obj target_obj2 = target->create_object(); + Obj target_obj3 = target->create_object(); + tr->commit_and_continue_as_read(); - Lst list = o.get_list(list_col); - list.insert_collection(0, CollectionType::Dictionary); - list.insert_collection(1, CollectionType::Dictionary); - - // Create link from a dictionary contained in a list - auto dict0 = list.get_dictionary(0); - dict0->insert("Key", target_obj2.get_link()); - - // Create link from a list contained in a dictionary contained in a list - auto dict1 = list.get_dictionary(1); - dict1->insert_collection("Hello", CollectionType::List); - ListMixedPtr list1 = dict1->get_list("Hello"); - list1->add(target_obj1.get_link()); + Obj o; + ListMixedPtr list; + ListMixedPtr list1; + ListMixedPtr list2; + Dictionary dict_any; + + auto create_links = [&]() { + tr->promote_to_write(); + o = origin->create_object(); + list = o.get_list_ptr(list_col); + list->insert_collection(0, CollectionType::Dictionary); + list->insert_collection(1, CollectionType::Dictionary); - // Create link from a collection nested in a Mixed property - o.set_collection(any_col, CollectionType::Dictionary); - auto dict_any = o.get_dictionary(any_col); - dict_any.insert("Godbye", target_obj1.get_link()); + // Create link from a dictionary contained in a list + auto dict0 = list->get_dictionary(0); + dict0->insert("Key", target_obj2.get_link()); + + // Create link from a list contained in a dictionary contained in a list + auto dict1 = list->get_dictionary(1); + dict1->insert_collection("Hello", CollectionType::List); + list1 = dict1->get_list("Hello"); + list1->add(target_obj1.get_link()); + + // Create link from a collection nested in a Mixed property + o.set_collection(any_col, CollectionType::Dictionary); + dict_any = o.get_dictionary(any_col); + dict_any.insert("Godbye", target_obj1.get_link()); + + // Create link from a list nested in a collection nested in a Mixed property + dict_any.insert_collection("List", CollectionType::List); + list2 = dict_any.get_list("List"); + list2->add(target_obj3.get_link()); + tr->commit_and_continue_as_read(); + // Check that backlinks are created + CHECK_EQUAL(target_obj1.get_backlink_count(), 2); + CHECK_EQUAL(target_obj2.get_backlink_count(), 1); + CHECK_EQUAL(target_obj3.get_backlink_count(), 1); + }; - // Check that backlinks are created - CHECK_EQUAL(target_obj1.get_backlink_count(), 2); - CHECK_EQUAL(target_obj2.get_backlink_count(), 1); - tr->commit_and_continue_as_read(); + create_links(); + // When target object is removed, link should be removed from list tr->promote_to_write(); target_obj1.remove(); tr->commit_and_continue_as_read(); - // When target object is removed, link should be removed from list CHECK_EQUAL(list1->size(), 0); // and cleared in dictionary CHECK_EQUAL(dict_any.get("Godbye"), Mixed()); - tr->promote_to_write(); // Create links again target_obj1 = target->create_object(); @@ -877,15 +894,25 @@ TEST(List_NestedCollection_Links) CHECK_EQUAL(target_obj1.get_backlink_count(), 2); // When list is removed, backlink should go - list.remove(1); + list->remove(1); CHECK_EQUAL(target_obj1.get_backlink_count(), 1); // This will implicitly delete dict_any o.set(any_col, Mixed(5)); CHECK_EQUAL(target_obj1.get_backlink_count(), 0); + CHECK_EQUAL(target_obj3.get_backlink_count(), 0); // Link still there CHECK_EQUAL(target_obj2.get_backlink_count(), 1); o.remove(); CHECK_EQUAL(target_obj2.get_backlink_count(), 0); + tr->commit_and_continue_as_read(); + + create_links(); + // Clearing dictionary should remove links + tr->promote_to_write(); + dict_any.clear(); + tr->commit_and_continue_as_read(); + CHECK_EQUAL(target_obj1.get_backlink_count(), 1); + CHECK_EQUAL(target_obj3.get_backlink_count(), 0); } TEST(List_NestedCollection_Unresolved)