Skip to content

Commit 3551a42

Browse files
ironagejedelbo
andauthored
RCORE-2175 RCORE-2176 RCORE-2182 RCORE-2083 Fix several problems with object removal and links (#7830)
* fix change of mode from Strong to All when deleting embedded objects that link to a tombstone * fix remove recursive to follow mixed links * fix removal of backlinks from mixed in large clusters * add more test coverage to verify single link removal * Replace ObjKey with RowKey in selected places * fix a few more type casts * touchup changelog --------- Co-authored-by: Jørgen Edelbo <[email protected]>
1 parent 5ee4390 commit 3551a42

File tree

11 files changed

+546
-119
lines changed

11 files changed

+546
-119
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
### Fixed
88
* <How do the end-user experience this issue? what was the impact?> ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?)
9-
* None.
9+
* Fixed a change of mode from Strong to All when removing links from an embedded object that links to a tombstone. This affects sync apps that use embedded objects which have a `Lst<Mixed>` that contains a link to another top level object which has been deleted by another sync client (creating a tombstone locally). In this particular case, the switch would cause any remaining link removals to recursively delete the destination object if there were no other links to it. ([#7828](https://github.com/realm/realm-core/issues/7828), since 14.0.0-beta.0)
10+
* Fixed removing backlinks from the wrong objects if the link came from a nested list, nested dictionary, top-level dictionary, or list of mixed, and the source table had more than 256 objects. This could manifest as `array_backlink.cpp:112: Assertion failed: int64_t(value >> 1) == key.value` when removing an object. ([#7594](https://github.com/realm/realm-core/issues/7594), since v11 for dictionaries)
11+
* Fixed the collapse/rejoin of clusters which contained nested collections with links. This could manifest as `array.cpp:319: Array::move() Assertion failed: begin <= end [2, 1]` when removing an object. ([#7839](https://github.com/realm/realm-core/issues/7839), since the introduction of nested collections in v14.0.0-beta.0)
1012

1113
### Breaking changes
1214
* None.
@@ -17,7 +19,7 @@
1719
-----------
1820

1921
### Internals
20-
* None.
22+
* Fixed `Table::remove_object_recursive` which wouldn't recursively follow links through a single `Mixed` property. This feature is exposed publicly on `Table` but no SDK currently uses it, so this is considered internal. ([#7829](https://github.com/realm/realm-core/issues/7829), likely since the introduction of Mixed)
2123

2224
----------------------------------------------
2325

src/realm/array_mixed.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ void ArrayMixed::move(ArrayMixed& dst, size_t ndx)
244244
{
245245
auto sz = size();
246246
size_t i = ndx;
247+
const size_t original_dst_size = dst.size();
247248
while (i < sz) {
248249
auto val = get(i++);
249250
dst.add(val);
@@ -254,7 +255,7 @@ void ArrayMixed::move(ArrayMixed& dst, size_t ndx)
254255
Array keys(Array::get_alloc());
255256
keys.set_parent(const_cast<ArrayMixed*>(this), payload_idx_key);
256257
keys.init_from_ref(ref);
257-
for (size_t j = 0, i = ndx; i < sz; i++, j++) {
258+
for (size_t j = original_dst_size, i = ndx; i < sz; i++, j++) {
258259
dst.set_key(j, keys.get(i));
259260
}
260261
keys.truncate(ndx);

src/realm/cluster.cpp

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const Table* ClusterNode::get_owning_table() const noexcept
8989

9090
void ClusterNode::get(ObjKey k, ClusterNode::State& state) const
9191
{
92-
if (!k || !try_get(k, state)) {
92+
if (!k || !try_get(RowKey(k), state)) {
9393
throw KeyNotFound(util::format("No object with key '%1' in '%2'", k.value, get_owning_table()->get_name()));
9494
}
9595
}
@@ -225,7 +225,7 @@ void Cluster::update_from_parent() noexcept
225225
}
226226
}
227227

228-
MemRef Cluster::ensure_writeable(ObjKey)
228+
MemRef Cluster::ensure_writeable(RowKey)
229229
{
230230
// By specifying the minimum size, we ensure that the array has a capacity
231231
// to hold m_size 64 bit refs.
@@ -234,7 +234,7 @@ MemRef Cluster::ensure_writeable(ObjKey)
234234
return get_mem();
235235
}
236236

237-
void Cluster::update_ref_in_parent(ObjKey, ref_type)
237+
void Cluster::update_ref_in_parent(RowKey, ref_type)
238238
{
239239
REALM_UNREACHABLE();
240240
}
@@ -343,13 +343,13 @@ inline void Cluster::do_insert_link(size_t ndx, ColKey col_key, Mixed init_val,
343343
}
344344
}
345345

346-
void Cluster::insert_row(size_t ndx, ObjKey k, const FieldValues& init_values)
346+
void Cluster::insert_row(size_t ndx, RowKey row_key, const FieldValues& init_values)
347347
{
348348
// Ensure the cluster array is big enough to hold 64 bit values.
349349
copy_on_write(m_size * 8);
350350

351351
if (m_keys.is_attached()) {
352-
m_keys.insert(ndx, k.value);
352+
m_keys.insert(ndx, row_key.value);
353353
}
354354
else {
355355
Array::set(s_key_ref_or_size_index, Array::get(s_key_ref_or_size_index) + 2); // Increments size by 1
@@ -377,6 +377,7 @@ void Cluster::insert_row(size_t ndx, ObjKey k, const FieldValues& init_values)
377377
}
378378

379379
bool nullable = attr.test(col_attr_Nullable);
380+
ObjKey obj_key(int64_t(row_key.value + get_offset()));
380381
switch (type) {
381382
case col_type_Int:
382383
if (attr.test(col_attr_Nullable)) {
@@ -402,7 +403,7 @@ void Cluster::insert_row(size_t ndx, ObjKey k, const FieldValues& init_values)
402403
do_insert_row<ArrayBinary>(ndx, col_key, init_value, nullable);
403404
break;
404405
case col_type_Mixed: {
405-
do_insert_mixed(ndx, col_key, init_value, ObjKey(k.value + get_offset()));
406+
do_insert_mixed(ndx, col_key, init_value, obj_key);
406407
break;
407408
}
408409
case col_type_Timestamp:
@@ -418,10 +419,10 @@ void Cluster::insert_row(size_t ndx, ObjKey k, const FieldValues& init_values)
418419
do_insert_row<ArrayUUIDNull>(ndx, col_key, init_value, nullable);
419420
break;
420421
case col_type_Link:
421-
do_insert_key(ndx, col_key, init_value, ObjKey(k.value + get_offset()));
422+
do_insert_key(ndx, col_key, init_value, obj_key);
422423
break;
423424
case col_type_TypedLink:
424-
do_insert_link(ndx, col_key, init_value, ObjKey(k.value + get_offset()));
425+
do_insert_link(ndx, col_key, init_value, obj_key);
425426
break;
426427
case col_type_BackLink: {
427428
ArrayBacklink arr(m_alloc);
@@ -664,7 +665,7 @@ void Cluster::remove_column(ColKey col_key)
664665
Array::set(idx, 0);
665666
}
666667

667-
ref_type Cluster::insert(ObjKey k, const FieldValues& init_values, ClusterNode::State& state)
668+
ref_type Cluster::insert(RowKey row_key, const FieldValues& init_values, ClusterNode::State& state)
668669
{
669670
int64_t current_key_value = -1;
670671
size_t sz;
@@ -673,34 +674,34 @@ ref_type Cluster::insert(ObjKey k, const FieldValues& init_values, ClusterNode::
673674

674675
auto on_error = [&] {
675676
throw KeyAlreadyUsed(
676-
util::format("When inserting key '%1' in '%2'", k.value, get_owning_table()->get_name()));
677+
util::format("When inserting key '%1' in '%2'", row_key.value, get_owning_table()->get_name()));
677678
};
678679

679680
if (m_keys.is_attached()) {
680681
sz = m_keys.size();
681-
ndx = m_keys.lower_bound(uint64_t(k.value));
682+
ndx = m_keys.lower_bound(row_key.value);
682683
if (ndx < sz) {
683684
current_key_value = m_keys.get(ndx);
684-
if (k.value == current_key_value) {
685+
if (row_key.value == uint64_t(current_key_value)) {
685686
on_error();
686687
}
687688
}
688689
}
689690
else {
690691
sz = size_t(Array::get(s_key_ref_or_size_index)) >> 1; // Size is stored as tagged integer
691-
if (uint64_t(k.value) < sz) {
692+
if (row_key.value < sz) {
692693
on_error();
693694
}
694695
// Key value is bigger than all other values, should be put last
695696
ndx = sz;
696-
if (uint64_t(k.value) > sz && sz < cluster_node_size) {
697+
if (row_key.value > sz && sz < cluster_node_size) {
697698
ensure_general_form();
698699
}
699700
}
700701

701702
REALM_ASSERT_DEBUG(sz <= cluster_node_size);
702703
if (REALM_LIKELY(sz < cluster_node_size)) {
703-
insert_row(ndx, k, init_values); // Throws
704+
insert_row(ndx, row_key, init_values); // Throws
704705
state.mem = get_mem();
705706
state.index = ndx;
706707
}
@@ -709,8 +710,8 @@ ref_type Cluster::insert(ObjKey k, const FieldValues& init_values, ClusterNode::
709710
Cluster new_leaf(0, m_alloc, m_tree_top);
710711
new_leaf.create();
711712
if (ndx == sz) {
712-
new_leaf.insert_row(0, ObjKey(0), init_values); // Throws
713-
state.split_key = k.value;
713+
new_leaf.insert_row(0, RowKey(0), init_values); // Throws
714+
state.split_key = int64_t(row_key.value);
714715
state.mem = new_leaf.get_mem();
715716
state.index = 0;
716717
}
@@ -719,7 +720,7 @@ ref_type Cluster::insert(ObjKey k, const FieldValues& init_values, ClusterNode::
719720
REALM_ASSERT_DEBUG(m_keys.is_attached());
720721
new_leaf.ensure_general_form();
721722
move(ndx, &new_leaf, current_key_value);
722-
insert_row(ndx, k, init_values); // Throws
723+
insert_row(ndx, row_key, init_values); // Throws
723724
state.mem = get_mem();
724725
state.split_key = current_key_value;
725726
state.index = ndx;
@@ -730,15 +731,15 @@ ref_type Cluster::insert(ObjKey k, const FieldValues& init_values, ClusterNode::
730731
return ret;
731732
}
732733

733-
bool Cluster::try_get(ObjKey k, ClusterNode::State& state) const noexcept
734+
bool Cluster::try_get(RowKey k, ClusterNode::State& state) const noexcept
734735
{
735736
state.mem = get_mem();
736737
if (m_keys.is_attached()) {
737-
state.index = m_keys.lower_bound(uint64_t(k.value));
738-
return state.index != m_keys.size() && m_keys.get(state.index) == uint64_t(k.value);
738+
state.index = m_keys.lower_bound(k.value);
739+
return state.index != m_keys.size() && m_keys.get(state.index) == k.value;
739740
}
740741
else {
741-
if (uint64_t(k.value) < uint64_t(Array::get(s_key_ref_or_size_index) >> 1)) {
742+
if (k.value < uint64_t(Array::get(s_key_ref_or_size_index) >> 1)) {
742743
state.index = size_t(k.value);
743744
return true;
744745
}
@@ -776,7 +777,7 @@ inline void Cluster::do_erase(size_t ndx, ColKey col_key)
776777
values.erase(ndx);
777778
}
778779

779-
inline void Cluster::do_erase_mixed(size_t ndx, ColKey col_key, ObjKey key, CascadeState& state)
780+
inline void Cluster::do_erase_mixed(size_t ndx, ColKey col_key, CascadeState& state)
780781
{
781782
const Table* origin_table = m_tree_top.get_owning_table();
782783
auto col_ndx = col_key.get_index();
@@ -788,19 +789,16 @@ inline void Cluster::do_erase_mixed(size_t ndx, ColKey col_key, ObjKey key, Casc
788789
Mixed value = values.get(ndx);
789790
if (value.is_type(type_TypedLink)) {
790791
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
792+
Obj obj(origin_table->m_own_ref, get_mem(), get_real_key(ndx), ndx);
793+
obj.remove_backlink(col_key, link, state);
796794
}
797795
if (value.is_type(type_List)) {
798-
Obj obj(origin_table->m_own_ref, get_mem(), key, ndx);
796+
Obj obj(origin_table->m_own_ref, get_mem(), get_real_key(ndx), ndx);
799797
Lst<Mixed> list(obj, col_key);
800798
list.remove_backlinks(state);
801799
}
802800
if (value.is_type(type_Dictionary)) {
803-
Obj obj(origin_table->m_own_ref, get_mem(), key, ndx);
801+
Obj obj(origin_table->m_own_ref, get_mem(), get_real_key(ndx), ndx);
804802
Dictionary dict(obj, col_key);
805803
dict.remove_backlinks(state);
806804
}
@@ -821,12 +819,12 @@ inline void Cluster::do_erase_key(size_t ndx, ColKey col_key, CascadeState& stat
821819
values.erase(ndx);
822820
}
823821

824-
size_t Cluster::get_ndx(ObjKey k, size_t ndx) const noexcept
822+
size_t Cluster::get_ndx(RowKey k, size_t ndx) const noexcept
825823
{
826824
size_t index;
827825
if (m_keys.is_attached()) {
828-
index = m_keys.lower_bound(uint64_t(k.value));
829-
if (index == m_keys.size() || m_keys.get(index) != uint64_t(k.value)) {
826+
index = m_keys.lower_bound(k.value);
827+
if (index == m_keys.size() || m_keys.get(index) != k.value) {
830828
return realm::npos;
831829
}
832830
}
@@ -839,11 +837,14 @@ size_t Cluster::get_ndx(ObjKey k, size_t ndx) const noexcept
839837
return index + ndx;
840838
}
841839

842-
size_t Cluster::erase(ObjKey key, CascadeState& state)
840+
size_t Cluster::erase(RowKey row_key, CascadeState& state)
843841
{
844-
size_t ndx = get_ndx(key, 0);
842+
size_t ndx = get_ndx(row_key, 0);
845843
if (ndx == realm::npos)
846-
throw KeyNotFound(util::format("When erasing key '%1' in '%2'", key.value, get_owning_table()->get_name()));
844+
throw KeyNotFound(util::format("When erasing key '%1' (offset '%2') in '%3'", row_key.value, m_offset,
845+
get_owning_table()->get_name()));
846+
847+
ObjKey real_key = get_real_key(ndx);
847848
std::vector<ColKey> backlink_column_keys;
848849

849850
auto erase_in_column = [&](ColKey col_key) {
@@ -860,7 +861,7 @@ size_t Cluster::erase(ObjKey key, CascadeState& state)
860861
const Table* origin_table = m_tree_top.get_owning_table();
861862
if (attr.test(col_attr_Dictionary)) {
862863
if (col_type == col_type_Mixed || col_type == col_type_Link) {
863-
Obj obj(origin_table->m_own_ref, get_mem(), key, ndx);
864+
Obj obj(origin_table->m_own_ref, get_mem(), real_key, ndx);
864865
Dictionary dict(obj, col_key);
865866
dict.remove_backlinks(state);
866867
}
@@ -869,7 +870,7 @@ size_t Cluster::erase(ObjKey key, CascadeState& state)
869870
BPlusTree<ObjKey> links(m_alloc);
870871
links.init_from_ref(ref);
871872
if (links.size() > 0) {
872-
do_remove_backlinks(ObjKey(key.value + m_offset), col_key, links.get_all(), state);
873+
do_remove_backlinks(real_key, col_key, links.get_all(), state);
873874
}
874875
}
875876
else if (col_type == col_type_TypedLink) {
@@ -880,11 +881,11 @@ size_t Cluster::erase(ObjKey key, CascadeState& state)
880881
auto target_obj = origin_table->get_parent_group()->get_object(link);
881882
ColKey backlink_col_key =
882883
target_obj.get_table()->find_backlink_column(col_key, origin_table->get_key());
883-
target_obj.remove_one_backlink(backlink_col_key, ObjKey(key.value + m_offset));
884+
target_obj.remove_one_backlink(backlink_col_key, real_key);
884885
}
885886
}
886887
else if (col_type == col_type_Mixed) {
887-
Obj obj(origin_table->m_own_ref, get_mem(), key, ndx);
888+
Obj obj(origin_table->m_own_ref, get_mem(), real_key, ndx);
888889
Lst<Mixed> list(obj, col_key);
889890
list.remove_backlinks(state);
890891
}
@@ -921,7 +922,7 @@ size_t Cluster::erase(ObjKey key, CascadeState& state)
921922
do_erase<ArrayBinary>(ndx, col_key);
922923
break;
923924
case col_type_Mixed:
924-
do_erase_mixed(ndx, col_key, key, state);
925+
do_erase_mixed(ndx, col_key, state);
925926
break;
926927
case col_type_Timestamp:
927928
do_erase<ArrayTimestamp>(ndx, col_key);
@@ -983,7 +984,7 @@ size_t Cluster::erase(ObjKey key, CascadeState& state)
983984
return node_size();
984985
}
985986

986-
void Cluster::nullify_incoming_links(ObjKey key, CascadeState& state)
987+
void Cluster::nullify_incoming_links(RowKey key, CascadeState& state)
987988
{
988989
size_t ndx = get_ndx(key, 0);
989990
if (ndx == realm::npos)
@@ -1541,7 +1542,7 @@ void Cluster::remove_backlinks(const Table* origin_table, ObjKey origin_key, Col
15411542
bool last_removed = target_obj.remove_one_backlink(backlink_col_key, origin_key); // Throws
15421543
if (is_unres) {
15431544
if (last_removed) {
1544-
// Check is there are more backlinks
1545+
// Check if there are more backlinks
15451546
if (!target_obj.has_backlinks(false)) {
15461547
// Tombstones can be erased right away - there is no cascading effect
15471548
target_table->m_tombstones->erase(link.get_obj_key(), state);

0 commit comments

Comments
 (0)