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

realm / realm-core / jorgen.edelbo_138

13 Mar 2024 08:41AM UTC coverage: 91.77% (-0.3%) from 92.078%
jorgen.edelbo_138

Pull #7356

Evergreen

jedelbo
Add ability to get path to modified collections in object notifications
Pull Request #7356: Add ability to get path to modified collections in object notifications

94532 of 174642 branches covered (54.13%)

118 of 163 new or added lines in 16 files covered. (72.39%)

765 existing lines in 41 files now uncovered.

242808 of 264584 relevant lines covered (91.77%)

5878961.32 hits per line

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

98.85
/test/object-store/object.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2017 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 <catch2/catch_all.hpp>
20

21
#include "util/event_loop.hpp"
22
#include "util/index_helpers.hpp"
23
#include "util/test_file.hpp"
24
#include "util/test_utils.hpp"
25

26
#include <realm/object-store/feature_checks.hpp>
27
#include <realm/object-store/collection_notifications.hpp>
28
#include <realm/object-store/object_accessor.hpp>
29
#include <realm/object-store/property.hpp>
30
#include <realm/object-store/schema.hpp>
31
#include <realm/object-store/object.hpp>
32
#include <realm/object-store/util/scheduler.hpp>
33

34
#include <realm/object-store/impl/realm_coordinator.hpp>
35
#include <realm/object-store/impl/object_accessor_impl.hpp>
36

37
#include <realm/group.hpp>
38
#include <realm/sync/subscriptions.hpp>
39
#include <realm/util/any.hpp>
40

41
#include <cstdint>
42

43
using namespace realm;
44
using util::any_cast;
45

46
namespace {
47
using AnyDict = std::map<std::string, std::any>;
48
using AnyVec = std::vector<std::any>;
49
template <class T>
50
std::vector<T> get_vector(std::initializer_list<T> list)
51
{
20✔
52
    return std::vector<T>(list);
20✔
53
}
20✔
54
} // namespace
55

56
struct TestContext : CppContext {
57
    std::map<std::string, AnyDict> defaults;
58

59
    using CppContext::CppContext;
60
    TestContext(TestContext& parent, realm::Obj& obj, realm::Property const& prop)
61
        : CppContext(parent, obj, prop)
62
        , defaults(parent.defaults)
63
    {
530✔
64
    }
530✔
65

66
    util::Optional<std::any> default_value_for_property(ObjectSchema const& object, Property const& prop)
67
    {
366✔
68
        auto obj_it = defaults.find(object.name);
366✔
69
        if (obj_it == defaults.end())
366✔
70
            return util::none;
282✔
71
        auto prop_it = obj_it->second.find(prop.name);
84✔
72
        if (prop_it == obj_it->second.end())
84✔
73
            return util::none;
30✔
74
        return prop_it->second;
54✔
75
    }
54✔
76

77
    void will_change(Object const&, Property const&) {}
1,052✔
78
    void did_change() {}
1,050✔
79
    std::string print(std::any)
80
    {
2✔
81
        return "not implemented";
2✔
82
    }
2✔
83
    bool allow_missing(std::any)
84
    {
2✔
85
        return false;
2✔
86
    }
2✔
87

88
    template <class T>
89
    T get(Object& obj, const std::string& name)
90
    {
224✔
91
        return util::any_cast<T>(obj.get_property_value<std::any>(*this, name));
224✔
92
    }
224✔
93
};
94

95
class CreatePolicyRecordingContext {
96
public:
97
    CreatePolicyRecordingContext(CreatePolicyRecordingContext&, Obj, Property const&) {}
×
98
    CreatePolicyRecordingContext() = default;
99
    CreatePolicyRecordingContext(std::shared_ptr<Realm>, const ObjectSchema*) {}
×
100

101
    util::Optional<std::any> value_for_property(std::any&, const Property&, size_t) const
102
    {
×
103
        return util::none;
×
104
    }
×
105

106
    template <typename Func>
107
    void enumerate_collection(std::any&, Func&&)
108
    {
×
109
    }
×
110

111
    template <typename Func>
112
    void enumerate_dictionary(std::any&, Func&&)
113
    {
×
114
    }
×
115

116
    bool is_same_set(object_store::Set const&, std::any const&)
117
    {
×
118
        return false;
×
119
    }
×
120

121
    bool is_same_list(List const&, std::any const&)
122
    {
×
123
        return false;
×
124
    }
×
125

126
    bool is_same_dictionary(const object_store::Dictionary&, const std::any&)
127
    {
×
128
        return false;
×
129
    }
×
130

131
    std::any box(Mixed v) const
132
    {
×
133
        return v;
×
134
    }
×
135

136
    template <typename T>
137
    T unbox(std::any& v, CreatePolicy p, ObjKey = ObjKey()) const
138
    {
10✔
139
        last_create_policy = p;
10✔
140
        return util::any_cast<T>(v);
10✔
141
    }
10✔
142

143
    bool is_null(std::any const& v) const noexcept
144
    {
10✔
145
        return !v.has_value();
10✔
146
    }
10✔
147
    std::any null_value() const noexcept
148
    {
×
149
        return {};
×
150
    }
×
151

152
    void will_change(Object const&, Property const&) {}
10✔
153
    void did_change() {}
10✔
154

155
    mutable CreatePolicy last_create_policy;
156
};
157

158
TEST_CASE("object") {
140✔
159
    using namespace std::string_literals;
140✔
160
    _impl::RealmCoordinator::assert_no_open_realms();
140✔
161

70✔
162
    InMemoryTestFile config;
140✔
163
    config.automatic_change_notifications = false;
140✔
164
    config.schema_mode = SchemaMode::AdditiveExplicit;
140✔
165
    config.schema = Schema{
140✔
166
        {"table",
140✔
167
         {
140✔
168
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
140✔
169
             {"value 1", PropertyType::Int},
140✔
170
             {"value 2", PropertyType::Int},
140✔
171
         },
140✔
172
         {
140✔
173
             {"origin", PropertyType::LinkingObjects | PropertyType::Array, "table2", "link"},
140✔
174
         }},
140✔
175
        {"table2",
140✔
176
         {
140✔
177
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
140✔
178
             {"value", PropertyType::Int},
140✔
179
             {"link", PropertyType::Object | PropertyType::Nullable, "table"},
140✔
180
             {"link2", PropertyType::Object | PropertyType::Array, "table2"},
140✔
181
         },
140✔
182
         {
140✔
183
             {"parent", PropertyType::LinkingObjects | PropertyType::Array, "table2", "link2"},
140✔
184
         }},
140✔
185
        {"all types",
140✔
186
         {
140✔
187
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
140✔
188
             {"bool", PropertyType::Bool},
140✔
189
             {"int", PropertyType::Int},
140✔
190
             {"float", PropertyType::Float},
140✔
191
             {"double", PropertyType::Double},
140✔
192
             {"string", PropertyType::String},
140✔
193
             {"data", PropertyType::Data},
140✔
194
             {"date", PropertyType::Date},
140✔
195
             {"object id", PropertyType::ObjectId},
140✔
196
             {"decimal", PropertyType::Decimal},
140✔
197
             {"uuid", PropertyType::UUID},
140✔
198
             {"mixed", PropertyType::Mixed | PropertyType::Nullable, Property::IsPrimary{false},
140✔
199
              Property::IsIndexed{true}},
140✔
200
             {"object", PropertyType::Object | PropertyType::Nullable, "link target"},
140✔
201

70✔
202
             {"bool array", PropertyType::Array | PropertyType::Bool},
140✔
203
             {"int array", PropertyType::Array | PropertyType::Int},
140✔
204
             {"float array", PropertyType::Array | PropertyType::Float},
140✔
205
             {"double array", PropertyType::Array | PropertyType::Double},
140✔
206
             {"string array", PropertyType::Array | PropertyType::String},
140✔
207
             {"data array", PropertyType::Array | PropertyType::Data},
140✔
208
             {"date array", PropertyType::Array | PropertyType::Date},
140✔
209
             {"object array", PropertyType::Array | PropertyType::Object, "array target"},
140✔
210
             {"object id array", PropertyType::Array | PropertyType::ObjectId},
140✔
211
             {"uuid array", PropertyType::Array | PropertyType::UUID},
140✔
212
             {"decimal array", PropertyType::Array | PropertyType::Decimal},
140✔
213
             {"mixed array", PropertyType::Array | PropertyType::Mixed | PropertyType::Nullable},
140✔
214

70✔
215
             {"dictionary", PropertyType::Dictionary | PropertyType::String},
140✔
216
         }},
140✔
217
        {"all optional types",
140✔
218
         {
140✔
219
             {"_id", PropertyType::Int | PropertyType::Nullable, Property::IsPrimary{true}},
140✔
220
             {"bool", PropertyType::Bool | PropertyType::Nullable},
140✔
221
             {"int", PropertyType::Int | PropertyType::Nullable},
140✔
222
             {"float", PropertyType::Float | PropertyType::Nullable},
140✔
223
             {"double", PropertyType::Double | PropertyType::Nullable},
140✔
224
             {"string", PropertyType::String | PropertyType::Nullable},
140✔
225
             {"data", PropertyType::Data | PropertyType::Nullable},
140✔
226
             {"date", PropertyType::Date | PropertyType::Nullable},
140✔
227
             {"object id", PropertyType::ObjectId | PropertyType::Nullable},
140✔
228
             {"decimal", PropertyType::Decimal | PropertyType::Nullable},
140✔
229
             {"uuid", PropertyType::UUID | PropertyType::Nullable},
140✔
230
             {"mixed", PropertyType::Mixed | PropertyType::Nullable, Property::IsPrimary{false},
140✔
231
              Property::IsIndexed{true}},
140✔
232

70✔
233
             {"bool array", PropertyType::Array | PropertyType::Bool | PropertyType::Nullable},
140✔
234
             {"int array", PropertyType::Array | PropertyType::Int | PropertyType::Nullable},
140✔
235
             {"float array", PropertyType::Array | PropertyType::Float | PropertyType::Nullable},
140✔
236
             {"double array", PropertyType::Array | PropertyType::Double | PropertyType::Nullable},
140✔
237
             {"string array", PropertyType::Array | PropertyType::String | PropertyType::Nullable},
140✔
238
             {"data array", PropertyType::Array | PropertyType::Data | PropertyType::Nullable},
140✔
239
             {"date array", PropertyType::Array | PropertyType::Date | PropertyType::Nullable},
140✔
240
             {"object id array", PropertyType::Array | PropertyType::ObjectId | PropertyType::Nullable},
140✔
241
             {"decimal array", PropertyType::Array | PropertyType::Decimal | PropertyType::Nullable},
140✔
242
             {"uuid array", PropertyType::Array | PropertyType::UUID | PropertyType::Nullable},
140✔
243
         }},
140✔
244
        {"link target",
140✔
245
         {
140✔
246
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
140✔
247
             {"value", PropertyType::Int},
140✔
248
         },
140✔
249
         {
140✔
250
             {"origin", PropertyType::LinkingObjects | PropertyType::Array, "all types", "object"},
140✔
251
         }},
140✔
252
        {"array target",
140✔
253
         {
140✔
254
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
140✔
255
             {"value", PropertyType::Int},
140✔
256
         }},
140✔
257
        {"pk after list",
140✔
258
         {
140✔
259
             {"array 1", PropertyType::Array | PropertyType::Object, "array target"},
140✔
260
             {"int 1", PropertyType::Int},
140✔
261
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
140✔
262
             {"int 2", PropertyType::Int},
140✔
263
             {"array 2", PropertyType::Array | PropertyType::Object, "array target"},
140✔
264
         }},
140✔
265
        {"nullable int pk",
140✔
266
         {
140✔
267
             {"_id", PropertyType::Int | PropertyType::Nullable, Property::IsPrimary{true}},
140✔
268
         }},
140✔
269
        {"nullable string pk",
140✔
270
         {
140✔
271
             {"_id", PropertyType::String | PropertyType::Nullable, Property::IsPrimary{true}},
140✔
272
         }},
140✔
273
        {"nullable object id pk",
140✔
274
         {
140✔
275
             {"_id", PropertyType::ObjectId | PropertyType::Nullable, Property::IsPrimary{true}},
140✔
276
         }},
140✔
277
        {"nullable uuid pk",
140✔
278
         {
140✔
279
             {"_id", PropertyType::UUID | PropertyType::Nullable, Property::IsPrimary{true}},
140✔
280
         }},
140✔
281
        {"person",
140✔
282
         {
140✔
283
             {"_id", PropertyType::String, Property::IsPrimary{true}},
140✔
284
             {"age", PropertyType::Int},
140✔
285
             {"scores", PropertyType::Array | PropertyType::Int},
140✔
286
             {"assistant", PropertyType::Object | PropertyType::Nullable, "person"},
140✔
287
             {"team", PropertyType::Array | PropertyType::Object, "person"},
140✔
288
         }},
140✔
289
    };
140✔
290
    config.schema_version = 0;
140✔
291
    auto r = Realm::get_shared_realm(config);
140✔
292
    auto& coordinator = *_impl::RealmCoordinator::get_coordinator(config.path);
140✔
293

70✔
294
    TestContext d(r);
140✔
295
    auto create = [&](std::any&& value, CreatePolicy policy = CreatePolicy::ForceCreate) {
85✔
296
        r->begin_transaction();
30✔
297
        auto obj = Object::create(d, r, *r->schema().find("all types"), value, policy);
30✔
298
        r->commit_transaction();
30✔
299
        return obj;
30✔
300
    };
30✔
301
    auto create_sub = [&](std::any&& value, CreatePolicy policy = CreatePolicy::ForceCreate) {
71✔
302
        r->begin_transaction();
2✔
303
        auto obj = Object::create(d, r, *r->schema().find("link target"), value, policy);
2✔
304
        r->commit_transaction();
2✔
305
        return obj;
2✔
306
    };
2✔
307
    auto create_company = [&](std::any&& value, CreatePolicy policy = CreatePolicy::ForceCreate) {
75✔
308
        r->begin_transaction();
10✔
309
        Object obj = Object::create(d, r, *r->schema().find("person"), value, policy);
10✔
310
        r->commit_transaction();
10✔
311
        return obj;
10✔
312
    };
10✔
313

70✔
314
    SECTION("add_notification_callback()") {
140✔
315
        auto table = r->read_group().get_table("class_table");
94✔
316
        auto col_keys = table->get_column_keys();
94✔
317
        std::vector<int64_t> pks = {3, 4, 7, 9, 10, 21, 24, 34, 42, 50};
94✔
318
        r->begin_transaction();
94✔
319
        for (int i = 0; i < 10; ++i)
1,034✔
320
            table->create_object_with_primary_key(pks[i]).set("value 1", i).set("value 2", i);
940✔
321
        r->commit_transaction();
94✔
322

47✔
323
        auto r2 = coordinator.get_realm();
94✔
324

47✔
325
        CollectionChangeSet change;
94✔
326
        auto obj = *table->begin();
94✔
327
        Object object(r, obj);
94✔
328

47✔
329
        auto write = [&](auto&& f) {
102✔
330
            r->begin_transaction();
102✔
331
            f();
102✔
332
            r->commit_transaction();
102✔
333

51✔
334
            advance_and_notify(*r);
102✔
335
        };
102✔
336

47✔
337
        auto require_change = [&](Object& object, std::optional<KeyPathArray> key_path_array = std::nullopt) {
76✔
338
            auto token = object.add_notification_callback(
58✔
339
                [&](CollectionChangeSet c) {
118✔
340
                    change = c;
118✔
341
                },
118✔
342
                key_path_array);
58✔
343
            advance_and_notify(*r);
58✔
344
            return token;
58✔
345
        };
58✔
346

47✔
347
        auto require_no_change = [&](Object& object, std::optional<KeyPathArray> key_path_array = std::nullopt) {
71✔
348
            bool first = true;
48✔
349
            auto token = object.add_notification_callback(
48✔
350
                [&](CollectionChangeSet) {
48✔
351
                    REQUIRE(first);
48!
352
                    first = false;
48✔
353
                },
48✔
354
                key_path_array);
48✔
355
            advance_and_notify(*r);
48✔
356
            return token;
48✔
357
        };
48✔
358

47✔
359
        SECTION("deleting the object sends a change notification") {
94✔
360
            auto token = require_change(object);
2✔
361
            write([&] {
2✔
362
                obj.remove();
2✔
363
            });
2✔
364
            REQUIRE_INDICES(change.deletions, 0);
2!
365
        }
2✔
366

47✔
367
        SECTION("unregistering prior to deleting the object sends no notification") {
94✔
368
            auto token = require_no_change(object);
2✔
369
            token.unregister();
2✔
370
            write([&] {
2✔
371
                obj.remove();
2✔
372
            });
2✔
373
        }
2✔
374

47✔
375
        SECTION("deleting object before first run of notifier") {
94✔
376
            auto token = object.add_notification_callback(
2✔
377
                [&](CollectionChangeSet c) {
2✔
378
                    change = std::move(c);
2✔
379
                },
2✔
380
                {});
2✔
381
            // Delete via a different Realm as begin_transaction() will wait
1✔
382
            // for the notifier to run
1✔
383
            r2->begin_transaction();
2✔
384
            r2->read_group().get_table("class_table")->begin()->remove();
2✔
385
            r2->commit_transaction();
2✔
386
            advance_and_notify(*r);
2✔
387
            REQUIRE_INDICES(change.deletions, 0);
2!
388
            write([] {});
2✔
389
        }
2✔
390

47✔
391
        SECTION("clearing the table sends a change notification") {
94✔
392
            auto token = require_change(object);
2✔
393
            write([&] {
2✔
394
                table->clear();
2✔
395
            });
2✔
396
            REQUIRE_INDICES(change.deletions, 0);
2!
397
        }
2✔
398

47✔
399
        SECTION("clearing the table sends a change notification to the last object") {
94✔
400
            obj = table->get_object(table->size() - 1);
2✔
401
            object = Object(r, obj);
2✔
402

1✔
403
            auto token = require_change(object);
2✔
404
            write([&] {
2✔
405
                table->clear();
2✔
406
            });
2✔
407
            REQUIRE_INDICES(change.deletions, 0);
2!
408
        }
2✔
409

47✔
410
        SECTION("modifying the object sends a change notification") {
94✔
411
            auto token = require_change(object);
2✔
412

1✔
413
            write([&] {
2✔
414
                obj.set(col_keys[0], 10);
2✔
415
            });
2✔
416
            REQUIRE_INDICES(change.modifications, 0);
2!
417
            REQUIRE(change.columns.size() == 1);
2!
418
            REQUIRE_INDICES(change.columns[col_keys[0].value], 0);
2!
419

1✔
420
            write([&] {
2✔
421
                obj.set(col_keys[1], 10);
2✔
422
            });
2✔
423
            REQUIRE_INDICES(change.modifications, 0);
2!
424
            REQUIRE(change.columns.size() == 1);
2!
425
            REQUIRE_INDICES(change.columns[col_keys[1].value], 0);
2!
426
        }
2✔
427

47✔
428
        SECTION("modifying a different object") {
94✔
429
            auto token = require_no_change(object);
2✔
430
            write([&] {
2✔
431
                table->get_object(1).set(col_keys[0], 10);
2✔
432
            });
2✔
433
        }
2✔
434

47✔
435
        SECTION("multiple write transactions") {
94✔
436
            auto token = require_change(object);
2✔
437

1✔
438
            auto r2row = r2->read_group().get_table("class_table")->get_object(0);
2✔
439
            r2->begin_transaction();
2✔
440
            r2row.set(col_keys[0], 1);
2✔
441
            r2->commit_transaction();
2✔
442
            r2->begin_transaction();
2✔
443
            r2row.set(col_keys[1], 2);
2✔
444
            r2->commit_transaction();
2✔
445

1✔
446
            advance_and_notify(*r);
2✔
447
            REQUIRE(change.columns.size() == 2);
2!
448
            REQUIRE_INDICES(change.columns[col_keys[0].value], 0);
2!
449
            REQUIRE_INDICES(change.columns[col_keys[1].value], 0);
2!
450
        }
2✔
451

47✔
452
        SECTION("skipping a notification") {
94✔
453
            auto token = require_no_change(object);
2✔
454
            write([&] {
2✔
455
                obj.set(col_keys[0], 1);
2✔
456
                token.suppress_next();
2✔
457
            });
2✔
458
        }
2✔
459

47✔
460
        SECTION("skipping only effects the current transaction even if no notification would occur anyway") {
94✔
461
            auto token = require_change(object);
2✔
462

1✔
463
            // would not produce a notification even if it wasn't skipped because no changes were made
1✔
464
            write([&] {
2✔
465
                token.suppress_next();
2✔
466
            });
2✔
467
            REQUIRE(change.empty());
2!
468

1✔
469
            // should now produce a notification
1✔
470
            write([&] {
2✔
471
                obj.set(col_keys[0], 1);
2✔
472
            });
2✔
473
            REQUIRE_INDICES(change.modifications, 0);
2!
474
        }
2✔
475

47✔
476
        SECTION("add notification callback, remove it, then add another notification callback") {
94✔
477
            {
2✔
478
                auto token = object.add_notification_callback([&](CollectionChangeSet) {
1✔
479
                    FAIL("This should never happen");
×
480
                });
×
481
            }
2✔
482
            auto token = require_change(object);
2✔
483
            write([&] {
2✔
484
                obj.remove();
2✔
485
            });
2✔
486
            REQUIRE_INDICES(change.deletions, 0);
2!
487
        }
2✔
488

47✔
489
        SECTION("observing deleted object throws") {
94✔
490
            write([&] {
2✔
491
                obj.remove();
2✔
492
            });
2✔
493
            REQUIRE_EXCEPTION(require_change(object), InvalidatedObject,
2✔
494
                              "Accessing object of type table which has been invalidated or deleted");
2✔
495
        }
2✔
496

47✔
497
        SECTION("keypath filtered notifications") {
94✔
498
            auto table_origin = r->read_group().get_table("class_table2");
70✔
499
            auto col_origin_value = table_origin->get_column_key("value");
70✔
500
            auto col_origin_link = table_origin->get_column_key("link");
70✔
501
            auto col_origin_link2 = table_origin->get_column_key("link2");
70✔
502

35✔
503
            auto table_target = r->read_group().get_table("class_table");
70✔
504
            auto col_target_value1 = table_target->get_column_key("value 1");
70✔
505
            auto col_target_value2 = table_target->get_column_key("value 2");
70✔
506
            auto col_target_backlink = table_origin->get_opposite_column(col_origin_link);
70✔
507

35✔
508
            r->begin_transaction();
70✔
509

35✔
510
            Obj obj_target = table_target->create_object_with_primary_key(200);
70✔
511
            Object object_target(r, obj_target);
70✔
512
            object_target.set_column_value("value 1", 201);
70✔
513
            object_target.set_column_value("value 2", 202);
70✔
514

35✔
515
            Obj obj_origin = table_origin->create_object_with_primary_key(100);
70✔
516
            Object object_origin(r, obj_origin);
70✔
517
            object_origin.set_column_value("value", 101);
70✔
518
            object_origin.set_property_value(d, "link", std::any(object_target));
70✔
519

35✔
520
            r->commit_transaction();
70✔
521

35✔
522
            KeyPathArray kpa_origin_value = r->create_key_path_array("table2", {"value"});
70✔
523
            KeyPathArray kpa_origin_link = r->create_key_path_array("table2", {"link"});
70✔
524
            KeyPathArray kpa_target_value1 = r->create_key_path_array("table", {"value 1"});
70✔
525
            KeyPathArray kpa_target_value2 = r->create_key_path_array("table", {"value 2"});
70✔
526

35✔
527
            KeyPathArray kpa_origin_to_target_value1 = r->create_key_path_array("table2", {"link.value 1"});
70✔
528
            KeyPathArray kpa_origin_to_target_value2 = r->create_key_path_array("table2", {"link.value 2"});
70✔
529
            KeyPathArray kpa_target_backlink = r->create_key_path_array("table", {"origin"});
70✔
530
            KeyPathArray kpa_target_to_origin_value = r->create_key_path_array("table", {"origin.value"});
70✔
531
            KeyPathArray kpa_target_to_origin_link = r->create_key_path_array("table", {"origin.link"});
70✔
532

35✔
533
            SECTION("callbacks on a single object") {
70✔
534
                SECTION("modifying origin table 'table2', property 'value' "
18✔
535
                        "while observing origin table 'table2', property 'value' "
18✔
536
                        "-> DOES send a notification") {
10✔
537
                    auto token = require_change(object_origin, kpa_origin_value);
2✔
538

1✔
539
                    write([&] {
2✔
540
                        object_origin.set_column_value("value", 105);
2✔
541
                    });
2✔
542
                    REQUIRE_INDICES(change.modifications, 0);
2!
543
                    REQUIRE(change.columns.size() == 1);
2!
544
                    REQUIRE_INDICES(change.columns[col_origin_value.value], 0);
2!
545
                }
2✔
546

9✔
547
                SECTION("modifying related table 'table', property 'value 1' "
18✔
548
                        "while observing related table 'table', property 'value 1' "
18✔
549
                        "-> does NOT send a notification") {
10✔
550
                    auto token = require_no_change(object_origin, kpa_origin_value);
2✔
551

1✔
552
                    write([&] {
2✔
553
                        object_target.set_column_value("value 1", 205);
2✔
554
                    });
2✔
555
                }
2✔
556

9✔
557
                SECTION("modifying related table 'table', property 'value 2' "
18✔
558
                        "while observing related table 'table', property 'value 2' "
18✔
559
                        "-> does NOT send a notification") {
10✔
560
                    auto token = require_no_change(object_origin, kpa_origin_value);
2✔
561

1✔
562
                    write([&] {
2✔
563
                        object_target.set_column_value("value 2", 205);
2✔
564
                    });
2✔
565
                }
2✔
566

9✔
567
                SECTION("modifying origin table 'table2', property 'value' "
18✔
568
                        "while observing related table 'table', property 'value 1' "
18✔
569
                        "-> does NOT send a notification") {
10✔
570
                    auto token = require_no_change(object_target, kpa_target_value1);
2✔
571

1✔
572
                    write([&] {
2✔
573
                        object_origin.set_column_value("value", 105);
2✔
574
                    });
2✔
575
                }
2✔
576

9✔
577
                SECTION("modifying related table 'table', property 'value 1' "
18✔
578
                        "while observing related table 'table', property 'value 1' "
18✔
579
                        "-> DOES send a notification") {
10✔
580
                    auto token = require_change(object_target, kpa_target_value1);
2✔
581

1✔
582
                    write([&] {
2✔
583
                        object_target.set_column_value("value 1", 205);
2✔
584
                    });
2✔
585
                    REQUIRE_INDICES(change.modifications, 0);
2!
586
                    REQUIRE(change.columns.size() == 1);
2!
587
                    REQUIRE_INDICES(change.columns[col_target_value1.value], 0);
2!
588
                }
2✔
589

9✔
590
                SECTION("modifying related table 'table', property 'value 2' "
18✔
591
                        "while observing related table 'table', property 'value 1' "
18✔
592
                        "-> does NOT send a notification") {
10✔
593
                    auto token = require_no_change(object_target, kpa_target_value1);
2✔
594

1✔
595
                    write([&] {
2✔
596
                        object_target.set_column_value("value 2", 205);
2✔
597
                    });
2✔
598
                }
2✔
599

9✔
600
                SECTION("modifying origin table 'table2', property 'value' "
18✔
601
                        "while observing related table 'table', property 'value 2' "
18✔
602
                        "-> does NOT send a notification") {
10✔
603
                    auto token = require_no_change(object_target, kpa_target_value2);
2✔
604

1✔
605
                    write([&] {
2✔
606
                        object_origin.set_column_value("value", 105);
2✔
607
                    });
2✔
608
                }
2✔
609

9✔
610
                SECTION("modifying related table 'table', property 'value 1' "
18✔
611
                        "while observing related table 'table', property 'value 2' "
18✔
612
                        "-> does NOT send a notification") {
10✔
613
                    auto token = require_no_change(object_target, kpa_target_value2);
2✔
614

1✔
615
                    write([&] {
2✔
616
                        object_target.set_column_value("value 1", 205);
2✔
617
                    });
2✔
618
                }
2✔
619

9✔
620
                SECTION("modifying related table 'table', property 'value 2' "
18✔
621
                        "while observing related table 'table', property 'value 2' "
18✔
622
                        "-> DOES send a notification") {
10✔
623
                    auto token = require_change(object_target, kpa_target_value2);
2✔
624

1✔
625
                    write([&] {
2✔
626
                        object_target.set_column_value("value 2", 205);
2✔
627
                    });
2✔
628
                    REQUIRE_INDICES(change.modifications, 0);
2!
629
                    REQUIRE(change.columns.size() == 1);
2!
630
                    REQUIRE_INDICES(change.columns[col_target_value2.value], 0);
2!
631
                }
2✔
632
            }
18✔
633

35✔
634
            SECTION("callbacks on linked objects") {
70✔
635
                SECTION("all callbacks filtered") {
12✔
636
                    SECTION("modifying origin table 'table2', property 'value' "
6✔
637
                            "while observing related table 'table', property 'value 1' "
6✔
638
                            "-> does NOT send a notification") {
4✔
639
                        auto token = require_no_change(object_origin, kpa_origin_to_target_value1);
2✔
640

1✔
641
                        write([&] {
2✔
642
                            object_origin.set_column_value("value", 105);
2✔
643
                        });
2✔
644
                    }
2✔
645

3✔
646
                    SECTION("modifying related table 'table', property 'value 1' "
6✔
647
                            "while observing related table 'table', property 'value 1' "
6✔
648
                            "-> DOES send a notification") {
4✔
649
                        auto token = require_change(object_origin, kpa_origin_to_target_value1);
2✔
650

1✔
651
                        write([&] {
2✔
652
                            object_target.set_column_value("value 1", 205);
2✔
653
                        });
2✔
654
                        REQUIRE_INDICES(change.modifications, 0);
2!
655
                        REQUIRE(change.columns.size() == 1);
2!
656
                        REQUIRE_INDICES(change.columns[col_origin_link.value], 0);
2!
657
                    }
2✔
658

3✔
659
                    SECTION("modifying related table 'table', property 'value 2' "
6✔
660
                            "while observing related table 'table', property 'value 1' "
6✔
661
                            "-> does NOT send a notification") {
4✔
662
                        auto token = require_no_change(object_origin, kpa_origin_to_target_value1);
2✔
663

1✔
664
                        write([&] {
2✔
665
                            object_target.set_column_value("value 2", 205);
2✔
666
                        });
2✔
667
                    }
2✔
668
                }
6✔
669

6✔
670
                SECTION("some callbacks filtered") {
12✔
671
                    SECTION("modifying origin table 'table2', property 'value' "
6✔
672
                            "while observing related table 'table', property 'value 1' "
6✔
673
                            "-> DOES send a notification") {
4✔
674
                        auto token_with_filter = require_change(object_origin, kpa_origin_to_target_value1);
2✔
675
                        auto token_without_filter = require_change(object_origin);
2✔
676

1✔
677
                        write([&] {
2✔
678
                            object_origin.set_column_value("value", 105);
2✔
679
                        });
2✔
680
                        REQUIRE_INDICES(change.modifications, 0);
2!
681
                        REQUIRE(change.columns.size() == 1);
2!
682
                        REQUIRE_INDICES(change.columns[col_origin_value.value], 0);
2!
683
                    }
2✔
684

3✔
685
                    SECTION("modifying related table 'table', property 'value 1' "
6✔
686
                            "while observing related table 'table', property 'value 1' "
6✔
687
                            "-> DOES send a notification") {
4✔
688
                        auto token_with_filter = require_change(object_origin, kpa_origin_to_target_value1);
2✔
689
                        auto token_without_filter = require_change(object_origin);
2✔
690

1✔
691
                        write([&] {
2✔
692
                            object_target.set_column_value("value 1", 205);
2✔
693
                        });
2✔
694
                        REQUIRE_INDICES(change.modifications, 0);
2!
695
                        REQUIRE(change.columns.size() == 1);
2!
696
                        REQUIRE_INDICES(change.columns[col_origin_link.value], 0);
2!
697
                    }
2✔
698

3✔
699
                    SECTION("modifying related table 'table', property 'value 2' "
6✔
700
                            "while observing related table 'table', property 'value 1' "
6✔
701
                            "-> does NOT send a notification") {
4✔
702
                        auto token_with_filter = require_no_change(object_origin, kpa_origin_to_target_value1);
2✔
703
                        auto token_without_filter = require_no_change(object_origin);
2✔
704

1✔
705
                        write([&] {
2✔
706
                            object_target.set_column_value("value 2", 205);
2✔
707
                        });
2✔
708
                    }
2✔
709
                }
6✔
710
            }
12✔
711

35✔
712
            SECTION("callback with empty keypatharray") {
70✔
713
                SECTION("modifying origin table 'table2', property 'value' "
6✔
714
                        "while observing related table 'table', property 'value 1' "
6✔
715
                        "-> does NOT send a notification") {
4✔
716
                    auto token = require_no_change(object_origin, KeyPathArray());
2✔
717

1✔
718
                    write([&] {
2✔
719
                        object_origin.set_column_value("value", 105);
2✔
720
                    });
2✔
721
                }
2✔
722

3✔
723
                SECTION("modifying related table 'table', property 'value 1' "
6✔
724
                        "while observing related table 'table', property 'value 1' "
6✔
725
                        "-> does NOT send a notification") {
4✔
726
                    auto token = require_no_change(object_origin, KeyPathArray());
2✔
727

1✔
728
                    write([&] {
2✔
729
                        object_target.set_column_value("value 1", 205);
2✔
730
                    });
2✔
731
                }
2✔
732

3✔
733
                SECTION("modifying related table 'table', property 'value 2' "
6✔
734
                        "while observing related table 'table', property 'value 1' "
6✔
735
                        "-> does NOT send a notification") {
4✔
736
                    auto token = require_no_change(object_origin, KeyPathArray());
2✔
737

1✔
738
                    write([&] {
2✔
739
                        object_target.set_column_value("value 2", 205);
2✔
740
                    });
2✔
741
                }
2✔
742
            }
6✔
743

35✔
744
            SECTION("callback with empty keypatharray, backlinks") {
70✔
745
                SECTION("modifying backlinked table 'table2', property 'value' "
8✔
746
                        "with empty KeyPathArray "
8✔
747
                        "-> DOES not send a notification") {
5✔
748
                    auto token_with_shallow_subscribtion = require_no_change(object_target, KeyPathArray());
2✔
749
                    write([&] {
2✔
750
                        object_origin.set_column_value("value", 105);
2✔
751
                    });
2✔
752
                }
2✔
753
                SECTION("modifying backlinked table 'table2', property 'link' "
8✔
754
                        "with empty KeyPathArray "
8✔
755
                        "-> does NOT send a notification") {
5✔
756
                    auto token_with_empty_key_path_array = require_no_change(object_target, KeyPathArray());
2✔
757
                    write([&] {
2✔
758
                        Obj obj_target2 = table_target->create_object_with_primary_key(300);
2✔
759
                        Object object_target2(r, obj_target2);
2✔
760
                        object_origin.set_property_value(d, "link", std::any(object_target2));
2✔
761
                    });
2✔
762
                }
2✔
763
                SECTION("adding a new origin pointing to the target "
8✔
764
                        "with empty KeyPathArray "
8✔
765
                        "-> does NOT send a notification") {
5✔
766
                    auto token_with_empty_key_path_array = require_no_change(object_target, KeyPathArray());
2✔
767
                    write([&] {
2✔
768
                        Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
769
                        Object object_origin2(r, obj_origin2);
2✔
770
                        object_origin2.set_property_value(d, "link", std::any(object_target));
2✔
771
                    });
2✔
772
                }
2✔
773
                SECTION("adding a new origin pointing to the target "
8✔
774
                        "with empty KeyPathArray "
8✔
775
                        "-> does NOT send a notification") {
5✔
776
                    auto token_with_empty_key_path_array = require_no_change(object_target, KeyPathArray());
2✔
777
                    write([&] {
2✔
778
                        Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
779
                        Object object_origin2(r, obj_origin2);
2✔
780
                        object_origin2.set_property_value(d, "link", std::any(object_target));
2✔
781
                    });
2✔
782
                }
2✔
783
            }
8✔
784

35✔
785
            SECTION("callbacks on objects with link depth > 4") {
70✔
786
                r->begin_transaction();
4✔
787

2✔
788
                Obj obj_depth6 = table_origin->create_object_with_primary_key(600);
4✔
789
                Object object_depth6(r, obj_depth6);
4✔
790
                object_depth6.set_column_value("value", 601);
4✔
791

2✔
792
                Obj obj_depth5 = table_origin->create_object_with_primary_key(500);
4✔
793
                Object object_depth5(r, obj_depth5);
4✔
794
                object_depth5.set_column_value("value", 501);
4✔
795
                object_depth5.set_property_value(d, "link2", std::any(AnyVec{std::any(object_depth6)}));
4✔
796

2✔
797
                Obj obj_depth4 = table_origin->create_object_with_primary_key(400);
4✔
798
                Object object_depth4(r, obj_depth4);
4✔
799
                object_depth4.set_column_value("value", 401);
4✔
800
                object_depth4.set_property_value(d, "link2", std::any(AnyVec{std::any(object_depth5)}));
4✔
801

2✔
802
                Obj obj_depth3 = table_origin->create_object_with_primary_key(300);
4✔
803
                Object object_depth3(r, obj_depth3);
4✔
804
                object_depth3.set_column_value("value", 301);
4✔
805
                object_depth3.set_property_value(d, "link2", std::any(AnyVec{std::any(object_depth4)}));
4✔
806

2✔
807
                Obj obj_depth2 = table_origin->create_object_with_primary_key(200);
4✔
808
                Object object_depth2(r, obj_depth2);
4✔
809
                object_depth2.set_column_value("value", 201);
4✔
810
                object_depth2.set_property_value(d, "link2", std::any(AnyVec{std::any(object_depth3)}));
4✔
811

2✔
812
                Obj obj_depth1 = table_origin->create_object_with_primary_key(100);
4✔
813
                Object object_depth1(r, obj_depth1);
4✔
814
                object_depth1.set_column_value("value", 101);
4✔
815
                object_depth1.set_property_value(d, "link2", std::any(AnyVec{std::any(object_depth2)}));
4✔
816

2✔
817
                r->commit_transaction();
4✔
818

2✔
819
                KeyPathArray kpa_to_depth_5 = r->create_key_path_array("table2", {"link2.link2.link2.link2.value"});
4✔
820
                KeyPathArray kpa_to_depth_6 =
4✔
821
                    r->create_key_path_array("table2", {"link2.link2.link2.link2.link2.value"});
4✔
822

2✔
823
                SECTION("modifying table 'table2', property 'link2' 5 levels deep "
4✔
824
                        "while observing table 'table2', property 'link2' 5 levels deep "
4✔
825
                        "-> DOES send a notification") {
3✔
826
                    auto token = require_change(object_depth1, kpa_to_depth_5);
2✔
827

1✔
828
                    write([&] {
2✔
829
                        object_depth5.set_column_value("value", 555);
2✔
830
                    });
2✔
831
                    REQUIRE_INDICES(change.modifications, 0);
2!
832
                    REQUIRE(change.columns.size() == 1);
2!
833
                    REQUIRE_INDICES(change.columns[col_origin_link2.value], 0);
2!
834
                }
2✔
835

2✔
836
                SECTION("modifying table 'table2', property 'link2' 6 depths deep "
4✔
837
                        "while observing table 'table2', property 'link2' 5 depths deep "
4✔
838
                        "-> does NOT send a notification") {
3✔
839
                    auto token = require_no_change(object_depth1, kpa_to_depth_5);
2✔
840

1✔
841
                    write([&] {
2✔
842
                        object_depth6.set_column_value("value", 555);
2✔
843
                    });
2✔
844
                }
2✔
845
            }
4✔
846

35✔
847
            SECTION("keypath filter with a backlink") {
70✔
848
                SECTION("all callbacks filtered") {
20✔
849
                    SECTION("modifying backlinked table 'table2', property 'value' "
4✔
850
                            "while observing backlinked table 'table2', property 'value' on origin "
4✔
851
                            "-> DOES send a notification") {
3✔
852
                        auto token_with_backlink = require_change(object_target, kpa_target_to_origin_value);
2✔
853
                        write([&] {
2✔
854
                            object_origin.set_column_value("value", 105);
2✔
855
                        });
2✔
856
                        REQUIRE_INDICES(change.modifications, 0);
2!
857
                        REQUIRE(change.columns.size() == 1);
2!
858
                        REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
859
                    }
2✔
860
                    SECTION("modifying backlinked table 'table2', property 'link' "
4✔
861
                            "while observing backlinked table 'table2', property 'value' on origin "
4✔
862
                            "-> does NOT send a notification") {
3✔
863
                        auto token_with_backlink = require_no_change(object_target, kpa_target_to_origin_value);
2✔
864
                        write([&] {
2✔
865
                            Obj obj_target2 = table_target->create_object_with_primary_key(300);
2✔
866
                            Object object_target2(r, obj_target2);
2✔
867
                            object_origin.set_property_value(d, "link", std::any(object_target2));
2✔
868
                        });
2✔
869
                    }
2✔
870
                }
4✔
871

10✔
872
                SECTION("adding a new origin pointing to the target "
20✔
873
                        "while observing target table 'table2's backlink "
20✔
874
                        "-> DOES send a notification") {
11✔
875
                    auto token_with_backlink = require_change(object_target, kpa_target_backlink);
2✔
876
                    write([&] {
2✔
877
                        Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
878
                        Object object_origin2(r, obj_origin2);
2✔
879
                        object_origin2.set_property_value(d, "link", std::any(object_target));
2✔
880
                    });
2✔
881
                    REQUIRE_INDICES(change.modifications, 0);
2!
882
                    REQUIRE(change.columns.size() == 1);
2!
883
                    REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
884
                }
2✔
885

10✔
886
                SECTION("adding a new origin pointing to the target "
20✔
887
                        "while observing target table 'table2', property 'link' on origin "
20✔
888
                        "-> DOES send a notification") {
11✔
889
                    auto token_with_backlink = require_change(object_target, kpa_target_to_origin_link);
2✔
890
                    write([&] {
2✔
891
                        Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
892
                        Object object_origin2(r, obj_origin2);
2✔
893
                        object_origin2.set_property_value(d, "link", std::any(object_target));
2✔
894
                    });
2✔
895
                    REQUIRE_INDICES(change.modifications, 0);
2!
896
                    REQUIRE(change.columns.size() == 1);
2!
897
                    REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
898
                }
2✔
899

10✔
900
                SECTION("adding a new origin pointing to the target "
20✔
901
                        "while observing target table 'table2', property 'value' on origin "
20✔
902
                        "-> DOES send a notification") {
11✔
903
                    auto token_with_backlink = require_change(object_target, kpa_target_to_origin_value);
2✔
904
                    write([&] {
2✔
905
                        Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
906
                        Object object_origin2(r, obj_origin2);
2✔
907
                        object_origin2.set_property_value(d, "link", std::any(object_target));
2✔
908
                    });
2✔
909
                    REQUIRE_INDICES(change.modifications, 0);
2!
910
                    REQUIRE(change.columns.size() == 1);
2!
911
                    REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
912
                }
2✔
913

10✔
914
                SECTION("some callbacks filtered") {
20✔
915
                    SECTION("modifying backlinked table 'table2', property 'value' "
10✔
916
                            "while observing backlinked table 'table2', property 'value' on origin "
10✔
917
                            "-> DOES send a notification") {
6✔
918
                        auto token_with_backlink = require_change(object_target, kpa_target_to_origin_value);
2✔
919
                        auto token_without_filter = require_change(object_target);
2✔
920
                        write([&] {
2✔
921
                            object_origin.set_column_value("value", 105);
2✔
922
                        });
2✔
923
                        REQUIRE_INDICES(change.modifications, 0);
2!
924
                        REQUIRE(change.columns.size() == 1);
2!
925
                        REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
926
                    }
2✔
927
                    SECTION("modifying backlinked table 'table2', property 'link2' "
10✔
928
                            "while observing backlinked table 'table2', property 'value' on origin "
10✔
929
                            "-> does NOT a notification") {
6✔
930
                        auto token_with_backlink = require_no_change(object_target, kpa_target_to_origin_value);
2✔
931
                        auto token_without_filter = require_no_change(object_target);
2✔
932
                        write([&] {
2✔
933
                            Obj obj_target2 = table_target->create_object_with_primary_key(300);
2✔
934
                            Object object_target2(r, obj_target2);
2✔
935
                            object_origin.set_property_value(d, "link", std::any(object_target2));
2✔
936
                        });
2✔
937
                    }
2✔
938
                    SECTION("adding a new origin pointing to the target "
10✔
939
                            "while observing target table 'table2's backlink "
10✔
940
                            "-> DOES send a notification") {
6✔
941
                        auto token_with_backlink = require_change(object_target, kpa_target_backlink);
2✔
942
                        auto token_without_filter = require_change(object_target);
2✔
943
                        write([&] {
2✔
944
                            Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
945
                            Object object_origin2(r, obj_origin2);
2✔
946
                            object_origin2.set_property_value(d, "link", std::any(object_target));
2✔
947
                        });
2✔
948
                        REQUIRE_INDICES(change.modifications, 0);
2!
949
                        REQUIRE(change.columns.size() == 1);
2!
950
                        REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
951
                    }
2✔
952
                    SECTION("adding a new origin pointing to the target "
10✔
953
                            "while observing target table 'table2', property 'value' on origin "
10✔
954
                            "-> DOES send a notification") {
6✔
955
                        auto token_with_backlink = require_change(object_target, kpa_target_to_origin_value);
2✔
956
                        auto token_without_filter = require_change(object_target);
2✔
957
                        write([&] {
2✔
958
                            Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
959
                            Object object_origin2(r, obj_origin2);
2✔
960
                            object_origin2.set_property_value(d, "link", std::any(object_target));
2✔
961
                        });
2✔
962
                        REQUIRE_INDICES(change.modifications, 0);
2!
963
                        REQUIRE(change.columns.size() == 1);
2!
964
                        REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
965
                    }
2✔
966
                    SECTION("changes to backlink are reported both to origin and destination object") {
10✔
967
                        Object object_origin2;
2✔
968
                        write([&] {
2✔
969
                            Obj obj_origin2 = table_origin->create_object_with_primary_key(300);
2✔
970
                            object_origin2 = Object{r, obj_origin2};
2✔
971
                        });
2✔
972

1✔
973
                        // add a backlink
1✔
974
                        auto token_with_backlink = require_change(object_target, kpa_target_backlink);
2✔
975
                        write([&] {
2✔
976
                            object_origin2.set_property_value(d, "link", util::Any(object_target));
2✔
977
                        });
2✔
978
                        REQUIRE_INDICES(change.modifications, 0);
2!
979
                        REQUIRE(change.columns.size() == 1);
2!
980
                        REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
981

1✔
982
                        // nullify a backlink
1✔
983
                        write([&] {
2✔
984
                            object_origin2.set_property_value(d, "link", std::any());
2✔
985
                        });
2✔
986
                        REQUIRE_INDICES(change.modifications, 0);
2!
987
                        REQUIRE(change.columns.size() == 1);
2!
988
                        REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
989

1✔
990
                        // remove a backlink
1✔
991
                        write([&] {
2✔
992
                            table_origin->remove_object(object_origin2.get_obj().get_key());
2✔
993
                        });
2✔
994
                        REQUIRE_INDICES(change.modifications, 0);
2!
995
                        REQUIRE(change.columns.size() == 1);
2!
996
                        REQUIRE_INDICES(change.columns[col_target_backlink.value], 0);
2!
997
                    }
2✔
998
                }
10✔
999
            }
20✔
1000

35✔
1001
            SECTION("deleting the object sends a change notification") {
70✔
1002
                auto token = require_change(object_origin, kpa_origin_value);
2✔
1003

1✔
1004
                write([&] {
2✔
1005
                    obj_origin.remove();
2✔
1006
                });
2✔
1007
                REQUIRE_INDICES(change.deletions, 0);
2!
1008
            }
2✔
1009
        }
70✔
1010
    }
94✔
1011

70✔
1012
    SECTION("create object") {
140✔
1013
        auto obj = create(AnyDict{
2✔
1014
            {"_id", INT64_C(1)},
2✔
1015
            {"bool", true},
2✔
1016
            {"int", INT64_C(5)},
2✔
1017
            {"float", 2.2f},
2✔
1018
            {"double", 3.3},
2✔
1019
            {"string", "hello"s},
2✔
1020
            {"data", "olleh"s},
2✔
1021
            {"date", Timestamp(10, 20)},
2✔
1022
            {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(10)}}},
2✔
1023
            {"object id", ObjectId("000000000000000000000001")},
2✔
1024
            {"decimal", Decimal128("1.23e45")},
2✔
1025
            {"uuid", UUID("3b241101-abba-baba-caca-4136c566a962")},
2✔
1026
            {"mixed", "mixed"s},
2✔
1027

1✔
1028
            {"bool array", AnyVec{true, false}},
2✔
1029
            {"int array", AnyVec{INT64_C(5), INT64_C(6)}},
2✔
1030
            {"float array", AnyVec{1.1f, 2.2f}},
2✔
1031
            {"double array", AnyVec{3.3, 4.4}},
2✔
1032
            {"string array", AnyVec{"a"s, "b"s, "c"s}},
2✔
1033
            {"data array", AnyVec{"d"s, "e"s, "f"s}},
2✔
1034
            {"date array", AnyVec{Timestamp(10, 20), Timestamp(30, 40)}},
2✔
1035
            {"object array", AnyVec{AnyDict{{"_id", INT64_C(20)}, {"value", INT64_C(20)}}}},
2✔
1036
            {"object id array", AnyVec{ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"), ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")}},
2✔
1037
            {"decimal array", AnyVec{Decimal128("1.23e45"), Decimal128("6.78e9")}},
2✔
1038
            {"uuid array", AnyVec{UUID(), UUID("3b241101-e2bb-4255-8caf-4136c566a962")}},
2✔
1039
            {"mixed array",
2✔
1040
             AnyVec{25, "b"s, 1.45, util::none, Timestamp(30, 40), Decimal128("1.23e45"),
2✔
1041
                    ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"), UUID("3b241101-e2bb-4255-8caf-4136c566a962")}},
2✔
1042
            {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1043
        });
2✔
1044

1✔
1045
        Obj row = obj.get_obj();
2✔
1046
        auto link_target = *r->read_group().get_table("class_link target")->begin();
2✔
1047
        TableRef table = row.get_table();
2✔
1048
        auto target_table = link_target.get_table();
2✔
1049
        auto array_target_table = r->read_group().get_table("class_array target");
2✔
1050
        REQUIRE(row.get<Int>(table->get_column_key("_id")) == 1);
2!
1051
        REQUIRE(row.get<Bool>(table->get_column_key("bool")) == true);
2!
1052
        REQUIRE(row.get<Int>(table->get_column_key("int")) == 5);
2!
1053
        REQUIRE(row.get<float>(table->get_column_key("float")) == 2.2f);
2!
1054
        REQUIRE(row.get<double>(table->get_column_key("double")) == 3.3);
2!
1055
        REQUIRE(row.get<String>(table->get_column_key("string")) == "hello");
2!
1056
        REQUIRE(row.get<Binary>(table->get_column_key("data")) == BinaryData("olleh", 5));
2!
1057
        REQUIRE(row.get<Timestamp>(table->get_column_key("date")) == Timestamp(10, 20));
2!
1058
        REQUIRE(row.get<ObjKey>(table->get_column_key("object")) == link_target.get_key());
2!
1059
        REQUIRE(row.get<ObjectId>(table->get_column_key("object id")) == ObjectId("000000000000000000000001"));
2!
1060
        REQUIRE(row.get<Decimal128>(table->get_column_key("decimal")) == Decimal128("1.23e45"));
2!
1061
        REQUIRE(row.get<UUID>(table->get_column_key("uuid")) == UUID("3b241101-abba-baba-caca-4136c566a962"));
2!
1062
        REQUIRE(row.get<Mixed>(table->get_column_key("mixed")) == Mixed("mixed"));
2!
1063

1✔
1064
        REQUIRE(link_target.get<Int>(target_table->get_column_key("value")) == 10);
2!
1065

1✔
1066
        auto check_array = [&](ColKey col, auto... values) {
20✔
1067
            auto vec = get_vector({values...});
20✔
1068
            using U = typename decltype(vec)::value_type;
20✔
1069
            auto list = row.get_list<U>(col);
20✔
1070
            size_t i = 0;
20✔
1071
            for (auto value : vec) {
44✔
1072
                CAPTURE(i);
44✔
1073
                REQUIRE(i < list.size());
44!
1074
                REQUIRE(value == list.get(i));
44!
1075
                ++i;
44✔
1076
            }
44✔
1077
        };
20✔
1078
        check_array(table->get_column_key("bool array"), true, false);
2✔
1079
        check_array(table->get_column_key("int array"), INT64_C(5), INT64_C(6));
2✔
1080
        check_array(table->get_column_key("float array"), 1.1f, 2.2f);
2✔
1081
        check_array(table->get_column_key("double array"), 3.3, 4.4);
2✔
1082
        check_array(table->get_column_key("string array"), StringData("a"), StringData("b"), StringData("c"));
2✔
1083
        check_array(table->get_column_key("data array"), BinaryData("d", 1), BinaryData("e", 1), BinaryData("f", 1));
2✔
1084
        check_array(table->get_column_key("date array"), Timestamp(10, 20), Timestamp(30, 40));
2✔
1085
        check_array(table->get_column_key("object id array"), ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"),
2✔
1086
                    ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB"));
2✔
1087
        check_array(table->get_column_key("decimal array"), Decimal128("1.23e45"), Decimal128("6.78e9"));
2✔
1088
        check_array(table->get_column_key("uuid array"), UUID(), UUID("3b241101-e2bb-4255-8caf-4136c566a962"));
2✔
1089
        {
2✔
1090
            auto list = row.get_list<Mixed>(table->get_column_key("mixed array"));
2✔
1091
            REQUIRE(list.size() == 8);
2!
1092
            REQUIRE(list.get(0).get_int() == 25);
2!
1093
            REQUIRE(list.get(1).get_string() == "b");
2!
1094
            REQUIRE(list.get(2).get_double() == 1.45);
2!
1095
            REQUIRE(list.get(3).is_null());
2!
1096
            REQUIRE(list.get(4).get_timestamp() == Timestamp(30, 40));
2!
1097
            REQUIRE(list.get(5).get_decimal() == Decimal128("1.23e45"));
2!
1098
            REQUIRE(list.get(6).get_object_id() == ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"));
2!
1099
            REQUIRE(list.get(7).get_uuid() == UUID("3b241101-e2bb-4255-8caf-4136c566a962"));
2!
1100
        }
2✔
1101

1✔
1102
        REQUIRE(row.get_dictionary(table->get_column_key("dictionary")).get("key") == Mixed("value"));
2!
1103

1✔
1104
        auto list = row.get_linklist_ptr(table->get_column_key("object array"));
2✔
1105
        REQUIRE(list->size() == 1);
2!
1106
        REQUIRE(list->get_object(0).get<Int>(array_target_table->get_column_key("value")) == 20);
2!
1107
    }
2✔
1108

70✔
1109
    SECTION("create uses defaults for missing values") {
140✔
1110
        d.defaults["all types"] = {
2✔
1111
            {"bool", true},
2✔
1112
            {"int", INT64_C(5)},
2✔
1113
            {"float", 2.2f},
2✔
1114
            {"double", 3.3},
2✔
1115
            {"string", "hello"s},
2✔
1116
            {"data", "olleh"s},
2✔
1117
            {"date", Timestamp(10, 20)},
2✔
1118
            {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(10)}}},
2✔
1119
            {"object id", ObjectId("000000000000000000000001")},
2✔
1120
            {"decimal", Decimal128("1.23e45")},
2✔
1121
            {"uuid", UUID("3b241101-1111-2222-3333-4136c566a962")},
2✔
1122

1✔
1123
            {"bool array", AnyVec{true, false}},
2✔
1124
            {"int array", AnyVec{INT64_C(5), INT64_C(6)}},
2✔
1125
            {"float array", AnyVec{1.1f, 2.2f}},
2✔
1126
            {"double array", AnyVec{3.3, 4.4}},
2✔
1127
            {"string array", AnyVec{"a"s, "b"s, "c"s}},
2✔
1128
            {"data array", AnyVec{"d"s, "e"s, "f"s}},
2✔
1129
            {"date array", AnyVec{}},
2✔
1130
            {"object array", AnyVec{AnyDict{{"_id", INT64_C(20)}, {"value", INT64_C(20)}}}},
2✔
1131
            {"object id array", AnyVec{ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"), ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")}},
2✔
1132
            {"decimal array", AnyVec{Decimal128("1.23e45"), Decimal128("6.78e9")}},
2✔
1133
            {"uuid array", AnyVec{UUID(), UUID("3b241101-e2bb-4255-8caf-4136c566a962")}},
2✔
1134
            {"dictionary", AnyDict{{"name", "John Doe"s}}},
2✔
1135
        };
2✔
1136

1✔
1137
        Object obj = create(AnyDict{
2✔
1138
            {"_id", INT64_C(1)},
2✔
1139
            {"float", 6.6f},
2✔
1140
        });
2✔
1141

1✔
1142
        Obj row = obj.get_obj();
2✔
1143
        TableRef table = row.get_table();
2✔
1144
        REQUIRE(row.get<Int>(table->get_column_key("_id")) == 1);
2!
1145
        REQUIRE(row.get<Bool>(table->get_column_key("bool")) == true);
2!
1146
        REQUIRE(row.get<Int>(table->get_column_key("int")) == 5);
2!
1147
        REQUIRE(row.get<float>(table->get_column_key("float")) == 6.6f);
2!
1148
        REQUIRE(row.get<double>(table->get_column_key("double")) == 3.3);
2!
1149
        REQUIRE(row.get<String>(table->get_column_key("string")) == "hello");
2!
1150
        REQUIRE(row.get<Binary>(table->get_column_key("data")) == BinaryData("olleh", 5));
2!
1151
        REQUIRE(row.get<Timestamp>(table->get_column_key("date")) == Timestamp(10, 20));
2!
1152
        REQUIRE(row.get<ObjectId>(table->get_column_key("object id")) == ObjectId("000000000000000000000001"));
2!
1153
        REQUIRE(row.get<Decimal128>(table->get_column_key("decimal")) == Decimal128("1.23e45"));
2!
1154
        REQUIRE(row.get<UUID>(table->get_column_key("uuid")) == UUID("3b241101-1111-2222-3333-4136c566a962"));
2!
1155
        REQUIRE(row.get_dictionary(table->get_column_key("dictionary")).get("name") == Mixed("John Doe"));
2!
1156

1✔
1157
        REQUIRE(row.get_listbase_ptr(table->get_column_key("bool array"))->size() == 2);
2!
1158
        REQUIRE(row.get_listbase_ptr(table->get_column_key("int array"))->size() == 2);
2!
1159
        REQUIRE(row.get_listbase_ptr(table->get_column_key("float array"))->size() == 2);
2!
1160
        REQUIRE(row.get_listbase_ptr(table->get_column_key("double array"))->size() == 2);
2!
1161
        REQUIRE(row.get_listbase_ptr(table->get_column_key("string array"))->size() == 3);
2!
1162
        REQUIRE(row.get_listbase_ptr(table->get_column_key("data array"))->size() == 3);
2!
1163
        REQUIRE(row.get_listbase_ptr(table->get_column_key("date array"))->size() == 0);
2!
1164
        REQUIRE(row.get_listbase_ptr(table->get_column_key("object array"))->size() == 1);
2!
1165
        REQUIRE(row.get_listbase_ptr(table->get_column_key("object id array"))->size() == 2);
2!
1166
        REQUIRE(row.get_listbase_ptr(table->get_column_key("decimal array"))->size() == 2);
2!
1167
        REQUIRE(row.get_listbase_ptr(table->get_column_key("uuid array"))->size() == 2);
2!
1168
    }
2✔
1169

70✔
1170
    SECTION("create can use defaults for primary key") {
140✔
1171
        d.defaults["all types"] = {
2✔
1172
            {"_id", INT64_C(10)},
2✔
1173
        };
2✔
1174
        auto obj = create(AnyDict{
2✔
1175
            {"bool", true},
2✔
1176
            {"int", INT64_C(5)},
2✔
1177
            {"float", 2.2f},
2✔
1178
            {"double", 3.3},
2✔
1179
            {"string", "hello"s},
2✔
1180
            {"data", "olleh"s},
2✔
1181
            {"date", Timestamp(10, 20)},
2✔
1182
            {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(10)}}},
2✔
1183
            {"array", AnyVector{AnyDict{{"value", INT64_C(20)}}}},
2✔
1184
            {"object id", ObjectId("000000000000000000000001")},
2✔
1185
            {"decimal", Decimal128("1.23e45")},
2✔
1186
            {"uuid", UUID("3b241101-0000-0000-0000-4136c566a962")},
2✔
1187
            {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1188
        });
2✔
1189

1✔
1190
        auto row = obj.get_obj();
2✔
1191
        REQUIRE(row.get<Int>(row.get_table()->get_column_key("_id")) == 10);
2!
1192
    }
2✔
1193

70✔
1194
    SECTION("create does not complain about missing values for nullable fields") {
140✔
1195
        r->begin_transaction();
2✔
1196
        realm::Object obj;
2✔
1197
        REQUIRE_NOTHROW(obj = Object::create(d, r, *r->schema().find("all optional types"), std::any(AnyDict{})));
2✔
1198
        r->commit_transaction();
2✔
1199

1✔
1200
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "_id").has_value());
2!
1201
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "bool").has_value());
2!
1202
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "int").has_value());
2!
1203
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "float").has_value());
2!
1204
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "double").has_value());
2!
1205
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "string").has_value());
2!
1206
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "data").has_value());
2!
1207
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "date").has_value());
2!
1208
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "object id").has_value());
2!
1209
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "uuid").has_value());
2!
1210

1✔
1211
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "bool array")).size() == 0);
2!
1212
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "int array")).size() == 0);
2!
1213
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "float array")).size() == 0);
2!
1214
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "double array")).size() == 0);
2!
1215
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "string array")).size() == 0);
2!
1216
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "data array")).size() == 0);
2!
1217
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "date array")).size() == 0);
2!
1218
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "object id array")).size() == 0);
2!
1219
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "uuid array")).size() == 0);
2!
1220
    }
2✔
1221

70✔
1222
    SECTION("create throws for missing values if there is no default") {
140✔
1223
        REQUIRE_EXCEPTION(create(AnyDict{{"_id", INT64_C(1)}, {"float", 6.6f}}), MissingPropertyValue,
2✔
1224
                          "Missing value for property 'all types.bool'");
2✔
1225
    }
2✔
1226

70✔
1227
    SECTION("create always sets the PK first") {
140✔
1228
        AnyDict value{
2✔
1229
            {"array 1", AnyVector{AnyDict{{"_id", INT64_C(1)}, {"value", INT64_C(1)}}}},
2✔
1230
            {"array 2", AnyVector{AnyDict{{"_id", INT64_C(2)}, {"value", INT64_C(2)}}}},
2✔
1231
            {"int 1", INT64_C(0)},
2✔
1232
            {"int 2", INT64_C(0)},
2✔
1233
            {"_id", INT64_C(7)},
2✔
1234
        };
2✔
1235
        // Core will throw if the list is populated before the PK is set
1✔
1236
        r->begin_transaction();
2✔
1237
        REQUIRE_NOTHROW(Object::create(d, r, *r->schema().find("pk after list"), std::any(value)));
2✔
1238
    }
2✔
1239

70✔
1240
    SECTION("create with update") {
140✔
1241
        CollectionChangeSet change;
2✔
1242
        bool callback_called;
2✔
1243
        Object obj = create(AnyDict{
2✔
1244
            {"_id", INT64_C(1)},
2✔
1245
            {"bool", true},
2✔
1246
            {"int", INT64_C(5)},
2✔
1247
            {"float", 2.2f},
2✔
1248
            {"double", 3.3},
2✔
1249
            {"string", "hello"s},
2✔
1250
            {"data", "olleh"s},
2✔
1251
            {"date", Timestamp(10, 20)},
2✔
1252
            {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(10)}}},
2✔
1253
            {"object id", ObjectId("000000000000000000000001")},
2✔
1254
            {"decimal", Decimal128("1.23e45")},
2✔
1255
            {"uuid", UUID("3b241101-9999-9999-9999-4136c566a962")},
2✔
1256
            {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1257

1✔
1258
            {"bool array", AnyVec{true, false}},
2✔
1259
            {"int array", AnyVec{INT64_C(5), INT64_C(6)}},
2✔
1260
            {"float array", AnyVec{1.1f, 2.2f}},
2✔
1261
            {"double array", AnyVec{3.3, 4.4}},
2✔
1262
            {"string array", AnyVec{"a"s, "b"s, "c"s}},
2✔
1263
            {"data array", AnyVec{"d"s, "e"s, "f"s}},
2✔
1264
            {"date array", AnyVec{}},
2✔
1265
            {"object array", AnyVec{AnyDict{{"_id", INT64_C(20)}, {"value", INT64_C(20)}}}},
2✔
1266
            {"object id array", AnyVec{ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"), ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")}},
2✔
1267
            {"decimal array", AnyVec{Decimal128("1.23e45"), Decimal128("6.78e9")}},
2✔
1268
            {"uuid array", AnyVec{UUID(), UUID("3b241101-1234-5678-9012-4136c566a962")}}});
2✔
1269

1✔
1270
        auto token = obj.add_notification_callback([&](CollectionChangeSet c) {
4✔
1271
            change = c;
4✔
1272
            callback_called = true;
4✔
1273
        });
4✔
1274
        advance_and_notify(*r);
2✔
1275

1✔
1276
        create(
2✔
1277
            AnyDict{
2✔
1278
                {"_id", INT64_C(1)},
2✔
1279
                {"int", INT64_C(6)},
2✔
1280
                {"string", "a"s},
2✔
1281
            },
2✔
1282
            CreatePolicy::UpdateAll);
2✔
1283

1✔
1284
        callback_called = false;
2✔
1285
        advance_and_notify(*r);
2✔
1286
        REQUIRE(callback_called);
2!
1287
        REQUIRE_INDICES(change.modifications, 0);
2!
1288

1✔
1289
        auto row = obj.get_obj();
2✔
1290
        auto table = row.get_table();
2✔
1291
        REQUIRE(row.get<Int>(table->get_column_key("_id")) == 1);
2!
1292
        REQUIRE(row.get<Bool>(table->get_column_key("bool")) == true);
2!
1293
        REQUIRE(row.get<Int>(table->get_column_key("int")) == 6);
2!
1294
        REQUIRE(row.get<float>(table->get_column_key("float")) == 2.2f);
2!
1295
        REQUIRE(row.get<double>(table->get_column_key("double")) == 3.3);
2!
1296
        REQUIRE(row.get<String>(table->get_column_key("string")) == "a");
2!
1297
        REQUIRE(row.get<Binary>(table->get_column_key("data")) == BinaryData("olleh", 5));
2!
1298
        REQUIRE(row.get<Timestamp>(table->get_column_key("date")) == Timestamp(10, 20));
2!
1299
        REQUIRE(row.get<ObjectId>(table->get_column_key("object id")) == ObjectId("000000000000000000000001"));
2!
1300
        REQUIRE(row.get<Decimal128>(table->get_column_key("decimal")) == Decimal128("1.23e45"));
2!
1301
        REQUIRE(row.get<UUID>(table->get_column_key("uuid")) == UUID("3b241101-9999-9999-9999-4136c566a962"));
2!
1302
    }
2✔
1303

70✔
1304
    SECTION("create with update - only with diffs") {
140✔
1305
        CollectionChangeSet change;
2✔
1306
        bool callback_called;
2✔
1307
        AnyDict adam{
2✔
1308
            {"_id", "pk0"s},
2✔
1309
            {"name", "Adam"s},
2✔
1310
            {"age", INT64_C(32)},
2✔
1311
            {"scores", AnyVec{INT64_C(1), INT64_C(2)}},
2✔
1312
        };
2✔
1313
        AnyDict brian{
2✔
1314
            {"_id", "pk1"s},
2✔
1315
            {"name", "Brian"s},
2✔
1316
            {"age", INT64_C(33)},
2✔
1317
        };
2✔
1318
        AnyDict charley{{"_id", "pk2"s}, {"name", "Charley"s}, {"age", INT64_C(34)}, {"team", AnyVec{adam, brian}}};
2✔
1319
        AnyDict donald{
2✔
1320
            {"_id", "pk3"s},
2✔
1321
            {"name", "Donald"s},
2✔
1322
            {"age", INT64_C(35)},
2✔
1323
        };
2✔
1324
        AnyDict eddie{{"_id", "pk4"s},
2✔
1325
                      {"name", "Eddie"s},
2✔
1326
                      {"age", INT64_C(36)},
2✔
1327
                      {"assistant", donald},
2✔
1328
                      {"team", AnyVec{donald, charley}}};
2✔
1329
        Object obj = create_company(eddie, CreatePolicy::UpdateAll);
2✔
1330

1✔
1331
        auto table = r->read_group().get_table("class_person");
2✔
1332
        REQUIRE(table->size() == 5);
2!
1333
        Results result(r, table);
2✔
1334
        result = result.sort({{"_id", false}});
2✔
1335
        auto token = result.add_notification_callback([&](CollectionChangeSet c) {
8✔
1336
            change = c;
8✔
1337
            callback_called = true;
8✔
1338
        });
8✔
1339
        advance_and_notify(*r);
2✔
1340

1✔
1341
        // First update unconditionally
1✔
1342
        create_company(eddie, CreatePolicy::UpdateAll);
2✔
1343

1✔
1344
        callback_called = false;
2✔
1345
        advance_and_notify(*r);
2✔
1346
        REQUIRE(callback_called);
2!
1347
        REQUIRE_INDICES(change.modifications, 0, 1, 2, 3, 4);
2!
1348

1✔
1349
        // Now, only update where differences (there should not be any diffs - so no update)
1✔
1350
        create_company(eddie, CreatePolicy::UpdateModified);
2✔
1351

1✔
1352
        REQUIRE(table->size() == 5);
2!
1353
        callback_called = false;
2✔
1354
        advance_and_notify(*r);
2✔
1355
        REQUIRE(!callback_called);
2!
1356

1✔
1357
        // Now, only update sub-object)
1✔
1358
        donald["scores"] = AnyVec{INT64_C(3), INT64_C(4), INT64_C(5)};
2✔
1359
        // Insert the new donald
1✔
1360
        eddie["assistant"] = donald;
2✔
1361
        create_company(eddie, CreatePolicy::UpdateModified);
2✔
1362

1✔
1363
        REQUIRE(table->size() == 5);
2!
1364
        callback_called = false;
2✔
1365
        advance_and_notify(*r);
2✔
1366
        REQUIRE(callback_called);
2!
1367
        REQUIRE_INDICES(change.modifications, 1);
2!
1368

1✔
1369
        // Shorten list
1✔
1370
        donald["scores"] = AnyVec{INT64_C(3), INT64_C(4)};
2✔
1371
        eddie["assistant"] = donald;
2✔
1372
        create_company(eddie, CreatePolicy::UpdateModified);
2✔
1373

1✔
1374
        REQUIRE(table->size() == 5);
2!
1375
        callback_called = false;
2✔
1376
        advance_and_notify(*r);
2✔
1377
        REQUIRE(callback_called);
2!
1378
        REQUIRE_INDICES(change.modifications, 1);
2!
1379
    }
2✔
1380

70✔
1381
    SECTION("create with update - identical sub-object") {
140✔
1382
        Object sub_obj = create_sub(AnyDict{{"value", INT64_C(10)}, {"_id", INT64_C(10)}});
2✔
1383
        Object obj = create(AnyDict{
2✔
1384
            {"_id", INT64_C(1)},
2✔
1385
            {"bool", true},
2✔
1386
            {"int", INT64_C(5)},
2✔
1387
            {"float", 2.2f},
2✔
1388
            {"double", 3.3},
2✔
1389
            {"string", "hello"s},
2✔
1390
            {"data", "olleh"s},
2✔
1391
            {"date", Timestamp(10, 20)},
2✔
1392
            {"object", sub_obj},
2✔
1393
            {"object id", ObjectId("000000000000000000000001")},
2✔
1394
            {"decimal", Decimal128("1.23e45")},
2✔
1395
            {"uuid", UUID("3b241101-9999-9999-9999-4136c566a962")},
2✔
1396
            {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1397
        });
2✔
1398

1✔
1399
        auto obj_table = r->read_group().get_table("class_all types");
2✔
1400
        Results result(r, obj_table);
2✔
1401
        bool callback_called;
2✔
1402
        bool results_callback_called;
2✔
1403
        bool sub_callback_called;
2✔
1404
        auto token1 = obj.add_notification_callback([&](CollectionChangeSet) {
2✔
1405
            callback_called = true;
2✔
1406
        });
2✔
1407
        auto token2 = result.add_notification_callback([&](CollectionChangeSet) {
4✔
1408
            results_callback_called = true;
4✔
1409
        });
4✔
1410
        auto token3 = sub_obj.add_notification_callback([&](CollectionChangeSet) {
4✔
1411
            sub_callback_called = true;
4✔
1412
        });
4✔
1413
        advance_and_notify(*r);
2✔
1414

1✔
1415
        auto table = r->read_group().get_table("class_link target");
2✔
1416
        REQUIRE(table->size() == 1);
2!
1417

1✔
1418
        create(
2✔
1419
            AnyDict{
2✔
1420
                {"_id", INT64_C(1)},
2✔
1421
                {"bool", true},
2✔
1422
                {"int", INT64_C(5)},
2✔
1423
                {"float", 2.2f},
2✔
1424
                {"double", 3.3},
2✔
1425
                {"string", "hello"s},
2✔
1426
                {"data", "olleh"s},
2✔
1427
                {"date", Timestamp(10, 20)},
2✔
1428
                {"object id", ObjectId("000000000000000000000001")},
2✔
1429
                {"decimal", Decimal128("1.23e45")},
2✔
1430
                {"uuid", UUID("3b241101-9999-9999-9999-4136c566a962")},
2✔
1431
                {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(10)}}},
2✔
1432
            },
2✔
1433
            CreatePolicy::UpdateModified);
2✔
1434

1✔
1435
        REQUIRE(table->size() == 1);
2!
1436
        callback_called = false;
2✔
1437
        results_callback_called = false;
2✔
1438
        sub_callback_called = false;
2✔
1439
        advance_and_notify(*r);
2✔
1440
        REQUIRE(!callback_called);
2!
1441
        REQUIRE(!results_callback_called);
2!
1442
        REQUIRE(!sub_callback_called);
2!
1443

1✔
1444
        // Now change sub object
1✔
1445
        create(
2✔
1446
            AnyDict{
2✔
1447
                {"_id", INT64_C(1)},
2✔
1448
                {"bool", true},
2✔
1449
                {"int", INT64_C(5)},
2✔
1450
                {"float", 2.2f},
2✔
1451
                {"double", 3.3},
2✔
1452
                {"string", "hello"s},
2✔
1453
                {"data", "olleh"s},
2✔
1454
                {"date", Timestamp(10, 20)},
2✔
1455
                {"object id", ObjectId("000000000000000000000001")},
2✔
1456
                {"decimal", Decimal128("1.23e45")},
2✔
1457
                {"uuid", UUID("3b241101-9999-9999-9999-4136c566a962")},
2✔
1458
                {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(11)}}},
2✔
1459
            },
2✔
1460
            CreatePolicy::UpdateModified);
2✔
1461

1✔
1462
        callback_called = false;
2✔
1463
        results_callback_called = false;
2✔
1464
        sub_callback_called = false;
2✔
1465
        advance_and_notify(*r);
2✔
1466
        REQUIRE(!callback_called);
2!
1467
        REQUIRE(results_callback_called);
2!
1468
        REQUIRE(sub_callback_called);
2!
1469
    }
2✔
1470

70✔
1471
    SECTION("create with update - identical array of sub-objects") {
140✔
1472
        bool callback_called;
2✔
1473
        auto dict = AnyDict{
2✔
1474
            {"_id", INT64_C(1)},
2✔
1475
            {"bool", true},
2✔
1476
            {"int", INT64_C(5)},
2✔
1477
            {"float", 2.2f},
2✔
1478
            {"double", 3.3},
2✔
1479
            {"string", "hello"s},
2✔
1480
            {"data", "olleh"s},
2✔
1481
            {"date", Timestamp(10, 20)},
2✔
1482
            {"object array", AnyVec{AnyDict{{"_id", INT64_C(20)}, {"value", INT64_C(20)}},
2✔
1483
                                    AnyDict{{"_id", INT64_C(21)}, {"value", INT64_C(21)}}}},
2✔
1484
            {"object id", ObjectId("000000000000000000000001")},
2✔
1485
            {"decimal", Decimal128("1.23e45")},
2✔
1486
            {"uuid", UUID("3b241101-aaaa-bbbb-cccc-4136c566a962")},
2✔
1487
            {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1488
        };
2✔
1489
        Object obj = create(dict);
2✔
1490

1✔
1491
        auto obj_table = r->read_group().get_table("class_all types");
2✔
1492
        Results result(r, obj_table);
2✔
1493
        auto token1 = result.add_notification_callback([&](CollectionChangeSet) {
4✔
1494
            callback_called = true;
4✔
1495
        });
4✔
1496
        advance_and_notify(*r);
2✔
1497

1✔
1498
        create(dict, CreatePolicy::UpdateModified);
2✔
1499

1✔
1500
        callback_called = false;
2✔
1501
        advance_and_notify(*r);
2✔
1502
        REQUIRE(!callback_called);
2!
1503

1✔
1504
        // Now change list
1✔
1505
        dict["object array"] = AnyVec{AnyDict{{"_id", INT64_C(23)}, {"value", INT64_C(23)}}};
2✔
1506
        create(dict, CreatePolicy::UpdateModified);
2✔
1507

1✔
1508
        callback_called = false;
2✔
1509
        advance_and_notify(*r);
2✔
1510
        REQUIRE(callback_called);
2!
1511
    }
2✔
1512

70✔
1513
    for (auto policy : {CreatePolicy::UpdateAll, CreatePolicy::UpdateModified}) {
280✔
1514
        SECTION("set existing fields to null with update "s + (policy.diff ? "(diffed)" : "(all)")) {
280✔
1515
            AnyDict initial_values{
4✔
1516
                {"_id", INT64_C(1)},
4✔
1517
                {"bool", true},
4✔
1518
                {"int", INT64_C(5)},
4✔
1519
                {"float", 2.2f},
4✔
1520
                {"double", 3.3},
4✔
1521
                {"string", "hello"s},
4✔
1522
                {"data", "olleh"s},
4✔
1523
                {"date", Timestamp(10, 20)},
4✔
1524
                {"object id", ObjectId("000000000000000000000001")},
4✔
1525
                {"decimal", Decimal128("1.23e45")},
4✔
1526
                {"uuid", UUID("3b241101-aaaa-bbbb-cccc-4136c566a962")},
4✔
1527

2✔
1528
                {"bool array", AnyVec{true, false}},
4✔
1529
                {"int array", AnyVec{INT64_C(5), INT64_C(6)}},
4✔
1530
                {"float array", AnyVec{1.1f, 2.2f}},
4✔
1531
                {"double array", AnyVec{3.3, 4.4}},
4✔
1532
                {"string array", AnyVec{"a"s, "b"s, "c"s}},
4✔
1533
                {"data array", AnyVec{"d"s, "e"s, "f"s}},
4✔
1534
                {"date array", AnyVec{}},
4✔
1535
                {"object array", AnyVec{AnyDict{{"_id", INT64_C(20)}, {"value", INT64_C(20)}}}},
4✔
1536
                {"object id array", AnyVec{ObjectId("000000000000000000000001")}},
4✔
1537
                {"decimal array", AnyVec{Decimal128("1.23e45")}},
4✔
1538
                {"uuid array", AnyVec{UUID("3b241101-1111-bbbb-cccc-4136c566a962")}},
4✔
1539
            };
4✔
1540
            r->begin_transaction();
4✔
1541
            auto obj = Object::create(d, r, *r->schema().find("all optional types"), std::any(initial_values));
4✔
1542

2✔
1543
            // Missing fields in dictionary do not update anything
2✔
1544
            Object::create(d, r, *r->schema().find("all optional types"), std::any(AnyDict{{"_id", INT64_C(1)}}),
4✔
1545
                           policy);
4✔
1546

2✔
1547
            REQUIRE(d.get<bool>(obj, "bool") == true);
4!
1548
            REQUIRE(d.get<int64_t>(obj, "int") == 5);
4!
1549
            REQUIRE(d.get<float>(obj, "float") == 2.2f);
4!
1550
            REQUIRE(d.get<double>(obj, "double") == 3.3);
4!
1551
            REQUIRE(d.get<std::string>(obj, "string") == "hello");
4!
1552
            REQUIRE(d.get<Timestamp>(obj, "date") == Timestamp(10, 20));
4!
1553
            REQUIRE(d.get<util::Optional<ObjectId>>(obj, "object id") == ObjectId("000000000000000000000001"));
4!
1554
            REQUIRE(d.get<Decimal128>(obj, "decimal") == Decimal128("1.23e45"));
4!
1555
            REQUIRE(d.get<util::Optional<UUID>>(obj, "uuid") == UUID("3b241101-aaaa-bbbb-cccc-4136c566a962"));
4!
1556

2✔
1557
            REQUIRE(d.get<List>(obj, "bool array").get<util::Optional<bool>>(0) == true);
4!
1558
            REQUIRE(d.get<List>(obj, "int array").get<util::Optional<int64_t>>(0) == 5);
4!
1559
            REQUIRE(d.get<List>(obj, "float array").get<util::Optional<float>>(0) == 1.1f);
4!
1560
            REQUIRE(d.get<List>(obj, "double array").get<util::Optional<double>>(0) == 3.3);
4!
1561
            REQUIRE(d.get<List>(obj, "string array").get<StringData>(0) == "a");
4!
1562
            REQUIRE(d.get<List>(obj, "date array").size() == 0);
4!
1563
            REQUIRE(d.get<List>(obj, "object id array").get<util::Optional<ObjectId>>(0) ==
4!
1564
                    ObjectId("000000000000000000000001"));
4✔
1565
            REQUIRE(d.get<List>(obj, "decimal array").get<Decimal128>(0) == Decimal128("1.23e45"));
4!
1566
            REQUIRE(d.get<List>(obj, "uuid array").get<util::Optional<UUID>>(0) ==
4!
1567
                    UUID("3b241101-1111-bbbb-cccc-4136c566a962"));
4✔
1568

2✔
1569
            // Set all properties to null
2✔
1570
            AnyDict null_values{
4✔
1571
                {"_id", INT64_C(1)},
4✔
1572
                {"bool", std::any()},
4✔
1573
                {"int", std::any()},
4✔
1574
                {"float", std::any()},
4✔
1575
                {"double", std::any()},
4✔
1576
                {"string", std::any()},
4✔
1577
                {"data", std::any()},
4✔
1578
                {"date", std::any()},
4✔
1579
                {"object id", std::any()},
4✔
1580
                {"decimal", std::any()},
4✔
1581
                {"uuid", std::any()},
4✔
1582

2✔
1583
                {"bool array", AnyVec{std::any()}},
4✔
1584
                {"int array", AnyVec{std::any()}},
4✔
1585
                {"float array", AnyVec{std::any()}},
4✔
1586
                {"double array", AnyVec{std::any()}},
4✔
1587
                {"string array", AnyVec{std::any()}},
4✔
1588
                {"data array", AnyVec{std::any()}},
4✔
1589
                {"date array", AnyVec{Timestamp()}},
4✔
1590
                {"object id array", AnyVec{std::any()}},
4✔
1591
                {"decimal array", AnyVec{Decimal128(realm::null())}},
4✔
1592
                {"uuid array", AnyVec{std::any()}},
4✔
1593
            };
4✔
1594
            Object::create(d, r, *r->schema().find("all optional types"), std::any(null_values), policy);
4✔
1595

2✔
1596
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "bool").has_value());
4!
1597
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "int").has_value());
4!
1598
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "float").has_value());
4!
1599
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "double").has_value());
4!
1600
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "string").has_value());
4!
1601
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "data").has_value());
4!
1602
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "date").has_value());
4!
1603
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "object id").has_value());
4!
1604
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "decimal").has_value());
4!
1605
            REQUIRE_FALSE(obj.get_property_value<std::any>(d, "uuid").has_value());
4!
1606

2✔
1607
            REQUIRE(d.get<List>(obj, "bool array").get<util::Optional<bool>>(0) == util::none);
4!
1608
            REQUIRE(d.get<List>(obj, "int array").get<util::Optional<int64_t>>(0) == util::none);
4!
1609
            REQUIRE(d.get<List>(obj, "float array").get<util::Optional<float>>(0) == util::none);
4!
1610
            REQUIRE(d.get<List>(obj, "double array").get<util::Optional<double>>(0) == util::none);
4!
1611
            REQUIRE(d.get<List>(obj, "string array").get<StringData>(0) == StringData());
4!
1612
            REQUIRE(d.get<List>(obj, "data array").get<BinaryData>(0) == BinaryData());
4!
1613
            REQUIRE(d.get<List>(obj, "date array").get<Timestamp>(0) == Timestamp());
4!
1614
            REQUIRE(d.get<List>(obj, "object id array").get<util::Optional<ObjectId>>(0) == util::none);
4!
1615
            REQUIRE(d.get<List>(obj, "decimal array").get<Decimal>(0) == Decimal128(realm::null()));
4!
1616
            REQUIRE(d.get<List>(obj, "uuid array").get<util::Optional<UUID>>(0) == util::none);
4!
1617

2✔
1618
            // Set all lists to null
2✔
1619
            AnyDict null_arrays{
4✔
1620
                {"_id", INT64_C(1)},           {"bool array", std::any()},   {"int array", std::any()},
4✔
1621
                {"float array", std::any()},   {"double array", std::any()}, {"string array", std::any()},
4✔
1622
                {"data array", std::any()},    {"date array", std::any()},   {"object id array", std::any()},
4✔
1623
                {"decimal array", std::any()}, {"uuid array", std::any()}};
4✔
1624
            Object::create(d, r, *r->schema().find("all optional types"), std::any(null_arrays), policy);
4✔
1625

2✔
1626
            REQUIRE(d.get<List>(obj, "bool array").size() == 0);
4!
1627
            REQUIRE(d.get<List>(obj, "int array").size() == 0);
4!
1628
            REQUIRE(d.get<List>(obj, "float array").size() == 0);
4!
1629
            REQUIRE(d.get<List>(obj, "double array").size() == 0);
4!
1630
            REQUIRE(d.get<List>(obj, "string array").size() == 0);
4!
1631
            REQUIRE(d.get<List>(obj, "data array").size() == 0);
4!
1632
            REQUIRE(d.get<List>(obj, "date array").size() == 0);
4!
1633
            REQUIRE(d.get<List>(obj, "object id array").size() == 0);
4!
1634
            REQUIRE(d.get<List>(obj, "decimal array").size() == 0);
4!
1635
            REQUIRE(d.get<List>(obj, "uuid array").size() == 0);
4!
1636

2✔
1637
            // Set all properties back to non-null
2✔
1638
            Object::create(d, r, *r->schema().find("all optional types"), std::any(initial_values), policy);
4✔
1639
            REQUIRE(d.get<bool>(obj, "bool") == true);
4!
1640
            REQUIRE(d.get<int64_t>(obj, "int") == 5);
4!
1641
            REQUIRE(d.get<float>(obj, "float") == 2.2f);
4!
1642
            REQUIRE(d.get<double>(obj, "double") == 3.3);
4!
1643
            REQUIRE(d.get<std::string>(obj, "string") == "hello");
4!
1644
            REQUIRE(d.get<Timestamp>(obj, "date") == Timestamp(10, 20));
4!
1645
            REQUIRE(d.get<util::Optional<ObjectId>>(obj, "object id").value() ==
4!
1646
                    ObjectId("000000000000000000000001"));
4✔
1647
            REQUIRE(d.get<Decimal128>(obj, "decimal") == Decimal128("1.23e45"));
4!
1648
            REQUIRE(d.get<util::Optional<UUID>>(obj, "uuid") == UUID("3b241101-aaaa-bbbb-cccc-4136c566a962"));
4!
1649

2✔
1650
            REQUIRE(d.get<List>(obj, "bool array").get<util::Optional<bool>>(0) == true);
4!
1651
            REQUIRE(d.get<List>(obj, "int array").get<util::Optional<int64_t>>(0) == 5);
4!
1652
            REQUIRE(d.get<List>(obj, "float array").get<util::Optional<float>>(0) == 1.1f);
4!
1653
            REQUIRE(d.get<List>(obj, "double array").get<util::Optional<double>>(0) == 3.3);
4!
1654
            REQUIRE(d.get<List>(obj, "string array").get<StringData>(0) == "a");
4!
1655
            REQUIRE(d.get<List>(obj, "date array").size() == 0);
4!
1656
            REQUIRE(d.get<List>(obj, "object id array").get<util::Optional<ObjectId>>(0) ==
4!
1657
                    ObjectId("000000000000000000000001"));
4✔
1658
            REQUIRE(d.get<List>(obj, "decimal array").get<Decimal128>(0) == Decimal128("1.23e45"));
4!
1659
            REQUIRE(d.get<List>(obj, "uuid array").get<util::Optional<UUID>>(0) ==
4!
1660
                    UUID("3b241101-1111-bbbb-cccc-4136c566a962"));
4✔
1661
        }
4✔
1662
    }
280✔
1663

70✔
1664
    SECTION("create throws for duplicate pk if update is not specified") {
140✔
1665
        create(AnyDict{
2✔
1666
            {"_id", INT64_C(1)},
2✔
1667
            {"bool", true},
2✔
1668
            {"int", INT64_C(5)},
2✔
1669
            {"float", 2.2f},
2✔
1670
            {"double", 3.3},
2✔
1671
            {"string", "hello"s},
2✔
1672
            {"data", "olleh"s},
2✔
1673
            {"date", Timestamp(10, 20)},
2✔
1674
            {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(10)}}},
2✔
1675
            {"array", AnyVector{AnyDict{{"value", INT64_C(20)}}}},
2✔
1676
            {"object id", ObjectId("000000000000000000000001")},
2✔
1677
            {"decimal", Decimal128("1.23e45")},
2✔
1678
            {"uuid", UUID("3b241101-aaaa-bbbb-cccc-4136c566a962")},
2✔
1679
            {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1680
        });
2✔
1681
        REQUIRE_EXCEPTION(create(AnyDict{
2✔
1682
                              {"_id", INT64_C(1)},
2✔
1683
                              {"bool", true},
2✔
1684
                              {"int", INT64_C(5)},
2✔
1685
                              {"float", 2.2f},
2✔
1686
                              {"double", 3.3},
2✔
1687
                              {"string", "hello"s},
2✔
1688
                              {"data", "olleh"s},
2✔
1689
                              {"date", Timestamp(10, 20)},
2✔
1690
                              {"object", AnyDict{{"_id", INT64_C(10)}, {"value", INT64_C(10)}}},
2✔
1691
                              {"array", AnyVector{AnyDict{{"value", INT64_C(20)}}}},
2✔
1692
                              {"object id", ObjectId("000000000000000000000001")},
2✔
1693
                              {"decimal", Decimal128("1.23e45")},
2✔
1694
                              {"uuid", UUID("3b241101-aaaa-bbbb-cccc-4136c566a962")},
2✔
1695
                              {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1696
                          }),
2✔
1697
                          ObjectAlreadyExists,
2✔
1698
                          "Attempting to create an object of type 'all types' with an existing primary key value "
2✔
1699
                          "'not implemented'");
2✔
1700
    }
2✔
1701

70✔
1702
    SECTION("create with explicit null pk does not fall back to default") {
140✔
1703
        d.defaults["nullable int pk"] = {
2✔
1704
            {"_id", INT64_C(10)},
2✔
1705
        };
2✔
1706
        d.defaults["nullable string pk"] = {
2✔
1707
            {"_id", "value"s},
2✔
1708
        };
2✔
1709
        auto create = [&](std::any&& value, StringData type) {
8✔
1710
            r->begin_transaction();
8✔
1711
            auto obj = Object::create(d, r, *r->schema().find(type), value);
8✔
1712
            r->commit_transaction();
8✔
1713
            return obj;
8✔
1714
        };
8✔
1715

1✔
1716
        auto obj = create(AnyDict{{"_id", d.null_value()}}, "nullable int pk");
2✔
1717
        auto col_pk_int = r->read_group().get_table("class_nullable int pk")->get_column_key("_id");
2✔
1718
        auto col_pk_str = r->read_group().get_table("class_nullable string pk")->get_column_key("_id");
2✔
1719
        REQUIRE(obj.get_obj().is_null(col_pk_int));
2!
1720
        obj = create(AnyDict{{"_id", d.null_value()}}, "nullable string pk");
2✔
1721
        REQUIRE(obj.get_obj().is_null(col_pk_str));
2!
1722

1✔
1723
        obj = create(AnyDict{{}}, "nullable int pk");
2✔
1724
        REQUIRE(obj.get_obj().get<util::Optional<Int>>(col_pk_int) == 10);
2!
1725
        obj = create(AnyDict{{}}, "nullable string pk");
2✔
1726
        REQUIRE(obj.get_obj().get<String>(col_pk_str) == "value");
2!
1727
    }
2✔
1728

70✔
1729
    SECTION("create null and 0 primary keys for Int types") {
140✔
1730
        auto create = [&](std::any&& value, StringData type) {
4✔
1731
            r->begin_transaction();
4✔
1732
            auto obj = Object::create(d, r, *r->schema().find(type), value);
4✔
1733
            r->commit_transaction();
4✔
1734
            return obj;
4✔
1735
        };
4✔
1736
        create(AnyDict{{"_id", std::any()}}, "all optional types");
2✔
1737
        create(AnyDict{{"_id", INT64_C(0)}}, "all optional types");
2✔
1738
        REQUIRE(Results(r, r->read_group().get_table("class_all optional types")).size() == 2);
2!
1739
    }
2✔
1740

70✔
1741
    SECTION("create null and default primary keys for ObjectId types") {
140✔
1742
        auto create = [&](std::any&& value, StringData type) {
4✔
1743
            r->begin_transaction();
4✔
1744
            auto obj = Object::create(d, r, *r->schema().find(type), value);
4✔
1745
            r->commit_transaction();
4✔
1746
            return obj;
4✔
1747
        };
4✔
1748
        create(AnyDict{{"_id", std::any()}}, "nullable object id pk");
2✔
1749
        create(AnyDict{{"_id", ObjectId::gen()}}, "nullable object id pk");
2✔
1750
        REQUIRE(Results(r, r->read_group().get_table("class_nullable object id pk")).size() == 2);
2!
1751
    }
2✔
1752

70✔
1753
    SECTION("create only requires properties explicitly in the schema") {
140✔
1754
        config.schema = Schema{{"all types", {{"_id", PropertyType::Int, Property::IsPrimary{true}}}}};
2✔
1755
        auto subset_realm = Realm::get_shared_realm(config);
2✔
1756
        subset_realm->begin_transaction();
2✔
1757
        REQUIRE_NOTHROW(Object::create(d, subset_realm, "all types", std::any(AnyDict{{"_id", INT64_C(123)}})));
2✔
1758
        subset_realm->commit_transaction();
2✔
1759

1✔
1760
        r->refresh();
2✔
1761
        auto obj = *r->read_group().get_table("class_all types")->begin();
2✔
1762
        REQUIRE(obj.get<int64_t>("_id") == 123);
2!
1763

1✔
1764
        // Other columns should have the default unset values
1✔
1765
        REQUIRE(obj.get<bool>("bool") == false);
2!
1766
        REQUIRE(obj.get<int64_t>("int") == 0);
2!
1767
        REQUIRE(obj.get<float>("float") == 0);
2!
1768
        REQUIRE(obj.get<StringData>("string") == "");
2!
1769
    }
2✔
1770

70✔
1771
    SECTION("getters and setters") {
140✔
1772
        r->begin_transaction();
2✔
1773

1✔
1774
        auto table = r->read_group().get_table("class_all types");
2✔
1775
        table->create_object_with_primary_key(1);
2✔
1776
        Object obj(r, *r->schema().find("all types"), *table->begin());
2✔
1777

1✔
1778
        auto link_table = r->read_group().get_table("class_link target");
2✔
1779
        link_table->create_object_with_primary_key(0);
2✔
1780
        Object linkobj(r, *r->schema().find("link target"), *link_table->begin());
2✔
1781

1✔
1782
        auto property = *r->schema().find("all types")->property_for_name("int");
2✔
1783
        obj.set_property_value(d, property, std::any(INT64_C(6)));
2✔
1784
        REQUIRE(util::any_cast<int64_t>(obj.get_property_value<std::any>(d, property)) == 6);
2!
1785

1✔
1786
        obj.set_property_value(d, "bool", std::any(true));
2✔
1787
        REQUIRE(util::any_cast<bool>(obj.get_property_value<std::any>(d, "bool")) == true);
2!
1788

1✔
1789
        obj.set_property_value(d, "int", std::any(INT64_C(5)));
2✔
1790
        REQUIRE(util::any_cast<int64_t>(obj.get_property_value<std::any>(d, "int")) == 5);
2!
1791

1✔
1792
        obj.set_property_value(d, "float", std::any(1.23f));
2✔
1793
        REQUIRE(util::any_cast<float>(obj.get_property_value<std::any>(d, "float")) == 1.23f);
2!
1794

1✔
1795
        obj.set_property_value(d, "double", std::any(1.23));
2✔
1796
        REQUIRE(util::any_cast<double>(obj.get_property_value<std::any>(d, "double")) == 1.23);
2!
1797

1✔
1798
        obj.set_property_value(d, "string", std::any("abc"s));
2✔
1799
        REQUIRE(util::any_cast<std::string>(obj.get_property_value<std::any>(d, "string")) == "abc");
2!
1800

1✔
1801
        obj.set_property_value(d, "data", std::any("abc"s));
2✔
1802
        REQUIRE(util::any_cast<std::string>(obj.get_property_value<std::any>(d, "data")) == "abc");
2!
1803

1✔
1804
        obj.set_property_value(d, "date", std::any(Timestamp(1, 2)));
2✔
1805
        REQUIRE(util::any_cast<Timestamp>(obj.get_property_value<std::any>(d, "date")) == Timestamp(1, 2));
2!
1806

1✔
1807
        obj.set_property_value(d, "object id", std::any(ObjectId("111111111111111111111111")));
2✔
1808
        REQUIRE(util::any_cast<ObjectId>(obj.get_property_value<std::any>(d, "object id")) ==
2!
1809
                ObjectId("111111111111111111111111"));
2✔
1810

1✔
1811
        obj.set_property_value(d, "decimal", std::any(Decimal128("42.4242e42")));
2✔
1812
        REQUIRE(util::any_cast<Decimal128>(obj.get_property_value<std::any>(d, "decimal")) ==
2!
1813
                Decimal128("42.4242e42"));
2✔
1814

1✔
1815
        obj.set_property_value(d, "uuid", std::any(UUID("3b241101-aaaa-bbbb-cccc-4136c566a962")));
2✔
1816
        REQUIRE(util::any_cast<UUID>(obj.get_property_value<std::any>(d, "uuid")) ==
2!
1817
                UUID("3b241101-aaaa-bbbb-cccc-4136c566a962"));
2✔
1818

1✔
1819
        obj.set_property_value(d, "mixed", std::any(25));
2✔
1820
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) == 25);
2!
1821
        obj.set_property_value(d, "mixed", std::any("Hello"s));
2✔
1822
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) == "Hello");
2!
1823
        obj.set_property_value(d, "mixed", std::any(1.23));
2✔
1824
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) == 1.23);
2!
1825
        obj.set_property_value(d, "mixed", std::any(123.45f));
2✔
1826
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) == 123.45f);
2!
1827
        obj.set_property_value(d, "mixed", std::any(Timestamp(30, 40)));
2✔
1828
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) == Timestamp(30, 40));
2!
1829
        obj.set_property_value(d, "mixed", std::any(ObjectId("111111111111111111111111")));
2✔
1830
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) ==
2!
1831
                ObjectId("111111111111111111111111"));
2✔
1832
        obj.set_property_value(d, "mixed", std::any(Decimal128("42.4242e42")));
2✔
1833
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) == Decimal128("42.4242e42"));
2!
1834
        obj.set_property_value(d, "mixed", std::any(UUID("3b241101-aaaa-bbbb-cccc-4136c566a962")));
2✔
1835
        REQUIRE(util::any_cast<Mixed>(obj.get_property_value<std::any>(d, "mixed")) ==
2!
1836
                UUID("3b241101-aaaa-bbbb-cccc-4136c566a962"));
2✔
1837

1✔
1838
        obj.set_property_value(d, "dictionary", std::any(AnyDict({{"k1", "v1"s}, {"k2", "v2"s}})));
2✔
1839
        auto dict = util::any_cast<object_store::Dictionary&&>(obj.get_property_value<std::any>(d, "dictionary"));
2✔
1840
        REQUIRE(dict.get_any("k1") == "v1");
2!
1841
        REQUIRE(dict.get_any("k2") == "v2");
2!
1842

1✔
1843
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "object").has_value());
2!
1844
        obj.set_property_value(d, "object", std::any(linkobj));
2✔
1845
        REQUIRE(util::any_cast<Object>(obj.get_property_value<std::any>(d, "object")).get_obj().get_key() ==
2!
1846
                linkobj.get_obj().get_key());
2✔
1847

1✔
1848
        auto linking = util::any_cast<Results>(linkobj.get_property_value<std::any>(d, "origin"));
2✔
1849
        REQUIRE(linking.size() == 1);
2!
1850

1✔
1851
        REQUIRE_EXCEPTION(obj.set_property_value(d, "_id", std::any(INT64_C(5))), ModifyPrimaryKey,
2✔
1852
                          "Cannot modify primary key after creation: 'all types._id'");
2✔
1853
        REQUIRE_EXCEPTION(obj.set_property_value(d, "not a property", std::any(INT64_C(5))), InvalidProperty,
2✔
1854
                          "Property 'all types.not a property' does not exist");
2✔
1855

1✔
1856
        r->commit_transaction();
2✔
1857

1✔
1858
        REQUIRE_EXCEPTION(obj.get_property_value<std::any>(d, "not a property"), InvalidProperty,
2✔
1859
                          "Property 'all types.not a property' does not exist");
2✔
1860
        REQUIRE_EXCEPTION(obj.set_property_value(d, "int", std::any(INT64_C(5))), WrongTransactionState,
2✔
1861
                          "Cannot modify managed objects outside of a write transaction.");
2✔
1862
    }
2✔
1863

70✔
1864
    SECTION("setter has correct create policy") {
140✔
1865
        r->begin_transaction();
2✔
1866
        auto table = r->read_group().get_table("class_all types");
2✔
1867
        table->create_object_with_primary_key(1);
2✔
1868
        Object obj(r, *r->schema().find("all types"), *table->begin());
2✔
1869
        CreatePolicyRecordingContext ctx;
2✔
1870

1✔
1871
        auto validate = [&obj, &ctx](CreatePolicy policy) {
10✔
1872
            obj.set_property_value(ctx, "mixed", std::any(Mixed("Hello")), policy);
10✔
1873
            REQUIRE(policy.copy == ctx.last_create_policy.copy);
10!
1874
            REQUIRE(policy.diff == ctx.last_create_policy.diff);
10!
1875
            REQUIRE(policy.create == ctx.last_create_policy.create);
10!
1876
            REQUIRE(policy.update == ctx.last_create_policy.update);
10!
1877
        };
10✔
1878

1✔
1879
        validate(CreatePolicy::Skip);
2✔
1880
        validate(CreatePolicy::ForceCreate);
2✔
1881
        validate(CreatePolicy::UpdateAll);
2✔
1882
        validate(CreatePolicy::UpdateModified);
2✔
1883
        validate(CreatePolicy::SetLink);
2✔
1884
        r->commit_transaction();
2✔
1885
    }
2✔
1886

70✔
1887
    SECTION("list property self-assign is a no-op") {
140✔
1888
        auto obj = create(AnyDict{
2✔
1889
            {"_id", INT64_C(1)},
2✔
1890
            {"bool", true},
2✔
1891
            {"int", INT64_C(5)},
2✔
1892
            {"float", 2.2f},
2✔
1893
            {"double", 3.3},
2✔
1894
            {"string", "hello"s},
2✔
1895
            {"data", "olleh"s},
2✔
1896
            {"date", Timestamp(10, 20)},
2✔
1897
            {"object id", ObjectId("000000000000000000000001")},
2✔
1898
            {"decimal", Decimal128("1.23e45")},
2✔
1899
            {"uuid", UUID("3b241101-aaaa-bbbb-cccc-4136c566a962")},
2✔
1900
            {"dictionary", AnyDict{{"key", "value"s}}},
2✔
1901

1✔
1902
            {"bool array", AnyVec{true, false}},
2✔
1903
            {"object array", AnyVec{AnyDict{{"_id", INT64_C(20)}, {"value", INT64_C(20)}}}},
2✔
1904
        });
2✔
1905

1✔
1906
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "bool array")).size() == 2);
2!
1907
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "object array")).size() == 1);
2!
1908

1✔
1909
        r->begin_transaction();
2✔
1910
        obj.set_property_value(d, "bool array", obj.get_property_value<std::any>(d, "bool array"));
2✔
1911
        obj.set_property_value(d, "object array", obj.get_property_value<std::any>(d, "object array"));
2✔
1912
        r->commit_transaction();
2✔
1913

1✔
1914
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "bool array")).size() == 2);
2!
1915
        REQUIRE(util::any_cast<List&&>(obj.get_property_value<std::any>(d, "object array")).size() == 1);
2!
1916
    }
2✔
1917

70✔
1918
    SECTION("Mixed emit notification on type change") {
140✔
1919
        auto validate_change = [&](std::any&& obj_dict, std::any&& value) {
6✔
1920
            r->begin_transaction();
6✔
1921
            auto obj =
6✔
1922
                Object::create(d, r, *r->schema().find("all optional types"), obj_dict, CreatePolicy::UpdateModified);
6✔
1923
            r->commit_transaction();
6✔
1924

3✔
1925
            CollectionChangeSet change;
6✔
1926
            auto token = obj.add_notification_callback([&](CollectionChangeSet c) {
12✔
1927
                change = c;
12✔
1928
            });
12✔
1929
            advance_and_notify(*r);
6✔
1930

3✔
1931
            r->begin_transaction();
6✔
1932
            obj.set_property_value(d, "mixed", value, CreatePolicy::UpdateModified);
6✔
1933
            r->commit_transaction();
6✔
1934

3✔
1935
            advance_and_notify(*r);
6✔
1936

3✔
1937
            REQUIRE_INDICES(change.modifications, 0);
6!
1938
        };
6✔
1939

1✔
1940
        validate_change(AnyDict{{"_id", std::any()}, {"mixed", true}}, std::any(1));
2✔
1941

1✔
1942
        validate_change(AnyDict{{"_id", std::any()}, {"mixed", false}}, std::any(0));
2✔
1943

1✔
1944
        auto object_id = ObjectId::gen();
2✔
1945

1✔
1946
        validate_change(AnyDict{{"_id", std::any()}, {"mixed", object_id}}, std::any(object_id.get_timestamp()));
2✔
1947
    }
2✔
1948

70✔
1949
    SECTION("get and set an unresolved object") {
140✔
1950
        r->begin_transaction();
2✔
1951

1✔
1952
        auto table = r->read_group().get_table("class_all types");
2✔
1953
        ColKey link_col = table->get_column_key("object");
2✔
1954
        table->create_object_with_primary_key(1);
2✔
1955
        Object obj(r, *r->schema().find("all types"), *table->begin());
2✔
1956

1✔
1957
        auto link_table = r->read_group().get_table("class_link target");
2✔
1958
        link_table->create_object_with_primary_key(0);
2✔
1959
        Object linkobj(r, *r->schema().find("link target"), *link_table->begin());
2✔
1960

1✔
1961
        REQUIRE_FALSE(obj.get_property_value<std::any>(d, "object").has_value());
2!
1962
        obj.set_property_value(d, "object", std::any(linkobj));
2✔
1963
        REQUIRE(util::any_cast<Object>(obj.get_property_value<std::any>(d, "object")).get_obj().get_key() ==
2!
1964
                linkobj.get_obj().get_key());
2✔
1965

1✔
1966
        REQUIRE(!obj.get_obj().is_unresolved(link_col));
2!
1967
        linkobj.get_obj().invalidate();
2✔
1968
        REQUIRE(obj.get_obj().is_unresolved(link_col));
2!
1969

1✔
1970
        CHECK_FALSE(obj.get_property_value<std::any>(d, "object").has_value());
2!
1971

1✔
1972
        obj.set_property_value(d, "object", std::any());
2✔
1973
        // Cancelling a transaction in which the first tombstone was created, caused the program to crash
1✔
1974
        // because we tried to update m_tombstones on a null ref. Now fixed
1✔
1975
        r->cancel_transaction();
2✔
1976
    }
2✔
1977

70✔
1978
#if REALM_ENABLE_SYNC
140✔
1979
    if (!util::EventLoop::has_implementation())
140✔
UNCOV
1980
        return;
×
1981
    SECTION("defaults do not override values explicitly passed to create()") {
140✔
1982
        TestSyncManager init_sync_manager({}, {false});
2✔
1983
        auto& server = init_sync_manager.sync_server();
2✔
1984
        SyncTestFile config1(init_sync_manager, "shared");
2✔
1985
        config1.schema = config.schema;
2✔
1986
        SyncTestFile config2(init_sync_manager, "shared");
2✔
1987
        config2.schema = config.schema;
2✔
1988

1✔
1989
        AnyDict v1{
2✔
1990
            {"_id", INT64_C(7)},
2✔
1991
            {"array 1", AnyVector{AnyDict{{"_id", INT64_C(1)}, {"value", INT64_C(1)}}}},
2✔
1992
            {"array 2", AnyVector{AnyDict{{"_id", INT64_C(2)}, {"value", INT64_C(2)}}}},
2✔
1993
        };
2✔
1994
        auto v2 = v1;
2✔
1995
        v1["int 1"] = INT64_C(1);
2✔
1996
        v2["int 2"] = INT64_C(2);
2✔
1997
        v2["array 1"] = AnyVector{AnyDict{{"_id", INT64_C(3)}, {"value", INT64_C(1)}}};
2✔
1998
        v2["array 2"] = AnyVector{AnyDict{{"_id", INT64_C(4)}, {"value", INT64_C(2)}}};
2✔
1999

1✔
2000
        auto r1 = Realm::get_shared_realm(config1);
2✔
2001
        auto r2 = Realm::get_shared_realm(config2);
2✔
2002

1✔
2003
        TestContext c1(r1);
2✔
2004
        TestContext c2(r2);
2✔
2005

1✔
2006
        c1.defaults["pk after list"] = {
2✔
2007
            {"int 1", INT64_C(10)},
2✔
2008
            {"int 2", INT64_C(10)},
2✔
2009
        };
2✔
2010
        c2.defaults = c1.defaults;
2✔
2011

1✔
2012
        r1->begin_transaction();
2✔
2013
        r2->begin_transaction();
2✔
2014
        auto object1 = Object::create(c1, r1, *r1->schema().find("pk after list"), std::any(v1));
2✔
2015
        auto object2 = Object::create(c2, r2, *r2->schema().find("pk after list"), std::any(v2));
2✔
2016
        r2->commit_transaction();
2✔
2017
        r1->commit_transaction();
2✔
2018

1✔
2019
        server.start();
2✔
2020
        util::EventLoop::main().run_until([&] {
33,088✔
2021
            return r1->read_group().get_table("class_array target")->size() == 4;
33,088✔
2022
        });
33,088✔
2023

1✔
2024
        Obj obj = object1.get_obj();
2✔
2025
        REQUIRE(obj.get<Int>("_id") == 7); // pk
2!
2026
        REQUIRE(obj.get_linklist("array 1").size() == 2);
2!
2027
        REQUIRE(obj.get<Int>("int 1") == 1); // non-default from r1
2!
2028
        REQUIRE(obj.get<Int>("int 2") == 2); // non-default from r2
2!
2029
        REQUIRE(obj.get_linklist("array 2").size() == 2);
2!
2030
    }
2✔
2031
#endif
140✔
2032
}
140✔
2033

2034
TEST_CASE("Multithreaded object notifications") {
6✔
2035
    InMemoryTestFile config;
6✔
2036
    auto r = Realm::get_shared_realm(config);
6✔
2037
    r->update_schema({{"object", {{"value", PropertyType::Int}}}});
6✔
2038

3✔
2039
    r->begin_transaction();
6✔
2040
    auto obj = r->read_group().get_table("class_object")->create_object();
6✔
2041
    r->commit_transaction();
6✔
2042

3✔
2043
    Object object(r, obj);
6✔
2044
    int64_t value = 0;
6✔
2045
    auto token = object.add_notification_callback([&](auto) {
3,289✔
2046
        value = obj.get<int64_t>("value");
3,289✔
2047
    });
3,289✔
2048
    constexpr const int end_value = 1000;
6✔
2049

3✔
2050
    // Try to verify that the notification machinery pins all versions that it
3✔
2051
    // needs to pin by performing a large number of very small writes on a
3✔
2052
    // background thread while the main thread is continously advancing via
3✔
2053
    // each of the three ways to advance reads.
3✔
2054
    JoiningThread thread([&] {
6✔
2055
        // Not actually frozen, but we need to disable thread-checks for libuv platforms
3✔
2056
        config.scheduler = util::Scheduler::make_frozen(VersionID());
6✔
2057
        auto r = Realm::get_shared_realm(config);
6✔
2058
        auto obj = *r->read_group().get_table("class_object")->begin();
6✔
2059
        for (int i = 0; i <= end_value; ++i) {
6,012✔
2060
            r->begin_transaction();
6,006✔
2061
            obj.set<int64_t>("value", i);
6,006✔
2062
            r->commit_transaction();
6,006✔
2063
        }
6,006✔
2064
    });
6✔
2065

3✔
2066
    SECTION("notify()") {
6✔
2067
        REQUIRE_NOTHROW(util::EventLoop::main().run_until([&] {
2✔
2068
            return value == end_value;
2✔
2069
        }));
2✔
2070
    }
2✔
2071
    SECTION("refresh()") {
6✔
2072
        while (value < end_value) {
428✔
2073
            REQUIRE_NOTHROW(r->refresh());
426✔
2074
        }
426✔
2075
    }
2✔
2076
    SECTION("begin_transaction()") {
6✔
2077
        while (value < end_value) {
2,006✔
2078
            REQUIRE_NOTHROW(r->begin_transaction());
2,004✔
2079
            r->cancel_transaction();
2,004✔
2080
        }
2,004✔
2081
    }
2✔
2082
}
6✔
2083

2084
TEST_CASE("Embedded Object") {
22✔
2085
    Schema schema{
22✔
2086
        {"all types",
22✔
2087
         {
22✔
2088
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
22✔
2089
             {"object", PropertyType::Object | PropertyType::Nullable, "link target"},
22✔
2090
             {"array", PropertyType::Object | PropertyType::Array, "array target"},
22✔
2091
         }},
22✔
2092
        {"all types no pk",
22✔
2093
         {
22✔
2094
             {"value", PropertyType::Int},
22✔
2095
             {"object", PropertyType::Object | PropertyType::Nullable, "link target"},
22✔
2096
             {"array", PropertyType::Object | PropertyType::Array, "array target"},
22✔
2097
         }},
22✔
2098
        {"link target",
22✔
2099
         ObjectSchema::ObjectType::Embedded,
22✔
2100
         {
22✔
2101
             {"value", PropertyType::Int},
22✔
2102
         }},
22✔
2103
        {"array target",
22✔
2104
         ObjectSchema::ObjectType::Embedded,
22✔
2105
         {
22✔
2106
             {"value", PropertyType::Int},
22✔
2107
         }},
22✔
2108
    };
22✔
2109
    InMemoryTestFile config;
22✔
2110
    config.automatic_change_notifications = false;
22✔
2111
    config.schema_mode = SchemaMode::Automatic;
22✔
2112
    config.schema = schema;
22✔
2113

11✔
2114
    auto realm = Realm::get_shared_realm(config);
22✔
2115
    CppContext ctx(realm);
22✔
2116

11✔
2117
    auto create = [&](std::any&& value, CreatePolicy policy = CreatePolicy::UpdateAll) {
34✔
2118
        realm->begin_transaction();
34✔
2119
        auto obj = Object::create(ctx, realm, *realm->schema().find("all types"), value, policy);
34✔
2120
        realm->commit_transaction();
34✔
2121
        return obj;
34✔
2122
    };
34✔
2123

11✔
2124
    SECTION("Basic object creation") {
22✔
2125
        auto obj = create(AnyDict{
2✔
2126
            {"_id", INT64_C(1)},
2✔
2127
            {"object", AnyDict{{"value", INT64_C(10)}}},
2✔
2128
            {"array", AnyVector{AnyDict{{"value", INT64_C(20)}}, AnyDict{{"value", INT64_C(30)}}}},
2✔
2129
        });
2✔
2130

1✔
2131
        REQUIRE(obj.get_obj().get<int64_t>("_id") == 1);
2!
2132
        auto linked_obj = util::any_cast<Object>(obj.get_property_value<std::any>(ctx, "object")).get_obj();
2✔
2133
        REQUIRE(linked_obj.is_valid());
2!
2134
        REQUIRE(linked_obj.get<int64_t>("value") == 10);
2!
2135
        auto list = util::any_cast<List>(obj.get_property_value<std::any>(ctx, "array"));
2✔
2136
        REQUIRE(list.size() == 2);
2!
2137
        REQUIRE(list.get(0).get<int64_t>("value") == 20);
2!
2138
        REQUIRE(list.get(1).get<int64_t>("value") == 30);
2!
2139
    }
2✔
2140

11✔
2141
    SECTION("set_property_value() on link to embedded object") {
22✔
2142
        auto obj = create(AnyDict{
8✔
2143
            {"_id", INT64_C(1)},
8✔
2144
            {"object", AnyDict{{"value", INT64_C(10)}}},
8✔
2145
            {"array", AnyVector{AnyDict{{"value", INT64_C(20)}}, AnyDict{{"value", INT64_C(30)}}}},
8✔
2146
        });
8✔
2147

4✔
2148
        SECTION("throws when given a managed object") {
8✔
2149
            realm->begin_transaction();
2✔
2150
            REQUIRE_EXCEPTION(obj.set_property_value(ctx, "object", obj.get_property_value<std::any>(ctx, "object")),
2✔
2151
                              InvalidArgument, "Cannot set a link to an existing managed embedded object");
2✔
2152
            realm->cancel_transaction();
2✔
2153
        }
2✔
2154

4✔
2155
        SECTION("replaces object when given a dictionary and CreatePolicy::UpdateAll") {
8✔
2156
            realm->begin_transaction();
2✔
2157
            auto old_linked = util::any_cast<Object>(obj.get_property_value<std::any>(ctx, "object"));
2✔
2158
            obj.set_property_value(ctx, "object", std::any(AnyDict{{"value", INT64_C(40)}}));
2✔
2159
            auto new_linked = util::any_cast<Object>(obj.get_property_value<std::any>(ctx, "object"));
2✔
2160
            REQUIRE_FALSE(old_linked.is_valid());
2!
2161
            REQUIRE(new_linked.get_obj().get<int64_t>("value") == 40);
2!
2162
            realm->cancel_transaction();
2✔
2163
        }
2✔
2164

4✔
2165
        SECTION("mutates existing object when given a dictionary and CreatePolicy::UpdateModified") {
8✔
2166
            realm->begin_transaction();
2✔
2167
            auto old_linked = util::any_cast<Object>(obj.get_property_value<std::any>(ctx, "object"));
2✔
2168
            obj.set_property_value(ctx, "object", std::any(AnyDict{{"value", INT64_C(40)}}),
2✔
2169
                                   CreatePolicy::UpdateModified);
2✔
2170
            auto new_linked = util::any_cast<Object>(obj.get_property_value<std::any>(ctx, "object"));
2✔
2171
            REQUIRE(old_linked.is_valid());
2!
2172
            REQUIRE(old_linked.get_obj() == new_linked.get_obj());
2!
2173
            REQUIRE(new_linked.get_obj().get<int64_t>("value") == 40);
2!
2174
            realm->cancel_transaction();
2✔
2175
        }
2✔
2176

4✔
2177
        SECTION("can set embedded link to null") {
8✔
2178
            realm->begin_transaction();
2✔
2179
            auto old_linked = util::any_cast<Object>(obj.get_property_value<std::any>(ctx, "object"));
2✔
2180
            obj.set_property_value(ctx, "object", std::any());
2✔
2181
            auto new_linked = obj.get_property_value<std::any>(ctx, "object");
2✔
2182
            REQUIRE_FALSE(old_linked.is_valid());
2!
2183
            REQUIRE_FALSE(new_linked.has_value());
2!
2184
            realm->cancel_transaction();
2✔
2185
        }
2✔
2186
    }
8✔
2187

11✔
2188
    SECTION("set_property_value() on list of embedded objects") {
22✔
2189
        auto obj = create(AnyDict{
8✔
2190
            {"_id", INT64_C(1)},
8✔
2191
            {"array", AnyVector{AnyDict{{"value", INT64_C(1)}}, AnyDict{{"value", INT64_C(2)}}}},
8✔
2192
        });
8✔
2193
        List list(realm, obj.get_obj().get_linklist("array"));
8✔
2194
        auto obj2 = create(AnyDict{
8✔
2195
            {"_id", INT64_C(2)},
8✔
2196
            {"array", AnyVector{AnyDict{{"value", INT64_C(1)}}, AnyDict{{"value", INT64_C(2)}}}},
8✔
2197
        });
8✔
2198
        List list2(realm, obj2.get_obj().get_linklist("array"));
8✔
2199

4✔
2200
        SECTION("throws when given a managed object") {
8✔
2201
            realm->begin_transaction();
2✔
2202
            REQUIRE_THROWS_WITH(obj.set_property_value(ctx, "array", std::any{AnyVector{list2.get(0)}}),
2✔
2203
                                "Cannot add an existing managed embedded object to a List.");
2✔
2204
            realm->cancel_transaction();
2✔
2205
        }
2✔
2206

4✔
2207
        SECTION("replaces objects when given a dictionary and CreatePolicy::UpdateAll") {
8✔
2208
            realm->begin_transaction();
2✔
2209
            auto old_obj_1 = list.get(0);
2✔
2210
            auto old_obj_2 = list.get(1);
2✔
2211
            obj.set_property_value(ctx, "array",
2✔
2212
                                   std::any(AnyVector{AnyDict{{"value", INT64_C(1)}}, AnyDict{{"value", INT64_C(2)}},
2✔
2213
                                                      AnyDict{{"value", INT64_C(3)}}}),
2✔
2214
                                   CreatePolicy::UpdateAll);
2✔
2215
            REQUIRE(list.size() == 3);
2!
2216
            REQUIRE_FALSE(old_obj_1.is_valid());
2!
2217
            REQUIRE_FALSE(old_obj_2.is_valid());
2!
2218
            REQUIRE(list.get(0).get<int64_t>("value") == 1);
2!
2219
            REQUIRE(list.get(1).get<int64_t>("value") == 2);
2!
2220
            REQUIRE(list.get(2).get<int64_t>("value") == 3);
2!
2221
            realm->cancel_transaction();
2✔
2222
        }
2✔
2223

4✔
2224
        SECTION("mutates existing objects when given a dictionary and CreatePolicy::UpdateModified") {
8✔
2225
            realm->begin_transaction();
2✔
2226
            auto old_obj_1 = list.get(0);
2✔
2227
            auto old_obj_2 = list.get(1);
2✔
2228
            obj.set_property_value(ctx, "array",
2✔
2229
                                   std::any(AnyVector{AnyDict{{"value", INT64_C(1)}}, AnyDict{{"value", INT64_C(2)}},
2✔
2230
                                                      AnyDict{{"value", INT64_C(3)}}}),
2✔
2231
                                   CreatePolicy::UpdateModified);
2✔
2232
            REQUIRE(list.size() == 3);
2!
2233
            REQUIRE(old_obj_1.is_valid());
2!
2234
            REQUIRE(old_obj_2.is_valid());
2!
2235
            REQUIRE(old_obj_1.get<int64_t>("value") == 1);
2!
2236
            REQUIRE(old_obj_2.get<int64_t>("value") == 2);
2!
2237
            REQUIRE(list.get(2).get<int64_t>("value") == 3);
2!
2238
            realm->cancel_transaction();
2✔
2239
        }
2✔
2240

4✔
2241
        SECTION("clears list when given null") {
8✔
2242
            realm->begin_transaction();
2✔
2243
            obj.set_property_value(ctx, "array", std::any());
2✔
2244
            REQUIRE(list.size() == 0);
2!
2245
            realm->cancel_transaction();
2✔
2246
        }
2✔
2247
    }
8✔
2248

11✔
2249
    SECTION("create with UpdateModified diffs child objects") {
22✔
2250
        auto obj = create(AnyDict{
2✔
2251
            {"_id", INT64_C(1)},
2✔
2252
            {"object", AnyDict{{"value", INT64_C(10)}}},
2✔
2253
            {"array", AnyVector{AnyDict{{"value", INT64_C(20)}}, AnyDict{{"value", INT64_C(30)}}}},
2✔
2254
        });
2✔
2255

1✔
2256
        auto array_table = realm->read_group().get_table("class_array target");
2✔
2257
        Results result(realm, array_table);
2✔
2258

1✔
2259
        bool obj_callback_called = false;
2✔
2260
        auto token = obj.add_notification_callback([&](CollectionChangeSet) {
2✔
2261
            obj_callback_called = true;
2✔
2262
        });
2✔
2263
        bool list_callback_called = false;
2✔
2264
        auto token1 = result.add_notification_callback([&](CollectionChangeSet) {
4✔
2265
            list_callback_called = true;
4✔
2266
        });
4✔
2267
        advance_and_notify(*realm);
2✔
2268

1✔
2269
        // Update with identical value
1✔
2270
        create(
2✔
2271
            AnyDict{
2✔
2272
                {"_id", INT64_C(1)},
2✔
2273
                {"object", AnyDict{{"value", INT64_C(10)}}},
2✔
2274
            },
2✔
2275
            CreatePolicy::UpdateModified);
2✔
2276

1✔
2277
        obj_callback_called = false;
2✔
2278
        list_callback_called = false;
2✔
2279
        advance_and_notify(*realm);
2✔
2280
        REQUIRE(!obj_callback_called);
2!
2281
        REQUIRE(!list_callback_called);
2!
2282

1✔
2283
        // Update with different values
1✔
2284
        create(
2✔
2285
            AnyDict{
2✔
2286
                {"_id", INT64_C(1)},
2✔
2287
                {"array", AnyVector{AnyDict{{"value", INT64_C(40)}}, AnyDict{{"value", INT64_C(50)}}}},
2✔
2288
            },
2✔
2289
            CreatePolicy::UpdateModified);
2✔
2290

1✔
2291
        obj_callback_called = false;
2✔
2292
        list_callback_called = false;
2✔
2293
        advance_and_notify(*realm);
2✔
2294
        REQUIRE(!obj_callback_called);
2!
2295
        REQUIRE(list_callback_called);
2!
2296
    }
2✔
2297

11✔
2298
    SECTION("deleting parent object sends change notification") {
22✔
2299
        auto parent = create(AnyDict{
2✔
2300
            {"_id", INT64_C(1)},
2✔
2301
            {"object", AnyDict{{"value", INT64_C(10)}}},
2✔
2302
            {"array", AnyVector{AnyDict{{"value", INT64_C(20)}}, AnyDict{{"value", INT64_C(30)}}}},
2✔
2303
        });
2✔
2304

1✔
2305
        CppContext ctx(realm);
2✔
2306
        auto child = util::any_cast<Object>(parent.get_property_value<std::any>(ctx, "object"));
2✔
2307

1✔
2308
        int calls = 0;
2✔
2309
        auto token = child.add_notification_callback([&](CollectionChangeSet const& c) {
4✔
2310
            if (++calls == 2) {
4✔
2311
                REQUIRE_INDICES(c.deletions, 0);
2!
2312
            }
2✔
2313
        });
4✔
2314
        advance_and_notify(*realm);
2✔
2315
        REQUIRE(calls == 1);
2!
2316

1✔
2317
        realm->begin_transaction();
2✔
2318
        parent.get_obj().remove();
2✔
2319
        realm->commit_transaction();
2✔
2320
        advance_and_notify(*realm);
2✔
2321
        REQUIRE(calls == 2);
2!
2322
    }
2✔
2323
}
22✔
2324

2325
#if REALM_ENABLE_SYNC
2326

2327
TEST_CASE("Asymmetric Object") {
8✔
2328
    Schema schema{
8✔
2329
        {"asymmetric",
8✔
2330
         ObjectSchema::ObjectType::TopLevelAsymmetric,
8✔
2331
         {{"_id", PropertyType::Int, Property::IsPrimary{true}}}},
8✔
2332
        {"asymmetric_link",
8✔
2333
         ObjectSchema::ObjectType::TopLevelAsymmetric,
8✔
2334
         {
8✔
2335
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
8✔
2336
             {"location", PropertyType::Mixed | PropertyType::Nullable},
8✔
2337
         }},
8✔
2338
        {"table", {{"_id", PropertyType::Int, Property::IsPrimary{true}}}},
8✔
2339
    };
8✔
2340

4✔
2341
    TestSyncManager tsm({}, {/*.start_immediately =*/false});
8✔
2342
    SyncTestFile config(tsm.fake_user(), schema, SyncConfig::FLXSyncEnabled{});
8✔
2343
    config.sync_config->flx_sync_requested = true;
8✔
2344

4✔
2345
    auto realm = Realm::get_shared_realm(config);
8✔
2346
    {
8✔
2347
        auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
8✔
2348
        mut_subs.insert_or_assign(Query(realm->read_group().get_table("class_table")));
8✔
2349
        std::move(mut_subs).commit();
8✔
2350
    }
8✔
2351
    CppContext ctx(realm);
8✔
2352

4✔
2353
    auto create = [&](std::any&& value, std::string table_name, CreatePolicy policy = CreatePolicy::ForceCreate) {
7✔
2354
        realm->begin_transaction();
6✔
2355
        auto obj = Object::create(ctx, realm, *realm->schema().find(table_name), value, policy);
6✔
2356
        realm->commit_transaction();
6✔
2357
        return obj;
6✔
2358
    };
6✔
2359

4✔
2360
    SECTION("Basic object creation") {
8✔
2361
        auto obj = create(AnyDict{{"_id", INT64_C(1)}}, "asymmetric");
2✔
2362
        // Object returned is not valid.
1✔
2363
        REQUIRE(!obj.get_obj().is_valid());
2!
2364
        // Object gets deleted immediately.
1✔
2365
        REQUIRE(realm->is_empty());
2!
2366
    }
2✔
2367

4✔
2368
    SECTION("Re-open realm") {
8✔
2369
        realm->close();
2✔
2370
        realm.reset();
2✔
2371
        realm = Realm::get_shared_realm(config);
2✔
2372
    }
2✔
2373

4✔
2374
    SECTION("Delete ephemeral object before comitting") {
8✔
2375
        realm->begin_transaction();
2✔
2376
        auto obj = realm->read_group().get_table("class_asymmetric")->create_object_with_primary_key(1);
2✔
2377
        obj.remove();
2✔
2378
        realm->commit_transaction();
2✔
2379
        REQUIRE(!obj.is_valid());
2!
2380
        REQUIRE(realm->is_empty());
2!
2381
    }
2✔
2382

4✔
2383
    SECTION("Outgoing link not allowed") {
8✔
2384
        auto obj = create(AnyDict{{"_id", INT64_C(1)}}, "table");
2✔
2385
        auto table = realm->read_group().get_table("class_table");
2✔
2386
        REQUIRE_EXCEPTION(create(
2✔
2387
                              AnyDict{
2✔
2388
                                  {"_id", INT64_C(1)},
2✔
2389
                                  {"location", Mixed(ObjLink{table->get_key(), obj.get_obj().get_key()})},
2✔
2390
                              },
2✔
2391
                              "asymmetric_link"),
2✔
2392
                          IllegalOperation, "Links not allowed in asymmetric tables");
2✔
2393
    }
2✔
2394
}
8✔
2395

2396
TEST_CASE("KeyPath generation - star notation") {
2✔
2397
    Schema schema{
2✔
2398
        {"Person",
2✔
2399
         {
2✔
2400
             {"name", PropertyType::String},
2✔
2401
             {"age", PropertyType::Int},
2✔
2402
             {"children", PropertyType::Object | PropertyType::Array, "Child"},
2✔
2403
         }},
2✔
2404
        {"Child",
2✔
2405
         {
2✔
2406
             {"name", PropertyType::String},
2✔
2407
             {"favoritePet", PropertyType::Object | PropertyType::Nullable, "Pet"},
2✔
2408
         },
2✔
2409
         {
2✔
2410
             {"parent", PropertyType::LinkingObjects | PropertyType::Array, "Person", "children"},
2✔
2411
         }},
2✔
2412
        {"Pet",
2✔
2413
         ObjectSchema::ObjectType::Embedded,
2✔
2414
         {
2✔
2415
             {"name", PropertyType::String},
2✔
2416
             {"breed", PropertyType::String},
2✔
2417
         }},
2✔
2418
    };
2✔
2419
    InMemoryTestFile config;
2✔
2420
    config.automatic_change_notifications = false;
2✔
2421
    config.schema_mode = SchemaMode::Automatic;
2✔
2422
    config.schema = schema;
2✔
2423

1✔
2424
    auto realm = Realm::get_shared_realm(config);
2✔
2425

1✔
2426
    auto kpa = realm->create_key_path_array("Person", {"*.*.*"});
2✔
2427
    CHECK(kpa.size() == 8);
2!
2428
    // {class_Person:name}
1✔
2429
    // {class_Person:age}
1✔
2430
    // {class_Person:children}{class_Child:name}
1✔
2431
    // {class_Person:children}{class_Child:favoritePet}{class_Pet:name}
1✔
2432
    // {class_Person:children}{class_Child:favoritePet}{class_Pet:breed}
1✔
2433
    // {class_Person:children}{class_Child:{class_Person:children}->}{class_Person:name}
1✔
2434
    // {class_Person:children}{class_Child:{class_Person:children}->}{class_Person:age}
1✔
2435
    // {class_Person:children}{class_Child:{class_Person:children}->}{class_Person:children}
1✔
2436
    // realm->print_key_path_array(kpa);
1✔
2437

1✔
2438
    kpa = realm->create_key_path_array("Person", {"*.name"});
2✔
2439
    CHECK(kpa.size() == 1);
2!
2440
    // {class_Person:children}{class_Child:name}
1✔
2441
    // realm->print_key_path_array(kpa);
1✔
2442

1✔
2443
    kpa = realm->create_key_path_array("Person", {"*.*.breed"});
2✔
2444
    CHECK(kpa.size() == 1);
2!
2445
    // {class_Person:children}{class_Child:favoritePet}{class_Pet:breed}
1✔
2446
    // realm->print_key_path_array(kpa);
1✔
2447

1✔
2448
    kpa = realm->create_key_path_array("Child", {"*.name"});
2✔
2449
    CHECK(kpa.size() == 2);
2!
2450
    // {class_Child:favoritePet}{class_Pet:name}
1✔
2451
    // {class_Child:{class_Person:children}->}{class_Person:name}
1✔
2452
    // realm->print_key_path_array(kpa);
1✔
2453

1✔
2454
    kpa = realm->create_key_path_array("Person", {"children.*.breed"});
2✔
2455
    CHECK(kpa.size() == 1);
2!
2456
    // {class_Person:children}{class_Child:favoritePet}{class_Pet:breed}
1✔
2457
    // realm->print_key_path_array(kpa);
1✔
2458

1✔
2459
    CHECK_THROWS_AS(realm->create_key_path_array("Person", {"children.favoritePet.colour"}), InvalidArgument);
2✔
2460
    CHECK_THROWS_AS(realm->create_key_path_array("Person", {"*.myPet.breed"}), InvalidArgument);
2✔
2461
}
2✔
2462

2463
#endif // REALM_ENABLE_SYNC
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