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

realm / realm-core / 2292

02 May 2024 08:09PM UTC coverage: 90.76% (+0.01%) from 90.747%
2292

push

Evergreen

web-flow
Fix a deadlock when accessing current user from inside an App listener (#7671)

App::switch_user() emitted changes without first releasing the lock on
m_user_mutex, leading to a deadlock if anyone inside the listener tried to
acquire the mutex. The rest of the places where we emitted changes were
correct.

The newly added wrapper catches this error when building with clang.

101984 of 180246 branches covered (56.58%)

14 of 17 new or added lines in 2 files covered. (82.35%)

45 existing lines in 16 files now uncovered.

212565 of 234206 relevant lines covered (90.76%)

5840998.47 hits per line

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

49.82
/test/util/compare_groups.cpp
1
#include "compare_groups.hpp"
2

3
#include <algorithm>
4
#include <vector>
5
#include <set>
6
#include <sstream>
7
#include <iostream>
8

9
#include <realm/table.hpp>
10
#include <realm/sync/object_id.hpp>
11
#include <realm/list.hpp>
12
#include <realm/dictionary.hpp>
13
#include <realm/set.hpp>
14

15
using namespace realm;
16

17
namespace {
18

19

20
class TableCompareLogger : public util::Logger {
21
public:
22
    TableCompareLogger(StringData table_name, util::Logger& base_logger) noexcept
23
        : util::Logger()
579✔
24
        , m_table_name{table_name}
579✔
25
        , m_base_logger{base_logger}
579✔
26
    {
1,162✔
27
        set_level_threshold(base_logger.get_level_threshold());
1,162✔
28
    }
1,162✔
29
    void do_log(const util::LogCategory& category, Level level, const std::string& message) override final
30
    {
×
31
        ensure_prefix();                                                    // Throws
×
32
        Logger::do_log(m_base_logger, category, level, m_prefix + message); // Throws
×
33
    }
×
34

35
private:
36
    const StringData m_table_name;
37
    util::Logger& m_base_logger;
38
    std::string m_prefix;
39
    void ensure_prefix()
40
    {
×
41
        if (REALM_LIKELY(!m_prefix.empty()))
×
42
            return;
×
43
        std::ostringstream out;
×
44
        out << "Table[" << m_table_name << "]: "; // Throws
×
45
        m_prefix = out.str();                     // Throws
×
46
    }
×
47
};
48

49

50
class ObjectCompareLogger : public util::Logger {
51
public:
52
    ObjectCompareLogger(Mixed pk, util::Logger& base_logger) noexcept
53
        : util::Logger()
3,606✔
54
        , m_pk{pk}
3,606✔
55
        , m_base_logger{base_logger}
3,606✔
56
    {
7,252✔
57
        set_level_threshold(base_logger.get_level_threshold());
7,252✔
58
    }
7,252✔
59
    void do_log(const util::LogCategory& category, Level level, const std::string& message) override final
60
    {
×
61
        ensure_prefix();                                                    // Throws
×
62
        Logger::do_log(m_base_logger, category, level, m_prefix + message); // Throws
×
63
    }
×
64

65
private:
66
    const Mixed m_pk;
67
    util::Logger& m_base_logger;
68
    std::string m_prefix;
69
    void ensure_prefix()
70
    {
×
71
        if (REALM_LIKELY(!m_prefix.empty()))
×
72
            return;
×
73
        std::ostringstream out;
×
74
        out << m_pk << ": ";  // Throws
×
75
        m_prefix = out.str(); // Throws
×
76
    }
×
77
};
78

79

80
template <class T, class Cmp = std::equal_to<>>
81
bool compare_arrays(T& a, T& b, Cmp equals = Cmp{})
82
{
356✔
83
    auto a_it = a.begin();
356✔
84
    auto b_it = b.begin();
356✔
85
    if (a.size() != b.size()) {
356!
86
#if REALM_DEBUG
×
87
        std::cerr << "LEFT size: " << a.size() << std::endl;
×
88
        std::cerr << "RIGHT size: " << b.size() << std::endl;
×
89
#endif // REALM_DEBUG
×
90
        return false;
×
91
    }
×
92

93
    // Compare entries
94
    for (; a_it != a.end(); ++a_it, ++b_it) {
854!
95
        if (!equals(*a_it, *b_it))
498!
96
            goto different;
×
97
    }
498✔
98

99
    return true;
356✔
100
different:
×
101
#if REALM_DEBUG
×
102
    std::cerr << "LEFT: " << *a_it << std::endl;
×
103
    std::cerr << "RIGHT: " << *b_it << std::endl;
×
104
#endif // REALM_DEBUG
×
105
    return false;
×
106
}
356✔
107

108
template <class T>
109
bool compare_set_values(const Set<T>& a, const Set<T>& b)
110
{
26✔
111
    return compare_arrays(a, b);
26✔
112
}
26✔
113

114
bool compare_dictionaries(const Dictionary& a, const Dictionary& b)
115
{
34✔
116
    auto a_it = a.begin();
34✔
117
    auto b_it = b.begin();
34✔
118
    if (a.size() != b.size()) {
34✔
119
#if REALM_DEBUG
×
120
        std::cerr << "LEFT size: " << a.size() << std::endl;
×
121
        std::cerr << "RIGHT size: " << b.size() << std::endl;
×
122
#endif
×
123
        return false;
×
124
    }
×
125

126
    // Compare entries
127
    for (; a_it != a.end(); ++a_it, ++b_it) {
186✔
128
        if (*a_it != *b_it)
152✔
129
            goto different;
×
130
    }
152✔
131

132
    return true;
34✔
133

134
different:
×
135
#if REALM_DEBUG
×
136
    std::cerr << "LEFT: " << (*a_it).first << " => " << (*a_it).second << std::endl;
×
137
    std::cerr << "RIGHT: " << (*b_it).first << " => " << (*b_it).second << std::endl;
×
138
#endif
×
139
    return false;
×
140
}
34✔
141

142
struct Column {
143
    StringData name;
144
    ColKey key_1, key_2;
145

146
    DataType get_type() const noexcept
147
    {
22,888✔
148
        return DataType(key_1.get_type());
22,888✔
149
    }
22,888✔
150

151
    bool is_list() const noexcept
152
    {
22,858✔
153
        return key_1.is_list();
22,858✔
154
    }
22,858✔
155

156
    bool is_dictionary() const noexcept
157
    {
22,922✔
158
        return key_1.is_dictionary();
22,922✔
159
    }
22,922✔
160

161
    bool is_set() const noexcept
162
    {
22,888✔
163
        return key_1.is_set();
22,888✔
164
    }
22,888✔
165

166
    bool is_nullable() const noexcept
167
    {
23,348✔
168
        return key_1.is_nullable();
23,348✔
169
    }
23,348✔
170
};
171

172
ObjKey row_for_primary_key(const Table& table, Mixed pk)
173
{
14,504✔
174
    ColKey pk_col = table.get_primary_key_column();
14,504✔
175
    if (pk_col) {
14,504✔
176
        return table.find_primary_key(pk); // will assert on type mismatch
14,504✔
177
    }
14,504✔
178

179
    if (pk.is_type(type_Link, type_TypedLink)) {
×
180
        return pk.get<ObjKey>();
×
181
    }
×
182
    else {
×
183
        REALM_TERMINATE("row_for_primary_key() with primary key, expected GlobalKey");
184
    }
×
185
    return {};
×
186
}
×
187

188
bool compare_objects(const Obj& obj_1, const Obj& obj_2, const std::vector<Column>& columns, util::Logger& logger);
189
bool compare_objects(Mixed& oid, const Table& table_1, const Table& table_2, const std::vector<Column>& columns,
190
                     util::Logger& logger);
191

192
bool compare_schemas(const Table& table_1, const Table& table_2, util::Logger& logger,
193
                     std::vector<Column>* out_columns = nullptr)
194
{
1,849✔
195
    bool equal = true;
1,849✔
196

197
    // Compare column names
198
    {
1,849✔
199
        auto col_keys = table_1.get_column_keys();
1,849✔
200
        for (auto key : col_keys) {
3,940✔
201
            StringData name = table_1.get_column_name(key);
3,940✔
202
            if (!table_2.get_column_key(name)) {
3,940✔
203
                logger.error("Column '%1' not found in right-hand side table", name);
×
204
                equal = false;
×
205
            }
×
206
        }
3,940✔
207
    }
1,849✔
208
    {
1,849✔
209
        auto col_keys = table_2.get_column_keys();
1,849✔
210
        for (auto key : col_keys) {
3,942✔
211
            StringData name = table_2.get_column_name(key);
3,942✔
212
            if (!table_1.get_column_key(name)) {
3,942✔
213
                logger.error("Column '%1' not found in left-hand side table", name);
×
214
                equal = false;
×
215
            }
×
216
        }
3,942✔
217
    }
1,849✔
218

219
    // Compare column signatures
220
    {
1,849✔
221
        auto keys_1 = table_1.get_column_keys();
1,849✔
222
        for (auto key_1 : keys_1) {
3,942✔
223
            StringData name = table_1.get_column_name(key_1);
3,942✔
224
            ColKey key_2 = table_2.get_column_key(name);
3,942✔
225
            if (!key_2)
3,942✔
226
                continue;
×
227
            DataType type_1 = table_1.get_column_type(key_1);
3,942✔
228
            DataType type_2 = table_2.get_column_type(key_2);
3,942✔
229
            if (type_1 != type_2) {
3,942✔
230
                logger.error("Type mismatch on column '%1'", name);
×
231
                equal = false;
×
232
                continue;
×
233
            }
×
234
            bool nullable_1 = table_1.is_nullable(key_1);
3,942✔
235
            bool nullable_2 = table_2.is_nullable(key_2);
3,942✔
236
            if (nullable_1 != nullable_2) {
3,942✔
237
                logger.error("Nullability mismatch on column '%1'", name);
×
238
                equal = false;
×
239
                continue;
×
240
            }
×
241
            bool is_list_1 = table_1.is_list(key_1);
3,942✔
242
            bool is_list_2 = table_2.is_list(key_2);
3,942✔
243
            if (is_list_1 != is_list_2) {
3,942✔
244
                logger.error("List type mismatch on column '%1'", name);
×
245
                equal = false;
×
246
                continue;
×
247
            }
×
248
            bool is_dictionary_1 = key_1.is_dictionary();
3,942✔
249
            bool is_dictionary_2 = key_2.is_dictionary();
3,942✔
250
            if (is_dictionary_1 != is_dictionary_2) {
3,942✔
251
                logger.error("Dictionary type mismatch on column '%1'", name);
×
252
                equal = false;
×
253
                continue;
×
254
            }
×
255
            bool is_set_1 = key_1.is_set();
3,942✔
256
            bool is_set_2 = key_2.is_set();
3,942✔
257
            if (is_set_1 != is_set_2) {
3,942✔
258
                logger.error("Set type mismatch on column '%1'", name);
×
259
                equal = false;
×
260
                continue;
×
261
            }
×
262
            if (type_1 == type_Link) {
3,942✔
263
                ConstTableRef target_1 = table_1.get_link_target(key_1);
838✔
264
                ConstTableRef target_2 = table_2.get_link_target(key_2);
838✔
265
                if (target_1->get_name() != target_2->get_name()) {
838✔
266
                    logger.error("Link target mismatch on column '%1'", name);
×
267
                    equal = false;
×
268
                    continue;
×
269
                }
×
270
            }
838✔
271
            if (out_columns)
3,942✔
272
                out_columns->push_back(Column{name, key_1, key_2});
3,942✔
273
        }
3,942✔
274
    }
1,849✔
275

276
    return equal;
1,849✔
277
}
1,849✔
278

279
bool compare_lists(const Column& col, const Obj& obj_1, const Obj& obj_2, util::Logger& logger)
280
{
1,022✔
281
    switch (col.get_type()) {
1,022✔
282
        case type_Int: {
148✔
283
            if (col.is_nullable()) {
148✔
284
                auto a = obj_1.get_list<util::Optional<int64_t>>(col.key_1);
×
285
                auto b = obj_2.get_list<util::Optional<int64_t>>(col.key_2);
×
286
                if (!compare_arrays(a, b)) {
×
287
                    logger.error("List mismatch in column '%1'", col.name);
×
288
                    return false;
×
289
                }
×
290
            }
×
291
            else {
148✔
292
                auto a = obj_1.get_list<int64_t>(col.key_1);
148✔
293
                auto b = obj_2.get_list<int64_t>(col.key_2);
148✔
294
                if (!compare_arrays(a, b)) {
148✔
295
                    logger.error("List mismatch in column '%1'", col.name);
×
296
                    return false;
×
297
                }
×
298
            }
148✔
299
            break;
148✔
300
        }
148✔
301
        case type_Bool: {
148✔
302
            auto a = obj_1.get_list<bool>(col.key_1);
×
303
            auto b = obj_2.get_list<bool>(col.key_2);
×
304
            if (!compare_arrays(a, b)) {
×
305
                logger.error("List mismatch in column '%1'", col.name);
×
306
                return false;
×
307
            }
×
308
            break;
×
309
        }
×
310
        case type_String: {
182✔
311
            auto a = obj_1.get_list<String>(col.key_1);
182✔
312
            auto b = obj_2.get_list<String>(col.key_2);
182✔
313
            if (!compare_arrays(a, b)) {
182✔
314
                logger.error("List mismatch in column '%1'", col.name);
×
315
                return false;
×
316
            }
×
317
            break;
182✔
318
        }
182✔
319
        case type_Binary: {
182✔
320
            auto a = obj_1.get_list<Binary>(col.key_1);
×
321
            auto b = obj_2.get_list<Binary>(col.key_2);
×
322
            if (!compare_arrays(a, b)) {
×
323
                logger.error("List mismatch in column '%1'", col.name);
×
324
                return false;
×
325
            }
×
326
            break;
×
327
        }
×
328
        case type_Float: {
✔
329
            auto a = obj_1.get_list<float>(col.key_1);
×
330
            auto b = obj_2.get_list<float>(col.key_2);
×
331
            if (!compare_arrays(a, b)) {
×
332
                logger.error("List mismatch in column '%1'", col.name);
×
333
                return false;
×
334
            }
×
335
            break;
×
336
        }
×
337
        case type_Double: {
✔
338
            auto a = obj_1.get_list<double>(col.key_1);
×
339
            auto b = obj_2.get_list<double>(col.key_2);
×
340
            if (!compare_arrays(a, b)) {
×
341
                logger.error("List mismatch in column '%1'", col.name);
×
342
                return false;
×
343
            }
×
344
            break;
×
345
        }
×
346
        case type_Timestamp: {
✔
347
            auto a = obj_1.get_list<Timestamp>(col.key_1);
×
348
            auto b = obj_2.get_list<Timestamp>(col.key_2);
×
349
            if (!compare_arrays(a, b)) {
×
350
                logger.error("List mismatch in column '%1'", col.name);
×
351
                return false;
×
352
            }
×
353
            break;
×
354
        }
×
355
        case type_ObjectId: {
✔
356
            auto a = obj_1.get_list<ObjectId>(col.key_1);
×
357
            auto b = obj_2.get_list<ObjectId>(col.key_2);
×
358
            if (!compare_arrays(a, b)) {
×
359
                logger.error("List mismatch in column '%1'", col.name);
×
360
                return false;
×
361
            }
×
362
            break;
×
363
        }
×
364
        case type_UUID: {
✔
365
            auto a = obj_1.get_list<UUID>(col.key_1);
×
366
            auto b = obj_2.get_list<UUID>(col.key_2);
×
367
            if (!compare_arrays(a, b)) {
×
368
                logger.error("List mismatch in column '%1'", col.name);
×
369
                return false;
×
370
            }
×
371
            break;
×
372
        }
×
373
        case type_Decimal: {
✔
374
            auto a = obj_1.get_list<Decimal128>(col.key_1);
×
375
            auto b = obj_2.get_list<Decimal128>(col.key_2);
×
376
            if (!compare_arrays(a, b)) {
×
377
                logger.error("List mismatch in column '%1'", col.name);
×
378
                return false;
×
379
            }
×
380
            break;
×
381
        }
×
382
        case type_Mixed: {
✔
383
            auto a = obj_1.get_list<Mixed>(col.key_1);
×
384
            auto b = obj_2.get_list<Mixed>(col.key_2);
×
385
            if (!compare_arrays(a, b)) {
×
386
                logger.error("List mismatch in column '%1'", col.name);
×
387
                return false;
×
388
            }
×
389
            break;
×
390
        }
×
391
        case type_TypedLink:
✔
392
            // FIXME: Implement
393
            break;
×
394
        case type_Link: {
692✔
395
            auto a = obj_1.get_list<ObjKey>(col.key_1);
692✔
396
            auto b = obj_2.get_list<ObjKey>(col.key_2);
692✔
397
            if (a.size() != b.size()) {
692✔
398
                logger.error("Link list size mismatch in column '%1'", col.name);
×
399
                return false;
×
400
                break;
×
401
            }
×
402
            auto table_1 = obj_1.get_table();
692✔
403
            auto table_2 = obj_2.get_table();
692✔
404
            ConstTableRef target_table_1 = table_1->get_link_target(col.key_1);
692✔
405
            ConstTableRef target_table_2 = table_2->get_link_target(col.key_2);
692✔
406

407
            bool is_embedded = target_table_1->is_embedded();
692✔
408
            std::vector<Column> embedded_columns;
692✔
409
            if (is_embedded) {
692✔
410
                // FIXME: This does the schema comparison for
411
                // embedded tables for every object with embedded
412
                // objects, just because we want to get the Column
413
                // info. Instead compare just the objects
414
                // themselves.
415
                bool schemas_equal = compare_schemas(*target_table_1, *target_table_2, logger, &embedded_columns);
640✔
416
                REALM_ASSERT(schemas_equal);
640✔
417
            }
640✔
418

419
            std::size_t n = a.size();
692✔
420
            for (std::size_t i = 0; i < n; ++i) {
1,448✔
421
                ObjKey link_1 = a.get(i);
756✔
422
                ObjKey link_2 = b.get(i);
756✔
423

424
                if (link_1.is_unresolved() || link_2.is_unresolved()) {
756✔
425
                    // if one link is unresolved, the other should also be unresolved
426
                    if (!link_1.is_unresolved() || !link_2.is_unresolved()) {
22✔
427
                        logger.error("Value mismatch in column '%1' at index %2 of the link "
×
428
                                     "list (%3 vs %4)",
×
429
                                     col.name, i, link_1, link_2);
×
430
                        return false;
×
431
                    }
×
432
                }
22✔
433
                else {
734✔
434
                    if (is_embedded) {
734✔
435
                        const Obj embedded_1 = target_table_1->get_object(link_1);
674✔
436
                        const Obj embedded_2 = target_table_2->get_object(link_2);
674✔
437
                        // Skip ID comparison for embedded objects, because
438
                        // they are only identified by their position in the
439
                        // database.
440
                        if (!compare_objects(embedded_1, embedded_2, embedded_columns, logger)) {
674✔
441
                            logger.error("Embedded object contents mismatch in column '%1'", col.name);
×
442
                            return false;
×
443
                        }
×
444
                    }
674✔
445
                    else {
60✔
446
                        Mixed target_oid_1 = target_table_1->get_primary_key(link_1);
60✔
447
                        Mixed target_oid_2 = target_table_2->get_primary_key(link_2);
60✔
448
                        if (target_oid_1 != target_oid_2) {
60✔
449
                            logger.error("Value mismatch in column '%1' at index %2 of the link "
×
450
                                         "list (%3 vs %4)",
×
451
                                         col.name, i, link_1, link_2);
×
452
                            return false;
×
453
                        }
×
454
                    }
60✔
455
                }
734✔
456
            }
756✔
457
            break;
692✔
458
        }
692✔
459
    }
1,022✔
460

461
    return true;
1,022✔
462
}
1,022✔
463

464
bool compare_sets(const Column& col, const Obj& obj_1, const Obj& obj_2, util::Logger& logger)
465
{
30✔
466
    switch (col.get_type()) {
30✔
467
        case type_Int: {
8✔
468
            if (col.is_nullable()) {
8✔
469
                auto a = obj_1.get_set<util::Optional<int64_t>>(col.key_1);
×
470
                auto b = obj_2.get_set<util::Optional<int64_t>>(col.key_2);
×
471
                if (!compare_set_values(a, b)) {
×
472
                    logger.error("Set mismatch in column '%1'", col.name);
×
473
                    return false;
×
474
                }
×
475
            }
×
476
            else {
8✔
477
                auto a = obj_1.get_set<int64_t>(col.key_1);
8✔
478
                auto b = obj_2.get_set<int64_t>(col.key_2);
8✔
479
                if (!compare_set_values(a, b)) {
8✔
480
                    logger.error("Set mismatch in column '%1'", col.name);
×
481
                    return false;
×
482
                }
×
483
            }
8✔
484
            break;
8✔
485
        }
8✔
486
        case type_Bool: {
8✔
487
            auto a = obj_1.get_set<bool>(col.key_1);
×
488
            auto b = obj_2.get_set<bool>(col.key_2);
×
489
            if (!compare_set_values(a, b)) {
×
490
                logger.error("Set mismatch in column '%1'", col.name);
×
491
                return false;
×
492
            }
×
493
            break;
×
494
        }
×
495
        case type_String: {
4✔
496
            auto a = obj_1.get_set<String>(col.key_1);
4✔
497
            auto b = obj_2.get_set<String>(col.key_2);
4✔
498
            if (!compare_set_values(a, b)) {
4✔
499
                logger.error("Set mismatch in column '%1'", col.name);
×
500
                return false;
×
501
            }
×
502
            break;
4✔
503
        }
4✔
504
        case type_Binary: {
4✔
505
            auto a = obj_1.get_set<Binary>(col.key_1);
×
506
            auto b = obj_2.get_set<Binary>(col.key_2);
×
507
            if (!compare_set_values(a, b)) {
×
508
                logger.error("Set mismatch in column '%1'", col.name);
×
509
                return false;
×
510
            }
×
511
            break;
×
512
        }
×
513
        case type_Float: {
✔
514
            auto a = obj_1.get_set<float>(col.key_1);
×
515
            auto b = obj_2.get_set<float>(col.key_2);
×
516
            if (!compare_set_values(a, b)) {
×
517
                logger.error("Set mismatch in column '%1'", col.name);
×
518
                return false;
×
519
            }
×
520
            break;
×
521
        }
×
522
        case type_Double: {
✔
523
            auto a = obj_1.get_set<double>(col.key_1);
×
524
            auto b = obj_2.get_set<double>(col.key_2);
×
525
            if (!compare_set_values(a, b)) {
×
526
                logger.error("Set mismatch in column '%1'", col.name);
×
527
                return false;
×
528
            }
×
529
            break;
×
530
        }
×
531
        case type_Timestamp: {
✔
532
            auto a = obj_1.get_set<Timestamp>(col.key_1);
×
533
            auto b = obj_2.get_set<Timestamp>(col.key_2);
×
534
            if (!compare_set_values(a, b)) {
×
535
                logger.error("Set mismatch in column '%1'", col.name);
×
536
                return false;
×
537
            }
×
538
            break;
×
539
        }
×
540
        case type_ObjectId: {
✔
541
            auto a = obj_1.get_set<ObjectId>(col.key_1);
×
542
            auto b = obj_2.get_set<ObjectId>(col.key_2);
×
543
            if (!compare_set_values(a, b)) {
×
544
                logger.error("Set mismatch in column '%1'", col.name);
×
545
                return false;
×
546
            }
×
547
            break;
×
548
        }
×
549
        case type_UUID: {
✔
550
            auto a = obj_1.get_set<UUID>(col.key_1);
×
551
            auto b = obj_2.get_set<UUID>(col.key_2);
×
552
            if (!compare_set_values(a, b)) {
×
553
                logger.error("Set mismatch in column '%1'", col.name);
×
554
                return false;
×
555
            }
×
556
            break;
×
557
        }
×
558
        case type_Decimal: {
✔
559
            auto a = obj_1.get_set<Decimal128>(col.key_1);
×
560
            auto b = obj_2.get_set<Decimal128>(col.key_2);
×
561
            if (!compare_set_values(a, b)) {
×
562
                logger.error("Set mismatch in column '%1'", col.name);
×
563
                return false;
×
564
            }
×
565
            break;
×
566
        }
×
567
        case type_Mixed: {
14✔
568
            auto a = obj_1.get_set<Mixed>(col.key_1);
14✔
569
            auto b = obj_2.get_set<Mixed>(col.key_2);
14✔
570
            if (!compare_set_values(a, b)) {
14✔
571
                logger.error("Set mismatch in column '%1'", col.name);
×
572
                return false;
×
573
            }
×
574
            break;
14✔
575
        }
14✔
576
        case type_Link: {
14✔
577
            auto a = obj_1.get_linkset(col.key_1);
4✔
578
            auto b = obj_2.get_linkset(col.key_2);
4✔
579
            if (a.size() != b.size()) {
4✔
580
                logger.error("Link set size mismatch in column '%1'", col.name);
×
581
                return false;
×
582
                break;
×
583
            }
×
584

585
            auto target_table_1 = a.get_target_table();
4✔
586
            auto target_table_2 = b.get_target_table();
4✔
587

588
            std::size_t n = a.size();
4✔
589
            for (std::size_t i = 0; i < n; ++i) {
8✔
590
                ObjKey link_1 = a.get(i);
4✔
591
                ObjKey link_2 = b.get(i);
4✔
592

593
                if (link_1.is_unresolved() || link_2.is_unresolved()) {
4✔
594
                    // if one link is unresolved, the other should also be unresolved
595
                    if (!link_1.is_unresolved() || !link_2.is_unresolved()) {
×
596
                        logger.error("Value mismatch in column '%1' at index %2 of the link "
×
597
                                     "set (%3 vs %4)",
×
598
                                     col.name, i, link_1, link_2);
×
599
                        return false;
×
600
                    }
×
601
                }
×
602
                else {
4✔
603
                    Mixed target_oid_1 = target_table_1->get_primary_key(link_1);
4✔
604
                    Mixed target_oid_2 = target_table_2->get_primary_key(link_2);
4✔
605
                    if (target_oid_1 != target_oid_2) {
4✔
606
                        logger.error("Value mismatch in column '%1' at index %2 of the link "
×
607
                                     "set (%3 vs %4)",
×
608
                                     col.name, i, link_1, link_2);
×
609
                        return false;
×
610
                    }
×
611
                }
4✔
612
            }
4✔
613
            break;
4✔
614
        }
4✔
615
        case type_TypedLink:
4✔
616
            // FIXME: Implement
617
            break;
×
618
    }
30✔
619

620
    return true;
30✔
621
}
30✔
622

623
bool compare_objects(const Obj& obj_1, const Obj& obj_2, const std::vector<Column>& columns, util::Logger& logger)
624
{
7,974✔
625
    bool equal = true;
7,974✔
626
    auto ptable_1 = obj_1.get_table();
7,974✔
627
    auto ptable_2 = obj_2.get_table();
7,974✔
628
    auto& table_1 = *ptable_1;
7,974✔
629
    auto& table_2 = *ptable_2;
7,974✔
630

631
    for (const Column& col : columns) {
23,192✔
632
        if (col.is_nullable()) {
23,192✔
633
            bool a = obj_1.is_null(col.key_1);
734✔
634
            bool b = obj_2.is_null(col.key_2);
734✔
635
            if (a && b)
734✔
636
                continue;
270✔
637
            if (a || b) {
464✔
638
                logger.error("Null/nonnull disagreement in column '%1' (%2 vs %3)", col.name, a, b);
×
639
                equal = false;
×
640
                continue;
×
641
            }
×
642
        }
464✔
643

644
        if (col.is_dictionary()) {
22,922✔
645
            auto a = obj_1.get_dictionary(col.key_1);
34✔
646
            auto b = obj_2.get_dictionary(col.key_2);
34✔
647
            if (!compare_dictionaries(a, b)) {
34✔
648
                logger.error("Dictionary mismatch in column '%1'", col.name);
×
649
                equal = false;
×
650
            }
×
651
            continue;
34✔
652
        }
34✔
653

654
        if (col.is_set()) {
22,888✔
655
            if (!compare_sets(col, obj_1, obj_2, logger)) {
30✔
656
                logger.error("Set mismatch in column '%1'", col.name);
×
657
                equal = false;
×
658
            }
×
659
            continue;
30✔
660
        }
30✔
661

662
        if (col.is_list()) {
22,858✔
663
            if (!compare_lists(col, obj_1, obj_2, logger)) {
1,022✔
664
                equal = false;
×
665
            }
×
666
            continue;
1,022✔
667
        }
1,022✔
668

669
        const bool nullable = table_1.is_nullable(col.key_1);
21,836✔
670
        REALM_ASSERT(table_2.is_nullable(col.key_2) == nullable);
21,836✔
671
        switch (col.get_type()) {
21,836✔
672
            case type_Int: {
20,308✔
673
                if (nullable) {
20,308✔
674
                    auto a = obj_1.get<util::Optional<int64_t>>(col.key_1);
62✔
675
                    auto b = obj_2.get<util::Optional<int64_t>>(col.key_2);
62✔
676
                    if (a != b) {
62✔
677
                        logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
678
                        equal = false;
×
679
                    }
×
680
                }
62✔
681
                else {
20,246✔
682
                    auto a = obj_1.get<int64_t>(col.key_1);
20,246✔
683
                    auto b = obj_2.get<int64_t>(col.key_2);
20,246✔
684
                    if (a != b) {
20,246✔
685
                        logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
686
                        equal = false;
×
687
                    }
×
688
                }
20,246✔
689
                continue;
20,308✔
690
            }
×
691
            case type_Bool: {
12✔
692
                if (nullable) {
12✔
693
                    auto a = obj_1.get<util::Optional<bool>>(col.key_1);
×
694
                    auto b = obj_2.get<util::Optional<bool>>(col.key_2);
×
695
                    if (a != b) {
×
696
                        logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
697
                        equal = false;
×
698
                    }
×
699
                }
×
700
                else {
12✔
701
                    auto a = obj_1.get<bool>(col.key_1);
12✔
702
                    auto b = obj_2.get<bool>(col.key_2);
12✔
703
                    if (a != b) {
12✔
704
                        logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
705
                        equal = false;
×
706
                    }
×
707
                }
12✔
708

709
                continue;
12✔
710
            }
×
711
            case type_Float: {
12✔
712
                auto a = obj_1.get<float>(col.key_1);
12✔
713
                auto b = obj_2.get<float>(col.key_2);
12✔
714
                if (a != b) {
12✔
715
                    logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
716
                    equal = false;
×
717
                }
×
718
                continue;
12✔
719
            }
×
720
            case type_Double: {
16✔
721
                auto a = obj_1.get<double>(col.key_1);
16✔
722
                auto b = obj_2.get<double>(col.key_2);
16✔
723
                if (a != b) {
16✔
724
                    logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
725
                    equal = false;
×
726
                }
×
727
                continue;
16✔
728
            }
×
729
            case type_String: {
294✔
730
                auto a = obj_1.get<StringData>(col.key_1);
294✔
731
                auto b = obj_2.get<StringData>(col.key_2);
294✔
732
                if (a != b) {
294✔
733
                    logger.error("Value mismatch in column '%1'", col.name);
×
734
                    equal = false;
×
735
                }
×
736
                continue;
294✔
737
            }
×
738
            case type_Binary: {
832✔
739
                // FIXME: This looks like an incorrect way of comparing BLOBs (Table::get_binary_iterator()).
740
                auto a = obj_1.get<BinaryData>(col.key_1);
832✔
741
                auto b = obj_2.get<BinaryData>(col.key_2);
832✔
742
                if (a != b) {
832✔
743
                    logger.error("Value mismatch in column '%1'", col.name);
×
744
                    equal = false;
×
745
                }
×
746
                continue;
832✔
747
            }
×
748
            case type_Timestamp: {
16✔
749
                auto a = obj_1.get<Timestamp>(col.key_1);
16✔
750
                auto b = obj_2.get<Timestamp>(col.key_2);
16✔
751
                if (a != b) {
16✔
752
                    logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
753
                    equal = false;
×
754
                }
×
755
                continue;
16✔
756
            }
×
757
            case type_ObjectId: {
8✔
758
                auto a = obj_1.get<ObjectId>(col.key_1);
8✔
759
                auto b = obj_2.get<ObjectId>(col.key_2);
8✔
760
                if (a != b) {
8✔
761
                    logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
762
                    equal = false;
×
763
                }
×
764
                continue;
8✔
765
            }
×
766
            case type_Decimal: {
8✔
767
                auto a = obj_1.get<Decimal128>(col.key_1);
8✔
768
                auto b = obj_2.get<Decimal128>(col.key_2);
8✔
769
                if (a != b) {
8✔
770
                    logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
771
                    equal = false;
×
772
                }
×
773
                continue;
8✔
774
            }
×
775
            case type_Mixed: {
270✔
776
                auto a = obj_1.get<Mixed>(col.key_1);
270✔
777
                auto b = obj_2.get<Mixed>(col.key_2);
270✔
778
                if (a != b) {
270✔
779
                    logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
780
                    equal = false;
×
781
                }
×
782
                continue;
270✔
783
            }
×
784
            case type_UUID: {
✔
785
                auto a = obj_1.get<UUID>(col.key_1);
×
786
                auto b = obj_2.get<UUID>(col.key_2);
×
787
                if (a != b) {
×
788
                    logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, a, b);
×
789
                    equal = false;
×
790
                }
×
791
                continue;
×
792
            }
×
793
            case type_TypedLink:
✔
794
                // FIXME: Implement
795
                continue;
×
796
            case type_Link: {
60✔
797
                auto link_1 = obj_1.get<ObjKey>(col.key_1);
60✔
798
                auto link_2 = obj_2.get<ObjKey>(col.key_2);
60✔
799
                ConstTableRef target_table_1 = table_1.get_link_target(col.key_1);
60✔
800
                ConstTableRef target_table_2 = table_2.get_link_target(col.key_2);
60✔
801

802
                if (!link_1 || !link_2) {
60✔
803
                    // If one link is null the other should also be null
804
                    if (link_1 != link_2) {
×
805
                        equal = false;
×
806
                        logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, link_1, link_2);
×
807
                    }
×
808
                }
×
809
                else {
60✔
810
                    bool is_embedded = target_table_1->is_embedded();
60✔
811
                    std::vector<Column> embedded_columns;
60✔
812
                    if (is_embedded) {
60✔
813
                        // FIXME: This does the schema comparison for
814
                        // embedded tables for every object with embedded
815
                        // objects, just because we want to get the Column
816
                        // info. Instead compare just the objects
817
                        // themselves.
818
                        bool schemas_equal =
48✔
819
                            compare_schemas(*target_table_1, *target_table_2, logger, &embedded_columns);
48✔
820
                        REALM_ASSERT(schemas_equal);
48✔
821
                    }
48✔
822

823
                    if (is_embedded) {
60✔
824
                        const Obj embedded_1 = target_table_1->get_object(link_1);
48✔
825
                        const Obj embedded_2 = target_table_2->get_object(link_2);
48✔
826
                        // Skip ID comparison for embedded objects, because
827
                        // they are only identified by their position in the
828
                        // database.
829
                        if (!compare_objects(embedded_1, embedded_2, embedded_columns, logger)) {
48✔
830
                            logger.error("Embedded object contents mismatch in column '%1'", col.name);
×
831
                            equal = false;
×
832
                        }
×
833
                    }
48✔
834
                    else {
12✔
835
                        Mixed target_oid_1 = target_table_1->get_primary_key(link_1);
12✔
836
                        Mixed target_oid_2 = target_table_2->get_primary_key(link_2);
12✔
837
                        if (target_oid_1 != target_oid_2) {
12✔
838
                            logger.error("Value mismatch in column '%1' (%2 vs %3)", col.name, target_oid_1,
×
839
                                         target_oid_2);
×
840
                            equal = false;
×
841
                        }
×
842
                    }
12✔
843
                }
60✔
844

845
                continue;
60✔
846
            }
×
847
        }
21,836✔
848
        REALM_TERMINATE("Unsupported column type.");
849
    }
×
850
    return equal;
7,974✔
851
}
7,974✔
852

853
bool compare_objects(Mixed& pk, const Table& table_1, const Table& table_2, const std::vector<Column>& columns,
854
                     util::Logger& logger)
855
{
7,252✔
856
    ObjKey oid_1 = row_for_primary_key(table_1, pk);
7,252✔
857
    ObjKey oid_2 = row_for_primary_key(table_2, pk);
7,252✔
858

859
    // Note: This is ensured by the inventory handling in compare_tables().
860
    REALM_ASSERT(oid_1);
7,252✔
861
    REALM_ASSERT(oid_2);
7,252✔
862
    const Obj obj_1 = table_1.get_object(oid_1);
7,252✔
863
    const Obj obj_2 = table_2.get_object(oid_2);
7,252✔
864
    return compare_objects(obj_1, obj_2, columns, logger);
7,252✔
865
}
7,252✔
866

867
} // anonymous namespace
868

869
namespace realm::test_util {
870

871
bool compare_tables(const Table& table_1, const Table& table_2, util::Logger& logger)
872
{
1,162✔
873
    bool equal = true;
1,162✔
874

875
    std::vector<Column> columns;
1,162✔
876
    equal = compare_schemas(table_1, table_2, logger, &columns);
1,162✔
877

878
    if (table_1.is_embedded() != table_2.is_embedded()) {
1,162✔
879
        logger.error("Table embeddedness mismatch");
×
880
        equal = false;
×
881
    }
×
882

883
    if (table_1.is_embedded() || table_2.is_embedded()) {
1,162✔
884
        if (table_1.size() != table_2.size()) {
90✔
885
            logger.error("Embedded table size mismatch (%1 vs %2): %3", table_1.size(), table_2.size(),
×
886
                         table_1.get_name());
×
887
            equal = false;
×
888
        }
×
889
        // Do not attempt to compare by row on embedded tables.
890
        return equal;
90✔
891
    }
90✔
892

893
    // Compare row sets
894
    using Objects = std::set<Mixed>;
1,072✔
895
    auto make_inventory = [](const Table& table, Objects& objects) {
2,144✔
896
        for (const Obj& obj : table) {
14,504✔
897
            objects.insert(obj.get_primary_key());
14,504✔
898
        }
14,504✔
899
    };
2,144✔
900
    Objects objects_1, objects_2;
1,072✔
901
    make_inventory(table_1, objects_1);
1,072✔
902
    make_inventory(table_2, objects_2);
1,072✔
903
    auto report_missing = [&](const char* hand_2, Objects& objects_1, Objects& objects_2) {
2,144✔
904
        std::vector<Mixed> missing;
2,144✔
905
        for (auto oid : objects_1) {
14,504✔
906
            if (objects_2.find(oid) == objects_2.end())
14,504✔
907
                missing.push_back(oid);
×
908
        }
14,504✔
909
        if (missing.empty())
2,144✔
910
            return;
2,144✔
UNCOV
911
        std::size_t n = missing.size();
×
UNCOV
912
        if (n == 1) {
×
913
            logger.error("One object missing in %1 side table: %2", hand_2, missing[0]);
×
914
            equal = false;
×
915
            return;
×
916
        }
×
UNCOV
917
        std::ostringstream out;
×
UNCOV
918
        out << missing[0];
×
UNCOV
919
        std::size_t m = std::min<std::size_t>(4, n);
×
UNCOV
920
        for (std::size_t i = 1; i < m; ++i)
×
921
            out << ", " << missing[i];
×
UNCOV
922
        if (m < n)
×
923
            out << ", ...";
×
UNCOV
924
        logger.error("%1 objects missing in %2 side table: %3", n, hand_2, out.str());
×
UNCOV
925
        equal = false;
×
UNCOV
926
    };
×
927
    report_missing("right-hand", objects_1, objects_2);
1,072✔
928
    report_missing("left-hand", objects_2, objects_1);
1,072✔
929

930
    // Compare individual rows
931
    for (auto pk : objects_1) {
7,252✔
932
        if (objects_2.find(pk) != objects_2.end()) {
7,252✔
933
            ObjectCompareLogger sublogger{pk, logger};
7,252✔
934
            if (!compare_objects(pk, table_1, table_2, columns, sublogger)) {
7,252✔
935
                equal = false;
×
936
            }
×
937
        }
7,252✔
938
    }
7,252✔
939

940
    return equal;
1,072✔
941
}
1,162✔
942

943

944
bool compare_groups(const Transaction& group_1, const Transaction& group_2)
945
{
304✔
946
    util::StderrLogger logger(util::Logger::Level::off);
304✔
947
    return compare_groups(group_1, group_2, logger);
304✔
948
}
304✔
949

950

951
bool compare_groups(const Transaction& group_1, const Transaction& group_2,
952
                    util::FunctionRef<bool(StringData)> filter_func, util::Logger& logger)
953
{
844✔
954
    auto filter = [&](const Group& group, std::vector<StringData>& tables) {
1,688✔
955
        auto table_keys = group.get_table_keys();
1,688✔
956
        for (auto i : table_keys) {
2,348✔
957
            ConstTableRef table = group.get_table(i);
2,348✔
958
            StringData name = table->get_name();
2,348✔
959
            if (name != "pk" && name != "metadata" && name != "client_reset_metadata" && filter_func(name))
2,348✔
960
                tables.push_back(name);
2,328✔
961
        }
2,348✔
962
    };
1,688✔
963

964
    std::vector<StringData> tables_1, tables_2;
844✔
965
    filter(group_1, tables_1);
844✔
966
    filter(group_2, tables_2);
844✔
967

968
    bool equal = true;
844✔
969
    for (StringData table_name : tables_1) {
1,164✔
970
        if (!group_2.has_table(table_name)) {
1,164✔
971
            logger.error("Table '%1' not found in right-hand side group", table_name);
2✔
972
            equal = false;
2✔
973
        }
2✔
974
    }
1,164✔
975
    for (StringData table_name : tables_2) {
1,164✔
976
        if (!group_1.has_table(table_name)) {
1,164✔
977
            logger.error("Table '%1' not found in left-hand side group", table_name);
2✔
978
            equal = false;
2✔
979
        }
2✔
980
    }
1,164✔
981

982
    for (StringData table_name : tables_1) {
1,164✔
983
        ConstTableRef table_1 = group_1.get_table(table_name);
1,164✔
984
        ConstTableRef table_2 = group_2.get_table(table_name);
1,164✔
985
        if (table_2) {
1,164✔
986
            TableCompareLogger sublogger{table_name, logger};
1,162✔
987
            if (!compare_tables(*table_1, *table_2, sublogger))
1,162✔
988
                equal = false;
×
989
        }
1,162✔
990
    }
1,164✔
991

992
    return equal;
844✔
993
}
844✔
994

995
} // namespace realm::test_util
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