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

realm / realm-core / 2304

10 May 2024 06:50AM UTC coverage: 91.065% (+0.3%) from 90.751%
2304

push

Evergreen

web-flow
RCORE-2030 Add full support for automatic client reset for collections in mixed (#7683)

* Add full support for automatic client reset for collections in mixed

* Changes after code review

* Add more tests for better coverage

* Update changelog

* Add integration test and bump baas version

106666 of 192066 branches covered (55.54%)

1631 of 1648 new or added lines in 6 files covered. (98.97%)

51 existing lines in 11 files now uncovered.

216128 of 237334 relevant lines covered (91.06%)

5454513.65 hits per line

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

89.65
/src/realm/sync/instruction_applier.cpp
1
#include <realm/sync/instruction_applier.hpp>
2
#include <realm/set.hpp>
3
#include <realm/util/scope_exit.hpp>
4

5
#include <realm/transaction.hpp>
6

7
namespace realm::sync {
8
namespace {
9

10
REALM_NORETURN void throw_bad_transaction_log(std::string msg)
11
{
4✔
12
    throw BadChangesetError{std::move(msg)};
4✔
13
}
4✔
14

15
} // namespace
16

17
REALM_NORETURN void InstructionApplier::bad_transaction_log(const std::string& msg) const
18
{
8✔
19
    if (m_last_object_key) {
8✔
20
        // If the last_object_key is valid then we should have a changeset and a current table
4✔
21
        REALM_ASSERT(m_log);
×
22
        REALM_ASSERT(m_last_table_name);
×
23
        std::stringstream ss;
24
        util::Optional<InternString> field_name;
25
        if (m_last_field_name) {
4!
26
            field_name = m_last_field_name;
4✔
27
        }
28
        const instr::Path* cur_path = m_current_path ? &(*m_current_path) : nullptr;
×
29
        m_log->print_path(ss, m_last_table_name, *m_last_object_key, field_name, cur_path);
×
30
        throw_bad_transaction_log(
×
31
            util::format("%1 (instruction target: %2, version: %3, last_integrated_remote_version: %4, "
×
32
                         "origin_file_ident: %5, timestamp: %6)",
×
33
                         msg, ss.str(), m_log->version, m_log->last_integrated_remote_version,
×
34
                         m_log->origin_file_ident, m_log->origin_timestamp));
×
35
    }
×
36
    else if (m_last_table_name) {
4✔
37
        // We should have a changeset if we have a table name defined.
38
        REALM_ASSERT(m_log);
4✔
39
        throw_bad_transaction_log(
4✔
40
            util::format("%1 (instruction table: %2, version: %3, last_integrated_remote_version: %4, "
4✔
41
                         "origin_file_ident: %5, timestamp: %6)",
4✔
42
                         msg, m_log->get_string(m_last_table_name), m_log->version,
4✔
43
                         m_log->last_integrated_remote_version, m_log->origin_file_ident, m_log->origin_timestamp));
8✔
44
    }
4✔
45
    else if (m_log) {
4✔
46
        // If all we have is a changeset, then we should log whatever we can about it.
4✔
47
        throw_bad_transaction_log(util::format("%1 (version: %2, last_integrated_remote_version: %3, "
4✔
48
                                               "origin_file_ident: %4, timestamp: %5)",
4✔
49
                                               msg, m_log->version, m_log->last_integrated_remote_version,
4✔
50
                                               m_log->origin_file_ident, m_log->origin_timestamp));
4✔
51
    }
4✔
52
    throw_bad_transaction_log(std::move(msg));
4!
53
}
4✔
54

55
template <class... Params>
56
REALM_NORETURN void InstructionApplier::bad_transaction_log(const char* msg, Params&&... params) const
57
{
4✔
58
    // FIXME: Avoid throwing in normal program flow (since changesets can come
59
    // in over the network, defective changesets are part of normal program
4✔
60
    // flow).
4✔
61
    bad_transaction_log(util::format(msg, std::forward<Params>(params)...));
4✔
62
}
4✔
63

64
StringData InstructionApplier::get_string(InternString str) const
4✔
65
{
363,402✔
66
    auto string = m_log->try_get_intern_string(str);
363,402✔
67
    if (REALM_UNLIKELY(!string))
363,402✔
68
        bad_transaction_log("string read fails");
4✔
69
    return m_log->get_string(*string);
363,406✔
70
}
363,402✔
71

72
StringData InstructionApplier::get_string(StringBufferRange range) const
348,828✔
73
{
443,258✔
74
    auto string = m_log->try_get_string(range);
443,258✔
75
    if (!string)
94,430✔
76
        bad_transaction_log("string read error");
348,828✔
77
    return *string;
443,258✔
78
}
94,430✔
79

80
BinaryData InstructionApplier::get_binary(StringBufferRange range) const
81,686✔
81
{
87,548✔
82
    auto string = m_log->try_get_string(range);
87,548✔
83
    if (!string)
5,862✔
84
        bad_transaction_log("binary read error");
81,686✔
85
    return BinaryData{string->data(), string->size()};
87,548✔
86
}
5,862✔
87

88
TableRef InstructionApplier::table_for_class_name(StringData class_name) const
5,862✔
89
{
5,918✔
90
    if (class_name.size() > Group::max_class_name_length)
5,918✔
91
        bad_transaction_log("class name too long");
×
92
    Group::TableNameBuffer buffer;
5,918✔
93
    return m_transaction.get_table(Group::class_name_to_table_name(class_name, buffer));
5,918✔
94
}
56✔
95

96
template <typename T>
56✔
97
struct TemporarySwapOut {
56✔
98
    explicit TemporarySwapOut(T& target)
99
        : target(target)
56✔
100
        , backup()
56✔
101
    {
7,168✔
102
        std::swap(target, backup);
7,112✔
103
    }
7,112✔
104

105
    ~TemporarySwapOut()
106
    {
13,426✔
107
        std::swap(backup, target);
13,426✔
108
    }
13,426✔
109

6,314✔
110
    T& target;
6,314✔
111
    T backup;
112
};
113

6,314✔
114
void InstructionApplier::operator()(const Instruction::AddTable& instr)
6,314✔
115
{
8,622✔
116
    auto table_name = get_table_name(instr);
2,308✔
117

118
    // Temporarily swap out the last object key so it doesn't get included in error messages
119
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
2,308✔
120

121
    auto add_table = util::overload{
2,308✔
122
        [&](const Instruction::AddTable::TopLevelTable& spec) {
4,500✔
123
            auto table_type = (spec.is_asymmetric ? Table::Type::TopLevelAsymmetric : Table::Type::TopLevel);
4,350✔
124
            if (spec.pk_type == Instruction::Payload::Type::GlobalKey) {
2,158✔
125
                m_transaction.get_or_add_table(table_name, table_type);
4✔
126
            }
2,196✔
127
            else {
2,154✔
128
                if (!is_valid_key_type(spec.pk_type)) {
4,346✔
129
                    bad_transaction_log("Invalid primary key type '%1' while adding table '%2'", int8_t(spec.pk_type),
2,192✔
130
                                        table_name);
2,044✔
131
                }
2,044✔
132
                DataType pk_type = get_data_type(spec.pk_type);
2,158✔
133
                StringData pk_field = get_string(spec.pk_field);
2,158✔
134
                bool nullable = spec.pk_nullable;
4,194✔
135

2,040✔
136
                if (!m_transaction.get_or_add_table_with_primary_key(table_name, pk_type, pk_field, nullable,
2,154✔
137
                                                                     table_type)) {
2,154✔
138
                    bad_transaction_log("AddTable: The existing table '%1' has different properties", table_name);
×
139
                }
2,040✔
140
            }
4,194✔
141
        },
4,198✔
142
        [&](const Instruction::AddTable::EmbeddedTable&) {
2,308✔
143
            if (TableRef table = m_transaction.get_table(table_name)) {
2,188✔
144
                if (!table->is_embedded()) {
2,040!
145
                    bad_transaction_log("AddTable: The existing table '%1' is not embedded", table_name);
×
146
                }
×
147
            }
2,040✔
148
            else {
2,192✔
149
                m_transaction.add_table(table_name, Table::Type::Embedded);
2,340✔
150
            }
296✔
151
        },
148!
152
    };
2,308✔
153

154
    mpark::visit(std::move(add_table), instr.type);
2,308✔
155
}
2,456✔
156

148✔
157
void InstructionApplier::operator()(const Instruction::EraseTable& instr)
148✔
158
{
240✔
159
    auto table_name = get_table_name(instr);
2,284✔
160
    // Temporarily swap out the last object key so it doesn't get included in error messages
161
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
2,284✔
162

2,192✔
163
    if (REALM_UNLIKELY(REALM_COVER_NEVER(!m_transaction.has_table(table_name)))) {
92✔
164
        // FIXME: Should EraseTable be considered idempotent?
165
        bad_transaction_log("table does not exist");
110✔
166
    }
110✔
167

168
    m_transaction.remove_table(table_name);
202✔
169
}
92✔
170

110✔
171
void InstructionApplier::operator()(const Instruction::CreateObject& instr)
172
{
23,538✔
173
    auto table = get_table(instr);
23,538✔
174
    ColKey pk_col = table->get_primary_key_column();
23,538✔
175
    m_last_object_key = instr.object;
23,648✔
176

110✔
177
    mpark::visit(util::overload{
23,538✔
178
                     [&](mpark::monostate) {
23,538✔
179
                         if (!pk_col) {
22,632✔
180
                             bad_transaction_log("CreateObject(NULL) on table without a primary key");
22,616✔
181
                         }
22,616✔
182
                         if (!table->is_nullable(pk_col)) {
22,632✔
183
                             bad_transaction_log("CreateObject(NULL) on a table with a non-nullable primary key");
184
                         }
22,616✔
185
                         m_last_object = table->create_object_with_primary_key(util::none);
22,632✔
186
                     },
32✔
187
                     [&](int64_t pk) {
23,538✔
188
                         if (!pk_col) {
18,640✔
189
                             bad_transaction_log("CreateObject(Int) on table without a primary key");
16✔
190
                         }
×
191
                         if (table->get_column_type(pk_col) != type_Int) {
18,640✔
192
                             bad_transaction_log("CreateObject(Int) on a table with primary key type %1",
16✔
193
                                                 table->get_column_type(pk_col));
16✔
194
                         }
22,616✔
195
                         m_last_object = table->create_object_with_primary_key(pk);
37,336✔
196
                     },
18,640✔
197
                     [&](InternString pk) {
23,538✔
198
                         if (!pk_col) {
19,274✔
199
                             bad_transaction_log("CreateObject(String) on table without a primary key");
×
200
                         }
×
201
                         if (table->get_column_type(pk_col) != type_String) {
578✔
202
                             bad_transaction_log("CreateObject(String) on a table with primary key type %1",
18,696✔
203
                                                 table->get_column_type(pk_col));
18,696✔
204
                         }
22,616✔
205
                         StringData str = get_string(pk);
906✔
206
                         m_last_object = table->create_object_with_primary_key(str);
578✔
207
                     },
578✔
208
                     [&](const ObjectId& id) {
23,866✔
209
                         if (!pk_col) {
4,274✔
210
                             bad_transaction_log("CreateObject(ObjectId) on table without a primary key");
×
211
                         }
×
212
                         if (table->get_column_type(pk_col) != type_ObjectId) {
4,602✔
213
                             bad_transaction_log("CreateObject(ObjectId) on a table with primary key type %1",
328✔
214
                                                 table->get_column_type(pk_col));
328✔
215
                         }
22,616✔
216
                         m_last_object = table->create_object_with_primary_key(id);
7,820✔
217
                     },
4,274✔
218
                     [&](const UUID& id) {
23,538✔
219
                         if (!pk_col) {
3,570✔
220
                             bad_transaction_log("CreateObject(UUID) on table without a primary key");
×
221
                         }
×
222
                         if (table->get_column_type(pk_col) != type_UUID) {
24✔
223
                             bad_transaction_log("CreateObject(UUID) on a table with primary key type %1",
3,546✔
224
                                                 table->get_column_type(pk_col));
3,546✔
225
                         }
22,616✔
226
                         m_last_object = table->create_object_with_primary_key(id);
48✔
227
                     },
24✔
228
                     [&](GlobalKey key) {
23,538✔
229
                         if (pk_col) {
30✔
230
                             bad_transaction_log("CreateObject(GlobalKey) on table with a primary key");
×
231
                         }
×
232
                         m_last_object = table->create_object(key);
6✔
233
                     },
30✔
234
                 },
23,562✔
235
                 instr.object);
46,154✔
236
}
23,544✔
237

238
void InstructionApplier::operator()(const Instruction::EraseObject& instr)
239
{
1,836✔
240
    // FIXME: Log actions.
6✔
241
    // Note: EraseObject is idempotent.
22,616✔
242
    if (auto obj = get_top_object(instr, "EraseObject")) {
24,446✔
243
        // This call will prevent incoming links to be nullified/deleted
22,616✔
244
        obj->invalidate();
1,152✔
245
    }
1,152✔
246
    m_last_object.reset();
3,644✔
247
}
1,830✔
248

249
template <class F>
1,814✔
250
void InstructionApplier::visit_payload(const Instruction::Payload& payload, F&& visitor)
251
{
326,036✔
252
    using Type = Instruction::Payload::Type;
326,036✔
253

1,814✔
254
    const auto& data = payload.data;
326,742✔
255
    switch (payload.type) {
324,928✔
256
        case Type::ObjectValue:
1,338✔
257
            return visitor(Instruction::Payload::ObjectValue{});
1,338✔
258
        case Type::Set:
311,394✔
259
            return visitor(Instruction::Payload::Set{});
311,394✔
260
        case Type::List:
402✔
261
            return visitor(Instruction::Payload::List{});
311,796✔
262
        case Type::Dictionary:
311,742✔
263
            return visitor(Instruction::Payload::Dictionary{});
1,686✔
264
        case Type::Erased:
1,456✔
265
            return visitor(Instruction::Payload::Erased{});
118✔
266
        case Type::GlobalKey:
✔
267
            return visitor(realm::util::none); // FIXME: Not sure about this
412✔
268
        case Type::Null:
606✔
269
            return visitor(realm::util::none);
542✔
270
        case Type::Int:
217,998✔
271
            return visitor(data.integer);
217,770✔
272
        case Type::Bool:
212✔
273
            return visitor(data.boolean);
92✔
274
        case Type::String: {
94,430✔
275
            StringData value = get_string(data.str);
94,634✔
276
            return visitor(value);
94,634✔
277
        }
217,622✔
278
        case Type::Binary: {
223,484✔
279
            BinaryData value = get_binary(data.binary);
5,954✔
280
            return visitor(value);
5,954✔
281
        }
81,686✔
282
        case Type::Timestamp:
83,630✔
283
            return visitor(data.timestamp);
83,630✔
284
        case Type::Float:
130✔
285
            return visitor(data.fnum);
5,992✔
286
        case Type::Double:
6,458✔
287
            return visitor(data.dnum);
6,458✔
288
        case Type::Decimal:
84✔
289
            return visitor(data.decimal);
1,242✔
290
        case Type::Link: {
2,182✔
291
            StringData class_name = get_string(data.link.target_table);
1,154✔
292
            Group::TableNameBuffer buffer;
1,154✔
293
            StringData target_table_name = Group::class_name_to_table_name(class_name, buffer);
1,620✔
294
            TableRef target_table = m_transaction.get_table(target_table_name);
1,620✔
295
            if (!target_table) {
1,108✔
296
                bad_transaction_log("Link with invalid target table '%1'", target_table_name);
84✔
297
            }
1,024✔
298
            if (target_table->is_embedded()) {
2,048✔
299
                bad_transaction_log("Link to embedded table '%1'", target_table_name);
1,024✔
300
            }
1,024✔
301
            ObjKey target = get_object_key(*target_table, data.link.target);
2,048✔
302
            ObjLink link = ObjLink{target_table->get_key(), target};
2,048✔
303
            return visitor(link);
1,024✔
304
        }
×
305
        case Type::ObjectId:
1,588✔
306
            return visitor(data.object_id);
564✔
307
        case Type::UUID:
152✔
308
            return visitor(data.uuid);
1,176✔
309
    }
325,952✔
310
}
325,952✔
311

312
void InstructionApplier::operator()(const Instruction::Update& instr)
564✔
313
{
242,586✔
314
    struct UpdateResolver : public PathResolver {
242,174✔
315
        UpdateResolver(InstructionApplier* applier, const Instruction::Update& instr)
242,174✔
316
            : PathResolver(applier, instr, "Update")
553,416✔
317
            , m_instr(instr)
553,416✔
318
        {
242,022✔
319
        }
242,020✔
320
        void on_property(Obj& obj, ColKey col) override
470,860✔
321
        {
470,860✔
322
            // Update of object field.
228,838✔
323

228,838✔
324
            auto table = obj.get_table();
468,702✔
325
            auto table_name = table->get_name();
468,702✔
326
            auto field_name = table->get_column_name(col);
468,700✔
327
            auto data_type = DataType(col.get_type());
468,702✔
328

228,838✔
329
            auto visitor = [&](const mpark::variant<ObjLink, Mixed, Instruction::Payload::ObjectValue,
239,864✔
330
                                                    Instruction::Payload::Dictionary, Instruction::Payload::List,
239,864✔
331
                                                    Instruction::Payload::Set, Instruction::Payload::Erased>& arg) {
466,590✔
332
                if (const auto link_ptr = mpark::get_if<ObjLink>(&arg)) {
466,590✔
333
                    if (data_type == type_Mixed || data_type == type_TypedLink) {
226,768✔
334
                        obj.set_any(col, *link_ptr, m_instr.is_default);
226,726✔
335
                    }
336
                    else if (data_type == type_Link) {
226,768✔
337
                        // Validate target table.
226,726✔
338
                        auto target_table = table->get_link_target(col);
226,780✔
339
                        if (target_table->get_key() != link_ptr->get_table_key()) {
226,780✔
340
                            m_applier->bad_transaction_log(
42✔
341
                                "Update: Target table mismatch (expected %1, got %2)", target_table->get_name(),
×
342
                                m_applier->m_transaction.get_table(link_ptr->get_table_key())->get_name());
×
343
                        }
42✔
344
                        obj.set<ObjKey>(col, link_ptr->get_obj_key(), m_instr.is_default);
42✔
345
                    }
84✔
346
                    else {
42✔
347
                        m_applier->bad_transaction_log("Update: Type mismatch in '%2.%1' (expected %3, got %4)",
×
348
                                                       field_name, table_name, col.get_type(), type_Link);
×
349
                    }
×
350
                }
42✔
351
                else if (const auto mixed_ptr = mpark::get_if<Mixed>(&arg)) {
239,864✔
352
                    if (mixed_ptr->is_null()) {
239,132✔
353
                        if (col.is_nullable()) {
104✔
354
                            obj.set_null(col, m_instr.is_default);
104✔
355
                        }
104✔
356
                        else {
×
357
                            m_applier->bad_transaction_log("Update: NULL in non-nullable field '%2.%1'", field_name,
42✔
358
                                                           table_name);
226,696✔
359
                        }
225,960✔
360
                    }
218✔
361
                    else if (data_type == type_Mixed || mixed_ptr->get_type() == data_type) {
239,102✔
362
                        obj.set_any(col, *mixed_ptr, m_instr.is_default);
239,102✔
363
                    }
238,988✔
364
                    else {
2,147,483,647✔
365
                        m_applier->bad_transaction_log("Update: Type mismatch in '%2.%1' (expected %3, got %4)",
2,147,483,647✔
366
                                                       field_name, table_name, col.get_type(), mixed_ptr->get_type());
2,147,483,647✔
367
                    }
2,147,483,761✔
368
                }
464,940✔
369
                else if (const auto obj_val_ptr = mpark::get_if<Instruction::Payload::ObjectValue>(&arg)) {
226,582✔
370
                    if (obj.is_null(col)) {
226,176✔
371
                        obj.create_and_set_linked_object(col);
2,147,483,959✔
372
                    }
2,147,483,959✔
373
                }
2,147,483,973✔
374
                else if (const auto erase_ptr = mpark::get_if<Instruction::Payload::Erased>(&arg)) {
2,147,484,053✔
375
                    m_applier->bad_transaction_log("Update: Dictionary erase at object field");
225,960✔
376
                }
736✔
377
                else if (mpark::get_if<Instruction::Payload::Dictionary>(&arg)) {
732✔
378
                    obj.set_collection(col, CollectionType::Dictionary);
492✔
379
                }
492✔
380
                else if (mpark::get_if<Instruction::Payload::List>(&arg)) {
552✔
381
                    obj.set_collection(col, CollectionType::List);
630✔
382
                }
220✔
383
                else if (mpark::get_if<Instruction::Payload::Set>(&arg)) {
6✔
384
                    obj.set_collection(col, CollectionType::Set);
410✔
385
                }
180✔
386
            };
240,044✔
387

230✔
388
            m_applier->visit_payload(m_instr.value, visitor);
240,088✔
389
        }
240,088✔
390
        Status on_list_index(LstBase& list, uint32_t index) override
242,028✔
391
        {
242,022✔
392
            // Update of list element.
393

226,738✔
394
            auto col = list.get_col_key();
256✔
395
            auto data_type = DataType(col.get_type());
226,982✔
396
            auto table = list.get_table();
226,982✔
397
            auto table_name = table->get_name();
226,982✔
398
            auto field_name = table->get_column_name(col);
229,094✔
399

228,838✔
400
            auto visitor = util::overload{
256✔
401
                [&](const ObjLink& link) {
256✔
402
                    if (data_type == type_TypedLink) {
272✔
403
                        REALM_ASSERT(dynamic_cast<Lst<ObjLink>*>(&list));
194!
404
                        auto& link_list = static_cast<Lst<ObjLink>&>(list);
194✔
405
                        link_list.set(index, link);
194✔
406
                    }
194✔
407
                    else if (data_type == type_Mixed) {
78✔
408
                        REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
194!
409
                        auto& mixed_list = static_cast<Lst<Mixed>&>(list);
194✔
410
                        mixed_list.set(index, link);
78✔
411
                    }
×
412
                    else if (data_type == type_Link) {
78✔
413
                        REALM_ASSERT(dynamic_cast<Lst<ObjKey>*>(&list));
78✔
414
                        auto& link_list = static_cast<Lst<ObjKey>&>(list);
78✔
415
                        // Validate the target.
78✔
416
                        auto target_table = table->get_link_target(col);
78!
417
                        if (target_table->get_key() != link.get_table_key()) {
78✔
418
                            m_applier->bad_transaction_log(
×
419
                                "Update: Target table mismatch (expected '%1', got '%2')", target_table->get_name(),
×
420
                                m_applier->m_transaction.get_table(link.get_table_key())->get_name());
78✔
421
                        }
78✔
422
                        link_list.set(index, link.get_obj_key());
156✔
423
                    }
78✔
424
                    else {
78✔
425
                        m_applier->bad_transaction_log(
78✔
426
                            "Update: Type mismatch in list at '%2.%1' (expected link type, was %3)", field_name,
×
427
                            table_name, data_type);
×
428
                    }
×
429
                },
78✔
430
                [&](Mixed value) {
334✔
431
                    if (value.is_null()) {
222✔
432
                        if (col.is_nullable()) {
×
433
                            list.set_null(index);
×
434
                        }
×
435
                        else {
×
436
                            m_applier->bad_transaction_log("Update: NULL in non-nullable list '%2.%1'", field_name,
×
437
                                                           table_name);
78✔
438
                        }
194✔
439
                    }
82✔
440
                    else {
144!
441
                        if (data_type == type_Mixed || value.get_type() == data_type) {
144✔
442
                            list.set_any(index, value);
144✔
443
                        }
144✔
444
                        else {
×
445
                            m_applier->bad_transaction_log(
×
446
                                "Update: Type mismatch in list at '%2.%1' (expected %3, got %4)", field_name,
×
447
                                table_name, data_type, value.get_type());
×
448
                        }
82✔
449
                    }
226✔
450
                },
226✔
451
                [&](const Instruction::Payload::ObjectValue&) {
338✔
452
                    // Embedded object creation is idempotent, and link lists cannot
453
                    // contain nulls, so this is a no-op.
454
                },
×
455
                [&](const Instruction::Payload::Dictionary&) {
256✔
456
                    list.set_collection(size_t(index), CollectionType::Dictionary);
8✔
457
                },
90✔
458
                [&](const Instruction::Payload::List&) {
338✔
459
                    list.set_collection(size_t(index), CollectionType::List);
220✔
460
                },
26✔
461
                [&](const Instruction::Payload::Set&) {
256✔
462
                    list.set_collection(size_t(index), CollectionType::Set);
×
463
                },
194✔
464
                [&](const Instruction::Payload::Erased&) {
264✔
465
                    m_applier->bad_transaction_log("Update: Dictionary erase of list element");
8✔
466
                },
194✔
467
            };
282✔
468

26✔
469
            m_applier->visit_payload(m_instr.value, visitor);
450✔
470
            return Status::Pending;
256✔
471
        }
256✔
472
        Status on_dictionary_key(Dictionary& dict, Mixed key) override
242,216✔
473
        {
242,022✔
474
            // Update (insert) of dictionary element.
475

194✔
476
            auto visitor = util::overload{
1,902✔
477
                [&](Mixed value) {
2,096✔
478
                    if (value.is_null()) {
1,580✔
479
                        // FIXME: Separate handling of NULL is needed because
194✔
480
                        // `Mixed::get_type()` asserts on NULL.
228,838✔
481
                        dict.insert(key, value);
228,894✔
482
                    }
56✔
483
                    else if (value.get_type() == type_Link) {
1,330✔
484
                        m_applier->bad_transaction_log("Update: Untyped links are not supported in dictionaries.");
1,914✔
485
                    }
1,914✔
486
                    else {
2,724✔
487
                        dict.insert(key, value);
1,330✔
488
                    }
1,330✔
489
                },
1,442✔
490
                [&](const Instruction::Payload::Erased&) {
1,958✔
491
                    dict.try_erase(key);
1,456✔
492
                },
118✔
493
                [&](const Instruction::Payload::ObjectValue&) {
1,902✔
494
                    dict.create_and_insert_linked_object(key);
1,520✔
495
                },
1,520✔
496
                [&](const Instruction::Payload::Dictionary&) {
3,240✔
497
                    dict.insert_collection(key.get_string(), CollectionType::Dictionary);
1,484✔
498
                },
2,004✔
499
                [&](const Instruction::Payload::List&) {
2,022✔
500
                    dict.insert_collection(key.get_string(), CollectionType::List);
246✔
501
                },
2,040✔
502
                [&](const Instruction::Payload::Set&) {
2,084✔
503
                    dict.insert_collection(key.get_string(), CollectionType::Set);
182✔
504
                },
1,914✔
505
            };
1,992✔
506

90✔
507
            m_applier->visit_payload(m_instr.value, visitor);
3,816✔
508
            return Status::Pending;
2,030✔
509
        }
2,030✔
510

1,914✔
511
    private:
242,022✔
512
        const Instruction::Update& m_instr;
242,022✔
513
    };
243,936✔
514
    UpdateResolver resolver(this, instr);
242,022✔
515
    resolver.resolve();
243,936✔
516
}
243,936✔
517

1,914✔
518
void InstructionApplier::operator()(const Instruction::AddInteger& instr)
519
{
230,192✔
520
    // FIXME: Implement increments of array elements, dictionary values.
228,838✔
521
    struct AddIntegerResolver : public PathResolver {
230,192✔
522
        AddIntegerResolver(InstructionApplier* applier, const Instruction::AddInteger& instr)
230,192✔
523
            : PathResolver(applier, instr, "AddInteger")
230,192✔
524
            , m_instr(instr)
230,192✔
525
        {
1,354✔
526
        }
1,354✔
527
        void on_property(Obj& obj, ColKey col)
2,680✔
528
        {
1,354✔
529
            // Increment of object field.
1,326✔
530
            if (!obj.is_null(col)) {
2,680✔
531
                try {
2,652✔
532
                    obj.add_int(col, m_instr.value);
2,652✔
533
                }
2,652✔
534
                catch (const LogicError&) {
2,652✔
535
                    auto table = obj.get_table();
1,326✔
536
                    m_applier->bad_transaction_log("AddInteger: Not an integer field '%2.%1'",
1,326✔
537
                                                   table->get_column_name(col), table->get_name());
538
                }
1,326✔
539
            }
2,624✔
540
        }
2,652✔
541

1,298✔
542
    private:
2,652✔
543
        const Instruction::AddInteger& m_instr;
1,354✔
544
    };
1,354✔
545
    AddIntegerResolver resolver(this, instr);
1,354✔
546
    resolver.resolve();
1,354✔
547
}
2,652✔
548

1,326✔
549
void InstructionApplier::operator()(const Instruction::AddColumn& instr)
1,326✔
550
{
4,712✔
551
    using Type = Instruction::Payload::Type;
6,038✔
552
    using CollectionType = Instruction::CollectionType;
6,038✔
553

1,326✔
554
    // Temporarily swap out the last object key so it doesn't get included in error messages
1,326✔
555
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
6,038✔
556

1,326✔
557
    auto table = get_table(instr, "AddColumn");
4,712✔
558
    auto col_name = get_string(instr.field);
4,712✔
559

4,012✔
560
    if (ColKey existing_key = table->get_column_key(col_name)) {
8,724✔
561
        DataType new_type = get_data_type(instr.type);
4,170✔
562
        ColumnType existing_type = existing_key.get_type();
158✔
563
        if (existing_type != ColumnType(new_type)) {
158✔
564
            bad_transaction_log("AddColumn: Schema mismatch for existing column in '%1.%2' (expected %3, got %4)",
4,016✔
565
                                table->get_name(), col_name, existing_type, new_type);
4✔
566
        }
4,016✔
567
        bool existing_is_list = existing_key.is_list();
4,170✔
568
        if ((instr.collection_type == CollectionType::List) != existing_is_list) {
158✔
569
            bad_transaction_log(
4,012✔
570
                "AddColumn: Schema mismatch for existing column in '%1.%2' (existing is%3 a list, the other is%4)",
178✔
571
                table->get_name(), col_name, existing_is_list ? "" : " not", existing_is_list ? " not" : "");
178!
572
        }
178✔
573
        bool existing_is_set = existing_key.is_set();
162✔
574
        if ((instr.collection_type == CollectionType::Set) != existing_is_set) {
162✔
575
            bad_transaction_log(
4✔
576
                "AddColumn: Schema mismatch for existing column in '%1.%2' (existing is%3 a set, the other is%4)",
178✔
577
                table->get_name(), col_name, existing_is_set ? "" : " not", existing_is_set ? " not" : "");
178!
578
        }
×
579
        bool existing_is_dict = existing_key.is_dictionary();
158✔
580
        if ((instr.collection_type == CollectionType::Dictionary) != existing_is_dict) {
158!
581
            bad_transaction_log("AddColumn: Schema mismatch for existing column in '%1.%2' (existing is%3 a "
×
582
                                "dictionary, the other is%4)",
178✔
583
                                table->get_name(), col_name, existing_is_dict ? "" : " not",
178✔
584
                                existing_is_dict ? " not" : "");
×
585
        }
×
586
        if (new_type == type_Link) {
158!
587
            Group::TableNameBuffer buffer;
8✔
588
            auto target_table_name = Group::class_name_to_table_name(get_string(instr.link_target_table), buffer);
186✔
589
            if (target_table_name != table->get_link_target(existing_key)->get_name()) {
186✔
590
                bad_transaction_log("AddColumn: Schema mismatch for existing column in '%1.%2' (link targets differ)",
×
591
                                    table->get_name(), col_name);
×
592
            }
×
593
        }
8!
594
        return;
158✔
595
    }
336✔
596

8✔
597
    if (instr.collection_type == CollectionType::Dictionary && instr.key_type != Type::String) {
4,562✔
598
        bad_transaction_log("AddColumn '%1.%3' adding dictionary column with non-string keys", table->get_name(),
8✔
599
                            col_name);
×
600
    }
×
601

602
    if (instr.type != Type::Link) {
4,562✔
603
        DataType type = get_data_type(instr.type);
4,072✔
604
        switch (instr.collection_type) {
4,072✔
605
            case CollectionType::Single: {
3,462✔
606
                table->add_column(type, col_name, instr.nullable);
7,296✔
607
                break;
3,462✔
608
            }
×
609
            case CollectionType::List: {
312✔
610
                table->add_column_list(type, col_name, instr.nullable);
312✔
611
                break;
4,146✔
612
            }
3,166✔
613
            case CollectionType::Dictionary: {
3,218✔
614
                DataType key_type = get_data_type(instr.key_type);
2,710✔
615
                table->add_column_dictionary(type, col_name, instr.nullable, key_type);
2,710✔
616
                break;
2,710✔
617
            }
×
618
            case CollectionType::Set: {
406✔
619
                table->add_column_set(type, col_name, instr.nullable);
406✔
620
                break;
406✔
621
            }
×
622
        }
3,996✔
623
    }
3,996✔
624
    else {
762✔
625
        Group::TableNameBuffer buffer;
762✔
626
        auto target_table_name = get_string(instr.link_target_table);
660✔
627
        if (target_table_name.size() != 0) {
728✔
628
            TableRef target = m_transaction.get_table(Group::class_name_to_table_name(target_table_name, buffer));
728✔
629
            if (!target) {
728✔
630
                bad_transaction_log("AddColumn(Link) '%1.%2' to table '%3' which doesn't exist", table->get_name(),
×
631
                                    col_name, target_table_name);
3,166✔
632
            }
3,166✔
633
            if (instr.collection_type == CollectionType::List) {
1,328✔
634
                table->add_column_list(*target, col_name);
970✔
635
            }
970✔
636
            else if (instr.collection_type == CollectionType::Set) {
1,026✔
637
                table->add_column_set(*target, col_name);
672✔
638
            }
672✔
639
            else if (instr.collection_type == CollectionType::Dictionary) {
354✔
640
                table->add_column_dictionary(*target, col_name);
16✔
641
            }
16✔
642
            else {
1,006✔
643
                REALM_ASSERT(instr.collection_type == CollectionType::Single);
644✔
644
                table->add_column(*target, col_name);
644✔
645
            }
700✔
646
        }
664✔
647
        else {
4✔
648
            if (instr.collection_type == CollectionType::List) {
358✔
649
                table->add_column_list(type_TypedLink, col_name);
16✔
650
            }
16✔
651
            else {
342✔
652
                REALM_ASSERT(instr.collection_type == CollectionType::Single);
342✔
653
                table->add_column(type_TypedLink, col_name);
342✔
654
            }
342✔
655
        }
668✔
656
    }
660✔
657
}
4,554!
658

659
void InstructionApplier::operator()(const Instruction::EraseColumn& instr)
660
{
×
661
    // Temporarily swap out the last object key so it doesn't get included in error messages
×
662
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
×
663

664
    auto table = get_table(instr, "EraseColumn");
×
665
    auto col_name = get_string(instr.field);
668✔
666

3,834✔
667
    ColKey col = table->get_column_key(col_name);
668
    if (!col) {
×
669
        bad_transaction_log("EraseColumn '%1.%2' which doesn't exist", table->get_name(), col_name);
×
670
    }
671

672
    table->remove_column(col);
673
}
×
674

675
void InstructionApplier::operator()(const Instruction::ArrayInsert& instr)
676
{
81,776✔
677
    struct ArrayInsertResolver : public PathResolver {
81,776!
678
        ArrayInsertResolver(InstructionApplier* applier, const Instruction::ArrayInsert& instr)
81,776✔
679
            : PathResolver(applier, instr, "ArrayInsert")
81,776✔
680
            , m_instr(instr)
81,776✔
681
        {
81,776✔
682
        }
81,776✔
683
        Status on_list_index(LstBase& list, uint32_t index) override
81,776✔
684
        {
81,776✔
685
            auto data_type = list.get_data_type();
163,194✔
686
            auto table = list.get_table();
163,194✔
687
            auto table_name = table->get_name();
163,194✔
688
            auto field_name = [&] {
163,194✔
689
                return table->get_column_name(list.get_col_key());
81,418✔
690
            };
81,418✔
691

81,418✔
692
            if (index > m_instr.prior_size) {
163,194✔
693
                m_applier->bad_transaction_log("ArrayInsert: Invalid insertion index (index = %1, prior_size = %2)",
81,418✔
694
                                               index, m_instr.prior_size);
81,418✔
695
            }
81,418✔
696

81,418✔
697
            if (index > list.size()) {
163,194✔
698
                m_applier->bad_transaction_log("ArrayInsert: Index out of bounds (%1 > %2)", index, list.size());
×
699
            }
×
700

701
            if (m_instr.prior_size != list.size()) {
163,194✔
702
                m_applier->bad_transaction_log("ArrayInsert: Invalid prior_size (list size = %1, prior_size = %2)",
×
703
                                               list.size(), m_instr.prior_size);
×
704
            }
×
705

706
            auto inserter = util::overload{
163,194✔
707
                [&](const ObjLink& link) {
81,776✔
708
                    if (data_type == type_TypedLink) {
626✔
709
                        REALM_ASSERT(dynamic_cast<Lst<ObjLink>*>(&list));
×
710
                        auto& link_list = static_cast<Lst<ObjLink>&>(list);
81,418✔
711
                        link_list.insert(index, link);
×
712
                    }
×
713
                    else if (data_type == type_Mixed) {
626✔
714
                        REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
82✔
715
                        auto& mixed_list = static_cast<Lst<Mixed>&>(list);
81,500✔
716
                        mixed_list.insert(index, link);
81,500✔
717
                    }
708✔
718
                    else if (data_type == type_Link) {
544✔
719
                        REALM_ASSERT(dynamic_cast<Lst<ObjKey>*>(&list));
544✔
720
                        auto& link_list = static_cast<Lst<ObjKey>&>(list);
544✔
721
                        // Validate the target.
722
                        auto target_table = table->get_link_target(list.get_col_key());
1,170✔
723
                        if (target_table->get_key() != link.get_table_key()) {
626✔
724
                            m_applier->bad_transaction_log(
82✔
725
                                "ArrayInsert: Target table mismatch (expected '%1', got '%2')",
82✔
726
                                target_table->get_name(),
82✔
727
                                m_applier->m_transaction.get_table(link.get_table_key())->get_name());
544✔
728
                        }
544✔
729
                        link_list.insert(index, link.get_obj_key());
1,088✔
730
                    }
544✔
731
                    else {
544✔
732
                        m_applier->bad_transaction_log(
544✔
733
                            "ArrayInsert: Type mismatch in list at '%2.%1' (expected link type, was %3)",
×
734
                            field_name(), table_name, data_type);
×
735
                    }
×
736
                },
626✔
737
                [&](Mixed value) {
81,776✔
738
                    if (data_type == type_Mixed) {
80,764✔
739
                        list.insert_any(index, value);
862✔
740
                    }
318✔
741
                    else if (value.is_null()) {
79,902✔
742
                        if (list.get_col_key().is_nullable()) {
24✔
743
                            list.insert_null(index);
24✔
744
                        }
24✔
745
                        else {
626✔
746
                            m_applier->bad_transaction_log("ArrayInsert: NULL in non-nullable list '%2.%1'",
81,418✔
747
                                                           field_name(), table_name);
79,858✔
748
                        }
330✔
749
                    }
354✔
750
                    else {
159,406✔
751
                        if (value.get_type() == data_type) {
79,902✔
752
                            list.insert_any(index, value);
79,902✔
753
                        }
79,902✔
754
                        else {
×
755
                            m_applier->bad_transaction_log(
×
756
                                "ArrayInsert: Type mismatch in list at '%2.%1' (expected %3, got %4)", field_name(),
×
757
                                table_name, data_type, value.get_type());
×
758
                        }
24✔
759
                    }
159,382✔
760
                },
159,724✔
761
                [&](const Instruction::Payload::ObjectValue&) {
161,280✔
762
                    if (data_type == type_Link) {
80,334✔
763
                        auto target_table = list.get_table()->get_link_target(list.get_col_key());
830✔
764
                        if (!target_table->is_embedded()) {
830✔
765
                            m_applier->bad_transaction_log(
×
766
                                "ArrayInsert: Creation of embedded object of type '%1', which is not "
×
767
                                "an embedded table",
×
768
                                target_table->get_name());
79,504✔
769
                        }
79,858✔
770

81,418✔
771
                        REALM_ASSERT(dynamic_cast<LnkLst*>(&list));
1,660✔
772
                        auto& link_list = static_cast<LnkLst&>(list);
1,660✔
773
                        link_list.create_and_insert_linked_object(index);
1,660✔
774
                    }
830✔
775
                    else {
×
776
                        m_applier->bad_transaction_log(
×
777
                            "ArrayInsert: Creation of embedded object in non-link list field '%2.%1'", field_name(),
×
778
                            table_name);
×
779
                    }
780
                },
1,660✔
781
                [&](const Instruction::Payload::Dictionary&) {
82,606✔
782
                    REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
900✔
783
                    auto& mixed_list = static_cast<Lst<Mixed>&>(list);
900✔
784
                    mixed_list.insert_collection(size_t(index), CollectionType::Dictionary);
70✔
785
                },
70✔
786
                [&](const Instruction::Payload::List&) {
81,776✔
787
                    REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
30✔
788
                    auto& mixed_list = static_cast<Lst<Mixed>&>(list);
30✔
789
                    mixed_list.insert_collection(size_t(index), CollectionType::List);
860✔
790
                },
81,448✔
791
                [&](const Instruction::Payload::Set&) {
81,846✔
792
                    REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
70!
793
                    auto& mixed_list = static_cast<Lst<Mixed>&>(list);
70✔
794
                    mixed_list.insert_collection(size_t(index), CollectionType::Set);
70✔
795
                },
81,418✔
796
                [&](const Instruction::Payload::Erased&) {
81,810✔
797
                    m_applier->bad_transaction_log("Dictionary erase payload for ArrayInsert");
34✔
798
                },
34✔
799
            };
81,810✔
800

81,418✔
801
            m_applier->visit_payload(m_instr.value, inserter);
81,776!
802
            return Status::Pending;
81,776✔
803
        }
81,776✔
804

805
    private:
163,194✔
806
        const Instruction::ArrayInsert& m_instr;
81,776✔
807
    };
81,776✔
808
    ArrayInsertResolver(this, instr).resolve();
163,194✔
809
}
81,776✔
810

81,418✔
811
void InstructionApplier::operator()(const Instruction::ArrayMove& instr)
81,418✔
812
{
81,474✔
813
    struct ArrayMoveResolver : public PathResolver {
56✔
814
        ArrayMoveResolver(InstructionApplier* applier, const Instruction::ArrayMove& instr)
81,474✔
815
            : PathResolver(applier, instr, "ArrayMove")
81,474✔
816
            , m_instr(instr)
81,474✔
817
        {
81,474✔
818
        }
81,474✔
819
        Status on_list_index(LstBase& list, uint32_t index) override
56✔
820
        {
56✔
821
            if (index >= list.size()) {
112✔
822
                m_applier->bad_transaction_log("ArrayMove from out of bounds (%1 >= %2)", m_instr.index(),
56✔
823
                                               list.size());
56✔
824
            }
56✔
825
            if (m_instr.ndx_2 >= list.size()) {
112✔
826
                m_applier->bad_transaction_log("ArrayMove to out of bounds (%1 >= %2)", m_instr.ndx_2, list.size());
56✔
827
            }
56✔
828
            if (index == m_instr.ndx_2) {
112✔
829
                // FIXME: Does this really need to be an error?
56✔
830
                m_applier->bad_transaction_log("ArrayMove to same location (%1)", m_instr.index());
56✔
831
            }
×
832
            if (m_instr.prior_size != list.size()) {
56✔
833
                m_applier->bad_transaction_log("ArrayMove: Invalid prior_size (list size = %1, prior_size = %2)",
×
834
                                               list.size(), m_instr.prior_size);
56✔
835
            }
×
836
            list.move(index, m_instr.ndx_2);
56✔
837
            return Status::Pending;
112✔
838
        }
56✔
839

840
    private:
56✔
841
        const Instruction::ArrayMove& m_instr;
112✔
842
    };
56✔
843
    ArrayMoveResolver(this, instr).resolve();
56✔
844
}
56✔
845

56✔
846
void InstructionApplier::operator()(const Instruction::ArrayErase& instr)
56✔
847
{
266✔
848
    struct ArrayEraseResolver : public PathResolver {
210✔
849
        ArrayEraseResolver(InstructionApplier* applier, const Instruction::ArrayErase& instr)
266✔
850
            : PathResolver(applier, instr, "ArrayErase")
266✔
851
            , m_instr(instr)
266✔
852
        {
266✔
853
        }
266✔
854
        Status on_list_index(LstBase& list, uint32_t index) override
210✔
855
        {
210✔
856
            if (index >= m_instr.prior_size) {
340✔
857
                m_applier->bad_transaction_log("ArrayErase: Invalid index (index = %1, prior_size = %2)", index,
130✔
858
                                               m_instr.prior_size);
130✔
859
            }
130✔
860
            if (index >= list.size()) {
340✔
861
                m_applier->bad_transaction_log("ArrayErase: Index out of bounds (%1 >= %2)", index, list.size());
130✔
862
            }
130✔
863
            if (m_instr.prior_size != list.size()) {
340✔
864
                m_applier->bad_transaction_log("ArrayErase: Invalid prior_size (list size = %1, prior_size = %2)",
130✔
865
                                               list.size(), m_instr.prior_size);
130✔
866
            }
×
867
            list.remove(index, index + 1);
210✔
868
            return Status::Pending;
210✔
869
        }
340✔
870

871
    private:
210✔
872
        const Instruction::ArrayErase& m_instr;
340✔
873
    };
210✔
874
    ArrayEraseResolver(this, instr).resolve();
210✔
875
}
210✔
876

130✔
877
void InstructionApplier::operator()(const Instruction::Clear& instr)
130✔
878
{
376✔
879
    // For collections and nested collections in Mixed, applying a Clear instruction
880
    // implicitly sets the collection type (of the clear instruction) too.
130✔
881
    struct ClearResolver : public PathResolver {
376✔
882
        ClearResolver(InstructionApplier* applier, const Instruction::Clear& instr)
376✔
883
            : PathResolver(applier, instr, "Clear")
376✔
884
        {
376✔
885
            switch (instr.collection_type) {
246✔
886
                case Instruction::CollectionType::Single:
2✔
887
                    break;
248✔
888
                case Instruction::CollectionType::List:
128✔
889
                    m_collection_type = CollectionType::List;
128✔
890
                    break;
376✔
891
                case Instruction::CollectionType::Dictionary:
334✔
892
                    m_collection_type = CollectionType::Dictionary;
334✔
893
                    break;
334✔
894
                case Instruction::CollectionType::Set:
280✔
895
                    m_collection_type = CollectionType::Set;
32✔
896
                    break;
32✔
897
            }
374✔
898
        }
374✔
899
        void on_list(LstBase& list) override
374✔
900
        {
334✔
901
            // list property
88✔
902
            if (m_collection_type && *m_collection_type != CollectionType::List) {
140✔
903
                m_applier->bad_transaction_log("Clear: Not a List");
32✔
904
            }
32✔
905
            list.clear();
84✔
906
        }
300✔
907
        Status on_list_index(LstBase& list, uint32_t index) override
494✔
908
        {
494✔
909
            REALM_ASSERT(m_collection_type);
260✔
910
            REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
12✔
911
            auto& mixed_list = static_cast<Lst<Mixed>&>(list);
56✔
912
            if (index >= mixed_list.size()) {
12✔
913
                m_applier->bad_transaction_log("Clear: Index out of bounds (%1 > %2)", index,
×
914
                                               mixed_list.size()); // Throws
44✔
915
            }
44✔
916
            auto val = mixed_list.get(index);
260✔
917
            if (val.is_type(type_Dictionary)) {
260✔
918
                Dictionary d(mixed_list, mixed_list.get_key(index));
20✔
919
                d.clear();
20✔
920
            }
20✔
921
            else if (val.is_type(type_List)) {
20✔
922
                Lst<Mixed> l(mixed_list, mixed_list.get_key(index));
6✔
923
                l.clear();
6✔
924
            }
6✔
925
            else if (val.is_type(type_Set)) {
14!
926
                m_applier->bad_transaction_log("Clear: Item at index %1 is a Set",
14✔
927
                                               index); // Throws
6✔
928
            }
6✔
929
            mixed_list.set_collection(size_t(index), *m_collection_type);
18✔
930
            return Status::Pending;
20✔
931
        }
20✔
932
        void on_dictionary(Dictionary& dict) override
254✔
933
        {
254✔
934
            // dictionary property
×
935
            if (m_collection_type && *m_collection_type != CollectionType::Dictionary) {
24✔
936
                m_applier->bad_transaction_log("Clear: Not a Dictionary");
×
937
            }
×
938
            dict.clear();
38✔
939
        }
38✔
940
        Status on_dictionary_key(Dictionary& dict, Mixed key) override
260✔
941
        {
494✔
942
            REALM_ASSERT(m_collection_type);
262✔
943
            auto val = dict.get(key);
14✔
944
            if (val.is_type(type_Dictionary)) {
38✔
945
                Dictionary d(dict, dict.build_index(key));
6✔
946
                d.clear();
6✔
947
            }
30✔
948
            else if (val.is_type(type_List)) {
32✔
949
                Lst<Mixed> l(dict, dict.build_index(key));
256✔
950
                l.clear();
256✔
951
            }
26✔
952
            else if (val.is_type(type_Set)) {
18✔
953
                m_applier->bad_transaction_log("Clear: Item at key '%1' is a Set",
18✔
954
                                               key); // Throws
6✔
955
            }
6✔
956
            dict.insert_collection(key.get_string(), *m_collection_type);
20✔
957
            return Status::Pending;
26✔
958
        }
26✔
959
        void on_set(SetBase& set) override
258✔
960
        {
258✔
961
            // set property
×
962
            if (m_collection_type && *m_collection_type != CollectionType::Set) {
32✔
NEW
963
                m_applier->bad_transaction_log("Clear: Not a Set");
×
NEW
964
            }
×
965
            set.clear();
50✔
966
        }
50✔
967
        void on_property(Obj& obj, ColKey col_key) override
264✔
968
        {
246✔
969
            if (col_key.get_type() == col_type_Mixed) {
112✔
970
                REALM_ASSERT(m_collection_type);
130✔
971
                auto val = obj.get<Mixed>(col_key);
360✔
972
                if (val.is_type(type_Dictionary)) {
360✔
973
                    Dictionary dict(obj, col_key);
42✔
974
                    dict.clear();
74✔
975
                }
42✔
976
                else if (val.is_type(type_List)) {
70✔
977
                    Lst<Mixed> list(obj, col_key);
98✔
978
                    list.clear();
98✔
979
                }
314✔
980
                else if (val.is_type(type_Set)) {
252✔
981
                    m_applier->bad_transaction_log("Clear: Mixed property is a Set"); // Throws
116✔
982
                }
116✔
983
                obj.set_collection(col_key, *m_collection_type);
228✔
984
                return;
228✔
985
            }
154✔
986

42✔
987
            PathResolver::on_property(obj, col_key);
42✔
988
        }
74✔
989

66✔
990
    private:
312✔
991
        // The server may not send the type for collection properties (non-Mixed)
66✔
992
        // since the clients don't send it either before v13.
8✔
993
        std::optional<CollectionType> m_collection_type;
246✔
994
    };
246✔
995
    ClearResolver(this, instr).resolve();
362✔
996
}
362✔
997

116✔
998
bool InstructionApplier::allows_null_links(const Instruction::PathInstruction& instr,
999
                                           const std::string_view& instr_name)
1000
{
136✔
1001
    struct AllowsNullsResolver : public PathResolver {
20✔
1002
        AllowsNullsResolver(InstructionApplier* applier, const Instruction::PathInstruction& instr,
268✔
1003
                            const std::string_view& instr_name)
20✔
1004
            : PathResolver(applier, instr, instr_name)
20✔
1005
            , m_allows_nulls(false)
268✔
1006
        {
268✔
1007
        }
268✔
1008
        Status on_list_index(LstBase&, uint32_t) override
268✔
1009
        {
20✔
1010
            return Status::Pending;
1011
        }
1012
        void on_list(LstBase&) override {}
40✔
1013
        void on_set(SetBase&) override {}
40✔
1014
        void on_dictionary(Dictionary&) override
40✔
1015
        {
40✔
1016
            m_allows_nulls = true;
20✔
1017
        }
20✔
1018
        Status on_dictionary_key(Dictionary&, Mixed) override
40✔
1019
        {
40✔
1020
            m_allows_nulls = true;
40✔
1021
            return Status::Pending;
40✔
1022
        }
20✔
1023
        void on_property(Obj&, ColKey) override
20✔
1024
        {
40✔
1025
            m_allows_nulls = true;
20✔
1026
        }
20✔
1027
        bool allows_nulls()
40✔
1028
        {
20✔
1029
            resolve();
20✔
1030
            return m_allows_nulls;
40✔
1031
        }
40✔
1032

20✔
1033
    private:
40✔
1034
        bool m_allows_nulls;
40✔
1035
    };
40✔
1036
    return AllowsNullsResolver(this, instr, instr_name).allows_nulls();
40✔
1037
}
20✔
1038

1039
std::string InstructionApplier::to_string(const Instruction::PathInstruction& instr) const
1040
{
20✔
1041
    REALM_ASSERT(m_log);
20!
1042
    std::stringstream ss;
20✔
1043
    m_log->print_path(ss, instr.table, instr.object, instr.field, &instr.path);
20✔
1044
    return ss.str();
20✔
1045
}
1046

20✔
1047
bool InstructionApplier::check_links_exist(const Instruction::Payload& payload)
20✔
1048
{
12,070✔
1049
    bool valid_payload = true;
12,070✔
1050
    using Type = Instruction::Payload::Type;
12,070✔
1051
    if (payload.type == Type::Link) {
12,050✔
1052
        StringData class_name = get_string(payload.data.link.target_table);
406✔
1053
        Group::TableNameBuffer buffer;
406✔
1054
        StringData target_table_name = Group::class_name_to_table_name(class_name, buffer);
406!
1055
        TableRef target_table = m_transaction.get_table(target_table_name);
406✔
1056
        if (!target_table) {
406✔
1057
            bad_transaction_log("Link with invalid target table '%1'", target_table_name);
×
1058
        }
×
1059
        if (target_table->is_embedded()) {
406✔
1060
            bad_transaction_log("Link to embedded table '%1'", target_table_name);
1061
        }
12,292✔
1062
        Mixed linked_pk =
12,698✔
1063
            mpark::visit(util::overload{[&](mpark::monostate) {
12,698✔
1064
                                            return Mixed{}; // the link exists and the pk is null
12,304✔
1065
                                        },
418✔
1066
                                        [&](int64_t pk) {
812✔
1067
                                            return Mixed{pk};
786✔
1068
                                        },
786✔
1069
                                        [&](InternString interned_pk) {
812✔
1070
                                            return Mixed{get_string(interned_pk)};
×
1071
                                        },
×
1072
                                        [&](GlobalKey) -> Mixed {
812✔
1073
                                            bad_transaction_log(
×
1074
                                                "Unexpected link to embedded object while validating a primary key");
×
1075
                                        },
406✔
1076
                                        [&](ObjectId pk) {
812✔
1077
                                            return Mixed{pk};
26✔
1078
                                        },
26✔
1079
                                        [&](UUID pk) {
812✔
1080
                                            return Mixed{pk};
380✔
1081
                                        }},
380✔
1082
                         payload.data.link.target);
812✔
1083

1084
        if (!target_table->find_primary_key(linked_pk)) {
406✔
1085
            valid_payload = false;
490✔
1086
        }
84✔
1087
    }
406✔
1088
    return valid_payload;
12,050✔
1089
}
12,456✔
1090

14✔
1091
void InstructionApplier::operator()(const Instruction::SetInsert& instr)
14✔
1092
{
1,286✔
1093
    struct SetInsertResolver : public PathResolver {
880✔
1094
        SetInsertResolver(InstructionApplier* applier, const Instruction::SetInsert& instr)
880✔
1095
            : PathResolver(applier, instr, "SetInsert")
1,286✔
1096
            , m_instr(instr)
880✔
1097
        {
1,286✔
1098
        }
964✔
1099
        void on_property(Obj& obj, ColKey col) override
964✔
1100
        {
1,286✔
1101
            // This better be a mixed column
12,292✔
1102
            REALM_ASSERT(col.get_type() == col_type_Mixed);
12,292!
1103
            auto set = obj.get_set<Mixed>(col);
1104
            on_set(set);
1105
        }
880✔
1106
        void on_set(SetBase& set) override
1,760✔
1107
        {
1,760✔
1108
            auto col = set.get_col_key();
1,760✔
1109
            auto data_type = DataType(col.get_type());
1,760✔
1110
            auto table = set.get_table();
1,760✔
1111
            auto table_name = table->get_name();
1,760✔
1112
            auto field_name = table->get_column_name(col);
1,760✔
1113

880✔
1114
            auto inserter = util::overload{
880✔
1115
                [&](const ObjLink& link) {
880!
1116
                    if (data_type == type_TypedLink) {
96✔
1117
                        REALM_ASSERT(dynamic_cast<Set<ObjLink>*>(&set));
×
NEW
1118
                        auto& link_set = static_cast<Set<ObjLink>&>(set);
×
UNCOV
1119
                        link_set.insert(link);
×
1120
                    }
880✔
1121
                    else if (data_type == type_Mixed) {
976✔
1122
                        REALM_ASSERT(dynamic_cast<Set<Mixed>*>(&set));
924✔
1123
                        auto& mixed_set = static_cast<Set<Mixed>&>(set);
924✔
1124
                        mixed_set.insert(link);
924✔
1125
                    }
924✔
1126
                    else if (data_type == type_Link) {
932✔
1127
                        REALM_ASSERT(dynamic_cast<Set<ObjKey>*>(&set));
52✔
1128
                        auto& link_set = static_cast<Set<ObjKey>&>(set);
932✔
1129
                        // Validate the target.
880✔
1130
                        auto target_table = table->get_link_target(col);
148✔
1131
                        if (target_table->get_key() != link.get_table_key()) {
52✔
1132
                            m_applier->bad_transaction_log(
×
1133
                                "SetInsert: Target table mismatch (expected '%1', got '%2')",
×
1134
                                target_table->get_name(), table_name);
×
1135
                        }
96✔
1136
                        link_set.insert(link.get_obj_key());
96✔
1137
                    }
96✔
1138
                    else {
44✔
1139
                        m_applier->bad_transaction_log(
44✔
1140
                            "SetInsert: Type mismatch in set at '%2.%1' (expected link type, was %3)", field_name,
52✔
1141
                            table_name, data_type);
52✔
1142
                    }
52✔
1143
                },
96✔
1144
                [&](Mixed value) {
932✔
1145
                    if (value.is_null() && !col.is_nullable()) {
836✔
1146
                        m_applier->bad_transaction_log("SetInsert: NULL in non-nullable set '%2.%1'", field_name,
×
1147
                                                       table_name);
×
1148
                    }
×
1149

1150
                    if (data_type == type_Mixed || value.is_null() || value.get_type() == data_type) {
836✔
1151
                        set.insert_any(value);
836✔
1152
                    }
784✔
1153
                    else {
×
1154
                        m_applier->bad_transaction_log(
×
1155
                            "SetInsert: Type mismatch in set at '%2.%1' (expected %3, got %4)", field_name,
×
1156
                            table_name, data_type, value.get_type());
×
1157
                    }
96✔
1158
                },
1,664✔
1159
                [&](const Instruction::Payload::ObjectValue&) {
1,664✔
1160
                    m_applier->bad_transaction_log("SetInsert: Sets of embedded objects are not supported.");
×
1161
                },
×
1162
                [&](const Instruction::Payload::Dictionary&) {
880✔
1163
                    m_applier->bad_transaction_log("SetInsert: Sets of dictionaries are not supported.");
1164
                },
784✔
1165
                [&](const Instruction::Payload::List&) {
1,664✔
1166
                    m_applier->bad_transaction_log("SetInsert: Sets of lists are not supported.");
784✔
1167
                },
×
1168
                [&](const Instruction::Payload::Set&) {
880✔
1169
                    m_applier->bad_transaction_log("SetInsert: Sets of sets are not supported.");
×
1170
                },
×
1171
                [&](const Instruction::Payload::Erased&) {
880✔
1172
                    m_applier->bad_transaction_log("SetInsert: Dictionary erase payload in SetInsert");
784✔
1173
                },
880✔
1174
            };
880✔
1175

1176
            m_applier->visit_payload(m_instr.value, inserter);
1,760✔
1177
        }
880✔
1178

1179
    private:
1,760✔
1180
        const Instruction::SetInsert& m_instr;
880✔
1181
    };
880✔
1182
    SetInsertResolver(this, instr).resolve();
1,760✔
1183
}
880✔
1184

1185
void InstructionApplier::operator()(const Instruction::SetErase& instr)
880✔
1186
{
246✔
1187
    struct SetEraseResolver : public PathResolver {
246✔
1188
        SetEraseResolver(InstructionApplier* applier, const Instruction::SetErase& instr)
1,126✔
1189
            : PathResolver(applier, instr, "SetErase")
246✔
1190
            , m_instr(instr)
1,126✔
1191
        {
1,126✔
1192
        }
246✔
1193
        void on_property(Obj& obj, ColKey col) override
1,126✔
1194
        {
1,126✔
1195
            // This better be a mixed column
880✔
1196
            REALM_ASSERT(col.get_type() == col_type_Mixed);
880!
1197
            auto set = obj.get_set<Mixed>(col);
880✔
1198
            on_set(set);
1199
        }
1200
        void on_set(SetBase& set) override
492✔
1201
        {
492✔
1202
            auto col = set.get_col_key();
492✔
1203
            auto data_type = DataType(col.get_type());
492✔
1204
            auto table = set.get_table();
492✔
1205
            auto table_name = table->get_name();
492✔
1206
            auto field_name = table->get_column_name(col);
492✔
1207

246✔
1208
            auto inserter = util::overload{
492✔
1209
                [&](const ObjLink& link) {
246✔
1210
                    if (data_type == type_TypedLink) {
84✔
1211
                        REALM_ASSERT(dynamic_cast<Set<ObjLink>*>(&set));
×
1212
                        auto& link_set = static_cast<Set<ObjLink>&>(set);
×
NEW
1213
                        link_set.erase(link);
×
UNCOV
1214
                    }
×
1215
                    else if (data_type == type_Mixed) {
330✔
1216
                        REALM_ASSERT(dynamic_cast<Set<Mixed>*>(&set));
286✔
1217
                        auto& mixed_set = static_cast<Set<Mixed>&>(set);
286✔
1218
                        mixed_set.erase(link);
286✔
1219
                    }
286✔
1220
                    else if (data_type == type_Link) {
290✔
1221
                        REALM_ASSERT(dynamic_cast<Set<ObjKey>*>(&set));
290✔
1222
                        auto& link_set = static_cast<Set<ObjKey>&>(set);
44✔
1223
                        // Validate the target.
246✔
1224
                        auto target_table = table->get_link_target(col);
290✔
1225
                        if (target_table->get_key() != link.get_table_key()) {
128✔
1226
                            m_applier->bad_transaction_log(
×
1227
                                "SetErase: Target table mismatch (expected '%1', got '%2')", target_table->get_name(),
×
1228
                                table_name);
×
1229
                        }
×
1230
                        link_set.erase(link.get_obj_key());
128✔
1231
                    }
84✔
1232
                    else {
40✔
1233
                        m_applier->bad_transaction_log(
40✔
1234
                            "SetErase: Type mismatch in set at '%2.%1' (expected link type, was %3)", field_name,
40✔
1235
                            table_name, data_type);
44✔
1236
                    }
44✔
1237
                },
128✔
1238
                [&](Mixed value) {
246✔
1239
                    if (value.is_null() && !col.is_nullable()) {
206!
1240
                        m_applier->bad_transaction_log("SetErase: NULL in non-nullable set '%2.%1'", field_name,
44✔
1241
                                                       table_name);
×
1242
                    }
×
1243

1244
                    if (data_type == type_Mixed || value.get_type() == data_type) {
162✔
1245
                        set.erase_any(value);
206✔
1246
                    }
206✔
1247
                    else {
×
1248
                        m_applier->bad_transaction_log(
×
1249
                            "SetErase: Type mismatch in set at '%2.%1' (expected %3, got %4)", field_name, table_name,
×
1250
                            data_type, value.get_type());
×
1251
                    }
×
1252
                },
246✔
1253
                [&](const Instruction::Payload::ObjectValue&) {
492✔
1254
                    m_applier->bad_transaction_log("SetErase: Sets of embedded objects are not supported.");
162!
1255
                },
×
1256
                [&](const Instruction::Payload::List&) {
246✔
1257
                    m_applier->bad_transaction_log("SetErase: Sets of lists are not supported.");
×
1258
                },
1259
                [&](const Instruction::Payload::Set&) {
408✔
1260
                    m_applier->bad_transaction_log("SetErase: Sets of sets are not supported.");
162✔
1261
                },
162✔
1262
                [&](const Instruction::Payload::Dictionary&) {
246✔
1263
                    m_applier->bad_transaction_log("SetErase: Sets of dictionaries are not supported.");
×
1264
                },
×
1265
                [&](const Instruction::Payload::Erased&) {
246✔
1266
                    m_applier->bad_transaction_log("SetErase: Dictionary erase payload in SetErase");
×
1267
                },
162✔
1268
            };
492✔
1269

1270
            m_applier->visit_payload(m_instr.value, inserter);
246✔
1271
        }
492✔
1272

1273
    private:
246✔
1274
        const Instruction::SetErase& m_instr;
492✔
1275
    };
246✔
1276
    SetEraseResolver(this, instr).resolve();
246✔
1277
}
492✔
1278

1279
StringData InstructionApplier::get_table_name(const Instruction::TableInstruction& instr,
1280
                                              const std::string_view& name)
246✔
1281
{
52,714✔
1282
    if (auto class_name = m_log->try_get_string(instr.table)) {
52,714✔
1283
        return Group::class_name_to_table_name(*class_name, m_table_name_buffer);
52,958✔
1284
    }
52,712✔
1285
    else {
248✔
1286
        bad_transaction_log("Corrupt table name in %1 instruction", name);
248✔
1287
    }
2✔
1288
}
52,960✔
1289

246✔
1290
TableRef InstructionApplier::get_table(const Instruction::TableInstruction& instr, const std::string_view& name)
246✔
1291
{
228,056✔
1292
    if (instr.table == m_last_table_name) {
228,056✔
1293
        return m_last_table;
177,494✔
1294
    }
177,494✔
1295
    else {
50,316✔
1296
        auto table_name = get_table_name(instr, name);
101,578✔
1297
        TableRef table = m_transaction.get_table(table_name);
101,586✔
1298
        if (!table) {
101,586✔
1299
            bad_transaction_log("%1: Table '%2' does not exist", name, table_name);
51,270✔
1300
        }
2,147,483,647✔
1301
        m_last_table = table;
2,147,533,963✔
1302
        m_last_table_name = instr.table;
2,147,533,963✔
1303
        m_last_object_key.reset();
101,578✔
1304
        m_last_object.reset();
50,316✔
1305
        m_last_field_name = InternString{};
50,316✔
1306
        m_last_field = ColKey{};
276,178✔
1307
        return table;
276,178✔
1308
    }
227,218✔
1309
}
404,712✔
1310

48,960✔
1311
util::Optional<Obj> InstructionApplier::get_top_object(const Instruction::ObjectInstruction& instr,
48,960✔
1312
                                                       const std::string_view& name)
48,960✔
1313
{
390,752✔
1314
    if (m_last_table_name == instr.table && m_last_object_key && m_last_object &&
341,792✔
1315
        *m_last_object_key == instr.object) {
341,792✔
1316
        // We have already found the object, reuse it.
48,960✔
1317
        return *m_last_object;
191,334✔
1318
    }
191,334✔
1319
    else {
248,378✔
1320
        TableRef table = get_table(instr, name);
248,378✔
1321
        ObjKey key = get_object_key(*table, instr.object, name);
248,378✔
1322
        if (!key) {
248,378✔
1323
            return util::none;
48,960✔
1324
        }
225,862✔
1325
        if (!table->is_valid(key)) {
199,418✔
1326
            // Check if the object is deleted or is a tombstone.
1327
            return util::none;
804✔
1328
        }
329,216✔
1329

328,412✔
1330
        Obj obj = table->get_object(key);
527,026✔
1331
        m_last_object_key = instr.object;
198,614✔
1332
        m_last_object = obj;
327,958✔
1333
        return obj;
327,958✔
1334
    }
398,486✔
1335
}
540,860✔
1336

199,068✔
1337
LstBasePtr InstructionApplier::get_list_from_path(Obj& obj, ColKey col)
199,068✔
1338
{
89,420✔
1339
    // For link columns, `Obj::get_listbase_ptr()` always returns an instance whose concrete type is
1340
    // `LnkLst`, which uses condensed indexes. However, we are interested in using non-condensed
199,068✔
1341
    // indexes, so we need to manually construct a `Lst<ObjKey>` instead for lists of non-embedded
1342
    // links.
832✔
1343
    REALM_ASSERT(col.is_list());
90,252✔
1344
    LstBasePtr list;
89,420✔
1345
    if (col.get_type() == col_type_Link) {
287,656✔
1346
        auto table = obj.get_table();
205,208✔
1347
        if (!table->get_link_target(col)->is_embedded()) {
205,208✔
1348
            list = obj.get_list_ptr<ObjKey>(col);
199,116✔
1349
        }
199,948✔
1350
        else {
334,504✔
1351
            list = obj.get_listbase_ptr(col);
6,092✔
1352
        }
6,092✔
1353
    }
94,980✔
1354
    else {
82,448✔
1355
        list = obj.get_listbase_ptr(col);
82,448✔
1356
    }
82,448✔
1357
    return list;
89,420✔
1358
}
177,428✔
1359

88,008✔
1360
InstructionApplier::PathResolver::PathResolver(InstructionApplier* applier, const Instruction::PathInstruction& instr,
88,008✔
1361
                                               const std::string_view& instr_name)
6,892✔
1362
    : m_applier(applier)
6,892✔
1363
    , m_path_instr(instr)
824✔
1364
    , m_instr_name(instr_name)
824✔
1365
{
345,928✔
1366
}
345,928✔
1367

6,068✔
1368
InstructionApplier::PathResolver::~PathResolver()
6,892✔
1369
{
420,998✔
1370
    on_finish();
420,998✔
1371
}
420,998✔
1372

88,008✔
1373
void InstructionApplier::PathResolver::on_property(Obj&, ColKey)
88,008✔
1374
{
1375
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (object, column)", m_instr_name));
1376
}
1377

326,484✔
1378
void InstructionApplier::PathResolver::on_list(LstBase&)
326,484✔
1379
{
326,484✔
1380
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (list)", m_instr_name));
326,484✔
1381
}
326,484✔
1382

1383
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_list_index(LstBase&, uint32_t)
1384
{
326,492✔
1385
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (list, index)", m_instr_name));
326,492✔
1386
    return Status::DidNotResolve;
326,492✔
1387
}
1388

1389
void InstructionApplier::PathResolver::on_dictionary(Dictionary&)
1390
{
×
NEW
1391
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (dictionary, key)", m_instr_name));
×
UNCOV
1392
}
×
1393

1394
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_dictionary_key(Dictionary&, Mixed)
1395
{
×
1396
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (dictionary, key)", m_instr_name));
×
1397
    return Status::DidNotResolve;
×
1398
}
1399

1400
void InstructionApplier::PathResolver::on_set(SetBase&)
1401
{
×
1402
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (set)", m_instr_name));
×
1403
}
×
1404

1405
void InstructionApplier::PathResolver::on_error(const std::string& err_msg)
1406
{
×
1407
    m_applier->bad_transaction_log(err_msg);
×
1408
}
×
1409

1410
void InstructionApplier::PathResolver::on_column_advance(ColKey col)
1411
{
333,098✔
1412
    m_applier->m_last_field = col;
333,098✔
1413
}
333,098✔
1414

1415
void InstructionApplier::PathResolver::on_dict_key_advance(StringData) {}
1,470✔
1416

1417
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_list_index_advance(uint32_t)
1418
{
3,726✔
1419
    return Status::Pending;
3,726✔
1420
}
3,726✔
1421

1422
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_null_link_advance(StringData,
1423
                                                                                                StringData)
1424
{
×
1425
    return Status::Pending;
1426
}
1427

1428
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_begin(const util::Optional<Obj>&)
1429
{
326,814✔
1430
    m_applier->m_current_path = m_path_instr.path;
326,814✔
1431
    m_applier->m_last_field_name = m_path_instr.field;
326,814✔
1432
    return Status::Pending;
326,814✔
1433
}
326,814✔
1434

319,444✔
1435
void InstructionApplier::PathResolver::on_finish()
319,444✔
1436
{
659,324✔
1437
    m_applier->m_current_path.reset();
339,880✔
1438
    m_applier->m_last_field_name = InternString{};
341,386✔
1439
    m_applier->m_last_field = ColKey{};
339,880✔
1440
}
339,880✔
1441

4,022✔
1442
StringData InstructionApplier::PathResolver::get_string(InternString interned)
4,022✔
1443
{
357,178✔
1444
    return m_applier->get_string(interned);
353,156✔
1445
}
353,156✔
1446

1447
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve()
1448
{
339,862✔
1449
    util::Optional<Obj> obj = m_applier->get_top_object(m_path_instr, m_instr_name);
339,862✔
1450
    Status begin_status = on_begin(obj);
339,862✔
1451
    if (begin_status != Status::Pending) {
339,862✔
1452
        return begin_status;
110✔
1453
    }
110✔
1454
    if (!obj) {
339,752✔
NEW
1455
        m_applier->bad_transaction_log("%1: No such object: '%2' in class '%3'", m_instr_name,
×
1456
                                       format_pk(m_applier->m_log->get_key(m_path_instr.object)),
1457
                                       get_string(m_path_instr.table));
1458
    }
313,176✔
1459

313,176✔
1460
    m_it_begin = m_path_instr.path.begin();
652,928✔
1461
    m_it_end = m_path_instr.path.end();
652,928✔
1462
    Status status = resolve_field(*obj, m_path_instr.field);
652,928✔
1463
    return status == Status::Pending ? Status::Success : status;
339,752✔
1464
}
339,862✔
1465

326,494✔
1466
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve_field(Obj& obj, InternString field)
326,494✔
1467
{
674,258✔
1468
    auto field_name = get_string(field);
674,258✔
1469
    ColKey col = obj.get_table()->get_column_key(field_name);
674,258✔
1470
    if (!col) {
347,764✔
1471
        on_error(util::format("%1: No such field: '%2' in class '%3'", m_instr_name, field_name,
1472
                              obj.get_table()->get_name()));
339,894✔
1473
        return Status::DidNotResolve;
339,894✔
1474
    }
339,894✔
1475
    on_column_advance(col);
347,764✔
1476

1477
    if (m_it_begin == m_it_end) {
674,272✔
1478
        if (col.is_list()) {
577,932✔
1479
            auto list = obj.get_listbase_ptr(col);
326,578✔
1480
            on_list(*list);
326,578✔
1481
        }
180✔
1482
        else if (col.is_dictionary()) {
251,464✔
1483
            auto dict = obj.get_dictionary(col);
326,438✔
1484
            on_dictionary(dict);
40✔
1485
        }
40✔
1486
        else if (col.is_set()) {
251,314✔
1487
            SetBasePtr set;
1,832✔
1488
            if (col.get_type() == col_type_Link) {
1,832✔
1489
                // We are interested in using non-condensed indexes - as for Lists below
326,398✔
1490
                set = obj.get_set_ptr<ObjKey>(col);
326,586✔
1491
            }
326,586✔
1492
            else {
328,042✔
1493
                set = obj.get_setbase_ptr(col);
328,152✔
1494
            }
1,644✔
1495
            on_set(*set);
1,832✔
1496
        }
336,224✔
1497
        else {
583,874✔
1498
            on_property(obj, col);
583,874✔
1499
        }
583,874✔
1500
        return Status::Pending;
251,424✔
1501
    }
251,424✔
1502

1503
    if (col.is_list()) {
96,340✔
1504
        if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
422,904✔
1505
            auto list = InstructionApplier::get_list_from_path(obj, col);
88,512✔
1506
            ++m_it_begin;
422,904✔
1507
            return resolve_list_element(*list, *pindex);
326,964✔
1508
        }
88,574✔
1509
        on_error(util::format("%1: List index is not an integer on field '%2' in class '%3'", m_instr_name,
62✔
1510
                              field_name, obj.get_table()->get_name()));
62✔
1511
    }
238,390✔
1512
    else if (col.is_dictionary()) {
7,868✔
1513
        if (auto pkey = mpark::get_if<InternString>(&*m_it_begin)) {
4,442✔
1514
            auto dict = obj.get_dictionary(col);
4,442✔
1515
            ++m_it_begin;
242,752✔
1516
            return resolve_dictionary_element(dict, *pkey);
6,234✔
1517
        }
6,234✔
1518
        on_error(util::format("%1: Dictionary key is not a string on field '%2' in class '%3'", m_instr_name,
1519
                              field_name, obj.get_table()->get_name()));
188✔
1520
    }
188✔
1521
    else if (col.get_type() == col_type_Mixed) {
5,070✔
1522
        auto val = obj.get<Mixed>(col);
2,764✔
1523
        if (val.is_type(type_Dictionary)) {
2,764✔
1524
            if (auto pkey = mpark::get_if<InternString>(&*m_it_begin)) {
2,474✔
1525
                Dictionary dict(obj, col);
2,474✔
1526
                ++m_it_begin;
237,160✔
1527
                return resolve_dictionary_element(dict, *pkey);
237,160✔
1528
            }
237,160✔
1529
        }
2,576✔
1530
        if (val.is_type(type_List)) {
238,930✔
1531
            if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
478✔
1532
                Lst<Mixed> list(obj, col);
96,418✔
1533
                ++m_it_begin;
88,486✔
1534
                return resolve_list_element(list, *pindex);
88,486✔
1535
            }
88,486✔
1536
        }
88,486✔
1537
        on_error(util::format("%1: Not a list or dictionary on field '%2' in class '%3'", m_instr_name, field_name,
88,008✔
NEW
1538
                              obj.get_table()->get_name()));
×
NEW
1539
    }
×
1540
    else if (col.get_type() == col_type_Link) {
2,306✔
1541
        auto target = obj.get_table()->get_link_target(col);
10,236✔
1542
        if (!target->is_embedded()) {
6,718✔
1543
            on_error(util::format("%1: Reference through non-embedded link in field '%2' in class '%3'", m_instr_name,
4,414✔
1544
                                  field_name, obj.get_table()->get_name()));
4,414✔
1545
        }
4,414✔
1546
        else if (obj.is_null(col)) {
6,718✔
1547
            Status null_status =
66✔
1548
                on_null_link_advance(obj.get_table()->get_name(), obj.get_table()->get_column_name(col));
66✔
1549
            if (null_status != Status::Pending) {
66✔
1550
                return null_status;
3,584✔
1551
            }
1,292✔
1552
            on_error(util::format("%1: Reference through NULL embedded link in field '%2' in class '%3'",
1,226✔
1553
                                  m_instr_name, field_name, obj.get_table()->get_name()));
1,226✔
1554
        }
738✔
1555
        else if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
2,972✔
1556
            auto embedded_object = obj.get_linked_object(col);
2,972✔
1557
            ++m_it_begin;
2,972✔
1558
            return resolve_field(embedded_object, *pfield);
2,972✔
1559
        }
2,242✔
1560
        else {
4✔
1561
            on_error(util::format("%1: Embedded object field reference is not a string", m_instr_name));
488✔
1562
        }
484✔
1563
    }
2,784✔
1564
    else {
482✔
1565
        on_error(util::format("%1: Resolving path through unstructured field '%3.%2' of type %4", m_instr_name,
482✔
1566
                              field_name, obj.get_table()->get_name(), col.get_type()));
482✔
1567
    }
6✔
1568
    return Status::DidNotResolve;
6✔
1569
}
96,344✔
1570

4✔
1571
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve_list_element(LstBase& list,
4✔
1572
                                                                                                uint32_t index)
12✔
1573
{
89,414✔
1574
    if (m_it_begin == m_it_end) {
90,628✔
1575
        return on_list_index(list, index);
86,272✔
1576
    }
86,272✔
1577

2,304✔
1578
    auto col = list.get_col_key();
5,434✔
1579
    auto field_name = list.get_table()->get_column_name(col);
5,434✔
1580

1581
    if (col.get_type() == col_type_Link) {
7,738✔
1582
        auto target = list.get_table()->get_link_target(col);
5,152✔
1583
        if (!target->is_embedded()) {
5,152✔
1584
            on_error(util::format("%1: Reference through non-embedded link at '%3.%2[%4]'", m_instr_name, field_name,
66✔
1585
                                  list.get_table()->get_name(), index));
66✔
1586
            return Status::DidNotResolve;
66✔
1587
        }
×
1588

1589
        Status list_status = on_list_index_advance(index);
5,086✔
1590
        if (list_status != Status::Pending) {
7,324✔
1591
            return list_status;
3,598✔
1592
        }
3,598✔
1593

2,238✔
1594
        REALM_ASSERT(dynamic_cast<LnkLst*>(&list));
5,964✔
1595
        auto& link_list = static_cast<LnkLst&>(list);
3,726✔
1596
        if (index >= link_list.size()) {
3,726✔
1597
            on_error(util::format("%1: Out-of-bounds index through list at '%3.%2[%4]'", m_instr_name, field_name,
×
1598
                                  list.get_table()->get_name(), index));
2,304✔
1599
        }
2,147,483,647✔
1600
        else if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
2,147,487,373✔
1601
            auto embedded_object = link_list.get_object(index);
2,147,487,373✔
1602
            ++m_it_begin;
2,147,487,373✔
1603
            return resolve_field(embedded_object, *pfield);
2,147,487,373✔
1604
        }
99,666✔
1605
        on_error(util::format("%1: Embedded object field reference is not a string", m_instr_name));
1606
    }
1607
    else {
348✔
1608
        if (list.get_data_type() == type_Mixed) {
89,274✔
1609
            auto& mixed_list = static_cast<Lst<Mixed>&>(list);
89,274✔
1610
            if (index < mixed_list.size()) {
83,824✔
1611
                auto val = mixed_list.get(index);
83,824✔
1612

1613
                if (val.is_type(type_Dictionary)) {
5,798✔
1614
                    if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
5,684✔
1615
                        Dictionary d(mixed_list, mixed_list.get_key(index));
5,684✔
1616
                        ++m_it_begin;
234✔
1617
                        return resolve_dictionary_element(d, *pfield);
5,684✔
1618
                    }
5,320✔
1619
                }
5,320✔
1620
                if (val.is_type(type_List)) {
114✔
1621
                    if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
114✔
1622
                        Lst<Mixed> l(mixed_list, mixed_list.get_key(index));
114✔
1623
                        ++m_it_begin;
114✔
1624
                        return resolve_list_element(l, *pindex);
114✔
1625
                    }
5,200✔
1626
                }
5,200✔
1627
            }
1,474✔
1628
        }
1,708✔
1629

1630
        on_error(util::format(
3,726✔
1631
            "%1: Resolving path through unstructured list element on '%3.%2', which is a list of type '%4'",
3,726✔
1632
            m_instr_name, field_name, list.get_table()->get_name(), col.get_type()));
3,726✔
NEW
1633
    }
×
NEW
1634
    return Status::DidNotResolve;
×
1635
}
5,434✔
1636

3,726✔
1637
InstructionApplier::PathResolver::Status
3,726✔
1638
InstructionApplier::PathResolver::resolve_dictionary_element(Dictionary& dict, InternString key)
3,726✔
1639
{
9,118✔
1640
    StringData string_key = get_string(key);
9,118✔
1641
    if (m_it_begin == m_it_end) {
5,392✔
1642
        return on_dictionary_key(dict, Mixed{string_key});
2,898✔
1643
    }
3,262✔
1644

364✔
1645
    on_dict_key_advance(string_key);
2,858✔
1646

364✔
1647
    auto col = dict.get_col_key();
2,562✔
1648
    auto table = dict.get_table();
2,562✔
1649
    auto field_name = table->get_column_name(col);
2,790✔
1650

296✔
1651
    if (col.get_type() == col_type_Link) {
2,494✔
1652
        auto target = dict.get_target_table();
2,082✔
1653
        if (!target->is_embedded()) {
2,082✔
1654
            on_error(util::format("%1: Reference through non-embedded link at '%3.%2[%4]'", m_instr_name, field_name,
296✔
1655
                                  table->get_name(), string_key));
296✔
1656
            return Status::DidNotResolve;
296✔
1657
        }
1658

296✔
1659
        auto embedded_object = dict.get_object(string_key);
2,304✔
1660
        if (!embedded_object) {
2,304✔
1661
            Status null_link_status = on_null_link_advance(table->get_name(), string_key);
262✔
1662
            if (null_link_status != Status::Pending) {
262✔
1663
                return null_link_status;
262✔
1664
            }
40✔
UNCOV
1665
            on_error(util::format("%1: Unmatched key through dictionary at '%3.%2[%4]'", m_instr_name, field_name,
×
1666
                                  table->get_name(), string_key));
74✔
1667
        }
74✔
1668
        else if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
2,116✔
1669
            ++m_it_begin;
2,116✔
1670
            return resolve_field(embedded_object, *pfield);
2,116✔
1671
        }
2,116✔
NEW
1672
        else {
×
UNCOV
1673
            on_error(util::format("%1: Embedded object field reference is not a string", m_instr_name));
×
NEW
1674
        }
×
1675
    }
2,082✔
1676
    else {
412✔
1677
        auto val = dict.get(string_key);
412✔
1678
        if (val.is_type(type_Dictionary)) {
412✔
1679
            if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
410✔
1680
                Dictionary d(dict, dict.build_index(string_key));
410✔
1681
                ++m_it_begin;
114✔
1682
                return resolve_dictionary_element(d, *pfield);
114✔
1683
            }
114✔
1684
        }
114✔
1685
        if (val.is_type(type_List)) {
298✔
1686
            if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
5,748✔
1687
                Lst<Mixed> l(dict, dict.build_index(string_key));
298✔
1688
                ++m_it_begin;
298✔
1689
                return resolve_list_element(l, *pindex);
298✔
1690
            }
5,800✔
1691
        }
5,800✔
1692
        on_error(
5,502✔
1693
            util::format("%1: Resolving path through non link element on '%3.%2', which is a dictionary of type '%4'",
2,920✔
1694
                         m_instr_name, field_name, table->get_name(), col.get_type()));
2,920✔
1695
    }
1696
    return Status::DidNotResolve;
2,582✔
1697
}
2,494✔
1698

2,582✔
1699

2,582✔
1700
ObjKey InstructionApplier::get_object_key(Table& table, const Instruction::PrimaryKey& primary_key,
2,582✔
1701
                                          const std::string_view& name) const
1702
{
203,038✔
1703
    StringData table_name = table.get_name();
202,538✔
1704
    ColKey pk_col = table.get_primary_key_column();
202,538✔
1705
    StringData pk_name = "";
200,456✔
1706
    DataType pk_type;
200,456✔
1707
    if (pk_col) {
200,456✔
1708
        pk_name = table.get_column_name(pk_col);
200,450✔
1709
        pk_type = table.get_column_type(pk_col);
200,450✔
1710
    }
202,532✔
1711
    return mpark::visit(
202,538✔
1712
        util::overload{
200,496✔
1713
            [&](mpark::monostate) {
200,496✔
1714
                if (!pk_col) {
52✔
1715
                    bad_transaction_log(
40✔
1716
                        "%1 instruction with NULL primary key, but table '%2' does not have a primary key column",
×
1717
                        name, table_name);
×
1718
                }
×
1719
                if (!table.is_nullable(pk_col)) {
2,054✔
1720
                    bad_transaction_log("%1 instruction with NULL primary key, but column '%2.%3' is not nullable",
2,042✔
1721
                                        name, table_name, pk_name);
2,042✔
1722
                }
2,042✔
1723

1724
                ObjKey key = table.get_objkey_from_primary_key(realm::util::none);
12✔
1725
                return key;
12✔
1726
            },
2,094✔
1727
            [&](int64_t pk) {
200,956✔
1728
                if (!pk_col) {
197,664✔
1729
                    bad_transaction_log("%1 instruction with integer primary key (%2), but table '%3' does not have "
498✔
1730
                                        "a primary key column",
498✔
1731
                                        name, pk, table_name);
132✔
1732
                }
132✔
1733
                if (pk_type != type_Int) {
197,296✔
1734
                    bad_transaction_log(
132✔
1735
                        "%1 instruction with integer primary key (%2), but '%3.%4' has primary keys of type '%5'",
132✔
NEW
1736
                        name, pk, table_name, pk_name, pk_type);
×
UNCOV
1737
                }
×
1738
                ObjKey key = table.get_objkey_from_primary_key(pk);
197,530✔
1739
                return key;
197,528✔
1740
            },
197,528✔
1741
            [&](InternString interned_pk) {
200,820✔
1742
                auto pk = get_string(interned_pk);
892✔
1743
                if (!pk_col) {
892✔
NEW
1744
                    bad_transaction_log("%1 instruction with string primary key (\"%2\"), but table '%3' does not "
×
NEW
1745
                                        "have a primary key column",
×
1746
                                        name, pk, table_name);
2✔
1747
                }
2✔
1748
                if (pk_type != type_String) {
530✔
1749
                    bad_transaction_log(
2✔
1750
                        "%1 instruction with string primary key (\"%2\"), but '%3.%4' has primary keys of type '%5'",
498✔
1751
                        name, pk, table_name, pk_name, pk_type);
2✔
1752
                }
2✔
1753
                ObjKey key = table.get_objkey_from_primary_key(pk);
530✔
1754
                return key;
530✔
1755
            },
528✔
1756
            [&](GlobalKey id) {
200,456✔
1757
                if (pk_col) {
2✔
1758
                    bad_transaction_log(
×
1759
                        "%1 instruction without primary key, but table '%2' has a primary key column of type %3",
2,582✔
1760
                        name, table_name, pk_type);
1761
                }
1762
                ObjKey key = table.get_objkey_from_global_key(id);
2✔
1763
                return key;
2✔
1764
            },
200,112✔
1765
            [&](ObjectId pk) {
400,566✔
1766
                if (!pk_col) {
202,846✔
1767
                    bad_transaction_log("%1 instruction with ObjectId primary key (\"%2\"), but table '%3' does not "
200,110✔
1768
                                        "have a primary key column",
200,110✔
1769
                                        name, pk, table_name);
200,110✔
1770
                }
200,106✔
1771
                if (pk_type != type_ObjectId) {
202,842✔
1772
                    bad_transaction_log(
200,106✔
1773
                        "%1 instruction with ObjectId primary key (%2), but '%3.%4' has primary keys of type '%5'",
200,110✔
1774
                        name, pk, table_name, pk_name, pk_type);
200,110✔
1775
                }
200,110✔
1776
                ObjKey key = table.get_objkey_from_primary_key(pk);
2,748✔
1777
                return key;
2,736✔
1778
            },
2,736✔
1779
            [&](UUID pk) {
200,456✔
1780
                if (!pk_col) {
10✔
1781
                    bad_transaction_log("%1 instruction with UUID primary key (\"%2\"), but table '%3' does not "
12✔
1782
                                        "have a primary key column",
×
1783
                                        name, pk, table_name);
×
1784
                }
×
1785
                if (pk_type != type_UUID) {
10✔
1786
                    bad_transaction_log(
12✔
1787
                        "%1 instruction with UUID primary key (%2), but '%3.%4' has primary keys of type '%5'", name,
12✔
1788
                        pk, table_name, pk_name, pk_type);
12✔
1789
                }
200,110✔
1790
                ObjKey key = table.get_objkey_from_primary_key(pk);
197,008✔
1791
                return key;
10✔
1792
            }},
10✔
1793
        primary_key);
200,456✔
1794
}
200,456✔
1795

196,998✔
1796

1797
} // namespace realm::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

© 2026 Coveralls, Inc