diff --git a/CHANGELOG.md b/CHANGELOG.md index af566db7591..23f75b8f8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) * CompensatingWriteErrorInfo reported string primary keys as boolean values instead ([PR #5938](https://github.com/realm/realm-core/pull/5938), since the introduction of CompensatingWriteErrorInfo in 12.1.0). * Fix a use-after-free if the last external reference to an encrypted Realm was closed between when a client reset error was received and when the download of the new Realm began. ([PR #5949](https://github.com/realm/realm-core/pull/5949), since 12.4.0). +* Fixed an assertion failure during client reset with recovery when recovering a list operation on an embedded object that has a link column in the path prefix to the list from the top level object. ([PR #5957](https://github.com/realm/realm-core/issues/5957), since introduction of automatic recovery in v11.16.0). ### Breaking changes * Rename RealmConfig::automatic_handle_backlicks_in_migrations to RealmConfig::automatically_handle_backlinks_in_migrations ([PR #5897](https://github.com/realm/realm-core/pull/5897)). diff --git a/src/realm/sync/noinst/client_reset_recovery.cpp b/src/realm/sync/noinst/client_reset_recovery.cpp index d94b5fd64eb..423bfe3c9c1 100644 --- a/src/realm/sync/noinst/client_reset_recovery.cpp +++ b/src/realm/sync/noinst/client_reset_recovery.cpp @@ -521,8 +521,7 @@ bool RecoverLocalChangesetsHandler::resolve_path(ListPath& path, Obj remote_obj, REALM_UNREACHABLE(); } } - else { - REALM_ASSERT(col.is_dictionary()); + else if (col.is_dictionary()) { ++it; REALM_ASSERT(it != path.end()); REALM_ASSERT(it->type == ListPath::Element::Type::InternKey); @@ -538,6 +537,15 @@ bool RecoverLocalChangesetsHandler::resolve_path(ListPath& path, Obj remote_obj, 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); + StringData col_name = remote_obj.get_table()->get_column_name(col); + remote_obj = remote_obj.get_linked_object(col); + local_obj = local_obj.get_linked_object(col_name); + ++it; + } } return false; } diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index 8a6e66564c0..bcc84a5e64e 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -3165,7 +3165,6 @@ TEST_CASE("client reset with embedded object", "[client reset][local][embedded o } SECTION("with shared initial state") { TopLevelContent initial; - initial.link_value = util::none; test_reset->setup([&](SharedRealm realm) { auto table = get_table(*realm, "TopLevel"); REQUIRE(table); @@ -3201,6 +3200,22 @@ TEST_CASE("client reset with embedded object", "[client reset][local][embedded o TopLevelContent expected_recovered = local; reset_embedded_object({local}, {remote}, expected_recovered); } + SECTION("local ArraySet to an embedded object through a deep link->linklist element which is removed by the " + "remote " + "triggers a list copy") { + local.link_value->array_vals[0] = 12345; + remote.link_value->array_vals.erase(remote.link_value->array_vals.begin()); + TopLevelContent expected_recovered = local; + reset_embedded_object({local}, {remote}, expected_recovered); + } + SECTION("local ArrayErase to an embedded object through a deep link->linklist element which is removed by " + "the remote " + "triggers a list copy") { + local.link_value->array_vals.erase(local.link_value->array_vals.begin()); + remote.link_value->array_vals.clear(); + TopLevelContent expected_recovered = local; + reset_embedded_object({local}, {remote}, expected_recovered); + } SECTION("local modifications to an embedded object through a linklist cleared by the remote triggers a list " "copy") { local.array_values.begin()->name = "modified";