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

realm / realm-core / 2545

06 Aug 2024 04:05PM UTC coverage: 91.089% (-0.02%) from 91.108%
2545

push

Evergreen

web-flow
RCORE-2223 reduce unnecessary table selections in replication (#7899)

* reduce unnecessary table selections in replication

fix collection notifications after embedded object creations

cleanup state, and avoid excessive collection selections

fix trace logging

* optimize

* changelog

102750 of 181570 branches covered (56.59%)

304 of 359 new or added lines in 3 files covered. (84.68%)

65 existing lines in 19 files now uncovered.

216969 of 238195 relevant lines covered (91.09%)

5832026.01 hits per line

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

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

19
#include "testsettings.hpp"
20
#ifdef TEST_REPLICATION
21

22
#include <algorithm>
23
#include <memory>
24

25
#include <realm.hpp>
26
#include <realm/util/features.h>
27
#include <realm/util/file.hpp>
28
#include <realm/util/overload.hpp>
29
#include <realm/replication.hpp>
30

31
#include "test.hpp"
32
#include "test_table_helper.hpp"
33

34
using namespace realm;
35
using namespace realm::util;
36
using namespace realm::test_util;
37
using unit_test::TestContext;
38

39

40
// Test independence and thread-safety
41
// -----------------------------------
42
//
43
// All tests must be thread safe and independent of each other. This
44
// is required because it allows for both shuffling of the execution
45
// order and for parallelized testing.
46
//
47
// In particular, avoid using std::rand() since it is not guaranteed
48
// to be thread safe. Instead use the API offered in
49
// `test/util/random.hpp`.
50
//
51
// All files created in tests must use the TEST_PATH macro (or one of
52
// its friends) to obtain a suitable file system path. See
53
// `test/util/test_path.hpp`.
54
//
55
//
56
// Debugging and the ONLY() macro
57
// ------------------------------
58
//
59
// A simple way of disabling all tests except one called `Foo`, is to
60
// replace TEST(Foo) with ONLY(Foo) and then recompile and rerun the
61
// test suite. Note that you can also use filtering by setting the
62
// environment varible `UNITTEST_FILTER`. See `README.md` for more on
63
// this.
64
//
65
// Another way to debug a particular test, is to copy that test into
66
// `experiments/testcase.cpp` and then run `sh build.sh
67
// check-testcase` (or one of its friends) from the command line.
68

69
namespace {
70
class ReplSyncClient : public Replication {
71
public:
72
    ReplSyncClient(int history_schema_version, uint64_t file_ident = 0)
73
        : m_file_ident(file_ident)
7✔
74
        , m_history_schema_version(history_schema_version)
7✔
75
    {
14✔
76
    }
14✔
77

78
    version_type prepare_changeset(const char*, size_t, version_type version) override
79
    {
12✔
80
        if (!m_arr) {
12✔
81
            using gf = _impl::GroupFriend;
10✔
82
            Allocator& alloc = gf::get_alloc(*m_group);
10✔
83
            m_arr = std::make_unique<BinaryColumn>(alloc);
10✔
84
            gf::prepare_history_parent(*m_group, *m_arr, hist_SyncClient, m_history_schema_version, 0);
10✔
85
            m_arr->create();
10✔
86
        }
10✔
87
        return version + 1;
12✔
88
    }
12✔
89

90
    bool is_upgraded() const
91
    {
2✔
92
        return m_upgraded;
2✔
93
    }
2✔
94

95
    bool is_upgradable_history_schema(int) const noexcept override
96
    {
2✔
97
        return true;
2✔
98
    }
2✔
99

100
    void upgrade_history_schema(int) override
101
    {
2✔
102
        m_group->set_sync_file_id(m_file_ident);
2✔
103
        m_upgraded = true;
2✔
104
    }
2✔
105

106
    HistoryType get_history_type() const noexcept override
107
    {
24✔
108
        return hist_SyncClient;
24✔
109
    }
24✔
110

111
    int get_history_schema_version() const noexcept override
112
    {
20✔
113
        return m_history_schema_version;
20✔
114
    }
20✔
115

116
    std::unique_ptr<_impl::History> _create_history_read() override
117
    {
2✔
118
        return {};
2✔
119
    }
2✔
120

121
private:
122
    Group* m_group;
123
    std::unique_ptr<BinaryColumn> m_arr;
124
    uint64_t m_file_ident;
125
    int m_history_schema_version;
126
    bool m_upgraded = false;
127

128
    void do_initiate_transact(Group& group, version_type version, bool hist_updated) override
129
    {
14✔
130
        Replication::do_initiate_transact(group, version, hist_updated);
14✔
131
        m_group = &group;
14✔
132
    }
14✔
133
};
134

135
TEST(Replication_HistorySchemaVersionNormal)
136
{
2✔
137
    SHARED_GROUP_TEST_PATH(path);
2✔
138
    ReplSyncClient repl(1);
2✔
139
    DBRef sg_1 = DB::create(repl, path);
2✔
140
    // it should be possible to have two open shared groups on the same thread
141
    // without any read/write transactions in between
142
    DBRef sg_2 = DB::create(repl, path);
2✔
143
}
2✔
144

145
TEST(Replication_HistorySchemaVersionDuringWT)
146
{
2✔
147
    SHARED_GROUP_TEST_PATH(path);
2✔
148

149
    ReplSyncClient repl(1);
2✔
150
    DBRef sg_1 = DB::create(repl, path);
2✔
151
    {
2✔
152
        // Do an empty commit to force the file format version to be established.
153
        WriteTransaction wt(sg_1);
2✔
154
        wt.commit();
2✔
155
    }
2✔
156

157
    auto wt = sg_1->start_write();
2✔
158
    wt->set_sync_file_id(2);
2✔
159

160
    // It should be possible to open a second db at the same path
161
    // while a WriteTransaction is active via another SharedGroup.
162
    DBRef sg_2 = DB::create(repl, path);
2✔
163
    wt->commit();
2✔
164

165
    auto rt = sg_2->start_read();
2✔
166
    CHECK_EQUAL(rt->get_sync_file_id(), 2);
2✔
167
}
2✔
168

169

170
// This is to test that the exported file has no memory leaks
171
TEST(Replication_GroupWriteWithoutHistory)
172
{
2✔
173
    SHARED_GROUP_TEST_PATH(path);
2✔
174
    SHARED_GROUP_TEST_PATH(out1);
2✔
175
    SHARED_GROUP_TEST_PATH(out2);
2✔
176

177
    ReplSyncClient repl(1);
2✔
178
    DBRef sg_1 = DB::create(repl, path);
2✔
179
    {
2✔
180
        WriteTransaction wt(sg_1);
2✔
181
        auto table = wt.add_table("Table");
2✔
182
        auto col = table->add_column(type_String, "strings");
2✔
183
        auto obj = table->create_object();
2✔
184
        obj.set(col, "Hello");
2✔
185
        wt.commit();
2✔
186
    }
2✔
187
    {
2✔
188
        ReadTransaction rt(sg_1);
2✔
189
        // Export file without history
190
        rt.get_group().write(out1);
2✔
191
    }
2✔
192

193
    {
2✔
194
        // Open without history
195
        DBRef sg_2 = DB::create(out1);
2✔
196
        ReadTransaction rt(sg_2);
2✔
197
        rt.get_group().verify();
2✔
198
    }
2✔
199

200
    {
2✔
201
        ReadTransaction rt(sg_1);
2✔
202
        // Export file with history
203
        rt.get_group().write(out2, nullptr, 1);
2✔
204
    }
2✔
205

206
    {
2✔
207
        // Open with history
208
        ReplSyncClient repl2(1);
2✔
209
        DBRef sg_2 = DB::create(repl2, out2);
2✔
210
        ReadTransaction rt(sg_2);
2✔
211
        rt.get_group().verify();
2✔
212
    }
2✔
213
}
2✔
214

215
TEST(Replication_HistorySchemaVersionUpgrade)
216
{
2✔
217
    SHARED_GROUP_TEST_PATH(path);
2✔
218

219
    {
2✔
220
        ReplSyncClient repl(1);
2✔
221
        DBRef sg = DB::create(repl, path);
2✔
222
        {
2✔
223
            // Do an empty commit to force the file format version to be established.
224
            WriteTransaction wt(sg);
2✔
225
            wt.commit();
2✔
226
        }
2✔
227
    }
2✔
228

229
    ReplSyncClient repl(2);
2✔
230
    DBRef sg_1 = DB::create(repl, path); // This will be the session initiator
2✔
231
    CHECK(repl.is_upgraded());
2✔
232
    WriteTransaction wt(sg_1);
2✔
233
    // When this one is opened, the file should have been upgraded
234
    // If this was not the case we would have triggered another upgrade
235
    // and the test would hang
236
    DBRef sg_2 = DB::create(repl, path);
2✔
237
}
2✔
238

239
TEST(Replication_WriteWithoutHistory)
240
{
2✔
241
    SHARED_GROUP_TEST_PATH(path_1);
2✔
242
    SHARED_GROUP_TEST_PATH(path_2);
2✔
243

244
    ReplSyncClient repl(1);
2✔
245
    DBRef sg = DB::create(repl, path_1);
2✔
246
    {
2✔
247
        // Do an empty commit to force the file format version to be established.
248
        WriteTransaction wt(sg);
2✔
249
        wt.add_table("Table");
2✔
250
        wt.commit();
2✔
251
    }
2✔
252

253
    {
2✔
254
        ReadTransaction rt(sg);
2✔
255
        rt.get_group().write(path_2, nullptr, rt.get_version(), false);
2✔
256
    }
2✔
257
    // Make sure the realm can be opened without history
258
    DBRef sg_2 = DB::create(path_2);
2✔
259
    {
2✔
260
        WriteTransaction wt(sg_2);
2✔
261
        auto table = wt.get_table("Table");
2✔
262
        CHECK(table);
2✔
263
        table->add_column(type_Int, "int");
2✔
264
        wt.commit();
2✔
265
    }
2✔
266
}
2✔
267

268
struct Select {
269
    TableKey table_key;
270
};
271

272
struct Create {
273
    int64_t obj_key;
274
};
275

276
struct Mutate {
277
    int64_t obj_key;
278
    ColKey col_key;
279
};
280

281
struct Remove {
282
    int64_t obj_key;
283
};
284

285
struct SelectColl {
286
    int64_t obj_key;
287
    ColKey col_key;
288
};
289

290
struct CollInsert {
291
    size_t ndx;
292
};
293

294
struct CollSet {
295
    size_t ndx;
296
};
297

298
using InstructionVariant = mpark::variant<Select, Create, Mutate, Remove, SelectColl, CollInsert, CollSet>;
299

300
std::ostream& print_instructions(std::ostream& os, const std::vector<InstructionVariant>& ivs,
301
                                 size_t first_difference) noexcept
NEW
302
{
×
NEW
303
    size_t ndx = 0;
×
NEW
304
    for (auto& element : ivs) {
×
NEW
305
        if (first_difference == ndx) {
×
NEW
306
            os << "==> ";
×
NEW
307
        }
×
NEW
308
        util::format(os, "[%1]: ", ndx++);
×
NEW
309
        auto print = overload{
×
NEW
310
            [&](Select st) {
×
NEW
311
                util::format(os, "Select{%1}", st.table_key);
×
NEW
312
            },
×
NEW
313
            [&](Create co) {
×
NEW
314
                util::format(os, "CreateObject{%1}", co.obj_key);
×
NEW
315
            },
×
NEW
316
            [&](Mutate mo) {
×
NEW
317
                util::format(os, "Mutate{%1, %2}", mo.obj_key, mo.col_key);
×
NEW
318
            },
×
NEW
319
            [&](Remove rm) {
×
NEW
320
                util::format(os, "RemoveObject{%1}", rm.obj_key);
×
NEW
321
            },
×
NEW
322
            [&](SelectColl sc) {
×
NEW
323
                util::format(os, "SelectCollection{%1, %2}", sc.obj_key, sc.col_key);
×
NEW
324
            },
×
NEW
325
            [&](CollInsert ci) {
×
NEW
326
                util::format(os, "CollectionInsert{%1}", ci.ndx);
×
NEW
327
            },
×
NEW
328
            [&](CollSet cs) {
×
NEW
329
                util::format(os, "CollectionSet{%1}", cs.ndx);
×
NEW
330
            },
×
NEW
331
        };
×
NEW
332
        mpark::visit(print, element);
×
NEW
333
        os << '\n';
×
NEW
334
    }
×
NEW
335
    return os;
×
NEW
336
}
×
337

338
bool compare_instructions(const InstructionVariant& a, const InstructionVariant& b)
339
{
178✔
340
    bool equal = false;
178✔
341
    auto comp = overload{
178✔
342
        [&](Select a_val) {
178✔
343
            if (const Select* b_val = mpark::get_if<Select>(&b)) {
58✔
344
                equal = a_val.table_key == b_val->table_key;
58✔
345
            }
58✔
346
        },
58✔
347
        [&](Create a_val) {
178✔
348
            if (const Create* b_val = mpark::get_if<Create>(&b)) {
36✔
349
                equal = a_val.obj_key == b_val->obj_key;
36✔
350
            }
36✔
351
        },
36✔
352
        [&](Mutate a_val) {
178✔
353
            if (const Mutate* b_val = mpark::get_if<Mutate>(&b)) {
22✔
354
                equal = (a_val.obj_key == b_val->obj_key && a_val.col_key == b_val->col_key);
22✔
355
            }
22✔
356
        },
22✔
357
        [&](Remove a_val) {
178✔
358
            if (const Remove* b_val = mpark::get_if<Remove>(&b)) {
2✔
359
                equal = a_val.obj_key == b_val->obj_key;
2✔
360
            }
2✔
361
        },
2✔
362
        [&](SelectColl a_val) {
178✔
363
            if (const SelectColl* b_val = mpark::get_if<SelectColl>(&b)) {
26✔
364
                equal = a_val.obj_key == b_val->obj_key && a_val.col_key == b_val->col_key;
26✔
365
            }
26✔
366
        },
26✔
367
        [&](CollInsert a_val) {
178✔
368
            if (const CollInsert* b_val = mpark::get_if<CollInsert>(&b)) {
30✔
369
                equal = a_val.ndx == b_val->ndx;
30✔
370
            }
30✔
371
        },
30✔
372
        [&](CollSet a_val) {
178✔
373
            if (const CollSet* b_val = mpark::get_if<CollSet>(&b)) {
4✔
374
                equal = a_val.ndx == b_val->ndx;
4✔
375
            }
4✔
376
        },
4✔
377
    };
178✔
378
    mpark::visit(comp, a);
178✔
379
    return equal;
178✔
380
}
178✔
381

382
struct RecordingObserver : _impl::NoOpTransactionLogParser {
383
    unit_test::TestContext& test_context;
384
    std::vector<InstructionVariant> m_expected_ops;
385
    std::vector<InstructionVariant> m_observed_ops;
386

387
    RecordingObserver(unit_test::TestContext& test_context, std::initializer_list<InstructionVariant> ops)
388
        : test_context(test_context)
15✔
389
        , m_expected_ops(ops.begin(), ops.end())
15✔
390
    {
30✔
391
    }
30✔
392

393
    RecordingObserver& operator=(RecordingObserver&& obs) noexcept
394
    {
18✔
395
        m_observed_ops.swap(obs.m_observed_ops);
18✔
396
        m_expected_ops.swap(obs.m_expected_ops);
18✔
397
        return *this;
18✔
398
    }
18✔
399

400
    bool select_table(TableKey t)
401
    {
58✔
402
        _impl::NoOpTransactionLogParser::select_table(t);
58✔
403
        m_observed_ops.push_back(Select{t});
58✔
404
        return true;
58✔
405
    }
58✔
406

407
    bool create_object(ObjKey obj_key)
408
    {
36✔
409
        m_observed_ops.push_back(Create{obj_key.value});
36✔
410
        return true;
36✔
411
    }
36✔
412
    bool modify_object(ColKey col, ObjKey obj)
413
    {
22✔
414
        m_observed_ops.push_back(Mutate{obj.value, col});
22✔
415
        return true;
22✔
416
    }
22✔
417
    bool remove_object(ObjKey obj)
418
    {
2✔
419
        m_observed_ops.push_back(Remove{obj.value});
2✔
420
        return true;
2✔
421
    }
2✔
422
    bool select_collection(ColKey col_key, ObjKey obj_key, const StablePath& path)
423
    {
26✔
424
        _impl::NoOpTransactionLogParser::select_collection(col_key, obj_key, path);
26✔
425
        m_observed_ops.push_back(SelectColl{obj_key.value, col_key});
26✔
426
        return true;
26✔
427
    }
26✔
428
    bool collection_insert(size_t ndx)
429
    {
30✔
430
        m_observed_ops.push_back(CollInsert{ndx});
30✔
431
        return true;
30✔
432
    }
30✔
433
    bool collection_set(size_t ndx)
434
    {
4✔
435
        m_observed_ops.push_back(CollSet{ndx});
4✔
436
        return true;
4✔
437
    }
4✔
438

439
    void check()
440
    {
30✔
441
        bool equality = m_observed_ops.size() == m_expected_ops.size();
30✔
442
        size_t first_difference = -1;
30✔
443
        if (equality) {
30✔
444
            for (size_t i = 0; i < m_expected_ops.size(); ++i) {
208✔
445
                if (!compare_instructions(m_observed_ops[i], m_expected_ops[i])) {
178✔
NEW
446
                    first_difference = i;
×
NEW
447
                    equality = false;
×
NEW
448
                    break;
×
NEW
449
                }
×
450
            }
178✔
451
        }
30✔
452

453
        CHECK(equality);
30✔
454
        if (!equality) {
30✔
NEW
455
            std::cerr << "expected: \n";
×
NEW
456
            print_instructions(std::cerr, m_expected_ops, first_difference);
×
NEW
457
            std::cerr << "\nactual: \n";
×
NEW
458
            print_instructions(std::cerr, m_observed_ops, first_difference);
×
NEW
459
            std::cerr << std::endl;
×
NEW
460
        }
×
461
    }
30✔
462
};
463

464
template <typename Fn>
465
void expect(DBRef db, RecordingObserver& observer, Fn&& write)
466
{
28✔
467
    auto read = db->start_read();
28✔
468
    {
28✔
469
        auto tr = db->start_write();
28✔
470
        write(*tr);
28✔
471
        tr->commit();
28✔
472
    }
28✔
473
    read->advance_read(&observer);
28✔
474
    observer.check();
28✔
475
}
28✔
476

477
TEST(Replication_MutationsOnNewlyCreatedObject)
478
{
2✔
479
    SHARED_GROUP_TEST_PATH(path);
2✔
480
    auto db = DB::create(make_in_realm_history(), path);
2✔
481

482
    TableKey tk;
2✔
483
    ColKey col;
2✔
484
    {
2✔
485
        auto tr = db->start_write();
2✔
486
        auto table = tr->add_table("table");
2✔
487
        tk = table->get_key();
2✔
488
        col = table->add_column(type_Int, "value");
2✔
489
        tr->commit();
2✔
490
    }
2✔
491

492
    // Object creations with immediate mutations should report creations only
493
    auto obs = RecordingObserver(test_context, {Select{tk}, Create{0}, Create{1}});
2✔
494
    expect(db, obs, [](auto& tr) {
2✔
495
        auto table = tr.get_table("table");
2✔
496
        table->create_object().set_all(1);
2✔
497
        table->create_object().set_all(1);
2✔
498
    });
2✔
499

500
    // Mutating existing objects should report modifications
501
    obs = RecordingObserver(test_context, {Select{tk}, Mutate{0, col}, Mutate{1, col}});
2✔
502
    expect(db, obs, [](auto& tr) {
2✔
503
        auto table = tr.get_table("table");
2✔
504
        table->get_object(0).set_all(1);
2✔
505
        table->get_object(1).set_all(1);
2✔
506
    });
2✔
507

508
    // Create two objects and then mutate them. We only track the most recently
509
    // created object, so this emits a mutation for the first object but not
510
    // the second.
511
    obs = RecordingObserver(test_context, {Select{tk}, Create{2}, Create{3}, Mutate{2, col}});
2✔
512
    expect(db, obs, [](auto& tr) {
2✔
513
        auto table = tr.get_table("table");
2✔
514
        auto obj1 = table->create_object();
2✔
515
        auto obj2 = table->create_object();
2✔
516
        obj1.set_all(1);
2✔
517
        obj2.set_all(1);
2✔
518
    });
2✔
519

520
    TableKey tk2;
2✔
521
    ColKey col2;
2✔
522
    {
2✔
523
        auto tr = db->start_write();
2✔
524
        auto table = tr->add_table("table 2");
2✔
525
        tk2 = table->get_key();
2✔
526
        col2 = table->add_column(type_Int, "value");
2✔
527
        tr->commit();
2✔
528
    }
2✔
529

530
    // Creating an object in one table and then modifying the object with the
531
    // same ObjKey in a different table
532
    obs = RecordingObserver(test_context, {Select{tk2}, Create{0}, Select{tk}, Mutate{0, col}});
2✔
533
    expect(db, obs, [&](auto& tr) {
2✔
534
        auto table1 = tr.get_table(tk);
2✔
535
        auto table2 = tr.get_table(tk2);
2✔
536
        auto obj1 = table1->get_object(0);
2✔
537
        auto obj2 = table2->create_object();
2✔
538
        CHECK_EQUAL(obj1.get_key(), obj2.get_key());
2✔
539
        obj1.set_all(1);
2✔
540
        obj2.set_all(1);
2✔
541
    });
2✔
542

543
    // Mutating an object whose Table has an index in group greater than the
544
    // higest of any created object after creating an object, which has to clear
545
    // the is-new-object flag
546
    obs = RecordingObserver(test_context, {Select{tk}, Create{4}, Select{tk2}, Mutate{0, col2}});
2✔
547
    expect(db, obs, [&](auto& tr) {
2✔
548
        auto table1 = tr.get_table(tk);
2✔
549
        auto table2 = tr.get_table(tk2);
2✔
550
        auto obj1 = table1->create_object();
2✔
551
        auto obj2 = table2->get_object(0);
2✔
552
        obj1.set_all(1);
2✔
553
        obj2.set_all(1);
2✔
554
    });
2✔
555

556
    // Splitting object creation and mutation over two different writes with the
557
    // same transaction object should produce mutation instructions
558
    obs = RecordingObserver(test_context, {Select{tk}, Create{5}, Select{tk}, Mutate{5, col}});
2✔
559
    {
2✔
560
        auto read = db->start_read();
2✔
561
        auto tr = db->start_write();
2✔
562
        auto table = tr->get_table(tk);
2✔
563
        auto obj = table->create_object(); // select tk
2✔
564
        tr->commit_and_continue_as_read();
2✔
565
        tr->promote_to_write();
2✔
566
        obj.set_all(1); // select tk
2✔
567
        tr->commit_and_continue_as_read();
2✔
568
        read->advance_read(&obs);
2✔
569
        obs.check();
2✔
570
    }
2✔
571
}
2✔
572

573
TEST(Replication_MutationsOnNewlyCreatedObject_Link)
574
{
2✔
575
    SHARED_GROUP_TEST_PATH(path);
2✔
576
    auto db = DB::create(make_in_realm_history(), path);
2✔
577
    auto tr = db->start_write();
2✔
578

579
    auto target_table = tr->add_table("target table");
2✔
580
    auto tk_target = target_table->get_key();
2✔
581
    auto ck_target_value = target_table->add_column(type_Int, "value");
2✔
582
    auto embedded_table = tr->add_table("embedded table", Table::Type::Embedded);
2✔
583
    embedded_table->add_column(type_Int, "value");
2✔
584

585
    auto table = tr->add_table("table");
2✔
586
    auto tk = table->get_key();
2✔
587
    ColKey ck_link_1 = table->add_column(*target_table, "link 1");
2✔
588
    ColKey ck_link_2 = table->add_column(*target_table, "link 2");
2✔
589
    ColKey ck_embedded_1 = table->add_column(*embedded_table, "embedded 1");
2✔
590
    ColKey ck_embedded_2 = table->add_column(*embedded_table, "embedded 2");
2✔
591
    tr->commit();
2✔
592

593
    // Each top-level object creation is reported along with the mutation on
594
    // target_1 due to that both target objects are created before the mutations.
595
    // Nothing is reported for embedded objects
596
    auto obs = RecordingObserver(
2✔
597
        test_context, {Select{tk}, Create{0}, Select{tk_target}, Create{0}, Create{1}, Mutate{0, ck_target_value}});
2✔
598
    expect(db, obs, [&](auto& tr) {
2✔
599
        auto table = tr.get_table(tk);
2✔
600
        auto target_table = tr.get_table(tk_target);
2✔
601
        Obj obj = table->create_object();             // select tk
2✔
602
        Obj target_1 = target_table->create_object(); // select tk_target
2✔
603
        Obj target_2 = target_table->create_object();
2✔
604

605
        obj.set(ck_link_1, target_1.get_key()); // select tk
2✔
606
        obj.set(ck_link_2, target_2.get_key());
2✔
607
        target_1.set_all(1); // select tk_target
2✔
608
        target_2.set_all(1);
2✔
609

610
        obj.create_and_set_linked_object(ck_embedded_1).set_all(1);
2✔
611
        obj.create_and_set_linked_object(ck_embedded_2).set_all(1);
2✔
612
    });
2✔
613

614
    // Nullifying links via object deletions in both new and pre-existing objects
615
    // only reports the mutation in the pre-existing object
616
    obs =
2✔
617
        RecordingObserver(test_context, {Select{tk}, Create{1}, Mutate{0, ck_link_1}, Select{tk_target}, Remove{0}});
2✔
618
    expect(db, obs, [&](auto& tr) {
2✔
619
        auto table = tr.get_table(tk);
2✔
620
        auto target_table = tr.get_table(tk_target);
2✔
621
        Obj obj = table->create_object();
2✔
622
        obj.set(ck_link_1, target_table->get_object(0).get_key());
2✔
623
        obj.set(ck_link_2, target_table->get_object(1).get_key());
2✔
624

625
        target_table->get_object(0).remove();
2✔
626
    });
2✔
627
}
2✔
628

629
TEST(Replication_MutationsOnNewlyCreatedObject_Collections)
630
{
2✔
631
    SHARED_GROUP_TEST_PATH(path);
2✔
632
    auto db = DB::create(make_in_realm_history(), path);
2✔
633
    auto tr = db->start_write();
2✔
634

635
    auto table = tr->add_table("table");
2✔
636
    auto tk = table->get_key();
2✔
637
    ColKey ck_value = table->add_column(type_Int, "value");
2✔
638
    ColKey ck_value_set = table->add_column_set(type_Int, "value set");
2✔
639
    ColKey ck_value_list = table->add_column_list(type_Int, "value list");
2✔
640
    ColKey ck_value_dictionary = table->add_column_dictionary(type_Int, "value dictionary");
2✔
641

642
    auto target_table = tr->add_table("target table");
2✔
643
    auto tk_target = target_table->get_key();
2✔
644
    auto ck_target_value = target_table->add_column(type_Int, "value");
2✔
645
    ColKey ck_obj_set = table->add_column_set(*target_table, "obj set");
2✔
646
    ColKey ck_obj_list = table->add_column_list(*target_table, "obj list");
2✔
647
    ColKey ck_obj_dictionary = table->add_column_dictionary(*target_table, "obj dictionary");
2✔
648

649
    auto embedded_table = tr->add_table("embedded table", Table::Type::Embedded);
2✔
650
    auto ck_embedded_value = embedded_table->add_column(type_Int, "value");
2✔
651
    ColKey ck_embedded_list = table->add_column_list(*embedded_table, "embedded list");
2✔
652
    ColKey ck_embedded_dictionary = table->add_column_dictionary(*embedded_table, "embedded dictionary");
2✔
653

654
    tr->commit();
2✔
655

656
    auto obs = RecordingObserver(test_context, {Select{tk}, Create{0}, Select{tk_target}, Create{0}});
2✔
657
    expect(db, obs, [&](auto& tr) {
2✔
658
        // Should report object creation but none of these mutations
659
        auto table = tr.get_table(tk);
2✔
660
        Obj obj = table->create_object();
2✔
661
        obj.set<int64_t>(ck_value, 1);
2✔
662
        obj.get_set<int64_t>(ck_value_set).insert(1);
2✔
663
        obj.get_list<int64_t>(ck_value_list).add(1);
2✔
664
        obj.get_dictionary(ck_value_dictionary).insert("a", 1);
2✔
665

666
        // Should report the object creation but not the mutations on either object,
667
        // as they're both the most recently created object in each table
668
        auto target_table = tr.get_table(tk_target);
2✔
669
        Obj target_obj = target_table->create_object();
2✔
670
        target_obj.set<int64_t>(ck_target_value, 1);
2✔
671
        obj.get_linkset(ck_obj_set).insert(target_obj.get_key());
2✔
672
        obj.get_linklist(ck_obj_list).add(target_obj.get_key());
2✔
673
        obj.get_dictionary(ck_obj_dictionary).insert("a", target_obj.get_key());
2✔
674

675
        // Should not produce any instructions: embedded object creations aren't
676
        // replicated (as you can't observe embedded tables directly), and the
677
        // mutations are on the newest object for each table
678
        obj.get_linklist(ck_embedded_list).create_and_insert_linked_object(0).set(ck_embedded_value, 1);
2✔
679
        obj.get_dictionary(ck_embedded_dictionary).create_and_insert_linked_object("a").set(ck_embedded_value, 1);
2✔
680
    });
2✔
681

682
    obs = RecordingObserver(test_context, {Select{tk},
2✔
683
                                           SelectColl{0, ck_value_set},
2✔
684
                                           CollInsert{1},
2✔
685
                                           SelectColl{0, ck_value_list},
2✔
686
                                           CollInsert{1},
2✔
687
                                           CollInsert{2},
2✔
688
                                           SelectColl{0, ck_value_dictionary},
2✔
689
                                           CollSet{0},
2✔
690
                                           CollInsert{1},
2✔
691
                                           Select{tk_target},
2✔
692
                                           Create{1},
2✔
693
                                           Select{tk},
2✔
694
                                           SelectColl{0, ck_obj_set},
2✔
695
                                           CollInsert{1},
2✔
696
                                           SelectColl{0, ck_obj_list},
2✔
697
                                           CollInsert{1},
2✔
698
                                           SelectColl{0, ck_obj_dictionary},
2✔
699
                                           CollSet{0},
2✔
700
                                           SelectColl{0, ck_embedded_list},
2✔
701
                                           CollInsert{0},
2✔
702
                                           SelectColl{0, ck_embedded_dictionary},
2✔
703
                                           CollInsert{1}});
2✔
704
    expect(db, obs, [&](auto& tr) {
2✔
705
        // Should report mutations on this existing object
706
        auto table = tr.get_table(tk);
2✔
707
        Obj obj = table->get_object(0);
2✔
708
        obj.get_set<int64_t>(ck_value_set).insert(5);
2✔
709
        obj.get_list<int64_t>(ck_value_list).add(1);
2✔
710
        obj.get_list<int64_t>(ck_value_list).add(2);
2✔
711
        obj.get_dictionary(ck_value_dictionary).insert("a", 1);
2✔
712
        obj.get_dictionary(ck_value_dictionary).insert("b", 2);
2✔
713

714
        // Should report the object creation and the mutations on each collection
715
        auto target_table = tr.get_table(tk_target);
2✔
716
        Obj target_obj = target_table->create_object();
2✔
717
        target_obj.set<int64_t>(ck_target_value, 2); // mutation skipped for this new object
2✔
718
        obj.get_linkset(ck_obj_set).insert(target_obj.get_key());
2✔
719
        obj.get_linklist(ck_obj_list).add(target_obj.get_key());
2✔
720
        obj.get_dictionary(ck_obj_dictionary).insert("a", target_obj.get_key());
2✔
721

722
        // Should not produce any instructions for the embedded objects created,
723
        // just mutations on the obj which is not newly created.
724
        obj.get_linklist(ck_embedded_list).create_and_insert_linked_object(0).set(ck_embedded_value, 1);
2✔
725
        obj.get_dictionary(ck_embedded_dictionary).create_and_insert_linked_object("b").set(ck_embedded_value, 1);
2✔
726
    });
2✔
727
}
2✔
728

729
TEST(Replication_NoSelectTableOnEmbeddedObjectMutations)
730
{
2✔
731
    SHARED_GROUP_TEST_PATH(path);
2✔
732
    auto db = DB::create(make_in_realm_history(), path);
2✔
733
    auto tr = db->start_write();
2✔
734

735
    auto table = tr->add_table("table");
2✔
736
    auto tk = table->get_key();
2✔
737
    ColKey ck_value = table->add_column(type_Int, "value");
2✔
738

739
    auto target_table = tr->add_table("target table");
2✔
740
    auto tk_target = target_table->get_key();
2✔
741
    auto ck_target_value = target_table->add_column(type_Int, "value");
2✔
742

743
    auto embedded_table = tr->add_table("embedded table", Table::Type::Embedded);
2✔
744
    auto tk_embedded = embedded_table->get_key();
2✔
745
    auto ck_embedded_value = embedded_table->add_column(type_Int, "value");
2✔
746
    auto embedded_table2 = tr->add_table("embedded_table2", Table::Type::Embedded);
2✔
747
    auto tk_embedded2 = embedded_table2->get_key();
2✔
748
    auto ck_embedded2_str = embedded_table2->add_column(type_String, "value_str");
2✔
749
    auto ck_embedded_link = embedded_table->add_column(*embedded_table2, "embed");
2✔
750
    ColKey ck_embedded_dictionary = table->add_column_dictionary(*embedded_table, "embedded dictionary");
2✔
751

752
    tr->commit();
2✔
753

754
    auto obs = RecordingObserver(test_context, {Select{tk}, Create{0}, Select{tk_target}, Create{0}, Create{1}});
2✔
755
    expect(db, obs, [&](auto& tr) {
2✔
756
        // Should report object creation but none of these mutations
757
        auto table = tr.get_table(tk);
2✔
758
        Obj obj = table->create_object();
2✔
759
        obj.set<int64_t>(ck_value, 1);
2✔
760

761
        // Should report the object creation but not the mutations on either object,
762
        // as they're both the most recently created object in each table
763
        auto target_table = tr.get_table(tk_target);
2✔
764
        Obj target_obj = target_table->create_object(); // select tk_target
2✔
765
        target_obj.set<int64_t>(ck_target_value, 1);
2✔
766

767
        // Should not produce any instructions: embedded object creations aren't
768
        // replicated (as you can't observe embedded tables directly), and the
769
        // mutations are on the newest object for each table
770
        Obj embedded_a = obj.get_dictionary(ck_embedded_dictionary).create_and_insert_linked_object("a");
2✔
771
        embedded_a.set(ck_embedded_value, 1);
2✔
772
        Obj embedded2_a = embedded_a.create_and_set_linked_object(ck_embedded_link);
2✔
773
        embedded2_a.set(ck_embedded2_str, "test");
2✔
774

775
        // target_table should still be selected, because there should have been
776
        // no select table emitted for the embedded objects above
777
        Obj target_obj2 = target_table->create_object();
2✔
778
        target_obj2.set<int64_t>(ck_target_value, 2);
2✔
779
    });
2✔
780

781
    obs = RecordingObserver(test_context, {Select{tk}, Create{1}, Select{tk_embedded}, Mutate{1, ck_embedded_value},
2✔
782
                                           Select{tk_embedded2}, Mutate{1, ck_embedded2_str}});
2✔
783
    expect(db, obs, [&](auto& tr) {
2✔
784
        // Should report object creation but none of these mutations
785
        auto table = tr.get_table(tk);
2✔
786
        Obj obj = table->create_object(); // select tk
2✔
787
        obj.set<int64_t>(ck_value, 1);
2✔
788

789
        // Should not produce any instructions: embedded object creations aren't
790
        // replicated (as you can't observe embedded tables directly), and the
791
        // mutations are on the newest object for each table
792
        Obj embedded_a = obj.get_dictionary(ck_embedded_dictionary).create_and_insert_linked_object("a");
2✔
793
        embedded_a.set(ck_embedded_value, 1);
2✔
794
        Obj embedded2_a = embedded_a.create_and_set_linked_object(ck_embedded_link);
2✔
795
        embedded2_a.set(ck_embedded2_str, "test a");
2✔
796

797
        Obj embedded_b = obj.get_dictionary(ck_embedded_dictionary).create_and_insert_linked_object("b");
2✔
798
        embedded_b.set(ck_embedded_value, 2);
2✔
799
        Obj embedded2_b = embedded_b.create_and_set_linked_object(ck_embedded_link);
2✔
800
        embedded2_b.set(ck_embedded2_str, "test b");
2✔
801

802
        // setting a property on embeded_a means that it is no longer the newest object
803
        // created, so we require a set_table, and modification for each modify:
804
        // select "embedded table"
805
        // modify embedded_a
806
        // select "embedded table 2"
807
        // modify embedded2_a
808
        embedded_a.set(ck_embedded_value, 3);
2✔
809
        embedded2_a.set(ck_embedded2_str, "test a 2");
2✔
810
    });
2✔
811
}
2✔
812

813
TEST(Replication_EmbeddedListInsertions)
814
{
2✔
815
    SHARED_GROUP_TEST_PATH(path);
2✔
816
    auto db = DB::create(make_in_realm_history(), path);
2✔
817
    auto tr = db->start_write();
2✔
818

819
    auto table = tr->add_table("table");
2✔
820
    auto tk = table->get_key();
2✔
821
    ColKey ck_value = table->add_column(type_Int, "value");
2✔
822

823
    auto embedded_table = tr->add_table("embedded table", Table::Type::Embedded);
2✔
824
    auto tk_embedded = embedded_table->get_key();
2✔
825
    auto ck_embedded_value = embedded_table->add_column(type_Int, "value");
2✔
826
    ColKey ck_embedded_list = table->add_column_list(*embedded_table, "embedded list");
2✔
827

828
    // initial state
829
    int64_t obj_key;
2✔
830
    {
2✔
831
        auto table = tr->get_table(tk);
2✔
832
        Obj obj = table->create_object();
2✔
833
        obj.set<int64_t>(ck_value, 1);
2✔
834
        obj_key = obj.get_key().value;
2✔
835
    }
2✔
836

837
    tr->commit();
2✔
838

839
    auto obs = RecordingObserver(test_context, {Select{tk}, SelectColl{obj_key, ck_embedded_list}, CollInsert{0},
2✔
840
                                                CollInsert{1}, Select{tk_embedded}, Mutate{0, ck_embedded_value}});
2✔
841
    expect(db, obs, [&](auto& tr) {
2✔
842
        auto table = tr.get_table(tk);
2✔
843
        Obj obj = table->get_object(0);
2✔
844
        LnkLst list = obj.get_linklist(ck_embedded_list);
2✔
845
        Obj link_0 = list.create_and_insert_linked_object(0);
2✔
846
        Obj link_1 = list.create_and_insert_linked_object(1);
2✔
847

848
        // both of these were just created, but only one is the "recently created"
849
        // so we do record one unnecessary mutation
850
        link_0.set<int64_t>(ck_embedded_value, 10);
2✔
851
        link_1.set<int64_t>(ck_embedded_value, 11);
2✔
852
    });
2✔
853
}
2✔
854

855
TEST(Replication_EmbeddedListInsertionsWithListMutations)
856
{
2✔
857
    SHARED_GROUP_TEST_PATH(path);
2✔
858
    auto db = DB::create(make_in_realm_history(), path);
2✔
859
    auto tr = db->start_write();
2✔
860

861
    auto table = tr->add_table("table");
2✔
862
    auto tk = table->get_key();
2✔
863
    ColKey ck_value = table->add_column(type_Int, "value");
2✔
864

865
    auto embedded_table = tr->add_table("embedded table", Table::Type::Embedded);
2✔
866
    auto tk_embedded = embedded_table->get_key();
2✔
867
    ColKey ck_embedded_list_of_ints = embedded_table->add_column_list(type_Int, "int list");
2✔
868
    ColKey ck_list_of_embeddeds = table->add_column_list(*embedded_table, "embedded list");
2✔
869

870
    // initial state
871
    int64_t obj_key;
2✔
872
    {
2✔
873
        auto table = tr->get_table(tk);
2✔
874
        Obj obj = table->create_object();
2✔
875
        obj.set<int64_t>(ck_value, 1);
2✔
876
        obj_key = obj.get_key().value;
2✔
877
    }
2✔
878

879
    tr->commit();
2✔
880

881
    // there should be no extra select collection on ck_list_of_embeddeds between insertions,
882
    // even though there are modifications to the embedded lists between insertions
883
    auto obs = RecordingObserver(
2✔
884
        test_context, {Select{tk}, SelectColl{obj_key, ck_list_of_embeddeds}, CollInsert{0}, CollInsert{1}});
2✔
885
    expect(db, obs, [&](auto& tr) {
2✔
886
        auto table = tr.get_table(tk);
2✔
887
        Obj obj = table->get_object(0);
2✔
888
        LnkLst list = obj.get_linklist(ck_list_of_embeddeds);
2✔
889
        Obj link_0 = list.create_and_insert_linked_object(0);
2✔
890
        link_0.get_list<Int>(ck_embedded_list_of_ints).insert(0, 0);
2✔
891
        link_0.get_list<Int>(ck_embedded_list_of_ints).insert(1, 1);
2✔
892
        Obj link_1 = list.create_and_insert_linked_object(1);
2✔
893
        link_1.get_list<Int>(ck_embedded_list_of_ints).insert(0, 10);
2✔
894
        link_1.get_list<Int>(ck_embedded_list_of_ints).insert(1, 11);
2✔
895
    });
2✔
896

897
    // modifications on an existing embedded object should make selections on the collection
898
    obs = RecordingObserver(test_context,
2✔
899
                            {Select{tk_embedded}, SelectColl{0, ck_embedded_list_of_ints}, CollInsert{0}, Select{tk},
2✔
900
                             SelectColl{obj_key, ck_list_of_embeddeds}, CollInsert{2}, Select{tk_embedded},
2✔
901
                             SelectColl{1, ck_embedded_list_of_ints}, CollInsert{0}});
2✔
902
    expect(db, obs, [&](auto& tr) {
2✔
903
        auto table = tr.get_table(tk);
2✔
904
        Obj obj = table->get_object(0);
2✔
905
        LnkLst list = obj.get_linklist(ck_list_of_embeddeds);
2✔
906
        CHECK_EQUAL(list.size(), 2);
2✔
907
        Obj link_0 = list.get_object(0);
2✔
908
        // select embedded table, select embedded collection, collection insert
909
        link_0.get_list<Int>(ck_embedded_list_of_ints).insert(0, 100);
2✔
910

911
        // select top table, select top collection, collection insert
912
        Obj link_2 = list.create_and_insert_linked_object(2);
2✔
913
        link_2.get_list<Int>(ck_embedded_list_of_ints).insert(0, 0);
2✔
914

915
        // select embedded table, select embedded collection, collection insert
916
        Obj link_1 = list.get_object(1);
2✔
917
        link_1.get_list<Int>(ck_embedded_list_of_ints).insert(0, 1000);
2✔
918
    });
2✔
919
}
2✔
920

921
} // anonymous namespace
922

923
#endif // TEST_REPLICATION
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

© 2025 Coveralls, Inc