• 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

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

19
#include "testsettings.hpp"
20

21
#include <realm.hpp>
22

23
#include "test.hpp"
24
#include <chrono>
25
#include <set>
26
// #include <valgrind/callgrind.h>
27
// valgrind --tool=callgrind --instr-atstart=no ./realm-tests
28

29
using namespace std::chrono;
30

31
using namespace realm;
32
using namespace realm::test_util;
33

34
#ifndef CALLGRIND_START_INSTRUMENTATION
35
#define CALLGRIND_START_INSTRUMENTATION
36
#endif
37

38
#ifndef CALLGRIND_STOP_INSTRUMENTATION
39
#define CALLGRIND_STOP_INSTRUMENTATION
40
#endif
41

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

71
extern unsigned int unit_test_random_seed;
72

73
TEST(Dictionary_Basics)
74
{
2✔
75
    Group g;
2✔
76
    auto cmp = [this](Mixed x, Mixed y) {
8✔
77
        CHECK_EQUAL(x, y);
8✔
78
    };
8✔
79

80
    Dictionary dummy;
2✔
81
    CHECK_THROW_ANY(dummy.insert("Hello", "world"));
2✔
82

83
    auto foo = g.add_table("foo");
2✔
84
    auto col_dict = foo->add_column_dictionary(type_Mixed, "dictionaries");
2✔
85

86
    Obj obj1 = foo->create_object();
2✔
87
    Obj obj2 = foo->create_object();
2✔
88
    StringData foo_key("foo.bar", 3); // The '.' must not be considered part of the key
2✔
89

90
    {
2✔
91
        Dictionary dict = obj1.get_dictionary(col_dict);
2✔
92

93
        CHECK_EQUAL(dict.size(), 0);
2✔
94
        CHECK_EQUAL(dict.find_any(9), realm::npos);
2✔
95

96
        CHECK(dict.insert("Hello", 9).second);
2✔
97
        CHECK_EQUAL(dict.size(), 1);
2✔
98
        CHECK_EQUAL(dict.get("Hello").get_int(), 9);
2✔
99
        CHECK(dict.contains("Hello"));
2✔
100
        CHECK_NOT(dict.insert("Hello", 10).second);
2✔
101
        CHECK_EQUAL(dict.get("Hello").get_int(), 10);
2✔
102
        CHECK_EQUAL(dict.find_any(9), realm::npos);
2✔
103
        CHECK_EQUAL(dict.find_any(10), 0);
2✔
104

105
        dict.insert("Goodbye", "cruel world");
2✔
106
        CHECK_EQUAL(dict.size(), 2);
2✔
107
        CHECK_EQUAL(dict["Goodbye"].get_string(), "cruel world");
2✔
108
        CHECK_THROW_ANY(dict.get("Baa").get_string()); // Within range
2✔
109
        CHECK_THROW_ANY(dict.get("Foo").get_string()); // Outside range
2✔
110
        CHECK_THROW_ANY(dict.insert("$foo", ""));      // Must not start with '$'
2✔
111
        CHECK_THROW_ANY(dict.insert("foo.bar", ""));   // Must not contain '.'
2✔
112
        CHECK(dict.insert(foo_key, 9).second);         // This should be ok
2✔
113
    }
2✔
114
    {
2✔
115
        Dictionary dict = obj1.get_dictionary(col_dict);
2✔
116
        CHECK_EQUAL(dict.size(), 3);
2✔
117
        cmp(dict.get("Hello"), 10);
2✔
118
        cmp(dict["Goodbye"], "cruel world");
2✔
119
        auto it = dict.find("puha");
2✔
120
        CHECK(it == dict.end());
2✔
121
        it = dict.find("Goodbye");
2✔
122
        cmp((*it).second, "cruel world");
2✔
123
        dict.erase(it);
2✔
124
        CHECK_EQUAL(dict.size(), 2);
2✔
125
        cmp(dict["Goodbye"], Mixed());
2✔
126
        CHECK_EQUAL(dict.size(), 3);
2✔
127
        dict.erase("foo");
2✔
128
        CHECK_EQUAL(dict.size(), 2);
2✔
129
        dict.clear();
2✔
130
        CHECK_EQUAL(dict.size(), 0);
2✔
131
        // Check that you can insert after clear
132
        CHECK(dict.insert("Hello", 9).second);
2✔
133
        CHECK_EQUAL(dict.size(), 1);
2✔
134
        dict.erase("Hello");
2✔
135
        CHECK_EQUAL(dict.size(), 0);
2✔
136
        CHECK_THROW_ANY(dict.erase("Hello"));   // Dictionary empty
2✔
137
        CHECK_THROW_ANY(dict.erase("$foo"));    // Must not start with '$'
2✔
138
        CHECK_THROW_ANY(dict.erase("foo.bar")); // Must not contain '.'
2✔
139
    }
2✔
140
    {
2✔
141
        Dictionary dict1 = obj1.get_dictionary(col_dict);
2✔
142
        Dictionary dict2 = obj2.get_dictionary(col_dict);
2✔
143
        CHECK_EQUAL(dict2.size(), 0);
2✔
144
        CHECK_THROW_ANY(dict2.get("Baa").get_string());
2✔
145

146
        dict2.insert("Hello", "world");
2✔
147
        dict1.insert("Hello", 9);
2✔
148
        obj2.remove();
2✔
149
        CHECK_NOT(dict2.is_attached());
2✔
150
        CHECK_EQUAL(dict1.size(), 1);
2✔
151
        dict1 = dict2;
2✔
152
        CHECK_NOT(dict1.is_attached());
2✔
153
    }
2✔
154
}
2✔
155

156
TEST(Dictionary_Links)
157
{
2✔
158
    Group g;
2✔
159
    auto cmp = [this](Mixed x, Mixed y) {
10✔
160
        CHECK_EQUAL(x, y);
10✔
161
    };
10✔
162

163
    auto dogs = g.add_table_with_primary_key("dog", type_String, "name");
2✔
164
    auto cats = g.add_table_with_primary_key("cat", type_String, "name");
2✔
165
    auto persons = g.add_table_with_primary_key("person", type_String, "name");
2✔
166
    auto col_dict = persons->add_column_dictionary(*dogs, "dictionaries");
2✔
167

168
    Obj adam = persons->create_object_with_primary_key("adam");
2✔
169
    Obj bernie = persons->create_object_with_primary_key("bernie");
2✔
170
    Obj pluto = dogs->create_object_with_primary_key("pluto");
2✔
171
    Obj lady = dogs->create_object_with_primary_key("lady");
2✔
172
    Obj garfield = cats->create_object_with_primary_key("garfield");
2✔
173

174
    {
2✔
175
        Dictionary dict = adam.get_dictionary(col_dict);
2✔
176
        CHECK(dict.insert("Pet", pluto).second);
2✔
177
        CHECK_EQUAL(pluto.get_backlink_count(), 1);
2✔
178
        CHECK_NOT(dict.insert("Pet", lady).second);
2✔
179
        CHECK(dict.insert("Dog", lady).second); // Have two links pointing to lady
2✔
180
        CHECK_EQUAL(pluto.get_backlink_count(), 0);
2✔
181
        CHECK_EQUAL(lady.get_backlink_count(*persons, col_dict), 2);
2✔
182
        CHECK_EQUAL(lady.get_backlink(*persons, col_dict, 0), adam.get_key());
2✔
183
        CHECK_EQUAL(lady.get_backlink_count(), 2);
2✔
184
        CHECK_EQUAL(dict.get("Pet").get<ObjKey>(), lady.get_key());
2✔
185
        lady.remove();
2✔
186
        cmp(dict["Pet"], Mixed());
2✔
187
        CHECK_THROW_ANY(dict.insert("Pet", garfield));
2✔
188
        CHECK_THROW_ANY(dict.insert("Pet", Mixed(ObjKey(27))));
2✔
189

190
        // Reinsert lady
191
        lady = dogs->create_object_with_primary_key("lady");
2✔
192
        dict.insert("Pet", lady);
2✔
193
        lady.invalidate(); // Make lady a tombstone :-(
2✔
194
        cmp(dict["Pet"], Mixed());
2✔
195
        lady = dogs->create_object_with_primary_key("lady");
2✔
196
        cmp(dict["Pet"].get<ObjKey>(), lady.get_key());
2✔
197

198
        auto invalid_link = pluto.get_link();
2✔
199
        pluto.remove();
2✔
200
        CHECK_THROW(dict.insert("Pet", invalid_link), InvalidArgument);
2✔
201

202
        dict = bernie.get_dictionary(col_dict);
2✔
203
        dict.insert("Pet", lady);
2✔
204
        CHECK_EQUAL(lady.get_backlink_count(), 2);
2✔
205
        adam.remove();
2✔
206
        CHECK_EQUAL(lady.get_backlink_count(), 1);
2✔
207
        dict.erase("Pet");
2✔
208
        CHECK_EQUAL(lady.get_backlink_count(), 0);
2✔
209

210
        dict.insert("Pet", dogs->get_objkey_from_primary_key("pongo"));
2✔
211
        cmp(dict["Pet"], Mixed());
2✔
212
        Obj pongo = dogs->create_object_with_primary_key("pongo");
2✔
213
        CHECK_EQUAL(pongo.get_backlink_count(), 1);
2✔
214
        cmp(dict["Pet"].get<ObjKey>(), pongo.get_key());
2✔
215
    }
2✔
216
}
2✔
217

218
TEST(Dictionary_TypedLinks)
219
{
2✔
220
    Group g;
2✔
221
    auto cmp = [this](Mixed x, Mixed y) {
10✔
222
        CHECK_EQUAL(x, y);
10✔
223
    };
10✔
224

225
    auto dogs = g.add_table_with_primary_key("dog", type_String, "name");
2✔
226
    auto persons = g.add_table_with_primary_key("person", type_String, "name");
2✔
227
    auto col_dict = persons->add_column_dictionary(type_Mixed, "dictionaries");
2✔
228

229
    Obj adam = persons->create_object_with_primary_key("adam");
2✔
230
    Obj pluto = dogs->create_object_with_primary_key("pluto");
2✔
231
    Obj lady = dogs->create_object_with_primary_key("lady");
2✔
232

233
    {
2✔
234
        Dictionary dict = adam.get_dictionary(col_dict);
2✔
235
        CHECK(dict.insert("Pet", pluto).second);
2✔
236
        CHECK_EQUAL(pluto.get_backlink_count(), 1);
2✔
237
        CHECK_NOT(dict.insert("Pet", lady).second);
2✔
238
        CHECK_EQUAL(pluto.get_backlink_count(), 0);
2✔
239
        CHECK_EQUAL(lady.get_backlink_count(*persons, col_dict), 1);
2✔
240
        CHECK_EQUAL(lady.get_backlink(*persons, col_dict, 0), adam.get_key());
2✔
241
        CHECK_EQUAL(lady.get_backlink_count(), 1);
2✔
242
        lady.remove();
2✔
243
        cmp(dict["Pet"], Mixed());
2✔
244

245
        // Reinsert lady
246
        lady = dogs->create_object_with_primary_key("lady");
2✔
247
        dict.insert("Pet", lady);
2✔
248
        lady.invalidate(); // Make lady a tombstone :-(
2✔
249
        cmp(dict["Pet"], Mixed());
2✔
250
        lady = dogs->create_object_with_primary_key("lady");
2✔
251
        cmp(dict["Pet"], Mixed(lady.get_link()));
2✔
252

253
        auto invalid_link = pluto.get_link();
2✔
254
        pluto.remove();
2✔
255
        CHECK_THROW(dict.insert("Pet", invalid_link), InvalidArgument);
2✔
256

257
        dict.insert("Pet", Mixed(ObjLink(dogs->get_key(), dogs->get_objkey_from_primary_key("pongo"))));
2✔
258
        cmp(dict["Pet"], Mixed());
2✔
259
        Obj pongo = dogs->create_object_with_primary_key("pongo");
2✔
260
        CHECK_EQUAL(pongo.get_backlink_count(), 1);
2✔
261
        cmp(dict["Pet"], Mixed(pongo.get_link()));
2✔
262
    }
2✔
263
}
2✔
264

265
TEST(Dictionary_Clear)
266
{
2✔
267
    Group g;
2✔
268
    auto dogs = g.add_table_with_primary_key("dog", type_String, "name");
2✔
269
    auto persons = g.add_table_with_primary_key("person", type_String, "name");
2✔
270
    auto col_dict_mixed = persons->add_column_dictionary(type_Mixed, "any");
2✔
271
    auto col_dict_implicit = persons->add_column_dictionary(*dogs, "implicit");
2✔
272

273
    Obj adam = persons->create_object_with_primary_key("adam");
2✔
274
    Obj pluto = dogs->create_object_with_primary_key("pluto");
2✔
275
    Obj lady = dogs->create_object_with_primary_key("lady");
2✔
276

277
    adam.get_dictionary(col_dict_mixed).insert("Dog1", pluto);
2✔
278
    adam.get_dictionary(col_dict_implicit).insert("DOg2", lady.get_key());
2✔
279

280
    CHECK_EQUAL(lady.get_backlink_count(), 1);
2✔
281
    CHECK_EQUAL(pluto.get_backlink_count(), 1);
2✔
282
    persons->clear();
2✔
283
    CHECK_EQUAL(lady.get_backlink_count(), 0);
2✔
284
    CHECK_EQUAL(pluto.get_backlink_count(), 0);
2✔
285
    g.verify();
2✔
286
}
2✔
287

288
TEST(Dictionary_Transaction)
289
{
2✔
290
    SHARED_GROUP_TEST_PATH(path);
2✔
291
    auto hist = make_in_realm_history();
2✔
292
    DBRef db = DB::create(*hist, path);
2✔
293
    ObjKey k0, k2;
2✔
294
    ColKey col_dict;
2✔
295
    auto cmp = [this](Mixed x, Mixed y) {
6✔
296
        CHECK_EQUAL(x, y);
6✔
297
    };
6✔
298

299
    auto rt = db->start_read();
2✔
300
    {
2✔
301
        WriteTransaction wt(db);
2✔
302
        auto foo = wt.add_table("foo");
2✔
303
        col_dict = foo->add_column_dictionary(type_Mixed, "dictionaries");
2✔
304

305
        Obj obj0 = foo->create_object();
2✔
306
        Obj obj1 = foo->create_object();
2✔
307
        Obj obj2 = foo->create_object();
2✔
308
        k0 = obj0.get_key();
2✔
309
        k2 = obj2.get_key();
2✔
310
        Dictionary dict = obj0.get_dictionary(col_dict);
2✔
311
        dict.insert("Hello", 9);
2✔
312
        dict.insert("Goodbye", "cruel world");
2✔
313

314
        dict = obj1.get_dictionary(col_dict);
2✔
315
        dict.insert("Link", obj2.get_link());
2✔
316
        wt.commit();
2✔
317
    }
2✔
318
    rt->advance_read();
2✔
319
    rt->verify();
2✔
320
    ConstTableRef table = rt->get_table("foo");
2✔
321
    Dictionary dict;
2✔
322
    dict = table->get_object(k0).get_dictionary(col_dict);
2✔
323
    cmp(dict.get("Hello"), 9);
2✔
324
    cmp(dict.get("Goodbye"), "cruel world");
2✔
325

326
    {
2✔
327
        WriteTransaction wt(db);
2✔
328
        auto foo = wt.get_table("foo");
2✔
329
        Dictionary d = foo->get_object(k0).get_dictionary(col_dict);
2✔
330
        d.insert("Good morning", "sunshine");
2✔
331
        foo->remove_object(k2); // Nullifies link in obj1.dictionaries["Link"]
2✔
332
        wt.commit();
2✔
333
    }
2✔
334
    rt->advance_read();
2✔
335
    rt->verify();
2✔
336
    // rt->to_json(std::cout);
337
    cmp(dict.get("Good morning"), "sunshine");
2✔
338

339
    {
2✔
340
        auto wt = db->start_write();
2✔
341
        auto foo = wt->get_table("foo");
2✔
342
        Dictionary d = foo->get_object(k0).get_dictionary(col_dict);
2✔
343
        d.clear();
2✔
344

345
        wt->commit_and_continue_as_read();
2✔
346
        wt->promote_to_write();
2✔
347
        wt->verify();
2✔
348
    }
2✔
349
}
2✔
350

351
TEST(Dictionary_Aggregate)
352
{
2✔
353
    SHARED_GROUP_TEST_PATH(path);
2✔
354
    auto hist = make_in_realm_history();
2✔
355
    DBRef db = DB::create(*hist, path);
2✔
356
    auto tr = db->start_write();
2✔
357
    auto foo = tr->add_table("foo");
2✔
358
    auto col_dict = foo->add_column_dictionary(type_Int, "dictionaries");
2✔
359

360
    Obj obj1 = foo->create_object();
2✔
361
    Obj obj2 = foo->create_object();
2✔
362
    Dictionary dict = obj1.get_dictionary(col_dict);
2✔
363
    std::vector<int64_t> random_idx(100);
2✔
364
    std::iota(random_idx.begin(), random_idx.end(), 0);
2✔
365
    std::shuffle(random_idx.begin(), random_idx.end(), std::mt19937(unit_test_random_seed));
2✔
366

367
    for (int i = 0; i < 100; i++) {
202✔
368
        dict.insert(util::to_string(i), random_idx[i]);
200✔
369
    }
200✔
370

371
    std::vector<size_t> indices;
2✔
372
    dict.sort(indices, true);
2✔
373
    int64_t last = -1;
2✔
374
    for (size_t ndc : indices) {
200✔
375
        int64_t val = dict.get_any(ndc).get_int();
200✔
376
        CHECK_GREATER(val, last);
200✔
377
        last = val;
200✔
378
    }
200✔
379
    tr->commit_and_continue_as_read();
2✔
380

381
    size_t ndx;
2✔
382
    auto max = dict.max(&ndx);
2✔
383
    CHECK(max);
2✔
384
    CHECK_EQUAL(max->get_int(), 99);
2✔
385

386
    auto min = dict.min(&ndx);
2✔
387
    CHECK(min);
2✔
388
    CHECK_EQUAL(min->get_int(), 0);
2✔
389

390
    size_t cnt;
2✔
391
    auto sum = dict.sum(&cnt);
2✔
392
    CHECK(sum);
2✔
393
    CHECK_EQUAL(cnt, 100);
2✔
394
    CHECK_EQUAL(sum->get_int(), 50 * 99);
2✔
395

396
    auto avg = dict.avg(&cnt);
2✔
397
    CHECK(avg);
2✔
398
    CHECK_EQUAL(cnt, 100);
2✔
399
    CHECK_EQUAL(avg->get_double(), double(50 * 99) / 100);
2✔
400

401
    dict = obj2.get_dictionary(col_dict);
2✔
402
    max = dict.max(&ndx);
2✔
403
    CHECK(max);
2✔
404
    CHECK(max->is_null());
2✔
405
    CHECK_EQUAL(ndx, realm::npos);
2✔
406
}
2✔
407

408
NONCONCURRENT_TEST(Dictionary_Performance)
409
{
2✔
410
    size_t nb_reps = 1000;
2✔
411

412
    Group g;
2✔
413
    auto foo = g.add_table("foo");
2✔
414
    auto col_dict = foo->add_column_dictionary(type_Int, "dictionaries", false);
2✔
415

416
    Obj obj1 = foo->create_object();
2✔
417
    Dictionary dict = obj1.get_dictionary(col_dict);
2✔
418
    std::vector<int64_t> random_idx(nb_reps);
2✔
419
    std::vector<std::string> random_str(nb_reps);
2✔
420
    std::iota(random_idx.begin(), random_idx.end(), 0);
2✔
421
    std::shuffle(random_idx.begin(), random_idx.end(), std::mt19937(unit_test_random_seed));
2✔
422

423
    std::transform(random_idx.begin(), random_idx.end(), random_str.begin(), [](int64_t i) {
2,000✔
424
        return std::string("s") + util::to_string(i);
2,000✔
425
    });
2,000✔
426

427
    auto t1 = steady_clock::now();
2✔
428

429
    int64_t i = 0;
2✔
430
    for (auto& str : random_str) {
2,000✔
431
        dict.insert(str, i++);
2,000✔
432
    }
2,000✔
433

434
    auto t2 = steady_clock::now();
2✔
435

436
    CALLGRIND_START_INSTRUMENTATION;
2✔
437
    i = 0;
2✔
438
    for (auto& str : random_str) {
2,000✔
439
        CHECK_EQUAL(dict.get(str), Mixed(i++));
2,000✔
440
    }
2,000✔
441
    CALLGRIND_STOP_INSTRUMENTATION;
2✔
442

443
    auto t3 = steady_clock::now();
2✔
444

445
    std::cout << nb_reps << " values in dictionary" << std::endl;
2✔
446
    std::cout << "    insertion: " << duration_cast<nanoseconds>(t2 - t1).count() / nb_reps << " ns/val" << std::endl;
2✔
447
    std::cout << "    lookup: " << duration_cast<nanoseconds>(t3 - t2).count() / nb_reps << " ns/val" << std::endl;
2✔
448
}
2✔
449

450
TEST(Dictionary_Tombstones)
451
{
2✔
452
    Group g;
2✔
453
    auto foos = g.add_table_with_primary_key("class_Foo", type_Int, "id");
2✔
454
    auto bars = g.add_table_with_primary_key("class_Bar", type_String, "id");
2✔
455
    auto col_int = bars->add_column(type_Int, "value");
2✔
456
    ColKey col_dict = foos->add_column_dictionary(*bars, "dict");
2✔
457

458
    auto foo = foos->create_object_with_primary_key(123);
2✔
459
    auto a = bars->create_object_with_primary_key("a").set(col_int, 1);
2✔
460
    auto b = bars->create_object_with_primary_key("b").set(col_int, 2);
2✔
461

462
    auto dict = foo.get_dictionary(col_dict);
2✔
463
    dict.insert("a", a);
2✔
464
    dict.insert("b", b);
2✔
465

466
    auto q = bars->where(dict).equal(col_int, 1);
2✔
467
    CHECK_EQUAL(q.count(), 1);
2✔
468

469
    a.invalidate();
2✔
470

471
    CHECK_EQUAL(dict.size(), 2);
2✔
472
    CHECK((*dict.find("a")).second.is_null());
2✔
473

474
    CHECK(dict.find("b") != dict.end());
2✔
475

476
    CHECK_EQUAL(q.count(), 0);
2✔
477
}
2✔
478

479
TEST(Dictionary_UseAfterFree)
480
{
2✔
481
    Group g;
2✔
482
    auto foos = g.add_table("Foo");
2✔
483
    ColKey col_dict = foos->add_column_dictionary(type_String, "dict");
2✔
484

485
    auto foo = foos->create_object();
2✔
486
    auto dict = foo.get_dictionary(col_dict);
2✔
487
    dict.insert("a", "monkey");
2✔
488
    dict.insert("b", "lion");
2✔
489
    dict.insert("c", "à");
2✔
490

491
    Query q;
2✔
492
    {
2✔
493
        auto str = std::make_unique<std::string>("à");
2✔
494
        auto col = foos->column<Dictionary>(col_dict);
2✔
495
        q = col.equal(StringData(*str), true); // A copy of the string must be taken here
2✔
496
    }
2✔
497
    CHECK_EQUAL(q.count(), 1);
2✔
498
}
2✔
499

500
NONCONCURRENT_TEST(Dictionary_HashCollision)
501
{
2✔
502
    constexpr int64_t nb_entries = 100;
2✔
503
    Group g;
2✔
504
    auto foos = g.add_table("Foo");
2✔
505
    ColKey col_dict = foos->add_column_dictionary(type_Int, "dict");
2✔
506

507
    auto foo = foos->create_object();
2✔
508
    auto dict = foo.get_dictionary(col_dict);
2✔
509
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
510
        std::string key = "key" + util::to_string(i);
200✔
511
        dict.insert(Mixed(key), i);
200✔
512
        dict.erase(key);
200✔
513
        dict.insert(Mixed(key), i);
200✔
514
    }
200✔
515

516
    // g.to_json(std::cout);
517

518
    // Check that values can be read back
519
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
520
        std::string key = "key" + util::to_string(i);
200✔
521
        CHECK_EQUAL(dict[key].get_int(), i);
200✔
522
    }
200✔
523

524
    // And these keys should not exist
525
    for (int64_t i = nb_entries; i < nb_entries + 20; i++) {
42✔
526
        std::string key = "key" + util::to_string(i);
40✔
527
        CHECK_NOT(dict.contains(key));
40✔
528
    }
40✔
529

530
    // Check that a query can find matching key and value
531
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
532
        std::string key = "key" + util::to_string(i);
200✔
533
        Query q = (foos->column<Dictionary>(col_dict).key(key) == Mixed(i));
200✔
534
        CHECK_EQUAL(q.count(), 1);
200✔
535
    }
200✔
536

537
    // Check that dict.find works
538
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
539
        std::string key = "key" + util::to_string(i);
200✔
540
        auto it = dict.find(key);
200✔
541
        CHECK_EQUAL((*it).second.get_int(), i);
200✔
542
    }
200✔
543

544
    // And these keys should not be found
545
    for (int64_t i = nb_entries; i < nb_entries + 20; i++) {
42✔
546
        std::string key = "key" + util::to_string(i);
40✔
547
        CHECK(dict.find(key) == dict.end());
40✔
548
    }
40✔
549

550
    auto check_aggregates = [&]() {
4✔
551
        int64_t expected_sum = nb_entries * (nb_entries - 1) / 2;
4✔
552
        size_t count = 0;
4✔
553
        util::Optional<Mixed> actual_sum = dict.sum(&count);
4✔
554
        CHECK(actual_sum);
4✔
555
        CHECK(actual_sum && *actual_sum == Mixed{expected_sum});
4✔
556
        CHECK_EQUAL(count, nb_entries);
4✔
557
        Query q = (foos->column<Dictionary>(col_dict).sum() == Mixed(expected_sum));
4✔
558
        CHECK_EQUAL(q.count(), 1);
4✔
559

560
        util::Optional<Mixed> actual_min = dict.min();
4✔
561
        CHECK(actual_min && *actual_min == Mixed{0});
4✔
562
        q = (foos->column<Dictionary>(col_dict).min() == Mixed(0));
4✔
563
        CHECK_EQUAL(q.count(), 1);
4✔
564

565
        util::Optional<Mixed> actual_max = dict.max();
4✔
566
        CHECK(actual_max && *actual_max == Mixed{nb_entries - 1});
4✔
567
        q = (foos->column<Dictionary>(col_dict).max() == Mixed(nb_entries - 1));
4✔
568
        CHECK_EQUAL(q.count(), 1);
4✔
569

570
        util::Optional<Mixed> actual_avg = dict.avg(&count);
4✔
571
        Mixed expected_avg{Decimal128(expected_sum) / nb_entries};
4✔
572
        CHECK_EQUAL(count, nb_entries);
4✔
573
        CHECK(actual_avg && *actual_avg == expected_avg);
4✔
574
        q = (foos->column<Dictionary>(col_dict).average() == expected_avg);
4✔
575
        CHECK_EQUAL(q.count(), 1);
4✔
576
    };
4✔
577

578
    check_aggregates();
2✔
579

580
    // Update with new values
581
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
582
        std::string key = "key" + util::to_string(i);
200✔
583
        dict.insert(Mixed(key), nb_entries - i - 1);
200✔
584
    }
200✔
585

586
    check_aggregates();
2✔
587

588
    // Check that values was updated properly
589
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
590
        std::string key = "key" + util::to_string(i);
200✔
591
        CHECK_EQUAL(dict[key].get_int(), nb_entries - i - 1);
200✔
592
    }
200✔
593

594
    // Now erase one entry at a time and check that the rest of the values are ok
595
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
596
        std::string key = "key" + util::to_string(i);
200✔
597
        dict.erase(key);
200✔
598
        CHECK_EQUAL(dict.size(), nb_entries - i - 1);
200✔
599

600
        // Check that remaining entries still can be found
601
        for (int64_t j = i + 1; j < nb_entries; j++) {
10,100✔
602
            std::string key_j = "key" + util::to_string(j);
9,900✔
603
            CHECK_EQUAL(dict[key_j].get_int(), nb_entries - j - 1);
9,900✔
604
        }
9,900✔
605
    }
200✔
606
}
2✔
607

608
class ModelDict {
609
public:
610
    // random generators
611
    std::string get_rnd_used_key()
612
    {
1,307✔
613
        if (the_map.size() == 0)
1,307✔
UNCOV
614
            return std::string();
×
615
        size_t idx = size_t(rnd()) % the_map.size();
1,307✔
616
        auto it = the_map.begin();
1,307✔
617
        while (idx--) {
216,918✔
618
            it++;
215,611✔
619
        }
215,611✔
620
        return it->first;
1,307✔
621
    }
1,307✔
622
    std::string get_rnd_unused_key()
623
    {
2,693✔
624
        int64_t key_i;
2,693✔
625
        std::string key;
2,693✔
626
        do {
2,693✔
627
            key_i = rnd();
2,693✔
628
            key = std::to_string(key_i);
2,693✔
629
        } while (the_map.find(key) != the_map.end());
2,693✔
630
        return key;
2,693✔
631
    }
2,693✔
632
    Mixed get_rnd_value()
633
    {
2,693✔
634
        int64_t v = rnd();
2,693✔
635
        return Mixed(v);
2,693✔
636
    }
2,693✔
637
    // model access
638
    Mixed get_value_by_key(std::string key)
639
    {
1,307✔
640
        auto it = the_map.find(key);
1,307✔
641
        REALM_ASSERT(it != the_map.end());
1,307✔
642
        return it->second;
1,307✔
643
    }
1,307✔
644
    bool insert(std::string key, Mixed value)
645
    {
2,693✔
646
        the_map[key] = value;
2,693✔
647
        return true;
2,693✔
648
    }
2,693✔
649
    bool erase(std::string key)
650
    {
1,307✔
651
        the_map.erase(key);
1,307✔
652
        return true;
1,307✔
653
    }
1,307✔
654

655
    std::map<std::string, Mixed> the_map;
656
    util::UniqueFunction<int64_t(void)> rnd = std::mt19937(unit_test_random_seed);
657
};
658

659
NONCONCURRENT_TEST(Dictionary_HashRandomOpsTransaction)
660
{
2✔
661
    ModelDict model;
2✔
662
    SHARED_GROUP_TEST_PATH(path);
2✔
663
    auto hist = make_in_realm_history();
2✔
664
    DBRef db = DB::create(*hist, path);
2✔
665
    auto tr = db->start_write();
2✔
666
    ColKey col_dict;
2✔
667
    Dictionary dict;
2✔
668
    {
2✔
669
        // initial setup
670
        auto foos = tr->add_table("Foo");
2✔
671
        col_dict = foos->add_column_dictionary(type_Int, "dict");
2✔
672

673
        auto foo = foos->create_object();
2✔
674
        dict = foo.get_dictionary(col_dict);
2✔
675
        tr->commit_and_continue_as_read();
2✔
676
    }
2✔
677
    auto tr2 = db->start_read();
2✔
678
    auto dict2 = tr2->get_table("Foo")->get_object(0).get_dictionary(col_dict);
2✔
679
    auto random_op = [&](Dictionary& dict) {
4,000✔
680
        if (model.rnd() % 3) { // 66%
4,000✔
681
            auto k = model.get_rnd_unused_key();
2,693✔
682
            auto v = model.get_rnd_value();
2,693✔
683
            model.insert(k, v);
2,693✔
684
            dict.insert(k, v);
2,693✔
685
        }
2,693✔
686
        else { // 33%
1,307✔
687
            auto k = model.get_rnd_used_key();
1,307✔
688
            if (!k.empty()) {
1,307✔
689
                auto v = model.get_value_by_key(k);
1,307✔
690
                CHECK(dict.get(k) == v);
1,307✔
691
                dict.erase(k);
1,307✔
692
                model.erase(k);
1,307✔
693
            }
1,307✔
694
        }
1,307✔
695
    };
4,000✔
696
    for (int it = 0; it < 1000; it++) {
2,002✔
697
        tr2->promote_to_write();
2,000✔
698
        {
2,000✔
699
            random_op(dict2);
2,000✔
700
        }
2,000✔
701
        tr2->commit_and_continue_as_read();
2,000✔
702
        tr->promote_to_write();
2,000✔
703
        tr->verify();
2,000✔
704
        {
2,000✔
705
            random_op(dict);
2,000✔
706
        }
2,000✔
707
        tr->commit_and_continue_as_read();
2,000✔
708
        tr->verify();
2,000✔
709
    }
2,000✔
710
}
2✔
711

712
static void do_Dictionary_HashCollisionTransaction(realm::test_util::unit_test::TestContext& test_context,
713
                                                   int64_t nb_entries)
714
{
4✔
715
    SHARED_GROUP_TEST_PATH(path);
4✔
716
    auto hist = make_in_realm_history();
4✔
717
    DBRef db = DB::create(*hist, path);
4✔
718

719
    {
4✔
720
        auto tr = db->start_write();
4✔
721
        auto foos = tr->add_table("Foo");
4✔
722
        auto bars = tr->add_table("Bar");
4✔
723
        ColKey col_dict = foos->add_column_dictionary(*bars, "dict");
4✔
724
        ColKey col_int = bars->add_column(type_Int, "ints");
4✔
725

726
        for (int64_t i = 0; i < nb_entries; i++) {
1,204✔
727
            bars->create_object().set(col_int, i);
1,200✔
728
        }
1,200✔
729

730
        auto foo = foos->create_object();
4✔
731
        auto dict = foo.get_dictionary(col_dict);
4✔
732
        for (int64_t i = 0; i < nb_entries; i++) {
1,204✔
733
            std::string key = "key" + util::to_string(i);
1,200✔
734
            dict.insert(Mixed(key), bars->find_first_int(col_int, i));
1,200✔
735
        }
1,200✔
736
        tr->commit();
4✔
737
    }
4✔
738

739
    {
4✔
740
        auto rt = db->start_read();
4✔
741
        auto foos = rt->get_table("Foo");
4✔
742
        auto bars = rt->get_table("Bar");
4✔
743
        ColKey col_dict = foos->get_column_key("dict");
4✔
744
        ColKey col_int = bars->get_column_key("ints");
4✔
745
        auto dict = foos->begin()->get_dictionary(col_dict);
4✔
746
        for (int64_t i = 0; i < nb_entries; i++) {
1,204✔
747
            std::string key = "key" + util::to_string(i);
1,200✔
748
            auto obj_key = dict[key].get<ObjKey>();
1,200✔
749
            CHECK_EQUAL(bars->get_object(obj_key).get<Int>(col_int), i);
1,200✔
750
        }
1,200✔
751
    }
4✔
752

753
    auto rt = db->start_read();
4✔
754
    for (int64_t i = 0; i < nb_entries; i++) {
1,204✔
755
        rt->promote_to_write();
1,200✔
756

757
        auto foos = rt->get_table("Foo");
1,200✔
758
        auto bars = rt->get_table("Bar");
1,200✔
759
        ColKey col_dict = foos->get_column_key("dict");
1,200✔
760
        ColKey col_int = bars->get_column_key("ints");
1,200✔
761
        auto dict = foos->begin()->get_dictionary(col_dict);
1,200✔
762

763
        std::string key = "key" + util::to_string(i);
1,200✔
764
        dict.erase(key);
1,200✔
765
        CHECK_EQUAL(dict.size(), nb_entries - i - 1);
1,200✔
766
        bars->remove_object(bars->find_first_int(col_int, i));
1,200✔
767

768
        rt->commit_and_continue_as_read();
1,200✔
769

770
        for (int64_t j = i + 1; j < nb_entries; j++) {
260,600✔
771
            std::string key_j = "key" + util::to_string(j);
259,400✔
772
            auto obj_key = dict[key_j].get<ObjKey>();
259,400✔
773
            CHECK_EQUAL(bars->get_object(obj_key).get<Int>("ints"), j);
259,400✔
774
        }
259,400✔
775
    }
1,200✔
776
}
4✔
777

778
NONCONCURRENT_TEST(Dictionary_HashCollisionTransaction)
779
{
2✔
780
    do_Dictionary_HashCollisionTransaction(test_context, 100); // One node cluster
2✔
781
    do_Dictionary_HashCollisionTransaction(test_context, 500); // Three node cluster
2✔
782
}
2✔
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