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

realm / realm-core / 2109

07 Mar 2024 01:56PM UTC coverage: 90.918% (+0.01%) from 90.908%
2109

push

Evergreen

web-flow
Fix querying with a path into nested collections with wildcards (#7404)

Comparing a collection with a list could fail if there was wildcards
in the path and therefore multiple collections to compare with right
hand list.

Linklist is implicitly having wildcard in the path, so if linklists is
in the path there will be a similar problem.  Do not merge values
from different objects into a common list in queries.

93972 of 173176 branches covered (54.26%)

323 of 332 new or added lines in 6 files covered. (97.29%)

91 existing lines in 18 files now uncovered.

238503 of 262328 relevant lines covered (90.92%)

6065347.74 hits per line

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

93.07
/src/realm/sync/noinst/client_reset.cpp
1
///////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2021 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/transaction.hpp>
20
#include <realm/dictionary.hpp>
21
#include <realm/object_converter.hpp>
22
#include <realm/table_view.hpp>
23
#include <realm/set.hpp>
24

25
#include <realm/sync/history.hpp>
26
#include <realm/sync/changeset_parser.hpp>
27
#include <realm/sync/instruction_applier.hpp>
28
#include <realm/sync/noinst/client_history_impl.hpp>
29
#include <realm/sync/noinst/client_reset.hpp>
30
#include <realm/sync/noinst/client_reset_recovery.hpp>
31
#include <realm/sync/subscriptions.hpp>
32

33
#include <realm/util/compression.hpp>
34

35
#include <algorithm>
36
#include <chrono>
37
#include <vector>
38

39
using namespace realm;
40
using namespace _impl;
41
using namespace sync;
42

43
namespace realm {
44

45
std::ostream& operator<<(std::ostream& os, const ClientResyncMode& mode)
46
{
22,710✔
47
    switch (mode) {
22,710✔
48
        case ClientResyncMode::Manual:
✔
49
            os << "Manual";
×
50
            break;
×
51
        case ClientResyncMode::DiscardLocal:
11,216✔
52
            os << "DiscardLocal";
11,216✔
53
            break;
11,216✔
54
        case ClientResyncMode::Recover:
11,374✔
55
            os << "Recover";
11,374✔
56
            break;
11,374✔
57
        case ClientResyncMode::RecoverOrDiscard:
120✔
58
            os << "RecoverOrDiscard";
120✔
59
            break;
120✔
60
    }
22,710✔
61
    return os;
22,710✔
62
}
22,710✔
63

64
} // namespace realm
65

66
namespace realm::_impl::client_reset {
67

68
static inline bool should_skip_table(const Transaction& group, TableKey key)
69
{
242,056✔
70
    return !group.table_is_public(key);
242,056✔
71
}
242,056✔
72

73
void transfer_group(const Transaction& group_src, Transaction& group_dst, util::Logger& logger,
74
                    bool allow_schema_additions)
75
{
7,356✔
76
    logger.debug(util::LogCategory::reset,
7,356✔
77
                 "transfer_group, src size = %1, dst size = %2, allow_schema_additions = %3", group_src.size(),
7,356✔
78
                 group_dst.size(), allow_schema_additions);
7,356✔
79

3,678✔
80
    // Turn off the sync history tracking during state transfer since it will be thrown
3,678✔
81
    // away immediately after anyways. This reduces the memory footprint of a client reset.
3,678✔
82
    ClientReplication* client_repl = dynamic_cast<ClientReplication*>(group_dst.get_replication());
7,356✔
83
    REALM_ASSERT_RELEASE(client_repl);
7,356✔
84
    TempShortCircuitReplication sync_history_guard(*client_repl);
7,356✔
85

3,678✔
86
    // Find all tables in dst that should be removed.
3,678✔
87
    std::set<std::string> tables_to_remove;
7,356✔
88
    for (auto table_key : group_dst.get_table_keys()) {
33,064✔
89
        if (should_skip_table(group_dst, table_key))
33,064✔
90
            continue;
15,440✔
91
        StringData table_name = group_dst.get_table_name(table_key);
17,624✔
92
        logger.debug(util::LogCategory::reset, "key = %1, table_name = %2", table_key.value, table_name);
17,624✔
93
        ConstTableRef table_src = group_src.get_table(table_name);
17,624✔
94
        if (!table_src) {
17,624✔
95
            logger.debug(util::LogCategory::reset, "Table '%1' will be removed", table_name);
24✔
96
            tables_to_remove.insert(table_name);
24✔
97
            continue;
24✔
98
        }
24✔
99
        // Check whether the table type is the same.
8,800✔
100
        TableRef table_dst = group_dst.get_table(table_key);
17,600✔
101
        auto pk_col_src = table_src->get_primary_key_column();
17,600✔
102
        auto pk_col_dst = table_dst->get_primary_key_column();
17,600✔
103
        bool has_pk_src = bool(pk_col_src);
17,600✔
104
        bool has_pk_dst = bool(pk_col_dst);
17,600✔
105
        if (has_pk_src != has_pk_dst) {
17,600✔
106
            throw ClientResetFailed(util::format("Client reset requires a primary key column in %1 table '%2'",
×
107
                                                 (has_pk_src ? "dest" : "source"), table_name));
×
108
        }
×
109
        if (!has_pk_src)
17,600✔
110
            continue;
664✔
111

8,468✔
112
        // Now the tables both have primary keys. Check type.
8,468✔
113
        if (pk_col_src.get_type() != pk_col_dst.get_type()) {
16,936✔
114
            throw ClientResetFailed(
4✔
115
                util::format("Client reset found incompatible primary key types (%1 vs %2) on '%3'",
4✔
116
                             pk_col_src.get_type(), pk_col_dst.get_type(), table_name));
4✔
117
        }
4✔
118
        // Check collection type, nullability etc. but having an index doesn't matter;
8,466✔
119
        ColumnAttrMask pk_col_src_attr = pk_col_src.get_attrs();
16,932✔
120
        ColumnAttrMask pk_col_dst_attr = pk_col_dst.get_attrs();
16,932✔
121
        pk_col_src_attr.reset(ColumnAttr::col_attr_Indexed);
16,932✔
122
        pk_col_dst_attr.reset(ColumnAttr::col_attr_Indexed);
16,932✔
123
        if (pk_col_src_attr != pk_col_dst_attr) {
16,932✔
124
            throw ClientResetFailed(
×
125
                util::format("Client reset found incompatible primary key attributes (%1 vs %2) on '%3'",
×
126
                             pk_col_src.value, pk_col_dst.value, table_name));
×
127
        }
×
128
        // Check name.
8,466✔
129
        StringData pk_col_name_src = table_src->get_column_name(pk_col_src);
16,932✔
130
        StringData pk_col_name_dst = table_dst->get_column_name(pk_col_dst);
16,932✔
131
        if (pk_col_name_src != pk_col_name_dst) {
16,932✔
132
            throw ClientResetFailed(
×
133
                util::format("Client reset requires equal pk column names but '%1' != '%2' on '%3'", pk_col_name_src,
×
134
                             pk_col_name_dst, table_name));
×
135
        }
×
136
        // The table survives.
8,466✔
137
        logger.debug(util::LogCategory::reset, "Table '%1' will remain", table_name);
16,932✔
138
    }
16,932✔
139

3,678✔
140
    // If there have been any tables marked for removal stop.
3,678✔
141
    // We consider two possible options for recovery:
3,678✔
142
    // 1: Remove the tables. But this will generate destructive schema
3,678✔
143
    //    schema changes that the local Realm cannot advance through.
3,678✔
144
    //    Since this action will fail down the line anyway, give up now.
3,678✔
145
    // 2: Keep the tables locally and ignore them. But the local app schema
3,678✔
146
    //    still has these classes and trying to modify anything in them will
3,678✔
147
    //    create sync instructions on tables that sync doesn't know about.
3,678✔
148
    // As an exception in recovery mode, we assume that the corresponding
3,678✔
149
    // additive schema changes will be part of the recovery upload. If they
3,678✔
150
    // are present, then the server can choose to allow them (if in dev mode).
3,678✔
151
    // If they are not present, then the server will emit an error the next time
3,678✔
152
    // a value is set on the unknown property.
3,678✔
153
    if (!allow_schema_additions && !tables_to_remove.empty()) {
7,354✔
154
        std::string names_list;
16✔
155
        for (const std::string& table_name : tables_to_remove) {
24✔
156
            names_list += Group::table_name_to_class_name(table_name);
24✔
157
            names_list += ", ";
24✔
158
        }
24✔
159
        if (names_list.size() > 2) {
16✔
160
            // remove the final ", "
8✔
161
            names_list = names_list.substr(0, names_list.size() - 2);
16✔
162
        }
16✔
163
        throw ClientResetFailed(
16✔
164
            util::format("Client reset cannot recover when classes have been removed: {%1}", names_list));
16✔
165
    }
16✔
166

3,668✔
167
    // Create new tables in dst if needed.
3,668✔
168
    for (auto table_key : group_src.get_table_keys()) {
25,164✔
169
        if (should_skip_table(group_src, table_key))
25,164✔
170
            continue;
7,580✔
171
        ConstTableRef table_src = group_src.get_table(table_key);
17,584✔
172
        StringData table_name = table_src->get_name();
17,584✔
173
        auto pk_col_src = table_src->get_primary_key_column();
17,584✔
174
        TableRef table_dst = group_dst.get_table(table_name);
17,584✔
175
        if (!table_dst) {
17,584✔
176
            // Create the table.
16✔
177
            if (table_src->is_embedded()) {
32✔
178
                REALM_ASSERT(!pk_col_src);
16✔
179
                group_dst.add_table(table_name, Table::Type::Embedded);
16✔
180
            }
16✔
181
            else {
16✔
182
                REALM_ASSERT(pk_col_src); // a sync table will have a pk
16✔
183
                auto pk_col_src = table_src->get_primary_key_column();
16✔
184
                DataType pk_type = DataType(pk_col_src.get_type());
16✔
185
                StringData pk_col_name = table_src->get_column_name(pk_col_src);
16✔
186
                group_dst.add_table_with_primary_key(table_name, pk_type, pk_col_name, pk_col_src.is_nullable(),
16✔
187
                                                     table_src->get_table_type());
16✔
188
            }
16✔
189
        }
32✔
190
    }
17,584✔
191

3,668✔
192
    // Now the class tables are identical.
3,668✔
193
    size_t num_tables;
7,336✔
194
    {
7,336✔
195
        size_t num_tables_src = 0;
7,336✔
196
        for (auto table_key : group_src.get_table_keys()) {
25,164✔
197
            if (!should_skip_table(group_src, table_key))
25,164✔
198
                ++num_tables_src;
17,584✔
199
        }
25,164✔
200
        size_t num_tables_dst = 0;
7,336✔
201
        for (auto table_key : group_dst.get_table_keys()) {
32,966✔
202
            if (!should_skip_table(group_dst, table_key))
32,966✔
203
                ++num_tables_dst;
17,582✔
204
        }
32,966✔
205
        REALM_ASSERT_EX(allow_schema_additions || num_tables_src == num_tables_dst, num_tables_src, num_tables_dst);
7,336✔
206
        num_tables = num_tables_src;
7,336✔
207
    }
7,336✔
208
    logger.debug(util::LogCategory::reset, "The number of tables is %1", num_tables);
7,336✔
209

3,668✔
210
    // Remove columns in dst if they are absent in src.
3,668✔
211
    for (auto table_key : group_src.get_table_keys()) {
25,160✔
212
        if (should_skip_table(group_src, table_key))
25,160✔
213
            continue;
7,580✔
214
        ConstTableRef table_src = group_src.get_table(table_key);
17,580✔
215
        StringData table_name = table_src->get_name();
17,580✔
216
        TableRef table_dst = group_dst.get_table(table_name);
17,580✔
217
        REALM_ASSERT(table_dst);
17,580✔
218
        std::vector<std::string> columns_to_remove;
17,580✔
219
        for (ColKey col_key : table_dst->get_column_keys()) {
56,816✔
220
            StringData col_name = table_dst->get_column_name(col_key);
56,816✔
221
            ColKey col_key_src = table_src->get_column_key(col_name);
56,816✔
222
            if (!col_key_src) {
56,816✔
223
                columns_to_remove.push_back(col_name);
12✔
224
                continue;
12✔
225
            }
12✔
226
        }
56,816✔
227
        if (!allow_schema_additions && !columns_to_remove.empty()) {
17,580✔
228
            std::string columns_list;
4✔
229
            for (const std::string& col_name : columns_to_remove) {
12✔
230
                columns_list += col_name;
12✔
231
                columns_list += ", ";
12✔
232
            }
12✔
233
            throw ClientResetFailed(
4✔
234
                util::format("Client reset cannot recover when columns have been removed from '%1': {%2}", table_name,
4✔
235
                             columns_list));
4✔
236
        }
4✔
237
    }
17,580✔
238

3,668✔
239
    // Add columns in dst if present in src and absent in dst.
3,668✔
240
    for (auto table_key : group_src.get_table_keys()) {
25,144✔
241
        if (should_skip_table(group_src, table_key))
25,144✔
242
            continue;
7,580✔
243
        ConstTableRef table_src = group_src.get_table(table_key);
17,564✔
244
        StringData table_name = table_src->get_name();
17,564✔
245
        TableRef table_dst = group_dst.get_table(table_name);
17,564✔
246
        REALM_ASSERT(table_dst);
17,564✔
247
        for (ColKey col_key : table_src->get_column_keys()) {
56,880✔
248
            StringData col_name = table_src->get_column_name(col_key);
56,880✔
249
            ColKey col_key_dst = table_dst->get_column_key(col_name);
56,880✔
250
            if (!col_key_dst) {
56,880✔
251
                DataType col_type = table_src->get_column_type(col_key);
136✔
252
                bool nullable = col_key.is_nullable();
136✔
253
                auto search_index_type = table_src->search_index_type(col_key);
136✔
254
                logger.trace(util::LogCategory::reset,
136✔
255
                             "Create column, table = %1, column name = %2, "
136✔
256
                             " type = %3, nullable = %4, search_index = %5",
136✔
257
                             table_name, col_name, col_key.get_type(), nullable, search_index_type);
136✔
258
                ColKey col_key_dst;
136✔
259
                if (Table::is_link_type(col_key.get_type())) {
136✔
260
                    ConstTableRef target_src = table_src->get_link_target(col_key);
48✔
261
                    TableRef target_dst = group_dst.get_table(target_src->get_name());
48✔
262
                    if (col_key.is_list()) {
48✔
263
                        col_key_dst = table_dst->add_column_list(*target_dst, col_name);
16✔
264
                    }
16✔
265
                    else if (col_key.is_set()) {
32✔
266
                        col_key_dst = table_dst->add_column_set(*target_dst, col_name);
×
267
                    }
×
268
                    else if (col_key.is_dictionary()) {
32✔
269
                        DataType key_type = table_src->get_dictionary_key_type(col_key);
8✔
270
                        col_key_dst = table_dst->add_column_dictionary(*target_dst, col_name, key_type);
8✔
271
                    }
8✔
272
                    else {
24✔
273
                        REALM_ASSERT(!col_key.is_collection());
24✔
274
                        col_key_dst = table_dst->add_column(*target_dst, col_name);
24✔
275
                    }
24✔
276
                }
48✔
277
                else if (col_key.is_list()) {
88✔
278
                    col_key_dst = table_dst->add_column_list(col_type, col_name, nullable);
8✔
279
                }
8✔
280
                else if (col_key.is_set()) {
80✔
281
                    col_key_dst = table_dst->add_column_set(col_type, col_name, nullable);
8✔
282
                }
8✔
283
                else if (col_key.is_dictionary()) {
72✔
284
                    DataType key_type = table_src->get_dictionary_key_type(col_key);
8✔
285
                    col_key_dst = table_dst->add_column_dictionary(col_type, col_name, nullable, key_type);
8✔
286
                }
8✔
287
                else {
64✔
288
                    REALM_ASSERT(!col_key.is_collection());
64✔
289
                    col_key_dst = table_dst->add_column(col_type, col_name, nullable);
64✔
290
                }
64✔
291

68✔
292
                if (search_index_type != IndexType::None)
136✔
293
                    table_dst->add_search_index(col_key_dst, search_index_type);
×
294
            }
136✔
295
            else {
56,744✔
296
                // column preexists in dest, make sure the types match
28,372✔
297
                if (col_key.get_type() != col_key_dst.get_type()) {
56,744✔
298
                    throw ClientResetFailed(util::format(
4✔
299
                        "Incompatible column type change detected during client reset for '%1.%2' (%3 vs %4)",
4✔
300
                        table_name, col_name, col_key.get_type(), col_key_dst.get_type()));
4✔
301
                }
4✔
302
                ColumnAttrMask src_col_attrs = col_key.get_attrs();
56,740✔
303
                ColumnAttrMask dst_col_attrs = col_key_dst.get_attrs();
56,740✔
304
                src_col_attrs.reset(ColumnAttr::col_attr_Indexed);
56,740✔
305
                dst_col_attrs.reset(ColumnAttr::col_attr_Indexed);
56,740✔
306
                // make sure the attributes such as collection type, nullability etc. match
28,370✔
307
                // but index equality doesn't matter here.
28,370✔
308
                if (src_col_attrs != dst_col_attrs) {
56,740✔
309
                    throw ClientResetFailed(util::format(
×
310
                        "Incompatable column attribute change detected during client reset for '%1.%2' (%3 vs %4)",
×
311
                        table_name, col_name, col_key.value, col_key_dst.value));
×
312
                }
×
313
            }
56,740✔
314
        }
56,880✔
315
    }
17,564✔
316

3,666✔
317
    // Now the schemas are identical.
3,666✔
318

3,666✔
319
    // Remove objects in dst that are absent in src.
3,666✔
320
    for (auto table_key : group_src.get_table_keys()) {
25,132✔
321
        if (should_skip_table(group_src, table_key))
25,132✔
322
            continue;
7,580✔
323
        auto table_src = group_src.get_table(table_key);
17,552✔
324
        // There are no primary keys in embedded tables but this is ok, because
8,776✔
325
        // embedded objects are tied to the lifetime of top level objects.
8,776✔
326
        if (table_src->is_embedded())
17,552✔
327
            continue;
680✔
328
        StringData table_name = table_src->get_name();
16,872✔
329
        logger.debug(util::LogCategory::reset, "Removing objects in '%1'", table_name);
16,872✔
330
        auto table_dst = group_dst.get_table(table_name);
16,872✔
331

8,436✔
332
        auto pk_col = table_dst->get_primary_key_column();
16,872✔
333
        REALM_ASSERT_DEBUG(pk_col); // sync realms always have a pk
16,872✔
334
        std::vector<std::pair<Mixed, ObjKey>> objects_to_remove;
16,872✔
335
        for (auto obj : *table_dst) {
24,804✔
336
            auto pk = obj.get_any(pk_col);
24,804✔
337
            if (!table_src->find_primary_key(pk)) {
24,804✔
338
                objects_to_remove.emplace_back(pk, obj.get_key());
832✔
339
            }
832✔
340
        }
24,804✔
341
        for (auto& pair : objects_to_remove) {
8,852✔
342
            logger.debug(util::LogCategory::reset, "  removing '%1'", pair.first);
832✔
343
            table_dst->remove_object(pair.second);
832✔
344
        }
832✔
345
    }
16,872✔
346

3,664✔
347
    // We must re-create any missing objects that are absent in dst before trying to copy
3,664✔
348
    // their properties because creating them may re-create any dangling links which would
3,664✔
349
    // otherwise cause inconsistencies when re-creating lists of links.
3,664✔
350
    for (auto table_key : group_src.get_table_keys()) {
25,132✔
351
        ConstTableRef table_src = group_src.get_table(table_key);
25,132✔
352
        auto table_name = table_src->get_name();
25,132✔
353
        if (should_skip_table(group_src, table_key) || table_src->is_embedded())
25,132✔
354
            continue;
8,260✔
355
        TableRef table_dst = group_dst.get_table(table_name);
16,872✔
356
        auto pk_col = table_src->get_primary_key_column();
16,872✔
357
        REALM_ASSERT(pk_col);
16,872✔
358
        logger.debug(util::LogCategory::reset,
16,872✔
359
                     "Creating missing objects for table '%1', number of rows = %2, "
16,872✔
360
                     "primary_key_col = %3, primary_key_type = %4",
16,872✔
361
                     table_name, table_src->size(), pk_col.get_index().val, pk_col.get_type());
16,872✔
362
        for (const Obj& src : *table_src) {
24,648✔
363
            bool created = false;
24,648✔
364
            table_dst->create_object_with_primary_key(src.get_primary_key(), &created);
24,648✔
365
            if (created) {
24,648✔
366
                logger.debug(util::LogCategory::reset, "   created %1", src.get_primary_key());
672✔
367
            }
672✔
368
        }
24,648✔
369
    }
16,872✔
370

3,664✔
371
    converters::EmbeddedObjectConverter embedded_tracker;
7,328✔
372
    // Now src and dst have identical schemas and all the top level objects are created.
3,664✔
373
    // What is left to do is to diff all properties of the existing objects.
3,664✔
374
    // Embedded objects are created on the fly.
3,664✔
375
    for (auto table_key : group_src.get_table_keys()) {
25,132✔
376
        if (should_skip_table(group_src, table_key))
25,132✔
377
            continue;
7,580✔
378
        ConstTableRef table_src = group_src.get_table(table_key);
17,552✔
379
        // Embedded objects don't have a primary key, so they are handled
8,776✔
380
        // as a special case when they are encountered as a link value.
8,776✔
381
        if (table_src->is_embedded())
17,552✔
382
            continue;
680✔
383
        StringData table_name = table_src->get_name();
16,872✔
384
        TableRef table_dst = group_dst.get_table(table_name);
16,872✔
385
        REALM_ASSERT_EX(allow_schema_additions || table_src->get_column_count() == table_dst->get_column_count(),
16,872✔
386
                        allow_schema_additions, table_src->get_column_count(), table_dst->get_column_count());
16,872✔
387
        auto pk_col = table_src->get_primary_key_column();
16,872✔
388
        REALM_ASSERT(pk_col);
16,872✔
389
        logger.debug(util::LogCategory::reset,
16,872✔
390
                     "Updating values for table '%1', number of rows = %2, "
16,872✔
391
                     "number of columns = %3, primary_key_col = %4, "
16,872✔
392
                     "primary_key_type = %5",
16,872✔
393
                     table_name, table_src->size(), table_src->get_column_count(), pk_col.get_index().val,
16,872✔
394
                     pk_col.get_type());
16,872✔
395

8,436✔
396
        converters::InterRealmObjectConverter converter(table_src, table_dst, &embedded_tracker);
16,872✔
397

8,436✔
398
        for (const Obj& src : *table_src) {
24,648✔
399
            auto src_pk = src.get_primary_key();
24,648✔
400
            // create the object - it should have been created above.
12,324✔
401
            auto dst = table_dst->get_object_with_primary_key(src_pk);
24,648✔
402
            REALM_ASSERT(dst);
24,648✔
403

12,324✔
404
            bool updated = false;
24,648✔
405
            converter.copy(src, dst, &updated);
24,648✔
406
            if (updated) {
24,648✔
407
                logger.debug(util::LogCategory::reset, "  updating %1", src_pk);
7,772✔
408
            }
7,772✔
409
        }
24,648✔
410
        embedded_tracker.process_pending();
16,872✔
411
    }
16,872✔
412
}
7,328✔
413

414
// A table without a "class_" prefix will not generate sync instructions.
415
constexpr static std::string_view s_meta_reset_table_name("client_reset_metadata");
416
constexpr static std::string_view s_pk_col_name("id");
417
constexpr static std::string_view s_version_column_name("version");
418
constexpr static std::string_view s_timestamp_col_name("event_time");
419
constexpr static std::string_view s_reset_type_col_name("type_of_reset");
420
constexpr int64_t metadata_version = 1;
421

422
void remove_pending_client_resets(Transaction& wt)
423
{
262✔
424
    if (auto table = wt.get_table(s_meta_reset_table_name); table && !table->is_empty()) {
262✔
425
        table->clear();
262✔
426
    }
262✔
427
}
262✔
428

429
util::Optional<PendingReset> has_pending_reset(const Transaction& rt)
430
{
17,844✔
431
    ConstTableRef table = rt.get_table(s_meta_reset_table_name);
17,844✔
432
    if (!table || table->size() == 0) {
17,844✔
433
        return util::none;
17,034✔
434
    }
17,034✔
435
    ColKey timestamp_col = table->get_column_key(s_timestamp_col_name);
810✔
436
    ColKey type_col = table->get_column_key(s_reset_type_col_name);
810✔
437
    ColKey version_col = table->get_column_key(s_version_column_name);
810✔
438
    REALM_ASSERT(timestamp_col);
810✔
439
    REALM_ASSERT(type_col);
810✔
440
    REALM_ASSERT(version_col);
810✔
441
    if (table->size() > 1) {
810✔
442
        // this may happen if a future version of this code changes the format and expectations around reset metadata.
443
        throw ClientResetFailed(
×
444
            util::format("Previous client resets detected (%1) but only one is expected.", table->size()));
×
445
    }
×
446
    Obj first = *table->begin();
810✔
447
    REALM_ASSERT(first);
810✔
448
    PendingReset pending;
810✔
449
    int64_t version = first.get<int64_t>(version_col);
810✔
450
    pending.time = first.get<Timestamp>(timestamp_col);
810✔
451
    if (version > metadata_version) {
810✔
452
        throw ClientResetFailed(util::format("Unsupported client reset metadata version: %1 vs %2, from %3", version,
×
453
                                             metadata_version, pending.time));
×
454
    }
×
455
    int64_t type = first.get<int64_t>(type_col);
810✔
456
    if (type == 0) {
810✔
457
        pending.type = ClientResyncMode::DiscardLocal;
440✔
458
    }
440✔
459
    else if (type == 1) {
370✔
460
        pending.type = ClientResyncMode::Recover;
370✔
461
    }
370✔
UNCOV
462
    else {
×
UNCOV
463
        throw ClientResetFailed(
×
UNCOV
464
            util::format("Unsupported client reset metadata type: %1 from %2", type, pending.time));
×
UNCOV
465
    }
×
466
    return pending;
810✔
467
}
810✔
468

469
void track_reset(Transaction& wt, ClientResyncMode mode)
470
{
7,396✔
471
    REALM_ASSERT(mode != ClientResyncMode::Manual);
7,396✔
472
    TableRef table = wt.get_table(s_meta_reset_table_name);
7,396✔
473
    ColKey version_col, timestamp_col, type_col;
7,396✔
474
    if (!table) {
7,396✔
475
        table = wt.add_table_with_primary_key(s_meta_reset_table_name, type_ObjectId, s_pk_col_name);
7,352✔
476
        REALM_ASSERT(table);
7,352✔
477
        version_col = table->add_column(type_Int, s_version_column_name);
7,352✔
478
        timestamp_col = table->add_column(type_Timestamp, s_timestamp_col_name);
7,352✔
479
        type_col = table->add_column(type_Int, s_reset_type_col_name);
7,352✔
480
    }
7,352✔
481
    else {
44✔
482
        version_col = table->get_column_key(s_version_column_name);
44✔
483
        timestamp_col = table->get_column_key(s_timestamp_col_name);
44✔
484
        type_col = table->get_column_key(s_reset_type_col_name);
44✔
485
    }
44✔
486
    REALM_ASSERT(version_col);
7,396✔
487
    REALM_ASSERT(timestamp_col);
7,396✔
488
    REALM_ASSERT(type_col);
7,396✔
489
    int64_t mode_val = 0; // Discard
7,396✔
490
    if (mode == ClientResyncMode::Recover || mode == ClientResyncMode::RecoverOrDiscard) {
7,396✔
491
        mode_val = 1; // Recover
3,732✔
492
    }
3,732✔
493

3,698✔
494
    if (table->size() > 1) {
7,396✔
495
        // this may happen if a future version of this code changes the format and expectations around reset metadata.
496
        throw ClientResetFailed(
×
497
            util::format("Previous client resets detected (%1) but only one is expected.", table->size()));
×
498
    }
×
499
    table->create_object_with_primary_key(ObjectId::gen(),
7,396✔
500
                                          {{version_col, metadata_version},
7,396✔
501
                                           {timestamp_col, Timestamp(std::chrono::system_clock::now())},
7,396✔
502
                                           {type_col, mode_val}});
7,396✔
503

3,698✔
504
    // Ensure we save the tracker object even if we encounter an error and roll
3,698✔
505
    // back the client reset later
3,698✔
506
    wt.commit_and_continue_writing();
7,396✔
507
}
7,396✔
508

509
static ClientResyncMode reset_precheck_guard(Transaction& wt, ClientResyncMode mode, bool recovery_is_allowed,
510
                                             util::Logger& logger)
511
{
7,396✔
512
    if (auto previous_reset = has_pending_reset(wt)) {
7,396✔
513
        logger.info(util::LogCategory::reset, "A previous reset was detected of type: '%1' at: %2",
32✔
514
                    previous_reset->type, previous_reset->time);
32✔
515
        switch (previous_reset->type) {
32✔
516
            case ClientResyncMode::Manual:
✔
517
                REALM_UNREACHABLE();
518
            case ClientResyncMode::DiscardLocal:
12✔
519
                throw ClientResetFailed(util::format("A previous '%1' mode reset from %2 did not succeed, "
12✔
520
                                                     "giving up on '%3' mode to prevent a cycle",
12✔
521
                                                     previous_reset->type, previous_reset->time, mode));
12✔
522
            case ClientResyncMode::Recover:
20✔
523
                switch (mode) {
20✔
524
                    case ClientResyncMode::Recover:
8✔
525
                        throw ClientResetFailed(util::format("A previous '%1' mode reset from %2 did not succeed, "
8✔
526
                                                             "giving up on '%3' mode to prevent a cycle",
8✔
527
                                                             previous_reset->type, previous_reset->time, mode));
8✔
528
                    case ClientResyncMode::RecoverOrDiscard:
8✔
529
                        mode = ClientResyncMode::DiscardLocal;
8✔
530
                        logger.info(util::LogCategory::reset,
8✔
531
                                    "A previous '%1' mode reset from %2 downgrades this mode ('%3') to DiscardLocal",
8✔
532
                                    previous_reset->type, previous_reset->time, mode);
8✔
533
                        remove_pending_client_resets(wt);
8✔
534
                        break;
8✔
535
                    case ClientResyncMode::DiscardLocal:
4✔
536
                        remove_pending_client_resets(wt);
4✔
537
                        // previous mode Recover and this mode is Discard, this is not a cycle yet
2✔
538
                        break;
4✔
539
                    case ClientResyncMode::Manual:
✔
540
                        REALM_UNREACHABLE();
541
                }
20✔
542
                break;
16✔
543
            case ClientResyncMode::RecoverOrDiscard:
10✔
544
                throw ClientResetFailed(util::format("Unexpected previous '%1' mode reset from %2 did not "
×
545
                                                     "succeed, giving up on '%3' mode to prevent a cycle",
×
546
                                                     previous_reset->type, previous_reset->time, mode));
×
547
        }
7,376✔
548
    }
7,376✔
549
    if (!recovery_is_allowed) {
7,376✔
550
        if (mode == ClientResyncMode::Recover) {
24✔
551
            throw ClientResetFailed(
4✔
552
                "Client reset mode is set to 'Recover' but the server does not allow recovery for this client");
4✔
553
        }
4✔
554
        else if (mode == ClientResyncMode::RecoverOrDiscard) {
20✔
555
            logger.info(util::LogCategory::reset,
12✔
556
                        "Client reset in 'RecoverOrDiscard' is choosing 'DiscardLocal' because the server does not "
12✔
557
                        "permit recovery for this client");
12✔
558
            mode = ClientResyncMode::DiscardLocal;
12✔
559
        }
12✔
560
    }
24✔
561
    track_reset(wt, mode);
7,374✔
562
    return mode;
7,372✔
563
}
7,376✔
564

565
bool perform_client_reset_diff(DB& db_local, DB& db_remote, sync::SaltedFileIdent client_file_ident,
566
                               util::Logger& logger, ClientResyncMode mode, bool recovery_is_allowed,
567
                               sync::SubscriptionStore* sub_store,
568
                               util::FunctionRef<void(int64_t)> on_flx_version_complete)
569
{
7,396✔
570
    auto wt_local = db_local.start_write();
7,396✔
571
    auto actual_mode = reset_precheck_guard(*wt_local, mode, recovery_is_allowed, logger);
7,396✔
572
    bool recover_local_changes =
7,396✔
573
        actual_mode == ClientResyncMode::Recover || actual_mode == ClientResyncMode::RecoverOrDiscard;
7,396✔
574

3,698✔
575
    logger.info(util::LogCategory::reset,
7,396✔
576
                "Client reset: path_local = %1, "
7,396✔
577
                "client_file_ident = (ident: %2, salt: %3), "
7,396✔
578
                "remote_path = %4, requested_mode = %5, recovery_is_allowed = %6, "
7,396✔
579
                "actual_mode = %7, will_recover = %8",
7,396✔
580
                db_local.get_path(), client_file_ident.ident, client_file_ident.salt, db_remote.get_path(), mode,
7,396✔
581
                recovery_is_allowed, actual_mode, recover_local_changes);
7,396✔
582

3,698✔
583
    auto& repl_local = dynamic_cast<ClientReplication&>(*db_local.get_replication());
7,396✔
584
    auto& history_local = repl_local.get_history();
7,396✔
585
    history_local.ensure_updated(wt_local->get_version());
7,396✔
586
    VersionID old_version_local = wt_local->get_version_of_current_transaction();
7,396✔
587

3,698✔
588
    auto& repl_remote = dynamic_cast<ClientReplication&>(*db_remote.get_replication());
7,396✔
589
    auto& history_remote = repl_remote.get_history();
7,396✔
590

3,698✔
591
    sync::SaltedVersion fresh_server_version = {0, 0};
7,396✔
592
    {
7,396✔
593
        SyncProgress remote_progress;
7,396✔
594
        sync::version_type remote_version_unused;
7,396✔
595
        SaltedFileIdent remote_ident_unused;
7,396✔
596
        history_remote.get_status(remote_version_unused, remote_ident_unused, remote_progress);
7,396✔
597
        fresh_server_version = remote_progress.latest_server_version;
7,396✔
598
    }
7,396✔
599

3,698✔
600
    TransactionRef tr_remote;
7,396✔
601
    std::vector<client_reset::RecoveredChange> recovered;
7,396✔
602
    if (recover_local_changes) {
7,396✔
603
        auto frozen_pre_local_state = db_local.start_frozen();
3,720✔
604
        auto local_changes = history_local.get_local_changes(wt_local->get_version());
3,720✔
605
        logger.info("Local changesets to recover: %1", local_changes.size());
3,720✔
606

1,860✔
607
        tr_remote = db_remote.start_write();
3,720✔
608
        recovered = process_recovered_changesets(*tr_remote, *frozen_pre_local_state, logger, local_changes);
3,720✔
609
    }
3,720✔
610
    else {
3,676✔
611
        tr_remote = db_remote.start_read();
3,676✔
612
    }
3,676✔
613

3,698✔
614
    // transform the local Realm such that all public tables become identical to the remote Realm
3,698✔
615
    transfer_group(*tr_remote, *wt_local, logger, false);
7,396✔
616

3,698✔
617
    // now that the state of the fresh and local Realms are identical,
3,698✔
618
    // reset the local sync history and steal the fresh Realm's ident
3,698✔
619
    history_local.set_client_reset_adjustments(logger, wt_local->get_version(), client_file_ident,
7,396✔
620
                                               fresh_server_version, recovered);
7,396✔
621

3,698✔
622
    int64_t subscription_version = 0;
7,396✔
623
    if (sub_store) {
7,396✔
624
        if (recover_local_changes) {
124✔
625
            subscription_version = sub_store->mark_active_as_complete(*wt_local);
76✔
626
        }
76✔
627
        else {
48✔
628
            subscription_version = sub_store->set_active_as_latest(*wt_local);
48✔
629
        }
48✔
630
    }
124✔
631

3,698✔
632
    wt_local->commit_and_continue_as_read();
7,396✔
633
    on_flx_version_complete(subscription_version);
7,396✔
634

3,698✔
635
    VersionID new_version_local = wt_local->get_version_of_current_transaction();
7,396✔
636
    logger.info(util::LogCategory::reset,
7,396✔
637
                "perform_client_reset_diff is done: old_version = (version: %1, index: %2), "
7,396✔
638
                "new_version = (version: %3, index: %4)",
7,396✔
639
                old_version_local.version, old_version_local.index, new_version_local.version,
7,396✔
640
                new_version_local.index);
7,396✔
641

3,698✔
642
    return recover_local_changes;
7,396✔
643
}
7,396✔
644

645
} // namespace realm::_impl::client_reset
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