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

realm / realm-core / 1664

13 Sep 2023 01:20PM UTC coverage: 91.218% (-0.003%) from 91.221%
1664

push

Evergreen

GitHub
More detailed logging (#6971)

95892 of 175862 branches covered (0.0%)

55 of 71 new or added lines in 6 files covered. (77.46%)

93 existing lines in 14 files now uncovered.

233636 of 256129 relevant lines covered (91.22%)

7415075.7 hits per line

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

91.12
/src/realm/transaction.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/transaction.hpp>
20
#include "impl/copy_replication.hpp"
21
#include <realm/list.hpp>
22
#include <realm/set.hpp>
23
#include <realm/dictionary.hpp>
24
#include <realm/table_view.hpp>
25
#include <realm/group_writer.hpp>
26

27
namespace {
28

29
using namespace realm;
30
using ColInfo = std::vector<std::pair<ColKey, Table*>>;
31

32
ColInfo get_col_info(const Table* table)
33
{
2,010✔
34
    std::vector<std::pair<ColKey, Table*>> cols;
2,010✔
35
    if (table) {
2,010✔
36
        for (auto col : table->get_column_keys()) {
654✔
37
            Table* embedded_table = nullptr;
654✔
38
            if (auto target_table = table->get_opposite_table(col)) {
654✔
39
                if (target_table->is_embedded())
216✔
40
                    embedded_table = target_table.unchecked_ptr();
168✔
41
            }
216✔
42
            cols.emplace_back(col, embedded_table);
654✔
43
        }
654✔
44
    }
282✔
45
    return cols;
2,010✔
46
}
2,010✔
47

48
void generate_properties_for_obj(Replication& repl, const Obj& obj, const ColInfo& cols)
49
{
846✔
50
    for (auto elem : cols) {
1,884✔
51
        auto col = elem.first;
1,884✔
52
        auto embedded_table = elem.second;
1,884✔
53
        auto cols_2 = get_col_info(embedded_table);
1,884✔
54
        auto update_embedded = [&](Mixed val) {
999✔
55
            if (val.is_null()) {
114✔
56
                return;
18✔
57
            }
18✔
58
            REALM_ASSERT(val.is_type(type_Link, type_TypedLink));
96✔
59
            Obj embedded_obj = embedded_table->get_object(val.get<ObjKey>());
96✔
60
            generate_properties_for_obj(repl, embedded_obj, cols_2);
96✔
61
        };
96✔
62

942✔
63
        if (col.is_list()) {
1,884✔
64
            auto list = obj.get_listbase_ptr(col);
96✔
65
            auto sz = list->size();
96✔
66
            repl.list_clear(*list);
96✔
67
            for (size_t n = 0; n < sz; n++) {
180✔
68
                auto val = list->get_any(n);
84✔
69
                repl.list_insert(*list, n, val, n);
84✔
70
                if (embedded_table) {
84✔
71
                    update_embedded(val);
18✔
72
                }
18✔
73
            }
84✔
74
        }
96✔
75
        else if (col.is_set()) {
1,788✔
76
            auto set = obj.get_setbase_ptr(col);
18✔
77
            auto sz = set->size();
18✔
78
            for (size_t n = 0; n < sz; n++) {
54✔
79
                repl.set_insert(*set, n, set->get_any(n));
36✔
80
                // Sets cannot have embedded objects
18✔
81
            }
36✔
82
        }
18✔
83
        else if (col.is_dictionary()) {
1,770✔
84
            auto dict = obj.get_dictionary(col);
96✔
85
            size_t n = 0;
96✔
86
            for (auto [key, value] : dict) {
87✔
87
                repl.dictionary_insert(dict, n++, key, value);
78✔
88
                if (embedded_table) {
78✔
89
                    update_embedded(value);
42✔
90
                }
42✔
91
            }
78✔
92
        }
96✔
93
        else {
1,674✔
94
            auto val = obj.get_any(col);
1,674✔
95
            repl.set(obj.get_table().unchecked_ptr(), col, obj.get_key(), val);
1,674✔
96
            if (embedded_table) {
1,674✔
97
                update_embedded(val);
54✔
98
            }
54✔
99
        }
1,674✔
100
    }
1,884✔
101
}
846✔
102

103
} // namespace
104

105
namespace realm {
106

107
std::map<DB::TransactStage, const char*> log_stage = {
108
    {DB::TransactStage::transact_Frozen, "frozen"},
109
    {DB::TransactStage::transact_Writing, "write"},
110
    {DB::TransactStage::transact_Reading, "read"},
111
};
112

113
Transaction::Transaction(DBRef _db, SlabAlloc* alloc, DB::ReadLockInfo& rli, DB::TransactStage stage)
114
    : Group(alloc)
115
    , db(_db)
116
    , m_read_lock(rli)
117
    , m_log_id(util::gen_log_id(this))
118
{
2,830,464✔
119
    bool writable = stage == DB::transact_Writing;
2,830,464✔
120
    m_transact_stage = DB::transact_Ready;
2,830,464✔
121
    set_metrics(db->m_metrics);
2,830,464✔
122
    set_transact_stage(stage);
2,830,464✔
123
    m_alloc.note_reader_start(this);
2,830,464✔
124
    attach_shared(m_read_lock.m_top_ref, m_read_lock.m_file_size, writable,
2,830,464✔
125
                  VersionID{rli.m_version, rli.m_reader_idx});
2,830,464✔
126
    if (db->m_logger) {
2,830,464✔
127
        db->m_logger->log(util::Logger::Level::trace, "Start %1 %2: %3 ref %4", log_stage[stage], m_log_id,
851,724✔
128
                          rli.m_version, m_read_lock.m_top_ref);
851,724✔
129
    }
851,724✔
130
}
2,830,464✔
131

132
Transaction::~Transaction()
133
{
2,831,271✔
134
    // Note that this does not call close() - calling close() is done
1,754,598✔
135
    // implicitly by the deleter.
1,754,598✔
136
}
2,831,271✔
137

138
void Transaction::close()
139
{
2,837,721✔
140
    if (m_transact_stage == DB::transact_Writing) {
2,837,721✔
141
        rollback();
9,900✔
142
    }
9,900✔
143
    if (m_transact_stage == DB::transact_Reading || m_transact_stage == DB::transact_Frozen) {
2,837,721✔
144
        do_end_read();
1,739,310✔
145
    }
1,739,310✔
146
}
2,837,721✔
147

148
size_t Transaction::get_commit_size() const
149
{
36,309✔
150
    size_t sz = 0;
36,309✔
151
    if (m_transact_stage == DB::transact_Writing) {
36,309✔
152
        sz = m_alloc.get_commit_size();
6,729✔
153
    }
6,729✔
154
    return sz;
36,309✔
155
}
36,309✔
156

157
DB::version_type Transaction::commit()
158
{
973,686✔
159
    check_attached();
973,686✔
160

491,304✔
161
    if (m_transact_stage != DB::transact_Writing)
973,686✔
162
        throw WrongTransactionState("Not a write transaction");
×
163

491,304✔
164
    REALM_ASSERT(is_attached());
973,686✔
165

491,304✔
166
    // before committing, allow any accessors at group level or below to sync
491,304✔
167
    flush_accessors_for_commit();
973,686✔
168

491,304✔
169
    DB::version_type new_version = db->do_commit(*this); // Throws
973,686✔
170

491,304✔
171
    // We need to set m_read_lock in order for wait_for_change to work.
491,304✔
172
    // To set it, we grab a readlock on the latest available snapshot
491,304✔
173
    // and release it again.
491,304✔
174
    DB::ReadLockInfo lock_after_commit = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID());
973,686✔
175
    db->release_read_lock(lock_after_commit);
973,686✔
176

491,304✔
177
    db->end_write_on_correct_thread();
973,686✔
178

491,304✔
179
    do_end_read();
973,686✔
180
    m_read_lock = lock_after_commit;
973,686✔
181

491,304✔
182
    return new_version;
973,686✔
183
}
973,686✔
184

185
void Transaction::rollback()
186
{
10,818✔
187
    // rollback may happen as a consequence of exception handling in cases where
5,421✔
188
    // the DB has detached. If so, just back out without trying to change state.
5,421✔
189
    // the DB object has already been closed and no further processing is possible.
5,421✔
190
    if (!is_attached())
10,818✔
191
        return;
6✔
192
    if (m_transact_stage == DB::transact_Ready)
10,812✔
193
        return; // Idempotency
×
194

5,418✔
195
    if (m_transact_stage != DB::transact_Writing)
10,812✔
196
        throw WrongTransactionState("Not a write transaction");
×
197
    db->reset_free_space_tracking();
10,812✔
198
    if (!holds_write_mutex())
10,812✔
199
        db->end_write_on_correct_thread();
10,812✔
200

5,418✔
201
    do_end_read();
10,812✔
202
}
10,812✔
203

204
void Transaction::end_read()
205
{
98,040✔
206
    if (m_transact_stage == DB::transact_Ready)
98,040✔
207
        return;
6✔
208
    if (m_transact_stage == DB::transact_Writing)
98,034✔
209
        throw WrongTransactionState("Illegal end_read when in write mode");
×
210
    do_end_read();
98,034✔
211
}
98,034✔
212

213
VersionID Transaction::commit_and_continue_as_read(bool commit_to_disk)
214
{
387,045✔
215
    check_attached();
387,045✔
216
    if (m_transact_stage != DB::transact_Writing)
387,045✔
217
        throw WrongTransactionState("Not a write transaction");
×
218

191,628✔
219
    flush_accessors_for_commit();
387,045✔
220

191,628✔
221
    DB::version_type version = db->do_commit(*this, commit_to_disk); // Throws
387,045✔
222

191,628✔
223
    // advance read lock but dont update accessors:
191,628✔
224
    // As this is done under lock, along with the addition above of the newest commit,
191,628✔
225
    // we know for certain that the read lock we will grab WILL refer to our own newly
191,628✔
226
    // completed commit.
191,628✔
227

191,628✔
228
    try {
387,045✔
229
        // Grabbing the new lock before releasing the old one prevents m_transaction_count
191,628✔
230
        // from going shortly to zero
191,628✔
231
        DB::ReadLockInfo new_read_lock = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID()); // Throws
387,045✔
232

191,628✔
233
        m_history = nullptr;
387,045✔
234
        set_transact_stage(DB::transact_Reading);
387,045✔
235

191,628✔
236
        if (commit_to_disk || m_oldest_version_not_persisted) {
387,045✔
237
            // Here we are either committing to disk or we are already
191,328✔
238
            // holding on to an older version. In either case there is
191,328✔
239
            // no need to hold onto this now historic version.
191,328✔
240
            db->release_read_lock(m_read_lock);
385,701✔
241
        }
385,701✔
242
        else {
1,344✔
243
            // We are not commiting to disk and there is no older
300✔
244
            // version not persisted, so hold onto this one
300✔
245
            m_oldest_version_not_persisted = m_read_lock;
1,344✔
246
        }
1,344✔
247

191,628✔
248
        if (commit_to_disk && m_oldest_version_not_persisted) {
387,045✔
249
            // We are committing to disk so we can release the
6✔
250
            // version we are holding on to
6✔
251
            db->release_read_lock(*m_oldest_version_not_persisted);
12✔
252
            m_oldest_version_not_persisted.reset();
12✔
253
        }
12✔
254
        m_read_lock = new_read_lock;
387,045✔
255
        // We can be sure that m_read_lock != m_oldest_version_not_persisted
191,628✔
256
        // because m_oldest_version_not_persisted is either equal to former m_read_lock
191,628✔
257
        // or older and former m_read_lock is older than current m_read_lock
191,628✔
258
        REALM_ASSERT(!m_oldest_version_not_persisted ||
387,045✔
259
                     m_read_lock.m_version != m_oldest_version_not_persisted->m_version);
387,045✔
260

191,628✔
261
        {
387,045✔
262
            util::CheckedLockGuard lock(m_async_mutex);
387,045✔
263
            REALM_ASSERT(m_async_stage != AsyncState::Syncing);
387,045✔
264
            if (commit_to_disk) {
387,045✔
265
                if (m_async_stage == AsyncState::Requesting) {
379,560✔
266
                    m_async_stage = AsyncState::HasLock;
×
267
                }
×
268
                else {
379,560✔
269
                    db->end_write_on_correct_thread();
379,560✔
270
                    m_async_stage = AsyncState::Idle;
379,560✔
271
                }
379,560✔
272
            }
379,560✔
273
            else {
7,485✔
274
                m_async_stage = AsyncState::HasCommits;
7,485✔
275
            }
7,485✔
276
        }
387,045✔
277

191,628✔
278
        // Remap file if it has grown, and update refs in underlying node structure.
191,628✔
279
        remap_and_update_refs(m_read_lock.m_top_ref, m_read_lock.m_file_size, false); // Throws
387,045✔
280
        return VersionID{version, new_read_lock.m_reader_idx};
387,045✔
281
    }
387,045✔
NEW
282
    catch (std::exception& e) {
×
NEW
283
        if (db->m_logger) {
×
NEW
284
            db->m_logger->log(util::Logger::Level::error, "Tr %1: Commit failed with exception: \"%2\"", m_log_id,
×
NEW
285
                              e.what());
×
NEW
286
        }
×
287
        // In case of failure, further use of the transaction for reading is unsafe
288
        set_transact_stage(DB::transact_Ready);
×
289
        throw;
×
290
    }
×
291
}
387,045✔
292

293
VersionID Transaction::commit_and_continue_writing()
294
{
798✔
295
    check_attached();
798✔
296
    if (m_transact_stage != DB::transact_Writing)
798✔
297
        throw WrongTransactionState("Not a write transaction");
×
298

399✔
299
    // before committing, allow any accessors at group level or below to sync
399✔
300
    flush_accessors_for_commit();
798✔
301

399✔
302
    DB::version_type version = db->do_commit(*this); // Throws
798✔
303

399✔
304
    // We need to set m_read_lock in order for wait_for_change to work.
399✔
305
    // To set it, we grab a readlock on the latest available snapshot
399✔
306
    // and release it again.
399✔
307
    DB::ReadLockInfo lock_after_commit = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID());
798✔
308
    db->release_read_lock(m_read_lock);
798✔
309
    m_read_lock = lock_after_commit;
798✔
310
    if (Replication* repl = db->get_replication()) {
798✔
311
        bool history_updated = false;
342✔
312
        repl->initiate_transact(*this, lock_after_commit.m_version, history_updated); // Throws
342✔
313
    }
342✔
314

399✔
315
    bool writable = true;
798✔
316
    remap_and_update_refs(m_read_lock.m_top_ref, m_read_lock.m_file_size, writable); // Throws
798✔
317
    return VersionID{version, lock_after_commit.m_reader_idx};
798✔
318
}
798✔
319

320
TransactionRef Transaction::freeze()
321
{
6,042✔
322
    if (m_transact_stage != DB::transact_Reading)
6,042✔
323
        throw WrongTransactionState("Can only freeze a read transaction");
6✔
324
    auto version = VersionID(m_read_lock.m_version, m_read_lock.m_reader_idx);
6,036✔
325
    return db->start_frozen(version);
6,036✔
326
}
6,036✔
327

328
TransactionRef Transaction::duplicate()
329
{
48,345✔
330
    auto version = VersionID(m_read_lock.m_version, m_read_lock.m_reader_idx);
48,345✔
331
    switch (m_transact_stage) {
48,345✔
332
        case DB::transact_Ready:
✔
333
            throw WrongTransactionState("Cannot duplicate a transaction which does not have a read lock.");
×
334
        case DB::transact_Reading:
48,333✔
335
            return db->start_read(version);
48,333✔
336
        case DB::transact_Frozen:
✔
337
            return db->start_frozen(version);
×
338
        case DB::transact_Writing:
12✔
339
            if (get_commit_size() != 0)
12✔
340
                throw WrongTransactionState(
×
341
                    "Can only duplicate a write transaction before any changes have been made.");
×
342
            return db->start_read(version);
12✔
343
    }
×
344
    REALM_UNREACHABLE();
×
345
}
×
346

347
void Transaction::copy_to(TransactionRef dest) const
348
{
42✔
349
    _impl::CopyReplication repl(dest);
42✔
350
    replicate(dest.get(), repl);
42✔
351
}
42✔
352

353
_impl::History* Transaction::get_history() const
354
{
2,800,164✔
355
    if (!m_history) {
2,800,164✔
356
        if (auto repl = db->get_replication()) {
1,134,717✔
357
            switch (m_transact_stage) {
1,116,789✔
358
                case DB::transact_Reading:
144,624✔
359
                case DB::transact_Frozen:
144,624✔
360
                    if (!m_history_read)
144,624✔
361
                        m_history_read = repl->_create_history_read();
142,050✔
362
                    m_history = m_history_read.get();
144,624✔
363
                    m_history->set_group(const_cast<Transaction*>(this), false);
144,624✔
364
                    break;
144,624✔
365
                case DB::transact_Writing:
972,165✔
366
                    m_history = repl->_get_history_write();
972,165✔
367
                    break;
972,165✔
368
                case DB::transact_Ready:
71,460✔
369
                    break;
×
370
            }
2,800,167✔
371
        }
2,800,167✔
372
    }
1,134,717✔
373
    return m_history;
2,800,167✔
374
}
2,800,167✔
375

376
Obj Transaction::import_copy_of(const Obj& original)
377
{
18,828✔
378
    if (bool(original) && original.is_valid()) {
18,828✔
379
        TableKey tk = original.get_table_key();
18,798✔
380
        ObjKey rk = original.get_key();
18,798✔
381
        auto table = get_table(tk);
18,798✔
382
        if (table->is_valid(rk))
18,798✔
383
            return table->get_object(rk);
18,240✔
384
    }
588✔
385
    return {};
588✔
386
}
588✔
387

388
TableRef Transaction::import_copy_of(ConstTableRef original)
389
{
224,928✔
390
    TableKey tk = original->get_key();
224,928✔
391
    return get_table(tk);
224,928✔
392
}
224,928✔
393

394
LnkLst Transaction::import_copy_of(const LnkLst& original)
395
{
×
396
    if (Obj obj = import_copy_of(original.get_obj())) {
×
397
        ColKey ck = original.get_col_key();
×
398
        return obj.get_linklist(ck);
×
399
    }
×
400
    return LnkLst();
×
401
}
×
402

403
LstBasePtr Transaction::import_copy_of(const LstBase& original)
404
{
12✔
405
    if (Obj obj = import_copy_of(original.get_obj())) {
12✔
406
        ColKey ck = original.get_col_key();
6✔
407
        return obj.get_listbase_ptr(ck);
6✔
408
    }
6✔
409
    return {};
6✔
410
}
6✔
411

412
SetBasePtr Transaction::import_copy_of(const SetBase& original)
413
{
×
414
    if (Obj obj = import_copy_of(original.get_obj())) {
×
415
        ColKey ck = original.get_col_key();
×
416
        return obj.get_setbase_ptr(ck);
×
417
    }
×
418
    return {};
×
419
}
×
420

421
CollectionBasePtr Transaction::import_copy_of(const CollectionBase& original)
422
{
11,430✔
423
    if (Obj obj = import_copy_of(original.get_obj())) {
11,430✔
424
        ColKey ck = original.get_col_key();
10,902✔
425
        return obj.get_collection_ptr(ck);
10,902✔
426
    }
10,902✔
427
    return {};
528✔
428
}
528✔
429

430
LnkLstPtr Transaction::import_copy_of(const LnkLstPtr& original)
431
{
6✔
432
    if (!bool(original))
6✔
433
        return nullptr;
×
434
    if (Obj obj = import_copy_of(original->get_obj())) {
6✔
435
        ColKey ck = original->get_col_key();
6✔
436
        return obj.get_linklist_ptr(ck);
6✔
437
    }
6✔
438
    return std::make_unique<LnkLst>();
×
439
}
×
440

441
LnkSetPtr Transaction::import_copy_of(const LnkSetPtr& original)
442
{
×
443
    if (!original)
×
444
        return nullptr;
×
445
    if (Obj obj = import_copy_of(original->get_obj())) {
×
446
        ColKey ck = original->get_col_key();
×
447
        return obj.get_linkset_ptr(ck);
×
448
    }
×
449
    return std::make_unique<LnkSet>();
×
450
}
×
451

452
LinkCollectionPtr Transaction::import_copy_of(const LinkCollectionPtr& original)
453
{
56,031✔
454
    if (!original)
56,031✔
455
        return nullptr;
55,335✔
456
    if (Obj obj = import_copy_of(original->get_owning_obj())) {
696✔
457
        ColKey ck = original->get_owning_col_key();
654✔
458
        return obj.get_linkcollection_ptr(ck);
654✔
459
    }
654✔
460
    // return some empty collection where size() == 0
21✔
461
    // the type shouldn't matter
21✔
462
    return std::make_unique<LnkLst>();
42✔
463
}
42✔
464

465
std::unique_ptr<Query> Transaction::import_copy_of(Query& query, PayloadPolicy policy)
466
{
59,472✔
467
    return query.clone_for_handover(this, policy);
59,472✔
468
}
59,472✔
469

470
std::unique_ptr<TableView> Transaction::import_copy_of(TableView& tv, PayloadPolicy policy)
471
{
37,371✔
472
    return tv.clone_for_handover(this, policy);
37,371✔
473
}
37,371✔
474

475
void Transaction::upgrade_file_format(int target_file_format_version)
476
{
234✔
477
    REALM_ASSERT(is_attached());
234✔
478
    if (fake_target_file_format && *fake_target_file_format == target_file_format_version) {
234✔
479
        // Testing, mockup scenario, not a real upgrade. Just pretend we're done!
15✔
480
        return;
30✔
481
    }
30✔
482

102✔
483
    // Be sure to revisit the following upgrade logic when a new file format
102✔
484
    // version is introduced. The following assert attempt to help you not
102✔
485
    // forget it.
102✔
486
    REALM_ASSERT_EX(target_file_format_version == 23, target_file_format_version);
204✔
487

102✔
488
    // DB::do_open() must ensure that only supported version are allowed.
102✔
489
    // It does that by asking backup if the current file format version is
102✔
490
    // included in the accepted versions, so be sure to align the list of
102✔
491
    // versions with the logic below
102✔
492

102✔
493
    int current_file_format_version = get_file_format_version();
204✔
494
    REALM_ASSERT(current_file_format_version < target_file_format_version);
204✔
495

102✔
496
    // Upgrade from version prior to 7 (new history schema version in top array)
102✔
497
    if (current_file_format_version <= 6 && target_file_format_version >= 7) {
204✔
498
        // If top array size is 9, then add the missing 10th element containing
15✔
499
        // the history schema version.
15✔
500
        std::size_t top_size = m_top.size();
30✔
501
        REALM_ASSERT(top_size <= 9);
30✔
502
        if (top_size == 9) {
30✔
503
            int initial_history_schema_version = 0;
18✔
504
            m_top.add(initial_history_schema_version); // Throws
18✔
505
        }
18✔
506
        set_file_format_version(7);
30✔
507
        commit_and_continue_writing();
30✔
508
    }
30✔
509

102✔
510
    // Upgrade from version prior to 10 (Cluster based db)
102✔
511
    if (current_file_format_version <= 9 && target_file_format_version >= 10) {
204✔
512
        DisableReplication disable_replication(*this);
102✔
513

51✔
514
        std::vector<TableRef> table_accessors;
102✔
515
        TableRef pk_table;
102✔
516
        TableRef progress_info;
102✔
517
        ColKey col_objects;
102✔
518
        ColKey col_links;
102✔
519
        std::map<TableRef, ColKey> pk_cols;
102✔
520

51✔
521
        // Use table lookup by name. The table keys are not generated yet
51✔
522
        for (size_t t = 0; t < m_table_names.size(); t++) {
438✔
523
            StringData name = m_table_names.get(t);
336✔
524
            // In file format version 9 files, all names represent existing tables.
168✔
525
            auto table = get_table(name);
336✔
526
            if (name == "pk") {
336✔
527
                pk_table = table;
54✔
528
            }
54✔
529
            else if (name == "!UPDATE_PROGRESS") {
282✔
530
                progress_info = table;
×
531
            }
×
532
            else {
282✔
533
                table_accessors.push_back(table);
282✔
534
            }
282✔
535
        }
336✔
536

51✔
537
        if (!progress_info) {
102✔
538
            // This is the first time. Prepare for moving objects in one go.
51✔
539
            progress_info = this->add_table_with_primary_key("!UPDATE_PROGRESS", type_String, "table_name");
102✔
540
            col_objects = progress_info->add_column(type_Bool, "objects_migrated");
102✔
541
            col_links = progress_info->add_column(type_Bool, "links_migrated");
102✔
542

51✔
543

51✔
544
            for (auto k : table_accessors) {
282✔
545
                k->migrate_column_info();
282✔
546
            }
282✔
547

51✔
548
            if (pk_table) {
102✔
549
                pk_table->migrate_column_info();
54✔
550
                pk_table->migrate_indexes(ColKey());
54✔
551
                pk_table->create_columns();
54✔
552
                pk_table->migrate_objects();
54✔
553
                pk_cols = get_primary_key_columns_from_pk_table(pk_table);
54✔
554
            }
54✔
555

51✔
556
            for (auto k : table_accessors) {
282✔
557
                k->migrate_indexes(pk_cols[k]);
282✔
558
            }
282✔
559
            for (auto k : table_accessors) {
282✔
560
                k->migrate_subspec();
282✔
561
            }
282✔
562
            for (auto k : table_accessors) {
282✔
563
                k->create_columns();
282✔
564
            }
282✔
565
            commit_and_continue_writing();
102✔
566
        }
102✔
567
        else {
×
568
            if (pk_table) {
×
569
                pk_cols = get_primary_key_columns_from_pk_table(pk_table);
×
570
            }
×
571
            col_objects = progress_info->get_column_key("objects_migrated");
×
572
            col_links = progress_info->get_column_key("links_migrated");
×
573
        }
×
574

51✔
575
        bool updates = false;
102✔
576
        for (auto k : table_accessors) {
282✔
577
            if (k->verify_column_keys()) {
282✔
578
                updates = true;
6✔
579
            }
6✔
580
        }
282✔
581
        if (updates) {
102✔
582
            commit_and_continue_writing();
6✔
583
        }
6✔
584

51✔
585
        // Migrate objects
51✔
586
        for (auto k : table_accessors) {
282✔
587
            auto progress_status = progress_info->create_object_with_primary_key(k->get_name());
282✔
588
            if (!progress_status.get<bool>(col_objects)) {
282✔
589
                bool no_links = k->migrate_objects();
282✔
590
                progress_status.set(col_objects, true);
282✔
591
                progress_status.set(col_links, no_links);
282✔
592
                commit_and_continue_writing();
282✔
593
            }
282✔
594
        }
282✔
595
        for (auto k : table_accessors) {
282✔
596
            auto progress_status = progress_info->create_object_with_primary_key(k->get_name());
282✔
597
            if (!progress_status.get<bool>(col_links)) {
282✔
598
                k->migrate_links();
60✔
599
                progress_status.set(col_links, true);
60✔
600
                commit_and_continue_writing();
60✔
601
            }
60✔
602
        }
282✔
603

51✔
604
        // Final cleanup
51✔
605
        for (auto k : table_accessors) {
282✔
606
            k->finalize_migration(pk_cols[k]);
282✔
607
        }
282✔
608

51✔
609
        if (pk_table) {
102✔
610
            remove_table("pk");
54✔
611
        }
54✔
612
        remove_table(progress_info->get_key());
102✔
613
    }
102✔
614

102✔
615
    // Ensure we have search index on all primary key columns.
102✔
616
    auto table_keys = get_table_keys();
204✔
617
    if (current_file_format_version < 22) {
204✔
618
        for (auto k : table_keys) {
384✔
619
            auto t = get_table(k);
384✔
620
            if (auto col = t->get_primary_key_column()) {
384✔
621
                t->do_add_search_index(col, IndexType::General);
228✔
622
            }
228✔
623
        }
384✔
624
    }
138✔
625

102✔
626
    if (current_file_format_version == 22) {
204✔
627
        // Check that asymmetric table are empty
33✔
628
        for (auto k : table_keys) {
180✔
629
            auto t = get_table(k);
180✔
630
            if (t->is_asymmetric() && t->size() > 0) {
180✔
631
                t->clear();
6✔
632
            }
6✔
633
        }
180✔
634
    }
66✔
635
    if (current_file_format_version >= 21 && current_file_format_version < 23) {
204✔
636
        // Upgrade Set and Dictionary columns
33✔
637
        for (auto k : table_keys) {
180✔
638
            auto t = get_table(k);
180✔
639
            t->migrate_sets_and_dictionaries();
180✔
640
        }
180✔
641
    }
66✔
642
    // NOTE: Additional future upgrade steps go here.
102✔
643
}
204✔
644

645
void Transaction::promote_to_async()
646
{
7,488✔
647
    util::CheckedLockGuard lck(m_async_mutex);
7,488✔
648
    if (m_async_stage == AsyncState::Idle) {
7,488✔
649
        m_async_stage = AsyncState::HasLock;
366✔
650
    }
366✔
651
}
7,488✔
652

653
void Transaction::replicate(Transaction* dest, Replication& repl) const
654
{
78✔
655
    // We should only create entries for public tables
39✔
656
    std::vector<TableKey> public_table_keys;
78✔
657
    for (auto tk : get_table_keys()) {
222✔
658
        if (table_is_public(tk))
222✔
659
            public_table_keys.push_back(tk);
180✔
660
    }
222✔
661

39✔
662
    // Create tables
39✔
663
    for (auto tk : public_table_keys) {
180✔
664
        auto table = get_table(tk);
180✔
665
        auto table_name = table->get_name();
180✔
666
        if (!table->is_embedded()) {
180✔
667
            auto pk_col = table->get_primary_key_column();
138✔
668
            if (!pk_col)
138✔
669
                throw RuntimeError(
×
670
                    ErrorCodes::BrokenInvariant,
×
671
                    util::format("Class '%1' must have a primary key", Group::table_name_to_class_name(table_name)));
×
672
            auto pk_name = table->get_column_name(pk_col);
138✔
673
            if (pk_name != "_id")
138✔
674
                throw RuntimeError(ErrorCodes::BrokenInvariant,
×
675
                                   util::format("Primary key of class '%1' must be named '_id'. Current is '%2'",
×
676
                                                Group::table_name_to_class_name(table_name), pk_name));
×
677
            repl.add_class_with_primary_key(tk, table_name, DataType(pk_col.get_type()), pk_name,
138✔
678
                                            pk_col.is_nullable(), table->get_table_type());
138✔
679
        }
138✔
680
        else {
42✔
681
            repl.add_class(tk, table_name, Table::Type::Embedded);
42✔
682
        }
42✔
683
    }
180✔
684
    // Create columns
39✔
685
    for (auto tk : public_table_keys) {
180✔
686
        auto table = get_table(tk);
180✔
687
        auto pk_col = table->get_primary_key_column();
180✔
688
        auto cols = table->get_column_keys();
180✔
689
        for (auto col : cols) {
450✔
690
            if (col == pk_col)
450✔
691
                continue;
138✔
692
            repl.insert_column(table.unchecked_ptr(), col, DataType(col.get_type()), table->get_column_name(col),
312✔
693
                               table->get_opposite_table(col).unchecked_ptr());
312✔
694
        }
312✔
695
    }
180✔
696
    dest->commit_and_continue_writing();
78✔
697
    // Now the schema should be in place - create the objects
39✔
698
#ifdef REALM_DEBUG
78✔
699
    constexpr int number_of_objects_to_create_before_committing = 100;
78✔
700
#else
701
    constexpr int number_of_objects_to_create_before_committing = 1000;
702
#endif
703
    auto n = number_of_objects_to_create_before_committing;
78✔
704
    for (auto tk : public_table_keys) {
168✔
705
        auto table = get_table(tk);
168✔
706
        if (table->is_embedded())
168✔
707
            continue;
42✔
708
        // std::cout << "Table: " << table->get_name() << std::endl;
63✔
709
        auto pk_col = table->get_primary_key_column();
126✔
710
        auto cols = get_col_info(table.unchecked_ptr());
126✔
711
        for (auto o : *table) {
750✔
712
            auto obj_key = o.get_key();
750✔
713
            Mixed pk = o.get_any(pk_col);
750✔
714
            // std::cout << "    Object: " << pk << std::endl;
375✔
715
            repl.create_object_with_primary_key(table.unchecked_ptr(), obj_key, pk);
750✔
716
            generate_properties_for_obj(repl, o, cols);
750✔
717
            if (--n == 0) {
750✔
718
                dest->commit_and_continue_writing();
6✔
719
                n = number_of_objects_to_create_before_committing;
6✔
720
            }
6✔
721
        }
750✔
722
    }
126✔
723
}
78✔
724

725
void Transaction::complete_async_commit()
726
{
1,332✔
727
    // sync to disk:
294✔
728
    DB::ReadLockInfo read_lock;
1,332✔
729
    try {
1,332✔
730
        read_lock = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID());
1,332✔
731
        if (db->m_logger) {
1,332✔
732
            db->m_logger->log(util::Logger::Level::trace, "Tr %1: Committing ref %2 to disk", m_log_id,
1,332✔
733
                              read_lock.m_top_ref);
1,332✔
734
        }
1,332✔
735
        GroupWriter out(*this);
1,332✔
736
        out.commit(read_lock.m_top_ref); // Throws
1,332✔
737
        // we must release the write mutex before the callback, because the callback
294✔
738
        // is allowed to re-request it.
294✔
739
        db->release_read_lock(read_lock);
1,332✔
740
        if (m_oldest_version_not_persisted) {
1,332✔
741
            db->release_read_lock(*m_oldest_version_not_persisted);
1,326✔
742
            m_oldest_version_not_persisted.reset();
1,326✔
743
        }
1,326✔
744
    }
1,332✔
745
    catch (const std::exception& e) {
297✔
746
        m_commit_exception = std::current_exception();
6✔
747
        if (db->m_logger) {
6✔
748
            db->m_logger->log(util::Logger::Level::error, "Tr %1: Committing to disk failed with exception: \"%2\"",
6✔
749
                              m_log_id, e.what());
6✔
750
        }
6✔
751
        m_async_commit_has_failed = true;
6✔
752
        db->release_read_lock(read_lock);
6✔
753
    }
6✔
754
}
1,332✔
755

756
void Transaction::async_complete_writes(util::UniqueFunction<void()> when_synchronized)
757
{
9,714✔
758
    util::CheckedLockGuard lck(m_async_mutex);
9,714✔
759
    if (m_async_stage == AsyncState::HasLock) {
9,714✔
760
        // Nothing to commit to disk - just release write lock
27✔
761
        m_async_stage = AsyncState::Idle;
54✔
762
        db->async_end_write();
54✔
763
    }
54✔
764
    else if (m_async_stage == AsyncState::HasCommits) {
9,660✔
765
        m_async_stage = AsyncState::Syncing;
1,308✔
766
        m_commit_exception = std::exception_ptr();
1,308✔
767
        // get a callback on the helper thread, in which to sync to disk
282✔
768
        db->async_sync_to_disk([this, cb = std::move(when_synchronized)]() noexcept {
1,308✔
769
            complete_async_commit();
1,308✔
770
            util::CheckedLockGuard lck(m_async_mutex);
1,308✔
771
            m_async_stage = AsyncState::Idle;
1,308✔
772
            if (m_waiting_for_sync) {
1,308✔
773
                m_waiting_for_sync = false;
366✔
774
                m_async_cv.notify_all();
366✔
775
            }
366✔
776
            else {
942✔
777
                cb();
942✔
778
            }
942✔
779
        });
1,308✔
780
    }
1,308✔
781
}
9,714✔
782

783
void Transaction::prepare_for_close()
784
{
2,989,545✔
785
    util::CheckedLockGuard lck(m_async_mutex);
2,989,545✔
786
    switch (m_async_stage) {
2,989,545✔
787
        case AsyncState::Idle:
3,002,727✔
788
            break;
3,002,727✔
789

790
        case AsyncState::Requesting:
24✔
791
            // We don't have the ability to cancel a wait on the write lock, so
12✔
792
            // unfortunately we have to wait for it to be acquired.
12✔
793
            REALM_ASSERT(m_transact_stage == DB::transact_Reading);
24✔
794
            REALM_ASSERT(!m_oldest_version_not_persisted);
24✔
795
            m_waiting_for_write_lock = true;
24✔
796
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
48✔
797
                return !m_waiting_for_write_lock;
48✔
798
            });
48✔
799
            db->end_write_on_correct_thread();
24✔
800
            break;
24✔
801

802
        case AsyncState::HasLock:
30✔
803
            // We have the lock and are currently in a write transaction, and
15✔
804
            // also may have some pending previous commits to write
15✔
805
            if (m_transact_stage == DB::transact_Writing) {
30✔
806
                db->reset_free_space_tracking();
18✔
807
                m_transact_stage = DB::transact_Reading;
18✔
808
            }
18✔
809
            if (m_oldest_version_not_persisted) {
30✔
810
                complete_async_commit();
×
811
            }
×
812
            db->end_write_on_correct_thread();
30✔
813
            break;
30✔
814

815
        case AsyncState::HasCommits:
24✔
816
            // We have commits which need to be synced to disk, so do that
12✔
817
            REALM_ASSERT(m_transact_stage == DB::transact_Reading);
24✔
818
            complete_async_commit();
24✔
819
            db->end_write_on_correct_thread();
24✔
820
            break;
24✔
821

822
        case AsyncState::Syncing:
24✔
823
            // The worker thread is currently writing, so wait for it to complete
12✔
824
            REALM_ASSERT(m_transact_stage == DB::transact_Reading);
24✔
825
            m_waiting_for_sync = true;
24✔
826
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
48✔
827
                return !m_waiting_for_sync;
48✔
828
            });
48✔
829
            break;
24✔
830
    }
3,003,126✔
831
    m_async_stage = AsyncState::Idle;
3,003,126✔
832
}
3,003,126✔
833

834
void Transaction::acquire_write_lock()
835
{
381,603✔
836
    util::CheckedUniqueLock lck(m_async_mutex);
381,603✔
837
    switch (m_async_stage) {
381,603✔
838
        case AsyncState::Idle:
381,156✔
839
            lck.unlock();
381,156✔
840
            db->do_begin_possibly_async_write();
381,156✔
841
            return;
381,156✔
842

843
        case AsyncState::Requesting:
105✔
844
            m_waiting_for_write_lock = true;
105✔
845
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
210✔
846
                return !m_waiting_for_write_lock;
210✔
847
            });
210✔
848
            return;
105✔
849

850
        case AsyncState::HasLock:
✔
851
        case AsyncState::HasCommits:
✔
852
            return;
×
853

854
        case AsyncState::Syncing:
342✔
855
            m_waiting_for_sync = true;
342✔
856
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
684✔
857
                return !m_waiting_for_sync;
684✔
858
            });
684✔
859
            lck.unlock();
342✔
860
            db->do_begin_possibly_async_write();
342✔
861
            break;
342✔
862
    }
381,603✔
863
}
381,603✔
864

865
void Transaction::do_end_read() noexcept
866
{
2,820,213✔
867
    if (db->m_logger)
2,820,213✔
868
        db->m_logger->log(util::Logger::Level::trace, "End transaction %1", m_log_id);
851,652✔
869

1,743,666✔
870
    prepare_for_close();
2,820,213✔
871
    detach();
2,820,213✔
872

1,743,666✔
873
    // We should always be ensuring that async commits finish before we get here,
1,743,666✔
874
    // but if the fsync() failed or we failed to update the top pointer then
1,743,666✔
875
    // there's not much we can do and we have to just accept that we're losing
1,743,666✔
876
    // those commits.
1,743,666✔
877
    if (m_oldest_version_not_persisted) {
2,820,213✔
878
        REALM_ASSERT(m_async_commit_has_failed);
6✔
879
        // We need to not release our read lock on m_oldest_version_not_persisted
3✔
880
        // as that's the version the top pointer is referencing and overwriting
3✔
881
        // that version will corrupt the Realm file.
3✔
882
        db->leak_read_lock(*m_oldest_version_not_persisted);
6✔
883
    }
6✔
884
    db->release_read_lock(m_read_lock);
2,820,213✔
885

1,743,666✔
886
    m_alloc.note_reader_end(this);
2,820,213✔
887
    set_transact_stage(DB::transact_Ready);
2,820,213✔
888
    // reset the std::shared_ptr to allow the DB object to release resources
1,743,666✔
889
    // as early as possible.
1,743,666✔
890
    db.reset();
2,820,213✔
891
}
2,820,213✔
892

893
// This is the same as do_end_read() above, but with the requirement that
894
// 1) This is called with the db->mutex locked already
895
// 2) No async commits outstanding
896
void Transaction::close_read_with_lock()
897
{
144✔
898
    REALM_ASSERT(m_transact_stage == DB::transact_Reading);
144✔
899
    {
144✔
900
        util::CheckedLockGuard lck(m_async_mutex);
144✔
901
        REALM_ASSERT_EX(m_async_stage == AsyncState::Idle, size_t(m_async_stage));
144✔
902
    }
144✔
903

72✔
904
    detach();
144✔
905
    REALM_ASSERT_EX(!m_oldest_version_not_persisted, m_oldest_version_not_persisted->m_type,
144✔
906
                    m_oldest_version_not_persisted->m_version, m_oldest_version_not_persisted->m_top_ref,
144✔
907
                    m_oldest_version_not_persisted->m_file_size);
144✔
908
    db->do_release_read_lock(m_read_lock);
144✔
909

72✔
910
    m_alloc.note_reader_end(this);
144✔
911
    set_transact_stage(DB::transact_Ready);
144✔
912
    // reset the std::shared_ptr to allow the DB object to release resources
72✔
913
    // as early as possible.
72✔
914
    db.reset();
144✔
915
}
144✔
916

917

918
void Transaction::initialize_replication()
919
{
102✔
920
    if (m_transact_stage == DB::transact_Writing) {
102✔
921
        if (Replication* repl = get_replication()) {
102✔
922
            auto current_version = m_read_lock.m_version;
96✔
923
            bool history_updated = false;
96✔
924
            repl->initiate_transact(*this, current_version, history_updated); // Throws
96✔
925
        }
96✔
926
    }
102✔
927
}
102✔
928

929
void Transaction::set_transact_stage(DB::TransactStage stage) noexcept
930
{
6,447,618✔
931
#if REALM_METRICS
6,447,618✔
932
    REALM_ASSERT(m_metrics == db->m_metrics);
6,447,618✔
933
    if (m_metrics) { // null if metrics are disabled
6,447,618✔
934
        size_t free_space;
660✔
935
        size_t used_space;
660✔
936
        db->get_stats(free_space, used_space);
660✔
937
        size_t total_size = used_space + free_space;
660✔
938

330✔
939
        size_t num_objects = m_total_rows;
660✔
940
        size_t num_available_versions = static_cast<size_t>(db->get_number_of_versions());
660✔
941
        size_t num_decrypted_pages = realm::util::get_num_decrypted_pages();
660✔
942

330✔
943
        if (stage == DB::transact_Reading) {
660✔
944
            if (m_transact_stage == DB::transact_Writing) {
132✔
945
                m_metrics->end_write_transaction(total_size, free_space, num_objects, num_available_versions,
×
946
                                                 num_decrypted_pages);
×
947
            }
×
948
            m_metrics->start_read_transaction();
132✔
949
        }
132✔
950
        else if (stage == DB::transact_Writing) {
528✔
951
            if (m_transact_stage == DB::transact_Reading) {
198✔
952
                m_metrics->end_read_transaction(total_size, free_space, num_objects, num_available_versions,
×
953
                                                num_decrypted_pages);
×
954
            }
×
955
            m_metrics->start_write_transaction();
198✔
956
        }
198✔
957
        else if (stage == DB::transact_Ready) {
330✔
958
            m_metrics->end_read_transaction(total_size, free_space, num_objects, num_available_versions,
330✔
959
                                            num_decrypted_pages);
330✔
960
            m_metrics->end_write_transaction(total_size, free_space, num_objects, num_available_versions,
330✔
961
                                             num_decrypted_pages);
330✔
962
        }
330✔
963
    }
660✔
964
#endif
6,447,618✔
965

3,893,166✔
966
    m_transact_stage = stage;
6,447,618✔
967
}
6,447,618✔
968

969
class NodeTree {
970
public:
971
    NodeTree(size_t evac_limit, size_t work_limit)
972
        : m_evac_limit(evac_limit)
973
        , m_work_limit(int64_t(work_limit))
974
        , m_moved(0)
975
    {
5,319✔
976
    }
5,319✔
977
    ~NodeTree()
978
    {
5,319✔
979
        // std::cout << "Moved: " << m_moved << std::endl;
2,649✔
980
    }
5,319✔
981

982
    /// Function used to traverse the node tree and "copy on write" nodes
983
    /// that are found above the evac_limit. The function will return
984
    /// when either the whole tree has been travesed or when the work_limit
985
    /// has been reached.
986
    /// \param current_node - node to process.
987
    /// \param level - the level at which current_node is placed in the tree
988
    /// \param progress - When the traversal is initiated, this vector identifies at which
989
    ///                   node the process should be resumed. It is subesequently updated
990
    ///                   to point to the node we have just processed
991
    bool trv(Array& current_node, unsigned level, std::vector<size_t>& progress)
992
    {
1,259,967✔
993
        if (m_work_limit < 0) {
1,259,967✔
994
            return false;
5,217✔
995
        }
5,217✔
996
        if (current_node.is_read_only()) {
1,254,750✔
997
            size_t byte_size = current_node.get_byte_size();
1,227,693✔
998
            if ((current_node.get_ref() + byte_size) > m_evac_limit) {
1,227,693✔
999
                current_node.copy_on_write();
1,171,080✔
1000
                m_moved++;
1,171,080✔
1001
                m_work_limit -= byte_size;
1,171,080✔
1002
            }
1,171,080✔
1003
        }
1,227,693✔
1004

627,207✔
1005
        if (current_node.has_refs()) {
1,254,750✔
1006
            auto sz = current_node.size();
41,502✔
1007
            m_work_limit -= sz;
41,502✔
1008
            if (progress.size() == level) {
41,502✔
1009
                progress.push_back(0);
10,362✔
1010
            }
10,362✔
1011
            REALM_ASSERT_EX(level < progress.size(), level, progress.size());
41,502✔
1012
            size_t ndx = progress[level];
41,502✔
1013
            while (ndx < sz) {
1,279,494✔
1014
                auto val = current_node.get(ndx);
1,269,198✔
1015
                if (val && !(val & 1)) {
1,269,198✔
1016
                    Array arr(current_node.get_alloc());
1,254,276✔
1017
                    arr.set_parent(&current_node, ndx);
1,254,276✔
1018
                    arr.init_from_parent();
1,254,276✔
1019
                    if (!trv(arr, level + 1, progress)) {
1,254,276✔
1020
                        return false;
31,206✔
1021
                    }
31,206✔
1022
                }
1,237,992✔
1023
                ndx = ++progress[level];
1,237,992✔
1024
            }
1,237,992✔
1025
            while (progress.size() > level)
31,125✔
1026
                progress.pop_back();
10,302✔
1027
        }
10,296✔
1028
        return true;
1,239,150✔
1029
    }
1,254,750✔
1030

1031
private:
1032
    size_t m_evac_limit;
1033
    int64_t m_work_limit;
1034
    size_t m_moved;
1035
};
1036

1037

1038
void Transaction::cow_outliers(std::vector<size_t>& progress, size_t evac_limit, size_t work_limit)
1039
{
5,319✔
1040
    NodeTree node_tree(evac_limit, work_limit);
5,319✔
1041
    if (progress.empty()) {
5,319✔
1042
        progress.push_back(s_table_name_ndx);
114✔
1043
    }
114✔
1044
    if (progress[0] == s_table_name_ndx) {
5,319✔
1045
        if (!node_tree.trv(m_table_names, 1, progress))
114✔
1046
            return;
×
1047
        progress.back() = s_table_refs_ndx; // Handle tables next
114✔
1048
    }
114✔
1049
    if (progress[0] == s_table_refs_ndx) {
5,319✔
1050
        if (!node_tree.trv(m_tables, 1, progress))
5,319✔
1051
            return;
5,217✔
1052
        progress.back() = s_hist_ref_ndx; // Handle history next
102✔
1053
    }
102✔
1054
    if (progress[0] == s_hist_ref_ndx && m_top.get(s_hist_ref_ndx)) {
2,712✔
1055
        Array hist_arr(m_top.get_alloc());
102✔
1056
        hist_arr.set_parent(&m_top, s_hist_ref_ndx);
102✔
1057
        hist_arr.init_from_parent();
102✔
1058
        if (!node_tree.trv(hist_arr, 1, progress))
102✔
1059
            return;
×
1060
    }
102✔
1061
    progress.clear();
102✔
1062
}
102✔
1063

1064
} // namespace realm
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