diff --git a/src/realm/object_converter.cpp b/src/realm/object_converter.cpp index 0b41d969117..8f4b9aa4d63 100644 --- a/src/realm/object_converter.cpp +++ b/src/realm/object_converter.cpp @@ -15,6 +15,7 @@ * limitations under the License. * **************************************************************************/ +#include #include @@ -27,7 +28,7 @@ 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) { // 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 +38,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 +63,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 +81,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 +97,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) { - 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 +128,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 +159,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 +174,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) { - 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 +246,193 @@ 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, update_out); + copy_list(src_list, dst_list, update_out); + } + 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, update_out); + } + 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, update_out); + copy_dictionary(src_dict, dst_dict, update_out); + } + 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); + } + } +} + +void InterRealmValueConverter::handle_list_in_mixed(const Lst& src_list, Lst& dst_list, + bool* update_out) +{ + // utility functions + auto handle_set_copy = [&, this](size_t i) { + if (i >= dst_list.size()) + dst_list.insert_collection(i, CollectionType::Set); + auto n_src_set = src_list.get_set(i); + auto n_dst_set = dst_list.get_set(i); + copy_set(*n_src_set, *n_dst_set, update_out); + }; + + auto handle_list_copy = [&, this](size_t i) { + if (i >= dst_list.size()) + dst_list.insert_collection(i, CollectionType::List); + auto n_src_list = src_list.get_list(i); + auto n_dst_list = dst_list.get_list(i); + handle_list_in_mixed(*n_src_list, *n_dst_list, update_out); + copy_list(*n_src_list, *n_dst_list, update_out); + }; + + auto handle_dict_copy = [&, this](size_t i) { + if (i >= dst_list.size()) + dst_list.insert_collection(i, CollectionType::Dictionary); + auto n_src_dict = src_list.get_dictionary(i); + auto n_dst_dict = dst_list.get_dictionary(i); + handle_dictionary_in_mixed(*n_src_dict, *n_dst_dict, update_out); + copy_dictionary(*n_src_dict, *n_dst_dict, update_out); + }; + + // main algorithm + auto n = src_list.size(); + for (size_t i = 0; i < n; ++i) { + auto any = src_list.get_any(i); + if (any.is_type(type_List)) { + if (i < dst_list.size() && check_collection_in_mixed_mismatch(any, dst_list.get_any(i), type_List)) + dst_list.insert_collection(i, CollectionType::List); + handle_list_copy(i); + } + else if (any.is_type(type_Set)) { + // nested collections do not support nested sets, set can only be a terminal leaf + if (i < dst_list.size() && check_collection_in_mixed_mismatch(any, dst_list.get_any(i), type_Set)) + dst_list.insert_collection(i, CollectionType::Set); + handle_set_copy(i); + } + else if (any.is_type(type_Dictionary)) { + if (i < dst_list.size() && check_collection_in_mixed_mismatch(any, dst_list.get_any(i), type_Dictionary)) + dst_list.insert_collection(i, CollectionType::Dictionary); + handle_dict_copy(i); + } + else { + // copy single element + InterRealmValueConverter::ConversionResult converted_src; + auto dst_obj = dst_list.get_obj(); + auto dst_col = dst_list.get_col_key(); + auto dst_mixed = dst_obj.get_any(dst_col); + if (cmp_src_to_dst(any, dst_mixed, &converted_src, update_out)) { + dst_list.insert(i, converted_src.converted_value); } } } } +void InterRealmValueConverter::handle_dictionary_in_mixed(const Dictionary& src_dict, Dictionary& dst_dict, + bool* update_out) +{ + // utility functions + + auto handle_set_copy = [&, this](StringData key) { + if (!dst_dict.contains(key)) + dst_dict.insert_collection(key, CollectionType::Set); + auto n_src_set = src_dict.get_set(key); + auto n_dst_set = dst_dict.get_set(key); + copy_set(*n_src_set, *n_dst_set, update_out); + }; + auto handle_list_copy = [&, this](StringData key) { + if (!dst_dict.contains(key)) + dst_dict.insert_collection(key, CollectionType::List); + auto n_src_list = src_dict.get_list(key); + auto n_dst_list = dst_dict.get_list(key); + handle_list_in_mixed(*n_src_list, *n_dst_list, update_out); + copy_list(*n_src_list, *n_dst_list, update_out); + }; + auto handle_dict_copy = [&, this](StringData key) { + if (!dst_dict.contains(key)) + dst_dict.insert_collection(key, CollectionType::Dictionary); + auto n_src_dict = src_dict.get_dictionary(key); + auto n_dst_dict = dst_dict.get_dictionary(key); + handle_dictionary_in_mixed(*n_src_dict, *n_dst_dict, update_out); + copy_dictionary(*n_src_dict, *n_dst_dict, update_out); + }; + + // main algorithm + auto n = src_dict.size(); + for (size_t i = 0; i < n; ++i) { + auto [key, any] = src_dict.get_pair(i); + if (any.is_type(type_List)) { + // if (dst_dict.contains(key)) + // check_collection_in_mixed_mismatch(any, dst_dict.get(key), type_List); + handle_list_copy(key.get_string()); + } + else if (any.is_type(type_Set)) { + // nested collections do not support nested sets, set can only be a terminal leaf + // if (dst_dict.contains(key)) + // check_collection_in_mixed_mismatch(any, dst_dict.get(key), type_Set); + handle_set_copy(key.get_string()); + } + else if (any.is_type(type_Dictionary)) { + // if (dst_dict.contains(key)) + // check_collection_in_mixed_mismatch(any, dst_dict.get(key), type_Dictionary); + handle_dict_copy(key.get_string()); + } + else { + // copy single element + InterRealmValueConverter::ConversionResult converted_src; + auto dst_obj = dst_dict.get_obj(); + auto dst_col = dst_dict.get_col_key(); + auto dst_mixed = dst_obj.get_any(dst_col); + if (cmp_src_to_dst(any, dst_mixed, &converted_src, update_out)) { + dst_dict.insert(key, converted_src.converted_value); + } + } + } +} +bool InterRealmValueConverter::check_collection_in_mixed_mismatch(Mixed, Mixed dst, DataType type) +{ + auto is_null = dst.is_null(); + if (!is_null && !dst.is_type(type)) { + return true; + } + return false; +} // 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 diff --git a/src/realm/object_converter.hpp b/src/realm/object_converter.hpp index caec6c4bbc4..c999e657945 100644 --- a/src/realm/object_converter.hpp +++ b/src/realm/object_converter.hpp @@ -53,9 +53,14 @@ struct InterRealmValueConverter { 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); + void copy_set(const SetBase& src_obj, SetBase& dst_obj, bool* update_out); + void copy_dictionary(const Dictionary& src_obj, Dictionary& dst_obj, bool* update_out); + // collection in mixed. + void handle_list_in_mixed(const Lst& src_list, Lst& dst_list, bool* update_out); + void handle_dictionary_in_mixed(const Dictionary& src_dict, Dictionary& dst_dict, bool* update_out); + bool check_collection_in_mixed_mismatch(Mixed src, Mixed dst, DataType type); TableRef m_dst_link_table; ConstTableRef m_src_table; diff --git a/src/realm/sync/noinst/client_reset.cpp b/src/realm/sync/noinst/client_reset.cpp index 04c57aa52d2..43cb94b33f0 100644 --- a/src/realm/sync/noinst/client_reset.cpp +++ b/src/realm/sync/noinst/client_reset.cpp @@ -285,6 +285,10 @@ void transfer_group(const Transaction& group_src, Transaction& group_dst, util:: } else { // column preexists in dest, make sure the types match + // Note: conflicts may still arise for nested collections. + // the type of the column for a nested collection is mixed + // but the underlying collection type can be different. + // This can be discovered while copying the data. 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)", diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index 1ef07b67ba2..fb90d29fd5b 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", "[client reset][local][embedded o {"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", "[client reset][local][embedded o 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,515 @@ TEST_CASE("client reset with embedded object", "[client reset][local][embedded o } } } + +TEST_CASE("client reset with nested collection", "[client reset][local][nested collection]") { + + if (!util::EventLoop::has_implementation()) + 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}, + {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "EmbeddedObject"}, + }}, + {"EmbeddedObject", + ObjectSchema::ObjectType::Embedded, + { + {"name", PropertyType::String | PropertyType::Nullable}, + {"int_value", PropertyType::Int}, + }}}; + + 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) { + 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: " + "{EmbeddedObject, TopLevel}"); + } + else { + test_reset + ->on_post_reset([&](SharedRealm 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(); + } + // The tests above are testing a very simple condition, in which we basically have a nested collection added + // remotely and we verify that we have copied all the data, furthermore that locally we can handle the + // collecitions stored inside the Mixed. The following tests are a little bit more complicated, we try to change + // the mixed type remotely and locally and trigger some conflicts, that for nested collections are a little bit + // more difficult to handle, because of the nature of nesting collections itself. + 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) { + 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) { + 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) { + if (test_mode == ClientResyncMode::DiscardLocal) + return; + 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); + REQUIRE(list.get_any(0) == 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) { + 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) { + 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) { + if (test_mode == ClientResyncMode::DiscardLocal) + return; + 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); + 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) { + 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) { + 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) { + if (test_mode == ClientResyncMode::DiscardLocal) + return; + 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 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) { + 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) { + 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) { + 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) { + 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{2}); // 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() == 2); + 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() == 2); + 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) { + 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) { + 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); + } + }) + ->run(); + } +}