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

realm / realm-core / nicola.cabiddu_1040

26 Sep 2023 05:08PM UTC coverage: 91.056% (-1.9%) from 92.915%
nicola.cabiddu_1040

Pull #6766

Evergreen

nicola-cab
several fixes and final client reset algo for collection in mixed
Pull Request #6766: Client Reset for collections in mixed / nested collections

97128 of 178458 branches covered (0.0%)

1524 of 1603 new or added lines in 5 files covered. (95.07%)

4511 existing lines in 109 files now uncovered.

236619 of 259862 relevant lines covered (91.06%)

7169640.31 hits per line

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

99.41
/src/realm/object-store/impl/collection_change_builder.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2016 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/collection_change_builder.hpp>
20

21
#include <realm/util/assert.hpp>
22

23
#include <algorithm>
24

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

28
CollectionChangeBuilder::CollectionChangeBuilder(IndexSet deletions, IndexSet insertions, IndexSet modifications,
29
                                                 std::vector<Move> moves, bool root_was_deleted,
30
                                                 bool collection_was_cleared)
31
    : CollectionChangeSet({std::move(deletions),
32
                           std::move(insertions),
33
                           std::move(modifications),
34
                           {},
35
                           std::move(moves),
36
                           root_was_deleted,
37
                           collection_was_cleared})
38
{
139,695✔
39
    for (auto&& move : this->moves) {
71,553✔
40
        this->deletions.add(move.from);
72✔
41
        this->insertions.add(move.to);
72✔
42
    }
72✔
43
}
139,695✔
44

45
void CollectionChangeBuilder::merge(CollectionChangeBuilder&& c)
46
{
64,047✔
47
    if (c.empty())
64,047✔
48
        return;
36,881✔
49
    if (empty()) {
27,166✔
50
        *this = std::move(c);
26,374✔
51
        return;
26,374✔
52
    }
26,374✔
53

225✔
54
    verify();
792✔
55
    c.verify();
792✔
56

225✔
57
    // FIXME: this is comically wasteful
225✔
58
    std::unordered_set<int64_t> col_keys;
792✔
59
    if (m_track_columns) {
792✔
60
        for (auto& col : columns)
792✔
61
            col_keys.insert(col.first);
811✔
62
        for (auto& col : c.columns)
792✔
63
            col_keys.insert(col.first);
761✔
64
    }
792✔
65

225✔
66
    auto for_each_col = [&](auto&& f) {
808✔
67
        f(modifications, c.modifications);
808✔
68
        if (m_track_columns) {
808✔
69
            for (auto col : col_keys)
808✔
70
                f(columns[col], c.columns[col]);
817✔
71
        }
808✔
72
    };
808✔
73

225✔
74
    // First update any old moves
225✔
75
    if (!c.moves.empty() || !c.deletions.empty() || !c.insertions.empty()) {
792✔
76
        auto it = std::remove_if(begin(moves), end(moves), [&](auto& old) {
96✔
77
            // Check if the moved row was moved again, and if so just update the destination
17✔
78
            auto it = std::find_if(begin(c.moves), end(c.moves), [&](auto const& m) {
26✔
79
                return old.to == m.from;
18✔
80
            });
18✔
81
            if (it != c.moves.end()) {
34✔
82
                for_each_col([&](auto& col, auto& other) {
6✔
83
                    if (col.contains(it->from))
6✔
84
                        other.add(it->to);
4✔
85
                });
6✔
86
                old.to = it->to;
6✔
87
                *it = c.moves.back();
6✔
88
                c.moves.pop_back();
6✔
89
                return false;
6✔
90
            }
6✔
91

14✔
92
            // Check if the destination was deleted
14✔
93
            // Removing the insert for this move will happen later
14✔
94
            if (c.deletions.contains(old.to))
28✔
95
                return true;
6✔
96

11✔
97
            // Update the destination to adjust for any new insertions and deletions
11✔
98
            old.to = c.insertions.shift(c.deletions.unshift(old.to));
22✔
99
            return false;
22✔
100
        });
22✔
101
        moves.erase(it, end(moves));
158✔
102
    }
158✔
103

225✔
104
    // Ignore new moves of rows which were previously inserted (the implicit
225✔
105
    // delete from the move will remove the insert)
225✔
106
    if (!insertions.empty() && !c.moves.empty()) {
792✔
107
        c.moves.erase(std::remove_if(begin(c.moves), end(c.moves),
18✔
108
                                     [&](auto const& m) {
18✔
109
                                         return insertions.contains(m.from);
18✔
110
                                     }),
18✔
111
                      end(c.moves));
18✔
112
    }
18✔
113

225✔
114
    // Ensure that any previously modified rows which were moved are still modified
225✔
115
    if (!modifications.empty() && !c.moves.empty()) {
792✔
116
        for (auto const& move : c.moves) {
10✔
117
            for_each_col([&](auto& col, auto& other) {
10✔
118
                if (col.contains(move.from))
10✔
119
                    other.add(move.to);
6✔
120
            });
10✔
121
        }
10✔
122
    }
10✔
123

225✔
124
    // Update the source position of new moves to compensate for the changes made
225✔
125
    // in the old changeset
225✔
126
    if (!deletions.empty() || !insertions.empty()) {
792✔
127
        for (auto& move : c.moves)
160✔
128
            move.from = deletions.shift(insertions.unshift(move.from));
22✔
129
    }
160✔
130

225✔
131
    moves.insert(end(moves), begin(c.moves), end(c.moves));
792✔
132

225✔
133
    // New deletion indices have been shifted by the insertions, so unshift them
225✔
134
    // before adding
225✔
135
    deletions.add_shifted_by(insertions, c.deletions);
792✔
136

225✔
137
    // Drop any inserted-then-deleted rows, then merge in new insertions
225✔
138
    insertions.erase_at(c.deletions);
792✔
139
    insertions.insert_at(c.insertions);
792✔
140

225✔
141
    clean_up_stale_moves();
792✔
142

225✔
143
    for_each_col([&](auto& col, auto& other) {
1,609✔
144
        col.erase_at(c.deletions);
1,609✔
145
        col.shift_for_insert_at(c.insertions);
1,609✔
146
        col.add(other);
1,609✔
147
    });
1,609✔
148
    if (c.collection_root_was_deleted) {
792✔
149
        collection_root_was_deleted = true;
×
150
    }
×
151
    collection_was_cleared = c.collection_was_cleared;
792✔
152
    c = {};
792✔
153
    verify();
792✔
154
}
792✔
155

156
void CollectionChangeBuilder::clean_up_stale_moves()
157
{
5,362✔
158
    // Look for moves which are now no-ops, and remove them plus the associated
2,510✔
159
    // insert+delete. Note that this isn't just checking for from == to due to
2,510✔
160
    // that rows can also be shifted by other inserts and deletes
2,510✔
161
    moves.erase(std::remove_if(begin(moves), end(moves),
5,362✔
162
                               [&](auto const& move) {
2,583✔
163
                                   if (move.from - deletions.count(0, move.from) !=
146✔
164
                                       move.to - insertions.count(0, move.to))
146✔
165
                                       return false;
126✔
166
                                   deletions.remove(move.from);
20✔
167
                                   insertions.remove(move.to);
20✔
168
                                   return true;
20✔
169
                               }),
20✔
170
                end(moves));
5,362✔
171
}
5,362✔
172

173
void CollectionChangeBuilder::modify(size_t ndx, size_t col)
174
{
342✔
175
    modifications.add(ndx);
342✔
176
    if (!m_track_columns || col == IndexSet::npos)
342✔
177
        return;
326✔
178
    columns[col].add(ndx);
16✔
179
}
16✔
180

181
template <typename Func>
182
void CollectionChangeBuilder::for_each_col(Func&& f)
183
{
1,850✔
184
    f(modifications);
1,850✔
185
    if (m_track_columns) {
1,850✔
186
        for (auto& col : columns)
1,850✔
187
            f(col.second);
12✔
188
    }
1,850✔
189
}
1,850✔
190

191
void CollectionChangeBuilder::insert(size_t index, size_t count, bool track_moves)
192
{
980✔
193
    REALM_ASSERT(count != 0);
980✔
194

490✔
195
    for_each_col([=](auto& col) {
982✔
196
        col.shift_for_insert_at(index, count);
982✔
197
    });
982✔
198
    if (!track_moves)
980✔
UNCOV
199
        return;
×
200

490✔
201
    insertions.insert_at(index, count);
980✔
202

490✔
203
    for (auto& move : moves) {
495✔
204
        if (move.to >= index)
10✔
205
            move.to += count;
6✔
206
    }
10✔
207

490✔
208
    collection_was_cleared = false;
980✔
209
}
980✔
210

211
void CollectionChangeBuilder::erase(size_t index)
212
{
740✔
213
    for_each_col([=](auto& col) {
744✔
214
        col.erase_at(index);
744✔
215
    });
744✔
216
    size_t unshifted = insertions.erase_or_unshift(index);
740✔
217
    if (unshifted != IndexSet::npos)
740✔
218
        deletions.add_shifted(unshifted);
728✔
219

370✔
220
    for (size_t i = 0; i < moves.size(); ++i) {
760✔
221
        auto& move = moves[i];
20✔
222
        if (move.to == index) {
20✔
223
            moves.erase(moves.begin() + i);
6✔
224
            --i;
6✔
225
        }
6✔
226
        else if (move.to > index)
14✔
227
            --move.to;
8✔
228
    }
20✔
229
}
740✔
230

231
void CollectionChangeBuilder::clear(size_t old_size)
232
{
300✔
233
    for (auto range : deletions)
300✔
234
        old_size += range.second - range.first;
12✔
235
    for (auto range : insertions)
300✔
236
        old_size -= range.second - range.first;
14✔
237

150✔
238
    modifications.clear();
300✔
239
    insertions.clear();
300✔
240
    moves.clear();
300✔
241
    columns.clear();
300✔
242
    deletions.set(old_size);
300✔
243
    collection_was_cleared = true;
300✔
244
}
300✔
245

246
void CollectionChangeBuilder::move(size_t from, size_t to)
247
{
130✔
248
    REALM_ASSERT(from != to);
130✔
249

65✔
250
    bool updated_existing_move = false;
130✔
251
    for (auto& move : moves) {
90✔
252
        if (move.to != from) {
50✔
253
            // Shift other moves if this row is moving from one side of them
18✔
254
            // to the other
18✔
255
            if (move.to >= to && move.to < from)
36✔
256
                ++move.to;
6✔
257
            else if (move.to <= to && move.to > from)
30✔
258
                --move.to;
10✔
259
            continue;
36✔
260
        }
36✔
261
        REALM_ASSERT(!updated_existing_move);
14✔
262

7✔
263
        // Collapse A -> B, B -> C into a single A -> C move
7✔
264
        move.to = to;
14✔
265
        updated_existing_move = true;
14✔
266

7✔
267
        insertions.erase_at(from);
14✔
268
        insertions.insert_at(to);
14✔
269
    }
14✔
270

65✔
271
    if (!updated_existing_move) {
130✔
272
        auto shifted_from = insertions.erase_or_unshift(from);
116✔
273
        insertions.insert_at(to);
116✔
274

58✔
275
        // Don't report deletions/moves for newly inserted rows
58✔
276
        if (shifted_from != IndexSet::npos) {
116✔
277
            shifted_from = deletions.add_shifted(shifted_from);
110✔
278
            moves.push_back({shifted_from, to});
110✔
279
        }
110✔
280
    }
116✔
281

65✔
282
    for_each_col([=](auto& col) {
136✔
283
        bool modified = col.contains(from);
136✔
284
        col.erase_at(from);
136✔
285

68✔
286
        if (modified)
136✔
287
            col.insert_at(to);
6✔
288
        else
130✔
289
            col.shift_for_insert_at(to);
130✔
290
    });
136✔
291
}
130✔
292

293
void CollectionChangeBuilder::verify()
294
{
5,928✔
295
#ifdef REALM_DEBUG
5,928✔
296
    for (auto&& move : moves) {
2,515✔
297
        REALM_ASSERT(deletions.contains(move.from));
128✔
298
        REALM_ASSERT(insertions.contains(move.to));
128✔
299
    }
128✔
300
#endif
5,928✔
301
}
5,928✔
302

303
namespace {
304
struct RowInfo {
305
    int64_t key;
306
    size_t prev_tv_index;
307
    size_t tv_index;
308
};
309

310
#if 0 // FIXME: this is applicable to backlinks still
311
// Calculates the insertions/deletions required for a query on a table without
312
// a sort, where `removed` includes the rows which were modified to no longer
313
// match the query (but not outright deleted rows, which are filtered out long
314
// before any of this logic), and `move_candidates` tracks the rows which may
315
// be the result of a move.
316
//
317
// This function is not strictly required, as calculate_moves_sorted() will
318
// produce correct results even for the scenarios where this function is used.
319
// However, this function has asymptotically better worst-case performance and
320
// extremely cheap best-case performance, and is guaranteed to produce a minimal
321
// diff.
322
void calculate_moves_backlinks(std::vector<RowInfo>& new_rows, IndexSet& removed,
323
                               IndexSet const& move_candidates,
324
                               CollectionChangeSet& changeset)
325
{
326
    // Here we track which row we expect to see, which in the absence of swap()
327
    // is always the row immediately after the last row which was not moved.
328
    size_t expected = 0;
329
    for (auto& row : new_rows) {
330
        if (row.shifted_tv_index == expected) {
331
            ++expected;
332
            continue;
333
        }
334

335
        // We didn't find the row we were expecting to find, which means that
336
        // either a row was moved forward to here, the row we were expecting was
337
        // removed, or the row we were expecting moved back.
338

339
        // First check if this row even could have moved. If it can't, just
340
        // treat it as a match and move on, and we'll handle the row we were
341
        // expecting when we hit it later.
342
        if (!move_candidates.contains(row.key)) {
343
            expected = row.shifted_tv_index + 1;
344
            continue;
345
        }
346

347
        // Next calculate where we expect this row to be based on the insertions
348
        // and removals (i.e. rows changed to not match the query), as it could
349
        // be that the row actually ends up in this spot due to the rows before
350
        // it being removed.
351
        size_t calc_expected = row.tv_index - changeset.insertions.count(0, row.tv_index) + removed.count(0, row.prev_tv_index);
352
        if (row.shifted_tv_index == calc_expected) {
353
            expected = calc_expected + 1;
354
            continue;
355
        }
356

357
        // The row still isn't the expected one, so record it as a move
358
        changeset.moves.push_back({row.prev_tv_index, row.tv_index});
359
        changeset.insertions.add(row.tv_index);
360
        removed.add(row.prev_tv_index);
361
    }
362
}
363
#endif
364

365
class LongestCommonSubsequenceCalculator {
366
public:
367
    // A pair of an object key and an index in the table view
368
    struct Row {
369
        int64_t key;
370
        size_t tv_index;
371
    };
372

373
    struct Match {
374
        // The index in `a` at which this match begins
375
        size_t i;
376
        // The index in `b` at which this match begins
377
        size_t j;
378
        // The length of this match
379
        size_t size;
380
        // The number of rows in this block which were modified
381
        size_t modified;
382
    };
383
    std::vector<Match> m_longest_matches;
384

385
    LongestCommonSubsequenceCalculator(std::vector<Row>& a, std::vector<Row>& b, size_t start_index,
386
                                       IndexSet const& modifications)
387
        : m_modified(modifications)
388
        , a(a)
389
        , b(b)
390
    {
244✔
391
        find_longest_matches(start_index, a.size(), start_index, b.size());
244✔
392
        m_longest_matches.push_back({a.size(), b.size(), 0});
244✔
393
    }
244✔
394

395
private:
396
    IndexSet const& m_modified;
397

398
    // The two arrays of rows being diffed
399
    // a is sorted by tv_index, b is sorted by key
400
    std::vector<Row>&a, &b;
401

402
    // Find the longest matching range in (a + begin1, a + end1) and (b + begin2, b + end2)
403
    // "Matching" is defined as "has the same row index"; the TV index is just
404
    // there to let us turn an index in a/b into an index which can be reported
405
    // in the output changeset.
406
    //
407
    // This is done with the O(N) space variant of the dynamic programming
408
    // algorithm for longest common subsequence, where N is the maximum number
409
    // of the most common row index (which for everything but linkview-derived
410
    // TVs will be 1).
411
    Match find_longest_match(size_t begin1, size_t end1, size_t begin2, size_t end2)
412
    {
332✔
413
        struct Length {
332✔
414
            size_t j, len;
332✔
415
        };
332✔
416
        // The length of the matching block for each `j` for the previously checked row
166✔
417
        std::vector<Length> prev;
332✔
418
        // The length of the matching block for each `j` for the row currently being checked
166✔
419
        std::vector<Length> cur;
332✔
420

166✔
421
        // Calculate the length of the matching block *ending* at b[j], which
166✔
422
        // is 1 if b[j - 1] did not match, and b[j - 1] + 1 otherwise.
166✔
423
        auto length = [&](size_t j) -> size_t {
1,026✔
424
            for (auto const& pair : prev) {
882✔
425
                if (pair.j + 1 == j)
738✔
426
                    return pair.len + 1;
310✔
427
            }
738✔
428
            return 1;
871✔
429
        };
1,026✔
430

166✔
431
        // Iterate over each `j` which has the same row index as a[i] and falls
166✔
432
        // within the range begin2 <= j < end2
166✔
433
        auto for_each_b_match = [&](size_t i, auto&& f) {
1,080✔
434
            auto ai = a[i].key;
1,080✔
435
            // Find the TV indicies at which this row appears in the new results
540✔
436
            // There should always be at least one (or it would have been
540✔
437
            // filtered out earlier), but there can be multiple if there are dupes
540✔
438
            auto it = lower_bound(begin(b), end(b), ai, [](auto lft, auto rgt) {
3,054✔
439
                return lft.key < rgt;
3,054✔
440
            });
3,054✔
441
            REALM_ASSERT(it != end(b) && it->key == ai);
1,080✔
442
            for (; it != end(b) && it->key == ai; ++it) {
2,174✔
443
                size_t j = it->tv_index;
1,114✔
444
                if (j < begin2)
1,114✔
445
                    continue;
68✔
446
                if (j >= end2)
1,046✔
447
                    break; // b is sorted by tv_index so this can't transition from false to true
20✔
448
                f(j);
1,026✔
449
            }
1,026✔
450
        };
1,080✔
451

166✔
452
        Match best = {begin1, begin2, 0, 0};
332✔
453
        for (size_t i = begin1; i < end1; ++i) {
1,412✔
454
            // prev = std::move(cur), but avoids discarding prev's heap allocation
540✔
455
            cur.swap(prev);
1,080✔
456
            cur.clear();
1,080✔
457

540✔
458
            for_each_b_match(i, [&](size_t j) {
1,053✔
459
                size_t size = length(j);
1,026✔
460

513✔
461
                cur.push_back({j, size});
1,026✔
462

513✔
463
                // If the matching block ending at a[i] and b[j] is longer than
513✔
464
                // the previous one, select it as the best
513✔
465
                if (size > best.size)
1,026✔
466
                    best = {i - size + 1, j - size + 1, size, IndexSet::npos};
590✔
467
                // Given two equal-length matches, prefer the one with fewer modified rows
218✔
468
                else if (size == best.size) {
436✔
469
                    if (best.modified == IndexSet::npos)
326✔
470
                        best.modified = m_modified.count(best.j - size + 1, best.j + 1);
242✔
471
                    auto count = m_modified.count(j - size + 1, j + 1);
326✔
472
                    if (count < best.modified)
326✔
473
                        best = {i - size + 1, j - size + 1, size, count};
96✔
474
                }
326✔
475

513✔
476
                // The best block should always fall within the range being searched
513✔
477
                REALM_ASSERT(best.i >= begin1 && best.i + best.size <= end1);
1,026✔
478
                REALM_ASSERT(best.j >= begin2 && best.j + best.size <= end2);
1,026✔
479
            });
1,026✔
480
        }
1,080✔
481
        return best;
332✔
482
    }
332✔
483

484
    void find_longest_matches(size_t begin1, size_t end1, size_t begin2, size_t end2)
485
    {
332✔
486
        // FIXME: recursion could get too deep here
166✔
487
        // recursion depth worst case is currently O(N) and each recursion uses 320 bytes of stack
166✔
488
        // could reduce worst case to O(sqrt(N)) (and typical case to O(log N))
166✔
489
        // biasing equal selections towards the middle, but that's still
166✔
490
        // insufficient for Android's 8 KB stacks
166✔
491
        auto m = find_longest_match(begin1, end1, begin2, end2);
332✔
492
        if (!m.size)
332✔
493
            return;
24✔
494
        if (m.i > begin1 && m.j > begin2)
308✔
495
            find_longest_matches(begin1, m.i, begin2, m.j);
34✔
496
        m_longest_matches.push_back(m);
308✔
497
        if (m.i + m.size < end2 && m.j + m.size < end2)
308✔
498
            find_longest_matches(m.i + m.size, end1, m.j + m.size, end2);
54✔
499
    }
308✔
500
};
501

502
void calculate_moves_sorted(std::vector<RowInfo>& rows, CollectionChangeSet& changeset)
503
{
2,686✔
504
    // The RowInfo array contains information about the old and new TV indices of
1,343✔
505
    // each row, which we need to turn into two sequences of rows, which we'll
1,343✔
506
    // then find matches in
1,343✔
507
    std::vector<LongestCommonSubsequenceCalculator::Row> a, b;
2,686✔
508

1,343✔
509
    a.reserve(rows.size());
2,686✔
510
    for (auto& row : rows)
2,686✔
511
        a.push_back({row.key, row.prev_tv_index});
5,108✔
512
    std::sort(begin(a), end(a), [](auto lft, auto rgt) {
4,876✔
513
        return std::tie(lft.tv_index, lft.key) < std::tie(rgt.tv_index, rgt.key);
4,876✔
514
    });
4,876✔
515

1,343✔
516
    // Before constructing `b`, first find the first index in `a` which will
1,343✔
517
    // actually differ in `b`, and skip everything else if there aren't any
1,343✔
518
    size_t first_difference = IndexSet::npos;
2,686✔
519
    for (size_t i = 0; i < a.size(); ++i) {
6,916✔
520
        if (a[i].key != rows[i].key) {
4,474✔
521
            first_difference = i;
244✔
522
            break;
244✔
523
        }
244✔
524
    }
4,474✔
525
    if (first_difference == IndexSet::npos)
2,686✔
526
        return;
2,442✔
527

122✔
528
    // Note that `b` is sorted by key, while `a` is sorted by tv_index
122✔
529
    b.reserve(rows.size());
244✔
530
    for (size_t i = 0; i < rows.size(); ++i)
1,260✔
531
        b.push_back({rows[i].key, i});
1,016✔
532
    std::sort(begin(b), end(b), [](auto lft, auto rgt) {
1,877✔
533
        return std::tie(lft.key, lft.tv_index) < std::tie(rgt.key, rgt.tv_index);
1,877✔
534
    });
1,877✔
535

122✔
536
    // Calculate the LCS of the two sequences
122✔
537
    auto matches =
244✔
538
        LongestCommonSubsequenceCalculator(a, b, first_difference, changeset.modifications).m_longest_matches;
244✔
539

122✔
540
    // And then insert and delete rows as needed to align them
122✔
541
    size_t i = first_difference, j = first_difference;
244✔
542
    for (auto match : matches) {
552✔
543
        for (; i < match.i; ++i)
840✔
544
            changeset.deletions.add(a[i].tv_index);
288✔
545
        for (; j < match.j; ++j)
840✔
546
            changeset.insertions.add(rows[j].tv_index);
288✔
547
        i += match.size;
552✔
548
        j += match.size;
552✔
549
    }
552✔
550
}
244✔
551

552
template <typename T>
553
void verify_changeset(std::vector<T> const& prev_rows, std::vector<T> const& next_rows,
554
                      CollectionChangeBuilder const& changeset)
555
{
3,552✔
556
#ifdef REALM_DEBUG
3,552✔
557
    { // Verify that applying the calculated change to prev_rows actually produces next_rows
3,552✔
558
        auto rows = prev_rows;
3,552✔
559
        auto it = util::make_reverse_iterator(changeset.deletions.end());
3,552✔
560
        auto end = util::make_reverse_iterator(changeset.deletions.begin());
3,552✔
561
        for (; it != end; ++it) {
4,574✔
562
            rows.erase(rows.begin() + it->first, rows.begin() + it->second);
1,022✔
563
        }
1,022✔
564

1,776✔
565
        for (auto i : changeset.insertions.as_indexes()) {
2,498✔
566
            rows.insert(rows.begin() + i, next_rows[i]);
1,444✔
567
        }
1,444✔
568

1,776✔
569
        REALM_ASSERT(rows == next_rows);
3,552✔
570
    }
3,552✔
571
#else
572
    static_cast<void>(prev_rows);
573
    static_cast<void>(next_rows);
574
    static_cast<void>(changeset);
575
#endif
576
}
3,552✔
577

578
void calculate(CollectionChangeBuilder& ret, std::vector<RowInfo> old_rows, std::vector<RowInfo> new_rows,
579
               util::FunctionRef<bool(int64_t)> key_did_change, bool in_table_order)
580
{
3,552✔
581
    // Now that our old and new sets of rows are sorted by key, we can
1,776✔
582
    // iterate over them and either record old+new TV indices for rows present
1,776✔
583
    // in both, or mark them as inserted/deleted if they appear only in one
1,776✔
584
    size_t i = 0, j = 0;
3,552✔
585
    while (i < old_rows.size() && j < new_rows.size()) {
11,926✔
586
        auto old_index = old_rows[i];
8,374✔
587
        auto& new_index = new_rows[j];
8,374✔
588
        if (old_index.key == new_index.key) {
8,374✔
589
            new_index.prev_tv_index = old_rows[i].tv_index;
7,622✔
590
            ++i;
7,622✔
591
            ++j;
7,622✔
592
        }
7,622✔
593
        else if (old_index.key < new_index.key) {
752✔
594
            ret.deletions.add(old_index.tv_index);
528✔
595
            ++i;
528✔
596
        }
528✔
597
        else {
224✔
598
            ret.insertions.add(new_index.tv_index);
224✔
599
            ++j;
224✔
600
        }
224✔
601
    }
8,374✔
602

1,776✔
603
    for (; i < old_rows.size(); ++i)
4,990✔
604
        ret.deletions.add(old_rows[i].tv_index);
1,438✔
605
    for (; j < new_rows.size(); ++j)
4,484✔
606
        ret.insertions.add(new_rows[j].tv_index);
932✔
607

1,776✔
608
    // Filter out the new insertions since we don't need them for any of the
1,776✔
609
    // further calculations
1,776✔
610
    new_rows.erase(std::remove_if(begin(new_rows), end(new_rows),
3,552✔
611
                                  [](auto& row) {
8,778✔
612
                                      return row.prev_tv_index == IndexSet::npos;
8,778✔
613
                                  }),
8,778✔
614
                   end(new_rows));
3,552✔
615
    std::sort(begin(new_rows), end(new_rows), [](auto& lft, auto& rgt) {
9,685✔
616
        return lft.tv_index < rgt.tv_index;
9,685✔
617
    });
9,685✔
618

1,776✔
619
    for (auto& row : new_rows) {
7,622✔
620
        if (key_did_change(row.key)) {
7,622✔
621
            ret.modifications.add(row.tv_index);
2,354✔
622
        }
2,354✔
623
    }
7,622✔
624

1,776✔
625
    if (!in_table_order)
3,552✔
626
        calculate_moves_sorted(new_rows, ret);
2,686✔
627
}
3,552✔
628

629
template <typename T>
630
int64_t to_int64_t(T val)
631
{
8,850✔
632
    return static_cast<int64_t>(val);
8,850✔
633
}
8,850✔
634

635
template <>
636
int64_t to_int64_t<ObjKey>(ObjKey val)
637
{
9,516✔
638
    return val.value;
9,516✔
639
}
9,516✔
640

641
void sort_row_info(std::vector<RowInfo>& info)
642
{
7,104✔
643
    std::sort(begin(info), end(info), [](auto& lft, auto& rgt) {
24,863✔
644
        return lft.key < rgt.key;
24,863✔
645
    });
24,863✔
646
}
7,104✔
647

648
template <typename T>
649
std::vector<RowInfo> build_row_info(const std::vector<T>& rows)
650
{
7,104✔
651
    std::vector<RowInfo> info;
7,104✔
652
    info.reserve(rows.size());
7,104✔
653
    for (size_t i = 0; i < rows.size(); ++i)
25,470✔
654
        info.push_back({to_int64_t(rows[i]), IndexSet::npos, i});
18,366✔
655
    sort_row_info(info);
7,104✔
656
    return info;
7,104✔
657
}
7,104✔
658

659
} // Anonymous namespace
660

661
CollectionChangeBuilder CollectionChangeBuilder::calculate(const ObjKeys& prev_objs, const ObjKeys& next_objs,
662
                                                           util::FunctionRef<bool(ObjKey)> key_did_change,
663
                                                           bool in_table_order)
664
{
2,262✔
665
    CollectionChangeBuilder ret;
2,262✔
666
    ::calculate(
2,262✔
667
        ret, build_row_info(prev_objs), build_row_info(next_objs),
2,262✔
668
        [&key_did_change](int64_t key) {
4,406✔
669
            return key_did_change(ObjKey(key));
4,406✔
670
        },
4,406✔
671
        in_table_order);
2,262✔
672
    ret.verify();
2,262✔
673
    verify_changeset(prev_objs, next_objs, ret);
2,262✔
674
    return ret;
2,262✔
675
}
2,262✔
676

677
CollectionChangeBuilder CollectionChangeBuilder::calculate(std::vector<size_t> const& prev_rows,
678
                                                           std::vector<size_t> const& next_rows,
679
                                                           util::FunctionRef<bool(size_t)> ndx_did_change)
680
{
1,290✔
681
    CollectionChangeBuilder ret;
1,290✔
682
    ::calculate(
1,290✔
683
        ret, build_row_info(prev_rows), build_row_info(next_rows),
1,290✔
684
        [&ndx_did_change](int64_t ndx) {
3,216✔
685
            return ndx_did_change(size_t(ndx));
3,216✔
686
        },
3,216✔
687
        false);
1,290✔
688
    ret.verify();
1,290✔
689
    verify_changeset(prev_rows, next_rows, ret);
1,290✔
690
    return ret;
1,290✔
691
}
1,290✔
692

693
CollectionChangeSet CollectionChangeBuilder::finalize() &&
694
{
22,221✔
695
    // Calculate which indices in the old collection were modified
11,787✔
696
    auto modifications_in_old = modifications;
22,221✔
697
    modifications_in_old.erase_at(insertions);
22,221✔
698
    modifications_in_old.shift_for_insert_at(deletions);
22,221✔
699

11,787✔
700
    // During changeset calculation we allow marking a row as both inserted and
11,787✔
701
    // modified in case changeset merging results in it no longer being an insert,
11,787✔
702
    // but we don't want inserts in the final modification set
11,787✔
703
    modifications.remove(insertions);
22,221✔
704

11,787✔
705
    return {std::move(deletions),     std::move(insertions), std::move(modifications_in_old),
22,221✔
706
            std::move(modifications), std::move(moves),      collection_root_was_deleted,
22,221✔
707
            collection_was_cleared,   std::move(columns)};
22,221✔
708
}
22,221✔
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