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

realm / realm-core / thomas.goyne_232

13 Mar 2024 01:00AM UTC coverage: 91.787% (+0.9%) from 90.924%
thomas.goyne_232

Pull #7402

Evergreen

tgoyne
Add more UpdateIfNeeded tests
Pull Request #7402: Make Obj trivial and add a separate ObjCollectionParent type

94460 of 174600 branches covered (54.1%)

496 of 559 new or added lines in 21 files covered. (88.73%)

848 existing lines in 34 files now uncovered.

242761 of 264484 relevant lines covered (91.79%)

6342666.36 hits per line

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

98.77
/test/test_list.cpp
1
/*************************************************************************
2
 *
3
 * Copyright 2023 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 <algorithm>
22
#include <cmath>
23
#include <limits>
24
#include <string>
25
#include <fstream>
26
#include <ostream>
27
#include <set>
28
#include <chrono>
29

30
using namespace std::chrono;
31

32
#include <realm.hpp>
33
#include <external/json/json.hpp>
34
#include "test.hpp"
35
#include "test_types_helper.hpp"
36

37
// #include <valgrind/callgrind.h>
38
// #define PERFORMACE_TESTING
39

40
using namespace realm;
41
using namespace realm::util;
42
using namespace realm::test_util;
43
using unit_test::TestContext;
44

45
TEST(List_basic)
46
{
2✔
47
    Table table;
2✔
48
    auto list_col = table.add_column_list(type_Int, "int_list");
2✔
49
    int sum = 0;
2✔
50

1✔
51
    {
2✔
52
        Obj obj = table.create_object(ObjKey(5));
2✔
53
        CHECK_NOT(obj.is_null(list_col));
2✔
54
        auto list = obj.get_list<int64_t>(list_col);
2✔
55
        CHECK_NOT(obj.is_null(list_col));
2✔
56
        CHECK(list.is_empty());
2✔
57

1✔
58
        size_t return_cnt = 0, return_ndx = 0;
2✔
59
        list.sum(&return_cnt);
2✔
60
        CHECK_EQUAL(return_cnt, 0);
2✔
61
        list.max(&return_ndx);
2✔
62
        CHECK_EQUAL(return_ndx, not_found);
2✔
63
        return_ndx = 0;
2✔
64
        list.min(&return_ndx);
2✔
65
        CHECK_EQUAL(return_ndx, not_found);
2✔
66
        list.avg(&return_cnt);
2✔
67
        CHECK_EQUAL(return_cnt, 0);
2✔
68

1✔
69
        for (int i = 0; i < 100; i++) {
202✔
70
            list.add(i + 1000);
200✔
71
            sum += (i + 1000);
200✔
72
        }
200✔
73
    }
2✔
74
    {
2✔
75
        Obj obj = table.get_object(ObjKey(5));
2✔
76
        auto list1 = obj.get_list<int64_t>(list_col);
2✔
77
        CHECK_EQUAL(list1.size(), 100);
2✔
78
        CHECK_EQUAL(list1.get(0), 1000);
2✔
79
        CHECK_EQUAL(list1.get(99), 1099);
2✔
80
        auto list_base = obj.get_listbase_ptr(list_col);
2✔
81
        CHECK_EQUAL(list_base->size(), 100);
2✔
82
        CHECK(dynamic_cast<Lst<Int>*>(list_base.get()));
2✔
83

1✔
84
        CHECK_EQUAL(list1.sum(), sum);
2✔
85
        CHECK_EQUAL(list1.max(), 1099);
2✔
86
        CHECK_EQUAL(list1.min(), 1000);
2✔
87
        CHECK_EQUAL(list1.avg(), double(sum) / 100);
2✔
88

1✔
89
        auto list2 = obj.get_list<int64_t>(list_col);
2✔
90
        list2.set(50, 747);
2✔
91
        CHECK_EQUAL(list1.get(50), 747);
2✔
92
        list1.resize(101);
2✔
93
        CHECK_EQUAL(list1.get(100), 0);
2✔
94
        list1.resize(50);
2✔
95
        CHECK_EQUAL(list1.size(), 50);
2✔
96
    }
2✔
97
    {
2✔
98
        Obj obj = table.create_object(ObjKey(7));
2✔
99
        auto list = obj.get_list<int64_t>(list_col);
2✔
100
        list.resize(10);
2✔
101
        CHECK_EQUAL(list.size(), 10);
2✔
102
        for (int i = 0; i < 10; i++) {
22✔
103
            CHECK_EQUAL(list.get(i), 0);
20✔
104
        }
20✔
105
    }
2✔
106
    table.remove_object(ObjKey(5));
2✔
107
}
2✔
108

109
TEST(List_SimpleTypes)
110
{
2✔
111
    Group g;
2✔
112
    std::vector<CollectionBase*> lists;
2✔
113
    TableRef t = g.add_table("table");
2✔
114
    ColKey int_col = t->add_column_list(type_Int, "integers");
2✔
115
    ColKey bool_col = t->add_column_list(type_Bool, "booleans");
2✔
116
    ColKey string_col = t->add_column_list(type_String, "strings");
2✔
117
    ColKey double_col = t->add_column_list(type_Double, "doubles");
2✔
118
    ColKey timestamp_col = t->add_column_list(type_Timestamp, "timestamps");
2✔
119
    Obj obj = t->create_object(ObjKey(7));
2✔
120

1✔
121
    std::vector<int64_t> integer_vector = {1, 2, 3, 4};
2✔
122
    obj.set_list_values(int_col, integer_vector);
2✔
123

1✔
124
    std::vector<bool> bool_vector = {false, false, true, false, true};
2✔
125
    obj.set_list_values(bool_col, bool_vector);
2✔
126

1✔
127
    std::vector<StringData> string_vector = {"monday", "tuesday", "thursday", "friday", "saturday", "sunday"};
2✔
128
    obj.set_list_values(string_col, string_vector);
2✔
129

1✔
130
    std::vector<double> double_vector = {898742.09382, 3.14159265358979, 2.71828182845904};
2✔
131
    obj.set_list_values(double_col, double_vector);
2✔
132

1✔
133
    time_t seconds_since_epoc = time(nullptr);
2✔
134
    std::vector<Timestamp> timestamp_vector = {Timestamp(seconds_since_epoc, 0),
2✔
135
                                               Timestamp(seconds_since_epoc + 60, 0)};
2✔
136
    obj.set_list_values(timestamp_col, timestamp_vector);
2✔
137

1✔
138
    auto int_list = obj.get_list<int64_t>(int_col);
2✔
139
    lists.push_back(&int_list);
2✔
140
    std::vector<int64_t> vec(int_list.size());
2✔
141
    CHECK_EQUAL(integer_vector.size(), int_list.size());
2✔
142
    // {1, 2, 3, 4}
1✔
143
    auto it = int_list.begin();
2✔
144
    CHECK_EQUAL(*it, 1);
2✔
145
    std::copy(int_list.begin(), int_list.end(), vec.begin());
2✔
146
    unsigned j = 0;
2✔
147
    for (auto i : int_list) {
8✔
148
        CHECK_EQUAL(vec[j], i);
8✔
149
        CHECK_EQUAL(integer_vector[j++], i);
8✔
150
    }
8✔
151
    auto f = std::find(int_list.begin(), int_list.end(), 3);
2✔
152
    CHECK_EQUAL(3, *f++);
2✔
153
    CHECK_EQUAL(4, *f);
2✔
154

1✔
155
    for (unsigned i = 0; i < int_list.size(); i++) {
10✔
156
        CHECK_EQUAL(integer_vector[i], int_list[i]);
8✔
157
    }
8✔
158

1✔
159
    CHECK_EQUAL(3, int_list.remove(2));
2✔
160
    // {1, 2, 4}
1✔
161
    CHECK_EQUAL(integer_vector.size() - 1, int_list.size());
2✔
162
    CHECK_EQUAL(4, int_list[2]);
2✔
163
    int_list.resize(6);
2✔
164
    // {1, 2, 4, 0, 0, 0}
1✔
165
    CHECK_EQUAL(int_list[5], 0);
2✔
166
    int_list.swap(0, 1);
2✔
167
    // {2, 1, 4, 0, 0, 0}
1✔
168
    CHECK_EQUAL(2, int_list[0]);
2✔
169
    CHECK_EQUAL(1, int_list[1]);
2✔
170
    int_list.move(1, 4);
2✔
171
    // {2, 4, 0, 0, 1, 0}
1✔
172
    CHECK_EQUAL(4, int_list[1]);
2✔
173
    CHECK_EQUAL(1, int_list[4]);
2✔
174
    int_list.remove(1, 3);
2✔
175
    // {2, 0, 1, 0}
1✔
176
    CHECK_EQUAL(1, int_list[2]);
2✔
177
    int_list.resize(2);
2✔
178
    // {2, 0}
1✔
179
    CHECK_EQUAL(2, int_list.size());
2✔
180
    CHECK_EQUAL(2, int_list[0]);
2✔
181
    CHECK_EQUAL(0, int_list[1]);
2✔
182
    CHECK_EQUAL(lists[0]->size(), 2);
2✔
183
    CHECK_EQUAL(lists[0]->get_col_key(), int_col);
2✔
184

1✔
185
    int_list.clear();
2✔
186
    auto int_list2 = obj.get_list<int64_t>(int_col);
2✔
187
    CHECK_EQUAL(0, int_list2.size());
2✔
188

1✔
189
    CHECK_THROW_ANY(obj.get_list<util::Optional<int64_t>>(int_col));
2✔
190

1✔
191
    auto bool_list = obj.get_list<bool>(bool_col);
2✔
192
    lists.push_back(&bool_list);
2✔
193
    CHECK_EQUAL(bool_vector.size(), bool_list.size());
2✔
194
    for (unsigned i = 0; i < bool_list.size(); i++) {
12✔
195
        CHECK_EQUAL(bool_vector[i], bool_list[i]);
10✔
196
    }
10✔
197

1✔
198
    auto bool_list_nullable = obj.get_list<util::Optional<bool>>(bool_col);
2✔
199
    CHECK_THROW_ANY(bool_list_nullable.set(0, util::none));
2✔
200

1✔
201
    auto string_list = obj.get_list<StringData>(string_col);
2✔
202
    auto str_min = string_list.min();
2✔
203
    CHECK(!str_min);
2✔
204
    CHECK_EQUAL(string_list.begin()->size(), string_vector.begin()->size());
2✔
205
    CHECK_EQUAL(string_vector.size(), string_list.size());
2✔
206
    for (unsigned i = 0; i < string_list.size(); i++) {
14✔
207
        CHECK_EQUAL(string_vector[i], string_list[i]);
12✔
208
    }
12✔
209

1✔
210
    string_list.insert(2, "Wednesday");
2✔
211
    CHECK_EQUAL(string_vector.size() + 1, string_list.size());
2✔
212
    CHECK_EQUAL(StringData("Wednesday"), string_list.get(2));
2✔
213
    CHECK_THROW_ANY(string_list.set(2, StringData{}));
2✔
214
    CHECK_THROW_ANY(string_list.add(StringData{}));
2✔
215
    CHECK_THROW_ANY(string_list.insert(2, StringData{}));
2✔
216

1✔
217
    auto double_list = obj.get_list<double>(double_col);
2✔
218
    CHECK_EQUAL(double_vector.size(), double_list.size());
2✔
219
    for (unsigned i = 0; i < double_list.size(); i++) {
8✔
220
        CHECK_EQUAL(double_vector[i], double_list.get(i));
6✔
221
    }
6✔
222

1✔
223
    auto timestamp_list = obj.get_list<Timestamp>(timestamp_col);
2✔
224
    CHECK_EQUAL(timestamp_vector.size(), timestamp_list.size());
2✔
225
    for (unsigned i = 0; i < timestamp_list.size(); i++) {
6✔
226
        CHECK_EQUAL(timestamp_vector[i], timestamp_list.get(i));
4✔
227
    }
4✔
228
    size_t return_ndx = 7;
2✔
229
    timestamp_list.min(&return_ndx);
2✔
230
    CHECK_EQUAL(return_ndx, 0);
2✔
231
    timestamp_list.max(&return_ndx);
2✔
232
    CHECK_EQUAL(return_ndx, 1);
2✔
233

1✔
234
    auto timestamp_list2 = timestamp_list.clone();
2✔
235
    CHECK_EQUAL(timestamp_list2->size(), timestamp_list.size());
2✔
236

1✔
237
    t->remove_object(ObjKey(7));
2✔
238
    auto timestamp_list3 = timestamp_list.clone();
2✔
239
    CHECK_NOT(timestamp_list.is_attached());
2✔
240
    CHECK_EQUAL(timestamp_list3->size(), 0);
2✔
241
}
2✔
242

243
template <typename T>
244
struct NullableTypeConverter {
245
    using NullableType = util::Optional<T>;
246
    static bool is_null(NullableType t)
247
    {
72✔
248
        return !bool(t);
72✔
249
    }
72✔
250
};
251

252
template <>
253
struct NullableTypeConverter<Decimal128> {
254
    using NullableType = Decimal128;
255
    static bool is_null(Decimal128 val)
256
    {
24✔
257
        return val.is_null();
24✔
258
    }
24✔
259
};
260

261
TEST_TYPES(List_nullable, int64_t, float, double, Decimal128)
262
{
8✔
263
    Table table;
8✔
264
    auto list_col = table.add_column_list(ColumnTypeTraits<TEST_TYPE>::id, "int_list", true);
8✔
265
    ColumnSumType<TEST_TYPE> sum = TEST_TYPE(0);
8✔
266

4✔
267
    {
8✔
268
        Obj obj = table.create_object(ObjKey(5));
8✔
269
        CHECK_NOT(obj.is_null(list_col));
8✔
270
        auto list = obj.get_list<typename NullableTypeConverter<TEST_TYPE>::NullableType>(list_col);
8✔
271
        CHECK_NOT(obj.is_null(list_col));
8✔
272
        CHECK(list.is_empty());
8✔
273
        for (int i = 0; i < 100; i++) {
808✔
274
            TEST_TYPE val = TEST_TYPE(i + 1000);
800✔
275
            list.add(val);
800✔
276
            sum += (val);
800✔
277
        }
800✔
278
    }
8✔
279
    {
8✔
280
        Obj obj = table.get_object(ObjKey(5));
8✔
281
        auto list1 = obj.get_list<typename NullableTypeConverter<TEST_TYPE>::NullableType>(list_col);
8✔
282
        CHECK_EQUAL(list1.size(), 100);
8✔
283
        CHECK_EQUAL(list1.get(0), TEST_TYPE(1000));
8✔
284
        CHECK_EQUAL(list1.get(99), TEST_TYPE(1099));
8✔
285
        CHECK_NOT(list1.is_null(0));
8✔
286
        auto list_base = obj.get_listbase_ptr(list_col);
8✔
287
        CHECK_EQUAL(list_base->size(), 100);
8✔
288
        CHECK_NOT(list_base->is_null(0));
8✔
289
        CHECK(dynamic_cast<Lst<typename NullableTypeConverter<TEST_TYPE>::NullableType>*>(list_base.get()));
8✔
290

4✔
291
        CHECK_EQUAL(list1.sum(), sum);
8✔
292
        CHECK_EQUAL(list1.max(), TEST_TYPE(1099));
8✔
293
        CHECK_EQUAL(list1.min(), TEST_TYPE(1000));
8✔
294
        CHECK_EQUAL(list1.avg(), typename ColumnTypeTraits<TEST_TYPE>::average_type(sum) / 100);
8✔
295

4✔
296
        auto list2 = obj.get_list<typename NullableTypeConverter<TEST_TYPE>::NullableType>(list_col);
8✔
297
        list2.set(50, TEST_TYPE(747));
8✔
298
        CHECK_EQUAL(list1.get(50), TEST_TYPE(747));
8✔
299
        list1.set_null(50);
8✔
300
        CHECK(NullableTypeConverter<TEST_TYPE>::is_null(list1.get(50)));
8✔
301
        list1.resize(101);
8✔
302
        CHECK(NullableTypeConverter<TEST_TYPE>::is_null(list1.get(100)));
8✔
303
    }
8✔
304
    {
8✔
305
        Obj obj = table.create_object(ObjKey(7));
8✔
306
        auto list = obj.get_list<typename NullableTypeConverter<TEST_TYPE>::NullableType>(list_col);
8✔
307
        list.resize(10);
8✔
308
        CHECK_EQUAL(list.size(), 10);
8✔
309
        for (int i = 0; i < 10; i++) {
88✔
310
            CHECK(NullableTypeConverter<TEST_TYPE>::is_null(list.get(i)));
80✔
311
        }
80✔
312
    }
8✔
313
    table.remove_object(ObjKey(5));
8✔
314
}
8✔
315

316

317
TEST_TYPES(List_Ops, Prop<Int>, Prop<Float>, Prop<Double>, Prop<Decimal>, Prop<ObjectId>, Prop<UUID>, Prop<Timestamp>,
318
           Prop<String>, Prop<Binary>, Prop<Bool>, Nullable<Int>, Nullable<Float>, Nullable<Double>,
319
           Nullable<Decimal>, Nullable<ObjectId>, Nullable<UUID>, Nullable<Timestamp>, Nullable<String>,
320
           Nullable<Binary>, Nullable<Bool>)
321
{
40✔
322
    using underlying_type = typename TEST_TYPE::underlying_type;
40✔
323
    using type = typename TEST_TYPE::type;
40✔
324
    TestValueGenerator gen;
40✔
325
    Table table;
40✔
326
    ColKey col = table.add_column_list(TEST_TYPE::data_type, "values", TEST_TYPE::is_nullable);
40✔
327

20✔
328
    Obj obj = table.create_object();
40✔
329
    Lst<type> list = obj.get_list<type>(col);
40✔
330
    list.add(gen.convert_for_test<underlying_type>(1));
40✔
331
    list.add(gen.convert_for_test<underlying_type>(2));
40✔
332
    list.swap(0, 1);
40✔
333
    CHECK_EQUAL(list.get(0), gen.convert_for_test<underlying_type>(2));
40✔
334
    CHECK_EQUAL(list.get(1), gen.convert_for_test<underlying_type>(1));
40✔
335
    CHECK_EQUAL(list.find_first(gen.convert_for_test<underlying_type>(2)), 0);
40✔
336
    CHECK_EQUAL(list.find_first(gen.convert_for_test<underlying_type>(1)), 1);
40✔
337
    CHECK(!list.is_null(0));
40✔
338
    CHECK(!list.is_null(1));
40✔
339

20✔
340
    Lst<type> list1;
40✔
341
    CHECK_EQUAL(list1.size(), 0);
40✔
342
    list1 = list;
40✔
343
    CHECK_EQUAL(list1.size(), 2);
40✔
344
    list.add(gen.convert_for_test<underlying_type>(3));
40✔
345
    CHECK_EQUAL(list.size(), 3);
40✔
346
    CHECK_EQUAL(list1.size(), 3);
40✔
347

20✔
348
    Query q = table.where().size_equal(col, 3); // SizeListNode
40✔
349
    CHECK_EQUAL(q.count(), 1);
40✔
350
    q = table.column<Lst<type>>(col).size() == 3; // SizeOperator expresison
40✔
351
    CHECK_EQUAL(q.count(), 1);
40✔
352

20✔
353
    Lst<type> list2 = list;
40✔
354
    CHECK_EQUAL(list2.size(), 3);
40✔
355
    list2.clear();
40✔
356
    CHECK_EQUAL(list2.size(), 0);
40✔
357

20✔
358
    if constexpr (TEST_TYPE::is_nullable) {
40✔
359
        list2.insert_null(0);
20✔
360
        CHECK_EQUAL(list.size(), 1);
20✔
361
        type item0 = list2.get(0);
20✔
362
        CHECK(value_is_null(item0));
20✔
363
        CHECK(list.is_null(0));
20✔
364
        CHECK(list.get_any(0).is_null());
20✔
365
    }
20✔
366
}
40✔
367

368
TEST_TYPES(List_Sort, Prop<int64_t>, Prop<float>, Prop<double>, Prop<Decimal128>, Prop<ObjectId>, Prop<Timestamp>,
369
           Prop<String>, Prop<BinaryData>, Prop<UUID>, Nullable<int64_t>, Nullable<float>, Nullable<double>,
370
           Nullable<Decimal128>, Nullable<ObjectId>, Nullable<Timestamp>, Nullable<String>, Nullable<BinaryData>,
371
           Nullable<UUID>)
372
{
36✔
373
    using type = typename TEST_TYPE::type;
36✔
374
    using underlying_type = typename TEST_TYPE::underlying_type;
36✔
375

18✔
376
    TestValueGenerator gen;
36✔
377
    Group g;
36✔
378
    TableRef t = g.add_table("table");
36✔
379
    ColKey col = t->add_column_list(TEST_TYPE::data_type, "values", TEST_TYPE::is_nullable);
36✔
380

18✔
381
    auto obj = t->create_object();
36✔
382
    auto list = obj.get_list<type>(col);
36✔
383

18✔
384
    std::vector<type> values = gen.values_from_int<type>({9, 4, 2, 7, 4, 1, 8, 11, 3, 4, 5, 22});
36✔
385
    std::vector<size_t> indices;
36✔
386
    type default_or_null = TEST_TYPE::default_value();
36✔
387
    values.push_back(default_or_null);
36✔
388
    obj.set_list_values(col, values);
36✔
389

18✔
390
    CHECK(list.has_changed());
36✔
391
    CHECK_NOT(list.has_changed());
36✔
392

18✔
393
    auto cmp = [&]() {
144✔
394
        CHECK_EQUAL(values.size(), indices.size());
144✔
395
        for (size_t i = 0; i < values.size(); i++) {
1,836✔
396
            CHECK_EQUAL(values[i], list.get(indices[i]));
1,692✔
397
        }
1,692✔
398
    };
144✔
399
    std::sort(values.begin(), values.end(), ::less());
36✔
400
    list.sort(indices);
36✔
401
    cmp();
36✔
402
    std::sort(values.begin(), values.end(), ::greater());
36✔
403
    list.sort(indices, false);
36✔
404
    cmp();
36✔
405
    CHECK_NOT(list.has_changed());
36✔
406

18✔
407
    underlying_type new_value = gen.convert_for_test<underlying_type>(6);
36✔
408
    values.push_back(new_value);
36✔
409
    list.add(type(new_value));
36✔
410
    CHECK(list.has_changed());
36✔
411
    std::sort(values.begin(), values.end(), ::less());
36✔
412
    list.sort(indices);
36✔
413
    cmp();
36✔
414

18✔
415
    values.resize(7);
36✔
416
    obj.set_list_values(col, values);
36✔
417
    std::sort(values.begin(), values.end(), ::greater());
36✔
418
    list.sort(indices, false);
36✔
419
    cmp();
36✔
420
}
36✔
421

422
TEST_TYPES(List_Distinct, Prop<int64_t>, Prop<float>, Prop<double>, Prop<Decimal128>, Prop<ObjectId>, Prop<Timestamp>,
423
           Prop<String>, Prop<BinaryData>, Prop<UUID>, Nullable<int64_t>, Nullable<float>, Nullable<double>,
424
           Nullable<Decimal128>, Nullable<ObjectId>, Nullable<Timestamp>, Nullable<String>, Nullable<BinaryData>,
425
           Nullable<UUID>)
426
{
36✔
427
    using type = typename TEST_TYPE::type;
36✔
428
    TestValueGenerator gen;
36✔
429
    Group g;
36✔
430
    TableRef t = g.add_table("table");
36✔
431
    ColKey col = t->add_column_list(TEST_TYPE::data_type, "values", TEST_TYPE::is_nullable);
36✔
432

18✔
433
    auto obj = t->create_object();
36✔
434
    auto list = obj.get_list<type>(col);
36✔
435

18✔
436
    std::vector<type> values = gen.values_from_int<type>({9, 4, 2, 7, 4, 9, 8, 11, 2, 4, 5});
36✔
437
    std::vector<type> distinct_values = gen.values_from_int<type>({9, 4, 2, 7, 8, 11, 5});
36✔
438
    type default_or_null = TEST_TYPE::default_value();
36✔
439
    values.push_back(default_or_null);
36✔
440
    distinct_values.push_back(default_or_null);
36✔
441
    std::vector<size_t> indices;
36✔
442
    obj.set_list_values(col, values);
36✔
443

18✔
444
    auto cmp = [&]() {
108✔
445
        CHECK_EQUAL(distinct_values.size(), indices.size());
108✔
446
        for (size_t i = 0; i < distinct_values.size(); i++) {
972✔
447
            CHECK_EQUAL(distinct_values[i], list.get(indices[i]));
864✔
448
        }
864✔
449
    };
108✔
450

18✔
451
    list.distinct(indices);
36✔
452
    cmp();
36✔
453
    list.distinct(indices, true);
36✔
454
    std::sort(distinct_values.begin(), distinct_values.end(), std::less<type>());
36✔
455
    cmp();
36✔
456
    list.distinct(indices, false);
36✔
457
    std::sort(distinct_values.begin(), distinct_values.end(), std::greater<type>());
36✔
458
    cmp();
36✔
459
}
36✔
460

461
TEST(List_MixedSwap)
462
{
2✔
463
    Group g;
2✔
464
    TableRef t = g.add_table("table");
2✔
465
    ColKey col = t->add_column_list(type_Mixed, "values");
2✔
466
    BinaryData bin("foo", 3);
2✔
467

1✔
468
    auto obj = t->create_object();
2✔
469
    auto list = obj.get_list<Mixed>(col);
2✔
470
    list.add("a");
2✔
471
    list.add("b");
2✔
472
    list.add("c");
2✔
473
    list.add(bin);
2✔
474
    list.move(2, 0);
2✔
475
    CHECK_EQUAL(list.get(0).get_string(), "c");
2✔
476
    CHECK_EQUAL(list.get(1).get_string(), "a");
2✔
477
    CHECK_EQUAL(list.get(2).get_string(), "b");
2✔
478
    CHECK_EQUAL(list.get(3).get_binary(), bin);
2✔
479
    list.swap(3, 2);
2✔
480
    CHECK_EQUAL(list.get(0).get_string(), "c");
2✔
481
    CHECK_EQUAL(list.get(1).get_string(), "a");
2✔
482
    CHECK_EQUAL(list.get(2).get_binary(), bin);
2✔
483
    CHECK_EQUAL(list.get(3).get_string(), "b");
2✔
484
}
2✔
485

486
TEST(List_DecimalMinMax)
487
{
2✔
488
    SHARED_GROUP_TEST_PATH(path);
2✔
489
    std::unique_ptr<Replication> hist(make_in_realm_history());
2✔
490
    DBRef sg = DB::create(*hist, path, DBOptions(crypt_key()));
2✔
491
    auto t = sg->start_write();
2✔
492
    auto table = t->add_table("the_table");
2✔
493
    auto col = table->add_column_list(type_Decimal, "the column");
2✔
494
    Obj o = table->create_object();
2✔
495
    Lst<Decimal128> lst = o.get_list<Decimal128>(col);
2✔
496
    std::string larger_than_max_int64_t = "123.45e99";
2✔
497
    lst.add(Decimal128(larger_than_max_int64_t));
2✔
498
    CHECK_EQUAL(lst.size(), 1);
2✔
499
    CHECK_EQUAL(lst.get(0), Decimal128(larger_than_max_int64_t));
2✔
500
    size_t min_ndx = realm::npos;
2✔
501
    auto min = lst.min(&min_ndx);
2✔
502
    CHECK(min);
2✔
503
    CHECK_EQUAL(min_ndx, 0);
2✔
504
    CHECK_EQUAL(min->get<Decimal128>(), Decimal128(larger_than_max_int64_t));
2✔
505
    lst.clear();
2✔
506
    CHECK_EQUAL(lst.size(), 0);
2✔
507
    std::string smaller_than_min_int64_t = "-123.45e99";
2✔
508
    lst.add(Decimal128(smaller_than_min_int64_t));
2✔
509
    CHECK_EQUAL(lst.size(), 1);
2✔
510
    CHECK_EQUAL(lst.get(0), Decimal128(smaller_than_min_int64_t));
2✔
511
    size_t max_ndx = realm::npos;
2✔
512
    auto max = lst.max(&max_ndx);
2✔
513
    CHECK(max);
2✔
514
    CHECK_EQUAL(max_ndx, 0);
2✔
515
    CHECK_EQUAL(max->get<Decimal128>(), Decimal128(smaller_than_min_int64_t));
2✔
516
}
2✔
517

518

519
template <typename T, typename U = T>
520
void test_lists_numeric_agg(TestContext& test_context, DBRef sg, const realm::DataType type_id, U null_value = U{},
521
                            bool optional = false)
522
{
16✔
523
    auto t = sg->start_write();
16✔
524
    auto table = t->add_table("the_table");
16✔
525
    auto col = table->add_column_list(type_id, "the column", optional);
16✔
526
    Obj o = table->create_object();
16✔
527
    Lst<T> lst = o.get_list<T>(col);
16✔
528
    for (int j = -1000; j < 1000; ++j) {
32,016✔
529
        T value = T(j);
32,000✔
530
        lst.add(value);
32,000✔
531
    }
32,000✔
532
    if (optional) {
16✔
533
        // given that sum/avg do not count nulls and min/max ignore nulls,
4✔
534
        // adding any number of null values should not affect the results of any aggregates
4✔
535
        for (size_t i = 0; i < 1000; ++i) {
8,008!
536
            lst.add(null_value);
8,000✔
537
        }
8,000✔
538
    }
8✔
539
    for (int j = -1000; j < 1000; ++j) {
32,016✔
540
        CHECK_EQUAL(lst.get(j + 1000), T(j));
32,000✔
541
    }
32,000✔
542
    {
16✔
543
        size_t ret_ndx = realm::npos;
16✔
544
        auto min = lst.min(&ret_ndx);
16✔
545
        CHECK(min);
16✔
546
        CHECK(!min->is_null());
16✔
547
        CHECK_EQUAL(ret_ndx, 0);
16✔
548
        CHECK_EQUAL(min->template get<ColumnMinMaxType<T>>(), ColumnMinMaxType<T>(-1000));
16✔
549
        auto max = lst.max(&ret_ndx);
16✔
550
        CHECK(max);
16✔
551
        CHECK(!max->is_null());
16✔
552
        CHECK_EQUAL(ret_ndx, 1999);
16✔
553
        CHECK_EQUAL(max->template get<ColumnMinMaxType<T>>(), ColumnMinMaxType<T>(999));
16✔
554
        size_t ret_count = 0;
16✔
555
        auto sum = lst.sum(&ret_count);
16✔
556
        CHECK(sum);
16✔
557
        CHECK(!sum->is_null());
16✔
558
        CHECK_EQUAL(ret_count, 2000);
16✔
559
        CHECK_EQUAL(sum->template get<ColumnSumType<T>>(), ColumnSumType<T>(-1000));
16✔
560
        auto avg = lst.avg(&ret_count);
16✔
561
        CHECK(avg);
16✔
562
        CHECK(!avg->is_null());
16✔
563
        CHECK_EQUAL(ret_count, 2000);
16✔
564
        CHECK_EQUAL(avg->template get<ColumnAverageType<T>>(),
16✔
565
                    (ColumnAverageType<T>(-1000) / ColumnAverageType<T>(2000)));
16✔
566
    }
16✔
567

8✔
568
    lst.clear();
16✔
569
    CHECK_EQUAL(lst.size(), 0);
16✔
570
    {
16✔
571
        size_t ret_ndx = realm::npos;
16✔
572
        auto min = lst.min(&ret_ndx);
16✔
573
        CHECK(min);
16✔
574
        CHECK_EQUAL(ret_ndx, realm::npos);
16✔
575
        ret_ndx = realm::npos;
16✔
576
        auto max = lst.max(&ret_ndx);
16✔
577
        CHECK(max);
16✔
578
        CHECK_EQUAL(ret_ndx, realm::npos);
16✔
579
        size_t ret_count = realm::npos;
16✔
580
        auto sum = lst.sum(&ret_count);
16✔
581
        CHECK(sum);
16✔
582
        CHECK_EQUAL(ret_count, 0);
16✔
583
        ret_count = realm::npos;
16✔
584
        auto avg = lst.avg(&ret_count);
16✔
585
        CHECK(avg);
16✔
586
        CHECK_EQUAL(ret_count, 0);
16✔
587
    }
16✔
588

8✔
589
    lst.add(T(1));
16✔
590
    {
16✔
591
        size_t ret_ndx = realm::npos;
16✔
592
        auto min = lst.min(&ret_ndx);
16✔
593
        CHECK(min);
16✔
594
        CHECK(!min->is_null());
16✔
595
        CHECK_EQUAL(ret_ndx, 0);
16✔
596
        CHECK_EQUAL(min->template get<ColumnMinMaxType<T>>(), ColumnMinMaxType<T>(1));
16✔
597
        auto max = lst.max(&ret_ndx);
16✔
598
        CHECK(max);
16✔
599
        CHECK(!max->is_null());
16✔
600
        CHECK_EQUAL(ret_ndx, 0);
16✔
601
        CHECK_EQUAL(max->template get<ColumnMinMaxType<T>>(), ColumnMinMaxType<T>(1));
16✔
602
        size_t ret_count = 0;
16✔
603
        auto sum = lst.sum(&ret_count);
16✔
604
        CHECK(sum);
16✔
605
        CHECK(!sum->is_null());
16✔
606
        CHECK_EQUAL(ret_count, 1);
16✔
607
        CHECK_EQUAL(sum->template get<ColumnSumType<T>>(), ColumnSumType<T>(1));
16✔
608
        auto avg = lst.avg(&ret_count);
16✔
609
        CHECK(avg);
16✔
610
        CHECK(!avg->is_null());
16✔
611
        CHECK_EQUAL(ret_count, 1);
16✔
612
        CHECK_EQUAL(avg->template get<ColumnAverageType<T>>(), ColumnAverageType<T>(1));
16✔
613
    }
16✔
614

8✔
615
    t->rollback();
16✔
616
}
16✔
617

618
TEST(List_AggOps)
619
{
2✔
620
    SHARED_GROUP_TEST_PATH(path);
2✔
621

1✔
622
    std::unique_ptr<Replication> hist(make_in_realm_history());
2✔
623
    DBRef sg = DB::create(*hist, path, DBOptions(crypt_key()));
2✔
624

1✔
625
    test_lists_numeric_agg<int64_t>(test_context, sg, type_Int);
2✔
626
    test_lists_numeric_agg<float>(test_context, sg, type_Float);
2✔
627
    test_lists_numeric_agg<double>(test_context, sg, type_Double);
2✔
628
    test_lists_numeric_agg<Decimal128>(test_context, sg, type_Decimal);
2✔
629

1✔
630
    test_lists_numeric_agg<Optional<int64_t>>(test_context, sg, type_Int, Optional<int64_t>{}, true);
2✔
631
    test_lists_numeric_agg<float>(test_context, sg, type_Float, realm::null::get_null_float<float>(), true);
2✔
632
    test_lists_numeric_agg<double>(test_context, sg, type_Double, realm::null::get_null_float<double>(), true);
2✔
633
    test_lists_numeric_agg<Decimal128>(test_context, sg, type_Decimal, Decimal128(realm::null()), true);
2✔
634
}
2✔
635

636
TEST(List_Nested_InMixed)
637
{
2✔
638
    SHARED_GROUP_TEST_PATH(path);
2✔
639
    std::string message;
2✔
640
    DBOptions options;
2✔
641
    options.logger = test_context.logger;
2✔
642
    DBRef db = DB::create(make_in_realm_history(), path, options);
2✔
643
    auto tr = db->start_write();
2✔
644
    auto table = tr->add_table_with_primary_key("table", type_Int, "id");
2✔
645
    auto col_any = table->add_column(type_Mixed, "something");
2✔
646

1✔
647
    Obj obj = table->create_object_with_primary_key(1);
2✔
648

1✔
649
    obj.set_collection(col_any, CollectionType::Dictionary);
2✔
650
    auto illegal = obj.get_list_ptr<Mixed>(col_any);
2✔
651
    CHECK_THROW(illegal->insert(0, "xyz"), IllegalOperation);
2✔
652
    auto dict = obj.get_dictionary_ptr(col_any);
2✔
653
    CHECK(dict->is_empty());
2✔
654
    dict->insert("Four", 4);
2✔
655
    obj.set_collection(col_any, CollectionType::Dictionary); // Idempotent
2✔
656
    tr->verify();
2✔
657
    tr->commit_and_continue_as_read();
2✔
658
    /*
1✔
659
    {
1✔
660
      "table": [
1✔
661
        {
1✔
662
          "_key": 0,
1✔
663
          "something": {
1✔
664
            "Four": 4
1✔
665
          }
1✔
666
        }
1✔
667
      ]
1✔
668
    }
1✔
669
    */
1✔
670
    CHECK_EQUAL(dict->get("Four"), Mixed(4));
2✔
671

1✔
672
    tr->promote_to_write();
2✔
673
    dict->insert_collection("Dict", CollectionType::Dictionary);
2✔
674
    auto dict2 = dict->get_dictionary("Dict");
2✔
675
    CHECK(dict2->is_empty());
2✔
676
    dict2->insert("Five", 5);
2✔
677
    tr->verify();
2✔
678
    tr->commit_and_continue_as_read();
2✔
679
    /*
1✔
680
    {
1✔
681
      "table": [
1✔
682
        {
1✔
683
          "_key": 0,
1✔
684
          "something": {
1✔
685
            "Dict": {
1✔
686
              "Five": 5
1✔
687
            },
1✔
688
            "Four": 4
1✔
689
          }
1✔
690
        }
1✔
691
      ]
1✔
692
    }
1✔
693
    */
1✔
694

1✔
695
    tr->promote_to_write();
2✔
696
    dict->insert_collection("Dict", CollectionType::Dictionary); // Idempotent, but updates dict accessor
2✔
697
    dict2->insert_collection("List", CollectionType::List);      // dict2 should update
2✔
698
    {
2✔
699
        auto list = dict2->get_list("List");
2✔
700
        CHECK_EQUAL(dict2->get_col_key(), col_any);
2✔
701
        CHECK(list->is_empty());
2✔
702
        CHECK_EQUAL(list->get_col_key(), col_any);
2✔
703
        list->add(8);
2✔
704
        list->add(9);
2✔
705
    }
2✔
706
    tr->verify();
2✔
707
    {
2✔
708
        std::stringstream ss;
2✔
709
        tr->to_json(ss, JSONOutputMode::output_mode_xjson_plus);
2✔
710
        auto j = nlohmann::json::parse(ss.str());
2✔
711
    }
2✔
712
    // std::cout << std::setw(2) << j << std::endl;
1✔
713
    tr->commit_and_continue_as_read();
2✔
714
    /*
1✔
715
    {
1✔
716
      "table": [
1✔
717
        {
1✔
718
          "_key": 0,
1✔
719
          "something": {
1✔
720
            "Dict": {
1✔
721
              "Five": 5,
1✔
722
              "List": [
1✔
723
                8,
1✔
724
                9
1✔
725
              ]
1✔
726
            },
1✔
727
            "Four": 4
1✔
728
          }
1✔
729
        }
1✔
730
      ]
1✔
731
    }
1✔
732
    */
1✔
733

1✔
734
    auto list = obj.get_collection_ptr({"something", "Dict", "List"});
2✔
735
    CHECK_EQUAL(dynamic_cast<Lst<Mixed>*>(list.get())->get(0).get_int(), 8);
2✔
736

1✔
737
    tr->promote_to_write();
2✔
738
    dict->insert("Dict", Mixed());
2✔
739
    CHECK_THROW_ANY_GET_MESSAGE(dict2->insert("Five", 5), message); // This dictionary ceased to be
2✔
740
    CHECK_EQUAL(message, "This collection is no more");
2✔
741
    // Try to insert a new dictionary. The old dict2 should still be stale
1✔
742
    // Well - we can't be sure of that. But it would not be critical - it is still a dictionary
1✔
743
    // dict->insert_collection("Dict", CollectionType::Dictionary);
1✔
744
    // CHECK_THROW_ANY_GET_MESSAGE(dict2->insert("Five", 5), message); // This dictionary ceased to be
1✔
745
    // CHECK_EQUAL(message, "This collection is no more");
1✔
746
    // Assign another value. The old dictionary should be disposed.
1✔
747
    obj.set(col_any, Mixed(5));
2✔
748
    tr->verify();
2✔
749
    tr->commit_and_continue_as_read();
2✔
750

1✔
751
    tr->promote_to_write();
2✔
752
    obj.set_collection(col_any, CollectionType::List);
2✔
753
    auto list2 = std::dynamic_pointer_cast<Lst<Mixed>>(obj.get_collection_ptr(col_any));
2✔
754
    CHECK(list2->is_empty());
2✔
755
    list2->add("Hello");
2✔
756
    list2->insert_collection(0, CollectionType::Dictionary);
2✔
757
    list2->add(42);
2✔
758
    dict2 = list2->get_dictionary(0);
2✔
759
    dict2->insert("Six", 6);
2✔
760
    tr->verify();
2✔
761
    dict2->insert("Seven", 7);
2✔
762
    list2->set_collection(2, CollectionType::Dictionary);
2✔
763
    dict2 = list2->get_dictionary(2);
2✔
764
    dict2->insert("Hello", "World");
2✔
765
    dict2->insert("Date", Timestamp(std::chrono::system_clock::now()));
2✔
766
    {
2✔
767
        std::stringstream ss;
2✔
768
        tr->to_json(ss, JSONOutputMode::output_mode_xjson_plus);
2✔
769
        auto j = nlohmann::json::parse(ss.str());
2✔
770
        // std::cout << std::setw(2) << j << std::endl;
1✔
771
    }
2✔
772
    tr->verify();
2✔
773
    tr->commit_and_continue_as_read();
2✔
774
    /*
1✔
775
    {
1✔
776
      "table": [
1✔
777
        {
1✔
778
          "_key": 0,
1✔
779
          "something": [
1✔
780
            {
1✔
781
              "Seven": 7,
1✔
782
              "Six": 6
1✔
783
            },
1✔
784
            "Hello",
1✔
785
            {
1✔
786
              "Date": "2023-05-09 07:52:49",
1✔
787
              "Hello": "World"
1✔
788
            }
1✔
789
          ]
1✔
790
        }
1✔
791
      ]
1✔
792
    }
1✔
793
    */
1✔
794
    CHECK_EQUAL(list2->get(1), Mixed("Hello"));
2✔
795
    tr->promote_to_write();
2✔
796
    list2->remove(1);
2✔
797
    CHECK_EQUAL(dict2->get("Hello"), Mixed("World"));
2✔
798
    obj.set(col_any, Mixed());
2✔
799
    CHECK_THROW_ANY_GET_MESSAGE(dict->size(), message);
2✔
800
    CHECK_EQUAL(message, "This collection is no more");
2✔
801
    CHECK_THROW_ANY_GET_MESSAGE(dict->insert("Five", 5), message); // This dictionary ceased to be
2✔
802
    CHECK_EQUAL(message, "This collection is no more");
2✔
803
    CHECK_THROW_ANY_GET_MESSAGE(dict->get("Five"), message);
2✔
804
    CHECK_EQUAL(message, "This collection is no more");
2✔
805

1✔
806
    obj.set_collection(col_any, CollectionType::List);
2✔
807
    auto list3 = obj.get_list_ptr<Mixed>(col_any);
2✔
808
    list3->add(5);
2✔
809
    obj.set(col_any, Mixed());
2✔
810
    CHECK_THROW_ANY(list3->size());
2✔
811
    CHECK_THROW_ANY_GET_MESSAGE(list3->add(42), message);
2✔
812
    CHECK_EQUAL(message, "This collection is no more");
2✔
813
    CHECK_THROW_ANY_GET_MESSAGE(list3->insert(5, 42), message);
2✔
814
    CHECK_EQUAL(message, "This collection is no more");
2✔
815
    CHECK_THROW_ANY_GET_MESSAGE(list3->get(5), message);
2✔
816
    CHECK_EQUAL(message, "This collection is no more");
2✔
817
    // Try creating a new list. list3 should still be stale
1✔
818
    obj.set_collection(col_any, CollectionType::List);
2✔
819
    CHECK_THROW_ANY_GET_MESSAGE(list3->add(42), message);
2✔
820
    CHECK_EQUAL(message, "This collection is no more");
2✔
821
    tr->verify();
2✔
822
    obj.set_json(col_any,
2✔
823
                 "[{\"Seven\":7, \"Six\":6}, \"Hello\", {\"Points\": [1.25, 4.5, 6.75], \"Hello\": \"World\"}]");
2✔
824
    CHECK_EQUAL(obj.get_list_ptr<Mixed>(col_any)->size(), 3);
2✔
825
    // tr->to_json(std::cout);
1✔
826
}
2✔
827

828

829
TEST(List_NestedCollection_Links)
830
{
2✔
831
    SHARED_GROUP_TEST_PATH(path);
2✔
832
    DBRef db = DB::create(make_in_realm_history(), path);
2✔
833
    auto tr = db->start_write();
2✔
834
    auto embedded = tr->add_table("embedded", Table::Type::Embedded);
2✔
835
    auto target = tr->add_table("target");
2✔
836
    auto origin = tr->add_table("origin");
2✔
837
    auto list_col = origin->add_column_list(type_Mixed, "any_list");
2✔
838
    auto any_col = origin->add_column(type_Mixed, "any");
2✔
839
    auto embedded_col = origin->add_column(*embedded, "sub");
2✔
840

1✔
841
    Obj target_obj1 = target->create_object();
2✔
842
    Obj target_obj2 = target->create_object();
2✔
843
    Obj target_obj3 = target->create_object();
2✔
844
    Obj parent = origin->create_object();
2✔
845
    parent.create_and_set_linked_object(embedded_col);
2✔
846
    auto child_obj = parent.get_linked_object(embedded_col);
2✔
847
    tr->commit_and_continue_as_read();
2✔
848

1✔
849
    Obj o;
2✔
850
    ListMixedPtr list;
2✔
851
    ListMixedPtr list1;
2✔
852
    ListMixedPtr list2;
2✔
853
    Dictionary dict_any;
2✔
854

1✔
855
    auto create_links = [&]() {
4✔
856
        tr->promote_to_write();
4✔
857
        o = origin->create_object();
4✔
858
        list = o.get_list_ptr<Mixed>(list_col);
4✔
859
        CHECK_THROW_ANY(list->add(child_obj.get_link()));
4✔
860
        list->insert_collection(0, CollectionType::Dictionary);
4✔
861
        list->insert_collection(1, CollectionType::Dictionary);
4✔
862

2✔
863
        // Create link from a dictionary contained in a list
2✔
864
        auto dict0 = list->get_dictionary(0);
4✔
865
        dict0->insert("Key", target_obj2.get_link());
4✔
866

2✔
867
        // Create link from a list contained in a dictionary contained in a list
2✔
868
        auto dict1 = list->get_dictionary(1);
4✔
869
        dict1->insert_collection("Hello", CollectionType::List);
4✔
870
        list1 = dict1->get_list("Hello");
4✔
871
        CHECK_THROW_ANY(list1->add(child_obj.get_link()));
4✔
872
        list1->add(target_obj1.get_link());
4✔
873

2✔
874
        // Create link from a collection nested in a Mixed property
2✔
875
        o.set_collection(any_col, CollectionType::Dictionary);
4✔
876
        dict_any = o.get_dictionary(any_col);
4✔
877
        dict_any.insert("Godbye", target_obj1.get_link());
4✔
878
        CHECK_THROW_ANY(dict_any.insert("Wrong", child_obj.get_link()));
4✔
879

2✔
880
        // Create link from a list nested in a collection nested in a Mixed property
2✔
881
        dict_any.insert_collection("List", CollectionType::List);
4✔
882
        list2 = dict_any.get_list("List");
4✔
883
        list2->add(target_obj3.get_link());
4✔
884
        tr->commit_and_continue_as_read();
4✔
885
        // Check that backlinks are created
2✔
886
        CHECK_EQUAL(target_obj1.get_backlink_count(), 2);
4✔
887
        CHECK_EQUAL(target_obj2.get_backlink_count(), 1);
4✔
888
        CHECK_EQUAL(target_obj3.get_backlink_count(), 1);
4✔
889
    };
4✔
890

1✔
891
    create_links();
2✔
892

1✔
893
    // When target object is removed, link should be removed from list
1✔
894
    tr->promote_to_write();
2✔
895
    target_obj1.remove();
2✔
896
    tr->commit_and_continue_as_read();
2✔
897

1✔
898
    CHECK_EQUAL(list1->size(), 0);
2✔
899
    // and cleared in dictionary
1✔
900
    CHECK_EQUAL(dict_any.get("Godbye"), Mixed());
2✔
901
    tr->promote_to_write();
2✔
902
    // Create links again
1✔
903
    target_obj1 = target->create_object();
2✔
904
    list1->insert(0, target_obj1.get_link());
2✔
905
    dict_any.insert("Godbye", target_obj1.get_link());
2✔
906
    CHECK_EQUAL(target_obj1.get_backlink_count(), 2);
2✔
907

1✔
908
    // When list is removed, backlink should go
1✔
909
    list->remove(1);
2✔
910
    CHECK_EQUAL(target_obj1.get_backlink_count(), 1);
2✔
911
    // This will implicitly delete dict_any
1✔
912
    o.set(any_col, Mixed(5));
2✔
913
    CHECK_EQUAL(target_obj1.get_backlink_count(), 0);
2✔
914
    CHECK_EQUAL(target_obj3.get_backlink_count(), 0);
2✔
915
    // Link still there
1✔
916
    CHECK_EQUAL(target_obj2.get_backlink_count(), 1);
2✔
917
    o.remove();
2✔
918
    CHECK_EQUAL(target_obj2.get_backlink_count(), 0);
2✔
919
    tr->commit_and_continue_as_read();
2✔
920

1✔
921
    create_links();
2✔
922
    // Clearing dictionary should remove links
1✔
923
    tr->promote_to_write();
2✔
924
    dict_any.clear();
2✔
925
    tr->commit_and_continue_as_read();
2✔
926
    CHECK_EQUAL(target_obj1.get_backlink_count(), 1);
2✔
927
    CHECK_EQUAL(target_obj3.get_backlink_count(), 0);
2✔
928
}
2✔
929

930
TEST(List_NestedCollection_Unresolved)
931
{
2✔
932
    SHARED_GROUP_TEST_PATH(path);
2✔
933
    DBRef db = DB::create(make_in_realm_history(), path);
2✔
934
    auto tr = db->start_write();
2✔
935
    auto target = tr->add_table_with_primary_key("target", type_String, "_id");
2✔
936
    auto origin = tr->add_table("origin");
2✔
937
    auto col_any = origin->add_column(type_Mixed, "any");
2✔
938

1✔
939
    Obj o = origin->create_object();
2✔
940
    Obj target_obj = target->create_object_with_primary_key("Adam");
2✔
941

1✔
942
    o.set_collection(col_any, CollectionType::Dictionary);
2✔
943
    Dictionary dict(o, col_any);
2✔
944

1✔
945
    dict.insert("A", target_obj.get_link());
2✔
946
    CHECK_EQUAL(target_obj.get_backlink_count(), 1);
2✔
947
    // Make a tombstone for Adam
1✔
948
    target->invalidate_object(target_obj.get_key());
2✔
949
    CHECK(dict.get("A").is_null());
2✔
950
    // And resurrect
1✔
951
    auto obj = target->create_object_with_primary_key("Adam");
2✔
952
    CHECK_EQUAL(obj.get_backlink_count(), 1);
2✔
953
    CHECK_EQUAL(dict.get("A"), Mixed(obj.get_link()));
2✔
954

1✔
955
    // Now do the same, but with a list
1✔
956
    o.set_collection(col_any, CollectionType::List);
2✔
957
    CHECK_EQUAL(obj.get_backlink_count(), 0);
2✔
958
    Lst<Mixed> list(o, col_any);
2✔
959

1✔
960
    list.insert(0, obj.get_link());
2✔
961
    CHECK_EQUAL(obj.get_backlink_count(), 1);
2✔
962
    // Make a tombstone for Adam
1✔
963
    target->invalidate_object(obj.get_key());
2✔
964
    CHECK_EQUAL(list.get(0), Mixed());
2✔
965
    // And resurrect
1✔
966
    obj = target->create_object_with_primary_key("Adam");
2✔
967
    CHECK_EQUAL(obj.get_backlink_count(), 1);
2✔
968
    CHECK_EQUAL(list.get(0), Mixed(obj.get_link()));
2✔
969
}
2✔
970

971
TEST(List_NestedList_Path)
972
{
2✔
973
    Group g;
2✔
974
    auto top_table = g.add_table_with_primary_key("top", type_String, "_id");
2✔
975
    auto embedded_table = g.add_table("embedded", Table::Type::Embedded);
2✔
976
    auto string_col = top_table->add_column_list(type_String, "strings");
2✔
977
    auto col_embedded_any = embedded_table->add_column(type_Mixed, "Any");
2✔
978
    auto col_any = top_table->add_column(type_Mixed, "Any");
2✔
979
    auto col_child = top_table->add_column(*embedded_table, "Child");
2✔
980

1✔
981
    Obj o = top_table->create_object_with_primary_key("Adam");
2✔
982

1✔
983
    // First level list
1✔
984
    {
2✔
985
        auto list_string = o.get_list<String>(string_col);
2✔
986
        auto path = list_string.get_path();
2✔
987
        CHECK_EQUAL(path.path_from_top.size(), 1);
2✔
988
        CHECK_EQUAL(path.path_from_top[0], string_col);
2✔
989
    }
2✔
990

1✔
991
    // List nested in Dictionary contained in embedded object
1✔
992
    {
2✔
993
        auto embedded_obj = o.create_and_set_linked_object(col_child);
2✔
994
        embedded_obj.set_collection(col_embedded_any, CollectionType::Dictionary);
2✔
995
        embedded_obj.get_dictionary(col_embedded_any).insert_collection("Foo", CollectionType::List);
2✔
996
        auto list_int = embedded_obj.get_list_ptr<Mixed>({"Any", "Foo"});
2✔
997
        list_int->add(5);
2✔
998
        auto path = list_int->get_path();
2✔
999
        CHECK_EQUAL(path.path_from_top.size(), 3);
2✔
1000
        CHECK_EQUAL(path.path_from_top[0], col_child);
2✔
1001
        CHECK_EQUAL(path.path_from_top[1], "Any");
2✔
1002
        CHECK_EQUAL(path.path_from_top[2], "Foo");
2✔
1003
    }
2✔
1004

1✔
1005
    // Collections contained in Mixed
1✔
1006
    {
2✔
1007
        o.set_collection(col_any, CollectionType::Dictionary);
2✔
1008
        auto dict = o.get_dictionary_ptr(col_any);
2✔
1009
        dict->insert_collection("List", CollectionType::List);
2✔
1010
        auto list = dict->get_list("List");
2✔
1011
        list->add(Mixed(5));
2✔
1012
        list->insert_collection(1, CollectionType::Dictionary);
2✔
1013
        auto dict2 = o.get_collection_ptr({"Any", "List", 1});
2✔
1014
        auto path = dict2->get_path();
2✔
1015
        CHECK_EQUAL(path.path_from_top.size(), 3);
2✔
1016
        CHECK_EQUAL(path.path_from_top[0], col_any);
2✔
1017
        CHECK_EQUAL(path.path_from_top[1], "List");
2✔
1018
        CHECK_EQUAL(path.path_from_top[2], 1);
2✔
1019
    }
2✔
1020
}
2✔
1021

1022
TEST(List_Nested_Replication)
1023
{
2✔
1024
    SHARED_GROUP_TEST_PATH(path);
2✔
1025
    DBRef db = DB::create(make_in_realm_history(), path);
2✔
1026
    auto tr = db->start_write();
2✔
1027
    auto table = tr->add_table("table");
2✔
1028
    auto col_any = table->add_column(type_Mixed, "something");
2✔
1029

1✔
1030
    Obj obj = table->create_object();
2✔
1031

1✔
1032
    obj.set_collection(col_any, CollectionType::Dictionary);
2✔
1033
    auto dict = obj.get_dictionary_ptr(col_any);
2✔
1034
    dict->insert_collection("level1", CollectionType::Dictionary);
2✔
1035
    auto dict2 = dict->get_dictionary("level1");
2✔
1036
    dict2->insert("Paul", "McCartney");
2✔
1037
    tr->commit_and_continue_as_read();
2✔
1038

1✔
1039
    {
2✔
1040
        auto wt = db->start_write();
2✔
1041
        auto t = wt->get_table("table");
2✔
1042
        auto o = *t->begin();
2✔
1043
        auto d = o.get_collection_ptr({"something", "level1"});
2✔
1044

1✔
1045
        dynamic_cast<Dictionary*>(d.get())->insert("John", "Lennon");
2✔
1046
        wt->commit();
2✔
1047
    }
2✔
1048

1✔
1049
    struct Parser : _impl::NoOpTransactionLogParser {
2✔
1050
        TestContext& test_context;
2✔
1051
        Parser(TestContext& context)
2✔
1052
            : test_context(context)
2✔
1053
        {
2✔
1054
        }
2✔
1055

1✔
1056
        bool collection_insert(size_t ndx)
2✔
1057
        {
2✔
1058
            auto collection_path = get_path();
2✔
1059
            CHECK(collection_path[1] == expected_path[1]);
2✔
1060
            CHECK(ndx == 0);
2✔
1061
            return true;
2✔
1062
        }
2✔
1063

1✔
1064
        StablePath expected_path;
2✔
1065
    } parser(test_context);
2✔
1066

1✔
1067
    auto dict2_index = dict->build_index("level1");
2✔
1068
    parser.expected_path.push_back(StableIndex());
2✔
1069
    parser.expected_path.push_back(dict2_index);
2✔
1070
    tr->advance_read(&parser);
2✔
1071
    Dictionary dict3(*dict, dict2_index);
2✔
1072
    CHECK_EQUAL(dict3.get_col_key(), col_any);
2✔
1073
}
2✔
1074

1075
namespace realm {
1076
static std::ostream& operator<<(std::ostream& os, UpdateStatus status)
NEW
1077
{
×
NEW
1078
    switch (status) {
×
NEW
1079
        case UpdateStatus::Detached:
×
NEW
1080
            os << "Detatched";
×
NEW
1081
            break;
×
NEW
1082
        case UpdateStatus::Updated:
×
NEW
1083
            os << "Updated";
×
NEW
1084
            break;
×
NEW
1085
        case UpdateStatus::NoChange:
×
NEW
1086
            os << "NoChange";
×
NEW
1087
            break;
×
NEW
1088
    }
×
NEW
1089
    return os;
×
NEW
1090
}
×
1091
} // namespace realm
1092

1093
TEST(List_UpdateIfNeeded)
1094
{
2✔
1095
    SHARED_GROUP_TEST_PATH(path);
2✔
1096
    DBRef db = DB::create(make_in_realm_history(), path);
2✔
1097
    auto tr = db->start_write();
2✔
1098
    auto table = tr->add_table("table");
2✔
1099
    auto col = table->add_column(type_Mixed, "mixed");
2✔
1100
    auto col2 = table->add_column(type_Mixed, "col2");
2✔
1101
    auto leading_obj = table->create_object();
2✔
1102
    Obj obj = table->create_object();
2✔
1103
    obj.set_collection(col, CollectionType::List);
2✔
1104

1✔
1105
    auto list_1 = obj.get_list<Mixed>(col);
2✔
1106
    auto list_2 = obj.get_list<Mixed>(col);
2✔
1107

1✔
1108
    // The underlying object starts out up-to-date
1✔
1109
    CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::NoChange);
2✔
1110

1✔
1111
    // Attempt to initialize the accessor and fail because the list is empty,
1✔
1112
    // leaving it detached (only size() can be called on an empty list)
1✔
1113
    CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Detached);
2✔
1114
    CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Detached);
2✔
1115

1✔
1116
    list_1.add(Mixed());
2✔
1117

1✔
1118
    // First accessor was used to create the list so it's already up to date,
1✔
1119
    // but the second is updated
1✔
1120
    CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::NoChange);
2✔
1121
    CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated);
2✔
1122

1✔
1123
    // The list is now non-empty, so a new accessor can initialize
1✔
1124
    auto list_3 = obj.get_list<Mixed>(col);
2✔
1125
    CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated);
2✔
1126
    CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::NoChange);
2✔
1127

1✔
1128
    // A copy of a list is lazily initialized, so it's updated on first call
1✔
1129
    // even if the source was up-to-date
1✔
1130
    auto list_4 = std::make_shared<Lst<Mixed>>(list_3);
2✔
1131
    CHECK_EQUAL(list_4->update_if_needed(), UpdateStatus::Updated);
2✔
1132

1✔
1133
    // Nested lists work the same way as top-level ones
1✔
1134
    list_4->insert_collection(1, CollectionType::List);
2✔
1135
    auto list_4_1 = list_4->get_list(1);
2✔
1136
    auto list_4_2 = list_4->get_list(1);
2✔
1137
    list_4_1->add(Mixed());
2✔
1138
    // FIXME: this should be NoChange
1✔
1139
    CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated);
2✔
1140
    CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated);
2✔
1141

1✔
1142
    // Update the row index of the parent object, forcing it to update
1✔
1143
    leading_obj.remove();
2✔
1144

1✔
1145
    // Updating the base object directly first doesn't change the result of
1✔
1146
    // updating the list
1✔
1147
    CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Updated);
2✔
1148
    CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Updated);
2✔
1149

1✔
1150
    CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated);
2✔
1151
    CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated);
2✔
1152

1✔
1153
    // These two lists share the same parent, so the first updates due to the
1✔
1154
    // parent returning Updated, and the second updates due to seeing that the
1✔
1155
    // parent version has changed
1✔
1156
    CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated);
2✔
1157
    CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated);
2✔
1158

1✔
1159
    tr->commit_and_continue_as_read();
2✔
1160

1✔
1161
    // Committing the write transaction changes the obj's ref, so everything
1✔
1162
    // has to update
1✔
1163
    CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Updated);
2✔
1164
    CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Updated);
2✔
1165
    CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated);
2✔
1166
    CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated);
2✔
1167
    CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated);
2✔
1168
    CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated);
2✔
1169

1✔
1170
    // Perform a write which does not result in obj changing
1✔
1171
    {
2✔
1172
        auto tr2 = db->start_write();
2✔
1173
        tr2->add_table("other table");
2✔
1174
        tr2->commit();
2✔
1175
    }
2✔
1176
    tr->advance_read();
2✔
1177

1✔
1178
    // The obj's storage version has changed, but nothing needs to update
1✔
1179
    CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::NoChange);
2✔
1180
    CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::NoChange);
2✔
1181
    CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::NoChange);
2✔
1182
    CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::NoChange);
2✔
1183
    CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::NoChange);
2✔
1184
    CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::NoChange);
2✔
1185

1✔
1186
    // Perform a write which does modify obj
1✔
1187
    {
2✔
1188
        auto tr2 = db->start_write();
2✔
1189
        tr2->get_table("table")->get_object(obj.get_key()).set_any(col2, "value");
2✔
1190
        tr2->commit();
2✔
1191
    }
2✔
1192
    tr->advance_read();
2✔
1193

1✔
1194
    // Everything needs to update even though the allocator's content version is unchanged
1✔
1195
    CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Updated);
2✔
1196
    CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Updated);
2✔
1197
    CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated);
2✔
1198
    CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated);
2✔
1199
    CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated);
2✔
1200
    CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated);
2✔
1201

1✔
1202
    // Everything updates to detached when the object is removed
1✔
1203
    tr->promote_to_write();
2✔
1204
    obj.remove();
2✔
1205

1✔
1206
    CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Detached);
2✔
1207
    CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Detached);
2✔
1208
    CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Detached);
2✔
1209
    CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Detached);
2✔
1210
    CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Detached);
2✔
1211
    CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Detached);
2✔
1212
}
2✔
1213

1214
TEST(List_AsCollectionParent)
1215
{
2✔
1216
    Group g;
2✔
1217
    auto table = g.add_table("table");
2✔
1218
    auto col = table->add_column(type_Mixed, "mixed");
2✔
1219

1✔
1220
    Obj obj = table->create_object();
2✔
1221
    obj.set_collection(col, CollectionType::List);
2✔
1222
    auto list_1 = obj.get_list<Mixed>(col);
2✔
1223
    list_1.insert_collection(0, CollectionType::List);
2✔
1224

1✔
1225
    // list_1 is stack allocated, so we have to create a new object which can
1✔
1226
    // serve as the owner. This object is not reused for multiple calls.
1✔
1227
    auto list_1_1 = list_1.get_list(0);
2✔
1228
    auto list_1_2 = list_1.get_list(0);
2✔
1229
    CHECK_NOT_EQUAL(list_1_1->get_owner(), &list_1);
2✔
1230
    CHECK_NOT_EQUAL(list_1_1->get_owner(), list_1_2->get_owner());
2✔
1231

1✔
1232
    // list_2 is heap allocated but not owned by a shared_ptr, so we have to
1✔
1233
    // create a new object which can serve as the owner. This object is not
1✔
1234
    // reused for multiple calls.
1✔
1235
    auto list_2 = obj.get_list_ptr<Mixed>(col);
2✔
1236
    auto list_2_1 = list_2->get_list(0);
2✔
1237
    auto list_2_2 = list_2->get_list(0);
2✔
1238
    CHECK_NOT_EQUAL(list_2_1->get_owner(), list_2.get());
2✔
1239
    CHECK_NOT_EQUAL(list_2_1->get_owner(), list_2_2->get_owner());
2✔
1240

1✔
1241
    // list_3 is owned by a shared_ptr, so we can just use it as the owner directly
1✔
1242
    auto list_3 = std::shared_ptr{std::move(list_2)};
2✔
1243
    auto list_3_1 = list_3->get_list(0);
2✔
1244
    auto list_3_2 = list_3->get_list(0);
2✔
1245
    CHECK_EQUAL(list_3_1->get_owner(), list_3.get());
2✔
1246
    CHECK_EQUAL(list_3_1->get_owner(), list_3_2->get_owner());
2✔
1247
}
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