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

realm / realm-core / github_pull_request_275914

25 Sep 2023 03:10PM UTC coverage: 92.915% (+1.7%) from 91.215%
github_pull_request_275914

Pull #6073

Evergreen

jedelbo
Merge tag 'v13.21.0' into next-major

"Feature/Bugfix release"
Pull Request #6073: Merge next-major

96928 of 177706 branches covered (0.0%)

8324 of 8714 new or added lines in 122 files covered. (95.52%)

181 existing lines in 28 files now uncovered.

247505 of 266379 relevant lines covered (92.91%)

7164945.17 hits per line

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

99.24
/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/replication.hpp>
25

26
#include <algorithm>
27

28
namespace realm {
29

30
namespace {
31
void validate_key_value(const Mixed& key)
32
{
57,387✔
33
    if (key.is_type(type_String)) {
57,387✔
34
        auto str = key.get_string();
115,440✔
35
        if (str.size()) {
115,440✔
36
            if (str[0] == '$')
115,425✔
37
                throw Exception(ErrorCodes::InvalidDictionaryKey, "Dictionary::insert: key must not start with '$'");
58,059✔
38
            if (memchr(str.data(), '.', str.size()))
115,404✔
39
                throw Exception(ErrorCodes::InvalidDictionaryKey, "Dictionary::insert: key must not contain '.'");
12✔
40
        }
115,398✔
41
    }
57,393✔
42
}
115,419✔
43

58,053✔
44
template <class T>
58,053✔
45
class SortedKeys {
46
public:
47
    SortedKeys(T* keys)
48
        : m_list(keys)
49
    {
478,485✔
50
    }
478,485✔
51
    CollectionIterator<T> begin() const
52
    {
478,485✔
53
        return CollectionIterator<T>(m_list, 0);
478,485✔
54
    }
558,747✔
55
    CollectionIterator<T> end() const
80,262✔
56
    {
478,485✔
57
        return CollectionIterator<T>(m_list, m_list->size());
478,485✔
58
    }
558,747✔
59

60
private:
61
    T* m_list;
62
};
63
} // namespace
13,947✔
64

13,947✔
65

13,947✔
66
/******************************** Dictionary *********************************/
13,947✔
67

13,947✔
68
Dictionary::Dictionary(const Obj& obj, ColKey col_key)
13,947✔
69
    : Base(obj, col_key)
13,947✔
70
    , m_key_type(m_obj.get_table()->get_dictionary_key_type(m_col_key))
13,947✔
71
{
92,187✔
72
    if (!col_key.is_dictionary()) {
92,187✔
73
        throw InvalidArgument(ErrorCodes::TypeMismatch, "Property not a dictionary");
13,947✔
74
    }
13,947✔
75
    if (!(m_key_type == type_String || m_key_type == type_Int))
78,240✔
76
        throw Exception(ErrorCodes::InvalidDictionaryKey, "Dictionary keys can only be strings or integers");
97,113✔
77
}
78,240✔
78

79
Dictionary::Dictionary(Allocator& alloc, ColKey col_key, ref_type ref)
2,835✔
80
    : Base(Obj{}, col_key)
2,835✔
81
    , m_key_type(type_String)
82
{
16,788✔
83
    REALM_ASSERT(ref);
13,953✔
84
    m_dictionary_top.reset(new Array(alloc));
16,788✔
85
    m_dictionary_top->init_from_ref(ref);
16,788✔
86
    m_keys.reset(new BPlusTree<StringData>(alloc));
16,788✔
87
    m_values.reset(new BPlusTree<Mixed>(alloc));
13,953✔
88
    m_keys->set_parent(m_dictionary_top.get(), 0);
16,788✔
89
    m_values->set_parent(m_dictionary_top.get(), 1);
16,788✔
90
    m_keys->init_from_parent();
13,953✔
91
    m_values->init_from_parent();
13,953✔
92
}
204,996✔
93

191,043✔
94
Dictionary::~Dictionary() = default;
115,521✔
95

96
Dictionary& Dictionary::operator=(const Dictionary& other)
170,391✔
97
{
173,019✔
98
    Base::operator=(static_cast<const Base&>(other));
2,628✔
99

2,628✔
100
    if (this != &other) {
11,568✔
101
        // Back to scratch
11,568✔
102
        m_dictionary_top.reset();
11,568✔
103
        reset_content_version();
2,628✔
104
    }
2,628✔
105

9,741✔
106
    return *this;
9,741✔
107
}
9,741✔
108

109
size_t Dictionary::size() const
110
{
224,481✔
111
    if (!update())
224,481✔
112
        return 0;
20,175✔
113

204,306✔
114
    return m_values->size();
204,306✔
115
}
211,410✔
116

117
DataType Dictionary::get_key_data_type() const
7,104✔
118
{
16,044✔
119
    return m_key_type;
16,044✔
120
}
16,044✔
121

122
DataType Dictionary::get_value_data_type() const
123
{
56,586✔
124
    return DataType(m_col_key.get_type());
7,113✔
125
}
56,586✔
126

49,473✔
127
bool Dictionary::is_null(size_t ndx) const
49,473✔
128
{
49,473✔
129
    return get_any(ndx).is_null();
130
}
131

26,541✔
132
Mixed Dictionary::get_any(size_t ndx) const
133
{
33,564✔
134
    // Note: `size()` calls `update_if_needed()`.
33,564✔
135
    CollectionBase::validate_index("get_any()", ndx, size());
33,564✔
136
    return do_get(ndx);
7,023✔
137
}
7,023✔
138

1,599✔
139
std::pair<Mixed, Mixed> Dictionary::get_pair(size_t ndx) const
1,353✔
140
{
90,771✔
141
    // Note: `size()` calls `update_if_needed()`.
89,172✔
142
    CollectionBase::validate_index("get_pair()", ndx, size());
89,172✔
143
    return do_get_pair(ndx);
107,013✔
144
}
107,013✔
145

13,923✔
146
Mixed Dictionary::get_key(size_t ndx) const
13,923✔
147
{
4,038✔
148
    // Note: `size()` calls `update_if_needed()`.
7,956✔
149
    CollectionBase::validate_index("get_key()", ndx, size());
7,956✔
150
    return do_get_key(ndx);
4,038✔
151
}
4,038✔
152

153
size_t Dictionary::find_any(Mixed value) const
8,892✔
154
{
10,488✔
155
    return size() ? m_values->find_first(value) : realm::not_found;
1,596✔
156
}
10,488✔
157

8,892✔
158
size_t Dictionary::find_any_key(Mixed key) const noexcept
8,892✔
159
{
51,915✔
160
    if (update()) {
43,023✔
161
        return do_find_key(key);
39,087✔
162
    }
33,060✔
163

23,247✔
164
    return realm::npos;
29,274✔
165
}
3,936✔
166

8,892✔
167
template <typename AggregateType>
8,892✔
168
void Dictionary::do_accumulate(size_t* return_ndx, AggregateType& agg) const
169
{
17,784✔
170
    size_t ndx = realm::npos;
8,898✔
171

17,784✔
172
    m_values->traverse([&](BPlusTreeNode* node, size_t offset) {
8,892✔
173
        auto leaf = static_cast<BPlusTree<Mixed>::LeafNode*>(node);
8,892✔
174
        size_t e = leaf->size();
10,944✔
175
        for (size_t i = 0; i < e; i++) {
36,204✔
176
            auto val = leaf->get(i);
27,312✔
177
            if (agg.accumulate(val)) {
27,312✔
178
                ndx = i + offset;
21,303✔
179
            }
19,251✔
180
        }
25,260✔
181
        // Continue
11,604✔
182
        return IteratorControl::AdvanceToNext;
11,604✔
183
    });
11,604✔
184

11,604✔
185
    if (return_ndx)
11,604✔
186
        *return_ndx = ndx;
6✔
187
}
8,892✔
188

2,082✔
189
util::Optional<Mixed> Dictionary::do_min(size_t* return_ndx) const
2,082✔
190
{
4,134✔
191
    aggregate_operations::Minimum<Mixed> agg;
2,073✔
192
    do_accumulate(return_ndx, agg);
2,073✔
193
    return agg.is_null() ? Mixed{} : agg.result();
2,073✔
194
}
2,061✔
195

21✔
196
util::Optional<Mixed> Dictionary::do_max(size_t* return_ndx) const
21✔
197
{
4,773✔
198
    aggregate_operations::Maximum<Mixed> agg;
2,754✔
199
    do_accumulate(return_ndx, agg);
2,754✔
200
    return agg.is_null() ? Mixed{} : agg.result();
2,754✔
201
}
2,712✔
202

42✔
203
util::Optional<Mixed> Dictionary::do_sum(size_t* return_cnt) const
42✔
204
{
4,101✔
205
    auto type = get_value_data_type();
2,124✔
206
    if (type == type_Int) {
2,124✔
207
        aggregate_operations::Sum<Int> agg;
63✔
208
        do_accumulate(nullptr, agg);
21✔
209
        if (return_cnt)
63✔
210
            *return_cnt = agg.items_counted();
51✔
211
        return Mixed{agg.result()};
21✔
212
    }
1,998✔
213
    else if (type == type_Double) {
4,038✔
214
        aggregate_operations::Sum<Double> agg;
2,019✔
215
        do_accumulate(nullptr, agg);
42✔
216
        if (return_cnt)
2,019✔
217
            *return_cnt = agg.items_counted();
1,977✔
218
        return Mixed{agg.result()};
42✔
219
    }
42✔
220
    else if (type == type_Float) {
4,065✔
221
        aggregate_operations::Sum<Float> agg;
2,088✔
222
        do_accumulate(nullptr, agg);
2,088✔
223
        if (return_cnt)
63✔
224
            *return_cnt = agg.items_counted();
21✔
225
        return Mixed{agg.result()};
63✔
226
    }
51✔
227

1,998✔
228
    aggregate_operations::Sum<Mixed> agg;
1,998✔
229
    do_accumulate(nullptr, agg);
4,002✔
230
    if (return_cnt)
2,007✔
231
        *return_cnt = agg.items_counted();
30✔
232
    return Mixed{agg.result()};
2,007✔
233
}
1,977✔
234

30✔
235
util::Optional<Mixed> Dictionary::do_avg(size_t* return_cnt) const
30✔
236
{
4,041✔
237
    auto type = get_value_data_type();
2,076✔
238
    if (type == type_Int) {
2,076✔
239
        aggregate_operations::Average<Int> agg;
51✔
240
        do_accumulate(nullptr, agg);
21✔
241
        if (return_cnt)
51✔
242
            *return_cnt = agg.items_counted();
39✔
243
        return agg.is_null() ? Mixed{} : agg.result();
21✔
244
    }
1,986✔
245
    else if (type == type_Double) {
3,990✔
246
        aggregate_operations::Average<Double> agg;
1,995✔
247
        do_accumulate(nullptr, agg);
30✔
248
        if (return_cnt)
1,995✔
249
            *return_cnt = agg.items_counted();
1,965✔
250
        return agg.is_null() ? Mixed{} : agg.result();
30✔
251
    }
30✔
252
    else if (type == type_Float) {
1,995✔
253
        aggregate_operations::Average<Float> agg;
333✔
254
        do_accumulate(nullptr, agg);
333✔
255
        if (return_cnt)
75✔
256
            *return_cnt = agg.items_counted();
69✔
257
        return agg.is_null() ? Mixed{} : agg.result();
123✔
258
    }
147✔
259
    // Decimal128 is covered with mixed as well.
2,124✔
260
    aggregate_operations::Average<Mixed> agg;
2,148✔
261
    do_accumulate(nullptr, agg);
2,148✔
262
    if (return_cnt)
2,085✔
263
        *return_cnt = agg.items_counted();
120✔
264
    return agg.is_null() ? Mixed{} : agg.result();
2,268✔
265
}
2,268✔
266

267
namespace {
300✔
268
bool can_minmax(DataType type)
300✔
269
{
345✔
270
    switch (type) {
369✔
271
        case type_Int:
273✔
272
        case type_Float:
297✔
273
        case type_Double:
339✔
274
        case type_Decimal:
339✔
275
        case type_Mixed:
327✔
276
        case type_Timestamp:
327✔
277
            return true;
483✔
278
        default:
483✔
279
            return false;
120✔
280
    }
303✔
281
}
303✔
282
bool can_sum(DataType type)
150✔
283
{
450✔
284
    switch (type) {
360✔
285
        case type_Int:
216✔
286
        case type_Float:
246✔
287
        case type_Double:
210✔
288
        case type_Decimal:
210✔
289
        case type_Mixed:
192✔
290
            return true;
156✔
291
        default:
192✔
292
            return false;
180✔
293
    }
300✔
294
}
300✔
295
} // anonymous namespace
153✔
296

153✔
297
util::Optional<Mixed> Dictionary::min(size_t* return_ndx) const
60✔
298
{
210✔
299
    if (!can_minmax(get_value_data_type())) {
243✔
300
        return std::nullopt;
114✔
301
    }
114✔
302
    if (update()) {
129✔
303
        return do_min(return_ndx);
57✔
304
    }
93✔
305
    if (return_ndx)
75✔
306
        *return_ndx = realm::not_found;
307
    return Mixed{};
36✔
308
}
186✔
309

150✔
310
util::Optional<Mixed> Dictionary::max(size_t* return_ndx) const
72✔
311
{
225✔
312
    if (!can_minmax(get_value_data_type())) {
231✔
313
        return std::nullopt;
108✔
314
    }
108✔
315
    if (update()) {
123✔
316
        return do_max(return_ndx);
54✔
317
    }
84✔
318
    if (return_ndx)
69✔
319
        *return_ndx = realm::not_found;
3✔
320
    return Mixed{};
39✔
321
}
189✔
322

150✔
323
util::Optional<Mixed> Dictionary::sum(size_t* return_cnt) const
72✔
324
{
222✔
325
    if (!can_sum(get_value_data_type())) {
228✔
326
        return std::nullopt;
120✔
327
    }
120✔
328
    if (update()) {
108✔
329
        return do_sum(return_cnt);
48✔
330
    }
78✔
331
    if (return_cnt)
60✔
332
        *return_cnt = 0;
333
    return Mixed{0};
30✔
334
}
2,553✔
335

2,523✔
336
util::Optional<Mixed> Dictionary::avg(size_t* return_cnt) const
2,523✔
337
{
2,673✔
338
    if (!can_sum(get_value_data_type())) {
2,673✔
339
        return std::nullopt;
72✔
340
    }
72✔
341
    if (update()) {
78✔
342
        return do_avg(return_cnt);
48✔
343
    }
11,727✔
344
    if (return_cnt)
30✔
345
        *return_cnt = 0;
9,156✔
346
    return Mixed{};
9,186✔
347
}
2,553✔
348

349
void Dictionary::align_indices(std::vector<size_t>& indices) const
350
{
2,523✔
351
    auto sz = size();
2,523✔
352
    auto sz2 = indices.size();
4,731✔
353
    indices.reserve(sz);
4,731✔
354
    if (sz < sz2) {
4,731✔
355
        // If list size has decreased, we have to start all over
17,082✔
356
        indices.clear();
16,440✔
357
        sz2 = 0;
17,082✔
358
    }
2,208✔
359
    for (size_t i = sz2; i < sz; i++) {
11,679✔
360
        // If list size has increased, just add the missing indices
9,156✔
361
        indices.push_back(i);
9,156✔
362
    }
11,049✔
363
}
4,416✔
364

1,893✔
365
namespace {
1,893✔
366
template <class T>
367
void do_sort(std::vector<size_t>& indices, bool ascending, const std::vector<T>& values)
368
{
2,523✔
369
    auto b = indices.begin();
2,523✔
370
    auto e = indices.end();
2,523✔
371
    std::sort(b, e, [ascending, &values](size_t i1, size_t i2) {
19,464✔
372
        return ascending ? values[i1] < values[i2] : values[i2] < values[i1];
19,464✔
373
    });
20,049✔
374
}
3,108✔
375
} // anonymous namespace
900✔
376

315✔
377
void Dictionary::sort(std::vector<size_t>& indices, bool ascending) const
378
{
2,208✔
379
    align_indices(indices);
1,893✔
380
    do_sort(indices, ascending, m_values->get_all());
1,956✔
381
}
1,956✔
382

315✔
383
void Dictionary::distinct(std::vector<size_t>& indices, util::Optional<bool> ascending) const
384
{
315✔
385
    align_indices(indices);
567✔
386
    auto values = m_values->get_all();
567✔
387
    do_sort(indices, ascending.value_or(true), values);
567✔
388
    indices.erase(std::unique(indices.begin(), indices.end(),
567✔
389
                              [&values](size_t i1, size_t i2) {
900✔
390
                                  return values[i1] == values[i2];
1,152✔
391
                              }),
1,152✔
392
                  indices.end());
567✔
393

567✔
394
    if (!ascending) {
567✔
395
        // need to return indices in original ordering
315✔
396
        std::sort(indices.begin(), indices.end(), std::less<size_t>());
63✔
397
    }
63✔
398
}
315✔
399

400
void Dictionary::sort_keys(std::vector<size_t>& indices, bool ascending) const
×
401
{
252✔
402
    align_indices(indices);
252✔
403
#ifdef REALM_DEBUG
252✔
404
    if (indices.size() > 1) {
252✔
405
        // We rely in the design that the keys are already sorted
504✔
406
        switch (m_key_type) {
504✔
407
            case type_String: {
504✔
408
                SortedKeys help(static_cast<BPlusTree<StringData>*>(m_keys.get()));
504✔
409
                auto is_sorted = std::is_sorted(help.begin(), help.end());
378✔
410
                REALM_ASSERT(is_sorted);
378✔
411
                break;
378✔
412
            }
126✔
413
            case type_Int: {
126✔
414
                SortedKeys help(static_cast<BPlusTree<Int>*>(m_keys.get()));
252✔
415
                auto is_sorted = std::is_sorted(help.begin(), help.end());
416
                REALM_ASSERT(is_sorted);
417
                break;
63✔
418
            }
419
            default:
63✔
420
                break;
63✔
421
        }
252✔
422
    }
252✔
423
#endif
252✔
424
    if (ascending) {
2,385✔
425
        std::sort(indices.begin(), indices.end());
2,259✔
426
    }
2,259✔
427
    else {
2,259✔
428
        std::sort(indices.begin(), indices.end(), std::greater<size_t>());
2,259✔
429
    }
2,259✔
430
}
252✔
431

432
void Dictionary::distinct_keys(std::vector<size_t>& indices, util::Optional<bool>) const
156✔
433
{
219✔
434
    // we rely on the design of dictionary to assume that the keys are unique
219✔
435
    align_indices(indices);
219✔
436
}
219✔
437

156✔
438

156✔
439
Obj Dictionary::create_and_insert_linked_object(Mixed key)
440
{
2,094✔
441
    Table& t = *get_target_table();
2,250✔
442
    auto o = t.is_embedded() ? t.create_linked_object() : t.create_object();
2,250✔
443
    insert(key, o.get_key());
2,094✔
444
    return o;
2,094✔
445
}
2,151✔
446

57✔
447
Mixed Dictionary::get(Mixed key) const
57✔
448
{
6,471✔
449
    if (auto opt_val = try_get(key)) {
6,483✔
450
        return *opt_val;
6,474✔
451
    }
6,474✔
452
    throw KeyNotFound("Dictionary::get");
66✔
453
}
9✔
454

455
util::Optional<Mixed> Dictionary::try_get(Mixed key) const noexcept
18✔
456
{
421,308✔
457
    if (update()) {
421,308✔
458
        auto ndx = do_find_key(key);
420,825✔
459
        if (ndx != realm::npos) {
420,825✔
460
            return do_get(ndx);
419,421✔
461
        }
419,421✔
462
    }
1,905✔
463
    return {};
1,887✔
464
}
1,887✔
465

150✔
466
Dictionary::Iterator Dictionary::begin() const
150✔
467
{
31,275✔
468
    // Need an update because the `Dictionary::Iterator` constructor relies on
31,245✔
469
    // `m_clusters` to determine if it was already at end.
31,275✔
470
    update();
31,275✔
471
    return Iterator(this, 0);
31,275✔
472
}
31,275✔
473

474
Dictionary::Iterator Dictionary::end() const
475
{
66,273✔
476
    return Iterator(this, size());
66,273✔
477
}
66,261✔
478

6,465✔
479
std::pair<Dictionary::Iterator, bool> Dictionary::insert(Mixed key, Mixed value)
12✔
480
{
51,417✔
481
    if (key.get_type() != m_key_type) {
51,405✔
482
        throw InvalidArgument(ErrorCodes::InvalidDictionaryKey, "Dictionary::insert: Invalid key type");
483
    }
419,478✔
484
    if (value.is_null()) {
470,883✔
485
        if (!m_col_key.is_nullable()) {
421,587✔
486
            throw InvalidArgument(ErrorCodes::InvalidDictionaryValue, "Dictionary::insert: Value cannot be null");
418,992✔
487
        }
417,906✔
488
    }
466,716✔
489
    else {
50,382✔
490
        if (m_col_key.get_type() == col_type_Link && value.get_type() == type_TypedLink) {
50,382✔
491
            if (m_obj.get_table()->get_opposite_table_key(m_col_key) != value.get<ObjLink>().get_table_key()) {
2,898✔
492
                throw InvalidArgument(ErrorCodes::InvalidDictionaryValue, "Dictionary::insert: Wrong object type");
3✔
493
            }
3✔
494
        }
60,234✔
495
        else if (m_col_key.get_type() != col_type_Mixed && value.get_type() != DataType(m_col_key.get_type())) {
47,484✔
496
            throw InvalidArgument(ErrorCodes::InvalidDictionaryValue, "Dictionary::insert: Wrong value type");
3✔
497
        }
12,753✔
498
    }
64,149✔
499

64,149✔
500
    validate_key_value(key);
51,399✔
501
    ensure_created();
51,399✔
502

92,922✔
503
    ObjLink new_link;
92,922✔
504
    if (value.is_type(type_TypedLink)) {
92,922✔
505
        new_link = value.get<ObjLink>();
3,531✔
506
        if (!new_link.is_unresolved())
3,531✔
507
            m_obj.get_table()->get_parent_group()->validate(new_link);
55,587✔
508
    }
55,632✔
509
    else if (value.is_type(type_Link)) {
99,969✔
510
        auto target_table = m_obj.get_table()->get_opposite_table(m_col_key);
5,955✔
511
        auto key = value.get<ObjKey>();
5,955✔
512
        if (!key.is_unresolved() && !target_table->is_valid(key)) {
58,056✔
513
            throw InvalidArgument(ErrorCodes::KeyNotFound, "Target object not found");
52,101✔
514
        }
2,619✔
515
        new_link = ObjLink(target_table->get_key(), key);
5,952✔
516
        value = Mixed(new_link);
5,952✔
517
    }
55,434✔
518

100,881✔
519
    if (!m_dictionary_top) {
100,881✔
520
        throw StaleAccessor("Stale dictionary");
1,356✔
521
    }
3✔
522

51,399✔
523
    bool old_entry = false;
51,399✔
524
    auto [ndx, actual_key] = find_impl(key);
99,522✔
525
    if (actual_key != key) {
99,522✔
526
        // key does not already exist
46,803✔
527
        switch (m_key_type) {
46,803✔
528
            case type_String:
94,929✔
529
                static_cast<BPlusTree<StringData>*>(m_keys.get())->insert(ndx, key.get_string());
46,803✔
530
                break;
46,803✔
NEW
531
            case type_Int:
×
532
                static_cast<BPlusTree<Int>*>(m_keys.get())->insert(ndx, key.get_int());
52,098✔
533
                break;
52,098✔
534
            default:
535
                break;
52,098✔
536
        }
98,901✔
537
        m_values->insert(ndx, value);
46,803✔
538
    }
98,901✔
539
    else {
56,691✔
540
        old_entry = true;
8,175✔
541
    }
8,175✔
542

54,930✔
543
    if (Replication* repl = this->m_obj.get_replication()) {
54,978✔
544
        if (old_entry) {
93,939✔
545
            repl->dictionary_set(*this, ndx, key, value);
10,158✔
546
        }
10,158✔
547
        else {
47,613✔
548
            repl->dictionary_insert(*this, ndx, key, value);
41,442✔
549
        }
41,442✔
550
    }
51,594✔
551

57,567✔
552
    bump_content_version();
57,567✔
553

51,396✔
554
    ObjLink old_link;
103,491✔
555
    if (old_entry) {
51,396✔
556
        Mixed old_value = m_values->get(ndx);
4,581✔
557
        if (old_value.is_type(type_TypedLink)) {
4,581✔
558
            old_link = old_value.get<ObjLink>();
52,404✔
559
        }
52,404✔
560
        m_values->set(ndx, value);
56,676✔
561
    }
4,581✔
562

98,871✔
563
    if (new_link != old_link) {
98,871✔
564
        CascadeState cascade_state(CascadeState::Mode::Strong);
56,964✔
565
        bool recurse = m_obj.replace_backlink(m_col_key, old_link, new_link, cascade_state);
56,964✔
566
        if (recurse)
9,489✔
567
            _impl::TableFriend::remove_recursive(*m_obj.get_table(), cascade_state); // Throws
144✔
568
    }
9,489✔
569

51,396✔
570
    return {Iterator(this, ndx), !old_entry};
51,396✔
571
}
98,871✔
572

47,475✔
573
const Mixed Dictionary::operator[](Mixed key)
47,475✔
574
{
411,018✔
575
    auto ret = try_get(key);
411,018✔
576
    if (!ret) {
411,018✔
577
        ret = Mixed{};
3✔
578
        insert(key, Mixed{});
52,098✔
579
    }
45,915✔
580

410,394✔
581
    return *ret;
410,394✔
582
}
448,314✔
583

41,916✔
584
Obj Dictionary::get_object(StringData key)
41,916✔
585
{
51,942✔
586
    if (auto val = try_get(key)) {
6,030✔
587
        if ((*val).is_type(type_TypedLink)) {
56,574✔
588
            return get_table()->get_parent_group()->get_object((*val).get_link());
4,419✔
589
        }
56,514✔
590
    }
53,706✔
591
    return {};
6,207✔
592
}
6,207✔
593

297✔
594
bool Dictionary::contains(Mixed key) const noexcept
297✔
595
{
6,132✔
596
    return find_any_key(key) != realm::npos;
6,132✔
597
}
1,536✔
598

52,095✔
599
Dictionary::Iterator Dictionary::find(Mixed key) const noexcept
9,771✔
600
{
25,374✔
601
    auto ndx = find_any_key(key);
25,374✔
602
    if (ndx != realm::npos) {
15,747✔
603
        return Iterator(this, ndx);
17,841✔
604
    }
8,070✔
605
    return end();
59,628✔
606
}
59,628✔
607

608
UpdateStatus Dictionary::update_if_needed() const
609
{
1,117,005✔
610
    auto status = Base::update_if_needed();
1,117,005✔
611
    switch (status) {
1,117,005✔
612
        case UpdateStatus::Detached: {
3✔
613
            m_dictionary_top.reset();
3✔
614
            return UpdateStatus::Detached;
3✔
615
        }
616
        case UpdateStatus::NoChange: {
1,049,688✔
617
            if (m_dictionary_top && m_dictionary_top->is_attached()) {
1,049,688✔
618
                return UpdateStatus::NoChange;
625,986✔
619
            }
625,986✔
620
            // The tree has not been initialized yet for this accessor, so
23,340✔
621
            // perform lazy initialization by treating it as an update.
23,340✔
622
            [[fallthrough]];
21,789✔
623
        }
21,729✔
624
        case UpdateStatus::Updated: {
89,046✔
625
            bool attached = init_from_parent(false);
86,232✔
626
            return attached ? UpdateStatus::Updated : UpdateStatus::Detached;
86,232✔
627
        }
1,611✔
628
    }
629
    REALM_UNREACHABLE();
630
}
1,536✔
631

1,536✔
632
UpdateStatus Dictionary::ensure_created()
1,536✔
633
{
51,396✔
634
    auto status = Base::ensure_created();
51,396✔
635
    switch (status) {
67,155✔
636
        case UpdateStatus::Detached:
15,759✔
637
            break; // Not possible (would have thrown earlier).
15,759✔
638
        case UpdateStatus::NoChange: {
40,437✔
639
            if (m_dictionary_top && m_dictionary_top->is_attached()) {
40,437✔
640
                return UpdateStatus::NoChange;
35,016✔
641
            }
35,016✔
642
            // The tree has not been initialized yet for this accessor, so
4,620✔
643
            // perform lazy initialization by treating it as an update.
4,620✔
644
            [[fallthrough]];
4,632✔
645
        }
4,632✔
646
        case UpdateStatus::Updated: {
23,871✔
647
            bool attached = init_from_parent(true);
23,871✔
648
            REALM_ASSERT(attached);
23,871✔
649
            return attached ? UpdateStatus::Updated : UpdateStatus::Detached;
23,859✔
650
        }
651
    }
54✔
652

54✔
653
    REALM_UNREACHABLE();
54✔
654
}
54✔
655

656
bool Dictionary::try_erase(Mixed key)
657
{
686,055✔
658
    validate_key_value(key);
686,055✔
659
    if (!update())
686,055✔
660
        return false;
3✔
661

5,991✔
662
    auto ndx = do_find_key(key);
5,991✔
663
    if (ndx == realm::npos) {
5,988✔
664
        return false;
611,247✔
665
    }
611,247✔
666

598,380✔
667
    do_erase(ndx, key);
598,380✔
668

5,373✔
669
    return true;
5,373✔
670
}
22,998✔
671

17,625✔
672

87,057✔
673
void Dictionary::erase(Mixed key)
674
{
5,433✔
675
    if (!try_erase(key)) {
92,490✔
676
        throw KeyNotFound(util::format("Cannot remove key %1 from dictionary: key not found", key));
87,366✔
677
    }
57,111✔
678
}
5,433✔
679

680
auto Dictionary::erase(Iterator it) -> Iterator
681
{
1,191✔
682
    auto pos = it.m_ndx;
1,191✔
683
    CollectionBase::validate_index("erase()", pos, size());
1,191✔
684

53,436✔
685
    do_erase(pos, do_get_key(pos));
53,436✔
686
    if (pos < size())
1,191✔
687
        pos++;
201✔
688
    return {this, pos};
25,473✔
689
}
25,473✔
690

24,282✔
691
void Dictionary::nullify(Mixed key)
24,282✔
692
{
52,386✔
693
    REALM_ASSERT(m_dictionary_top);
141✔
694
    auto ndx = do_find_key(key);
141✔
695
    REALM_ASSERT(ndx != realm::npos);
6,099✔
696

6,099✔
697
    if (Replication* repl = this->m_obj.get_replication()) {
6,099✔
698
        repl->dictionary_set(*this, ndx, key, Mixed());
135✔
699
    }
135✔
700

6,099✔
701
    m_values->set(ndx, Mixed());
6,099✔
702
}
756✔
703

615✔
704
void Dictionary::remove_backlinks(CascadeState& state) const
705
{
5,514✔
706
    if (size() > 0) {
171✔
707
        m_values->for_all([&](Mixed val) {
6,006✔
708
            clear_backlink(val, state);
6,006✔
709
        });
663✔
710
    }
171✔
711
}
171✔
712

5,403✔
713
void Dictionary::clear()
5,403✔
714
{
1,554✔
715
    if (size() > 0) {
1,554✔
716
        // TODO: Should we have a "dictionary_clear" instruction?
6,543✔
717
        Replication* repl = m_obj.get_replication();
1,140✔
718
        bool recurse = false;
1,140✔
719
        CascadeState cascade_state(CascadeState::Mode::Strong);
2,331✔
720
        for (auto&& elem : *this) {
5,667✔
721
            if (clear_backlink(elem.second, cascade_state))
5,667✔
722
                recurse = true;
9✔
723
            if (repl) {
5,667✔
724
                // Logically we always erase the first element
5,658✔
725
                repl->dictionary_erase(*this, 0, elem.first);
4,668✔
726
            }
5,658✔
727
        }
5,667✔
728

1,140✔
729
        // Just destroy the whole cluster
1,140✔
730
        m_dictionary_top->destroy_deep();
1,287✔
731
        m_dictionary_top.reset();
1,287✔
732

1,287✔
733
        update_child_ref(0, 0);
1,140✔
734

1,287✔
735
        if (recurse)
1,278✔
736
            _impl::TableFriend::remove_recursive(*m_obj.get_table(), cascade_state); // Throws
141✔
737
    }
1,278✔
738
}
1,245✔
739

147✔
740
bool Dictionary::init_from_parent(bool allow_create) const
147✔
741
{
108,480✔
742
    auto ref = m_obj._get<int64_t>(m_col_key.get_index());
108,480✔
743

108,633✔
744
    if ((ref || allow_create) && !m_dictionary_top) {
108,633✔
745
        m_dictionary_top.reset(new Array(m_obj.get_alloc()));
64,842✔
746
        m_dictionary_top->set_parent(const_cast<Dictionary*>(this), m_obj.get_row_ndx());
64,836✔
747
        switch (m_key_type) {
64,836✔
748
            case type_String: {
64,836✔
749
                m_keys.reset(new BPlusTree<StringData>(m_obj.get_alloc()));
64,695✔
750
                break;
64,689✔
751
            }
6✔
752
            case type_Int: {
9✔
753
                m_keys.reset(new BPlusTree<Int>(m_obj.get_alloc()));
6✔
754
                break;
6✔
755
            }
6✔
NEW
756
            default:
×
NEW
757
                break;
×
758
        }
64,689✔
759
        m_keys->set_parent(m_dictionary_top.get(), 0);
64,689✔
760
        m_values.reset(new BPlusTree<Mixed>(m_obj.get_alloc()));
64,695✔
761
        m_values->set_parent(m_dictionary_top.get(), 1);
64,695✔
762
    }
64,692✔
763

108,483✔
764
    if (ref) {
108,483✔
765
        m_dictionary_top->init_from_parent();
65,775✔
766
        m_keys->init_from_parent();
65,775✔
767
        m_values->init_from_parent();
65,778✔
768
    }
65,778✔
769
    else {
42,711✔
770
        // dictionary detached
42,861✔
771
        if (!allow_create) {
42,708✔
772
            m_dictionary_top.reset();
29,424✔
773
            return false;
29,484✔
774
        }
29,484✔
775

13,344✔
776
        // Create dictionary
13,344✔
777
        m_dictionary_top->create(Array::type_HasRefs, false, 2, 0);
13,344✔
778
        m_values->create();
13,344✔
779
        m_keys->create();
13,344✔
780
        m_dictionary_top->update_parent();
13,284✔
781
    }
13,284✔
782

108,480✔
783
    return true;
108,480!
784
}
108,480✔
785

786
size_t Dictionary::do_find_key(Mixed key) const noexcept
×
787
{
440,679✔
788
    auto [ndx, actual_key] = find_impl(key);
440,679!
789
    if (actual_key == key) {
440,679✔
790
        return ndx;
434,244✔
791
    }
434,244✔
792
    return realm::npos;
6,435!
793
}
6,435✔
794

×
795
std::pair<size_t, Mixed> Dictionary::find_impl(Mixed key) const noexcept
796
{
492,090✔
797
    auto sz = m_keys->size();
492,090✔
798
    Mixed actual;
492,090✔
799
    if (sz && key.is_type(m_key_type)) {
492,090✔
800
        switch (m_key_type) {
478,233✔
801
            case type_String: {
478,293✔
802
                auto keys = static_cast<BPlusTree<StringData>*>(m_keys.get());
478,233✔
803
                StringData val = key.get<StringData>();
478,233✔
804
                SortedKeys help(keys);
478,446✔
805
                auto it = std::lower_bound(help.begin(), help.end(), val);
478,446✔
806
                if (it.index() < sz) {
479,178✔
807
                    actual = *it;
454,173✔
808
                }
454,173✔
809
                return {it.index(), actual};
478,698✔
810
                break;
465✔
811
            }
267✔
812
            case type_Int: {
6✔
813
                auto keys = static_cast<BPlusTree<Int>*>(m_keys.get());
6✔
814
                Int val = key.get<Int>();
6✔
815
                SortedKeys help(keys);
261✔
816
                auto it = std::lower_bound(help.begin(), help.end(), val);
6✔
817
                if (it.index() < sz) {
6✔
818
                    actual = *it;
6✔
819
                }
732✔
820
                return {it.index(), actual};
213✔
821
                break;
822
            }
823
            default:
22,386✔
824
                break;
22,386✔
825
        }
36,243✔
826
    }
13,857✔
827

13,857✔
828
    return {sz, actual};
15,141✔
829
}
15,141✔
830

1,173✔
831
Mixed Dictionary::do_get(size_t ndx) const
1,173✔
832
{
516,768✔
833
    Mixed val = m_values->get(ndx);
516,768✔
834

516,762✔
835
    // Filter out potential unresolved links
516,762✔
836
    if (val.is_type(type_TypedLink) && val.get<ObjKey>().is_unresolved()) {
520,137✔
837
        return {};
4,884✔
838
    }
351✔
839
    return val;
519,795✔
840
}
515,253✔
841

1,173✔
842
void Dictionary::do_erase(size_t ndx, Mixed key)
1,173✔
843
{
6,558✔
844
    auto old_value = m_values->get(ndx);
7,731✔
845

6,558✔
846
    CascadeState cascade_state(CascadeState::Mode::Strong);
7,731✔
847
    bool recurse = clear_backlink(old_value, cascade_state);
6,561✔
848
    if (recurse)
7,731✔
849
        _impl::TableFriend::remove_recursive(*m_obj.get_table(), cascade_state); // Throws
1,335✔
850

6,558✔
851
    if (Replication* repl = this->m_obj.get_replication()) {
6,558✔
852
        repl->dictionary_erase(*this, ndx, key);
117,285✔
853
    }
117,285✔
854

117,897✔
855
    m_keys->erase(ndx);
117,897✔
856
    m_values->erase(ndx);
72,801✔
857

72,801✔
858
    bump_content_version();
72,801✔
859
}
72,801✔
860

66,243✔
861
Mixed Dictionary::do_get_key(size_t ndx) const
66,243✔
862
{
160,695✔
863
    switch (m_key_type) {
94,452✔
864
        case type_String: {
94,452✔
865
            return static_cast<BPlusTree<StringData>*>(m_keys.get())->get(ndx);
94,452✔
NEW
866
        }
×
NEW
867
        case type_Int: {
×
NEW
868
            return static_cast<BPlusTree<Int>*>(m_keys.get())->get(ndx);
✔
NEW
869
        }
×
870
        default:
66,243✔
871
            break;
66,243✔
872
    }
66,243✔
873

66,243✔
874
    return {};
66,243✔
875
}
876

111,339✔
877
std::pair<Mixed, Mixed> Dictionary::do_get_pair(size_t ndx) const
67,512✔
878
{
156,681✔
879
    return {do_get_key(ndx), do_get(ndx)};
156,681✔
880
}
156,681✔
881

43,827✔
882
bool Dictionary::clear_backlink(Mixed value, CascadeState& state) const
883
{
55,524✔
884
    if (value.is_type(type_TypedLink)) {
41,943✔
885
        return m_obj.remove_backlink(m_col_key, value.get_link(), state);
34,041✔
886
    }
34,041✔
887
    return false;
7,902✔
888
}
7,902✔
889

13,581✔
890
void Dictionary::swap_content(Array& fields1, Array& fields2, size_t index1, size_t index2)
13,581✔
891
{
13,581✔
892
    std::string buf1, buf2;
13,581✔
893

13,581✔
894
    // Swap keys
895
    REALM_ASSERT(m_key_type == type_String);
81,093✔
896
    ArrayString keys(m_obj.get_alloc());
18✔
897
    keys.set_parent(&fields1, 1);
18✔
898
    keys.init_from_parent();
18✔
899
    buf1 = keys.get(index1);
18✔
900

18✔
901
    keys.set_parent(&fields2, 1);
111,339✔
902
    keys.init_from_parent();
903
    buf2 = keys.get(index2);
904
    keys.set(index2, buf1);
438,867✔
905

438,867✔
906
    keys.set_parent(&fields1, 1);
438,867✔
907
    keys.init_from_parent();
432,786✔
908
    keys.set(index1, buf2);
432,786✔
909

6,081✔
910
    // Swap values
6,081✔
911
    ArrayMixed values(m_obj.get_alloc());
912
    values.set_parent(&fields1, 2);
913
    values.init_from_parent();
490,965✔
914
    Mixed val1 = values.get(index1);
490,965✔
915
    val1.use_buffer(buf1);
490,965✔
916

490,965✔
917
    values.set_parent(&fields2, 2);
476,826✔
918
    values.init_from_parent();
476,826✔
919
    Mixed val2 = values.get(index2);
476,826✔
920
    val2.use_buffer(buf2);
476,826✔
921
    values.set(index2, val1);
476,826✔
922

476,826✔
923
    values.set_parent(&fields1, 2);
476,826✔
924
    values.init_from_parent();
452,073✔
925
    values.set(index1, val2);
452,073✔
926
}
476,826✔
927

928
Mixed Dictionary::find_value(Mixed value) const noexcept
929
{
54✔
930
    size_t ndx = update() ? m_values->find_first(value) : realm::npos;
54✔
931
    return (ndx == realm::npos) ? Mixed{} : do_get_key(ndx);
54✔
932
}
54✔
933

934
void Dictionary::verify() const
×
935
{
6,087✔
936
    m_keys->verify();
6,087✔
937
    m_values->verify();
6,087✔
938
    REALM_ASSERT(m_keys->size() == m_values->size());
6,087✔
939
}
6,087✔
940

2✔
941
void Dictionary::migrate()
942
{
14,142✔
943
    // Dummy implementation of legacy dictionary cluster tree
14,142✔
944
    class DictionaryClusterTree : public ClusterTree {
3✔
945
    public:
14,142✔
946
        DictionaryClusterTree(ArrayParent* owner, Allocator& alloc, size_t ndx)
14,142✔
947
            : ClusterTree(nullptr, alloc, ndx)
3✔
948
            , m_owner(owner)
3✔
949
        {
474,807✔
950
        }
474,807✔
951

3✔
952
        std::unique_ptr<ClusterNode> get_root_from_parent() final
3✔
953
        {
474,807✔
954
            return create_root_from_parent(m_owner, m_top_position_for_cluster_tree);
348✔
955
        }
348✔
956

474,462✔
957
    private:
474,462✔
958
        ArrayParent* m_owner;
3✔
959
    };
3✔
960

6,531✔
961
    if (auto dict_ref = m_obj._get<int64_t>(get_col_key().get_index())) {
6,531✔
962
        DictionaryClusterTree cluster_tree(this, m_obj.get_alloc(), m_obj.get_row_ndx());
3✔
963
        if (cluster_tree.init_from_parent()) {
6,531✔
964
            // Create an empty dictionary in the old ones place
6,531✔
965
            m_obj.set_int(get_col_key(), 0);
6,531✔
966
            ensure_created();
54✔
967

3✔
968
            ArrayString keys(m_obj.get_alloc()); // We only support string type keys.
6,531✔
969
            ArrayMixed values(m_obj.get_alloc());
5,919✔
970
            constexpr ColKey key_col(ColKey::Idx{0}, col_type_String, ColumnAttrMask(), 0);
5,919✔
971
            constexpr ColKey value_col(ColKey::Idx{1}, col_type_Mixed, ColumnAttrMask(), 0);
3✔
972
            size_t nb_elements = cluster_tree.size();
6,531✔
973
            cluster_tree.traverse([&](const Cluster* cluster) {
6,531✔
974
                cluster->init_leaf(key_col, &keys);
3✔
975
                cluster->init_leaf(value_col, &values);
6,531✔
976
                auto sz = cluster->node_size();
6,531✔
977
                for (size_t i = 0; i < sz; i++) {
30✔
978
                    // Just use low level functions to insert elements. All keys must be legal and
27✔
979
                    // unique and all values must match expected type. Links should just be preserved
77,769✔
980
                    // so no need to worry about backlinks.
77,769✔
981
                    StringData key = keys.get(i);
77,769✔
982
                    auto [ndx, actual_key] = find_impl(key);
77,769✔
983
                    REALM_ASSERT(actual_key != key);
27✔
984
                    static_cast<BPlusTree<StringData>*>(m_keys.get())->insert(ndx, key);
27✔
985
                    m_values->insert(ndx, values.get(i));
27✔
986
                }
27✔
987
                return IteratorControl::AdvanceToNext;
3✔
988
            });
3✔
989
            REALM_ASSERT(size() == nb_elements);
3✔
990
            Array::destroy_deep(to_ref(dict_ref), m_obj.get_alloc());
3✔
991
        }
3✔
992
        else {
×
993
            REALM_UNREACHABLE();
994
        }
995
    }
49,473✔
996
}
49,473✔
997

49,470✔
998
/************************* DictionaryLinkValues *************************/
999

1000
DictionaryLinkValues::DictionaryLinkValues(const Obj& obj, ColKey col_key)
11,070✔
1001
    : m_source(obj, col_key)
11,070✔
1002
{
3,378✔
1003
    REALM_ASSERT_EX(col_key.get_type() == col_type_Link, col_key.get_type());
3,378✔
1004
}
7,692✔
1005

7,692✔
1006
DictionaryLinkValues::DictionaryLinkValues(const Dictionary& source)
1007
    : m_source(source)
1008
{
1,557✔
1009
    REALM_ASSERT_EX(source.get_value_data_type() == type_Link, source.get_value_data_type());
1,557✔
1010
}
1,557✔
1011

1012
ObjKey DictionaryLinkValues::get_key(size_t ndx) const
×
1013
{
36✔
1014
    Mixed val = m_source.get_any(ndx);
36✔
1015
    if (val.is_type(type_Link, type_TypedLink)) {
36✔
1016
        return val.get<ObjKey>();
24✔
1017
    }
24✔
1018
    return {};
12✔
1019
}
12✔
1020

1021
Obj DictionaryLinkValues::get_object(size_t row_ndx) const
1022
{
1,200✔
1023
    Mixed val = m_source.get_any(row_ndx);
1,200✔
1024
    if (val.is_type(type_TypedLink)) {
1,200✔
1025
        return get_table()->get_parent_group()->get_object(val.get_link());
1,191✔
1026
    }
1,191✔
1027
    return {};
9✔
1028
}
9✔
1029

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

© 2025 Coveralls, Inc