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

realm / realm-core / github_pull_request_301264

30 Jul 2024 07:11PM UTC coverage: 91.111% (+0.009%) from 91.102%
github_pull_request_301264

Pull #7936

Evergreen

web-flow
Add support for multi-process subscription state change notifications (#7862)

As with the other multi-process notifications, the core idea here is to
eliminate the in-memory state and produce notifications based entirely on the
current state of the Realm file.

SubscriptionStore::update_state() has been replaced with separate functions for
the specific legal state transitions, which also take a write transaction as a
parameter. These functions are called by PendingBootstrapStore inside the same
write transaction as the bootstrap updates which changed the subscription
state. This is both a minor performance optimization (due to fewer writes) and
eliminates a brief window between the two writes where the Realm file was in an
inconsistent state.

There's a minor functional change here: previously old subscription sets were
superseded when the new one reached the Completed state, and now they are
superseded on AwaitingMark. This aligns it with when the new subscription set
becomes the one which is returned by get_active().
Pull Request #7936: Fix connection callback crashes when reloading with React Native

102800 of 181570 branches covered (56.62%)

216840 of 237996 relevant lines covered (91.11%)

5918493.47 hits per line

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

99.83
/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
        // make sure that inserting the same link again does nothing
243
        CHECK_NOT(dict.insert("Pet", lady).second);
2✔
244
        CHECK_EQUAL(lady.get_backlink_count(), 1);
2✔
245
        lady.remove();
2✔
246
        cmp(dict["Pet"], Mixed());
2✔
247

248
        // Reinsert lady
249
        lady = dogs->create_object_with_primary_key("lady");
2✔
250
        dict.insert("Pet", lady);
2✔
251
        dict.insert("Pet", lady);
2✔
252
        CHECK_EQUAL(lady.get_backlink_count(), 1);
2✔
253
        lady.invalidate(); // Make lady a tombstone :-(
2✔
254
        cmp(dict["Pet"], Mixed());
2✔
255
        lady = dogs->create_object_with_primary_key("lady");
2✔
256
        cmp(dict["Pet"], Mixed(lady.get_link()));
2✔
257
        CHECK_EQUAL(lady.get_backlink_count(), 1);
2✔
258

259
        auto invalid_link = pluto.get_link();
2✔
260
        pluto.remove();
2✔
261
        CHECK_THROW(dict.insert("Pet", invalid_link), InvalidArgument);
2✔
262

263
        dict.insert("Pet", Mixed(ObjLink(dogs->get_key(), dogs->get_objkey_from_primary_key("pongo"))));
2✔
264
        cmp(dict["Pet"], Mixed());
2✔
265
        Obj pongo = dogs->create_object_with_primary_key("pongo");
2✔
266
        CHECK_EQUAL(pongo.get_backlink_count(), 1);
2✔
267
        cmp(dict["Pet"], Mixed(pongo.get_link()));
2✔
268
    }
2✔
269
}
2✔
270

271
TEST(Dictionary_Clear)
272
{
2✔
273
    Group g;
2✔
274
    auto dogs = g.add_table_with_primary_key("dog", type_String, "name");
2✔
275
    auto persons = g.add_table_with_primary_key("person", type_String, "name");
2✔
276
    auto col_dict_mixed = persons->add_column_dictionary(type_Mixed, "any");
2✔
277
    auto col_dict_implicit = persons->add_column_dictionary(*dogs, "implicit");
2✔
278

279
    Obj adam = persons->create_object_with_primary_key("adam");
2✔
280
    Obj pluto = dogs->create_object_with_primary_key("pluto");
2✔
281
    Obj lady = dogs->create_object_with_primary_key("lady");
2✔
282

283
    adam.get_dictionary(col_dict_mixed).insert("Dog1", pluto);
2✔
284
    adam.get_dictionary(col_dict_implicit).insert("DOg2", lady.get_key());
2✔
285

286
    CHECK_EQUAL(lady.get_backlink_count(), 1);
2✔
287
    CHECK_EQUAL(pluto.get_backlink_count(), 1);
2✔
288
    persons->clear();
2✔
289
    CHECK_EQUAL(lady.get_backlink_count(), 0);
2✔
290
    CHECK_EQUAL(pluto.get_backlink_count(), 0);
2✔
291
    g.verify();
2✔
292
}
2✔
293

294
TEST(Dictionary_Transaction)
295
{
2✔
296
    SHARED_GROUP_TEST_PATH(path);
2✔
297
    auto hist = make_in_realm_history();
2✔
298
    DBRef db = DB::create(*hist, path);
2✔
299
    ObjKey k0, k2;
2✔
300
    ColKey col_dict;
2✔
301
    auto cmp = [this](Mixed x, Mixed y) {
6✔
302
        CHECK_EQUAL(x, y);
6✔
303
    };
6✔
304

305
    auto rt = db->start_read();
2✔
306
    {
2✔
307
        WriteTransaction wt(db);
2✔
308
        auto foo = wt.add_table("foo");
2✔
309
        col_dict = foo->add_column_dictionary(type_Mixed, "dictionaries");
2✔
310

311
        Obj obj0 = foo->create_object();
2✔
312
        Obj obj1 = foo->create_object();
2✔
313
        Obj obj2 = foo->create_object();
2✔
314
        k0 = obj0.get_key();
2✔
315
        k2 = obj2.get_key();
2✔
316
        Dictionary dict = obj0.get_dictionary(col_dict);
2✔
317
        dict.insert("Hello", 9);
2✔
318
        dict.insert("Goodbye", "cruel world");
2✔
319

320
        dict = obj1.get_dictionary(col_dict);
2✔
321
        dict.insert("Link", obj2.get_link());
2✔
322
        wt.commit();
2✔
323
    }
2✔
324
    rt->advance_read();
2✔
325
    rt->verify();
2✔
326
    ConstTableRef table = rt->get_table("foo");
2✔
327
    Dictionary dict;
2✔
328
    dict = table->get_object(k0).get_dictionary(col_dict);
2✔
329
    cmp(dict.get("Hello"), 9);
2✔
330
    cmp(dict.get("Goodbye"), "cruel world");
2✔
331

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

345
    {
2✔
346
        auto wt = db->start_write();
2✔
347
        auto foo = wt->get_table("foo");
2✔
348
        Dictionary d = foo->get_object(k0).get_dictionary(col_dict);
2✔
349
        d.clear();
2✔
350

351
        wt->commit_and_continue_as_read();
2✔
352
        wt->promote_to_write();
2✔
353
        wt->verify();
2✔
354
    }
2✔
355
}
2✔
356

357
TEST(Dictionary_Aggregate)
358
{
2✔
359
    SHARED_GROUP_TEST_PATH(path);
2✔
360
    auto hist = make_in_realm_history();
2✔
361
    DBRef db = DB::create(*hist, path);
2✔
362
    auto tr = db->start_write();
2✔
363
    auto foo = tr->add_table("foo");
2✔
364
    auto col_dict = foo->add_column_dictionary(type_Int, "dictionaries");
2✔
365

366
    Obj obj1 = foo->create_object();
2✔
367
    Obj obj2 = foo->create_object();
2✔
368
    Dictionary dict = obj1.get_dictionary(col_dict);
2✔
369
    std::vector<int64_t> random_idx(100);
2✔
370
    std::iota(random_idx.begin(), random_idx.end(), 0);
2✔
371
    std::shuffle(random_idx.begin(), random_idx.end(), std::mt19937(unit_test_random_seed));
2✔
372

373
    for (int i = 0; i < 100; i++) {
202✔
374
        dict.insert(util::to_string(i), random_idx[i]);
200✔
375
    }
200✔
376

377
    std::vector<size_t> indices;
2✔
378
    dict.sort(indices, true);
2✔
379
    int64_t last = -1;
2✔
380
    for (size_t ndc : indices) {
200✔
381
        int64_t val = dict.get_any(ndc).get_int();
200✔
382
        CHECK_GREATER(val, last);
200✔
383
        last = val;
200✔
384
    }
200✔
385
    tr->commit_and_continue_as_read();
2✔
386

387
    size_t ndx;
2✔
388
    auto max = dict.max(&ndx);
2✔
389
    CHECK(max);
2✔
390
    CHECK_EQUAL(max->get_int(), 99);
2✔
391

392
    auto min = dict.min(&ndx);
2✔
393
    CHECK(min);
2✔
394
    CHECK_EQUAL(min->get_int(), 0);
2✔
395

396
    size_t cnt;
2✔
397
    auto sum = dict.sum(&cnt);
2✔
398
    CHECK(sum);
2✔
399
    CHECK_EQUAL(cnt, 100);
2✔
400
    CHECK_EQUAL(sum->get_int(), 50 * 99);
2✔
401

402
    auto avg = dict.avg(&cnt);
2✔
403
    CHECK(avg);
2✔
404
    CHECK_EQUAL(cnt, 100);
2✔
405
    CHECK_EQUAL(avg->get_double(), double(50 * 99) / 100);
2✔
406

407
    dict = obj2.get_dictionary(col_dict);
2✔
408
    max = dict.max(&ndx);
2✔
409
    CHECK(max);
2✔
410
    CHECK(max->is_null());
2✔
411
    CHECK_EQUAL(ndx, realm::npos);
2✔
412
}
2✔
413

414
NONCONCURRENT_TEST(Dictionary_Performance)
415
{
2✔
416
    size_t nb_reps = 1000;
2✔
417

418
    Group g;
2✔
419
    auto foo = g.add_table("foo");
2✔
420
    auto col_dict = foo->add_column_dictionary(type_Int, "dictionaries", false);
2✔
421

422
    Obj obj1 = foo->create_object();
2✔
423
    Dictionary dict = obj1.get_dictionary(col_dict);
2✔
424
    std::vector<int64_t> random_idx(nb_reps);
2✔
425
    std::vector<std::string> random_str(nb_reps);
2✔
426
    std::iota(random_idx.begin(), random_idx.end(), 0);
2✔
427
    std::shuffle(random_idx.begin(), random_idx.end(), std::mt19937(unit_test_random_seed));
2✔
428

429
    std::transform(random_idx.begin(), random_idx.end(), random_str.begin(), [](int64_t i) {
2,000✔
430
        return std::string("s") + util::to_string(i);
2,000✔
431
    });
2,000✔
432

433
    auto t1 = steady_clock::now();
2✔
434

435
    int64_t i = 0;
2✔
436
    for (auto& str : random_str) {
2,000✔
437
        dict.insert(str, i++);
2,000✔
438
    }
2,000✔
439

440
    auto t2 = steady_clock::now();
2✔
441

442
    CALLGRIND_START_INSTRUMENTATION;
2✔
443
    i = 0;
2✔
444
    for (auto& str : random_str) {
2,000✔
445
        CHECK_EQUAL(dict.get(str), Mixed(i++));
2,000✔
446
    }
2,000✔
447
    CALLGRIND_STOP_INSTRUMENTATION;
2✔
448

449
    auto t3 = steady_clock::now();
2✔
450

451
    std::cout << nb_reps << " values in dictionary" << std::endl;
2✔
452
    std::cout << "    insertion: " << duration_cast<nanoseconds>(t2 - t1).count() / nb_reps << " ns/val" << std::endl;
2✔
453
    std::cout << "    lookup: " << duration_cast<nanoseconds>(t3 - t2).count() / nb_reps << " ns/val" << std::endl;
2✔
454
}
2✔
455

456
TEST(Dictionary_Tombstones)
457
{
2✔
458
    Group g;
2✔
459
    auto foos = g.add_table_with_primary_key("class_Foo", type_Int, "id");
2✔
460
    auto bars = g.add_table_with_primary_key("class_Bar", type_String, "id");
2✔
461
    auto col_int = bars->add_column(type_Int, "value");
2✔
462
    ColKey col_dict = foos->add_column_dictionary(*bars, "dict");
2✔
463

464
    auto foo = foos->create_object_with_primary_key(123);
2✔
465
    auto a = bars->create_object_with_primary_key("a").set(col_int, 1);
2✔
466
    auto b = bars->create_object_with_primary_key("b").set(col_int, 2);
2✔
467

468
    auto dict = foo.get_dictionary(col_dict);
2✔
469
    dict.insert("a", a);
2✔
470
    dict.insert("b", b);
2✔
471

472
    auto q = bars->where(dict).equal(col_int, 1);
2✔
473
    CHECK_EQUAL(q.count(), 1);
2✔
474

475
    a.invalidate();
2✔
476

477
    CHECK_EQUAL(dict.size(), 2);
2✔
478
    CHECK((*dict.find("a")).second.is_null());
2✔
479

480
    CHECK(dict.find("b") != dict.end());
2✔
481

482
    CHECK_EQUAL(q.count(), 0);
2✔
483
}
2✔
484

485
TEST(Dictionary_UseAfterFree)
486
{
2✔
487
    Group g;
2✔
488
    auto foos = g.add_table("Foo");
2✔
489
    ColKey col_dict = foos->add_column_dictionary(type_String, "dict");
2✔
490

491
    auto foo = foos->create_object();
2✔
492
    auto dict = foo.get_dictionary(col_dict);
2✔
493
    dict.insert("a", "monkey");
2✔
494
    dict.insert("b", "lion");
2✔
495
    dict.insert("c", "à");
2✔
496

497
    Query q;
2✔
498
    {
2✔
499
        auto str = std::make_unique<std::string>("à");
2✔
500
        auto col = foos->column<Dictionary>(col_dict);
2✔
501
        q = col.equal(StringData(*str), true); // A copy of the string must be taken here
2✔
502
    }
2✔
503
    CHECK_EQUAL(q.count(), 1);
2✔
504
}
2✔
505

506
NONCONCURRENT_TEST(Dictionary_HashCollision)
507
{
2✔
508
    constexpr int64_t nb_entries = 100;
2✔
509
    Group g;
2✔
510
    auto foos = g.add_table("Foo");
2✔
511
    ColKey col_dict = foos->add_column_dictionary(type_Int, "dict");
2✔
512

513
    auto foo = foos->create_object();
2✔
514
    auto dict = foo.get_dictionary(col_dict);
2✔
515
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
516
        std::string key = "key" + util::to_string(i);
200✔
517
        dict.insert(Mixed(key), i);
200✔
518
        dict.erase(key);
200✔
519
        dict.insert(Mixed(key), i);
200✔
520
    }
200✔
521

522
    // g.to_json(std::cout);
523

524
    // Check that values can be read back
525
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
526
        std::string key = "key" + util::to_string(i);
200✔
527
        CHECK_EQUAL(dict[key].get_int(), i);
200✔
528
    }
200✔
529

530
    // And these keys should not exist
531
    for (int64_t i = nb_entries; i < nb_entries + 20; i++) {
42✔
532
        std::string key = "key" + util::to_string(i);
40✔
533
        CHECK_NOT(dict.contains(key));
40✔
534
    }
40✔
535

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

543
    // Check that dict.find works
544
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
545
        std::string key = "key" + util::to_string(i);
200✔
546
        auto it = dict.find(key);
200✔
547
        CHECK_EQUAL((*it).second.get_int(), i);
200✔
548
    }
200✔
549

550
    // And these keys should not be found
551
    for (int64_t i = nb_entries; i < nb_entries + 20; i++) {
42✔
552
        std::string key = "key" + util::to_string(i);
40✔
553
        CHECK(dict.find(key) == dict.end());
40✔
554
    }
40✔
555

556
    auto check_aggregates = [&]() {
4✔
557
        int64_t expected_sum = nb_entries * (nb_entries - 1) / 2;
4✔
558
        size_t count = 0;
4✔
559
        util::Optional<Mixed> actual_sum = dict.sum(&count);
4✔
560
        CHECK(actual_sum);
4✔
561
        CHECK(actual_sum && *actual_sum == Mixed{expected_sum});
4✔
562
        CHECK_EQUAL(count, nb_entries);
4✔
563
        Query q = (foos->column<Dictionary>(col_dict).sum() == Mixed(expected_sum));
4✔
564
        CHECK_EQUAL(q.count(), 1);
4✔
565

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

571
        util::Optional<Mixed> actual_max = dict.max();
4✔
572
        CHECK(actual_max && *actual_max == Mixed{nb_entries - 1});
4✔
573
        q = (foos->column<Dictionary>(col_dict).max() == Mixed(nb_entries - 1));
4✔
574
        CHECK_EQUAL(q.count(), 1);
4✔
575

576
        util::Optional<Mixed> actual_avg = dict.avg(&count);
4✔
577
        Mixed expected_avg{Decimal128(expected_sum) / nb_entries};
4✔
578
        CHECK_EQUAL(count, nb_entries);
4✔
579
        CHECK(actual_avg && *actual_avg == expected_avg);
4✔
580
        q = (foos->column<Dictionary>(col_dict).average() == expected_avg);
4✔
581
        CHECK_EQUAL(q.count(), 1);
4✔
582
    };
4✔
583

584
    check_aggregates();
2✔
585

586
    // Update with new values
587
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
588
        std::string key = "key" + util::to_string(i);
200✔
589
        dict.insert(Mixed(key), nb_entries - i - 1);
200✔
590
    }
200✔
591

592
    check_aggregates();
2✔
593

594
    // Check that values was updated properly
595
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
596
        std::string key = "key" + util::to_string(i);
200✔
597
        CHECK_EQUAL(dict[key].get_int(), nb_entries - i - 1);
200✔
598
    }
200✔
599

600
    // Now erase one entry at a time and check that the rest of the values are ok
601
    for (int64_t i = 0; i < nb_entries; i++) {
202✔
602
        std::string key = "key" + util::to_string(i);
200✔
603
        dict.erase(key);
200✔
604
        CHECK_EQUAL(dict.size(), nb_entries - i - 1);
200✔
605

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

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

661
    std::map<std::string, Mixed> the_map;
662
    util::UniqueFunction<int64_t(void)> rnd = std::mt19937(unit_test_random_seed);
663
};
664

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

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

718
static void do_Dictionary_HashCollisionTransaction(realm::test_util::unit_test::TestContext& test_context,
719
                                                   int64_t nb_entries)
720
{
4✔
721
    SHARED_GROUP_TEST_PATH(path);
4✔
722
    auto hist = make_in_realm_history();
4✔
723
    DBRef db = DB::create(*hist, path);
4✔
724

725
    {
4✔
726
        auto tr = db->start_write();
4✔
727
        auto foos = tr->add_table("Foo");
4✔
728
        auto bars = tr->add_table("Bar");
4✔
729
        ColKey col_dict = foos->add_column_dictionary(*bars, "dict");
4✔
730
        ColKey col_int = bars->add_column(type_Int, "ints");
4✔
731

732
        for (int64_t i = 0; i < nb_entries; i++) {
1,204✔
733
            bars->create_object().set(col_int, i);
1,200✔
734
        }
1,200✔
735

736
        auto foo = foos->create_object();
4✔
737
        auto dict = foo.get_dictionary(col_dict);
4✔
738
        for (int64_t i = 0; i < nb_entries; i++) {
1,204✔
739
            std::string key = "key" + util::to_string(i);
1,200✔
740
            dict.insert(Mixed(key), bars->find_first_int(col_int, i));
1,200✔
741
        }
1,200✔
742
        tr->commit();
4✔
743
    }
4✔
744

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

759
    auto rt = db->start_read();
4✔
760
    for (int64_t i = 0; i < nb_entries; i++) {
1,204✔
761
        rt->promote_to_write();
1,200✔
762

763
        auto foos = rt->get_table("Foo");
1,200✔
764
        auto bars = rt->get_table("Bar");
1,200✔
765
        ColKey col_dict = foos->get_column_key("dict");
1,200✔
766
        ColKey col_int = bars->get_column_key("ints");
1,200✔
767
        auto dict = foos->begin()->get_dictionary(col_dict);
1,200✔
768

769
        std::string key = "key" + util::to_string(i);
1,200✔
770
        dict.erase(key);
1,200✔
771
        CHECK_EQUAL(dict.size(), nb_entries - i - 1);
1,200✔
772
        bars->remove_object(bars->find_first_int(col_int, i));
1,200✔
773

774
        rt->commit_and_continue_as_read();
1,200✔
775

776
        for (int64_t j = i + 1; j < nb_entries; j++) {
260,600✔
777
            std::string key_j = "key" + util::to_string(j);
259,400✔
778
            auto obj_key = dict[key_j].get<ObjKey>();
259,400✔
779
            CHECK_EQUAL(bars->get_object(obj_key).get<Int>("ints"), j);
259,400✔
780
        }
259,400✔
781
    }
1,200✔
782
}
4✔
783

784
NONCONCURRENT_TEST(Dictionary_HashCollisionTransaction)
785
{
2✔
786
    do_Dictionary_HashCollisionTransaction(test_context, 100); // One node cluster
2✔
787
    do_Dictionary_HashCollisionTransaction(test_context, 500); // Three node cluster
2✔
788
}
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