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

realm / realm-core / thomas.goyne_478

02 Aug 2024 05:19PM UTC coverage: 91.089% (-0.01%) from 91.1%
thomas.goyne_478

Pull #7944

Evergreen

tgoyne
Only track pending client resets done by the same core version

If the previous attempt at performing a client reset was done with a different
core version then we should retry the client reset as the new version may have
fixed a bug that made the previous attempt fail (or may be a downgrade to a
version before when the bug was introduced). This also simplifies the tracking
as it means that we don't need to be able to read trackers created by different
versions.

This also means that we can freely change the schema of the table, which this
takes advantage of to drop the unused primary key and make the error required,
as we never actually stored null and the code reading it would have crashed if
it encountered a null error.
Pull Request #7944: Only track pending client resets done by the same core version

102704 of 181534 branches covered (56.58%)

138 of 153 new or added lines in 10 files covered. (90.2%)

85 existing lines in 16 files now uncovered.

216717 of 237917 relevant lines covered (91.09%)

5947762.1 hits per line

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

68.01
/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
constexpr static std::string_view
11
    s_dict_key_wrong_type_err("%1: Dictionary key is not a string on field '%2' in class '%3'");
12
constexpr static std::string_view
13
    s_list_index_wrong_type_err("%1: List index is not an integer on field '%2' in class '%3'");
14
constexpr static std::string_view
15
    s_wrong_collection_type_err("%1: Not a list or dictionary on field '%2' in class '%3'");
16

17
REALM_NORETURN void throw_bad_transaction_log(std::string msg)
18
{
8✔
19
    throw BadChangesetError{std::move(msg)};
8✔
20
}
8✔
21

22
} // namespace
23

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

62
template <class... Params>
63
REALM_NORETURN void InstructionApplier::bad_transaction_log(const char* msg, Params&&... params) const
64
{
8✔
65
    // FIXME: Avoid throwing in normal program flow (since changesets can come
66
    // in over the network, defective changesets are part of normal program
67
    // flow).
68
    bad_transaction_log(util::format(msg, std::forward<Params>(params)...));
8✔
69
}
8✔
70

71
StringData InstructionApplier::get_string(InternString str) const
72
{
790,818✔
73
    auto string = m_log->try_get_intern_string(str);
790,818✔
74
    if (REALM_UNLIKELY(!string))
790,818✔
75
        bad_transaction_log("string read fails");
×
76
    return m_log->get_string(*string);
790,818✔
77
}
790,818✔
78

79
StringData InstructionApplier::get_string(StringBufferRange range) const
80
{
234,114✔
81
    auto string = m_log->try_get_string(range);
234,114✔
82
    if (!string)
234,114✔
83
        bad_transaction_log("string read error");
×
84
    return *string;
234,114✔
85
}
234,114✔
86

87
BinaryData InstructionApplier::get_binary(StringBufferRange range) const
88
{
11,880✔
89
    auto string = m_log->try_get_string(range);
11,880✔
90
    if (!string)
11,880✔
91
        bad_transaction_log("binary read error");
×
92
    return BinaryData{string->data(), string->size()};
11,880✔
93
}
11,880✔
94

95
TableRef InstructionApplier::table_for_class_name(StringData class_name) const
96
{
112✔
97
    if (class_name.size() > Group::max_class_name_length)
112✔
98
        bad_transaction_log("class name too long");
×
99
    Group::TableNameBuffer buffer;
112✔
100
    return m_transaction.get_table(Group::class_name_to_table_name(class_name, buffer));
112✔
101
}
112✔
102

103
template <typename T>
104
struct TemporarySwapOut {
105
    explicit TemporarySwapOut(T& target)
106
        : target(target)
6,562✔
107
        , backup()
6,562✔
108
    {
14,084✔
109
        std::swap(target, backup);
14,084✔
110
    }
14,084✔
111

112
    ~TemporarySwapOut()
113
    {
14,084✔
114
        std::swap(backup, target);
14,084✔
115
    }
14,084✔
116

117
    T& target;
118
    T backup;
119
};
120

121
void InstructionApplier::operator()(const Instruction::AddTable& instr)
122
{
4,590✔
123
    auto table_name = get_table_name(instr);
4,590✔
124

125
    // Temporarily swap out the last object key so it doesn't get included in error messages
126
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
4,590✔
127

128
    auto add_table = util::overload{
4,590✔
129
        [&](const Instruction::AddTable::TopLevelTable& spec) {
4,590✔
130
            auto table_type = (spec.is_asymmetric ? Table::Type::TopLevelAsymmetric : Table::Type::TopLevel);
4,294✔
131
            if (spec.pk_type == Instruction::Payload::Type::GlobalKey) {
4,294✔
132
                m_transaction.get_or_add_table(table_name, table_type);
8✔
133
            }
8✔
134
            else {
4,286✔
135
                if (!is_valid_key_type(spec.pk_type)) {
4,286✔
136
                    bad_transaction_log("Invalid primary key type '%1' while adding table '%2'", int8_t(spec.pk_type),
×
137
                                        table_name);
×
138
                }
×
139
                DataType pk_type = get_data_type(spec.pk_type);
4,286✔
140
                StringData pk_field = get_string(spec.pk_field);
4,286✔
141
                bool nullable = spec.pk_nullable;
4,286✔
142

143
                if (!m_transaction.get_or_add_table_with_primary_key(table_name, pk_type, pk_field, nullable,
4,286✔
144
                                                                     table_type)) {
4,286✔
145
                    bad_transaction_log("AddTable: The existing table '%1' has different properties", table_name);
×
146
                }
×
147
            }
4,286✔
148
        },
4,294✔
149
        [&](const Instruction::AddTable::EmbeddedTable&) {
4,590✔
150
            if (TableRef table = m_transaction.get_table(table_name)) {
296✔
151
                if (!table->is_embedded()) {
×
152
                    bad_transaction_log("AddTable: The existing table '%1' is not embedded", table_name);
×
153
                }
×
154
            }
×
155
            else {
296✔
156
                m_transaction.add_table(table_name, Table::Type::Embedded);
296✔
157
            }
296✔
158
        },
296✔
159
    };
4,590✔
160

161
    mpark::visit(std::move(add_table), instr.type);
4,590✔
162
}
4,590✔
163

164
void InstructionApplier::operator()(const Instruction::EraseTable& instr)
165
{
148✔
166
    auto table_name = get_table_name(instr);
148✔
167
    // Temporarily swap out the last object key so it doesn't get included in error messages
168
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
148✔
169

170
    if (REALM_UNLIKELY(REALM_COVER_NEVER(!m_transaction.has_table(table_name)))) {
148✔
171
        // FIXME: Should EraseTable be considered idempotent?
172
        bad_transaction_log("table does not exist");
×
173
    }
×
174

175
    m_transaction.remove_table(table_name);
148✔
176
}
148✔
177

178
void InstructionApplier::operator()(const Instruction::CreateObject& instr)
179
{
65,238✔
180
    auto table = get_table(instr);
65,238✔
181
    ColKey pk_col = table->get_primary_key_column();
65,238✔
182
    m_last_object_key = instr.object;
65,238✔
183

184
    mpark::visit(util::overload{
65,238✔
185
                     [&](mpark::monostate) {
65,238✔
186
                         if (!pk_col) {
32✔
187
                             bad_transaction_log("CreateObject(NULL) on table without a primary key");
×
188
                         }
×
189
                         if (!table->is_nullable(pk_col)) {
32✔
190
                             bad_transaction_log("CreateObject(NULL) on a table with a non-nullable primary key");
×
191
                         }
×
192
                         m_last_object = table->create_object_with_primary_key(util::none);
32✔
193
                     },
32✔
194
                     [&](int64_t pk) {
65,238✔
195
                         if (!pk_col) {
37,386✔
196
                             bad_transaction_log("CreateObject(Int) on table without a primary key");
×
197
                         }
×
198
                         if (table->get_column_type(pk_col) != type_Int) {
37,386✔
199
                             bad_transaction_log("CreateObject(Int) on a table with primary key type %1",
×
200
                                                 table->get_column_type(pk_col));
×
201
                         }
×
202
                         m_last_object = table->create_object_with_primary_key(pk);
37,386✔
203
                     },
37,386✔
204
                     [&](InternString pk) {
65,238✔
205
                         if (!pk_col) {
954✔
206
                             bad_transaction_log("CreateObject(String) on table without a primary key");
×
207
                         }
×
208
                         if (table->get_column_type(pk_col) != type_String) {
954✔
209
                             bad_transaction_log("CreateObject(String) on a table with primary key type %1",
×
210
                                                 table->get_column_type(pk_col));
×
211
                         }
×
212
                         StringData str = get_string(pk);
954✔
213
                         m_last_object = table->create_object_with_primary_key(str);
954✔
214
                     },
954✔
215
                     [&](const ObjectId& id) {
65,238✔
216
                         if (!pk_col) {
26,810✔
217
                             bad_transaction_log("CreateObject(ObjectId) on table without a primary key");
×
218
                         }
×
219
                         if (table->get_column_type(pk_col) != type_ObjectId) {
26,810✔
220
                             bad_transaction_log("CreateObject(ObjectId) on a table with primary key type %1",
×
221
                                                 table->get_column_type(pk_col));
×
222
                         }
×
223
                         m_last_object = table->create_object_with_primary_key(id);
26,810✔
224
                     },
26,810✔
225
                     [&](const UUID& id) {
65,238✔
226
                         if (!pk_col) {
48✔
227
                             bad_transaction_log("CreateObject(UUID) on table without a primary key");
×
228
                         }
×
229
                         if (table->get_column_type(pk_col) != type_UUID) {
48✔
230
                             bad_transaction_log("CreateObject(UUID) on a table with primary key type %1",
×
231
                                                 table->get_column_type(pk_col));
×
232
                         }
×
233
                         m_last_object = table->create_object_with_primary_key(id);
48✔
234
                     },
48✔
235
                     [&](GlobalKey key) {
65,238✔
236
                         if (pk_col) {
12✔
237
                             bad_transaction_log("CreateObject(GlobalKey) on table with a primary key");
×
238
                         }
×
239
                         m_last_object = table->create_object(key);
12✔
240
                     },
12✔
241
                 },
65,238✔
242
                 instr.object);
65,238✔
243
}
65,238✔
244

245
void InstructionApplier::operator()(const Instruction::EraseObject& instr)
246
{
26,114✔
247
    // FIXME: Log actions.
248
    // Note: EraseObject is idempotent.
249
    if (auto obj = get_top_object(instr, "EraseObject")) {
26,114✔
250
        // This call will prevent incoming links to be nullified/deleted
251
        obj->invalidate();
5,430✔
252
    }
5,430✔
253
    m_last_object.reset();
26,114✔
254
}
26,114✔
255

256
template <class F>
257
void InstructionApplier::visit_payload(const Instruction::Payload& payload, F&& visitor)
258
{
713,514✔
259
    using Type = Instruction::Payload::Type;
713,514✔
260

261
    const auto& data = payload.data;
713,514✔
262
    switch (payload.type) {
713,514✔
263
        case Type::ObjectValue:
2,676✔
264
            return visitor(Instruction::Payload::ObjectValue{});
2,676✔
265
        case Type::Set:
✔
266
            return visitor(Instruction::Payload::Set{});
×
267
        case Type::List:
840✔
268
            return visitor(Instruction::Payload::List{});
840✔
269
        case Type::Dictionary:
712✔
270
            return visitor(Instruction::Payload::Dictionary{});
712✔
271
        case Type::Erased:
240✔
272
            return visitor(Instruction::Payload::Erased{});
240✔
273
        case Type::GlobalKey:
✔
274
            return visitor(realm::util::none); // FIXME: Not sure about this
×
275
        case Type::Null:
398✔
276
            return visitor(realm::util::none);
398✔
277
        case Type::Int:
454,274✔
278
            return visitor(data.integer);
454,274✔
279
        case Type::Bool:
184✔
280
            return visitor(data.boolean);
184✔
281
        case Type::String: {
234,114✔
282
            StringData value = get_string(data.str);
234,114✔
283
            return visitor(value);
234,114✔
284
        }
×
285
        case Type::Binary: {
11,880✔
286
            BinaryData value = get_binary(data.binary);
11,880✔
287
            return visitor(value);
11,880✔
288
        }
×
289
        case Type::Timestamp:
3,102✔
290
            return visitor(data.timestamp);
3,102✔
291
        case Type::Float:
260✔
292
            return visitor(data.fnum);
260✔
293
        case Type::Double:
1,192✔
294
            return visitor(data.dnum);
1,192✔
295
        case Type::Decimal:
168✔
296
            return visitor(data.decimal);
168✔
297
        case Type::Link: {
2,048✔
298
            StringData class_name = get_string(data.link.target_table);
2,048✔
299
            Group::TableNameBuffer buffer;
2,048✔
300
            StringData target_table_name = Group::class_name_to_table_name(class_name, buffer);
2,048✔
301
            TableRef target_table = m_transaction.get_table(target_table_name);
2,048✔
302
            if (!target_table) {
2,048✔
303
                bad_transaction_log("Link with invalid target table '%1'", target_table_name);
×
304
            }
×
305
            if (target_table->is_embedded()) {
2,048✔
306
                bad_transaction_log("Link to embedded table '%1'", target_table_name);
×
307
            }
×
308
            ObjKey target = get_object_key(*target_table, data.link.target);
2,048✔
309
            ObjLink link = ObjLink{target_table->get_key(), target};
2,048✔
310
            return visitor(link);
2,048✔
311
        }
×
312
        case Type::ObjectId:
1,128✔
313
            return visitor(data.object_id);
1,128✔
314
        case Type::UUID:
304✔
315
            return visitor(data.uuid);
304✔
316
    }
713,514✔
317
}
713,514✔
318

319
void InstructionApplier::operator()(const Instruction::Update& instr)
320
{
527,132✔
321
    struct UpdateResolver : public PathResolver {
527,132✔
322
        UpdateResolver(InstructionApplier* applier, const Instruction::Update& instr)
527,132✔
323
            : PathResolver(applier, instr, "Update")
527,132✔
324
            , m_instr(instr)
527,132✔
325
        {
527,134✔
326
        }
527,120✔
327
        Status on_property(Obj& obj, ColKey col) override
527,132✔
328
        {
527,132✔
329
            // Update of object field.
330

331
            auto table = obj.get_table();
523,318✔
332
            auto table_name = table->get_name();
523,318✔
333
            auto field_name = table->get_column_name(col);
523,318✔
334
            auto data_type = DataType(col.get_type());
523,318✔
335

336
            auto visitor = [&](const mpark::variant<ObjLink, Mixed, Instruction::Payload::ObjectValue,
523,318✔
337
                                                    Instruction::Payload::Dictionary, Instruction::Payload::List,
523,318✔
338
                                                    Instruction::Payload::Set, Instruction::Payload::Erased>& arg) {
523,334✔
339
                if (const auto link_ptr = mpark::get_if<ObjLink>(&arg)) {
523,334✔
340
                    if (data_type == type_Mixed || data_type == type_TypedLink) {
84✔
341
                        obj.set_any(col, *link_ptr, m_instr.is_default);
×
342
                    }
×
343
                    else if (data_type == type_Link) {
84✔
344
                        // Validate target table.
345
                        auto target_table = table->get_link_target(col);
84✔
346
                        if (target_table->get_key() != link_ptr->get_table_key()) {
84✔
347
                            m_applier->bad_transaction_log(
×
348
                                "Update: Target table mismatch (expected %1, got %2)", target_table->get_name(),
×
349
                                m_applier->m_transaction.get_table(link_ptr->get_table_key())->get_name());
×
350
                        }
×
351
                        obj.set<ObjKey>(col, link_ptr->get_obj_key(), m_instr.is_default);
84✔
352
                    }
84✔
353
                    else {
×
354
                        m_applier->bad_transaction_log("Update: Type mismatch in '%2.%1' (expected %3, got %4)",
×
355
                                                       field_name, table_name, col.get_type(), type_Link);
×
356
                    }
×
357
                }
84✔
358
                else if (const auto mixed_ptr = mpark::get_if<Mixed>(&arg)) {
523,250✔
359
                    if (mixed_ptr->is_null()) {
521,550✔
360
                        if (col.is_nullable()) {
218✔
361
                            obj.set_null(col, m_instr.is_default);
218✔
362
                        }
218✔
363
                        else {
×
364
                            m_applier->bad_transaction_log("Update: NULL in non-nullable field '%2.%1'", field_name,
×
365
                                                           table_name);
×
366
                        }
×
367
                    }
218✔
368
                    else if (data_type == type_Mixed || mixed_ptr->get_type() == data_type) {
521,336✔
369
                        obj.set_any(col, *mixed_ptr, m_instr.is_default);
521,262✔
370
                    }
521,262✔
371
                    else {
2,147,483,721✔
372
                        m_applier->bad_transaction_log("Update: Type mismatch in '%2.%1' (expected %3, got %4)",
2,147,483,721✔
373
                                                       field_name, table_name, col.get_type(), mixed_ptr->get_type());
2,147,483,721✔
374
                    }
2,147,483,721✔
375
                }
521,550✔
376
                else if (mpark::get_if<Instruction::Payload::ObjectValue>(&arg)) {
1,700✔
377
                    if (obj.is_null(col)) {
652✔
378
                        obj.create_and_set_linked_object(col);
624✔
379
                    }
624✔
380
                }
652✔
381
                else if (mpark::get_if<Instruction::Payload::Erased>(&arg)) {
1,048✔
382
                    m_applier->bad_transaction_log("Update: Dictionary erase at object field");
×
383
                }
×
384
                else if (mpark::get_if<Instruction::Payload::Dictionary>(&arg)) {
1,048✔
385
                    obj.set_collection(col, CollectionType::Dictionary);
368✔
386
                }
368✔
387
                else if (mpark::get_if<Instruction::Payload::List>(&arg)) {
680✔
388
                    obj.set_collection(col, CollectionType::List);
456✔
389
                }
456✔
390
                else if (mpark::get_if<Instruction::Payload::Set>(&arg)) {
224✔
391
                    obj.set_collection(col, CollectionType::Set);
×
392
                }
×
393
            };
523,334✔
394

395
            m_applier->visit_payload(m_instr.value, visitor);
523,318✔
396
            return Status::Pending;
523,318✔
397
        }
523,318✔
398
        Status on_list_index(LstBase& list, uint32_t index) override
527,132✔
399
        {
527,132✔
400
            // Update of list element.
401

402
            auto col = list.get_col_key();
390✔
403
            auto data_type = DataType(col.get_type());
390✔
404
            auto table = list.get_table();
390✔
405
            auto table_name = table->get_name();
390✔
406
            auto field_name = table->get_column_name(col);
390✔
407

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

477
            m_applier->visit_payload(m_instr.value, visitor);
390✔
478
            return Status::Pending;
390✔
479
        }
390✔
480
        Status on_dictionary_key(Dictionary& dict, Mixed key) override
527,132✔
481
        {
527,132✔
482
            // Update (insert) of dictionary element.
483

484
            auto visitor = util::overload{
3,864✔
485
                [&](Mixed value) {
3,864✔
486
                    if (value.is_null()) {
2,824✔
487
                        // FIXME: Separate handling of NULL is needed because
488
                        // `Mixed::get_type()` asserts on NULL.
489
                        dict.insert(key, value);
112✔
490
                    }
112✔
491
                    else if (value.get_type() == type_Link) {
2,712✔
492
                        m_applier->bad_transaction_log("Update: Untyped links are not supported in dictionaries.");
×
493
                    }
×
494
                    else {
2,712✔
495
                        dict.insert(key, value);
2,712✔
496
                    }
2,712✔
497
                },
2,824✔
498
                [&](const Instruction::Payload::Erased&) {
3,864✔
499
                    dict.try_erase(key);
240✔
500
                },
240✔
501
                [&](const Instruction::Payload::ObjectValue&) {
3,864✔
502
                    dict.create_and_insert_linked_object(key);
364✔
503
                },
364✔
504
                [&](const Instruction::Payload::Dictionary&) {
3,864✔
505
                    dict.insert_collection(key.get_string(), CollectionType::Dictionary);
180✔
506
                },
180✔
507
                [&](const Instruction::Payload::List&) {
3,864✔
508
                    dict.insert_collection(key.get_string(), CollectionType::List);
256✔
509
                },
256✔
510
                [&](const Instruction::Payload::Set&) {
3,864✔
511
                    dict.insert_collection(key.get_string(), CollectionType::Set);
×
512
                },
×
513
            };
3,864✔
514

515
            m_applier->visit_payload(m_instr.value, visitor);
3,864✔
516
            return Status::Pending;
3,864✔
517
        }
3,864✔
518

519
    private:
527,132✔
520
        const Instruction::Update& m_instr;
527,132✔
521
    };
527,132✔
522
    UpdateResolver resolver(this, instr);
527,132✔
523
    resolver.resolve();
527,132✔
524
}
527,132✔
525

526
void InstructionApplier::operator()(const Instruction::AddInteger& instr)
527
{
2,692✔
528
    // FIXME: Implement increments of array elements, dictionary values.
529
    struct AddIntegerResolver : public PathResolver {
2,692✔
530
        AddIntegerResolver(InstructionApplier* applier, const Instruction::AddInteger& instr)
2,692✔
531
            : PathResolver(applier, instr, "AddInteger")
2,692✔
532
            , m_instr(instr)
2,692✔
533
        {
2,692✔
534
        }
2,692✔
535
        Status on_property(Obj& obj, ColKey col)
2,692✔
536
        {
2,692✔
537
            // Increment of object field.
538
            if (!obj.is_null(col)) {
2,692✔
539
                try {
2,636✔
540
                    obj.add_int(col, m_instr.value);
2,636✔
541
                }
2,636✔
542
                catch (const LogicError&) {
2,636✔
543
                    auto table = obj.get_table();
×
544
                    m_applier->bad_transaction_log("AddInteger: Not an integer field '%2.%1'",
×
545
                                                   table->get_column_name(col), table->get_name());
×
546
                }
×
547
            }
2,636✔
548
            return Status::Pending;
2,692✔
549
        }
2,692✔
550

551
    private:
2,692✔
552
        const Instruction::AddInteger& m_instr;
2,692✔
553
    };
2,692✔
554
    AddIntegerResolver resolver(this, instr);
2,692✔
555
    resolver.resolve();
2,692✔
556
}
2,692✔
557

558
void InstructionApplier::operator()(const Instruction::AddColumn& instr)
559
{
9,346✔
560
    using Type = Instruction::Payload::Type;
9,346✔
561
    using CollectionType = Instruction::CollectionType;
9,346✔
562

563
    // Temporarily swap out the last object key so it doesn't get included in error messages
564
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
9,346✔
565

566
    auto table = get_table(instr, "AddColumn");
9,346✔
567
    auto col_name = get_string(instr.field);
9,346✔
568

569
    if (ColKey existing_key = table->get_column_key(col_name)) {
9,346✔
570
        DataType new_type = get_data_type(instr.type);
372✔
571
        ColumnType existing_type = existing_key.get_type();
372✔
572
        if (existing_type != ColumnType(new_type)) {
372✔
573
            bad_transaction_log("AddColumn: Schema mismatch for existing column in '%1.%2' (expected %3, got %4)",
8✔
574
                                table->get_name(), col_name, existing_type, new_type);
8✔
575
        }
8✔
576
        bool existing_is_list = existing_key.is_list();
372✔
577
        if ((instr.collection_type == CollectionType::List) != existing_is_list) {
372✔
578
            bad_transaction_log(
×
579
                "AddColumn: Schema mismatch for existing column in '%1.%2' (existing is%3 a list, the other is%4)",
×
580
                table->get_name(), col_name, existing_is_list ? "" : " not", existing_is_list ? " not" : "");
×
581
        }
×
582
        bool existing_is_set = existing_key.is_set();
372✔
583
        if ((instr.collection_type == CollectionType::Set) != existing_is_set) {
372✔
584
            bad_transaction_log(
×
585
                "AddColumn: Schema mismatch for existing column in '%1.%2' (existing is%3 a set, the other is%4)",
×
586
                table->get_name(), col_name, existing_is_set ? "" : " not", existing_is_set ? " not" : "");
×
587
        }
×
588
        bool existing_is_dict = existing_key.is_dictionary();
372✔
589
        if ((instr.collection_type == CollectionType::Dictionary) != existing_is_dict) {
372✔
590
            bad_transaction_log("AddColumn: Schema mismatch for existing column in '%1.%2' (existing is%3 a "
×
591
                                "dictionary, the other is%4)",
×
592
                                table->get_name(), col_name, existing_is_dict ? "" : " not",
×
593
                                existing_is_dict ? " not" : "");
×
594
        }
×
595
        if (new_type == type_Link) {
372✔
596
            Group::TableNameBuffer buffer;
16✔
597
            auto target_table_name = Group::class_name_to_table_name(get_string(instr.link_target_table), buffer);
16✔
598
            if (target_table_name != table->get_link_target(existing_key)->get_name()) {
16✔
599
                bad_transaction_log("AddColumn: Schema mismatch for existing column in '%1.%2' (link targets differ)",
×
600
                                    table->get_name(), col_name);
×
601
            }
×
602
        }
16✔
603
        return;
372✔
604
    }
372✔
605

606
    if (instr.collection_type == CollectionType::Dictionary && instr.key_type != Type::String) {
8,974✔
607
        bad_transaction_log("AddColumn '%1.%3' adding dictionary column with non-string keys", table->get_name(),
×
608
                            col_name);
×
609
    }
×
610

611
    if (instr.type != Type::Link) {
8,974✔
612
        DataType type = get_data_type(instr.type);
7,634✔
613
        switch (instr.collection_type) {
7,634✔
614
            case CollectionType::Single: {
6,674✔
615
                table->add_column(type, col_name, instr.nullable);
6,674✔
616
                break;
6,674✔
617
            }
×
618
            case CollectionType::List: {
620✔
619
                table->add_column_list(type, col_name, instr.nullable);
620✔
620
                break;
620✔
621
            }
×
622
            case CollectionType::Dictionary: {
204✔
623
                DataType key_type = get_data_type(instr.key_type);
204✔
624
                table->add_column_dictionary(type, col_name, instr.nullable, key_type);
204✔
625
                break;
204✔
626
            }
×
627
            case CollectionType::Set: {
136✔
628
                table->add_column_set(type, col_name, instr.nullable);
136✔
629
                break;
136✔
630
            }
×
631
        }
7,634✔
632
    }
7,634✔
633
    else {
1,340✔
634
        Group::TableNameBuffer buffer;
1,340✔
635
        auto target_table_name = get_string(instr.link_target_table);
1,340✔
636
        if (target_table_name.size() != 0) {
1,340✔
637
            TableRef target = m_transaction.get_table(Group::class_name_to_table_name(target_table_name, buffer));
1,340✔
638
            if (!target) {
1,340✔
639
                bad_transaction_log("AddColumn(Link) '%1.%2' to table '%3' which doesn't exist", table->get_name(),
×
640
                                    col_name, target_table_name);
×
641
            }
×
642
            if (instr.collection_type == CollectionType::List) {
1,340✔
643
                table->add_column_list(*target, col_name);
616✔
644
            }
616✔
645
            else if (instr.collection_type == CollectionType::Set) {
724✔
646
                table->add_column_set(*target, col_name);
8✔
647
            }
8✔
648
            else if (instr.collection_type == CollectionType::Dictionary) {
716✔
649
                table->add_column_dictionary(*target, col_name);
32✔
650
            }
32✔
651
            else {
684✔
652
                REALM_ASSERT(instr.collection_type == CollectionType::Single);
684✔
653
                table->add_column(*target, col_name);
684✔
654
            }
684✔
655
        }
1,340✔
UNCOV
656
        else {
×
UNCOV
657
            if (instr.collection_type == CollectionType::List) {
×
658
                table->add_column_list(type_TypedLink, col_name);
×
659
            }
×
UNCOV
660
            else {
×
UNCOV
661
                REALM_ASSERT(instr.collection_type == CollectionType::Single);
×
UNCOV
662
                table->add_column(type_TypedLink, col_name);
×
UNCOV
663
            }
×
UNCOV
664
        }
×
665
    }
1,340✔
666
}
8,974✔
667

668
void InstructionApplier::operator()(const Instruction::EraseColumn& instr)
669
{
×
670
    // Temporarily swap out the last object key so it doesn't get included in error messages
671
    TemporarySwapOut<decltype(m_last_object_key)> last_object_key_guard(m_last_object_key);
×
672

673
    auto table = get_table(instr, "EraseColumn");
×
674
    auto col_name = get_string(instr.field);
×
675

676
    ColKey col = table->get_column_key(col_name);
×
677
    if (!col) {
×
678
        bad_transaction_log("EraseColumn '%1.%2' which doesn't exist", table->get_name(), col_name);
×
679
    }
×
680

681
    table->remove_column(col);
×
682
}
×
683

684
void InstructionApplier::operator()(const Instruction::ArrayInsert& instr)
685
{
183,688✔
686
    struct ArrayInsertResolver : public PathResolver {
183,688✔
687
        ArrayInsertResolver(InstructionApplier* applier, const Instruction::ArrayInsert& instr)
183,688✔
688
            : PathResolver(applier, instr, "ArrayInsert")
183,688✔
689
            , m_instr(instr)
183,688✔
690
        {
183,688✔
691
        }
183,688✔
692
        Status on_list_index(LstBase& list, uint32_t index) override
183,688✔
693
        {
183,688✔
694
            auto data_type = list.get_data_type();
183,688✔
695
            auto table = list.get_table();
183,688✔
696
            auto table_name = table->get_name();
183,688✔
697
            auto field_name = [&] {
183,688✔
698
                return table->get_column_name(list.get_col_key());
×
699
            };
×
700

701
            if (index > m_instr.prior_size) {
183,688✔
702
                m_applier->bad_transaction_log("ArrayInsert: Invalid insertion index (index = %1, prior_size = %2)",
×
703
                                               index, m_instr.prior_size);
×
704
            }
×
705

706
            if (index > list.size()) {
183,688✔
707
                m_applier->bad_transaction_log("ArrayInsert: Index out of bounds (%1 > %2)", index, list.size());
×
708
            }
×
709

710
            if (m_instr.prior_size != list.size()) {
183,688✔
711
                m_applier->bad_transaction_log("ArrayInsert: Invalid prior_size (list size = %1, prior_size = %2)",
×
712
                                               list.size(), m_instr.prior_size);
×
713
            }
×
714

715
            auto inserter = util::overload{
183,688✔
716
                [&](const ObjLink& link) {
183,688✔
717
                    if (data_type == type_TypedLink) {
1,252✔
718
                        REALM_ASSERT(dynamic_cast<Lst<ObjLink>*>(&list));
×
719
                        auto& link_list = static_cast<Lst<ObjLink>&>(list);
×
720
                        link_list.insert(index, link);
×
721
                    }
×
722
                    else if (data_type == type_Mixed) {
1,252✔
723
                        REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
164✔
724
                        auto& mixed_list = static_cast<Lst<Mixed>&>(list);
164✔
725
                        mixed_list.insert(index, link);
164✔
726
                    }
164✔
727
                    else if (data_type == type_Link) {
1,088✔
728
                        REALM_ASSERT(dynamic_cast<Lst<ObjKey>*>(&list));
1,088✔
729
                        auto& link_list = static_cast<Lst<ObjKey>&>(list);
1,088✔
730
                        // Validate the target.
731
                        auto target_table = table->get_link_target(list.get_col_key());
1,088✔
732
                        if (target_table->get_key() != link.get_table_key()) {
1,088✔
733
                            m_applier->bad_transaction_log(
×
734
                                "ArrayInsert: Target table mismatch (expected '%1', got '%2')",
×
735
                                target_table->get_name(),
×
736
                                m_applier->m_transaction.get_table(link.get_table_key())->get_name());
×
737
                        }
×
738
                        link_list.insert(index, link.get_obj_key());
1,088✔
739
                    }
1,088✔
740
                    else {
×
741
                        m_applier->bad_transaction_log(
×
742
                            "ArrayInsert: Type mismatch in list at '%2.%1' (expected link type, was %3)",
×
743
                            field_name(), table_name, data_type);
×
744
                    }
×
745
                },
1,252✔
746
                [&](Mixed value) {
183,688✔
747
                    if (data_type == type_Mixed) {
180,552✔
748
                        list.insert_any(index, value);
728✔
749
                    }
728✔
750
                    else if (value.is_null()) {
179,824✔
751
                        if (list.get_col_key().is_nullable()) {
48✔
752
                            list.insert_null(index);
48✔
753
                        }
48✔
754
                        else {
×
755
                            m_applier->bad_transaction_log("ArrayInsert: NULL in non-nullable list '%2.%1'",
×
756
                                                           field_name(), table_name);
×
757
                        }
×
758
                    }
48✔
759
                    else {
179,776✔
760
                        if (value.get_type() == data_type) {
179,776✔
761
                            list.insert_any(index, value);
179,776✔
762
                        }
179,776✔
763
                        else {
×
764
                            m_applier->bad_transaction_log(
×
765
                                "ArrayInsert: Type mismatch in list at '%2.%1' (expected %3, got %4)", field_name(),
×
766
                                table_name, data_type, value.get_type());
×
767
                        }
×
768
                    }
179,776✔
769
                },
180,552✔
770
                [&](const Instruction::Payload::ObjectValue&) {
183,688✔
771
                    if (data_type == type_Link) {
1,660✔
772
                        auto target_table = list.get_table()->get_link_target(list.get_col_key());
1,660✔
773
                        if (!target_table->is_embedded()) {
1,660✔
774
                            m_applier->bad_transaction_log(
×
775
                                "ArrayInsert: Creation of embedded object of type '%1', which is not "
×
776
                                "an embedded table",
×
777
                                target_table->get_name());
×
778
                        }
×
779

780
                        REALM_ASSERT(dynamic_cast<LnkLst*>(&list));
1,660✔
781
                        auto& link_list = static_cast<LnkLst&>(list);
1,660✔
782
                        link_list.create_and_insert_linked_object(index);
1,660✔
783
                    }
1,660✔
784
                    else {
×
785
                        m_applier->bad_transaction_log(
×
786
                            "ArrayInsert: Creation of embedded object in non-link list field '%2.%1'", field_name(),
×
787
                            table_name);
×
788
                    }
×
789
                },
1,660✔
790
                [&](const Instruction::Payload::Dictionary&) {
183,688✔
791
                    REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
148✔
792
                    auto& mixed_list = static_cast<Lst<Mixed>&>(list);
148✔
793
                    mixed_list.insert_collection(size_t(index), CollectionType::Dictionary);
148✔
794
                },
148✔
795
                [&](const Instruction::Payload::List&) {
183,688✔
796
                    REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
76✔
797
                    auto& mixed_list = static_cast<Lst<Mixed>&>(list);
76✔
798
                    mixed_list.insert_collection(size_t(index), CollectionType::List);
76✔
799
                },
76✔
800
                [&](const Instruction::Payload::Set&) {
183,688✔
801
                    REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
×
802
                    auto& mixed_list = static_cast<Lst<Mixed>&>(list);
×
803
                    mixed_list.insert_collection(size_t(index), CollectionType::Set);
×
804
                },
×
805
                [&](const Instruction::Payload::Erased&) {
183,688✔
806
                    m_applier->bad_transaction_log("Dictionary erase payload for ArrayInsert");
×
807
                },
×
808
            };
183,688✔
809

810
            m_applier->visit_payload(m_instr.value, inserter);
183,688✔
811
            return Status::Pending;
183,688✔
812
        }
183,688✔
813

814
    private:
183,688✔
815
        const Instruction::ArrayInsert& m_instr;
183,688✔
816
    };
183,688✔
817
    ArrayInsertResolver(this, instr).resolve();
183,688✔
818
}
183,688✔
819

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

849
    private:
112✔
850
        const Instruction::ArrayMove& m_instr;
112✔
851
    };
112✔
852
    ArrayMoveResolver(this, instr).resolve();
112✔
853
}
112✔
854

855
void InstructionApplier::operator()(const Instruction::ArrayErase& instr)
856
{
342✔
857
    struct ArrayEraseResolver : public PathResolver {
342✔
858
        ArrayEraseResolver(InstructionApplier* applier, const Instruction::ArrayErase& instr)
342✔
859
            : PathResolver(applier, instr, "ArrayErase")
342✔
860
            , m_instr(instr)
342✔
861
        {
342✔
862
        }
342✔
863
        Status on_list_index(LstBase& list, uint32_t index) override
342✔
864
        {
342✔
865
            if (index >= m_instr.prior_size) {
342✔
866
                m_applier->bad_transaction_log("ArrayErase: Invalid index (index = %1, prior_size = %2)", index,
×
867
                                               m_instr.prior_size);
×
868
            }
×
869
            if (index >= list.size()) {
342✔
870
                m_applier->bad_transaction_log("ArrayErase: Index out of bounds (%1 >= %2)", index, list.size());
×
871
            }
×
872
            if (m_instr.prior_size != list.size()) {
342✔
873
                m_applier->bad_transaction_log("ArrayErase: Invalid prior_size (list size = %1, prior_size = %2)",
×
874
                                               list.size(), m_instr.prior_size);
×
875
            }
×
876
            list.remove(index, index + 1);
342✔
877
            return Status::Pending;
342✔
878
        }
342✔
879

880
    private:
342✔
881
        const Instruction::ArrayErase& m_instr;
342✔
882
    };
342✔
883
    ArrayEraseResolver(this, instr).resolve();
342✔
884
}
342✔
885

886
void InstructionApplier::operator()(const Instruction::Clear& instr)
887
{
558✔
888
    // For collections and nested collections in Mixed, applying a Clear instruction
889
    // implicitly sets the collection type (of the clear instruction) too.
890
    struct ClearResolver : public PathResolver {
558✔
891
        ClearResolver(InstructionApplier* applier, const Instruction::Clear& instr)
558✔
892
            : PathResolver(applier, instr, "Clear")
558✔
893
        {
558✔
894
            switch (instr.collection_type) {
558✔
895
                case Instruction::CollectionType::Single:
✔
896
                    break;
×
897
                case Instruction::CollectionType::List:
294✔
898
                    m_collection_type = CollectionType::List;
294✔
899
                    break;
294✔
900
                case Instruction::CollectionType::Dictionary:
200✔
901
                    m_collection_type = CollectionType::Dictionary;
200✔
902
                    break;
200✔
903
                case Instruction::CollectionType::Set:
64✔
904
                    m_collection_type = CollectionType::Set;
64✔
905
                    break;
64✔
906
            }
558✔
907
        }
558✔
908
        void on_list(LstBase& list) override
558✔
909
        {
558✔
910
            // list property
911
            if (m_collection_type && *m_collection_type != CollectionType::List) {
94✔
912
                m_applier->bad_transaction_log("Clear: Not a List");
×
913
            }
×
914
            list.clear();
94✔
915
        }
94✔
916
        Status on_list_index(LstBase& list, uint32_t index) override
558✔
917
        {
558✔
918
            REALM_ASSERT(m_collection_type);
52✔
919
            REALM_ASSERT(dynamic_cast<Lst<Mixed>*>(&list));
52✔
920
            auto& mixed_list = static_cast<Lst<Mixed>&>(list);
52✔
921
            if (index >= mixed_list.size()) {
52✔
922
                m_applier->bad_transaction_log("Clear: Index out of bounds (%1 > %2)", index,
×
923
                                               mixed_list.size()); // Throws
×
924
            }
×
925
            auto val = mixed_list.get(index);
52✔
926
            if (val.is_type(type_Dictionary)) {
52✔
927
                Dictionary d(mixed_list, mixed_list.get_key(index));
24✔
928
                d.clear();
24✔
929
            }
24✔
930
            else if (val.is_type(type_List)) {
28✔
931
                Lst<Mixed> l(mixed_list, mixed_list.get_key(index));
28✔
932
                l.clear();
28✔
933
            }
28✔
934
            else if (val.is_type(type_Set)) {
×
935
                m_applier->bad_transaction_log("Clear: Item at index %1 is a Set",
×
936
                                               index); // Throws
×
937
            }
×
938
            mixed_list.set_collection(size_t(index), *m_collection_type);
52✔
939
            return Status::Pending;
52✔
940
        }
52✔
941
        void on_dictionary(Dictionary& dict) override
558✔
942
        {
558✔
943
            // dictionary property
944
            if (m_collection_type && *m_collection_type != CollectionType::Dictionary) {
48✔
945
                m_applier->bad_transaction_log("Clear: Not a Dictionary");
×
946
            }
×
947
            dict.clear();
48✔
948
        }
48✔
949
        Status on_dictionary_key(Dictionary& dict, Mixed key) override
558✔
950
        {
558✔
951
            REALM_ASSERT(m_collection_type);
36✔
952
            if (auto val = dict.try_get(key)) {
36✔
953
                if (val->is_type(type_Dictionary)) {
36✔
954
                    Dictionary d(dict, dict.build_index(key));
12✔
955
                    d.clear();
12✔
956
                }
12✔
957
                else if (val->is_type(type_List)) {
24✔
958
                    Lst<Mixed> l(dict, dict.build_index(key));
24✔
959
                    l.clear();
24✔
960
                }
24✔
961
                else if (val->is_type(type_Set)) {
×
962
                    m_applier->bad_transaction_log("Clear: Item at key '%1' is a Set",
×
963
                                                   key); // Throws
×
964
                }
×
965
                dict.insert_collection(key.get_string(), *m_collection_type);
36✔
966
                return Status::Pending;
36✔
967
            }
36✔
968
            m_applier->bad_transaction_log("Clear: Key '%1' not found", key);
×
969
            return Status::DidNotResolve;
×
970
        }
36✔
971
        void on_set(SetBase& set) override
558✔
972
        {
558✔
973
            // set property
974
            if (m_collection_type && *m_collection_type != CollectionType::Set) {
64✔
975
                m_applier->bad_transaction_log("Clear: Not a Set");
×
976
            }
×
977
            set.clear();
64✔
978
        }
64✔
979
        Status on_property(Obj& obj, ColKey col_key) override
558✔
980
        {
558✔
981
            if (col_key.get_type() == col_type_Mixed) {
264✔
982
                REALM_ASSERT(m_collection_type);
264✔
983
                auto val = obj.get<Mixed>(col_key);
264✔
984
                if (val.is_type(type_Dictionary)) {
264✔
985
                    Dictionary dict(obj, col_key);
96✔
986
                    dict.clear();
96✔
987
                }
96✔
988
                else if (val.is_type(type_List)) {
168✔
989
                    Lst<Mixed> list(obj, col_key);
152✔
990
                    list.clear();
152✔
991
                }
152✔
992
                else if (val.is_type(type_Set)) {
16✔
993
                    m_applier->bad_transaction_log("Clear: Mixed property is a Set"); // Throws
×
994
                }
×
995
                obj.set_collection(col_key, *m_collection_type);
264✔
996
                return Status::Pending;
264✔
997
            }
264✔
998

999
            return PathResolver::on_property(obj, col_key);
×
1000
        }
264✔
1001

1002
    private:
558✔
1003
        // The server may not send the type for collection properties (non-Mixed)
1004
        // since the clients don't send it either before v13.
1005
        std::optional<CollectionType> m_collection_type;
558✔
1006
    };
558✔
1007
    ClearResolver(this, instr).resolve();
558✔
1008
}
558✔
1009

1010
bool InstructionApplier::allows_null_links(const Instruction::PathInstruction& instr,
1011
                                           const std::string_view& instr_name)
1012
{
40✔
1013
    struct AllowsNullsResolver : public PathResolver {
40✔
1014
        AllowsNullsResolver(InstructionApplier* applier, const Instruction::PathInstruction& instr,
40✔
1015
                            const std::string_view& instr_name)
40✔
1016
            : PathResolver(applier, instr, instr_name)
40✔
1017
            , m_allows_nulls(false)
40✔
1018
        {
40✔
1019
        }
40✔
1020
        Status on_list_index(LstBase&, uint32_t) override
40✔
1021
        {
40✔
1022
            return Status::Pending;
×
1023
        }
×
1024
        void on_list(LstBase&) override {}
40✔
1025
        void on_set(SetBase&) override {}
40✔
1026
        void on_dictionary(Dictionary&) override
40✔
1027
        {
40✔
1028
            m_allows_nulls = true;
×
1029
        }
×
1030
        Status on_dictionary_key(Dictionary&, Mixed) override
40✔
1031
        {
40✔
1032
            m_allows_nulls = true;
40✔
1033
            return Status::Pending;
40✔
1034
        }
40✔
1035
        Status on_property(Obj&, ColKey) override
40✔
1036
        {
40✔
1037
            m_allows_nulls = true;
×
1038
            return Status::Pending;
×
1039
        }
×
1040
        bool allows_nulls()
40✔
1041
        {
40✔
1042
            resolve();
40✔
1043
            return m_allows_nulls;
40✔
1044
        }
40✔
1045

1046
    private:
40✔
1047
        bool m_allows_nulls;
40✔
1048
    };
40✔
1049
    return AllowsNullsResolver(this, instr, instr_name).allows_nulls();
40✔
1050
}
40✔
1051

1052
std::string InstructionApplier::to_string(const Instruction::PathInstruction& instr) const
1053
{
×
1054
    REALM_ASSERT(m_log);
×
1055
    std::stringstream ss;
×
1056
    m_log->print_path(ss, instr.table, instr.object, instr.field, &instr.path);
×
1057
    return ss.str();
×
1058
}
×
1059

1060
bool InstructionApplier::check_links_exist(const Instruction::Payload& payload)
1061
{
24,564✔
1062
    bool valid_payload = true;
24,564✔
1063
    using Type = Instruction::Payload::Type;
24,564✔
1064
    if (payload.type == Type::Link) {
24,564✔
1065
        StringData class_name = get_string(payload.data.link.target_table);
812✔
1066
        Group::TableNameBuffer buffer;
812✔
1067
        StringData target_table_name = Group::class_name_to_table_name(class_name, buffer);
812✔
1068
        TableRef target_table = m_transaction.get_table(target_table_name);
812✔
1069
        if (!target_table) {
812✔
1070
            bad_transaction_log("Link with invalid target table '%1'", target_table_name);
×
1071
        }
×
1072
        if (target_table->is_embedded()) {
812✔
1073
            bad_transaction_log("Link to embedded table '%1'", target_table_name);
×
1074
        }
×
1075
        Mixed linked_pk =
812✔
1076
            mpark::visit(util::overload{[&](mpark::monostate) {
812✔
1077
                                            return Mixed{}; // the link exists and the pk is null
24✔
1078
                                        },
24✔
1079
                                        [&](int64_t pk) {
812✔
1080
                                            return Mixed{pk};
760✔
1081
                                        },
760✔
1082
                                        [&](InternString interned_pk) {
812✔
1083
                                            return Mixed{get_string(interned_pk)};
×
1084
                                        },
×
1085
                                        [&](GlobalKey) -> Mixed {
812✔
1086
                                            bad_transaction_log(
×
1087
                                                "Unexpected link to embedded object while validating a primary key");
×
1088
                                        },
×
1089
                                        [&](ObjectId pk) {
812✔
1090
                                            return Mixed{pk};
28✔
1091
                                        },
28✔
1092
                                        [&](UUID pk) {
812✔
1093
                                            return Mixed{pk};
×
1094
                                        }},
×
1095
                         payload.data.link.target);
812✔
1096

1097
        if (!target_table->find_primary_key(linked_pk)) {
812✔
1098
            valid_payload = false;
168✔
1099
        }
168✔
1100
    }
812✔
1101
    return valid_payload;
24,564✔
1102
}
24,564✔
1103

1104
void InstructionApplier::operator()(const Instruction::SetInsert& instr)
1105
{
1,760✔
1106
    struct SetInsertResolver : public PathResolver {
1,760✔
1107
        SetInsertResolver(InstructionApplier* applier, const Instruction::SetInsert& instr)
1,760✔
1108
            : PathResolver(applier, instr, "SetInsert")
1,760✔
1109
            , m_instr(instr)
1,760✔
1110
        {
1,760✔
1111
        }
1,760✔
1112
        Status on_property(Obj& obj, ColKey col) override
1,760✔
1113
        {
1,760✔
1114
            // This better be a mixed column
1115
            REALM_ASSERT(col.get_type() == col_type_Mixed);
×
1116
            auto set = obj.get_set<Mixed>(col);
×
1117
            on_set(set);
×
1118
            return Status::Pending;
×
1119
        }
×
1120
        void on_set(SetBase& set) override
1,760✔
1121
        {
1,760✔
1122
            auto col = set.get_col_key();
1,760✔
1123
            auto data_type = DataType(col.get_type());
1,760✔
1124
            auto table = set.get_table();
1,760✔
1125
            auto table_name = table->get_name();
1,760✔
1126
            auto field_name = table->get_column_name(col);
1,760✔
1127

1128
            auto inserter = util::overload{
1,760✔
1129
                [&](const ObjLink& link) {
1,760✔
1130
                    if (data_type == type_TypedLink) {
192✔
1131
                        REALM_ASSERT(dynamic_cast<Set<ObjLink>*>(&set));
×
1132
                        auto& link_set = static_cast<Set<ObjLink>&>(set);
×
1133
                        link_set.insert(link);
×
1134
                    }
×
1135
                    else if (data_type == type_Mixed) {
192✔
1136
                        REALM_ASSERT(dynamic_cast<Set<Mixed>*>(&set));
88✔
1137
                        auto& mixed_set = static_cast<Set<Mixed>&>(set);
88✔
1138
                        mixed_set.insert(link);
88✔
1139
                    }
88✔
1140
                    else if (data_type == type_Link) {
104✔
1141
                        REALM_ASSERT(dynamic_cast<Set<ObjKey>*>(&set));
104✔
1142
                        auto& link_set = static_cast<Set<ObjKey>&>(set);
104✔
1143
                        // Validate the target.
1144
                        auto target_table = table->get_link_target(col);
104✔
1145
                        if (target_table->get_key() != link.get_table_key()) {
104✔
1146
                            m_applier->bad_transaction_log(
×
1147
                                "SetInsert: Target table mismatch (expected '%1', got '%2')",
×
1148
                                target_table->get_name(), table_name);
×
1149
                        }
×
1150
                        link_set.insert(link.get_obj_key());
104✔
1151
                    }
104✔
1152
                    else {
×
1153
                        m_applier->bad_transaction_log(
×
1154
                            "SetInsert: Type mismatch in set at '%2.%1' (expected link type, was %3)", field_name,
×
1155
                            table_name, data_type);
×
1156
                    }
×
1157
                },
192✔
1158
                [&](Mixed value) {
1,760✔
1159
                    if (value.is_null() && !col.is_nullable()) {
1,568✔
1160
                        m_applier->bad_transaction_log("SetInsert: NULL in non-nullable set '%2.%1'", field_name,
×
1161
                                                       table_name);
×
1162
                    }
×
1163

1164
                    if (data_type == type_Mixed || value.is_null() || value.get_type() == data_type) {
1,568✔
1165
                        set.insert_any(value);
1,568✔
1166
                    }
1,568✔
1167
                    else {
×
1168
                        m_applier->bad_transaction_log(
×
1169
                            "SetInsert: Type mismatch in set at '%2.%1' (expected %3, got %4)", field_name,
×
1170
                            table_name, data_type, value.get_type());
×
1171
                    }
×
1172
                },
1,568✔
1173
                [&](const Instruction::Payload::ObjectValue&) {
1,760✔
1174
                    m_applier->bad_transaction_log("SetInsert: Sets of embedded objects are not supported.");
×
1175
                },
×
1176
                [&](const Instruction::Payload::Dictionary&) {
1,760✔
1177
                    m_applier->bad_transaction_log("SetInsert: Sets of dictionaries are not supported.");
×
1178
                },
×
1179
                [&](const Instruction::Payload::List&) {
1,760✔
1180
                    m_applier->bad_transaction_log("SetInsert: Sets of lists are not supported.");
×
1181
                },
×
1182
                [&](const Instruction::Payload::Set&) {
1,760✔
1183
                    m_applier->bad_transaction_log("SetInsert: Sets of sets are not supported.");
×
1184
                },
×
1185
                [&](const Instruction::Payload::Erased&) {
1,760✔
1186
                    m_applier->bad_transaction_log("SetInsert: Dictionary erase payload in SetInsert");
×
1187
                },
×
1188
            };
1,760✔
1189

1190
            m_applier->visit_payload(m_instr.value, inserter);
1,760✔
1191
        }
1,760✔
1192

1193
    private:
1,760✔
1194
        const Instruction::SetInsert& m_instr;
1,760✔
1195
    };
1,760✔
1196
    SetInsertResolver(this, instr).resolve();
1,760✔
1197
}
1,760✔
1198

1199
void InstructionApplier::operator()(const Instruction::SetErase& instr)
1200
{
492✔
1201
    struct SetEraseResolver : public PathResolver {
492✔
1202
        SetEraseResolver(InstructionApplier* applier, const Instruction::SetErase& instr)
492✔
1203
            : PathResolver(applier, instr, "SetErase")
492✔
1204
            , m_instr(instr)
492✔
1205
        {
492✔
1206
        }
492✔
1207
        Status on_property(Obj& obj, ColKey col) override
492✔
1208
        {
492✔
1209
            // This better be a mixed column
1210
            REALM_ASSERT(col.get_type() == col_type_Mixed);
×
1211
            auto set = obj.get_set<Mixed>(col);
×
1212
            on_set(set);
×
1213
            return Status::Pending;
×
1214
        }
×
1215
        void on_set(SetBase& set) override
492✔
1216
        {
492✔
1217
            auto col = set.get_col_key();
492✔
1218
            auto data_type = DataType(col.get_type());
492✔
1219
            auto table = set.get_table();
492✔
1220
            auto table_name = table->get_name();
492✔
1221
            auto field_name = table->get_column_name(col);
492✔
1222

1223
            auto inserter = util::overload{
492✔
1224
                [&](const ObjLink& link) {
492✔
1225
                    if (data_type == type_TypedLink) {
168✔
1226
                        REALM_ASSERT(dynamic_cast<Set<ObjLink>*>(&set));
×
1227
                        auto& link_set = static_cast<Set<ObjLink>&>(set);
×
1228
                        link_set.erase(link);
×
1229
                    }
×
1230
                    else if (data_type == type_Mixed) {
168✔
1231
                        REALM_ASSERT(dynamic_cast<Set<Mixed>*>(&set));
80✔
1232
                        auto& mixed_set = static_cast<Set<Mixed>&>(set);
80✔
1233
                        mixed_set.erase(link);
80✔
1234
                    }
80✔
1235
                    else if (data_type == type_Link) {
88✔
1236
                        REALM_ASSERT(dynamic_cast<Set<ObjKey>*>(&set));
88✔
1237
                        auto& link_set = static_cast<Set<ObjKey>&>(set);
88✔
1238
                        // Validate the target.
1239
                        auto target_table = table->get_link_target(col);
88✔
1240
                        if (target_table->get_key() != link.get_table_key()) {
88✔
1241
                            m_applier->bad_transaction_log(
×
1242
                                "SetErase: Target table mismatch (expected '%1', got '%2')", target_table->get_name(),
×
1243
                                table_name);
×
1244
                        }
×
1245
                        link_set.erase(link.get_obj_key());
88✔
1246
                    }
88✔
1247
                    else {
×
1248
                        m_applier->bad_transaction_log(
×
1249
                            "SetErase: Type mismatch in set at '%2.%1' (expected link type, was %3)", field_name,
×
1250
                            table_name, data_type);
×
1251
                    }
×
1252
                },
168✔
1253
                [&](Mixed value) {
492✔
1254
                    if (value.is_null() && !col.is_nullable()) {
324!
1255
                        m_applier->bad_transaction_log("SetErase: NULL in non-nullable set '%2.%1'", field_name,
×
1256
                                                       table_name);
×
1257
                    }
×
1258

1259
                    if (data_type == type_Mixed || value.get_type() == data_type) {
324✔
1260
                        set.erase_any(value);
324✔
1261
                    }
324✔
1262
                    else {
×
1263
                        m_applier->bad_transaction_log(
×
1264
                            "SetErase: Type mismatch in set at '%2.%1' (expected %3, got %4)", field_name, table_name,
×
1265
                            data_type, value.get_type());
×
1266
                    }
×
1267
                },
324✔
1268
                [&](const Instruction::Payload::ObjectValue&) {
492✔
1269
                    m_applier->bad_transaction_log("SetErase: Sets of embedded objects are not supported.");
×
1270
                },
×
1271
                [&](const Instruction::Payload::List&) {
492✔
1272
                    m_applier->bad_transaction_log("SetErase: Sets of lists are not supported.");
×
1273
                },
×
1274
                [&](const Instruction::Payload::Set&) {
492✔
1275
                    m_applier->bad_transaction_log("SetErase: Sets of sets are not supported.");
×
1276
                },
×
1277
                [&](const Instruction::Payload::Dictionary&) {
492✔
1278
                    m_applier->bad_transaction_log("SetErase: Sets of dictionaries are not supported.");
×
1279
                },
×
1280
                [&](const Instruction::Payload::Erased&) {
492✔
1281
                    m_applier->bad_transaction_log("SetErase: Dictionary erase payload in SetErase");
×
1282
                },
×
1283
            };
492✔
1284

1285
            m_applier->visit_payload(m_instr.value, inserter);
492✔
1286
        }
492✔
1287

1288
    private:
492✔
1289
        const Instruction::SetErase& m_instr;
492✔
1290
    };
492✔
1291
    SetEraseResolver(this, instr).resolve();
492✔
1292
}
492✔
1293

1294
StringData InstructionApplier::get_table_name(const Instruction::TableInstruction& instr,
1295
                                              const std::string_view& name)
1296
{
107,046✔
1297
    if (auto class_name = m_log->try_get_string(instr.table)) {
107,058✔
1298
        return Group::class_name_to_table_name(*class_name, m_table_name_buffer);
107,054✔
1299
    }
107,054✔
1300
    else {
2,147,483,651✔
1301
        bad_transaction_log("Corrupt table name in %1 instruction", name);
2,147,483,651✔
1302
    }
2,147,483,651✔
1303
}
107,046✔
1304

1305
TableRef InstructionApplier::get_table(const Instruction::TableInstruction& instr, const std::string_view& name)
1306
{
495,438✔
1307
    if (instr.table == m_last_table_name) {
495,438✔
1308
        return m_last_table;
393,154✔
1309
    }
393,154✔
1310
    else {
102,284✔
1311
        auto table_name = get_table_name(instr, name);
102,284✔
1312
        TableRef table = m_transaction.get_table(table_name);
102,284✔
1313
        if (!table) {
102,284✔
1314
            bad_transaction_log("%1: Table '%2' does not exist", name, table_name);
×
1315
        }
×
1316
        m_last_table = table;
102,284✔
1317
        m_last_table_name = instr.table;
102,284✔
1318
        m_last_object_key.reset();
102,284✔
1319
        m_last_object.reset();
102,284✔
1320
        m_last_field_name = InternString{};
102,284✔
1321
        m_last_field = ColKey{};
102,284✔
1322
        return table;
102,284✔
1323
    }
102,284✔
1324
}
495,438✔
1325

1326
util::Optional<Obj> InstructionApplier::get_top_object(const Instruction::ObjectInstruction& instr,
1327
                                                       const std::string_view& name)
1328
{
769,968✔
1329
    if (m_last_table_name == instr.table && m_last_object_key && m_last_object &&
769,968✔
1330
        *m_last_object_key == instr.object) {
769,968✔
1331
        // We have already found the object, reuse it.
1332
        return *m_last_object;
349,520✔
1333
    }
349,520✔
1334
    else {
420,448✔
1335
        TableRef table = get_table(instr, name);
420,448✔
1336
        ObjKey key = get_object_key(*table, instr.object, name);
420,448✔
1337
        if (!key) {
420,448✔
1338
            return util::none;
×
1339
        }
×
1340
        if (!table->is_valid(key)) {
420,448✔
1341
            // Check if the object is deleted or is a tombstone.
1342
            return util::none;
20,936✔
1343
        }
20,936✔
1344

1345
        Obj obj = table->get_object(key);
399,512✔
1346
        m_last_object_key = instr.object;
399,512✔
1347
        m_last_object = obj;
399,512✔
1348
        return obj;
399,512✔
1349
    }
420,448✔
1350
}
769,968✔
1351

1352
LstBasePtr InstructionApplier::get_list_from_path(Obj& obj, ColKey col)
1353
{
196,976✔
1354
    // For link columns, `Obj::get_listbase_ptr()` always returns an instance whose concrete type is
1355
    // `LnkLst`, which uses condensed indexes. However, we are interested in using non-condensed
1356
    // indexes, so we need to manually construct a `Lst<ObjKey>` instead for lists of non-embedded
1357
    // links.
1358
    REALM_ASSERT(col.is_list());
196,976✔
1359
    LstBasePtr list;
196,976✔
1360
    if (col.get_type() == col_type_Link) {
196,976✔
1361
        auto table = obj.get_table();
13,784✔
1362
        if (!table->get_link_target(col)->is_embedded()) {
13,784✔
1363
            list = obj.get_list_ptr<ObjKey>(col);
1,648✔
1364
        }
1,648✔
1365
        else {
12,136✔
1366
            list = obj.get_listbase_ptr(col);
12,136✔
1367
        }
12,136✔
1368
    }
13,784✔
1369
    else {
183,192✔
1370
        list = obj.get_listbase_ptr(col);
183,192✔
1371
    }
183,192✔
1372
    return list;
196,976✔
1373
}
196,976✔
1374

1375
InstructionApplier::PathResolver::PathResolver(InstructionApplier* applier, const Instruction::PathInstruction& instr,
1376
                                               const std::string_view& instr_name)
1377
    : m_applier(applier)
365,110✔
1378
    , m_path_instr(instr)
365,110✔
1379
    , m_instr_name(instr_name)
365,110✔
1380
{
743,494✔
1381
}
743,494✔
1382

1383
InstructionApplier::PathResolver::~PathResolver()
1384
{
743,896✔
1385
    on_finish();
743,896✔
1386
}
743,896✔
1387

1388
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_property(Obj&, ColKey)
1389
{
×
1390
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (object, column)", m_instr_name));
×
1391
    return Status::DidNotResolve;
×
1392
}
×
1393

1394
void InstructionApplier::PathResolver::on_list(LstBase&)
1395
{
×
1396
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (list)", m_instr_name));
×
1397
}
×
1398

1399
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_list_index(LstBase&, uint32_t)
1400
{
×
1401
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (list, index)", m_instr_name));
×
1402
    return Status::DidNotResolve;
×
1403
}
×
1404

1405
void InstructionApplier::PathResolver::on_dictionary(Dictionary&)
1406
{
×
1407
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (dictionary, key)", m_instr_name));
×
1408
}
×
1409

1410
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_dictionary_key(Dictionary&, Mixed)
1411
{
×
1412
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (dictionary, key)", m_instr_name));
×
1413
    return Status::DidNotResolve;
×
1414
}
×
1415

1416
void InstructionApplier::PathResolver::on_set(SetBase&)
1417
{
×
1418
    m_applier->bad_transaction_log(util::format("Invalid path for %1 (set)", m_instr_name));
×
1419
}
×
1420

1421
void InstructionApplier::PathResolver::on_error(const std::string& err_msg)
1422
{
×
1423
    m_applier->bad_transaction_log(err_msg);
×
1424
}
×
1425

1426
InstructionApplier::PathResolver::Status
1427
InstructionApplier::PathResolver::on_mixed_type_changed(const std::string& err_msg)
1428
{
×
1429
    m_applier->bad_transaction_log(err_msg);
×
1430
    return Status::DidNotResolve;
×
1431
}
×
1432

1433
void InstructionApplier::PathResolver::on_column_advance(ColKey col)
1434
{
729,920✔
1435
    m_applier->m_last_field = col;
729,920✔
1436
}
729,920✔
1437

1438
void InstructionApplier::PathResolver::on_dict_key_advance(StringData) {}
3,012✔
1439

1440
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_list_index_advance(uint32_t)
1441
{
8,092✔
1442
    return Status::Pending;
8,092✔
1443
}
8,092✔
1444

1445
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_null_link_advance(StringData,
1446
                                                                                                StringData)
1447
{
×
1448
    return Status::Pending;
×
1449
}
×
1450

1451
InstructionApplier::PathResolver::Status
1452
InstructionApplier::PathResolver::on_dict_key_not_found(StringData, StringData, StringData)
1453
{
×
1454
    return Status::Pending;
×
1455
}
×
1456

1457
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::on_begin(const util::Optional<Obj>&)
1458
{
717,502✔
1459
    m_applier->m_current_path = m_path_instr.path;
717,502✔
1460
    m_applier->m_last_field_name = m_path_instr.field;
717,502✔
1461
    return Status::Pending;
717,502✔
1462
}
717,502✔
1463

1464
void InstructionApplier::PathResolver::on_finish()
1465
{
743,890✔
1466
    m_applier->m_current_path.reset();
743,890✔
1467
    m_applier->m_last_field_name = InternString{};
743,890✔
1468
    m_applier->m_last_field = ColKey{};
743,890✔
1469
}
743,890✔
1470

1471
StringData InstructionApplier::PathResolver::get_string(InternString interned)
1472
{
770,614✔
1473
    return m_applier->get_string(interned);
770,614✔
1474
}
770,614✔
1475

1476
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve()
1477
{
743,628✔
1478
    util::Optional<Obj> obj = m_applier->get_top_object(m_path_instr, m_instr_name);
743,628✔
1479
    Status begin_status = on_begin(obj);
743,628✔
1480
    if (begin_status != Status::Pending) {
743,628✔
1481
        return begin_status;
220✔
1482
    }
220✔
1483
    if (!obj) {
743,408✔
1484
        m_applier->bad_transaction_log("%1: No such object: '%2' in class '%3'", m_instr_name,
×
1485
                                       format_pk(m_applier->m_log->get_key(m_path_instr.object)),
×
1486
                                       get_string(m_path_instr.table));
×
1487
    }
×
1488

1489
    m_it_begin = m_path_instr.path.begin();
743,408✔
1490
    m_it_end = m_path_instr.path.end();
743,408✔
1491
    Status status = resolve_field(*obj, m_path_instr.field);
743,408✔
1492
    return status == Status::Pending ? Status::Success : status;
743,408✔
1493
}
743,628✔
1494

1495
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve_field(Obj& obj, InternString field)
1496
{
759,574✔
1497
    auto field_name = get_string(field);
759,574✔
1498
    ColKey col = obj.get_table()->get_column_key(field_name);
759,574✔
1499
    if (!col) {
759,574✔
1500
        on_error(util::format("%1: No such field: '%2' in class '%3'", m_instr_name, field_name,
×
1501
                              obj.get_table()->get_name()));
×
1502
        return Status::DidNotResolve;
×
1503
    }
×
1504
    on_column_advance(col);
759,574✔
1505

1506
    if (m_it_begin == m_it_end) {
759,574✔
1507
        if (col.is_list()) {
546,908✔
1508
            auto list = obj.get_listbase_ptr(col);
130✔
1509
            on_list(*list);
130✔
1510
        }
130✔
1511
        else if (col.is_dictionary()) {
546,778✔
1512
            auto dict = obj.get_dictionary(col);
80✔
1513
            on_dictionary(dict);
80✔
1514
        }
80✔
1515
        else if (col.is_set()) {
546,698✔
1516
            SetBasePtr set;
3,664✔
1517
            if (col.get_type() == col_type_Link) {
3,664✔
1518
                // We are interested in using non-condensed indexes - as for Lists below
1519
                set = obj.get_set_ptr<ObjKey>(col);
376✔
1520
            }
376✔
1521
            else {
3,288✔
1522
                set = obj.get_setbase_ptr(col);
3,288✔
1523
            }
3,288✔
1524
            on_set(*set);
3,664✔
1525
        }
3,664✔
1526
        else {
543,034✔
1527
            return on_property(obj, col);
543,034✔
1528
        }
543,034✔
1529
        return Status::Pending;
3,874✔
1530
    }
546,908✔
1531

1532
    if (col.is_list()) {
212,666✔
1533
        if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
196,976✔
1534
            auto list = InstructionApplier::get_list_from_path(obj, col);
196,976✔
1535
            ++m_it_begin;
196,976✔
1536
            return resolve_list_element(*list, *pindex);
196,976✔
1537
        }
196,976✔
1538
        on_error(
×
1539
            util::format(s_list_index_wrong_type_err.data(), m_instr_name, field_name, obj.get_table()->get_name()));
×
1540
    }
×
1541
    else if (col.is_dictionary()) {
15,690✔
1542
        if (auto pkey = mpark::get_if<InternString>(&*m_it_begin)) {
8,828✔
1543
            auto dict = obj.get_dictionary(col);
8,828✔
1544
            ++m_it_begin;
8,828✔
1545
            return resolve_dictionary_element(dict, *pkey);
8,828✔
1546
        }
8,828✔
1547
        on_error(
×
1548
            util::format(s_dict_key_wrong_type_err.data(), m_instr_name, field_name, obj.get_table()->get_name()));
×
1549
    }
×
1550
    else if (col.get_type() == col_type_Mixed) {
6,862✔
1551
        auto val = obj.get<Mixed>(col);
2,476✔
1552
        std::string_view error_msg;
2,476✔
1553
        if (val.is_type(type_Dictionary)) {
2,476✔
1554
            if (auto pkey = mpark::get_if<InternString>(&*m_it_begin)) {
1,488✔
1555
                Dictionary dict(obj, col);
1,480✔
1556
                ++m_it_begin;
1,480✔
1557
                return resolve_dictionary_element(dict, *pkey);
1,480✔
1558
            }
1,480✔
1559
            error_msg = s_dict_key_wrong_type_err;
8✔
1560
        }
8✔
1561
        else if (val.is_type(type_List)) {
988✔
1562
            if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
980✔
1563
                Lst<Mixed> list(obj, col);
972✔
1564
                ++m_it_begin;
972✔
1565
                return resolve_list_element(list, *pindex);
972✔
1566
            }
972✔
1567
            error_msg = s_list_index_wrong_type_err;
8✔
1568
        }
8✔
1569
        else {
8✔
1570
            error_msg = s_wrong_collection_type_err;
8✔
1571
        }
8✔
1572
        return on_mixed_type_changed(
24✔
1573
            util::format(error_msg.data(), m_instr_name, field_name, obj.get_table()->get_name()));
24✔
1574
    }
2,476✔
1575
    else if (col.get_type() == col_type_Link) {
4,608✔
1576
        auto target = obj.get_table()->get_link_target(col);
4,608✔
1577
        if (!target->is_embedded()) {
4,608✔
1578
            on_error(util::format("%1: Reference through non-embedded link in field '%2' in class '%3'", m_instr_name,
×
1579
                                  field_name, obj.get_table()->get_name()));
×
1580
        }
×
1581
        else if (obj.is_null(col)) {
4,608✔
1582
            Status null_status =
132✔
1583
                on_null_link_advance(obj.get_table()->get_name(), obj.get_table()->get_column_name(col));
132✔
1584
            if (null_status != Status::Pending) {
132✔
1585
                return null_status;
132✔
1586
            }
132✔
1587
            on_error(util::format("%1: Reference through NULL embedded link in field '%2' in class '%3'",
×
1588
                                  m_instr_name, field_name, obj.get_table()->get_name()));
×
1589
        }
×
1590
        else if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
4,476✔
1591
            auto embedded_object = obj.get_linked_object(col);
4,476✔
1592
            ++m_it_begin;
4,476✔
1593
            return resolve_field(embedded_object, *pfield);
4,476✔
1594
        }
4,476✔
1595
        else {
×
1596
            on_error(util::format("%1: Embedded object field reference is not a string", m_instr_name));
×
1597
        }
×
1598
    }
4,608✔
1599
    else {
4,294,967,294✔
1600
        on_error(util::format("%1: Resolving path through unstructured field '%3.%2' of type %4", m_instr_name,
4,294,967,294✔
1601
                              field_name, obj.get_table()->get_name(), col.get_type()));
4,294,967,294✔
1602
    }
4,294,967,294✔
1603
    return Status::DidNotResolve;
4,294,967,294✔
1604
}
212,666✔
1605

1606
InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve_list_element(LstBase& list,
1607
                                                                                                uint32_t index)
1608
{
198,848✔
1609
    if (m_it_begin == m_it_end) {
198,848✔
1610
        return on_list_index(list, index);
187,900✔
1611
    }
187,900✔
1612

1613
    auto col = list.get_col_key();
10,948✔
1614
    auto table = list.get_table();
10,948✔
1615
    auto field_name = table->get_column_name(col);
10,948✔
1616

1617
    if (col.get_type() == col_type_Link) {
10,948✔
1618
        auto target = table->get_link_target(col);
10,172✔
1619
        if (!target->is_embedded()) {
10,172✔
1620
            on_error(util::format("%1: Reference through non-embedded link at '%2.%3[%4]'", m_instr_name,
×
1621
                                  table->get_name(), field_name, index));
×
1622
            return Status::DidNotResolve;
×
1623
        }
×
1624

1625
        Status list_status = on_list_index_advance(index);
10,172✔
1626
        if (list_status != Status::Pending) {
10,172✔
1627
            return list_status;
2,720✔
1628
        }
2,720✔
1629

1630
        REALM_ASSERT(dynamic_cast<LnkLst*>(&list));
7,452✔
1631
        auto& link_list = static_cast<LnkLst&>(list);
7,452✔
1632
        if (index >= link_list.size()) {
7,452✔
1633
            on_error(util::format("%1: Out-of-bounds index through list at '%2.%3[%4]'", m_instr_name,
×
1634
                                  table->get_name(), field_name, index));
×
1635
        }
×
1636
        else if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
7,452✔
1637
            auto embedded_object = link_list.get_object(index);
7,452✔
1638
            ++m_it_begin;
7,452✔
1639
            return resolve_field(embedded_object, *pfield);
7,452✔
1640
        }
7,452✔
1641
        on_error(util::format("%1: Embedded object field reference is not a string", m_instr_name));
×
1642
    }
×
1643
    else {
776✔
1644
        if (list.get_data_type() == type_Mixed) {
776✔
1645
            Status list_status = on_list_index_advance(index);
776✔
1646
            if (list_status != Status::Pending) {
776✔
1647
                return list_status;
136✔
1648
            }
136✔
1649
            auto& mixed_list = static_cast<Lst<Mixed>&>(list);
640✔
1650
            if (index >= mixed_list.size()) {
640✔
1651
                on_error(util::format("%1: Out-of-bounds index '%2' through list along path '%2.%3'", m_instr_name,
×
1652
                                      index, table->get_name(), field_name));
×
1653
            }
×
1654
            else {
640✔
1655
                auto val = mixed_list.get(index);
640✔
1656
                std::string_view error_msg;
640✔
1657

1658
                if (val.is_type(type_Dictionary)) {
640✔
1659
                    if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
468✔
1660
                        Dictionary d(mixed_list, mixed_list.get_key(index));
468✔
1661
                        ++m_it_begin;
468✔
1662
                        return resolve_dictionary_element(d, *pfield);
468✔
1663
                    }
468✔
1664
                    error_msg = s_dict_key_wrong_type_err;
×
1665
                }
×
1666
                else if (val.is_type(type_List)) {
172✔
1667
                    if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
172✔
1668
                        Lst<Mixed> l(mixed_list, mixed_list.get_key(index));
172✔
1669
                        ++m_it_begin;
172✔
1670
                        return resolve_list_element(l, *pindex);
172✔
1671
                    }
172✔
1672
                    error_msg = s_list_index_wrong_type_err;
×
1673
                }
×
1674
                else {
×
1675
                    error_msg = s_wrong_collection_type_err;
×
1676
                }
×
1677
                return on_mixed_type_changed(
×
1678
                    util::format(error_msg.data(), m_instr_name, field_name, table->get_name()));
×
1679
            }
640✔
1680
        }
640✔
UNCOV
1681
        on_error(util::format(
×
UNCOV
1682
            "%1: Resolving path through unstructured list element on '%2.%3', which is a list of type '%4'",
×
UNCOV
1683
            m_instr_name, table->get_name(), field_name, col.get_type()));
×
UNCOV
1684
    }
×
UNCOV
1685
    return Status::DidNotResolve;
×
1686
}
10,948✔
1687

1688
InstructionApplier::PathResolver::Status
1689
InstructionApplier::PathResolver::resolve_dictionary_element(Dictionary& dict, InternString key)
1690
{
11,040✔
1691
    StringData string_key = get_string(key);
11,040✔
1692
    if (m_it_begin == m_it_end) {
11,040✔
1693
        return on_dictionary_key(dict, Mixed{string_key});
5,876✔
1694
    }
5,876✔
1695

1696
    on_dict_key_advance(string_key);
5,164✔
1697

1698
    auto col = dict.get_col_key();
5,164✔
1699
    auto table = dict.get_table();
5,164✔
1700
    auto field_name = table->get_column_name(col);
5,164✔
1701

1702
    if (col.get_type() == col_type_Link) {
5,164✔
1703
        auto target = dict.get_target_table();
4,164✔
1704
        if (!target->is_embedded()) {
4,164✔
1705
            on_error(util::format("%1: Reference through non-embedded link at '%3.%2[%4]'", m_instr_name, field_name,
×
1706
                                  table->get_name(), string_key));
×
1707
            return Status::DidNotResolve;
×
1708
        }
×
1709

1710
        auto embedded_object = dict.get_object(string_key);
4,164✔
1711
        if (!embedded_object) {
4,164✔
1712
            Status null_link_status = on_null_link_advance(table->get_name(), string_key);
80✔
1713
            if (null_link_status != Status::Pending) {
80✔
1714
                return null_link_status;
80✔
1715
            }
80✔
1716
            on_error(util::format("%1: Unmatched key through dictionary at '%3.%2[%4]'", m_instr_name, field_name,
×
1717
                                  table->get_name(), string_key));
×
1718
        }
×
1719
        else if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
4,084✔
1720
            ++m_it_begin;
4,084✔
1721
            return resolve_field(embedded_object, *pfield);
4,084✔
1722
        }
4,084✔
1723
        else {
×
1724
            on_error(util::format("%1: Embedded object field reference is not a string", m_instr_name));
×
1725
        }
×
1726
    }
4,164✔
1727
    else {
1,000✔
1728
        if (auto val = dict.try_get(string_key)) {
1,000✔
1729
            std::string_view error_msg;
996✔
1730
            if (val->is_type(type_Dictionary)) {
996✔
1731
                if (auto pfield = mpark::get_if<InternString>(&*m_it_begin)) {
264✔
1732
                    Dictionary d(dict, dict.build_index(string_key));
264✔
1733
                    ++m_it_begin;
264✔
1734
                    return resolve_dictionary_element(d, *pfield);
264✔
1735
                }
264✔
1736
                error_msg = s_dict_key_wrong_type_err;
×
1737
            }
×
1738
            else if (val->is_type(type_List)) {
732✔
1739
                if (auto pindex = mpark::get_if<uint32_t>(&*m_it_begin)) {
728✔
1740
                    Lst<Mixed> l(dict, dict.build_index(string_key));
728✔
1741
                    ++m_it_begin;
728✔
1742
                    return resolve_list_element(l, *pindex);
728✔
1743
                }
728✔
1744
                error_msg = s_list_index_wrong_type_err;
×
1745
            }
×
1746
            else {
4✔
1747
                error_msg = s_wrong_collection_type_err;
4✔
1748
            }
4✔
1749
            return on_mixed_type_changed(util::format(error_msg.data(), m_instr_name, field_name, table->get_name()));
4✔
1750
        }
996✔
1751
        Status key_not_found_status = on_dict_key_not_found(table->get_name(), field_name, string_key);
4✔
1752
        if (key_not_found_status != Status::Pending) {
4✔
1753
            return key_not_found_status;
4✔
1754
        }
4✔
1755
        on_error(util::format("%1: Unmatched key '%2' through dictionary along path '%3.%4'", m_instr_name,
×
1756
                              string_key, table->get_name(), field_name));
×
1757
    }
×
1758
    return Status::DidNotResolve;
×
1759
}
5,164✔
1760

1761

1762
ObjKey InstructionApplier::get_object_key(Table& table, const Instruction::PrimaryKey& primary_key,
1763
                                          const std::string_view& name) const
1764
{
422,620✔
1765
    StringData table_name = table.get_name();
422,620✔
1766
    ColKey pk_col = table.get_primary_key_column();
422,620✔
1767
    StringData pk_name = "";
422,620✔
1768
    DataType pk_type;
422,620✔
1769
    if (pk_col) {
422,712✔
1770
        pk_name = table.get_column_name(pk_col);
422,712✔
1771
        pk_type = table.get_column_type(pk_col);
422,712✔
1772
    }
422,712✔
1773
    return mpark::visit(
422,620✔
1774
        util::overload{
422,620✔
1775
            [&](mpark::monostate) {
422,620✔
1776
                if (!pk_col) {
24✔
1777
                    bad_transaction_log(
×
1778
                        "%1 instruction with NULL primary key, but table '%2' does not have a primary key column",
×
1779
                        name, table_name);
×
1780
                }
×
1781
                if (!table.is_nullable(pk_col)) {
24✔
1782
                    bad_transaction_log("%1 instruction with NULL primary key, but column '%2.%3' is not nullable",
×
1783
                                        name, table_name, pk_name);
×
1784
                }
×
1785

1786
                ObjKey key = table.get_objkey_from_primary_key(realm::util::none);
24✔
1787
                return key;
24✔
1788
            },
24✔
1789
            [&](int64_t pk) {
422,620✔
1790
                if (!pk_col) {
393,748✔
1791
                    bad_transaction_log("%1 instruction with integer primary key (%2), but table '%3' does not have "
×
1792
                                        "a primary key column",
×
1793
                                        name, pk, table_name);
×
1794
                }
×
1795
                if (pk_type != type_Int) {
393,748✔
1796
                    bad_transaction_log(
×
1797
                        "%1 instruction with integer primary key (%2), but '%3.%4' has primary keys of type '%5'",
×
1798
                        name, pk, table_name, pk_name, pk_type);
×
1799
                }
×
1800
                ObjKey key = table.get_objkey_from_primary_key(pk);
393,748✔
1801
                return key;
393,748✔
1802
            },
393,748✔
1803
            [&](InternString interned_pk) {
422,620✔
1804
                auto pk = get_string(interned_pk);
1,008✔
1805
                if (!pk_col) {
1,008✔
1806
                    bad_transaction_log("%1 instruction with string primary key (\"%2\"), but table '%3' does not "
×
1807
                                        "have a primary key column",
×
1808
                                        name, pk, table_name);
×
1809
                }
×
1810
                if (pk_type != type_String) {
1,008✔
1811
                    bad_transaction_log(
×
1812
                        "%1 instruction with string primary key (\"%2\"), but '%3.%4' has primary keys of type '%5'",
×
1813
                        name, pk, table_name, pk_name, pk_type);
×
1814
                }
×
1815
                ObjKey key = table.get_objkey_from_primary_key(pk);
1,008✔
1816
                return key;
1,008✔
1817
            },
1,008✔
1818
            [&](GlobalKey id) {
422,620✔
1819
                if (pk_col) {
4✔
1820
                    bad_transaction_log(
×
1821
                        "%1 instruction without primary key, but table '%2' has a primary key column of type %3",
×
1822
                        name, table_name, pk_type);
×
1823
                }
×
1824
                ObjKey key = table.get_objkey_from_global_key(id);
4✔
1825
                return key;
4✔
1826
            },
4✔
1827
            [&](ObjectId pk) {
422,620✔
1828
                if (!pk_col) {
27,896✔
1829
                    bad_transaction_log("%1 instruction with ObjectId primary key (\"%2\"), but table '%3' does not "
×
1830
                                        "have a primary key column",
×
1831
                                        name, pk, table_name);
×
1832
                }
×
1833
                if (pk_type != type_ObjectId) {
27,896✔
1834
                    bad_transaction_log(
×
1835
                        "%1 instruction with ObjectId primary key (%2), but '%3.%4' has primary keys of type '%5'",
×
1836
                        name, pk, table_name, pk_name, pk_type);
×
1837
                }
×
1838
                ObjKey key = table.get_objkey_from_primary_key(pk);
27,896✔
1839
                return key;
27,896✔
1840
            },
27,896✔
1841
            [&](UUID pk) {
422,620✔
1842
                if (!pk_col) {
20✔
1843
                    bad_transaction_log("%1 instruction with UUID primary key (\"%2\"), but table '%3' does not "
×
1844
                                        "have a primary key column",
×
1845
                                        name, pk, table_name);
×
1846
                }
×
1847
                if (pk_type != type_UUID) {
20✔
1848
                    bad_transaction_log(
×
1849
                        "%1 instruction with UUID primary key (%2), but '%3.%4' has primary keys of type '%5'", name,
×
1850
                        pk, table_name, pk_name, pk_type);
×
1851
                }
×
1852
                ObjKey key = table.get_objkey_from_primary_key(pk);
20✔
1853
                return key;
20✔
1854
            }},
20✔
1855
        primary_key);
422,620✔
1856
}
422,620✔
1857

1858

1859
} // 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