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

realm / realm-core / jonathan.reams_3093

12 Mar 2024 04:31PM UTC coverage: 92.212% (+1.3%) from 90.928%
jonathan.reams_3093

push

Evergreen

web-flow
RCORE-1490 Sync collections in Mixed and nested collections (#7353)

* Update the merge nested rules to handle sentinel instructions on conflicting data types

* Allow syncing nested collections and collections in mixed

* Add integration tests with the legacy sync server

* Do not expose set_collection on a dictionary

* Fix OT rule for add_int on Mixed fields

* Small fixes

* Fix linting errors

* Apply Clear instructions received from the server on nested collections

* Add OT rule Clear vs Update

* Reduce boilerplate in collections in mixed unit test (#7421)

* Document unit-tests

* Changes after code review

* Fix test comments & update changelog

---------

Co-authored-by: Jonathan Reams <jbreams@mongodb.com>

99588 of 175432 branches covered (56.77%)

1350 of 1354 new or added lines in 7 files covered. (99.7%)

26 existing lines in 5 files now uncovered.

245244 of 265957 relevant lines covered (92.21%)

8803432.4 hits per line

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

87.31
/src/realm/dictionary.cpp
1
/*************************************************************************
2
 *
3
 * Copyright 2019 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/dictionary.hpp>
20
#include <realm/aggregate_ops.hpp>
21
#include <realm/array_mixed.hpp>
22
#include <realm/array_ref.hpp>
23
#include <realm/group.hpp>
24
#include <realm/list.hpp>
25
#include <realm/set.hpp>
26
#include <realm/replication.hpp>
27

28
#include <algorithm>
29

30
namespace realm {
31

32
namespace {
33
void validate_key_value(const Mixed& key)
34
{
181,899✔
35
    if (key.is_type(type_String)) {
181,899✔
36
        auto str = key.get_string();
181,899✔
37
        if (str.size()) {
181,899✔
38
            if (str[0] == '$')
181,809✔
39
                throw Exception(ErrorCodes::InvalidDictionaryKey, "Dictionary::insert: key must not start with '$'");
18✔
40
            if (memchr(str.data(), '.', str.size()))
181,791✔
41
                throw Exception(ErrorCodes::InvalidDictionaryKey, "Dictionary::insert: key must not contain '.'");
18✔
42
        }
181,791✔
43
    }
181,899✔
44
}
181,899✔
45

46
} // namespace
47

48

49
/******************************** Dictionary *********************************/
50

51
Dictionary::Dictionary(ColKey col_key, size_t level)
52
    : Base(col_key)
81,999✔
53
    , CollectionParent(level)
81,999✔
54
{
244,371✔
55
    if (!(col_key.is_dictionary() || col_key.get_type() == col_type_Mixed)) {
244,371✔
56
        throw InvalidArgument(ErrorCodes::TypeMismatch, "Property not a dictionary");
×
57
    }
×
58
}
244,371✔
59

60
Dictionary::Dictionary(Allocator& alloc, ColKey col_key, ref_type ref)
61
    : Base(Obj{}, col_key)
13,956✔
62
    , m_key_type(type_String)
13,956✔
63
{
41,862✔
64
    set_alloc(alloc);
41,862✔
65
    REALM_ASSERT(ref);
41,862✔
66
    m_dictionary_top.reset(new Array(alloc));
41,862✔
67
    m_dictionary_top->init_from_ref(ref);
41,862✔
68
    m_keys.reset(new BPlusTree<StringData>(alloc));
41,862✔
69
    m_values.reset(new BPlusTreeMixed(alloc));
41,862✔
70
    m_keys->set_parent(m_dictionary_top.get(), 0);
41,862✔
71
    m_values->set_parent(m_dictionary_top.get(), 1);
41,862✔
72
    m_keys->init_from_parent();
41,862✔
73
    m_values->init_from_parent();
41,862✔
74
}
41,862✔
75

76
Dictionary::~Dictionary() = default;
297,303✔
77

78
Dictionary& Dictionary::operator=(const Dictionary& other)
79
{
9,882✔
80
    Base::operator=(static_cast<const Base&>(other));
9,882✔
81

3,150✔
82
    if (this != &other) {
9,882✔
83
        // Back to scratch
3,150✔
84
        m_dictionary_top.reset();
9,882✔
85
        reset_content_version();
9,882✔
86
    }
9,882✔
87

3,150✔
88
    return *this;
9,882✔
89
}
9,882✔
90

91
size_t Dictionary::size() const
92
{
578,370✔
93
    if (!update())
578,370✔
94
        return 0;
62,343✔
95

169,563✔
96
    return m_values->size();
516,027✔
97
}
536,721✔
98

99
DataType Dictionary::get_key_data_type() const
100
{
26,820✔
101
    return m_key_type;
26,820✔
102
}
26,820✔
103

104
DataType Dictionary::get_value_data_type() const
105
{
21,351✔
106
    return DataType(m_col_key.get_type());
21,351✔
107
}
21,351✔
108

109
bool Dictionary::is_null(size_t ndx) const
110
{
×
111
    return get_any(ndx).is_null();
×
112
}
×
113

114
Mixed Dictionary::get_any(size_t ndx) const
115
{
24,444✔
116
    // Note: `size()` calls `update_if_needed()`.
8,103✔
117
    auto current_size = size();
24,444✔
118
    CollectionBase::validate_index("get_any()", ndx, current_size);
24,444✔
119
    return do_get(ndx);
24,444✔
120
}
24,444✔
121

122
std::pair<Mixed, Mixed> Dictionary::get_pair(size_t ndx) const
123
{
139,878✔
124
    // Note: `size()` calls `update_if_needed()`.
46,212✔
125
    auto current_size = size();
139,878✔
126
    CollectionBase::validate_index("get_pair()", ndx, current_size);
139,878✔
127
    return do_get_pair(ndx);
139,878✔
128
}
139,878✔
129

130
Mixed Dictionary::get_key(size_t ndx) const
131
{
70,476✔
132
    // Note: `size()` calls `update_if_needed()`.
23,436✔
133
    CollectionBase::validate_index("get_key()", ndx, size());
70,476✔
134
    return do_get_key(ndx);
70,476✔
135
}
70,476✔
136

137
size_t Dictionary::find_any(Mixed value) const
138
{
4,797✔
139
    return size() ? m_values->find_first(value) : realm::not_found;
4,551✔
140
}
4,797✔
141

142
size_t Dictionary::find_any_key(Mixed key) const noexcept
143
{
57,276✔
144
    if (update()) {
57,276✔
145
        return do_find_key(key);
45,234✔
146
    }
45,234✔
147

4,014✔
148
    return realm::npos;
12,042✔
149
}
27,537✔
150

151
template <typename AggregateType>
152
void Dictionary::do_accumulate(size_t* return_ndx, AggregateType& agg) const
153
{
26,676✔
154
    size_t ndx = realm::npos;
26,676✔
155

8,892✔
156
    m_values->traverse([&](BPlusTreeNode* node, size_t offset) {
26,676✔
157
        auto leaf = static_cast<BPlusTree<Mixed>::LeafNode*>(node);
26,676✔
158
        size_t e = leaf->size();
26,676✔
159
        for (size_t i = 0; i < e; i++) {
102,690✔
160
            auto val = leaf->get(i);
76,014✔
161
            if (agg.accumulate(val)) {
76,014✔
162
                ndx = i + offset;
57,888✔
163
            }
57,888✔
164
        }
76,014✔
165
        // Continue
8,892✔
166
        return IteratorControl::AdvanceToNext;
26,676✔
167
    });
26,676✔
168

8,892✔
169
    if (return_ndx)
26,676✔
170
        *return_ndx = ndx;
18✔
171
}
26,676✔
172

173
util::Optional<Mixed> Dictionary::do_min(size_t* return_ndx) const
174
{
6,156✔
175
    aggregate_operations::Minimum<Mixed> agg;
6,156✔
176
    do_accumulate(return_ndx, agg);
6,156✔
177
    return agg.is_null() ? Mixed{} : agg.result();
6,156✔
178
}
6,156✔
179

180
util::Optional<Mixed> Dictionary::do_max(size_t* return_ndx) const
181
{
8,136✔
182
    aggregate_operations::Maximum<Mixed> agg;
8,136✔
183
    do_accumulate(return_ndx, agg);
8,136✔
184
    return agg.is_null() ? Mixed{} : agg.result();
8,136✔
185
}
8,136✔
186

187
util::Optional<Mixed> Dictionary::do_sum(size_t* return_cnt) const
188
{
6,246✔
189
    auto type = get_value_data_type();
6,246✔
190
    if (type == type_Int) {
6,246✔
191
        aggregate_operations::Sum<Int> agg;
63✔
192
        do_accumulate(nullptr, agg);
63✔
193
        if (return_cnt)
63✔
194
            *return_cnt = agg.items_counted();
27✔
195
        return Mixed{agg.result()};
63✔
196
    }
63✔
197
    else if (type == type_Double) {
6,183✔
198
        aggregate_operations::Sum<Double> agg;
126✔
199
        do_accumulate(nullptr, agg);
126✔
200
        if (return_cnt)
126✔
201
            *return_cnt = agg.items_counted();
×
202
        return Mixed{agg.result()};
126✔
203
    }
126✔
204
    else if (type == type_Float) {
6,057✔
205
        aggregate_operations::Sum<Float> agg;
126✔
206
        do_accumulate(nullptr, agg);
126✔
207
        if (return_cnt)
126✔
208
            *return_cnt = agg.items_counted();
×
209
        return Mixed{agg.result()};
126✔
210
    }
126✔
211

1,977✔
212
    aggregate_operations::Sum<Mixed> agg;
5,931✔
213
    do_accumulate(nullptr, agg);
5,931✔
214
    if (return_cnt)
5,931✔
215
        *return_cnt = agg.items_counted();
×
216
    return Mixed{agg.result()};
5,931✔
217
}
6,036✔
218

219
util::Optional<Mixed> Dictionary::do_avg(size_t* return_cnt) const
220
{
6,138✔
221
    auto type = get_value_data_type();
6,138✔
222
    if (type == type_Int) {
6,138✔
223
        aggregate_operations::Average<Int> agg;
63✔
224
        do_accumulate(nullptr, agg);
63✔
225
        if (return_cnt)
63✔
226
            *return_cnt = agg.items_counted();
27✔
227
        return agg.is_null() ? Mixed{} : agg.result();
63✔
228
    }
63✔
229
    else if (type == type_Double) {
6,075✔
230
        aggregate_operations::Average<Double> agg;
90✔
231
        do_accumulate(nullptr, agg);
90✔
232
        if (return_cnt)
90✔
233
            *return_cnt = agg.items_counted();
×
234
        return agg.is_null() ? Mixed{} : agg.result();
90✔
235
    }
90✔
236
    else if (type == type_Float) {
5,985✔
237
        aggregate_operations::Average<Float> agg;
90✔
238
        do_accumulate(nullptr, agg);
90✔
239
        if (return_cnt)
90✔
240
            *return_cnt = agg.items_counted();
×
241
        return agg.is_null() ? Mixed{} : agg.result();
90✔
242
    }
90✔
243
    // Decimal128 is covered with mixed as well.
1,965✔
244
    aggregate_operations::Average<Mixed> agg;
5,895✔
245
    do_accumulate(nullptr, agg);
5,895✔
246
    if (return_cnt)
5,895✔
247
        *return_cnt = agg.items_counted();
×
248
    return agg.is_null() ? Mixed{} : agg.result();
5,895✔
249
}
5,976✔
250

251
namespace {
252
bool can_minmax(DataType type)
253
{
909✔
254
    switch (type) {
909✔
255
        case type_Int:
273✔
256
        case type_Float:
321✔
257
        case type_Double:
369✔
258
        case type_Decimal:
417✔
259
        case type_Mixed:
501✔
260
        case type_Timestamp:
549✔
261
            return true;
549✔
262
        default:
423✔
263
            return false;
360✔
264
    }
909✔
265
}
909✔
266
bool can_sum(DataType type)
267
{
900✔
268
    switch (type) {
900✔
269
        case type_Int:
240✔
270
        case type_Float:
288✔
271
        case type_Double:
336✔
272
        case type_Decimal:
384✔
273
        case type_Mixed:
468✔
274
            return true;
468✔
275
        default:
444✔
276
            return false;
432✔
277
    }
900✔
278
}
900✔
279
} // anonymous namespace
280

281
util::Optional<Mixed> Dictionary::min(size_t* return_ndx) const
282
{
450✔
283
    if (!can_minmax(get_value_data_type())) {
450✔
284
        return std::nullopt;
180✔
285
    }
180✔
286
    if (update()) {
270✔
287
        return do_min(return_ndx);
162✔
288
    }
162✔
289
    if (return_ndx)
108✔
290
        *return_ndx = realm::not_found;
×
291
    return Mixed{};
108✔
292
}
162✔
293

294
util::Optional<Mixed> Dictionary::max(size_t* return_ndx) const
295
{
459✔
296
    if (!can_minmax(get_value_data_type())) {
459✔
297
        return std::nullopt;
180✔
298
    }
180✔
299
    if (update()) {
279✔
300
        return do_max(return_ndx);
162✔
301
    }
162✔
302
    if (return_ndx)
117✔
303
        *return_ndx = realm::not_found;
9✔
304
    return Mixed{};
117✔
305
}
171✔
306

307
util::Optional<Mixed> Dictionary::sum(size_t* return_cnt) const
308
{
450✔
309
    if (!can_sum(get_value_data_type())) {
450✔
310
        return std::nullopt;
216✔
311
    }
216✔
312
    if (update()) {
234✔
313
        return do_sum(return_cnt);
144✔
314
    }
144✔
315
    if (return_cnt)
90✔
316
        *return_cnt = 0;
×
317
    return Mixed{0};
90✔
318
}
138✔
319

320
util::Optional<Mixed> Dictionary::avg(size_t* return_cnt) const
321
{
450✔
322
    if (!can_sum(get_value_data_type())) {
450✔
323
        return std::nullopt;
216✔
324
    }
216✔
325
    if (update()) {
234✔
326
        return do_avg(return_cnt);
144✔
327
    }
144✔
328
    if (return_cnt)
90✔
329
        *return_cnt = 0;
×
330
    return Mixed{};
90✔
331
}
138✔
332

333
void Dictionary::align_indices(std::vector<size_t>& indices) const
334
{
7,569✔
335
    auto sz = size();
7,569✔
336
    auto sz2 = indices.size();
7,569✔
337
    indices.reserve(sz);
7,569✔
338
    if (sz < sz2) {
7,569✔
339
        // If list size has decreased, we have to start all over
340
        indices.clear();
×
341
        sz2 = 0;
×
342
    }
×
343
    for (size_t i = sz2; i < sz; i++) {
35,037✔
344
        // If list size has increased, just add the missing indices
9,156✔
345
        indices.push_back(i);
27,468✔
346
    }
27,468✔
347
}
7,569✔
348

349
namespace {
350
template <class T>
351
void do_sort(std::vector<size_t>& indices, bool ascending, const std::vector<T>& values)
352
{
6,624✔
353
    auto b = indices.begin();
6,624✔
354
    auto e = indices.end();
6,624✔
355
    std::sort(b, e, [ascending, &values](size_t i1, size_t i2) {
54,744✔
356
        return ascending ? values[i1] < values[i2] : values[i2] < values[i1];
54,102✔
357
    });
54,744✔
358
}
6,624✔
359
} // anonymous namespace
360

361
void Dictionary::sort(std::vector<size_t>& indices, bool ascending) const
362
{
5,679✔
363
    align_indices(indices);
5,679✔
364
    do_sort(indices, ascending, m_values->get_all());
5,679✔
365
}
5,679✔
366

367
void Dictionary::distinct(std::vector<size_t>& indices, util::Optional<bool> ascending) const
368
{
945✔
369
    align_indices(indices);
945✔
370
    auto values = m_values->get_all();
945✔
371
    do_sort(indices, ascending.value_or(true), values);
945✔
372
    indices.erase(std::unique(indices.begin(), indices.end(),
945✔
373
                              [&values](size_t i1, size_t i2) {
2,700✔
374
                                  return values[i1] == values[i2];
2,700✔
375
                              }),
2,700✔
376
                  indices.end());
945✔
377

315✔
378
    if (!ascending) {
945✔
379
        // need to return indices in original ordering
63✔
380
        std::sort(indices.begin(), indices.end(), std::less<size_t>());
189✔
381
    }
189✔
382
}
945✔
383

384
void Dictionary::sort_keys(std::vector<size_t>& indices, bool ascending) const
385
{
756✔
386
    align_indices(indices);
756✔
387
#ifdef REALM_DEBUG
756✔
388
    if (indices.size() > 1) {
756✔
389
        // We rely in the design that the keys are already sorted
252✔
390
        switch (m_key_type) {
756✔
391
            case type_String: {
756✔
392
                IteratorAdapter help(static_cast<BPlusTree<StringData>*>(m_keys.get()));
756✔
393
                auto is_sorted = std::is_sorted(help.begin(), help.end());
756✔
394
                REALM_ASSERT(is_sorted);
756✔
395
                break;
756✔
396
            }
×
397
            case type_Int: {
✔
398
                IteratorAdapter help(static_cast<BPlusTree<Int>*>(m_keys.get()));
×
399
                auto is_sorted = std::is_sorted(help.begin(), help.end());
×
400
                REALM_ASSERT(is_sorted);
×
401
                break;
×
402
            }
×
403
            default:
✔
404
                break;
×
405
        }
756✔
406
    }
756✔
407
#endif
756✔
408
    if (ascending) {
756✔
409
        std::sort(indices.begin(), indices.end());
378✔
410
    }
378✔
411
    else {
378✔
412
        std::sort(indices.begin(), indices.end(), std::greater<size_t>());
378✔
413
    }
378✔
414
}
756✔
415

416
void Dictionary::distinct_keys(std::vector<size_t>& indices, util::Optional<bool>) const
417
{
189✔
418
    // we rely on the design of dictionary to assume that the keys are unique
63✔
419
    align_indices(indices);
189✔
420
}
189✔
421

422

423
Obj Dictionary::create_and_insert_linked_object(Mixed key)
424
{
6,498✔
425
    Table& t = *get_target_table();
6,498✔
426
    auto o = t.is_embedded() ? t.create_linked_object() : t.create_object();
6,498✔
427
    insert(key, o.get_key());
6,498✔
428
    return o;
6,498✔
429
}
6,498✔
430

431
void Dictionary::insert_collection(const PathElement& path_elem, CollectionType dict_or_list)
432
{
1,716✔
433
    if (dict_or_list == CollectionType::Set) {
1,716✔
434
        throw IllegalOperation("Set nested in Dictionary is not supported");
×
435
    }
×
436

186✔
437
    check_level();
1,716✔
438
    ensure_created();
1,716✔
439
    Mixed new_val(0, dict_or_list);
1,716✔
440
    auto old_val = try_get(path_elem.get_key());
1,716✔
441
    if (!old_val || *old_val != new_val) {
1,716✔
442
        m_values->ensure_keys();
1,572✔
443
        auto [it, inserted] = insert(path_elem.get_key(), new_val);
1,572✔
444
        int64_t key = generate_key(size());
1,572✔
445
        while (m_values->find_key(key) != realm::not_found) {
1,572✔
446
            key++;
×
447
        }
×
448
        m_values->set_key(it.index(), key);
1,572✔
449
    }
1,572✔
450
}
1,716✔
451

452
DictionaryPtr Dictionary::get_dictionary(const PathElement& path_elem) const
453
{
714✔
454
    update();
714✔
455
    auto weak = const_cast<Dictionary*>(this)->weak_from_this();
714✔
456
    auto shared = weak.expired() ? std::make_shared<Dictionary>(*this) : weak.lock();
636✔
457
    DictionaryPtr ret = std::make_shared<Dictionary>(m_col_key, get_level() + 1);
714✔
458
    ret->set_owner(shared, build_index(path_elem.get_key()));
714✔
459
    return ret;
714✔
460
}
714✔
461

462
std::shared_ptr<Lst<Mixed>> Dictionary::get_list(const PathElement& path_elem) const
463
{
1,422✔
464
    update();
1,422✔
465
    auto weak = const_cast<Dictionary*>(this)->weak_from_this();
1,422✔
466
    auto shared = weak.expired() ? std::make_shared<Dictionary>(*this) : weak.lock();
1,293✔
467
    std::shared_ptr<Lst<Mixed>> ret = std::make_shared<Lst<Mixed>>(m_col_key, get_level() + 1);
1,422✔
468
    ret->set_owner(shared, build_index(path_elem.get_key()));
1,422✔
469
    return ret;
1,422✔
470
}
1,422✔
471

472
Mixed Dictionary::get(Mixed key) const
473
{
21,579✔
474
    if (auto opt_val = try_get(key)) {
21,579✔
475
        return *opt_val;
21,543✔
476
    }
21,543✔
477
    throw KeyNotFound("Dictionary::get");
36✔
478
}
7,581✔
479

480
util::Optional<Mixed> Dictionary::try_get(Mixed key) const
481
{
1,262,805✔
482
    if (update()) {
1,262,805✔
483
        auto ndx = do_find_key(key);
1,261,311✔
484
        if (ndx != realm::npos) {
1,261,311✔
485
            return do_get(ndx);
1,256,673✔
486
        }
1,256,673✔
487
    }
424,959✔
488
    return {};
6,132✔
489
}
425,457✔
490

491
Dictionary::Iterator Dictionary::begin() const
492
{
34,809✔
493
    // Need an update because the `Dictionary::Iterator` constructor relies on
11,418✔
494
    // `m_clusters` to determine if it was already at end.
11,418✔
495
    update();
34,809✔
496
    return Iterator(this, 0);
34,809✔
497
}
34,809✔
498

499
Dictionary::Iterator Dictionary::end() const
500
{
125,919✔
501
    return Iterator(this, size());
125,919✔
502
}
125,919✔
503

504
std::pair<Dictionary::Iterator, bool> Dictionary::insert(Mixed key, Mixed value)
505
{
163,857✔
506
    auto my_table = get_table_unchecked();
163,857✔
507
    if (key.get_type() != m_key_type) {
163,857✔
508
        throw InvalidArgument(ErrorCodes::InvalidDictionaryKey, "Dictionary::insert: Invalid key type");
×
509
    }
×
510
    if (m_col_key) {
163,857✔
511
        if (value.is_null()) {
163,848✔
512
            if (!m_col_key.is_nullable()) {
7,914✔
513
                throw InvalidArgument(ErrorCodes::InvalidDictionaryValue, "Dictionary::insert: Value cannot be null");
×
514
            }
×
515
        }
106,230✔
516
        else {
155,934✔
517
            if (m_col_key.get_type() == col_type_Link && value.get_type() == type_TypedLink) {
155,934✔
518
                if (my_table->get_opposite_table_key(m_col_key) != value.get<ObjLink>().get_table_key()) {
4,239✔
519
                    throw InvalidArgument(ErrorCodes::InvalidDictionaryValue,
9✔
520
                                          "Dictionary::insert: Wrong object type");
9✔
521
                }
9✔
522
            }
102,183✔
523
            else if (m_col_key.get_type() != col_type_Mixed && value.get_type() != DataType(m_col_key.get_type())) {
151,695✔
524
                throw InvalidArgument(ErrorCodes::InvalidDictionaryValue, "Dictionary::insert: Wrong value type");
×
525
            }
×
526
            else if (value.is_type(type_Link) && m_col_key.get_type() != col_type_Link) {
151,695✔
527
                throw InvalidArgument(ErrorCodes::InvalidDictionaryValue,
×
528
                                      "Dictionary::insert: No target table for link");
×
529
            }
×
530
        }
161,214✔
531
    }
163,848✔
532

53,613✔
533
    validate_key_value(key);
163,848✔
534
    ensure_created();
163,848✔
535

53,613✔
536
    ObjLink new_link;
163,848✔
537
    if (value.is_type(type_TypedLink)) {
163,848✔
538
        new_link = value.get<ObjLink>();
12,240✔
539
        if (!new_link.is_unresolved())
12,240✔
540
            my_table->get_parent_group()->validate(new_link);
11,880✔
541
    }
12,240✔
542
    else if (value.is_type(type_Link)) {
151,608✔
543
        auto target_table = my_table->get_opposite_table(m_col_key);
19,377✔
544
        auto key = value.get<ObjKey>();
19,377✔
545
        if (!key.is_unresolved() && !target_table->is_valid(key)) {
19,377✔
546
            throw InvalidArgument(ErrorCodes::KeyNotFound, "Target object not found");
9✔
547
        }
9✔
548
        new_link = ObjLink(target_table->get_key(), key);
19,368✔
549
        value = Mixed(new_link);
19,368✔
550
    }
19,368✔
551

53,613✔
552
    if (!m_dictionary_top) {
163,842✔
553
        throw StaleAccessor("Stale dictionary");
×
554
    }
×
555

53,610✔
556
    bool old_entry = false;
163,839✔
557
    auto [ndx, actual_key] = find_impl(key);
163,839✔
558
    if (actual_key != key) {
163,839✔
559
        // key does not already exist
48,885✔
560
        switch (m_key_type) {
149,268✔
561
            case type_String:
149,268✔
562
                static_cast<BPlusTree<StringData>*>(m_keys.get())->insert(ndx, key.get_string());
149,268✔
563
                break;
149,268✔
564
            case type_Int:
✔
565
                static_cast<BPlusTree<Int>*>(m_keys.get())->insert(ndx, key.get_int());
×
566
                break;
×
567
            default:
✔
568
                break;
×
569
        }
149,268✔
570
        m_values->insert(ndx, value);
149,268✔
571
    }
149,268✔
572
    else {
14,571✔
573
        old_entry = true;
14,571✔
574
    }
14,571✔
575

53,610✔
576
    if (Replication* repl = get_replication()) {
163,839✔
577
        if (old_entry) {
143,373✔
578
            repl->dictionary_set(*this, ndx, key, value);
12,690✔
579
        }
12,690✔
580
        else {
130,683✔
581
            repl->dictionary_insert(*this, ndx, key, value);
130,683✔
582
        }
130,683✔
583
    }
143,373✔
584

53,610✔
585
    bump_content_version();
163,839✔
586

53,610✔
587
    ObjLink old_link;
163,839✔
588
    if (old_entry) {
163,839✔
589
        Mixed old_value = m_values->get(ndx);
14,490✔
590
        if (old_value.is_type(type_TypedLink)) {
14,490✔
591
            old_link = old_value.get<ObjLink>();
1,152✔
592
        }
1,152✔
593
        m_values->set(ndx, value);
14,490✔
594
    }
14,490✔
595

53,610✔
596
    if (new_link != old_link) {
163,839✔
597
        CascadeState cascade_state(CascadeState::Mode::Strong);
31,680✔
598
        bool recurse = Base::replace_backlink(m_col_key, old_link, new_link, cascade_state);
31,680✔
599
        if (recurse)
31,680✔
600
            _impl::TableFriend::remove_recursive(*my_table, cascade_state); // Throws
441✔
601
    }
31,680✔
602

53,610✔
603
    return {Iterator(this, ndx), !old_entry};
163,839✔
604
}
163,839✔
605

606
const Mixed Dictionary::operator[](Mixed key)
607
{
1,219,200✔
608
    auto ret = try_get(key);
1,219,200✔
609
    if (!ret) {
1,219,200✔
610
        ret = Mixed{};
9✔
611
        insert(key, Mixed{});
9✔
612
    }
9✔
613

406,398✔
614
    return *ret;
1,219,200✔
615
}
1,219,200✔
616

617
Obj Dictionary::get_object(StringData key)
618
{
18,624✔
619
    if (auto val = try_get(key)) {
18,624✔
620
        if ((*val).is_type(type_TypedLink)) {
13,827✔
621
            return get_table()->get_parent_group()->get_object((*val).get_link());
13,647✔
622
        }
13,647✔
623
    }
7,926✔
624
    return {};
4,977✔
625
}
9,525✔
626

627
bool Dictionary::contains(Mixed key) const noexcept
628
{
4,608✔
629
    return find_any_key(key) != realm::npos;
4,608✔
630
}
4,608✔
631

632
Dictionary::Iterator Dictionary::find(Mixed key) const noexcept
633
{
51,030✔
634
    auto ndx = find_any_key(key);
51,030✔
635
    if (ndx != realm::npos) {
51,030✔
636
        return Iterator(this, ndx);
28,017✔
637
    }
28,017✔
638
    return end();
23,013✔
639
}
32,769✔
640

641
void Dictionary::add_index(Path& path, const Index& index) const
642
{
966✔
643
    auto ndx = m_values->find_key(index.get_salt());
966✔
644
    auto keys = static_cast<BPlusTree<StringData>*>(m_keys.get());
966✔
645
    path.emplace_back(keys->get(ndx));
966✔
646
}
966✔
647

648
size_t Dictionary::find_index(const Index& index) const
649
{
375✔
650
    update();
375✔
651
    return m_values->find_key(index.get_salt());
375✔
652
}
375✔
653

654
UpdateStatus Dictionary::update_if_needed_with_status() const
655
{
2,057,463✔
656
    auto status = Base::get_update_status();
2,057,463✔
657
    switch (status) {
2,057,463✔
658
        case UpdateStatus::Detached: {
9✔
659
            m_dictionary_top.reset();
9✔
660
            return UpdateStatus::Detached;
9✔
661
        }
×
662
        case UpdateStatus::NoChange: {
1,849,410✔
663
            if (m_dictionary_top && m_dictionary_top->is_attached()) {
1,849,410✔
664
                return UpdateStatus::NoChange;
1,795,938✔
665
            }
1,795,938✔
666
            // The tree has not been initialized yet for this accessor, so
17,640✔
667
            // perform lazy initialization by treating it as an update.
17,640✔
668
            [[fallthrough]];
655,794✔
669
        }
53,472✔
670
        case UpdateStatus::Updated: {
261,516✔
671
            // Try to initialize. If the dictionary is not initialized
85,230✔
672
            // the function will return false;
85,230✔
673
            bool attached = init_from_parent(false);
261,516✔
674
            Base::update_content_version();
261,516✔
675
            CollectionParent::m_parent_version++;
261,516✔
676
            return attached ? UpdateStatus::Updated : UpdateStatus::Detached;
230,583✔
677
        }
17,706✔
678
    }
690,027✔
679
    REALM_UNREACHABLE();
680
}
×
681

682
void Dictionary::ensure_created()
683
{
165,537✔
684
    if (Base::should_update() || !(m_dictionary_top && m_dictionary_top->is_attached())) {
165,537✔
685
        // When allow_create is true, init_from_parent will always succeed
25,728✔
686
        // In case of errors, an exception is thrown.
25,728✔
687
        constexpr bool allow_create = true;
79,899✔
688
        init_from_parent(allow_create); // Throws
79,899✔
689
        CollectionParent::m_parent_version++;
79,899✔
690
        Base::update_content_version();
79,899✔
691
    }
79,899✔
692
}
165,537✔
693

694
bool Dictionary::try_erase(Mixed key)
695
{
18,060✔
696
    validate_key_value(key);
18,060✔
697
    if (!update())
18,060✔
698
        return false;
×
699

6,015✔
700
    auto ndx = do_find_key(key);
18,060✔
701
    if (ndx == realm::npos) {
18,060✔
702
        return false;
1,845✔
703
    }
1,845✔
704

5,400✔
705
    do_erase(ndx, key);
16,215✔
706

5,400✔
707
    return true;
16,215✔
708
}
16,830✔
709

710

711
void Dictionary::erase(Mixed key)
10,935✔
712
{
16,395✔
713
    if (!try_erase(key)) {
6,078✔
714
        throw KeyNotFound(util::format("Cannot remove key %1 from dictionary: key not found", key));
927✔
715
    }
11,244✔
716
}
5,460✔
717

718
auto Dictionary::erase(Iterator it) -> Iterator
2,616✔
719
{
3,903✔
720
    auto pos = it.m_ndx;
3,903✔
721
    CollectionBase::validate_index("erase()", pos, size());
1,287✔
722

3,903✔
723
    do_erase(pos, do_get_key(pos));
3,903✔
724
    if (pos < size())
1,767✔
725
        pos++;
2,841✔
726
    return {this, pos};
3,903✔
727
}
1,287✔
728

729
void Dictionary::nullify(size_t ndx)
726✔
730
{
1,089✔
731
    REALM_ASSERT(m_dictionary_top);
1,089✔
732
    REALM_ASSERT(ndx != realm::npos);
363✔
733

1,089✔
734
    if (Replication* repl = get_replication()) {
1,071✔
735
        auto key = do_get_key(ndx);
1,062✔
736
        repl->dictionary_set(*this, ndx, key, Mixed());
1,062✔
737
    }
354✔
738

1,089✔
739
    m_values->set(ndx, Mixed());
1,089✔
740
}
363✔
741

742
bool Dictionary::nullify(ObjLink target_link)
738✔
743
{
1,107✔
744
    size_t ndx = find_first(target_link);
1,107✔
745
    if (ndx != realm::not_found) {
1,095✔
746
        nullify(ndx);
1,089✔
747
        return true;
1,089✔
748
    }
375✔
749
    else {
6✔
750
        // There must be a link in a nested collection
18✔
751
        size_t sz = size();
24✔
752
        for (size_t ndx = 0; ndx < sz; ndx++) {
21✔
753
            auto val = m_values->get(ndx);
18✔
754
            auto key = do_get_key(ndx);
18✔
755
            if (val.is_type(type_Dictionary)) {
6✔
756
                auto dict = get_dictionary(key.get_string());
×
757
                if (dict->nullify(target_link)) {
×
758
                    return true;
×
759
                }
6✔
760
            }
18✔
761
            if (val.is_type(type_List)) {
12✔
762
                auto list = get_list(key.get_string());
9✔
763
                if (list->nullify(target_link)) {
9✔
764
                    return true;
9✔
765
                }
9✔
766
            }
15✔
767
        }
18✔
768
    }
12✔
769
    return false;
1,107✔
770
}
369✔
771

772
bool Dictionary::replace_link(ObjLink old_link, ObjLink replace_link)
264✔
773
{
396✔
774
    size_t ndx = find_first(old_link);
396✔
775
    if (ndx != realm::not_found) {
396✔
776
        auto key = do_get_key(ndx);
396✔
777
        insert(key, replace_link);
396✔
778
        return true;
396✔
779
    }
132✔
780
    else {
781
        // There must be a link in a nested collection
×
782
        size_t sz = size();
×
783
        for (size_t ndx = 0; ndx < sz; ndx++) {
×
784
            auto val = m_values->get(ndx);
×
785
            auto key = do_get_key(ndx);
×
786
            if (val.is_type(type_Dictionary)) {
×
787
                auto dict = get_dictionary(key.get_string());
×
788
                if (dict->replace_link(old_link, replace_link)) {
×
789
                    return true;
×
790
                }
×
791
            }
×
792
            if (val.is_type(type_List)) {
×
793
                auto list = get_list(key.get_string());
×
794
                if (list->replace_link(old_link, replace_link)) {
×
795
                    return true;
×
796
                }
×
797
            }
×
798
        }
×
UNCOV
799
    }
×
800
    return false;
396✔
801
}
132✔
802

803
bool Dictionary::remove_backlinks(CascadeState& state) const
3,054✔
804
{
4,440✔
805
    size_t sz = size();
4,440✔
806
    bool recurse = false;
15,354✔
807
    for (size_t ndx = 0; ndx < sz; ndx++) {
17,562✔
808
        if (clear_backlink(ndx, state)) {
5,631✔
809
            recurse = true;
549✔
810
        }
11,094✔
811
    }
8,316✔
812
    return recurse;
4,440✔
813
}
1,386✔
814

815
size_t Dictionary::find_first(Mixed value) const
39,186✔
816
{
58,761✔
817
    return update() ? m_values->find_first(value) : realm::not_found;
58,761✔
818
}
19,575✔
819

820
void Dictionary::clear()
2,724✔
821
{
4,020✔
822
    if (size() > 0) {
3,786✔
823
        if (Replication* repl = get_replication()) {
3,657✔
824
            repl->dictionary_clear(*this);
3,651✔
825
        }
3,663✔
826
        CascadeState cascade_state(CascadeState::Mode::Strong);
3,669✔
827
        bool recurse = remove_backlinks(cascade_state);
1,179✔
828

1,179✔
829
        // Just destroy the whole cluster
3,669✔
830
        m_dictionary_top->destroy_deep();
3,669✔
831
        m_dictionary_top.reset();
1,179✔
832

3,669✔
833
        update_child_ref(0, 0);
1,179✔
834

3,669✔
835
        if (recurse)
1,185✔
836
            _impl::TableFriend::remove_recursive(*get_table_unchecked(), cascade_state); // Throws
2,493✔
837
    }
3,903✔
838
}
1,296✔
839

840
bool Dictionary::init_from_parent(bool allow_create) const
230,457✔
841
{
341,415✔
842
    try {
341,415✔
843
        auto ref = Base::get_collection_ref();
341,415✔
844
        if ((ref || allow_create) && !m_dictionary_top) {
248,763✔
845
            Allocator& alloc = get_alloc();
203,919✔
846
            m_dictionary_top.reset(new Array(alloc));
203,919✔
847
            m_dictionary_top->set_parent(const_cast<Dictionary*>(this), 0);
203,919✔
848
            switch (m_key_type) {
203,919✔
849
                case type_String: {
203,919✔
850
                    m_keys.reset(new BPlusTree<StringData>(alloc));
203,919✔
851
                    break;
66,114✔
852
                }
✔
853
                case type_Int: {
×
854
                    m_keys.reset(new BPlusTree<Int>(alloc));
×
855
                    break;
×
856
                }
✔
857
                default:
×
858
                    break;
137,805✔
859
            }
203,919✔
860
            m_keys->set_parent(m_dictionary_top.get(), 0);
203,919✔
861
            m_values.reset(new BPlusTreeMixed(alloc));
203,919✔
862
            m_values->set_parent(m_dictionary_top.get(), 1);
203,919✔
863
        }
66,114✔
864

341,415✔
865
        if (ref) {
249,831✔
866
            m_dictionary_top->init_from_ref(ref);
205,428✔
867
            m_keys->init_from_parent();
205,428✔
868
            m_values->init_from_parent();
205,428✔
869
        }
158,139✔
870
        else {
44,403✔
871
            // dictionary detached
135,987✔
872
            if (!allow_create) {
105,411✔
873
                m_dictionary_top.reset();
90,933✔
874
                return false;
90,933✔
875
            }
29,925✔
876

14,478✔
877
            // Create dictionary
45,054✔
878
            m_dictionary_top->create(Array::type_HasRefs, false, 2, 0);
45,054✔
879
            m_values->create();
45,054✔
880
            m_keys->create();
45,054✔
881
            m_dictionary_top->update_parent();
45,054✔
882
        }
14,478✔
883

280,407✔
884
        return true;
225,705✔
885
    }
114,762✔
886
    catch (...) {
45✔
887
        m_dictionary_top.reset();
45✔
888
        throw;
45✔
889
    }
230,472✔
890
}
110,958✔
891

892
size_t Dictionary::do_find_key(Mixed key) const noexcept
884,967✔
893
{
1,324,587✔
894
    auto [ndx, actual_key] = find_impl(key);
1,324,587✔
895
    if (actual_key == key) {
1,311,069✔
896
        return ndx;
1,304,676✔
897
    }
446,745✔
898
    return realm::npos;
455,682✔
899
}
6,393✔
900

901
std::pair<size_t, Mixed> Dictionary::find_impl(Mixed key) const noexcept
995,196✔
902
{
1,488,426✔
903
    auto sz = m_keys->size();
1,488,426✔
904
    Mixed actual;
1,488,426✔
905
    if (sz && key.is_type(m_key_type)) {
1,455,936✔
906
        switch (m_key_type) {
1,440,804✔
907
            case type_String: {
1,440,804✔
908
                auto keys = static_cast<BPlusTree<StringData>*>(m_keys.get());
1,440,804✔
909
                StringData val = key.get<StringData>();
1,440,804✔
910
                IteratorAdapter help(keys);
1,440,804✔
911
                auto it = std::lower_bound(help.begin(), help.end(), val);
1,440,804✔
912
                if (it.index() < sz) {
1,389,270✔
913
                    actual = *it;
1,363,842✔
914
                }
1,415,376✔
915
                return {it.index(), actual};
478,098✔
916
                break;
×
917
            }
✔
918
            case type_Int: {
×
919
                auto keys = static_cast<BPlusTree<Int>*>(m_keys.get());
×
920
                Int val = key.get<Int>();
×
921
                IteratorAdapter help(keys);
×
922
                auto it = std::lower_bound(help.begin(), help.end(), val);
×
923
                if (it.index() < sz) {
×
924
                    actual = *it;
×
925
                }
×
926
                return {it.index(), actual};
×
927
                break;
×
928
            }
✔
929
            default:
×
930
                break;
497,595✔
931
        }
512,727✔
932
    }
15,132✔
933

47,622✔
934
    return {sz, actual};
528,924✔
935
}
15,132✔
936

937
Mixed Dictionary::do_get(size_t ndx) const
949,137✔
938
{
1,421,778✔
939
    Mixed val = m_values->get(ndx);
472,641✔
940

472,641✔
941
    // Filter out potential unresolved links
1,421,778✔
942
    if (val.is_type(type_TypedLink) && val.get<ObjKey>().is_unresolved()) {
473,745✔
943
        return {};
1,656✔
944
    }
948,585✔
945
    return val;
1,420,674✔
946
}
472,089✔
947

948
void Dictionary::do_erase(size_t ndx, Mixed key)
13,419✔
949
{
20,100✔
950
    CascadeState cascade_state(CascadeState::Mode::Strong);
20,100✔
951
    bool recurse = clear_backlink(ndx, cascade_state);
6,681✔
952

20,100✔
953
    if (recurse)
6,783✔
954
        _impl::TableFriend::remove_recursive(*get_table_unchecked(), cascade_state); // Throws
51✔
955

20,100✔
956
    if (Replication* repl = get_replication()) {
18,846✔
957
        repl->dictionary_erase(*this, ndx, key);
18,219✔
958
    }
6,054✔
959

20,100✔
960
    m_keys->erase(ndx);
20,100✔
961
    m_values->erase(ndx);
20,100✔
962
    bump_content_version();
20,100✔
963
}
6,681✔
964

965
Mixed Dictionary::do_get_key(size_t ndx) const
144,918✔
966
{
216,624✔
967
    switch (m_key_type) {
216,624✔
968
        case type_String: {
216,624✔
969
            return static_cast<BPlusTree<StringData>*>(m_keys.get())->get(ndx);
71,706✔
970
        }
✔
971
        case type_Int: {
×
972
            return static_cast<BPlusTree<Int>*>(m_keys.get())->get(ndx);
×
973
        }
✔
974
        default:
×
975
            break;
72,294✔
976
    }
977

×
978
    return {};
72,294✔
979
}
980

981
std::pair<Mixed, Mixed> Dictionary::do_get_pair(size_t ndx) const
93,660✔
982
{
139,869✔
983
    return {do_get_key(ndx), do_get(ndx)};
139,869✔
984
}
46,209✔
985

986
bool Dictionary::clear_backlink(size_t ndx, CascadeState& state) const
24,333✔
987
{
36,276✔
988
    auto value = m_values->get(ndx);
36,276✔
989
    if (value.is_type(type_TypedLink)) {
19,779✔
990
        return Base::remove_backlink(m_col_key, value.get_link(), state);
11,745✔
991
    }
20,406✔
992
    if (value.is_type(type_Dictionary)) {
8,064✔
993
        auto key = do_get_key(ndx);
33✔
994
        return get_dictionary(key.get_string())->remove_backlinks(state);
33✔
995
    }
16,470✔
996
    if (value.is_type(type_List)) {
8,091✔
997
        auto key = do_get_key(ndx);
75✔
998
        return get_list(key.get_string())->remove_backlinks(state);
75✔
999
    }
16,422✔
1000
    return false;
24,453✔
1001
}
8,016✔
1002

1003
void Dictionary::swap_content(Array& fields1, Array& fields2, size_t index1, size_t index2)
1004
{
×
1005
    std::string buf1, buf2;
1006

1007
    // Swap keys
×
1008
    REALM_ASSERT(m_key_type == type_String);
×
1009
    ArrayString keys(get_alloc());
×
1010
    keys.set_parent(&fields1, 1);
×
1011
    keys.init_from_parent();
×
1012
    buf1 = keys.get(index1);
1013

×
1014
    keys.set_parent(&fields2, 1);
×
1015
    keys.init_from_parent();
×
1016
    buf2 = keys.get(index2);
×
1017
    keys.set(index2, buf1);
1018

×
1019
    keys.set_parent(&fields1, 1);
×
1020
    keys.init_from_parent();
×
1021
    keys.set(index1, buf2);
1022

1023
    // Swap values
×
1024
    ArrayMixed values(get_alloc());
×
1025
    values.set_parent(&fields1, 2);
×
1026
    values.init_from_parent();
×
1027
    Mixed val1 = values.get(index1);
×
1028
    val1.use_buffer(buf1);
1029

×
1030
    values.set_parent(&fields2, 2);
×
1031
    values.init_from_parent();
×
1032
    Mixed val2 = values.get(index2);
×
1033
    val2.use_buffer(buf2);
×
1034
    values.set(index2, val1);
1035

×
1036
    values.set_parent(&fields1, 2);
×
1037
    values.init_from_parent();
×
1038
    values.set(index1, val2);
×
1039
}
1040

1041
Mixed Dictionary::find_value(Mixed value) const noexcept
1042
{
×
1043
    size_t ndx = update() ? m_values->find_first(value) : realm::npos;
×
1044
    return (ndx == realm::npos) ? Mixed{} : do_get_key(ndx);
×
1045
}
1046

1047
StableIndex Dictionary::build_index(Mixed key) const
3,066✔
1048
{
3,351✔
1049
    auto it = find(key);
3,351✔
1050
    int64_t index = (it != end()) ? m_values->get_key(it.index()) : 0;
3,351✔
1051
    return {index};
3,351✔
1052
}
285✔
1053

1054

1055
void Dictionary::verify() const
12,174✔
1056
{
18,264✔
1057
    m_keys->verify();
18,264✔
1058
    m_values->verify();
18,264✔
1059
    REALM_ASSERT(m_keys->size() == m_values->size());
18,264✔
1060
}
6,090✔
1061

1062
void Dictionary::get_key_type()
164,598✔
1063
{
244,371✔
1064
    m_key_type = get_table()->get_dictionary_key_type(m_col_key);
244,371!
1065
    if (!(m_key_type == type_String || m_key_type == type_Int))
79,773✔
1066
        throw Exception(ErrorCodes::InvalidDictionaryKey, "Dictionary keys can only be strings or integers");
164,598✔
1067
}
79,773✔
1068

1069
void Dictionary::migrate()
6✔
1070
{
3✔
1071
    // Dummy implementation of legacy dictionary cluster tree
9✔
1072
    class DictionaryClusterTree : public ClusterTree {
9✔
1073
    public:
9✔
1074
        DictionaryClusterTree(ArrayParent* owner, Allocator& alloc, size_t ndx)
9✔
1075
            : ClusterTree(nullptr, alloc, ndx)
9✔
1076
            , m_owner(owner)
9✔
1077
        {
9✔
1078
        }
3✔
1079

9✔
1080
        std::unique_ptr<ClusterNode> get_root_from_parent() final
9✔
1081
        {
9✔
1082
            return create_root_from_parent(m_owner, m_top_position_for_cluster_tree);
9✔
1083
        }
3✔
1084

9✔
1085
    private:
9✔
1086
        ArrayParent* m_owner;
9✔
1087
    };
3✔
1088

9✔
1089
    if (auto dict_ref = Base::get_collection_ref()) {
9✔
1090
        Allocator& alloc = get_alloc();
9✔
1091
        DictionaryClusterTree cluster_tree(this, alloc, 0);
9✔
1092
        if (cluster_tree.init_from_parent()) {
3✔
1093
            // Create an empty dictionary in the old ones place
9✔
1094
            Base::set_collection_ref(0);
9✔
1095
            ensure_created();
3✔
1096

9✔
1097
            ArrayString keys(alloc); // We only support string type keys.
9✔
1098
            ArrayMixed values(alloc);
9✔
1099
            constexpr ColKey key_col(ColKey::Idx{0}, col_type_String, ColumnAttrMask(), 0);
9✔
1100
            constexpr ColKey value_col(ColKey::Idx{1}, col_type_Mixed, ColumnAttrMask(), 0);
9✔
1101
            size_t nb_elements = cluster_tree.size();
9✔
1102
            cluster_tree.traverse([&](const Cluster* cluster) {
9✔
1103
                cluster->init_leaf(key_col, &keys);
9✔
1104
                cluster->init_leaf(value_col, &values);
9✔
1105
                auto sz = cluster->node_size();
63✔
1106
                for (size_t i = 0; i < sz; i++) {
30✔
1107
                    // Just use low level functions to insert elements. All keys must be legal and
27✔
1108
                    // unique and all values must match expected type. Links should just be preserved
27✔
1109
                    // so no need to worry about backlinks.
81✔
1110
                    StringData key = keys.get(i);
81✔
1111
                    auto [ndx, actual_key] = find_impl(key);
81✔
1112
                    REALM_ASSERT(actual_key != key);
81✔
1113
                    static_cast<BPlusTree<StringData>*>(m_keys.get())->insert(ndx, key);
81✔
1114
                    m_values->insert(ndx, values.get(i));
81✔
1115
                }
33✔
1116
                return IteratorControl::AdvanceToNext;
9✔
1117
            });
9✔
1118
            REALM_ASSERT(size() == nb_elements);
9✔
1119
            Array::destroy_deep(to_ref(dict_ref), alloc);
9✔
1120
        }
3✔
1121
        else {
1122
            REALM_UNREACHABLE();
1123
        }
6✔
1124
    }
9✔
1125
}
3✔
1126

1127
template <>
1128
void CollectionBaseImpl<DictionaryBase>::to_json(std::ostream&, JSONOutputMode,
1129
                                                 util::FunctionRef<void(const Mixed&)>) const
1130
{
×
1131
}
1132

1133
void Dictionary::to_json(std::ostream& out, JSONOutputMode output_mode,
1134
                         util::FunctionRef<void(const Mixed&)> fn) const
438✔
1135
{
657✔
1136
    if (output_mode == output_mode_xjson_plus) {
375✔
1137
        out << "{ \"$dictionary\": ";
234✔
1138
    }
516✔
1139
    out << "{";
219✔
1140

657✔
1141
    auto sz = size();
1,185✔
1142
    for (size_t i = 0; i < sz; i++) {
1,011✔
1143
        if (i > 0)
390✔
1144
            out << ",";
591✔
1145
        out << do_get_key(i) << ":";
792✔
1146
        Mixed val = do_get(i);
792✔
1147
        if (val.is_type(type_TypedLink)) {
408✔
1148
            fn(val);
216✔
1149
        }
456✔
1150
        else if (val.is_type(type_Dictionary)) {
204✔
1151
            DummyParent parent(this->get_table(), val.get_ref());
18✔
1152
            Dictionary dict(parent, 0);
18✔
1153
            dict.to_json(out, output_mode, fn);
18✔
1154
        }
378✔
1155
        else if (val.is_type(type_List)) {
198✔
1156
            DummyParent parent(this->get_table(), val.get_ref());
18✔
1157
            Lst<Mixed> list(parent, 0);
18✔
1158
            list.to_json(out, output_mode, fn);
18✔
1159
        }
366✔
1160
        else {
540✔
1161
            val.to_json(out, output_mode);
540✔
1162
        }
708✔
1163
    }
264✔
1164

657✔
1165
    out << "}";
657✔
1166
    if (output_mode == output_mode_xjson_plus) {
375✔
1167
        out << "}";
234✔
1168
    }
516✔
1169
}
219✔
1170

1171
ref_type Dictionary::get_collection_ref(Index index, CollectionType type) const
5,478✔
1172
{
6,138✔
1173
    auto ndx = m_values->find_key(index.get_salt());
6,138✔
1174
    if (ndx != realm::not_found) {
6,132✔
1175
        auto val = m_values->get(ndx);
6,129✔
1176
        if (val.is_type(DataType(int(type)))) {
6,129✔
1177
            return val.get_ref();
6,129✔
1178
        }
657✔
1179
        throw realm::IllegalOperation(util::format("Not a %1", type));
2,736✔
1180
    }
6✔
1181
    throw StaleAccessor("This collection is no more");
3✔
1182
    return 0;
2,745✔
1183
}
3✔
1184

1185
bool Dictionary::check_collection_ref(Index index, CollectionType type) const noexcept
1,014✔
1186
{
1,248✔
1187
    auto ndx = m_values->find_key(index.get_salt());
1,248✔
1188
    if (ndx != realm::not_found) {
1,230✔
1189
        return m_values->get(ndx).is_type(DataType(int(type)));
1,221✔
1190
    }
243✔
1191
    return false;
525✔
1192
}
9✔
1193

1194
void Dictionary::set_collection_ref(Index index, ref_type ref, CollectionType type)
1,650✔
1195
{
1,818✔
1196
    auto ndx = m_values->find_key(index.get_salt());
1,818✔
1197
    if (ndx == realm::not_found) {
168✔
1198
        throw StaleAccessor("Collection has been deleted");
×
1199
    }
1,650✔
1200
    m_values->set(ndx, Mixed(ref, type));
1,818✔
1201
}
168✔
1202

1203
bool Dictionary::update_if_needed() const
2,244✔
1204
{
2,568✔
1205
    auto status = update_if_needed_with_status();
2,568✔
1206
    if (status == UpdateStatus::Detached) {
324✔
1207
        throw StaleAccessor("CollectionList no longer exists");
×
1208
    }
2,244✔
1209
    return status == UpdateStatus::Updated;
2,568✔
1210
}
324✔
1211

1212
/************************* DictionaryLinkValues *************************/
1213

1214
DictionaryLinkValues::DictionaryLinkValues(const Obj& obj, ColKey col_key)
1215
    : m_source(obj, col_key)
1216
{
×
1217
    REALM_ASSERT_EX(col_key.get_type() == col_type_Link, col_key.get_type());
×
1218
}
1219

1220
DictionaryLinkValues::DictionaryLinkValues(const Dictionary& source)
1,557✔
1221
    : m_source(source)
3,114✔
1222
{
4,671✔
1223
    REALM_ASSERT_EX(source.get_value_data_type() == type_Link, source.get_value_data_type());
4,671✔
1224
}
1,557✔
1225

1226
ObjKey DictionaryLinkValues::get_key(size_t ndx) const
72✔
1227
{
108✔
1228
    Mixed val = m_source.get_any(ndx);
108✔
1229
    if (val.is_type(type_Link, type_TypedLink)) {
84✔
1230
        return val.get<ObjKey>();
72✔
1231
    }
48✔
1232
    return {};
60✔
1233
}
12✔
1234

1235
Obj DictionaryLinkValues::get_object(size_t row_ndx) const
2,400✔
1236
{
3,600✔
1237
    Mixed val = m_source.get_any(row_ndx);
3,600✔
1238
    if (val.is_type(type_TypedLink)) {
3,582✔
1239
        return get_table()->get_parent_group()->get_object(val.get_link());
3,573✔
1240
    }
1,209✔
1241
    return {};
1,218✔
1242
}
9✔
1243

1244
} // 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