• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2110

07 Mar 2024 05:28PM UTC coverage: 90.909% (-0.009%) from 90.918%
2110

push

Evergreen

web-flow
Merge pull request #7424 from realm/tg/notifier-perf

Improve performance with very large numbers of notifiers

93980 of 173178 branches covered (54.27%)

28 of 30 new or added lines in 2 files covered. (93.33%)

79 existing lines in 13 files now uncovered.

238498 of 262347 relevant lines covered (90.91%)

6026341.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.81
/src/realm/object-store/impl/deep_change_checker.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2021 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/object-store/impl/deep_change_checker.hpp>
20
#include <realm/dictionary.hpp>
21
#include <realm/list.hpp>
22
#include <realm/set.hpp>
23
#include <realm/table.hpp>
24

25
using namespace realm;
26
using namespace realm::_impl;
27

28
namespace {
29
template <typename T>
30
void sort_and_unique(T& container)
31
{
72,984✔
32
    std::sort(container.begin(), container.end());
72,984✔
33
    container.erase(std::unique(container.begin(), container.end()), container.end());
72,984✔
34
}
72,984✔
35
} // namespace
36

37
void DeepChangeChecker::find_related_tables(std::vector<RelatedTable>& related_tables, Table const& table,
38
                                            const KeyPathArray& key_path_array)
39
{
9,853✔
40
    struct LinkInfo {
9,853✔
41
        std::vector<ColKey> forward_links;
9,853✔
42
        std::vector<TableKey> forward_tables;
9,853✔
43
        std::vector<TableKey> backlink_tables;
9,853✔
44
        bool processed_table = false;
9,853✔
45
    };
9,853✔
46

4,928✔
47
    auto has_key_paths = std::any_of(begin(key_path_array), end(key_path_array), [&](auto key_path) {
5,022✔
48
        return key_path.size() > 0;
188✔
49
    });
188✔
50

4,928✔
51
    // Build up the complete forward mapping from the back links.
4,928✔
52
    // Following forward link columns does not account for TypedLink
4,928✔
53
    // values as part of Dictionary<String, Mixed> for example. But
4,928✔
54
    // we do not want to assume that all Mixed columns contain links,
4,928✔
55
    // so we rely on the fact that if there are any TypedLinks from a
4,928✔
56
    // Mixed value, there will be a corresponding backlink column
4,928✔
57
    // created at the destination table.
4,928✔
58
    Group* group = table.get_parent_group();
9,853✔
59
    REALM_ASSERT(group);
9,853✔
60
    std::vector<LinkInfo> complete_mapping;
9,853✔
61
    complete_mapping.resize(group->size());
9,853✔
62
    auto get_mapping = [&](TableKey key) -> LinkInfo& {
24,781✔
63
        size_t ndx = Group::key2ndx(key);
24,781✔
64
        // Removed tables leave gaps, so the maximum index can be greater than
12,395✔
65
        // the size. This is very unusual, though.
12,395✔
66
        if (ndx >= complete_mapping.size()) {
24,781✔
NEW
67
            complete_mapping.resize(ndx + 1);
×
NEW
68
        }
×
69
        return complete_mapping[ndx];
24,781✔
70
    };
24,781✔
71

4,928✔
72
    auto all_table_keys = group->get_table_keys();
9,853✔
73
    for (auto table_key : all_table_keys) {
36,492✔
74
        auto cur_table = group->get_table(table_key).unchecked_ptr();
36,492✔
75
        REALM_ASSERT(cur_table);
36,492✔
76

18,249✔
77
        LinkInfo* backlinks = nullptr;
36,492✔
78
        if (has_key_paths) {
36,492✔
79
            backlinks = &get_mapping(table_key);
1,570✔
80
        }
1,570✔
81
        cur_table->for_each_backlink_column([&](ColKey backlink_col_key) {
22,556✔
82
            auto origin_table_key = cur_table->get_opposite_table_key(backlink_col_key);
8,617✔
83
            auto origin_link_col = cur_table->get_opposite_column(backlink_col_key);
8,617✔
84
            auto& links = get_mapping(origin_table_key);
8,617✔
85
            links.forward_links.push_back(origin_link_col);
8,617✔
86
            links.forward_tables.push_back(table_key);
8,617✔
87

4,310✔
88
            if (any_of(key_path_array.begin(), key_path_array.end(), [&](const KeyPath& key_path) {
8,617✔
89
                    return any_of(key_path.begin(), key_path.end(), [&](std::pair<TableKey, ColKey> pair) {
3,870✔
90
                        return pair.first == cur_table->get_key() && pair.second == backlink_col_key;
3,870✔
91
                    });
3,870✔
92
                })) {
1,087✔
93
                backlinks->backlink_tables.push_back(cur_table->get_link_target(backlink_col_key)->get_key());
38✔
94
            }
38✔
95
            return IteratorControl::AdvanceToNext;
8,617✔
96
        });
8,617✔
97
    }
36,492✔
98

4,928✔
99
    // Remove duplicates:
4,928✔
100
    // duplicates in link_columns can occur when a Mixed(TypedLink) contain links to different tables
4,928✔
101
    // duplicates in connected_tables can occur when there are different link paths to the same table
4,928✔
102
    for (auto& info : complete_mapping) {
36,492✔
103
        sort_and_unique(info.forward_links);
36,492✔
104
        sort_and_unique(info.forward_tables);
36,492✔
105
    }
36,492✔
106

4,928✔
107
    std::vector<TableKey> tables_to_check = {table.get_key()};
9,853✔
108
    while (tables_to_check.size()) {
24,447✔
109
        auto table_key_to_check = tables_to_check.back();
14,594✔
110
        tables_to_check.pop_back();
14,594✔
111
        auto& link_info = get_mapping(table_key_to_check);
14,594✔
112
        if (link_info.processed_table) {
14,594✔
113
            continue;
685✔
114
        }
685✔
115
        link_info.processed_table = true;
13,909✔
116

6,956✔
117
        related_tables.push_back({table_key_to_check, std::move(link_info.forward_links)});
13,909✔
118

6,956✔
119
        // Add all tables reachable via a forward link to the vector of tables that need to be checked
6,956✔
120
        tables_to_check.insert(tables_to_check.end(), link_info.forward_tables.begin(),
13,909✔
121
                               link_info.forward_tables.end());
13,909✔
122

6,956✔
123
        // Backlinks can only come into consideration when added via key paths.
6,956✔
124
        REALM_ASSERT(has_key_paths || link_info.backlink_tables.empty());
13,909✔
125
        tables_to_check.insert(tables_to_check.end(), link_info.backlink_tables.begin(),
13,909✔
126
                               link_info.backlink_tables.end());
13,909✔
127
    }
13,909✔
128
}
9,853✔
129

130
DeepChangeChecker::DeepChangeChecker(TransactionChangeInfo const& info, Table const& root_table,
131
                                     DeepChangeChecker::RelatedTables const& related_tables,
132
                                     const KeyPathArray& key_path_array, bool all_callbacks_filtered)
133
    : m_info(info)
134
    , m_root_table(root_table)
135
    , m_key_path_array(key_path_array)
136
    , m_root_object_changes([&] {
3,546✔
137
        auto it = info.tables.find(root_table.get_key());
3,546✔
138
        return it != info.tables.end() ? &it->second : nullptr;
3,305✔
139
    }())
3,546✔
140
    , m_related_tables(related_tables)
141
{
3,546✔
142
    // If all callbacks do have a filter, every `KeyPathArray` will have entries.
1,773✔
143
    // In this case we need to check the `ColKey`s and pass the filtered columns
1,773✔
144
    // to the checker.
1,773✔
145
    // If at least one `NotificationCallback` does not have a filter we notify on any change.
1,773✔
146
    // This is signaled by leaving the `m_filtered_columns_in_root_table` and
1,773✔
147
    // `m_filtered_columns` empty.
1,773✔
148
    if (all_callbacks_filtered) {
3,546✔
149
        for (const auto& key_path : key_path_array) {
1,028✔
150
            if (key_path.size() != 0) {
880✔
151
                m_filtered_columns_in_root_table.push_back(key_path[0].second);
880✔
152
            }
880✔
153
            for (const auto& key_path_element : key_path) {
980✔
154
                m_filtered_columns.push_back(key_path_element.second);
980✔
155
            }
980✔
156
        }
880✔
157
    }
1,176✔
158
}
3,546✔
159

160
bool DeepChangeChecker::do_check_mixed_for_link(Group& group, TableRef& cached_linked_table, Mixed value,
161
                                                const std::vector<ColKey>& filtered_columns, size_t depth)
162
{
3,360✔
163
    if (!value.is_type(type_TypedLink)) {
3,360✔
164
        return false;
348✔
165
    }
348✔
166
    auto link = value.get_link();
3,012✔
167
    if (link.is_unresolved()) {
3,012✔
168
        return false;
192✔
169
    }
192✔
170

1,409✔
171
    if (!cached_linked_table || cached_linked_table->get_key() != link.get_table_key()) {
2,820✔
172
        cached_linked_table = group.get_table(link.get_table_key());
1,806✔
173
        REALM_ASSERT_EX(cached_linked_table, link.get_table_key());
1,806✔
174
    }
1,806✔
175
    return check_row(*cached_linked_table, link.get_obj_key(), filtered_columns, depth + 1);
2,820✔
176
}
2,820✔
177

178
template <typename T>
179
bool DeepChangeChecker::check_collection(ref_type ref, const Obj& obj, ColKey col,
180
                                         const std::vector<ColKey>& filtered_columns, size_t depth)
181
{
870✔
182
    BPlusTree<T> bp(obj.get_alloc());
870✔
183
    bp.init_from_ref(ref);
870✔
184
    size_t size = bp.size();
870✔
185
    if (size == 0) {
870✔
186
        return false;
×
187
    }
×
188

435✔
189
    if constexpr (std::is_same_v<ObjKey, T>) {
870✔
190
        auto target = obj.get_table()->get_link_target(col);
444✔
191
        for (size_t i = 0; i < size; ++i) {
1,266✔
192
            auto key = bp.get(i);
1,104✔
193
            if (key && !key.is_unresolved() && check_row(*target, key, filtered_columns, depth + 1)) {
1,104✔
194
                return true;
264✔
195
            }
264✔
196
        }
1,104✔
197
    }
426✔
198
    else {
444✔
199
        static_cast<void>(col); // older gcc warns about things used in only one branch of if constexpr
444✔
200
        TableRef cached_linked_table;
444✔
201
        Group& group = *obj.get_table()->get_parent_group();
444✔
202
        for (size_t i = 0; i < size; ++i) {
1,344✔
203
            if (do_check_mixed_for_link(group, cached_linked_table, bp.get(i), filtered_columns, depth)) {
1,178✔
204
                return true;
278✔
205
            }
278✔
206
        }
1,178✔
207
    }
444✔
208
    return false;
599✔
209
}
870✔
210

211
bool DeepChangeChecker::do_check_for_collection_modifications(const Obj& obj, ColKey col,
212
                                                              const std::vector<ColKey>& filtered_columns,
213
                                                              size_t depth)
214
{
4,262✔
215
    auto ref = Obj::Internal::get_ref(obj, col);
4,262✔
216
    if (!ref) {
4,262✔
217
        return false;
2,916✔
218
    }
2,916✔
219

673✔
220
    if ((col.is_list() || col.is_set()) && col.get_type() == col_type_Link) {
1,346✔
221
        return check_collection<ObjKey>(ref, obj, col, filtered_columns, depth);
426✔
222
    }
426✔
223

460✔
224
    if ((col.is_set() || col.is_list()) && col.get_type() == col_type_Mixed) {
920✔
225
        return check_collection<Mixed>(ref, obj, col, filtered_columns, depth);
444✔
226
    }
444✔
227

238✔
228
    if (col.is_dictionary()) {
476✔
229
        auto dict = obj.get_dictionary(col);
476✔
230
        TableRef cached_linked_table;
476✔
231
        auto& group = *obj.get_table()->get_parent_group();
476✔
232
        return std::any_of(dict.begin(), dict.end(), [&](auto key_value_pair) {
1,216✔
233
            Mixed value = key_value_pair.second;
1,216✔
234
            // Here we rely on Dictionaries storing all links as a TypedLink
608✔
235
            // even if the dictionary is set to a single object type.
608✔
236
            REALM_ASSERT(!value.is_type(type_Link));
1,216✔
237
            return do_check_mixed_for_link(group, cached_linked_table, value, filtered_columns, depth);
1,216✔
238
        });
1,216✔
239
    }
476✔
240

241
    // at this point, we have not handled all datatypes
242
    REALM_UNREACHABLE();
243
}
×
244

245
bool DeepChangeChecker::check_outgoing_links(Table const& table, ObjKey obj_key,
246
                                             const std::vector<ColKey>& filtered_columns, size_t depth)
247
{
7,726✔
248
    REALM_ASSERT(depth < m_current_path.size());
7,726✔
249
    auto table_key = table.get_key();
7,726✔
250

3,862✔
251
    // First we create an iterator pointing at the table identified by `table_key` within the `m_related_tables`.
3,862✔
252
    auto it = std::find_if(begin(m_related_tables), end(m_related_tables), [&](const auto& related_table) {
9,846✔
253
        return related_table.table_key == table_key;
9,846✔
254
    });
9,846✔
255
    if (it == m_related_tables.end())
7,726✔
256
        return false;
×
257
    // Likewise if the table could be found but does not have any (outgoing) links.
3,862✔
258
    if (it->links.empty())
7,726✔
259
        return false;
1,116✔
260

3,304✔
261
    // Check if we're already checking if the destination of the link is
3,304✔
262
    // modified, and if not add it to the stack
3,304✔
263
    auto already_checking = [&](ColKey col) {
16,430✔
264
        auto end = m_current_path.begin() + depth;
16,430✔
265
        auto match = std::find_if(m_current_path.begin(), end, [&](const auto& p) {
14,699✔
266
            return p.obj_key == obj_key && p.col_key == col;
12,961✔
267
        });
12,961✔
268
        if (match != end) {
16,430✔
269
            for (; match < end; ++match) {
2,592✔
270
                match->depth_exceeded = true;
1,404✔
271
            }
1,404✔
272
            return true;
1,188✔
273
        }
1,188✔
274
        m_current_path[depth] = {obj_key, col, false};
15,242✔
275
        return false;
15,242✔
276
    };
15,242✔
277

3,304✔
278
    const Obj obj = table.get_object(ObjKey(obj_key));
6,610✔
279
    auto linked_object_changed = [&](ColKey const& outgoing_link_column) {
16,430✔
280
        if (already_checking(outgoing_link_column))
16,430✔
281
            return false;
1,188✔
282
        if (outgoing_link_column.is_collection()) {
15,242✔
283
            return do_check_for_collection_modifications(obj, outgoing_link_column, filtered_columns, depth);
4,262✔
284
        }
4,262✔
285
        if (outgoing_link_column.get_type() == col_type_Mixed) {
10,980✔
286
            TableRef no_cached;
966✔
287
            Mixed value = obj.get<Mixed>(outgoing_link_column);
966✔
288
            return do_check_mixed_for_link(*table.get_parent_group(), no_cached, value, filtered_columns, depth);
966✔
289
        }
966✔
290
        REALM_ASSERT_EX(outgoing_link_column.get_type() == col_type_Link, outgoing_link_column.get_type());
10,014✔
291
        ConstTableRef dst_table = table.get_link_target(outgoing_link_column);
10,014✔
292
        ObjKey dst_key = obj.get<ObjKey>(outgoing_link_column);
10,014✔
293

5,005✔
294
        if (!dst_key) // do not descend into a null or unresolved link
10,014✔
295
            return false;
7,334✔
296
        return check_row(*dst_table, dst_key, filtered_columns, depth + 1);
2,680✔
297
    };
2,680✔
298

3,304✔
299
    // Check the `links` of all `m_related_tables` and return true if any of them has a `linked_object_changed`.
3,304✔
300
    return std::any_of(begin(it->links), end(it->links), linked_object_changed);
6,610✔
301
}
6,610✔
302

303
bool DeepChangeChecker::check_row(Table const& table, ObjKey object_key, const std::vector<ColKey>& filtered_columns,
304
                                  size_t depth)
305
{
10,074✔
306
    REALM_ASSERT(!ObjKey(object_key).is_unresolved());
10,074✔
307

5,036✔
308
    TableKey table_key = table.get_key();
10,074✔
309

5,036✔
310
    // First check if the object was modified directly. We skip this if we're
5,036✔
311
    // looking at the root object because that check is done more efficiently
5,036✔
312
    // in operator() before calling this.
5,036✔
313
    if (depth > 0) {
10,074✔
314
        auto it = m_info.tables.find(table_key);
6,412✔
315
        if (it != m_info.tables.end() && it->second.modifications_contains(object_key, filtered_columns))
6,412✔
316
            return true;
1,040✔
317
    }
9,034✔
318

4,516✔
319
    // The object wasn't modified, so we move onto checking for if it links to
4,516✔
320
    // a modified object. This has an arbitrary maximum depth on how far it'll
4,516✔
321
    // search for performance.
4,516✔
322
    if (depth + 1 == m_current_path.size()) {
9,034✔
323
        // Don't mark any of the intermediate rows checked along the path as
502✔
324
        // not modified, as a search starting from them might hit a modification
502✔
325
        for (size_t i = 0; i < m_current_path.size(); ++i)
5,025✔
326
            m_current_path[i].depth_exceeded = true;
4,020✔
327
        return false;
1,005✔
328
    }
1,005✔
329

4,014✔
330
    // We may have already performed deep checking on this object and discovered
4,014✔
331
    // that it is not possible to reach a modified object from it.
4,014✔
332
    auto& not_modified = m_not_modified[table_key];
8,029✔
333
    auto it = not_modified.find(object_key);
8,029✔
334
    if (it != not_modified.end())
8,029✔
335
        return false;
303✔
336

3,862✔
337
    bool ret = check_outgoing_links(table, ObjKey(object_key), filtered_columns, depth);
7,726✔
338
    // If this object isn't modified and we didn't exceed the maximum search depth,
3,862✔
339
    // cache that result to avoid having to repeat it.
3,862✔
340
    if (!ret && (depth == 0 || !m_current_path[depth - 1].depth_exceeded))
7,726✔
341
        not_modified.insert(object_key);
3,652✔
342
    return ret;
7,726✔
343
}
7,726✔
344

345
bool DeepChangeChecker::operator()(ObjKey key)
346
{
4,658✔
347
    // First check if the root object was modified. We could skip this and do
2,329✔
348
    // it in check_row(), but this skips a few lookups.
2,329✔
349
    if (m_root_object_changes &&
4,658✔
350
        m_root_object_changes->modifications_contains(key, m_filtered_columns_in_root_table)) {
4,139✔
351
        return true;
992✔
352
    }
992✔
353

1,833✔
354
    // In production code it shouldn't be possible for a notifier to call this on
1,833✔
355
    // an invalidated object, but we do have tests for it just in case.
1,833✔
356
    if (ObjKey(key).is_unresolved()) {
3,666✔
357
        return false;
4✔
358
    }
4✔
359

1,831✔
360
    // The object itself wasn't modified, so move on to check if any of the
1,831✔
361
    // objects it links to were modified.
1,831✔
362
    return check_row(m_root_table, key, m_filtered_columns, 0);
3,662✔
363
}
3,662✔
364

365
CollectionKeyPathChangeChecker::CollectionKeyPathChangeChecker(TransactionChangeInfo const& info,
366
                                                               Table const& root_table,
367
                                                               std::vector<RelatedTable> const& related_tables,
368
                                                               const KeyPathArray& key_path_array,
369
                                                               bool all_callbacks_filtered)
370
    : DeepChangeChecker(info, root_table, related_tables, key_path_array, all_callbacks_filtered)
371
{
252✔
372
}
252✔
373

374
bool CollectionKeyPathChangeChecker::operator()(ObjKey object_key)
375
{
1,380✔
376
    std::vector<ColKey> changed_columns;
1,380✔
377

690✔
378
    // In production code it shouldn't be possible for a notifier to call this on
690✔
379
    // an invalidated object, but we do have tests for it just in case.
690✔
380
    if (object_key.is_unresolved()) {
1,380✔
381
        return false;
4✔
382
    }
4✔
383

688✔
384
    for (auto& key_path : m_key_path_array) {
1,376✔
385
        find_changed_columns(changed_columns, key_path, 0, m_root_table, object_key);
1,364✔
386
    }
1,364✔
387

688✔
388
    return changed_columns.size() > 0;
1,376✔
389
}
1,376✔
390

391
void CollectionKeyPathChangeChecker::find_changed_columns(std::vector<ColKey>& changed_columns,
392
                                                          const KeyPath& key_path, size_t depth, const Table& table,
393
                                                          const ObjKey& object_key)
394
{
2,530✔
395
    REALM_ASSERT(!object_key.is_unresolved());
2,530✔
396

1,265✔
397
    if (depth >= key_path.size()) {
2,530✔
398
        // We've reached the end of the key path.
41✔
399

41✔
400
        // For the special case of having a backlink at the end of a key path we need to check this level too.
41✔
401
        // Modifications to a backlink are found via the insertions on the origin table (which we are in right
41✔
402
        // now).
41✔
403
        auto last_key_path_element = key_path[key_path.size() - 1];
82✔
404
        auto last_column_key = last_key_path_element.second;
82✔
405
        if (last_column_key.get_type() == col_type_BackLink) {
82✔
406
            auto iterator = m_info.tables.find(table.get_key());
60✔
407
            auto table_has_changed = [iterator] {
60✔
408
                return !iterator->second.insertions_empty() || !iterator->second.modifications_empty() ||
60✔
409
                       !iterator->second.deletions_empty();
31✔
410
            };
60✔
411
            if (iterator != m_info.tables.end() && table_has_changed()) {
60✔
412
                ColKey root_column_key = key_path[0].second;
60✔
413
                changed_columns.push_back(root_column_key);
60✔
414
            }
60✔
415
        }
60✔
416

41✔
417
        return;
82✔
418
    }
82✔
419

1,224✔
420
    auto [table_key, column_key] = key_path.at(depth);
2,448✔
421

1,224✔
422
    // Check for a change on the current depth level.
1,224✔
423
    auto iterator = m_info.tables.find(table_key);
2,448✔
424
    if (iterator != m_info.tables.end() && (iterator->second.modifications_contains(object_key, {column_key}) ||
2,448✔
425
                                            iterator->second.insertions_contains(object_key))) {
1,613✔
426
        // If an object linked to the root object was changed we only mark the
52✔
427
        // property of the root objects as changed.
52✔
428
        // This is also the reason why we can return right after doing so because we would only mark the same root
52✔
429
        // property again in case we find another change deeper down the same path.
52✔
430
        auto root_column_key = key_path[0].second;
104✔
431
        changed_columns.push_back(root_column_key);
104✔
432
        return;
104✔
433
    }
104✔
434

1,172✔
435
    // Only continue for any kind of link.
1,172✔
436
    auto column_type = column_key.get_type();
2,344✔
437
    if (column_type != col_type_Link && column_type != col_type_BackLink && column_type != col_type_TypedLink &&
2,344✔
438
        column_type != col_type_Mixed) {
1,791✔
439
        return;
726✔
440
    }
726✔
441

809✔
442
    auto check_mixed_object = [&](const Mixed& mixed_object) {
1,618✔
443
        if (mixed_object.is_type(type_Link, type_TypedLink)) {
320✔
444
            auto object_key = mixed_object.get<ObjKey>();
300✔
445
            if (object_key.is_unresolved()) {
300✔
446
                return;
×
447
            }
×
448
            auto target_table_key = mixed_object.get_link().get_table_key();
300✔
449
            Group* group = table.get_parent_group();
300✔
450
            auto target_table = group->get_table(target_table_key);
300✔
451
            find_changed_columns(changed_columns, key_path, depth + 1, *target_table, object_key);
300✔
452
        }
300✔
453
    };
320✔
454

809✔
455
    // Advance one level deeper into the key path.
809✔
456
    auto object = table.get_object(object_key);
1,618✔
457
    if (column_key.is_list()) {
1,618✔
458
        if (column_type == col_type_Mixed) {
176✔
459
            auto list = object.get_list<Mixed>(column_key);
78✔
460
            for (size_t i = 0; i < list.size(); i++) {
90✔
461
                auto target_object = list.get_any(i);
12✔
462
                check_mixed_object(target_object);
12✔
463
            }
12✔
464
        }
78✔
465
        else {
98✔
466
            REALM_ASSERT(column_type == col_type_Link);
98✔
467
            auto list = object.get_linklist(column_key);
98✔
468
            auto target_table = table.get_link_target(column_key);
98✔
469
            for (size_t i = 0; i < list.size(); i++) {
140✔
470
                auto target_object = list.get(i);
42✔
471
                find_changed_columns(changed_columns, key_path, depth + 1, *target_table, target_object);
42✔
472
            }
42✔
473
        }
98✔
474
    }
176✔
475
    else if (column_key.is_set()) {
1,442✔
476
        if (column_type == col_type_Mixed) {
156✔
477
            auto set = object.get_set<Mixed>(column_key);
78✔
478
            for (auto& mixed_val : set) {
45✔
479
                check_mixed_object(mixed_val);
12✔
480
            }
12✔
481
        }
78✔
482
        else {
78✔
483
            REALM_ASSERT(column_type == col_type_Link);
78✔
484
            auto set = object.get_linkset(column_key);
78✔
485
            auto target_table = table.get_link_target(column_key);
78✔
486
            for (auto& target_object : set) {
42✔
487
                find_changed_columns(changed_columns, key_path, depth + 1, *target_table, target_object);
6✔
488
            }
6✔
489
        }
78✔
490
    }
156✔
491
    else if (column_key.is_dictionary()) {
1,286✔
492
        // a dictionary always stores mixed values
78✔
493
        auto dictionary = object.get_dictionary(column_key);
156✔
494
        dictionary.for_all_values([&](Mixed val) {
87✔
495
            check_mixed_object(val);
18✔
496
        });
18✔
497
    }
156✔
498
    else if (column_type == col_type_Mixed) {
1,130✔
499
        check_mixed_object(object.get_any(column_key));
278✔
500
    }
278✔
501
    else if (column_type == col_type_Link) {
852✔
502
        // A forward link will only have one target object.
333✔
503
        auto target_object = object.get<ObjKey>(column_key);
666✔
504
        if (!target_object || target_object.is_unresolved()) {
666✔
505
            return;
116✔
506
        }
116✔
507
        auto target_table = table.get_link_target(column_key);
550✔
508
        find_changed_columns(changed_columns, key_path, depth + 1, *target_table, target_object);
550✔
509
    }
550✔
510
    else if (column_type == col_type_BackLink) {
186✔
511
        // A backlink can have multiple origin objects. We need to iterate over all of them.
93✔
512
        auto origin_table = table.get_opposite_table(column_key);
186✔
513
        auto origin_column_key = table.get_opposite_column(column_key);
186✔
514
        size_t backlink_count = object.get_backlink_count(*origin_table, origin_column_key);
186✔
515
        for (size_t i = 0; i < backlink_count; i++) {
388✔
516
            auto origin_object = object.get_backlink(*origin_table, origin_column_key, i);
202✔
517
            find_changed_columns(changed_columns, key_path, depth + 1, *origin_table, origin_object);
202✔
518
        }
202✔
519
    }
186✔
520
    else {
×
521
        REALM_UNREACHABLE(); // unhandled column type
522
    }
×
523
}
1,618✔
524

525
ObjectKeyPathChangeChecker::ObjectKeyPathChangeChecker(TransactionChangeInfo const& info, Table const& root_table,
526
                                                       std::vector<RelatedTable> const& related_tables,
527
                                                       const KeyPathArray& key_path_array,
528
                                                       bool all_callbacks_filtered)
529
    : CollectionKeyPathChangeChecker(info, root_table, related_tables, key_path_array, all_callbacks_filtered)
530
{
78✔
531
}
78✔
532

533
std::vector<ColKey> ObjectKeyPathChangeChecker::operator()(ObjKey object_key)
534
{
78✔
535
    std::vector<ColKey> changed_columns;
78✔
536

39✔
537
    for (auto& key_path : m_key_path_array) {
72✔
538
        find_changed_columns(changed_columns, key_path, 0, m_root_table, object_key);
66✔
539
    }
66✔
540

39✔
541
    return changed_columns;
78✔
542
}
78✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc