diff --git a/src/realm/dictionary.cpp b/src/realm/dictionary.cpp index 4b2a3191dda..3ff4b49e46a 100644 --- a/src/realm/dictionary.cpp +++ b/src/realm/dictionary.cpp @@ -959,9 +959,16 @@ 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); + + if (old_value.is_type(type_List)) { + get_list(key.get_string())->remove_backlinks(cascade_state); + } + else if (old_value.is_type(type_Dictionary)) { + get_dictionary(key.get_string())->remove_backlinks(cascade_state); + } + if (recurse) _impl::TableFriend::remove_recursive(*get_table_unchecked(), cascade_state); // Throws @@ -971,7 +978,6 @@ void Dictionary::do_erase(size_t ndx, Mixed key) m_keys->erase(ndx); m_values->erase(ndx); - bump_content_version(); } diff --git a/src/realm/obj.cpp b/src/realm/obj.cpp index a430e798fc8..983a05473b8 100644 --- a/src/realm/obj.cpp +++ b/src/realm/obj.cpp @@ -2031,11 +2031,15 @@ CollectionPtr Obj::get_collection_by_stable_path(const StablePath& path) const if (collection->get_collection_type() == CollectionType::List) { auto list_of_mixed = dynamic_cast*>(collection.get()); size_t ndx = list_of_mixed->find_index(index); + if (ndx == realm::not_found) + return {Mixed{}, PathElement{}}; return {list_of_mixed->get(ndx), PathElement(ndx)}; } else { auto dict = dynamic_cast(collection.get()); size_t ndx = dict->find_index(index); + if (ndx == realm::not_found) + return {Mixed{}, PathElement{}}; return {dict->get_any(ndx), PathElement(dict->get_key(ndx).get_string())}; } }; diff --git a/src/realm/object_converter.cpp b/src/realm/object_converter.cpp index ee0d4833146..30c2b015f18 100644 --- a/src/realm/object_converter.cpp +++ b/src/realm/object_converter.cpp @@ -23,7 +23,6 @@ #include #include -#include namespace realm::converters { // Takes two lists, src and dst, and makes dst equal src. src is unchanged. @@ -383,89 +382,63 @@ void InterRealmValueConverter::handle_list_in_mixed(const Lst& src_list, void InterRealmValueConverter::handle_dictionary_in_mixed(Dictionary& src_dictionary, Dictionary& dst_dictionary) const { - int sz = (int)std::min(src_dictionary.size(), dst_dictionary.size()); - int left = 0; - int right = (int)sz - 1; - - // find fist not matching element from beginning - while (left < sz) { - const auto [key_src, src_any] = src_dictionary.get_pair(left); - const auto [key_dst, dst_any] = dst_dictionary.get_pair(left); - if (src_any != dst_any || key_src != key_dst) - break; - if (is_collection(src_any) && !check_matching_dictionary(src_dictionary, dst_dictionary, key_src.get_string(), - to_collection_type(src_any))) - break; - left += 1; - } - - // find first not matching element from end - while (right >= 0) { - const auto [key_src, src_any] = src_dictionary.get_pair(right); - const auto [key_dst, dst_any] = dst_dictionary.get_pair(right); - if (src_any != dst_any || key_src != key_dst) - break; - if (is_collection(src_any) && !check_matching_dictionary(src_dictionary, dst_dictionary, key_src.get_string(), - to_collection_type(src_any))) - break; - right -= 1; - } - - // Replace all different elements in [left, right] - while (left <= right) { - const auto [key_src, src_any] = src_dictionary.get_pair(left); - const auto [key_dst, dst_any] = dst_dictionary.get_pair(left); - - // handle possible key mismatches - if (key_src != key_dst) { - dst_dictionary.erase(key_dst); - dst_dictionary.insert(key_src, src_any); - } - - if (is_collection(src_any)) { - auto coll_type = to_collection_type(src_any); - const auto key = key_src.get_string(); - - if (!dst_any.is_type(src_any.get_type())) { - // Mixed vs Collection - const auto key = key_src.get_string(); - dst_dictionary.set_collection(key, coll_type); - copy_dictionary_in_mixed(src_dictionary, dst_dictionary, key, coll_type); + 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 (!check_matching_dictionary(src_dictionary, dst_dictionary, key, coll_type)) { - // Collection vs Collection - dst_dictionary.insert(key_src, src_any); - copy_dictionary_in_mixed(src_dictionary, dst_dictionary, key, coll_type); + 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 (dst_any != src_any) { - // Mixed vs Mixed - dst_dictionary.insert(key_src, src_any); + else if (cmp < 0) { + to_insert.push_back(src_ndx); + src_ndx += 1; + } + else { + to_delete.push_back(dst_ndx); + dst_ndx += 1; } - left += 1; } - // remove dst elements not present in src - if (dst_dictionary.size() > src_dictionary.size()) { - auto dst_size = dst_dictionary.size(); - auto src_size = src_dictionary.size(); - while (dst_size > src_size) { - const auto [dst_key, dst_any] = dst_dictionary.get_pair(--dst_size); - dst_dictionary.erase(dst_key); - } + // append src to dst + while (src_ndx < src_dictionary.size()) { + to_insert.push_back(src_ndx); + src_ndx += 1; } - // append remainig src into dst - for (size_t i = dst_dictionary.size(); i < src_dictionary.size(); ++i) { - const auto [src_key, src_any] = src_dictionary.get_pair(i); - if (is_collection(src_any)) { - const auto coll_type = to_collection_type(src_any); - const auto key = src_key.get_string(); - dst_dictionary.insert_collection(key, coll_type); - copy_dictionary_in_mixed(src_dictionary, dst_dictionary, key, coll_type); + // 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(src_key, src_any); + dst_dictionary.insert(key, any); } } } diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index 34124598839..d9cd212e1ff 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -5216,7 +5216,7 @@ TEST_CASE("client reset with nested collection", "[client reset][local][nested c }) ->run(); } - SECTION("Verify copy and notification logic for List with scalar mixed types and nested collections") { + SECTION("Verify copy and notification logic for List and scalar types") { Results results; Object object; List list_listener, nlist_setup_listener, nlist_local_listener; @@ -5361,4 +5361,441 @@ TEST_CASE("client reset with nested collection", "[client reset][local][nested c }) ->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(); + } }