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

realm / realm-core / github_pull_request_281922

31 Oct 2023 09:13AM UTC coverage: 90.445% (-0.08%) from 90.528%
github_pull_request_281922

Pull #7039

Evergreen

jedelbo
Merge branch 'next-major' into je/global-key
Pull Request #7039: Remove ability to synchronize objects without primary key

95324 of 175822 branches covered (0.0%)

101 of 105 new or added lines in 13 files covered. (96.19%)

238 existing lines in 19 files now uncovered.

232657 of 257235 relevant lines covered (90.45%)

6351359.67 hits per line

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

99.85
/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

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

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

1✔
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

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

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

1✔
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

1✔
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
1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
190
        // Reinsert lady
1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
245
        // Reinsert lady
1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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);
1✔
337
    cmp(dict.get("Good morning"), "sunshine");
2✔
338

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

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

1✔
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

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

1✔
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

1✔
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

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

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

1✔
469
    a.invalidate();
2✔
470

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

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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
518
    // Check that values can be read back
1✔
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

1✔
524
    // And these keys should not exist
1✔
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

1✔
530
    // Check that a query can find matching key and value
1✔
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

1✔
537
    // Check that dict.find works
1✔
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

1✔
544
    // And these keys should not be found
1✔
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

1✔
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

2✔
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

2✔
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

2✔
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

1✔
578
    check_aggregates();
2✔
579

1✔
580
    // Update with new values
1✔
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

1✔
586
    check_aggregates();
2✔
587

1✔
588
    // Check that values was updated properly
1✔
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

1✔
594
    // Now erase one entry at a time and check that the rest of the values are ok
1✔
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

100✔
600
        // Check that remaining entries still can be found
100✔
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,318✔
613
        if (the_map.size() == 0)
1,318✔
UNCOV
614
            return std::string();
×
615
        size_t idx = size_t(rnd()) % the_map.size();
1,318✔
616
        auto it = the_map.begin();
1,318✔
617
        while (idx--) {
218,732✔
618
            it++;
217,414✔
619
        }
217,414✔
620
        return it->first;
1,318✔
621
    }
1,318✔
622
    std::string get_rnd_unused_key()
623
    {
2,682✔
624
        int64_t key_i;
2,682✔
625
        std::string key;
2,682✔
626
        do {
2,682✔
627
            key_i = rnd();
2,682✔
628
            key = std::to_string(key_i);
2,682✔
629
        } while (the_map.find(key) != the_map.end());
2,682✔
630
        return key;
2,682✔
631
    }
2,682✔
632
    Mixed get_rnd_value()
633
    {
2,682✔
634
        int64_t v = rnd();
2,682✔
635
        return Mixed(v);
2,682✔
636
    }
2,682✔
637
    // model access
638
    Mixed get_value_by_key(std::string key)
639
    {
1,318✔
640
        auto it = the_map.find(key);
1,318✔
641
        REALM_ASSERT(it != the_map.end());
1,318✔
642
        return it->second;
1,318✔
643
    }
1,318✔
644
    bool insert(std::string key, Mixed value)
645
    {
2,682✔
646
        the_map[key] = value;
2,682✔
647
        return true;
2,682✔
648
    }
2,682✔
649
    bool erase(std::string key)
650
    {
1,318✔
651
        the_map.erase(key);
1,318✔
652
        return true;
1,318✔
653
    }
1,318✔
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
1✔
670
        auto foos = tr->add_table("Foo");
2✔
671
        col_dict = foos->add_column_dictionary(type_Int, "dict");
2✔
672

1✔
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,682✔
682
            auto v = model.get_rnd_value();
2,682✔
683
            model.insert(k, v);
2,682✔
684
            dict.insert(k, v);
2,682✔
685
        }
2,682✔
686
        else { // 33%
1,318✔
687
            auto k = model.get_rnd_used_key();
1,318✔
688
            if (!k.empty()) {
1,318✔
689
                auto v = model.get_value_by_key(k);
1,318✔
690
                CHECK(dict.get(k) == v);
1,318✔
691
                dict.erase(k);
1,318✔
692
                model.erase(k);
1,318✔
693
            }
1,318✔
694
        }
1,318✔
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

2✔
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

2✔
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

2✔
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

2✔
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

2✔
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

600✔
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

600✔
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

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

600✔
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