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

realm / realm-core / 2466

02 Jul 2024 04:06AM UTC coverage: 90.974% (-0.2%) from 91.147%
2466

push

Evergreen

web-flow
Merge pull request #7576 from realm/tg/multi-process-launch-actions

RCORE-1900 Make "next launch" metadata actions multiprocess-safe

102260 of 180446 branches covered (56.67%)

348 of 356 new or added lines in 15 files covered. (97.75%)

334 existing lines in 18 files now uncovered.

215128 of 236473 relevant lines covered (90.97%)

5909897.4 hits per line

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

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

19
#if REALM_ENABLE_SYNC
20
#if REALM_ENABLE_AUTH_TESTS
21

22
#include <util/crypt_key.hpp>
23
#include <util/sync/baas_admin_api.hpp>
24
#include <util/sync/flx_sync_harness.hpp>
25

26
#include <realm/object-store/impl/object_accessor_impl.hpp>
27
#include <realm/object-store/impl/realm_coordinator.hpp>
28
#include <realm/object-store/sync/async_open_task.hpp>
29
#include <realm/object-store/sync/mongo_client.hpp>
30
#include <realm/object-store/sync/mongo_collection.hpp>
31
#include <realm/object-store/sync/mongo_database.hpp>
32
#include <realm/object-store/util/scheduler.hpp>
33

34
#include <realm/sync/noinst/client_history_impl.hpp>
35

36
#include <catch2/catch_all.hpp>
37

38
#include <any>
39

40
using namespace std::string_literals;
41
using namespace realm::sync;
42

43
namespace realm::app {
44

45
namespace {
46

47
void create_schema(const AppSession& app_session, Schema target_schema, int64_t target_schema_version)
48
{
38✔
49
    auto create_config = app_session.config;
38✔
50
    create_config.schema = target_schema;
38✔
51
    app_session.admin_api.create_schema(app_session.server_app_id, create_config);
38✔
52

53
    timed_sleeping_wait_for(
38✔
54
        [&] {
121✔
55
            auto versions = app_session.admin_api.get_schema_versions(app_session.server_app_id);
121✔
56
            return std::any_of(versions.begin(), versions.end(), [&](const AdminAPISession::SchemaVersionInfo& info) {
132✔
57
                return info.version_major == target_schema_version;
132✔
58
            });
132✔
59
        },
121✔
60
        std::chrono::minutes(5), std::chrono::seconds(1));
38✔
61

62
    // FIXME: There is a delay on the server between the schema being created and actually ready to use. This is due
63
    // to resource pool key cache keys using second precision (BAAS-18361). So we wait for a couple of seconds so the
64
    // app is refreshed.
65
    const auto wait_start = std::chrono::steady_clock::now();
38✔
66
    using namespace std::chrono_literals;
38✔
67
    util::EventLoop::main().run_until([&]() -> bool {
130,950,984✔
68
        return std::chrono::steady_clock::now() - wait_start >= 2s;
130,950,984✔
69
    });
130,950,984✔
70
}
38✔
71

72
std::vector<ObjectSchema> get_schema_v0()
73
{
44✔
74
    return {
44✔
75
        {"Embedded", ObjectSchema::ObjectType::Embedded, {{"str_field", PropertyType::String}}},
44✔
76
        {"TopLevel",
44✔
77
         {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
44✔
78
          {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
44✔
79
          {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
44✔
80
          {"non_queryable_field", PropertyType::String | PropertyType::Nullable},
44✔
81
          {"non_queryable_field2", PropertyType::String}}},
44✔
82
        {"TopLevel2",
44✔
83
         {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
44✔
84
          {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
44✔
85
          {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
44✔
86
          {"non_queryable_field", PropertyType::String | PropertyType::Nullable}}},
44✔
87
        {"TopLevel3",
44✔
88
         {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
44✔
89
          {"queryable_int_field", PropertyType::Int},
44✔
90
          {"link", PropertyType::Object | PropertyType::Nullable, "TopLevel"},
44✔
91
          {"embedded_link", PropertyType::Object | PropertyType::Nullable, "Embedded"}}},
44✔
92
    };
44✔
93
}
44✔
94

95
auto get_subscription_initializer_callback_for_schema_v0()
96
{
22✔
97
    return [](std::shared_ptr<Realm> realm) mutable {
22✔
98
        REQUIRE(realm);
18!
99
        auto table = realm->read_group().get_table("class_TopLevel");
18✔
100
        auto col_key = table->get_column_key("queryable_int_field");
18✔
101
        auto query = Query(table).greater_equal(col_key, int64_t(0));
18✔
102
        auto table2 = realm->read_group().get_table("class_TopLevel2");
18✔
103
        Query query2(table2);
18✔
104
        table = realm->read_group().get_table("class_TopLevel3");
18✔
105
        col_key = table->get_column_key("queryable_int_field");
18✔
106
        auto query3 = Query(table).greater_equal(col_key, int64_t(0));
18✔
107
        auto subs = realm->get_latest_subscription_set().make_mutable_copy();
18✔
108
        subs.clear();
18✔
109
        subs.insert_or_assign(query);
18✔
110
        subs.insert_or_assign(query2);
18✔
111
        subs.insert_or_assign(query3);
18✔
112
        subs.commit();
18✔
113
    };
18✔
114
}
22✔
115

116
// The following breaking changes are applied to schema at v0:
117
//  * Table 'TopLevel2' is removed
118
//  * Field 'queryable_str_field' in table 'TopLevel' is removed (the user does not query on it)
119
//  * Field 'non_queryable_field' in table 'TopLevel' is marked required
120
//  * Field 'non_queryable_field2' in table 'TopLevel' is marked optional
121
//  * Filed 'queryable_int_field' in table 'TopLevel3' is removed (the user queries on it)
122
std::vector<ObjectSchema> get_schema_v1()
123
{
36✔
124
    return {
36✔
125
        {"Embedded", ObjectSchema::ObjectType::Embedded, {{"str_field", PropertyType::String}}},
36✔
126
        {"TopLevel",
36✔
127
         {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
36✔
128
          {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
36✔
129
          {"non_queryable_field", PropertyType::String},
36✔
130
          {"non_queryable_field2", PropertyType::String | PropertyType::Nullable}}},
36✔
131
        {"TopLevel3",
36✔
132
         {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
36✔
133
          {"link", PropertyType::Object | PropertyType::Nullable, "TopLevel"},
36✔
134
          {"embedded_link", PropertyType::Object | PropertyType::Nullable, "Embedded"}}},
36✔
135
    };
36✔
136
}
36✔
137

138
auto get_subscription_initializer_callback_for_schema_v1()
139
{
18✔
140
    return [](std::shared_ptr<Realm> realm) mutable {
18✔
141
        REQUIRE(realm);
16!
142
        auto table = realm->read_group().get_table("class_TopLevel");
16✔
143
        Query query(table);
16✔
144
        table = realm->read_group().get_table("class_TopLevel3");
16✔
145
        Query query2(table);
16✔
146
        auto subs = realm->get_latest_subscription_set().make_mutable_copy();
16✔
147
        subs.clear();
16✔
148
        subs.insert_or_assign(query);
16✔
149
        subs.insert_or_assign(query2);
16✔
150
        subs.commit();
16✔
151
    };
16✔
152
}
18✔
153

154
// The following breaking changes are applied to schema at v1:
155
//  * Field 'queryable_int_field' in table 'TopLevel' is marked required
156
std::vector<ObjectSchema> get_schema_v2()
157
{
4✔
158
    return {
4✔
159
        {"Embedded", ObjectSchema::ObjectType::Embedded, {{"str_field", PropertyType::String}}},
4✔
160
        {"TopLevel",
4✔
161
         {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
162
          {"queryable_int_field", PropertyType::Int},
4✔
163
          {"non_queryable_field", PropertyType::String},
4✔
164
          {"non_queryable_field2", PropertyType::String | PropertyType::Nullable}}},
4✔
165
        {"TopLevel3",
4✔
166
         {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
167
          {"link", PropertyType::Object | PropertyType::Nullable, "TopLevel"},
4✔
168
          {"embedded_link", PropertyType::Object | PropertyType::Nullable, "Embedded"}}},
4✔
169
    };
4✔
170
}
4✔
171

172
auto get_subscription_initializer_callback_for_schema_v2()
173
{
4✔
174
    return [](std::shared_ptr<Realm> realm) mutable {
4✔
175
        REQUIRE(realm);
4!
176
        auto table = realm->read_group().get_table("class_TopLevel");
4✔
177
        auto col_key = table->get_column_key("queryable_int_field");
4✔
178
        auto query = Query(table).greater_equal(col_key, int64_t(5));
4✔
179
        table = realm->read_group().get_table("class_TopLevel3");
4✔
180
        Query query2(table);
4✔
181
        auto subs = realm->get_latest_subscription_set().make_mutable_copy();
4✔
182
        subs.clear();
4✔
183
        subs.insert_or_assign(query);
4✔
184
        subs.insert_or_assign(query2);
4✔
185
        subs.commit();
4✔
186
    };
4✔
187
}
4✔
188

189
// Sort 'computed_properties' and 'persisted_properties'.
190
ObjectSchema sort_schema_properties(const ObjectSchema& schema)
191
{
680✔
192
    ObjectSchema target_schema = schema;
680✔
193
    auto predicate = [](const Property& a, const Property& b) {
3,744✔
194
        return a.name < b.name;
3,744✔
195
    };
3,744✔
196
    std::vector<Property> persisted_properties = schema.persisted_properties;
680✔
197
    std::sort(std::begin(persisted_properties), std::end(persisted_properties), predicate);
680✔
198
    target_schema.persisted_properties = persisted_properties;
680✔
199
    std::vector<Property> computed_properties = schema.computed_properties;
680✔
200
    std::sort(std::begin(computed_properties), std::end(computed_properties), predicate);
680✔
201
    target_schema.computed_properties = computed_properties;
680✔
202
    return target_schema;
680✔
203
}
680✔
204

205
// Check realm's schema and target_schema match.
206
void check_realm_schema(const std::string& path, const std::vector<ObjectSchema>& target_schema,
207
                        uint64_t target_schema_version)
208
{
96✔
209
    DBOptions options;
96✔
210
    options.encryption_key = test_util::crypt_key();
96✔
211
    auto db = DB::create(sync::make_client_replication(), path, options);
96✔
212
    auto realm_schema = ObjectStore::schema_from_group(*db->start_read());
96✔
213
    auto realm_schema_version = ObjectStore::get_schema_version(*db->start_read());
96✔
214
    CHECK(realm_schema_version == target_schema_version);
96!
215
    CHECK(realm_schema.size() == target_schema.size());
96!
216

217
    for (auto& object : target_schema) {
340✔
218
        auto it = realm_schema.find(object);
340✔
219
        CHECK(it != realm_schema.end());
340!
220
        auto target_object_schema = sort_schema_properties(object);
340✔
221
        auto realm_object_schema = sort_schema_properties(*it);
340✔
222
        CHECK(target_object_schema == realm_object_schema);
340!
223
    }
340✔
224
}
96✔
225

226
auto make_error_handler()
227
{
16✔
228
    auto [error_promise, error_future] = util::make_promise_future<SyncError>();
16✔
229
    auto shared_promise = std::make_shared<decltype(error_promise)>(std::move(error_promise));
16✔
230
    auto fn = [error_promise = std::move(shared_promise)](std::shared_ptr<SyncSession>, SyncError err) {
16✔
231
        error_promise->emplace_value(std::move(err));
16✔
232
    };
16✔
233
    return std::make_pair(std::move(error_future), std::move(fn));
16✔
234
}
16✔
235

236
} // namespace
237

238
TEST_CASE("Sync schema migrations don't work with sync open", "[sync][flx][flx schema migration][baas]") {
4✔
239
    auto schema_v0 = get_schema_v0();
4✔
240
    FLXSyncTestHarness harness("flx_sync_schema_migration",
4✔
241
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
4✔
242
    auto config = harness.make_test_file();
4✔
243

244
    // First open the realm at schema version 0.
245
    {
4✔
246
        auto realm = Realm::get_shared_realm(config);
4✔
247
        subscribe_to_all_and_bootstrap(*realm);
4✔
248
        wait_for_upload(*realm);
4✔
249
        check_realm_schema(config.path, schema_v0, 0);
4✔
250
    }
4✔
251

252
    const AppSession& app_session = harness.session().app_session();
4✔
253

254
    // Bump the schema version.
255
    config.schema_version = 1;
4✔
256
    auto schema_v1 = schema_v0;
4✔
257

258
    SECTION("Breaking change detected by client") {
4✔
259
        // Make field 'non_queryable_field2' of table 'TopLevel' optional.
260
        schema_v1[1].persisted_properties.back() = {"non_queryable_field2",
2✔
261
                                                    PropertyType::String | PropertyType::Nullable};
2✔
262
        config.schema = schema_v1;
2✔
263
        create_schema(app_session, *config.schema, config.schema_version);
2✔
264

265
        REQUIRE_THROWS_AS(Realm::get_shared_realm(config), InvalidAdditiveSchemaChangeException);
2✔
266
        check_realm_schema(config.path, schema_v0, 0);
2✔
267
    }
2✔
268

269
    SECTION("Breaking change detected by server") {
4✔
270
        // Remove table 'TopLevel2'.
271
        schema_v1.erase(schema_v1.begin() + 2);
2✔
272
        config.schema = schema_v1;
2✔
273
        create_schema(app_session, *config.schema, config.schema_version);
2✔
274

275
        config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession>,
2✔
276
                                                            const SyncClientHookData& data) mutable {
6✔
277
            if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
6✔
278
                return SyncClientHookAction::NoAction;
4✔
279
            }
4✔
280

281
            auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
2✔
282
            if (error_code == sync::ProtocolError::initial_sync_not_completed) {
2✔
283
                return SyncClientHookAction::NoAction;
×
284
            }
×
285
            CHECK(error_code == sync::ProtocolError::schema_version_changed);
2!
286
            return SyncClientHookAction::NoAction;
2✔
287
        };
2✔
288
        auto realm = Realm::get_shared_realm(config);
2✔
289
        wait_for_download(*realm);
2✔
290
        wait_for_upload(*realm);
2✔
291

292
        auto table = realm->read_group().get_table("class_TopLevel2");
2✔
293
        // Migration did not succeed because table 'TopLevel2' still exists (but there is no error).
294
        CHECK(table);
2!
295
        check_realm_schema(config.path, schema_v0, 1);
2✔
296
    }
2✔
297
}
4✔
298

299
TEST_CASE("Cannot migrate schema to unknown version", "[sync][flx][flx schema migration][baas]") {
12✔
300
    auto schema_v0 = get_schema_v0();
12✔
301
    FLXSyncTestHarness harness("flx_sync_schema_migration",
12✔
302
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
12✔
303
    auto config = harness.make_test_file();
12✔
304

305
    const AppSession& app_session = harness.session().app_session();
12✔
306
    auto schema_v1 = get_schema_v1();
12✔
307

308
    uint64_t target_schema_version = 0;
12✔
309
    std::vector<ObjectSchema> target_schema;
12✔
310

311
    SECTION("Fresh realm") {
12✔
312
        target_schema_version = -1;
4✔
313
        target_schema = {};
4✔
314

315
        SECTION("No schema versions") {
4✔
316
        }
2✔
317

318
        SECTION("Schema versions") {
4✔
319
            create_schema(app_session, schema_v1, 1);
2✔
320
        }
2✔
321
    }
4✔
322

323
    SECTION("Existing realm") {
12✔
324
        auto schema_version = GENERATE(0, 42);
8✔
325

326
        // First open the realm at schema version 0.
327
        {
8✔
328
            auto realm = Realm::get_shared_realm(config);
8✔
329
            subscribe_to_all_and_bootstrap(*realm);
8✔
330
            wait_for_upload(*realm);
8✔
331
        }
8✔
332

333
        // Then set the right schema version.
334
        DBOptions options;
8✔
335
        options.encryption_key = test_util::crypt_key();
8✔
336
        auto db = DB::create(sync::make_client_replication(), config.path, options);
8✔
337
        auto tr = db->start_write();
8✔
338
        ObjectStore::set_schema_version(*tr, schema_version);
8✔
339
        tr->commit();
8✔
340

341
        target_schema_version = schema_version;
8✔
342
        target_schema = schema_v0;
8✔
343

344
        SECTION(util::format("No schema versions | Realm schema: %1", schema_version)) {
8✔
345
        }
4✔
346

347
        SECTION(util::format("Schema versions | Realm schema: %1", schema_version)) {
8✔
348
            create_schema(app_session, schema_v1, 1);
4✔
349
        }
4✔
350
    }
8✔
351

352
    // Bump the schema to a version the server does not know about.
353
    config.schema_version = 42;
12✔
354
    config.schema = schema_v0;
12✔
355
    auto&& [error_future, err_handler] = make_error_handler();
12✔
356
    config.sync_config->error_handler = err_handler;
12✔
357

358
    {
12✔
359
        auto status = async_open_realm(config);
12✔
360
        REQUIRE_FALSE(status.is_ok());
12!
361
        REQUIRE_THAT(status.get_status().reason(),
12✔
362
                     Catch::Matchers::ContainsSubstring("Client provided invalid schema version"));
12✔
363
        error_future.get();
12✔
364
        check_realm_schema(config.path, target_schema, target_schema_version);
12✔
365
    }
12✔
366

367
    // Update schema version to 0 and try again (the version now matches the actual schema).
368
    config.schema_version = 0;
12✔
369
    config.sync_config->error_handler = nullptr;
12✔
370
    REQUIRE(async_open_realm(config).is_ok());
12!
371
    check_realm_schema(config.path, schema_v0, 0);
12✔
372
}
12✔
373

374
TEST_CASE("Schema version mismatch between client and server", "[sync][flx][flx schema migration][baas]") {
4✔
375
    auto schema_v0 = get_schema_v0();
4✔
376
    FLXSyncTestHarness harness("flx_sync_schema_migration",
4✔
377
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
4✔
378
    auto config = harness.make_test_file();
4✔
379

380
    const AppSession& app_session = harness.session().app_session();
4✔
381
    auto schema_v1 = get_schema_v1();
4✔
382
    create_schema(app_session, schema_v1, 1);
4✔
383

384
    {
4✔
385
        auto realm = Realm::get_shared_realm(config);
4✔
386
        subscribe_to_all_and_bootstrap(*realm);
4✔
387
        wait_for_upload(*realm);
4✔
388

389
        realm->sync_session()->shutdown_and_wait();
4✔
390
        check_realm_schema(config.path, schema_v0, 0);
4✔
391
    }
4✔
392
    REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config.path));
4!
393

394
    SECTION("Realm already on the latest schema version") {
4✔
395
        DBOptions options;
2✔
396
        options.encryption_key = test_util::crypt_key();
2✔
397
        auto db = DB::create(sync::make_client_replication(), config.path, options);
2✔
398
        auto tr = db->start_write();
2✔
399
        ObjectStore::set_schema_version(*tr, 1);
2✔
400
        tr->commit();
2✔
401
        auto schema_version = ObjectStore::get_schema_version(*db->start_read());
2✔
402
        CHECK(schema_version == 1);
2!
403
    }
2✔
404

405
    SECTION("Open realm with the lastest schema version for the first time") {
4✔
406
    }
2✔
407

408
    config.schema_version = 1;
4✔
409
    config.schema = schema_v0;
4✔
410

411
    auto schema_migration_required = false;
4✔
412
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
4✔
413
    config.sync_config->error_handler = nullptr;
4✔
414
    config.sync_config->on_sync_client_event_hook =
4✔
415
        [&schema_migration_required](std::weak_ptr<SyncSession>, const SyncClientHookData& data) mutable {
50✔
416
            if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
50✔
417
                return SyncClientHookAction::NoAction;
46✔
418
            }
46✔
419
            auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
4✔
420
            if (error_code != sync::ProtocolError::schema_version_changed) {
4✔
421
                return SyncClientHookAction::NoAction;
×
422
            }
×
423
            schema_migration_required = true;
4✔
424
            return SyncClientHookAction::NoAction;
4✔
425
        };
4✔
426

427
    auto status = async_open_realm(config);
4✔
428
    REQUIRE_FALSE(status.is_ok());
4!
429
    REQUIRE_THAT(
4✔
430
        status.get_status().reason(),
4✔
431
        Catch::Matchers::ContainsSubstring("The following changes cannot be made in additive-only schema mode"));
4✔
432
    REQUIRE(schema_migration_required);
4!
433
    // Applying the new schema (and version) fails, therefore the schema is unversioned (the metadata table is removed
434
    // during migration). There is a schema though because the server schema is already applied by the time the client
435
    // applies the mismatch schema.
436
    check_realm_schema(config.path, schema_v1, ObjectStore::NotVersioned);
4✔
437
    wait_for_sessions_to_close(harness.session());
4✔
438
}
4✔
439

440
TEST_CASE("Fresh realm does not require schema migration", "[sync][flx][flx schema migration][baas]") {
2✔
441
    auto schema_v0 = get_schema_v0();
2✔
442
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
443
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
444
    auto config = harness.make_test_file();
2✔
445

446
    const AppSession& app_session = harness.session().app_session();
2✔
447
    auto schema_v1 = get_schema_v1();
2✔
448
    create_schema(app_session, schema_v1, 1);
2✔
449

450
    config.schema_version = 1;
2✔
451
    config.schema = schema_v1;
2✔
452
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
453
    config.sync_config->on_sync_client_event_hook = [](std::weak_ptr<SyncSession>,
2✔
454
                                                       const SyncClientHookData& data) mutable {
28✔
455
        if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
28✔
456
            return SyncClientHookAction::NoAction;
28✔
457
        }
28✔
458
        auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
×
459
        CHECK(error_code == sync::ProtocolError::initial_sync_not_completed);
×
460
        return SyncClientHookAction::NoAction;
×
461
    };
×
462

463
    REQUIRE(async_open_realm(config).is_ok());
2!
464
    check_realm_schema(config.path, schema_v1, 1);
2✔
465
}
2✔
466

467
TEST_CASE("Upgrade schema version (with recovery) then downgrade", "[sync][flx][flx schema migration][baas]") {
2✔
468
    auto schema_v0 = get_schema_v0();
2✔
469
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
470
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
471
    auto config = harness.make_test_file();
2✔
472

473
    {
2✔
474
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
475
        auto realm = Realm::get_shared_realm(config);
2✔
476
        wait_for_download(*realm);
2✔
477
        wait_for_upload(*realm);
2✔
478
        check_realm_schema(config.path, schema_v0, 0);
2✔
479

480
        realm->sync_session()->shutdown_and_wait();
2✔
481

482
        // Subscription to recover when upgrading the schema.
483
        auto subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
484
        CHECK(subs.erase_by_class_name("TopLevel2"));
2!
485
        auto table = realm->read_group().get_table("class_TopLevel2");
2✔
486
        auto col_key = table->get_column_key("queryable_int_field");
2✔
487
        auto query = Query(table).greater_equal(col_key, int64_t(0));
2✔
488
        subs.insert_or_assign(query);
2✔
489
        subs.commit();
2✔
490

491
        // Object to recover when upgrading the schema.
492
        realm->begin_transaction();
2✔
493
        CppContext c(realm);
2✔
494
        Object::create(c, realm, "TopLevel",
2✔
495
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
496
                                        {"queryable_str_field", "biz"s},
2✔
497
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
498
                                        {"non_queryable_field2", "non queryable 33"s}}));
2✔
499
        realm->commit_transaction();
2✔
500
        // The server filters out this object because the schema version the client migrates to removes the queryable
501
        // field.
502
        realm->begin_transaction();
2✔
503
        Object::create(
2✔
504
            c, realm, "TopLevel3",
2✔
505
            std::any(AnyDict{{"_id", ObjectId::gen()}, {"queryable_int_field", static_cast<int64_t>(42)}}));
2✔
506
        realm->commit_transaction();
2✔
507
        realm->close();
2✔
508
    }
2✔
509

510
    auto obj3_id = ObjectId::gen();
2✔
511
    harness.load_initial_data([&](SharedRealm realm) {
2✔
512
        CppContext c(realm);
2✔
513
        Object::create(c, realm, "TopLevel",
2✔
514
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
515
                                        {"queryable_str_field", "foo"s},
2✔
516
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
517
                                        {"non_queryable_field", "non queryable 1"s},
2✔
518
                                        {"non_queryable_field2", "non queryable 11"s}}));
2✔
519
        Object::create(c, realm, "TopLevel",
2✔
520
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
521
                                        {"queryable_str_field", "bar"s},
2✔
522
                                        {"queryable_int_field", static_cast<int64_t>(10)},
2✔
523
                                        {"non_queryable_field", "non queryable 2"s},
2✔
524
                                        {"non_queryable_field2", "non queryable 22"s}}));
2✔
525
        Object::create(c, realm, "TopLevel2",
2✔
526
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
527
                                        {"queryable_str_field", "foo2"s},
2✔
528
                                        {"queryable_int_field", static_cast<int64_t>(10)},
2✔
529
                                        {"non_queryable_field", "non queryable 2"s}}));
2✔
530
        Object::create(c, realm, "TopLevel3",
2✔
531
                       std::any(AnyDict{{"_id", obj3_id}, {"queryable_int_field", static_cast<int64_t>(10000)}}));
2✔
532
    });
2✔
533

534
    const AppSession& app_session = harness.session().app_session();
2✔
535
    auto schema_v1 = get_schema_v1();
2✔
536
    create_schema(app_session, schema_v1, 1);
2✔
537
    auto schema_v2 = get_schema_v2();
2✔
538
    create_schema(app_session, schema_v2, 2);
2✔
539

540
    // First schema upgrade.
541
    {
2✔
542
        // Upgrade the schema version
543
        config.schema_version = 1;
2✔
544
        config.schema = schema_v1;
2✔
545
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
546
        auto realm = successfully_async_open_realm(config);
2✔
547
        check_realm_schema(config.path, schema_v1, 1);
2✔
548

549
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
550
        CHECK(table->size() == 3);
2!
551
        table = realm->read_group().get_table("class_TopLevel2");
2✔
552
        CHECK(!table);
2!
553
        table = realm->read_group().get_table("class_TopLevel3");
2✔
554
        CHECK(table->size() == 1);
2!
555
        CHECK(table->get_object_with_primary_key(obj3_id));
2!
556

557
        realm->begin_transaction();
2✔
558
        CppContext c(realm);
2✔
559
        Object::create(c, realm, "TopLevel",
2✔
560
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
561
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
562
                                        {"non_queryable_field", "non queryable 4"s},
2✔
563
                                        {"non_queryable_field2", "non queryable 44"s}}));
2✔
564
        realm->commit_transaction();
2✔
565

566
        wait_for_upload(*realm);
2✔
567
        wait_for_download(*realm);
2✔
568
    }
2✔
569

570
    // Second schema upgrade.
571
    {
2✔
572
        config.schema_version = 2;
2✔
573
        config.schema = schema_v2;
2✔
574
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v2();
2✔
575

576
        auto realm = successfully_async_open_realm(config);
2✔
577
        check_realm_schema(config.path, schema_v2, 2);
2✔
578

579
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
580
        CHECK(table->size() == 4);
2!
581
        table = realm->read_group().get_table("class_TopLevel2");
2✔
582
        CHECK(!table);
2!
583
        table = realm->read_group().get_table("class_TopLevel3");
2✔
584
        CHECK(table->size() == 1);
2!
585
        CHECK(table->get_object_with_primary_key(obj3_id));
2!
586
    }
2✔
587

588
    // First schema downgrade.
589
    {
2✔
590
        config.schema_version = 1;
2✔
591
        config.schema = schema_v1;
2✔
592
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
593

594
        auto realm = successfully_async_open_realm(config);
2✔
595
        check_realm_schema(config.path, schema_v1, 1);
2✔
596

597
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
598
        CHECK(table->size() == 4);
2!
599
        table = realm->read_group().get_table("class_TopLevel2");
2✔
600
        CHECK(!table);
2!
601
        table = realm->read_group().get_table("class_TopLevel3");
2✔
602
        CHECK(table->size() == 1);
2!
603
        CHECK(table->get_object_with_primary_key(obj3_id));
2!
604
    }
2✔
605

606
    // Second schema downgrade.
607
    {
2✔
608
        config.schema_version = 0;
2✔
609
        config.schema = schema_v0;
2✔
610
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
611

612
        auto realm = successfully_async_open_realm(config);
2✔
613
        check_realm_schema(config.path, schema_v0, 0);
2✔
614

615
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
616
        CHECK(table->size() == 4);
2!
617
        table = realm->read_group().get_table("class_TopLevel2");
2✔
618
        CHECK(table->is_empty());
2!
619
        auto table3 = realm->read_group().get_table("class_TopLevel3");
2✔
620
        CHECK(table3->is_empty());
2!
621

622
        // The subscription for 'TopLevel3' is on a removed field (i.e, the field does not exist in the previous
623
        // schema version used), so data cannot be synced.
624
        // Update subscription so data can be synced.
625
        auto subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
626
        CHECK(subs.erase_by_class_name("TopLevel3"));
2!
627
        subs.insert_or_assign(Query(table3));
2✔
628
        auto new_subs = subs.commit();
2✔
629
        new_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
630
        realm->refresh();
2✔
631
        CHECK(table3->size() == 1);
2!
632
        CHECK(table3->get_object_with_primary_key(obj3_id));
2!
633
    }
2✔
634
}
2✔
635

636
TEST_CASE("An interrupted schema migration can recover on the next session",
637
          "[sync][flx][flx schema migration][baas]") {
2✔
638
    auto schema_v0 = get_schema_v0();
2✔
639
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
640
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
641
    auto config = harness.make_test_file();
2✔
642

643
    {
2✔
644
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
645
        auto realm = Realm::get_shared_realm(config);
2✔
646
        wait_for_download(*realm);
2✔
647
        wait_for_upload(*realm);
2✔
648
        check_realm_schema(config.path, schema_v0, 0);
2✔
649
    }
2✔
650
    REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config.path));
2!
651

652
    const AppSession& app_session = harness.session().app_session();
2✔
653
    auto schema_v1 = get_schema_v1();
2✔
654
    create_schema(app_session, schema_v1, 1);
2✔
655

656
    config.schema_version = 1;
2✔
657
    config.schema = schema_v1;
2✔
658
    auto schema_version_changed_count = 0;
2✔
659
    std::shared_ptr<AsyncOpenTask> task;
2✔
660
    auto pf = util::make_promise_future<void>();
2✔
661
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
662
    config.sync_config->on_sync_client_event_hook = [&schema_version_changed_count, &task,
2✔
663
                                                     &pf](std::weak_ptr<SyncSession>,
2✔
664
                                                          const SyncClientHookData& data) mutable {
38✔
665
        if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
38✔
666
            return SyncClientHookAction::NoAction;
34✔
667
        }
34✔
668

669
        auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
4✔
670
        if (error_code == sync::ProtocolError::initial_sync_not_completed) {
4✔
UNCOV
671
            return SyncClientHookAction::NoAction;
×
NEW
672
        }
×
673

674
        CHECK(error_code == sync::ProtocolError::schema_version_changed);
4!
675
        // Cancel the async open task (the sync session closes too) the first time a schema migration is required.
676
        if (++schema_version_changed_count == 1) {
4✔
677
            task->cancel();
2✔
678
            pf.promise.emplace_value();
2✔
679
        }
2✔
680
        return SyncClientHookAction::NoAction;
4✔
681
    };
4✔
682

683
    {
2✔
684
        task = Realm::get_synchronized_realm(config);
2✔
685
        task->start([](ThreadSafeReference, std::exception_ptr) {
2✔
686
            FAIL();
×
687
        });
×
688
        pf.future.get();
2✔
689
        task.reset();
2✔
690
        check_realm_schema(config.path, schema_v0, 0);
2✔
691
    }
2✔
692

693
    // Retry the migration.
694
    REQUIRE(async_open_realm(config).is_ok());
2!
695
    REQUIRE(schema_version_changed_count == 2);
2!
696
    check_realm_schema(config.path, schema_v1, 1);
2✔
697
}
2✔
698

699
TEST_CASE("Migrate to new schema version with a schema subset", "[sync][flx][flx schema migration][baas]") {
2✔
700
    auto schema_v0 = get_schema_v0();
2✔
701
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
702
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
703
    auto config = harness.make_test_file();
2✔
704

705
    {
2✔
706
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
707
        auto realm = Realm::get_shared_realm(config);
2✔
708
        wait_for_download(*realm);
2✔
709
        wait_for_upload(*realm);
2✔
710
        check_realm_schema(config.path, schema_v0, 0);
2✔
711
    }
2✔
712

713
    const AppSession& app_session = harness.session().app_session();
2✔
714
    auto schema_v1 = get_schema_v1();
2✔
715
    create_schema(app_session, schema_v1, 1);
2✔
716

717
    config.schema_version = 1;
2✔
718
    auto schema_subset = schema_v1;
2✔
719
    // One of the columns in 'TopLevel' is not needed by the user.
720
    schema_subset[0].persisted_properties.pop_back();
2✔
721
    config.schema = schema_subset;
2✔
722
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
723

724
    REQUIRE(async_open_realm(config).is_ok());
2!
725
    check_realm_schema(config.path, schema_v1, 1);
2✔
726
}
2✔
727

728
TEST_CASE("Client reset during schema migration", "[sync][flx][flx schema migration][baas]") {
2✔
729
    auto schema_v0 = get_schema_v0();
2✔
730
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
731
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
732
    auto config = harness.make_test_file();
2✔
733

734
    {
2✔
735
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
736
        auto realm = Realm::get_shared_realm(config);
2✔
737
        wait_for_download(*realm);
2✔
738
        wait_for_upload(*realm);
2✔
739
        check_realm_schema(config.path, schema_v0, 0);
2✔
740

741
        realm->sync_session()->shutdown_and_wait();
2✔
742

743
        realm->begin_transaction();
2✔
744
        CppContext c(realm);
2✔
745
        Object::create(c, realm, "TopLevel",
2✔
746
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
747
                                        {"queryable_str_field", "foo"s},
2✔
748
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
749
                                        {"non_queryable_field2", "non queryable 11"s}}));
2✔
750
        // The server filters out this object because the schema version the client migrates to removes the queryable
751
        // field.
752
        Object::create(
2✔
753
            c, realm, "TopLevel3",
2✔
754
            std::any(AnyDict{{"_id", ObjectId::gen()}, {"queryable_int_field", static_cast<int64_t>(42)}}));
2✔
755
        realm->commit_transaction();
2✔
756
    }
2✔
757
    REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config.path));
2!
758

759
    const AppSession& app_session = harness.session().app_session();
2✔
760
    auto schema_v1 = get_schema_v1();
2✔
761
    create_schema(app_session, schema_v1, 1);
2✔
762

763
    config.schema_version = 1;
2✔
764
    config.schema = schema_v1;
2✔
765
    auto schema_version_changed_count = 0;
2✔
766
    bool once = false;
2✔
767
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
768
    config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
769
    config.sync_config->on_sync_client_event_hook = [&harness, &schema_version_changed_count,
2✔
770
                                                     &once](std::weak_ptr<SyncSession> weak_session,
2✔
771
                                                            const SyncClientHookData& data) mutable {
84✔
772
        if (schema_version_changed_count == 1 && data.event == SyncClientHookEvent::DownloadMessageReceived &&
84✔
773
            !once) {
84✔
774
            once = true;
2✔
775
            return SyncClientHookAction::SuspendWithRetryableError;
2✔
776
        }
2✔
777
        if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
82✔
778
            return SyncClientHookAction::NoAction;
76✔
779
        }
76✔
780
        auto session = weak_session.lock();
6✔
781
        REQUIRE(session);
6!
782

783
        auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
6✔
784
        if (error_code == sync::ProtocolError::initial_sync_not_completed) {
6✔
785
            return SyncClientHookAction::NoAction;
×
786
        }
×
787

788
        if (error_code == sync::ProtocolError::schema_version_changed) {
6✔
789
            if (++schema_version_changed_count == 1) {
4✔
790
                reset_utils::trigger_client_reset(harness.session().app_session(), *session);
2✔
791
            }
2✔
792
        }
4✔
793

794
        return SyncClientHookAction::NoAction;
6✔
795
    };
6✔
796
    size_t before_reset_count = 0;
2✔
797
    size_t after_reset_count = 0;
2✔
798
    config.sync_config->notify_before_client_reset = [&before_reset_count](SharedRealm) {
2✔
799
        ++before_reset_count;
×
800
    };
×
801
    config.sync_config->notify_after_client_reset = [&after_reset_count](SharedRealm, ThreadSafeReference, bool) {
2✔
802
        ++after_reset_count;
×
803
    };
×
804

805
    auto realm = successfully_async_open_realm(config);
2✔
806
    REQUIRE(before_reset_count == 0);
2!
807
    REQUIRE(after_reset_count == 0);
2!
808
    check_realm_schema(config.path, schema_v1, 1);
2✔
809

810
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
811
    CHECK(table->size() == 1);
2!
812
    table = realm->read_group().get_table("class_TopLevel3");
2✔
813
    CHECK(table->is_empty());
2!
814
}
2✔
815

816
TEST_CASE("Migrate to new schema version after migration to intermediate version is interrupted",
817
          "[sync][flx][flx schema migration][baas]") {
2✔
818
    auto schema_v0 = get_schema_v0();
2✔
819
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
820
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
821
    auto config = harness.make_test_file();
2✔
822

823
    {
2✔
824
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
825
        auto realm = Realm::get_shared_realm(config);
2✔
826
        wait_for_download(*realm);
2✔
827
        wait_for_upload(*realm);
2✔
828
        check_realm_schema(config.path, schema_v0, 0);
2✔
829

830
        realm->sync_session()->shutdown_and_wait();
2✔
831

832
        realm->begin_transaction();
2✔
833
        CppContext c(realm);
2✔
834
        Object::create(c, realm, "TopLevel",
2✔
835
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
836
                                        {"queryable_str_field", "foo"s},
2✔
837
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
838
                                        {"non_queryable_field2", "non queryable 11"s}}));
2✔
839
        Object::create(
2✔
840
            c, realm, "TopLevel3",
2✔
841
            std::any(AnyDict{{"_id", ObjectId::gen()}, {"queryable_int_field", static_cast<int64_t>(42)}}));
2✔
842
        realm->commit_transaction();
2✔
843
        realm->close();
2✔
844
    }
2✔
845
    REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config.path));
2!
846

847
    const AppSession& app_session = harness.session().app_session();
2✔
848
    auto schema_v1 = get_schema_v1();
2✔
849
    create_schema(app_session, schema_v1, 1);
2✔
850
    auto schema_v2 = get_schema_v2();
2✔
851
    create_schema(app_session, schema_v2, 2);
2✔
852

853
    config.schema_version = 1;
2✔
854
    config.schema = schema_v1;
2✔
855
    auto schema_version_changed_count = 0;
2✔
856
    std::shared_ptr<AsyncOpenTask> task;
2✔
857
    auto pf = util::make_promise_future<void>();
2✔
858
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
859
    config.sync_config->on_sync_client_event_hook = [&schema_version_changed_count, &task,
2✔
860
                                                     &pf](std::weak_ptr<SyncSession>,
2✔
861
                                                          const SyncClientHookData& data) mutable {
56✔
862
        if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
56✔
863
            return SyncClientHookAction::NoAction;
52✔
864
        }
52✔
865

866
        auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
4✔
867
        if (error_code == sync::ProtocolError::initial_sync_not_completed) {
4✔
UNCOV
868
            return SyncClientHookAction::NoAction;
×
NEW
869
        }
×
870

871
        CHECK(error_code == sync::ProtocolError::schema_version_changed);
4!
872
        // Cancel the async open task (the sync session closes too) the first time a schema migration is required.
873
        if (++schema_version_changed_count == 1) {
4✔
874
            task->cancel();
2✔
875
            pf.promise.emplace_value();
2✔
876
        }
2✔
877
        return SyncClientHookAction::NoAction;
4✔
878
    };
4✔
879

880
    {
2✔
881
        task = Realm::get_synchronized_realm(config);
2✔
882
        task->start([](ThreadSafeReference, std::exception_ptr) {
2✔
883
            FAIL();
×
884
        });
×
885
        pf.future.get();
2✔
886
        task.reset();
2✔
887
        check_realm_schema(config.path, schema_v0, 0);
2✔
888
    }
2✔
889

890
    // Migrate to v2.
891
    config.schema_version = 2;
2✔
892
    config.schema = schema_v2;
2✔
893
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v2();
2✔
894
    auto realm = successfully_async_open_realm(config);
2✔
895
    REQUIRE(schema_version_changed_count == 2);
2!
896
    check_realm_schema(config.path, schema_v2, 2);
2✔
897

898
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
899
    CHECK(table->size() == 1);
2!
900
    table = realm->read_group().get_table("class_TopLevel3");
2✔
901
    CHECK(table->is_empty());
2!
902
}
2✔
903

904
TEST_CASE("Send schema version zero if no schema is used to open the realm",
905
          "[sync][flx][flx schema migration][baas]") {
2✔
906
    auto schema_v0 = get_schema_v0();
2✔
907
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
908
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
909
    auto config = harness.make_test_file();
2✔
910

911
    const AppSession& app_session = harness.session().app_session();
2✔
912
    auto schema_v1 = get_schema_v1();
2✔
913
    create_schema(app_session, schema_v1, 1);
2✔
914

915
    config.schema = {};
2✔
916
    config.schema_version = -1; // override the schema version set by SyncTestFile constructor
2✔
917
    REQUIRE(async_open_realm(config).is_ok());
2!
918
    // The schema is received from the server, but it is unversioned.
919
    check_realm_schema(config.path, schema_v0, ObjectStore::NotVersioned);
2✔
920
}
2✔
921

922
TEST_CASE("Allow resetting the schema version to zero after bad schema version error",
923
          "[sync][flx][flx schema migration][baas]") {
4✔
924
    auto schema_v0 = get_schema_v0();
4✔
925
    FLXSyncTestHarness harness("flx_sync_schema_migration",
4✔
926
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
4✔
927
    auto config = harness.make_test_file();
4✔
928
    config.schema_version = 42;
4✔
929

930
    SECTION("Fresh realm") {
4✔
931
    }
2✔
932

933
    SECTION("Existing realm") {
4✔
934
        DBOptions options;
2✔
935
        options.encryption_key = test_util::crypt_key();
2✔
936
        auto db = DB::create(sync::make_client_replication(), config.path, options);
2✔
937
        auto tr = db->start_write();
2✔
938
        ObjectStore::set_schema_version(*tr, config.schema_version);
2✔
939
        tr->commit();
2✔
940
        auto schema_version = ObjectStore::get_schema_version(*db->start_read());
2✔
941
        CHECK(schema_version == 42);
2!
942
    }
2✔
943

944
    {
4✔
945
        auto&& [error_future, err_handler] = make_error_handler();
4✔
946
        config.sync_config->error_handler = err_handler;
4✔
947
        auto realm = Realm::get_shared_realm(config);
4✔
948
        auto error = error_future.get();
4✔
949
        REQUIRE(error.status == ErrorCodes::SyncSchemaMigrationError);
4!
950
        REQUIRE_THAT(error.status.reason(),
4✔
951
                     Catch::Matchers::ContainsSubstring("Client provided invalid schema version"));
4✔
952
        check_realm_schema(config.path, schema_v0, 42);
4✔
953
    }
4✔
954

955
    config.schema_version = 0;
4✔
956
    config.sync_config->error_handler = nullptr;
4✔
957
    auto realm = Realm::get_shared_realm(config);
4✔
958
    wait_for_download(*realm);
4✔
959
    check_realm_schema(config.path, schema_v0, 0);
4✔
960
}
4✔
961

962
TEST_CASE("Client reset and schema migration", "[sync][flx][flx schema migration][baas]") {
2✔
963
    auto schema_v0 = get_schema_v0();
2✔
964
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
965
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
966
    auto config = harness.make_test_file();
2✔
967

968
    {
2✔
969
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
970
        auto realm = Realm::get_shared_realm(config);
2✔
971
        wait_for_download(*realm);
2✔
972
        wait_for_upload(*realm);
2✔
973
        check_realm_schema(config.path, schema_v0, 0);
2✔
974

975
        realm->sync_session()->shutdown_and_wait();
2✔
976

977
        realm->begin_transaction();
2✔
978
        CppContext c(realm);
2✔
979
        Object::create(c, realm, "TopLevel",
2✔
980
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
981
                                        {"queryable_str_field", "foo"s},
2✔
982
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
983
                                        {"non_queryable_field2", "non queryable 11"s}}));
2✔
984
        Object::create(
2✔
985
            c, realm, "TopLevel3",
2✔
986
            std::any(AnyDict{{"_id", ObjectId::gen()}, {"queryable_int_field", static_cast<int64_t>(42)}}));
2✔
987
        realm->commit_transaction();
2✔
988

989
        // Trigger a client reset.
990
        reset_utils::trigger_client_reset(harness.session().app_session(), *realm->sync_session());
2✔
991
    }
2✔
992
    REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config.path));
2!
993

994
    const AppSession& app_session = harness.session().app_session();
2✔
995
    auto schema_v1 = get_schema_v1();
2✔
996
    create_schema(app_session, schema_v1, 1);
2✔
997

998
    config.schema_version = 1;
2✔
999
    config.schema = schema_v1;
2✔
1000
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
1001
    config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1002
    config.sync_config->on_sync_client_event_hook = [](std::weak_ptr<SyncSession>,
2✔
1003
                                                       const SyncClientHookData& data) mutable {
80✔
1004
        if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
80✔
1005
            return SyncClientHookAction::NoAction;
76✔
1006
        }
76✔
1007

1008
        auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
4✔
1009
        if (error_code == sync::ProtocolError::initial_sync_not_completed) {
4✔
1010
            return SyncClientHookAction::NoAction;
×
1011
        }
×
1012
        CHECK((error_code == sync::ProtocolError::schema_version_changed ||
4!
1013
               error_code == sync::ProtocolError::bad_client_file_ident));
4✔
1014
        return SyncClientHookAction::NoAction;
4✔
1015
    };
4✔
1016
    size_t before_reset_count = 0;
2✔
1017
    size_t after_reset_count = 0;
2✔
1018
    config.sync_config->notify_before_client_reset = [&before_reset_count](SharedRealm) {
2✔
1019
        ++before_reset_count;
×
1020
    };
×
1021
    config.sync_config->notify_after_client_reset = [&after_reset_count](SharedRealm, ThreadSafeReference, bool) {
2✔
1022
        ++after_reset_count;
×
1023
    };
×
1024

1025
    auto realm = successfully_async_open_realm(config);
2✔
1026
    REQUIRE(before_reset_count == 0);
2!
1027
    REQUIRE(after_reset_count == 0);
2!
1028
    check_realm_schema(config.path, schema_v1, 1);
2✔
1029

1030
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
1031
    CHECK(table->size() == 1);
2!
1032
    table = realm->read_group().get_table("class_TopLevel3");
2✔
1033
    CHECK(table->is_empty());
2!
1034
}
2✔
1035

1036
TEST_CASE("Multiple async open tasks trigger a schema migration", "[sync][flx][flx schema migration][baas]") {
2✔
1037
    auto schema_v0 = get_schema_v0();
2✔
1038
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
1039
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
1040
    auto config = harness.make_test_file();
2✔
1041
    config.sync_config->rerun_init_subscription_on_open = true;
2✔
1042

1043
    {
2✔
1044
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
1045
        auto realm = Realm::get_shared_realm(config);
2✔
1046
        wait_for_download(*realm);
2✔
1047
        wait_for_upload(*realm);
2✔
1048
        check_realm_schema(config.path, schema_v0, 0);
2✔
1049

1050
        realm->sync_session()->shutdown_and_wait();
2✔
1051

1052
        // Subscription to recover when upgrading the schema.
1053
        auto subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
1054
        CHECK(subs.erase_by_class_name("TopLevel2"));
2!
1055
        auto table = realm->read_group().get_table("class_TopLevel2");
2✔
1056
        auto col_key = table->get_column_key("queryable_int_field");
2✔
1057
        auto query = Query(table).greater_equal(col_key, int64_t(0));
2✔
1058
        subs.insert_or_assign(query);
2✔
1059
        subs.commit();
2✔
1060

1061
        // Object to recover when upgrading the schema.
1062
        realm->begin_transaction();
2✔
1063
        CppContext c(realm);
2✔
1064
        Object::create(c, realm, "TopLevel",
2✔
1065
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
1066
                                        {"queryable_str_field", "biz"s},
2✔
1067
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
1068
                                        {"non_queryable_field2", "non queryable 33"s}}));
2✔
1069
        realm->commit_transaction();
2✔
1070
        // The server filters out this object because the schema version the client migrates to removes the queryable
1071
        // field.
1072
        realm->begin_transaction();
2✔
1073
        Object::create(
2✔
1074
            c, realm, "TopLevel3",
2✔
1075
            std::any(AnyDict{{"_id", ObjectId::gen()}, {"queryable_int_field", static_cast<int64_t>(42)}}));
2✔
1076
        realm->commit_transaction();
2✔
1077
        realm->close();
2✔
1078
    }
2✔
1079

1080
    const AppSession& app_session = harness.session().app_session();
2✔
1081
    auto schema_v1 = get_schema_v1();
2✔
1082
    create_schema(app_session, schema_v1, 1);
2✔
1083

1084
    // Upgrade the schema version
1085
    config.schema_version = 1;
2✔
1086
    config.schema = schema_v1;
2✔
1087
    config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v1();
2✔
1088

1089
    auto task1 = Realm::get_synchronized_realm(config);
2✔
1090
    auto task2 = Realm::get_synchronized_realm(config);
2✔
1091

1092
    auto open_task1_pf = util::make_promise_future<SharedRealm>();
2✔
1093
    auto open_task2_pf = util::make_promise_future<SharedRealm>();
2✔
1094
    auto open_callback1 = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
2✔
1095
        REQUIRE_FALSE(err);
2!
1096
        auto realm = Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy());
2✔
1097
        REQUIRE(realm);
2!
1098
        open_task1_pf.promise.emplace_value(realm);
2✔
1099
    };
2✔
1100
    auto open_callback2 = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
2✔
1101
        REQUIRE_FALSE(err);
2!
1102
        auto realm = Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy());
2✔
1103
        REQUIRE(realm);
2!
1104
        open_task2_pf.promise.emplace_value(realm);
2✔
1105
    };
2✔
1106

1107
    task1->start(open_callback1);
2✔
1108
    task2->start(open_callback2);
2✔
1109

1110
    auto realm1 = open_task1_pf.future.get();
2✔
1111
    auto realm2 = open_task2_pf.future.get();
2✔
1112

1113
    auto verify_realm = [&](SharedRealm realm) {
4✔
1114
        check_realm_schema(config.path, schema_v1, 1);
4✔
1115

1116
        auto table = realm->read_group().get_table("class_TopLevel");
4✔
1117
        CHECK(table->size() == 1);
4!
1118
        table = realm->read_group().get_table("class_TopLevel2");
4✔
1119
        CHECK(!table);
4!
1120
        table = realm->read_group().get_table("class_TopLevel3");
4✔
1121
        CHECK(table->is_empty());
4!
1122
    };
4✔
1123

1124
    verify_realm(realm1);
2✔
1125
    verify_realm(realm2);
2✔
1126
}
2✔
1127

1128
TEST_CASE("Upgrade schema version with no subscription initializer", "[sync][flx][flx schema migration][baas]") {
2✔
1129
    auto schema_v0 = get_schema_v0();
2✔
1130
    FLXSyncTestHarness harness("flx_sync_schema_migration",
2✔
1131
                               {schema_v0, {"queryable_str_field", "queryable_int_field"}});
2✔
1132
    auto config = harness.make_test_file();
2✔
1133

1134
    {
2✔
1135
        config.sync_config->subscription_initializer = get_subscription_initializer_callback_for_schema_v0();
2✔
1136
        auto realm = Realm::get_shared_realm(config);
2✔
1137
        wait_for_download(*realm);
2✔
1138
        wait_for_upload(*realm);
2✔
1139
        check_realm_schema(config.path, schema_v0, 0);
2✔
1140

1141
        realm->sync_session()->shutdown_and_wait();
2✔
1142

1143
        // Object to recover when upgrading the schema.
1144
        realm->begin_transaction();
2✔
1145
        CppContext c(realm);
2✔
1146
        Object::create(c, realm, "TopLevel",
2✔
1147
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
1148
                                        {"queryable_str_field", "biz"s},
2✔
1149
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
1150
                                        {"non_queryable_field2", "non queryable 33"s}}));
2✔
1151
        realm->commit_transaction();
2✔
1152
        realm->close();
2✔
1153
    }
2✔
1154

1155
    const AppSession& app_session = harness.session().app_session();
2✔
1156
    auto schema_v1 = get_schema_v1();
2✔
1157
    create_schema(app_session, schema_v1, 1);
2✔
1158

1159
    {
2✔
1160
        // Upgrade the schema version
1161
        config.schema_version = 1;
2✔
1162
        config.schema = schema_v1;
2✔
1163
        config.sync_config->subscription_initializer = nullptr;
2✔
1164
        auto realm = successfully_async_open_realm(config);
2✔
1165
        check_realm_schema(config.path, schema_v1, 1);
2✔
1166

1167
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
1168
        CHECK(table->is_empty());
2!
1169
        table = realm->read_group().get_table("class_TopLevel2");
2✔
1170
        CHECK(!table);
2!
1171
        table = realm->read_group().get_table("class_TopLevel3");
2✔
1172
        CHECK(table->is_empty());
2!
1173
    }
2✔
1174
}
2✔
1175

1176
} // namespace realm::app
1177

1178
#endif // REALM_ENABLE_AUTH_TESTS
1179
#endif // REALM_ENABLE_SYNC
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc