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

realm / realm-core / github_pull_request_281750

30 Oct 2023 03:37PM UTC coverage: 90.528% (-1.0%) from 91.571%
github_pull_request_281750

Pull #6073

Evergreen

jedelbo
Log free space and history sizes when opening file
Pull Request #6073: Merge next-major

95488 of 175952 branches covered (0.0%)

8973 of 12277 new or added lines in 149 files covered. (73.09%)

622 existing lines in 51 files now uncovered.

233503 of 257934 relevant lines covered (90.53%)

6533720.56 hits per line

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

83.08
/src/realm/sync/noinst/client_reset_recovery.cpp
1
///////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2022 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/sync/noinst/client_reset_recovery.hpp>
20

21
#include <realm/db.hpp>
22
#include <realm/dictionary.hpp>
23
#include <realm/object_converter.hpp>
24
#include <realm/set.hpp>
25
#include <realm/transaction.hpp>
26

27
#include <realm/sync/history.hpp>
28
#include <realm/sync/changeset_parser.hpp>
29
#include <realm/sync/noinst/client_history_impl.hpp>
30
#include <realm/sync/noinst/client_reset.hpp>
31
#include <realm/sync/subscriptions.hpp>
32

33
#include <realm/util/compression.hpp>
34

35
#include <algorithm>
36
#include <vector>
37

38
using namespace realm;
39
using namespace _impl;
40
using namespace sync;
41

42
namespace realm::_impl::client_reset {
43

44
util::Optional<ListTracker::CrossListIndex> ListTracker::insert(uint32_t local_index, size_t remote_list_size)
45
{
1,856✔
46
    if (m_requires_manual_copy) {
1,856✔
47
        return util::none;
516✔
48
    }
516✔
49
    uint32_t remote_index = local_index;
1,340✔
50
    if (remote_index > remote_list_size) {
1,340✔
51
        remote_index = static_cast<uint32_t>(remote_list_size);
72✔
52
    }
72✔
53
    for (auto& ndx : m_indices_allowed) {
1,294✔
54
        if (ndx.local >= local_index) {
1,248✔
55
            ++ndx.local;
4✔
56
            ++ndx.remote;
4✔
57
        }
4✔
58
    }
1,248✔
59
    ListTracker::CrossListIndex inserted{local_index, remote_index};
1,340✔
60
    m_indices_allowed.push_back(inserted);
1,340✔
61
    return inserted;
1,340✔
62
}
1,340✔
63

64
util::Optional<ListTracker::CrossListIndex> ListTracker::update(uint32_t index)
65
{
3,324✔
66
    if (m_requires_manual_copy) {
3,324✔
67
        return util::none;
160✔
68
    }
160✔
69
    for (auto& ndx : m_indices_allowed) {
5,656✔
70
        if (ndx.local == index) {
5,656✔
71
            return ndx;
2,568✔
72
        }
2,568✔
73
    }
5,656✔
74
    queue_for_manual_copy();
1,880✔
75
    return util::none;
596✔
76
}
3,164✔
77

78
void ListTracker::clear()
79
{
36✔
80
    // any local operations to a list after a clear are
18✔
81
    // strictly on locally added elements so no need to continue tracking
18✔
82
    m_requires_manual_copy = false;
36✔
83
    m_indices_allowed.clear();
36✔
84
}
36✔
85

86
bool ListTracker::move(uint32_t from, uint32_t to, size_t lst_size, uint32_t& remote_from_out,
87
                       uint32_t& remote_to_out)
88
{
84✔
89
    if (m_requires_manual_copy) {
84✔
90
        return false;
8✔
91
    }
8✔
92
    remote_from_out = from;
76✔
93
    remote_to_out = to;
76✔
94

38✔
95
    // Only allow move operations that operate on known indices.
38✔
96
    // This requires that both local elements 'from' and 'to' are known.
38✔
97
    auto target_from = m_indices_allowed.end();
76✔
98
    auto target_to = m_indices_allowed.end();
76✔
99
    for (auto it = m_indices_allowed.begin(); it != m_indices_allowed.end(); ++it) {
212✔
100
        if (it->local == from) {
136✔
101
            REALM_ASSERT(target_from == m_indices_allowed.end());
64✔
102
            target_from = it;
64✔
103
        }
64✔
104
        else if (it->local == to) {
72✔
105
            REALM_ASSERT(target_to == m_indices_allowed.end());
64✔
106
            target_to = it;
64✔
107
        }
64✔
108
    }
136✔
109
    if (target_from == m_indices_allowed.end() || target_to == m_indices_allowed.end()) {
76✔
110
        queue_for_manual_copy();
12✔
111
        return false;
12✔
112
    }
12✔
113
    REALM_ASSERT_EX(target_from->remote <= lst_size, from, to, target_from->remote, target_to->remote, lst_size);
64✔
114
    REALM_ASSERT_EX(target_to->remote <= lst_size, from, to, target_from->remote, target_to->remote, lst_size);
64✔
115

32✔
116
    if (from < to) {
64✔
117
        for (auto it = m_indices_allowed.begin(); it != m_indices_allowed.end(); ++it) {
124✔
118
            if (it->local > from && it->local <= to) {
84✔
119
                REALM_ASSERT(it->local != 0);
40✔
120
                REALM_ASSERT(it->remote != 0);
40✔
121
                --it->local;
40✔
122
                --it->remote;
40✔
123
            }
40✔
124
        }
84✔
125
        remote_from_out = target_from->remote;
40✔
126
        remote_to_out = target_to->remote + 1;
40✔
127
        target_from->local = target_to->local + 1;
40✔
128
        target_from->remote = target_to->remote + 1;
40✔
129
        return true;
40✔
130
    }
40✔
131
    else if (from > to) {
24✔
132
        for (auto it = m_indices_allowed.begin(); it != m_indices_allowed.end(); ++it) {
76✔
133
            if (it->local < from && it->local >= to) {
52✔
134
                REALM_ASSERT_EX(it->remote + 1 < lst_size, it->remote, lst_size);
28✔
135
                ++it->local;
28✔
136
                ++it->remote;
28✔
137
            }
28✔
138
        }
52✔
139
        remote_from_out = target_from->remote;
24✔
140
        remote_to_out = target_to->remote - 1;
24✔
141
        target_from->local = target_to->local - 1;
24✔
142
        target_from->remote = target_to->remote - 1;
24✔
143
        return true;
24✔
144
    }
24✔
145
    // from == to
146
    // we shouldn't be generating an instruction for this case, but it is a no-op
147
    return true; // LCOV_EXCL_LINE
×
148
}
×
149

150
bool ListTracker::remove(uint32_t index, uint32_t& remote_index_out)
151
{
320✔
152
    if (m_requires_manual_copy) {
320✔
153
        return false;
36✔
154
    }
36✔
155
    remote_index_out = index;
284✔
156
    bool found = false;
284✔
157
    for (auto it = m_indices_allowed.begin(); it != m_indices_allowed.end();) {
348✔
158
        if (it->local == index) {
64✔
159
            found = true;
44✔
160
            remote_index_out = it->remote;
44✔
161
            it = m_indices_allowed.erase(it);
44✔
162
            continue;
44✔
163
        }
44✔
164
        else if (it->local > index) {
20✔
165
            --it->local;
20✔
166
            --it->remote;
20✔
167
        }
20✔
168
        ++it;
42✔
169
    }
20✔
170
    if (!found) {
284✔
171
        queue_for_manual_copy();
240✔
172
        return false;
240✔
173
    }
240✔
174
    return true;
44✔
175
}
44✔
176

177
bool ListTracker::requires_manual_copy() const
178
{
1,388✔
179
    return m_requires_manual_copy;
1,388✔
180
}
1,388✔
181

182
void ListTracker::queue_for_manual_copy()
183
{
864✔
184
    m_requires_manual_copy = true;
864✔
185
    m_indices_allowed.clear();
864✔
186
}
864✔
187

188
std::string_view InterningBuffer::get_key(const InternDictKey& key) const
189
{
4,000✔
190
    if (key.is_null()) {
4,000✔
191
        return {};
×
192
    }
×
193
    if (key.m_size == 0) {
4,000✔
194
        return "";
40✔
195
    }
40✔
196
    REALM_ASSERT(key.m_pos < m_dict_keys_buffer.size());
3,960✔
197
    REALM_ASSERT(key.m_pos + key.m_size <= m_dict_keys_buffer.size());
3,960✔
198
    return std::string_view{m_dict_keys_buffer.data() + key.m_pos, key.m_size};
3,960✔
199
}
3,960✔
200

201
InternDictKey InterningBuffer::get_or_add(const std::string_view& str)
202
{
1,984✔
203
    for (auto& key : m_dict_keys) {
3,992✔
204
        std::string_view existing = get_key(key);
3,992✔
205
        if (existing == str) {
3,992✔
206
            return key;
1,792✔
207
        }
1,792✔
208
    }
3,992✔
209
    InternDictKey new_key{};
1,088✔
210
    if (str.data() == nullptr) {
192✔
211
        m_dict_keys.push_back(new_key);
×
212
    }
×
213
    else {
192✔
214
        size_t next_pos = m_dict_keys_buffer.size();
192✔
215
        new_key.m_pos = next_pos;
192✔
216
        new_key.m_size = str.size();
192✔
217
        m_dict_keys_buffer.append(str);
192✔
218
        m_dict_keys.push_back(new_key);
192✔
219
    }
192✔
220
    return new_key;
192✔
221
}
1,984✔
222

223
InternDictKey InterningBuffer::get_interned_key(const std::string_view& str) const
224
{
×
225
    if (str.data() == nullptr) {
×
226
        return {};
×
227
    }
×
228
    for (auto& key : m_dict_keys) {
×
229
        StringData existing = get_key(key);
×
230
        if (existing == str) {
×
231
            return key;
×
232
        }
×
233
    }
×
234
    throw std::runtime_error(
×
235
        util::format("InterningBuffer::get_interned_key(%1) did not contain the requested key", str));
×
236
    return {};
×
237
}
×
238

239
std::string InterningBuffer::print() const
240
{
×
241
    return util::format("InterningBuffer of size=%1:'%2'", m_dict_keys.size(), m_dict_keys_buffer);
×
242
}
×
243

244
ListPath::Element::Element(size_t stable_ndx)
245
    : index(stable_ndx)
246
    , type(Type::ListIndex)
247
{
×
248
}
×
249

250
ListPath::Element::Element(const InternDictKey& str)
251
    : intern_key(str)
252
    , type(Type::InternKey)
253
{
1,984✔
254
}
1,984✔
255

256
ListPath::Element::Element(ColKey key)
257
    : col_key(key)
258
    , type(Type::ColumnKey)
259
{
27,048✔
260
}
27,048✔
261

262
bool ListPath::Element::operator==(const Element& other) const noexcept
263
{
7,720✔
264
    if (type == other.type) {
7,720✔
265
        switch (type) {
7,720✔
266
            case Type::InternKey:
344✔
267
                return intern_key == other.intern_key;
344✔
268
            case Type::ListIndex:
✔
269
                return index == other.index;
×
270
            case Type::ColumnKey:
7,376✔
271
                return col_key == other.col_key;
7,376✔
272
        }
×
273
    }
×
274
    return false;
×
275
}
×
276

277
bool ListPath::Element::operator!=(const Element& other) const noexcept
278
{
7,720✔
279
    return !(operator==(other));
7,720✔
280
}
7,720✔
281

282
bool ListPath::Element::operator<(const Element& other) const noexcept
283
{
19,988✔
284
    if (type < other.type) {
19,988✔
285
        return true;
×
286
    }
×
287
    if (type == other.type) {
19,988✔
288
        switch (type) {
19,988✔
289
            case Type::InternKey:
1,192✔
290
                return intern_key < other.intern_key;
1,192✔
291
            case Type::ListIndex:
✔
292
                return index < other.index;
×
293
            case Type::ColumnKey:
18,796✔
294
                return col_key < other.col_key;
18,796✔
295
        }
×
296
    }
×
297
    return false;
×
298
}
×
299

300
ListPath::ListPath(TableKey table_key, ObjKey obj_key)
301
    : m_table_key(table_key)
302
    , m_obj_key(obj_key)
303
{
47,544✔
304
}
47,544✔
305

306
void ListPath::append(const Element& item)
307
{
29,032✔
308
    m_path.push_back(item);
29,032✔
309
}
29,032✔
310

311
bool ListPath::operator<(const ListPath& other) const noexcept
312
{
10,664✔
313
    if (m_table_key < other.m_table_key || m_obj_key < other.m_obj_key || m_path.size() < other.m_path.size()) {
10,664✔
314
        return true;
180✔
315
    }
180✔
316
    return std::lexicographical_compare(m_path.begin(), m_path.end(), other.m_path.begin(), other.m_path.end());
10,484✔
317
}
10,484✔
318

319
bool ListPath::operator==(const ListPath& other) const noexcept
320
{
6,944✔
321
    if (m_table_key == other.m_table_key && m_obj_key == other.m_obj_key && m_path.size() == other.m_path.size()) {
6,944✔
322
        for (size_t i = 0; i < m_path.size(); ++i) {
14,664✔
323
            if (m_path[i] != other.m_path[i]) {
7,720✔
324
                return false;
×
325
            }
×
326
        }
7,720✔
327
        return true;
6,944✔
328
    }
×
329
    return false;
×
330
}
×
331

332
bool ListPath::operator!=(const ListPath& other) const noexcept
333
{
6,944✔
334
    return !(operator==(other));
6,944✔
335
}
6,944✔
336

337
std::string ListPath::path_to_string(Transaction& remote, const InterningBuffer& buffer)
338
{
864✔
339
    TableRef remote_table = remote.get_table(m_table_key);
864✔
340

432✔
341
    std::string path = util::format("%1", remote_table->get_name());
864✔
342
    if (Obj base_obj = remote_table->try_get_object(m_obj_key)) {
864✔
343
        path += util::format(".pk=%1", base_obj.get_primary_key());
856✔
344
    }
856✔
345
    else {
8✔
346
        path += util::format(".%1(removed)", m_obj_key);
8✔
347
    }
8✔
348
    for (auto& e : m_path) {
880✔
349
        switch (e.type) {
880✔
350
            case Element::Type::ColumnKey:
876✔
351
                path += util::format(".%1", remote_table->get_column_name(e.col_key));
876✔
352
                remote_table = remote_table->get_link_target(e.col_key);
876✔
353
                break;
876✔
354
            case Element::Type::ListIndex:
✔
355
                path += util::format("[%1]", e.index);
×
356
                break;
×
357
            case Element::Type::InternKey:
4✔
358
                path += util::format("[key='%1']", buffer.get_key(e.intern_key));
4✔
359
                break;
4✔
360
        }
880✔
361
    }
880✔
362
    return path;
864✔
363
}
864✔
364

365
RecoverLocalChangesetsHandler::RecoverLocalChangesetsHandler(Transaction& dest_wt,
366
                                                             Transaction& frozen_pre_local_state,
367
                                                             util::Logger& logger, Replication* repl)
368
    : InstructionApplier(dest_wt)
369
    , m_frozen_pre_local_state{frozen_pre_local_state}
370
    , m_logger{logger}
371
    , m_replication{repl}
372
{
3,384✔
373
}
3,384✔
374

375
RecoverLocalChangesetsHandler::~RecoverLocalChangesetsHandler() {}
3,384✔
376

377
REALM_NORETURN void RecoverLocalChangesetsHandler::handle_error(const std::string& message) const
378
{
12✔
379
    std::string full_message =
12✔
380
        util::format("Unable to automatically recover local changes during client reset: '%1'", message);
12✔
381
    m_logger.error(util::LogCategory::reset, full_message.c_str());
12✔
382
    throw realm::_impl::client_reset::ClientResetFailed(full_message);
12✔
383
}
12✔
384

385
void RecoverLocalChangesetsHandler::process_changesets(const std::vector<ClientHistory::LocalChange>& changesets,
386
                                                       std::vector<sync::SubscriptionSet>&& pending_subscriptions)
387
{
3,384✔
388
    // When recovering in PBS, we can iterate through all the changes and apply them in a single commit.
1,692✔
389
    // This has the nice property that any exception while applying will revert the entire recovery and leave
1,692✔
390
    // the Realm in a "pre reset" state.
1,692✔
391
    //
1,692✔
392
    // When recovering in FLX mode, we must apply subscription sets interleaved between the correct commits.
1,692✔
393
    // This handles the case where some objects were subscribed to for only one commit and then unsubscribed after.
1,692✔
394

1,692✔
395
    size_t subscription_index = 0;
3,384✔
396
    auto write_pending_subscriptions_up_to = [&](version_type version) {
10,148✔
397
        while (subscription_index < pending_subscriptions.size() &&
10,256✔
398
               pending_subscriptions[subscription_index].snapshot_version() <= version) {
5,234✔
399
            if (m_transaction.get_transact_stage() == DB::TransactStage::transact_Writing) {
108✔
400
                // List modifications may have happened on an object which we are only subscribed to
54✔
401
                // for this commit so we need to apply them as we go.
54✔
402
                copy_lists_with_unrecoverable_changes();
108✔
403
                m_transaction.commit_and_continue_as_read();
108✔
404
            }
108✔
405
            auto pre_sub = pending_subscriptions[subscription_index++];
108✔
406
            auto post_sub = pre_sub.make_mutable_copy().commit();
108✔
407
            m_logger.info(util::LogCategory::reset,
108✔
408
                          "Recovering pending subscription version: %1 -> %2, snapshot: %3 -> %4", pre_sub.version(),
108✔
409
                          post_sub.version(), pre_sub.snapshot_version(), post_sub.snapshot_version());
108✔
410
        }
108✔
411
        if (m_transaction.get_transact_stage() != DB::TransactStage::transact_Writing) {
10,148✔
412
            m_transaction.promote_to_write();
108✔
413
        }
108✔
414
    };
10,148✔
415

1,692✔
416
    for (const ClientHistory::LocalChange& change : changesets) {
10,408✔
417
        if (change.changeset.size() == 0)
10,408✔
418
            continue;
3,632✔
419

3,388✔
420
        ChunkedBinaryInputStream in{change.changeset};
6,776✔
421
        size_t decompressed_size;
6,776✔
422
        auto decompressed = util::compression::decompress_nonportable_input_stream(in, decompressed_size);
6,776✔
423
        if (!decompressed)
6,776✔
424
            continue;
×
425

3,388✔
426
        write_pending_subscriptions_up_to(change.version);
6,776✔
427

3,388✔
428
        sync::Changeset parsed_changeset;
6,776✔
429
        sync::parse_changeset(*decompressed, parsed_changeset); // Throws
6,776✔
430
#if REALM_DEBUG
6,776✔
431
        if (m_logger.would_log(util::LogCategory::reset, util::Logger::Level::trace)) {
6,776✔
432
            std::stringstream dumped_changeset;
×
433
            parsed_changeset.print(dumped_changeset);
×
NEW
434
            m_logger.trace(util::LogCategory::reset, "Recovering changeset: %1", dumped_changeset.str());
×
435
        }
×
436
#endif
6,776✔
437

3,388✔
438
        InstructionApplier::begin_apply(parsed_changeset);
6,776✔
439
        for (auto instr : parsed_changeset) {
27,720✔
440
            if (!instr)
27,720✔
441
                continue;
×
442
            instr->visit(*this); // Throws
27,720✔
443
        }
27,720✔
444
        InstructionApplier::end_apply();
6,776✔
445
    }
6,776✔
446

1,692✔
447
    // write any remaining subscriptions
1,692✔
448
    write_pending_subscriptions_up_to(std::numeric_limits<version_type>::max());
3,384✔
449
    REALM_ASSERT_EX(subscription_index == pending_subscriptions.size(), subscription_index);
3,384✔
450

1,692✔
451
    copy_lists_with_unrecoverable_changes();
3,384✔
452
}
3,384✔
453

454
void RecoverLocalChangesetsHandler::copy_lists_with_unrecoverable_changes()
455
{
3,480✔
456
    // Any modifications, moves or deletes to list elements which were not also created in the recovery
1,740✔
457
    // cannot be reliably applied because there is no way to know if the indices on the server have
1,740✔
458
    // shifted without a reliable server side history. For these lists, create a consistant state by
1,740✔
459
    // copying over the entire list from the recovering client's state. This does create a "last recovery wins"
1,740✔
460
    // scenario for modifications to lists, but this is only a best effort.
1,740✔
461
    // For example, consider a list [A,B].
1,740✔
462
    // Now the server has been reset, and applied an ArrayMove from a different client producing [B,A]
1,740✔
463
    // A client being reset tries to recover the instruction ArrayErase(index=0) intending to erase A.
1,740✔
464
    // But if this instruction were to be applied to the server's array, element B would be erased which is wrong.
1,740✔
465
    // So to prevent this, upon discovery of this type of instruction, replace the entire array to the client's
1,740✔
466
    // final state which would be [B].
1,740✔
467
    // IDEA: if a unique id were associated with each list element, we could recover lists correctly because
1,740✔
468
    // we would know where list elements ended up or if they were deleted by the server.
1,740✔
469
    using namespace realm::converters;
3,480✔
470
    EmbeddedObjectConverter embedded_object_tracker;
3,480✔
471
    for (auto& it : m_lists) {
2,434✔
472
        if (!it.second.requires_manual_copy())
1,388✔
473
            continue;
524✔
474

432✔
475
        std::string path_str = it.first.path_to_string(m_transaction, m_intern_keys);
864✔
476
        bool did_translate = resolve(it.first, [&](LstBase& remote_list, LstBase& local_list) {
860✔
477
            ConstTableRef local_table = local_list.get_table();
856✔
478
            ConstTableRef remote_table = remote_list.get_table();
856✔
479
            ColKey local_col_key = local_list.get_col_key();
856✔
480
            ColKey remote_col_key = remote_list.get_col_key();
856✔
481
            Obj local_obj = local_list.get_obj();
856✔
482
            Obj remote_obj = remote_list.get_obj();
856✔
483
            InterRealmValueConverter value_converter(local_table, local_col_key, remote_table, remote_col_key,
856✔
484
                                                     &embedded_object_tracker);
856✔
485
            m_logger.debug(util::LogCategory::reset, "Recovery overwrites list for '%1' size: %2 -> %3", path_str,
856✔
486
                           remote_list.size(), local_list.size());
856✔
487
            value_converter.copy_value(local_obj, remote_obj, nullptr);
856✔
488
            embedded_object_tracker.process_pending();
856✔
489
        });
856✔
490
        if (!did_translate) {
864✔
491
            // object no longer exists in the local state, ignore and continue
4✔
492
            m_logger.warn(util::LogCategory::reset,
8✔
493
                          "Discarding a list recovery made to an object which could not be resolved. "
8✔
494
                          "remote_path='%1'",
8✔
495
                          path_str);
8✔
496
        }
8✔
497
    }
864✔
498
    embedded_object_tracker.process_pending();
3,480✔
499
    m_lists.clear();
3,480✔
500
}
3,480✔
501

502
bool RecoverLocalChangesetsHandler::resolve_path(ListPath& path, Obj remote_obj, Obj local_obj,
503
                                                 util::UniqueFunction<void(LstBase&, LstBase&)> callback)
504
{
856✔
505
    for (auto it = path.begin(); it != path.end();) {
868✔
506
        if (!remote_obj || !local_obj) {
868✔
507
            return false;
×
508
        }
×
509
        REALM_ASSERT(it->type == ListPath::Element::Type::ColumnKey);
868✔
510
        ColKey col = it->col_key;
868✔
511
        REALM_ASSERT(col);
868✔
512
        if (col.is_list()) {
868✔
513
            auto remote_list = get_list_from_path(remote_obj, col);
856✔
514
            ColKey local_col = local_obj.get_table()->get_column_key(remote_obj.get_table()->get_column_name(col));
856✔
515
            REALM_ASSERT(local_col);
856✔
516
            auto local_list = get_list_from_path(local_obj, local_col);
856✔
517
            ++it;
856✔
518
            if (it == path.end()) {
856✔
519
                callback(*remote_list, *local_list);
856✔
520
                return true;
856✔
521
            }
856✔
522
            else {
×
523
                REALM_ASSERT(it->type == ListPath::Element::Type::ListIndex);
×
524
                REALM_ASSERT(it != path.end());
×
525
                size_t stable_index_id = it->index;
×
526
                REALM_ASSERT(stable_index_id != realm::npos);
×
527
                // This code path could be implemented, but because it is currently not possible to
528
                // excercise in tests, it is marked unreachable. The assumption here is that only the
529
                // first embedded object list would ever need to be copied over. If the first embedded
530
                // list is allowed then all sub objects are allowed, and likewise if the first embedded
531
                // list is copied, then this implies that all embedded children are also copied over.
532
                // Therefore, we should never have a situtation where a secondary embedded list needs copying.
533
                REALM_UNREACHABLE();
×
534
            }
×
535
        }
856✔
536
        else if (col.is_dictionary()) {
12✔
537
            ++it;
4✔
538
            REALM_ASSERT(it != path.end());
4✔
539
            REALM_ASSERT(it->type == ListPath::Element::Type::InternKey);
4✔
540
            Dictionary remote_dict = remote_obj.get_dictionary(col);
4✔
541
            Dictionary local_dict = local_obj.get_dictionary(remote_obj.get_table()->get_column_name(col));
4✔
542
            StringData dict_key = m_intern_keys.get_key(it->intern_key);
4✔
543
            if (remote_dict.contains(dict_key) && local_dict.contains(dict_key)) {
4✔
544
                remote_obj = remote_dict.get_object(dict_key);
4✔
545
                local_obj = local_dict.get_object(dict_key);
4✔
546
                ++it;
4✔
547
            }
4✔
548
            else {
×
549
                return false;
×
550
            }
×
551
        }
8✔
552
        else if (col.get_type() == col_type_Mixed) {
8✔
NEW
553
            StringData col_name = remote_obj.get_table()->get_column_name(col);
×
NEW
554
            auto local_any = local_obj.get_any(col_name);
×
NEW
555
            auto remote_any = remote_obj.get_any(col);
×
556

NEW
557
            if (local_any.is_type(type_List) && remote_any.is_type(type_List)) {
×
NEW
558
                ++it;
×
NEW
559
                if (it == path.end()) {
×
NEW
560
                    auto local_col = local_obj.get_table()->get_column_key(col_name);
×
NEW
561
                    Lst<Mixed> local_list{local_obj, local_col};
×
NEW
562
                    Lst<Mixed> remote_list{remote_obj, col};
×
NEW
563
                    callback(remote_list, local_list);
×
NEW
564
                    return true;
×
NEW
565
                }
×
NEW
566
                else {
×
567
                    // same as above.
NEW
568
                    REALM_UNREACHABLE();
×
NEW
569
                }
×
NEW
570
            }
×
NEW
571
            else if (local_any.is_type(type_Dictionary) && remote_any.is_type(type_Dictionary)) {
×
NEW
572
                ++it;
×
NEW
573
                REALM_ASSERT(it != path.end());
×
NEW
574
                REALM_ASSERT(it->type == ListPath::Element::Type::InternKey);
×
NEW
575
                StringData col_name = remote_obj.get_table()->get_column_name(col);
×
NEW
576
                auto local_col = local_obj.get_table()->get_column_key(col_name);
×
NEW
577
                Dictionary remote_dict{remote_obj, col};
×
NEW
578
                Dictionary local_dict{local_obj, local_col};
×
NEW
579
                StringData dict_key = m_intern_keys.get_key(it->intern_key);
×
NEW
580
                if (remote_dict.contains(dict_key) && local_dict.contains(dict_key)) {
×
NEW
581
                    remote_obj = remote_dict.get_object(dict_key);
×
NEW
582
                    local_obj = local_dict.get_object(dict_key);
×
NEW
583
                    ++it;
×
NEW
584
                }
×
NEW
585
                else {
×
NEW
586
                    return false;
×
NEW
587
                }
×
588
            }
8✔
589
        }
8✔
590
        else {
8✔
591
            // single link to embedded object
4✔
592
            // Neither embedded object sets nor Mixed(TypedLink) to embedded objects are supported.
4✔
593
            REALM_ASSERT_EX(!col.is_collection(), col);
8✔
594
            REALM_ASSERT_EX(col.get_type() == col_type_Link, col);
8✔
595
            StringData col_name = remote_obj.get_table()->get_column_name(col);
8✔
596
            remote_obj = remote_obj.get_linked_object(col);
8✔
597
            local_obj = local_obj.get_linked_object(col_name);
8✔
598
            ++it;
8✔
599
        }
8✔
600
    }
868✔
601
    return false;
428✔
602
}
856✔
603

604
bool RecoverLocalChangesetsHandler::resolve(ListPath& path, util::UniqueFunction<void(LstBase&, LstBase&)> callback)
605
{
864✔
606
    auto remote_table = m_transaction.get_table(path.table_key());
864✔
607
    if (!remote_table)
864✔
608
        return false;
×
609

432✔
610
    auto local_table = m_frozen_pre_local_state.get_table(remote_table->get_name());
864✔
611
    if (!local_table)
864✔
612
        return false;
×
613

432✔
614
    auto remote_obj = remote_table->try_get_object(path.obj_key());
864✔
615
    if (!remote_obj)
864✔
616
        return false;
8✔
617

428✔
618
    auto local_obj_key = local_table->find_primary_key(remote_obj.get_primary_key());
856✔
619
    if (!local_obj_key)
856✔
620
        return false;
×
621

428✔
622
    return resolve_path(path, remote_obj, local_table->get_object(local_obj_key), std::move(callback));
856✔
623
}
856✔
624

625
RecoverLocalChangesetsHandler::RecoveryResolver::RecoveryResolver(RecoverLocalChangesetsHandler* applier,
626
                                                                  Instruction::PathInstruction& instr,
627
                                                                  const std::string_view& instr_name)
628
    : InstructionApplier::PathResolver(applier, instr, instr_name)
629
    , m_list_path(TableKey{}, ObjKey{})
630
    , m_mutable_instr(instr)
631
    , m_recovery_applier(applier)
632
{
23,876✔
633
}
23,876✔
634

635
void RecoverLocalChangesetsHandler::RecoveryResolver::on_property(Obj&, ColKey)
636
{
×
637
    m_recovery_applier->handle_error(util::format("Invalid path for %1 (object, column)", m_instr_name));
×
638
}
×
639

640
void RecoverLocalChangesetsHandler::RecoveryResolver::on_list(LstBase&)
641
{
×
642
    m_recovery_applier->handle_error(util::format("Invalid path for %1 (list)", m_instr_name));
×
643
}
×
644

645
RecoverLocalChangesetsHandler::RecoveryResolver::Status
646
RecoverLocalChangesetsHandler::RecoveryResolver::on_list_index(LstBase&, uint32_t)
647
{
×
648
    m_recovery_applier->handle_error(util::format("Invalid path for %1 (list, index)", m_instr_name));
×
649
    return Status::DidNotResolve;
×
650
}
×
651

652
void RecoverLocalChangesetsHandler::RecoveryResolver::on_dictionary(Dictionary&)
653
{
×
654
    m_recovery_applier->handle_error(util::format("Invalid path for %1 (dictionary)", m_instr_name));
×
655
}
×
656

657
RecoverLocalChangesetsHandler::RecoveryResolver::Status
658
RecoverLocalChangesetsHandler::RecoveryResolver::on_dictionary_key(Dictionary&, Mixed)
659
{
×
660
    m_recovery_applier->handle_error(util::format("Invalid path for %1 (dictionary, key)", m_instr_name));
×
661
    return Status::DidNotResolve;
×
662
}
×
663

664
void RecoverLocalChangesetsHandler::RecoveryResolver::on_set(SetBase&)
665
{
×
666
    m_recovery_applier->handle_error(util::format("Invalid path for %1 (set)", m_instr_name));
×
667
}
×
668

669
void RecoverLocalChangesetsHandler::RecoveryResolver::on_error(const std::string& err_msg)
670
{
×
671
    m_recovery_applier->handle_error(err_msg);
×
672
}
×
673

674
void RecoverLocalChangesetsHandler::RecoveryResolver::on_column_advance(ColKey col)
675
{
27,048✔
676
    m_list_path.append(ListPath::Element(col));
27,048✔
677
}
27,048✔
678

679
void RecoverLocalChangesetsHandler::RecoveryResolver::on_dict_key_advance(StringData string_key)
680
{
1,984✔
681
    InternDictKey translated_key = m_recovery_applier->m_intern_keys.get_or_add(std::string_view(string_key));
1,984✔
682
    m_list_path.append(ListPath::Element(translated_key));
1,984✔
683
}
1,984✔
684

685
RecoverLocalChangesetsHandler::RecoveryResolver::Status
686
RecoverLocalChangesetsHandler::RecoveryResolver::on_list_index_advance(uint32_t index)
687
{
2,720✔
688
    if (m_recovery_applier->m_lists.count(m_list_path) != 0) {
2,720✔
689
        auto& list_tracker = m_recovery_applier->m_lists.at(m_list_path);
2,704✔
690
        auto cross_ndx = list_tracker.update(index);
2,704✔
691
        if (!cross_ndx) {
2,704✔
692
            return Status::DidNotResolve; // not allowed to modify this list item
136✔
693
        }
136✔
694
        REALM_ASSERT(cross_ndx->remote != uint32_t(-1));
2,568✔
695

1,284✔
696
        set_last_path_index(cross_ndx->remote); // translate the index of the path
2,568✔
697

1,284✔
698
        // At this point, the first part of an embedded object path has been allowed.
1,284✔
699
        // This implies that all parts of the rest of the path are also allowed so the index translation is
1,284✔
700
        // not necessary because instructions are operating on local only operations.
1,284✔
701
        return Status::Success;
2,568✔
702
    }
2,568✔
703
    // no record of this base list so far, track it for verbatim copy
8✔
704
    m_recovery_applier->m_lists.at(m_list_path).queue_for_manual_copy();
16✔
705
    return Status::DidNotResolve;
16✔
706
}
16✔
707

708
RecoverLocalChangesetsHandler::RecoveryResolver::Status
709
RecoverLocalChangesetsHandler::RecoveryResolver::on_null_link_advance(StringData table_name, StringData link_name)
710
{
212✔
711
    m_recovery_applier->m_logger.warn(
212✔
712
        util::LogCategory::reset,
212✔
713
        "Discarding a local %1 made to an embedded object which no longer exists along path '%2.%3'", m_instr_name,
212✔
714
        table_name, link_name);
212✔
715
    return Status::DidNotResolve; // discard this instruction as it operates over a null link
212✔
716
}
212✔
717

718
RecoverLocalChangesetsHandler::RecoveryResolver::Status
719
RecoverLocalChangesetsHandler::RecoveryResolver::on_begin(const util::Optional<Obj>& obj)
720
{
23,876✔
721
    if (!obj) {
23,876✔
722
        m_recovery_applier->m_logger.warn(util::LogCategory::reset,
208✔
723
                                          "Cannot recover '%1' which operates on a deleted object", m_instr_name);
208✔
724
        return Status::DidNotResolve;
208✔
725
    }
208✔
726
    m_list_path = ListPath(obj->get_table()->get_key(), obj->get_key());
23,668✔
727
    return Status::Pending;
23,668✔
728
}
23,668✔
729

730
void RecoverLocalChangesetsHandler::RecoveryResolver::on_finish() {}
×
731

732
ListPath& RecoverLocalChangesetsHandler::RecoveryResolver::list_path()
733
{
×
734
    return m_list_path;
×
735
}
×
736

737
void RecoverLocalChangesetsHandler::RecoveryResolver::set_last_path_index(uint32_t ndx)
738
{
2,568✔
739
    REALM_ASSERT(m_it_begin != m_path_instr.path.begin());
2,568✔
740
    size_t distance = (m_it_begin - m_path_instr.path.begin()) - 1;
2,568✔
741
    REALM_ASSERT_EX(distance < m_path_instr.path.size(), distance, m_path_instr.path.size());
2,568✔
742
    REALM_ASSERT(mpark::holds_alternative<uint32_t>(m_path_instr.path[distance]));
2,568✔
743
    m_mutable_instr.path[distance] = ndx;
2,568✔
744
}
2,568✔
745

746
RecoverLocalChangesetsHandler::RecoveryResolver::~RecoveryResolver() {}
23,876✔
747

748
void RecoverLocalChangesetsHandler::operator()(const Instruction::AddTable& instr)
749
{
56✔
750
    // Rely on InstructionApplier to validate existing tables
28✔
751
    StringData class_name = get_string(instr.table);
56✔
752
    try {
56✔
753
        auto table = table_for_class_name(class_name);
56✔
754
        InstructionApplier::operator()(instr);
56✔
755

28✔
756
        // if the table already existed then no instruction was
28✔
757
        // added to the history so we need to add one now
28✔
758
        if (m_replication && table) {
56✔
759
            if (table->is_embedded()) {
40✔
760
                m_replication->add_class(table->get_key(), table->get_name(), table->get_table_type());
×
761
            }
×
762
            else {
40✔
763
                ColKey pk_col = table->get_primary_key_column();
40✔
764
                REALM_ASSERT_EX(pk_col, class_name);
40✔
765
                m_replication->add_class_with_primary_key(table->get_key(), table->get_name(),
40✔
766
                                                          DataType(pk_col.get_type()), table->get_column_name(pk_col),
40✔
767
                                                          pk_col.is_nullable(), table->get_table_type());
40✔
768
            }
40✔
769
        }
40✔
770
    }
56✔
771
    catch (const std::runtime_error& err) {
28✔
772
        handle_error(util::format(
×
773
            "While recovering from a client reset, an AddTable instruction for '%1' could not be applied: '%2'",
×
774
            class_name, err.what()));
×
775
    }
×
776
}
56✔
777

778
void RecoverLocalChangesetsHandler::operator()(const Instruction::EraseTable& instr)
779
{
×
780
    // Destructive schema changes are not allowed by the resetting client.
781
    StringData class_name = get_string(instr.table);
×
782
    handle_error(util::format("Types cannot be erased during client reset recovery: '%1'", class_name));
×
783
}
×
784

785
void RecoverLocalChangesetsHandler::operator()(const Instruction::CreateObject& instr)
786
{
3,440✔
787
    // This should always succeed, and no path translation is needed because Create operates on top level objects.
1,720✔
788
    InstructionApplier::operator()(instr);
3,440✔
789
}
3,440✔
790

791
void RecoverLocalChangesetsHandler::operator()(const Instruction::EraseObject& instr)
792
{
124✔
793
    if (auto obj = get_top_object(instr, "EraseObject")) {
124✔
794
        // The InstructionApplier uses obj->invalidate() rather than remove(). It should have the same net
50✔
795
        // effect, but that is not the case. Notably when erasing an object which has links from a Lst<Mixed> the
50✔
796
        // list size does not decrease because there is no hiding the unresolved (null) element.
50✔
797
        // To avoid dangling links, just remove the object here rather than using the InstructionApplier.
50✔
798
        obj->remove();
100✔
799
    }
100✔
800
    // if the object doesn't exist, a local delete is a no-op.
62✔
801
}
124✔
802

803
void RecoverLocalChangesetsHandler::operator()(const Instruction::Update& instr)
804
{
18,836✔
805
    struct UpdateResolver : public RecoveryResolver {
18,836✔
806
        UpdateResolver(RecoverLocalChangesetsHandler* applier, Instruction::Update& instr,
18,836✔
807
                       const std::string_view& instr_name)
18,836✔
808
            : RecoveryResolver(applier, instr, instr_name)
18,836✔
809
            , m_instr(instr)
18,836✔
810
        {
18,836✔
811
        }
18,836✔
812
        Status on_dictionary_key(Dictionary& dict, Mixed key) override
18,836✔
813
        {
10,306✔
814
            if (m_instr.value.type == instr::Payload::Type::Erased && dict.find(key) == dict.end()) {
1,776✔
815
                // removing a dictionary value on a key that no longer exists is ignored
56✔
816
                return Status::DidNotResolve;
112✔
817
            }
112✔
818
            return Status::Pending;
1,664✔
819
        }
1,664✔
820
        Status on_list_index(LstBase& list, uint32_t index) override
18,836✔
821
        {
9,728✔
822
            util::Optional<ListTracker::CrossListIndex> cross_index;
620✔
823
            cross_index = m_recovery_applier->m_lists.at(m_list_path).update(index);
620✔
824
            if (cross_index) {
620✔
825
                m_instr.prior_size = static_cast<uint32_t>(list.size());
×
826
                m_instr.path.back() = cross_index->remote;
×
827
            }
×
828
            else {
620✔
829
                return Status::DidNotResolve;
620✔
830
            }
620✔
831
            return Status::Pending;
×
832
        }
×
833
        void on_property(Obj&, ColKey) override {}
16,786✔
834

9,418✔
835
    private:
18,836✔
836
        Instruction::Update& m_instr;
18,836✔
837
    };
18,836✔
838
    static constexpr std::string_view instr_name("Update");
18,836✔
839
    Instruction::Update instr_copy = instr;
18,836✔
840

9,418✔
841
    if (UpdateResolver(this, instr_copy, instr_name).resolve() == RecoveryResolver::Status::Success) {
18,836✔
842
        if (!check_links_exist(instr_copy.value)) {
17,796✔
843
            if (!allows_null_links(instr_copy, instr_name)) {
16✔
NEW
844
                m_logger.warn(util::LogCategory::reset, "Discarding an update which links to a deleted object");
×
845
                return;
×
846
            }
×
847
            instr_copy.value = {};
16✔
848
        }
16✔
849
        InstructionApplier::operator()(instr_copy);
17,796✔
850
    }
17,796✔
851
}
18,836✔
852

853
void RecoverLocalChangesetsHandler::operator()(const Instruction::AddInteger& instr)
854
{
16✔
855
    struct AddIntegerResolver : public RecoveryResolver {
16✔
856
        AddIntegerResolver(RecoverLocalChangesetsHandler* applier, Instruction::AddInteger& instr)
16✔
857
            : RecoveryResolver(applier, instr, "AddInteger")
16✔
858
        {
16✔
859
        }
16✔
860
        void on_property(Obj&, ColKey) override
16✔
861
        {
12✔
862
            // AddInteger only applies to a property
4✔
863
        }
8✔
864
    };
16✔
865
    Instruction::AddInteger instr_copy = instr;
16✔
866
    if (AddIntegerResolver(this, instr_copy).resolve() == RecoveryResolver::Status::Success) {
16✔
867
        InstructionApplier::operator()(instr_copy);
8✔
868
    }
8✔
869
}
16✔
870

871
void RecoverLocalChangesetsHandler::operator()(const Instruction::Clear& instr)
872
{
136✔
873
    struct ClearResolver : public RecoveryResolver {
136✔
874
        ClearResolver(RecoverLocalChangesetsHandler* applier, Instruction::Clear& instr)
136✔
875
            : RecoveryResolver(applier, instr, "Clear")
136✔
876
        {
136✔
877
        }
136✔
878
        void on_list(LstBase&) override
136✔
879
        {
86✔
880
            m_recovery_applier->m_lists.at(m_list_path).clear();
36✔
881
            // Clear.prior_size is ignored and always zero
18✔
882
        }
36✔
883
        void on_set(SetBase&) override {}
84✔
884
        void on_dictionary(Dictionary&) override {}
84✔
885
    };
136✔
886
    Instruction::Clear instr_copy = instr;
136✔
887
    if (ClearResolver(this, instr_copy).resolve() == RecoveryResolver::Status::Success) {
136✔
888
        InstructionApplier::operator()(instr_copy);
100✔
889
    }
100✔
890
}
136✔
891

892
void RecoverLocalChangesetsHandler::operator()(const Instruction::AddColumn& instr)
893
{
184✔
894
    // Rather than duplicating a bunch of validation, use the existing type checking
92✔
895
    // that happens when adding a preexisting column and if there is a problem catch
92✔
896
    // the BadChangesetError and stop recovery
92✔
897
    try {
184✔
898
        const TableRef table = get_table(instr, "AddColumn");
184✔
899
        auto col_name = get_string(instr.field);
184✔
900
        ColKey col_key = table->get_column_key(col_name);
184✔
901

92✔
902
        InstructionApplier::operator()(instr);
184✔
903

92✔
904
        // if the column already existed then no instruction was
92✔
905
        // added to the history so we need to add one now
92✔
906
        if (m_replication && col_key) {
184✔
907
            REALM_ASSERT(col_key);
96✔
908
            TableRef linked_table = table->get_opposite_table(col_key);
96✔
909
            DataType new_type = get_data_type(instr.type);
96✔
910
            m_replication->insert_column(table.unchecked_ptr(), col_key, new_type, col_name,
96✔
911
                                         linked_table.unchecked_ptr()); // Throws
96✔
912
        }
96✔
913
    }
184✔
914
    catch (const BadChangesetError& err) {
94✔
915
        handle_error(
4✔
916
            util::format("While recovering during client reset, an AddColumn instruction could not be applied: '%1'",
4✔
917
                         err.reason()));
4✔
918
    }
4✔
919
}
184✔
920

921
void RecoverLocalChangesetsHandler::operator()(const Instruction::EraseColumn& instr)
922
{
8✔
923
    // Destructive schema changes are not allowed by the resetting client.
4✔
924
    static_cast<void>(instr);
8✔
925
    handle_error(util::format("Properties cannot be erased during client reset recovery"));
8✔
926
}
8✔
927

928
void RecoverLocalChangesetsHandler::operator()(const Instruction::ArrayInsert& instr)
929
{
2,628✔
930
    struct ArrayInsertResolver : public RecoveryResolver {
2,628✔
931
        ArrayInsertResolver(RecoverLocalChangesetsHandler* applier, Instruction::ArrayInsert& instr,
2,628✔
932
                            const std::string_view& instr_name)
2,628✔
933
            : RecoveryResolver(applier, instr, instr_name)
2,628✔
934
            , m_instr(instr)
2,628✔
935
        {
2,620✔
936
        }
2,612✔
937
        Status on_list_index(LstBase& list, uint32_t index) override
2,628✔
938
        {
2,242✔
939
            REALM_ASSERT(index != uint32_t(-1));
1,856✔
940
            size_t list_size = list.size();
1,856✔
941
            auto cross_index = m_recovery_applier->m_lists.at(m_list_path).insert(index, list_size);
1,856✔
942
            if (cross_index) {
1,856✔
943
                m_instr.path.back() = cross_index->remote;
1,340✔
944
                m_instr.prior_size = static_cast<uint32_t>(list_size);
1,340✔
945
                return Status::Pending;
1,340✔
946
            }
1,340✔
947
            return Status::DidNotResolve;
516✔
948
        }
516✔
949

1,314✔
950
    private:
2,628✔
951
        Instruction::ArrayInsert& m_instr;
2,628✔
952
    };
2,628✔
953

1,314✔
954
    static constexpr std::string_view instr_name("ArrayInsert");
2,628✔
955
    if (!check_links_exist(instr.value)) {
2,628✔
956
        m_logger.warn(util::LogCategory::reset, "Discarding %1 which links to a deleted object", instr_name);
16✔
957
        return;
16✔
958
    }
16✔
959
    Instruction::ArrayInsert instr_copy = instr;
2,612✔
960
    if (ArrayInsertResolver(this, instr_copy, instr_name).resolve() == RecoveryResolver::Status::Success) {
2,612✔
961
        InstructionApplier::operator()(instr_copy);
2,012✔
962
    }
2,012✔
963
}
2,612✔
964

965
void RecoverLocalChangesetsHandler::operator()(const Instruction::ArrayMove& instr)
966
{
88✔
967
    struct ArrayMoveResolver : public RecoveryResolver {
88✔
968
        ArrayMoveResolver(RecoverLocalChangesetsHandler* applier, Instruction::ArrayMove& instr)
88✔
969
            : RecoveryResolver(applier, instr, "ArrayMove")
88✔
970
            , m_instr(instr)
88✔
971
        {
88✔
972
        }
88✔
973
        Status on_list_index(LstBase& list, uint32_t index) override
88✔
974
        {
86✔
975
            REALM_ASSERT(index != uint32_t(-1));
84✔
976
            size_t lst_size = list.size();
84✔
977
            uint32_t translated_from, translated_to;
84✔
978
            bool allowed_to_move =
84✔
979
                m_recovery_applier->m_lists.at(m_list_path)
84✔
980
                    .move(static_cast<uint32_t>(index), m_instr.ndx_2, lst_size, translated_from, translated_to);
84✔
981
            if (allowed_to_move) {
84✔
982
                m_instr.prior_size = static_cast<uint32_t>(lst_size);
64✔
983
                m_instr.path.back() = translated_from;
64✔
984
                m_instr.ndx_2 = translated_to;
64✔
985
                return Status::Pending;
64✔
986
            }
64✔
987
            return Status::DidNotResolve;
20✔
988
        }
20✔
989

44✔
990
    private:
88✔
991
        Instruction::ArrayMove& m_instr;
88✔
992
    };
88✔
993
    Instruction::ArrayMove instr_copy = instr;
88✔
994
    if (ArrayMoveResolver(this, instr_copy).resolve() == RecoveryResolver::Status::Success) {
88✔
995
        InstructionApplier::operator()(instr_copy);
68✔
996
    }
68✔
997
}
88✔
998

999
void RecoverLocalChangesetsHandler::operator()(const Instruction::ArrayErase& instr)
1000
{
336✔
1001
    struct ArrayEraseResolver : public RecoveryResolver {
336✔
1002
        ArrayEraseResolver(RecoverLocalChangesetsHandler* applier, Instruction::ArrayErase& instr)
336✔
1003
            : RecoveryResolver(applier, instr, "ArrayErase")
336✔
1004
            , m_instr(instr)
336✔
1005
        {
336✔
1006
        }
336✔
1007
        Status on_list_index(LstBase& list, uint32_t index) override
336✔
1008
        {
328✔
1009
            uint32_t translated_index;
320✔
1010
            bool allowed_to_delete =
320✔
1011
                m_recovery_applier->m_lists.at(m_list_path).remove(static_cast<uint32_t>(index), translated_index);
320✔
1012
            if (allowed_to_delete) {
320✔
1013
                m_instr.prior_size = static_cast<uint32_t>(list.size());
44✔
1014
                m_instr.path.back() = translated_index;
44✔
1015
                return Status::Pending;
44✔
1016
            }
44✔
1017
            return Status::DidNotResolve;
276✔
1018
        }
276✔
1019

168✔
1020
    private:
336✔
1021
        Instruction::ArrayErase& m_instr;
336✔
1022
    };
336✔
1023
    Instruction::ArrayErase instr_copy = instr;
336✔
1024
    if (ArrayEraseResolver(this, instr_copy).resolve() == RecoveryResolver::Status::Success) {
336✔
1025
        InstructionApplier::operator()(instr_copy);
44✔
1026
    }
44✔
1027
}
336✔
1028

1029
void RecoverLocalChangesetsHandler::operator()(const Instruction::SetInsert& instr)
1030
{
1,448✔
1031
    struct SetInsertResolver : public RecoveryResolver {
1,448✔
1032
        SetInsertResolver(RecoverLocalChangesetsHandler* applier, Instruction::SetInsert& instr,
1,448✔
1033
                          const std::string_view& instr_name)
1,448✔
1034
            : RecoveryResolver(applier, instr, instr_name)
1,448✔
1035
        {
1,440✔
1036
        }
1,432✔
1037
        void on_set(SetBase&) {}
1,152✔
1038
    };
1,448✔
1039
    static constexpr std::string_view instr_name("SetInsert");
1,448✔
1040
    if (!check_links_exist(instr.value)) {
1,448✔
1041
        m_logger.warn(util::LogCategory::reset, "Discarding a %1 which links to a deleted object", instr_name);
16✔
1042
        return;
16✔
1043
    }
16✔
1044
    Instruction::SetInsert instr_copy = instr;
1,432✔
1045
    if (SetInsertResolver(this, instr_copy, instr_name).resolve() == RecoveryResolver::Status::Success) {
1,432✔
1046
        InstructionApplier::operator()(instr_copy);
1,344✔
1047
    }
1,344✔
1048
}
1,432✔
1049

1050
void RecoverLocalChangesetsHandler::operator()(const Instruction::SetErase& instr)
1051
{
420✔
1052
    struct SetEraseResolver : public RecoveryResolver {
420✔
1053
        SetEraseResolver(RecoverLocalChangesetsHandler* applier, Instruction::SetErase& instr)
420✔
1054
            : RecoveryResolver(applier, instr, "SetErase")
420✔
1055
        {
420✔
1056
        }
420✔
1057
        void on_set(SetBase&) override {}
400✔
1058
    };
420✔
1059
    Instruction::SetErase instr_copy = instr;
420✔
1060
    if (SetEraseResolver(this, instr_copy).resolve() == RecoveryResolver::Status::Success) {
420✔
1061
        InstructionApplier::operator()(instr_copy);
388✔
1062
    }
388✔
1063
}
420✔
1064

1065
} // namespace realm::_impl::client_reset
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