Skip to content

Commit a45922f

Browse files
authored
RCORE-2108: Clear backlinks when object containing nested collections is deleted (#7680)
1 parent c967ba1 commit a45922f

File tree

8 files changed

+96
-24
lines changed

8 files changed

+96
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
### Fixed
3131
* Fixed a bug when running a IN query on a String/Int/UUID/ObjectId property that was indexed. ([7642](https://github.com/realm/realm-core/issues/7642) since v14.6.0)
3232
* Fixed a bug when running a IN query on a integer property where double/float parameters were ignored. ([7642](https://github.com/realm/realm-core/issues/7642) since v14.6.0)
33+
* Having links in a nested collections would leave the file inconsistent if the top object is removed. ([#7657](https://github.com/realm/realm-core/issues/7657), since 14.0.0)
3334

3435
### Breaking changes
3536
* None.

src/realm/array_backlink.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,20 @@ void ArrayBacklink::verify() const
220220
REALM_ASSERT(src_obj.get_collection_ptr(src_col_key)->find_any(target_link) != npos);
221221
}
222222
else {
223-
REALM_ASSERT(src_obj.get<Mixed>(src_col_key).get_link() == target_link);
223+
auto val = src_obj.get<Mixed>(src_col_key);
224+
if (val.is_type(type_TypedLink)) {
225+
REALM_ASSERT(src_obj.get<Mixed>(src_col_key).get_link() == target_link);
226+
}
227+
else if (val.is_type(type_List)) {
228+
DummyParent parent(src_table, val.get_ref());
229+
Lst<Mixed> list(parent, 0);
230+
REALM_ASSERT(list.find_any(target_link) != npos);
231+
}
232+
else if (val.is_type(type_Dictionary)) {
233+
DummyParent parent(src_table, val.get_ref());
234+
Dictionary dict(parent, 0);
235+
REALM_ASSERT(dict.find_any(target_link) != npos);
236+
}
224237
}
225238
continue;
226239
}

src/realm/cluster.cpp

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -761,24 +761,48 @@ inline void Cluster::do_erase(size_t ndx, ColKey col_key)
761761
values.set_parent(this, col_ndx.val + s_first_col_index);
762762
set_spec<T>(values, col_ndx);
763763
values.init_from_parent();
764-
ObjLink link;
765764
if constexpr (std::is_same_v<T, ArrayTypedLink>) {
766-
link = values.get(ndx);
767-
}
768-
if constexpr (std::is_same_v<T, ArrayMixed>) {
769-
Mixed value = values.get(ndx);
770-
if (value.is_type(type_TypedLink)) {
771-
link = value.get<ObjLink>();
765+
if (ObjLink link = values.get(ndx)) {
766+
if (const Table* origin_table = m_tree_top.get_owning_table()) {
767+
auto target_obj = origin_table->get_parent_group()->get_object(link);
768+
769+
ColKey backlink_col_key =
770+
target_obj.get_table()->find_backlink_column(col_key, origin_table->get_key());
771+
REALM_ASSERT(backlink_col_key);
772+
target_obj.remove_one_backlink(backlink_col_key, get_real_key(ndx)); // Throws
773+
}
772774
}
773775
}
774-
if (link) {
775-
if (const Table* origin_table = m_tree_top.get_owning_table()) {
776-
auto target_obj = origin_table->get_parent_group()->get_object(link);
776+
values.erase(ndx);
777+
}
777778

778-
ColKey backlink_col_key = target_obj.get_table()->find_backlink_column(col_key, origin_table->get_key());
779-
REALM_ASSERT(backlink_col_key);
780-
target_obj.remove_one_backlink(backlink_col_key, get_real_key(ndx)); // Throws
781-
}
779+
inline void Cluster::do_erase_mixed(size_t ndx, ColKey col_key, ObjKey key, CascadeState& state)
780+
{
781+
const Table* origin_table = m_tree_top.get_owning_table();
782+
auto col_ndx = col_key.get_index();
783+
784+
ArrayMixed values(m_alloc);
785+
values.set_parent(this, col_ndx.val + s_first_col_index);
786+
values.init_from_parent();
787+
788+
Mixed value = values.get(ndx);
789+
if (value.is_type(type_TypedLink)) {
790+
ObjLink link = value.get<ObjLink>();
791+
auto target_obj = origin_table->get_parent_group()->get_object(link);
792+
793+
ColKey backlink_col_key = target_obj.get_table()->find_backlink_column(col_key, origin_table->get_key());
794+
REALM_ASSERT(backlink_col_key);
795+
target_obj.remove_one_backlink(backlink_col_key, get_real_key(ndx)); // Throws
796+
}
797+
if (value.is_type(type_List)) {
798+
Obj obj(origin_table->m_own_ref, get_mem(), key, ndx);
799+
Lst<Mixed> list(obj, col_key);
800+
list.remove_backlinks(state);
801+
}
802+
if (value.is_type(type_Dictionary)) {
803+
Obj obj(origin_table->m_own_ref, get_mem(), key, ndx);
804+
Dictionary dict(obj, col_key);
805+
dict.remove_backlinks(state);
782806
}
783807
values.erase(ndx);
784808
}
@@ -897,7 +921,7 @@ size_t Cluster::erase(ObjKey key, CascadeState& state)
897921
do_erase<ArrayBinary>(ndx, col_key);
898922
break;
899923
case col_type_Mixed:
900-
do_erase<ArrayMixed>(ndx, col_key);
924+
do_erase_mixed(ndx, col_key, key, state);
901925
break;
902926
case col_type_Timestamp:
903927
do_erase<ArrayTimestamp>(ndx, col_key);

src/realm/cluster.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ class Cluster : public ClusterNode {
349349
remove_backlinks(get_owning_table(), origin_key, col, links, state);
350350
}
351351
void do_erase_key(size_t ndx, ColKey col, CascadeState& state);
352+
void do_erase_mixed(size_t ndx, ColKey col, ObjKey key, CascadeState& state);
352353
void do_insert_key(size_t ndx, ColKey col, Mixed init_val, ObjKey origin_key);
353354
void do_insert_link(size_t ndx, ColKey col, Mixed init_val, ObjKey origin_key);
354355
void do_insert_mixed(size_t ndx, ColKey col_key, Mixed init_value, ObjKey origin_key);

src/realm/replication.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ void Replication::select_obj(ObjKey key)
199199
}
200200
}
201201
m_selected_obj = key;
202+
m_selected_list = CollectionId();
202203
}
203204

204205
void Replication::do_set(const Table* t, ColKey col_key, ObjKey key, _impl::Instruction variant)

src/realm/replication.hpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ class Replication {
363363
return m_logger;
364364
}
365365

366+
util::Logger* would_log(util::Logger::Level level) const noexcept
367+
{
368+
if (m_logger && m_logger->would_log(level))
369+
return m_logger;
370+
return nullptr;
371+
}
372+
366373
protected:
367374
Replication() = default;
368375

@@ -439,13 +446,6 @@ class Replication {
439446
Mixed index) const;
440447
Path get_prop_name(Path&&) const;
441448
size_t transact_log_size();
442-
443-
util::Logger* would_log(util::Logger::Level level) const noexcept
444-
{
445-
if (m_logger && m_logger->would_log(level))
446-
return m_logger;
447-
return nullptr;
448-
}
449449
};
450450

451451
class Replication::Interrupted : public std::exception {

src/realm/table.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,6 +2367,13 @@ Obj Table::create_object_with_primary_key(const Mixed& primary_key, FieldValues&
23672367

23682368
// Check if unresolved exists
23692369
if (unres_key) {
2370+
if (Replication* repl = get_repl()) {
2371+
if (auto logger = repl->would_log(util::Logger::Level::debug)) {
2372+
logger->log(LogCategory::object, util::Logger::Level::debug, "Cancel tombstone on '%1': %2",
2373+
get_class_name(), unres_key);
2374+
}
2375+
}
2376+
23702377
auto tombstone = m_tombstones->get(unres_key);
23712378
ret.assign_pk_and_backlinks(tombstone);
23722379
// If tombstones had no links to it, it may still be alive
@@ -2638,6 +2645,13 @@ Obj Table::get_or_create_tombstone(ObjKey key, ColKey pk_col, Mixed pk_val)
26382645
}
26392646
return tombstone;
26402647
}
2648+
if (Replication* repl = get_repl()) {
2649+
if (auto logger = repl->would_log(util::Logger::Level::debug)) {
2650+
logger->log(LogCategory::object, util::Logger::Level::debug,
2651+
"Create tombstone for object '%1' with primary key %2 : %3", get_class_name(), pk_val,
2652+
unres_key);
2653+
}
2654+
}
26412655
return m_tombstones->insert(unres_key, {{pk_col, pk_val}});
26422656
}
26432657

test/test_list.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,11 +837,13 @@ TEST(List_NestedCollection_Links)
837837
auto origin = tr->add_table("origin");
838838
auto list_col = origin->add_column_list(type_Mixed, "any_list");
839839
auto any_col = origin->add_column(type_Mixed, "any");
840+
auto any1_col = origin->add_column(type_Mixed, "any1");
840841
auto embedded_col = origin->add_column(*embedded, "sub");
841842

842843
Obj target_obj1 = target->create_object();
843844
Obj target_obj2 = target->create_object();
844845
Obj target_obj3 = target->create_object();
846+
Obj target_obj4 = target->create_object();
845847
Obj parent = origin->create_object();
846848
parent.create_and_set_linked_object(embedded_col);
847849
auto child_obj = parent.get_linked_object(embedded_col);
@@ -877,6 +879,8 @@ TEST(List_NestedCollection_Links)
877879
dict_any = o.get_dictionary(any_col);
878880
dict_any.insert("Godbye", target_obj1.get_link());
879881
CHECK_THROW_ANY(dict_any.insert("Wrong", child_obj.get_link()));
882+
o.set_collection(any1_col, CollectionType::List);
883+
o.get_list<Mixed>(any1_col).add(target_obj4.get_link());
880884

881885
// Create link from a list nested in a collection nested in a Mixed property
882886
dict_any.insert_collection("List", CollectionType::List);
@@ -887,6 +891,7 @@ TEST(List_NestedCollection_Links)
887891
CHECK_EQUAL(target_obj1.get_backlink_count(), 2);
888892
CHECK_EQUAL(target_obj2.get_backlink_count(), 1);
889893
CHECK_EQUAL(target_obj3.get_backlink_count(), 1);
894+
CHECK_EQUAL(target_obj4.get_backlink_count(), 1);
890895
};
891896

892897
create_links();
@@ -917,15 +922,28 @@ TEST(List_NestedCollection_Links)
917922
CHECK_EQUAL(target_obj2.get_backlink_count(), 1);
918923
o.remove();
919924
CHECK_EQUAL(target_obj2.get_backlink_count(), 0);
925+
CHECK_EQUAL(target_obj4.get_backlink_count(), 0);
920926
tr->commit_and_continue_as_read();
921927

922928
create_links();
923929
// Clearing dictionary should remove links
924930
tr->promote_to_write();
925931
dict_any.clear();
926-
tr->commit_and_continue_as_read();
927932
CHECK_EQUAL(target_obj1.get_backlink_count(), 1);
928933
CHECK_EQUAL(target_obj3.get_backlink_count(), 0);
934+
o.remove();
935+
tr->commit_and_continue_as_read();
936+
937+
create_links();
938+
tr->promote_to_write();
939+
// Removing the top object should remove all backlinks.
940+
// This includes the links contained in the collections
941+
// held by the any (dictionary) and any1 (list) properties.
942+
o.remove();
943+
CHECK_EQUAL(target_obj1.get_backlink_count(), 0);
944+
CHECK_EQUAL(target_obj2.get_backlink_count(), 0);
945+
CHECK_EQUAL(target_obj3.get_backlink_count(), 0);
946+
CHECK_EQUAL(target_obj4.get_backlink_count(), 0);
929947
}
930948

931949
TEST(List_NestedCollection_Unresolved)

0 commit comments

Comments
 (0)