diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c96e329f1e..68ea1f78d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,14 @@ ### Enhancements * (PR [#????](https://github.com/realm/realm-core/pull/????)) -* None. +* Improve performance of client reset with automatic recovery and converting top-level tables into embedded tables. ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) * None. ### Breaking changes -* None. +* Rename RealmConfig::automatic_handle_backlicks_in_migrations to RealmConfig::automatically_handle_backlinks_in_migrations. ### Compatibility * Fileformat: Generates files with format v22. Reads and automatically upgrade from fileformat v5. @@ -18,6 +18,7 @@ ### Internals * Remove the unused utility function `copy_dir_recursive()`. +* StringData and Timestamp are now constexpr-constructible. ---------------------------------------------- diff --git a/src/realm/cluster.cpp b/src/realm/cluster.cpp index 679201b9f9e..1805bf68d8a 100644 --- a/src/realm/cluster.cpp +++ b/src/realm/cluster.cpp @@ -972,8 +972,8 @@ void Cluster::nullify_incoming_links(ObjKey key, CascadeState& state) { size_t ndx = get_ndx(key, 0); if (ndx == realm::npos) - throw KeyNotFound(util::format("When nullify incoming links for key '%1' in '%2'", key.value, - get_owning_table()->get_name())); + throw KeyNotFound(util::format("Key '%1' not found in '%2' when nullifying incoming links", key.value, + get_owning_table()->get_class_name())); // We must start with backlink columns in case the corresponding link // columns are in the same table so that we can nullify links before diff --git a/src/realm/group.cpp b/src/realm/group.cpp index 1b7289f541e..18af548681f 100644 --- a/src/realm/group.cpp +++ b/src/realm/group.cpp @@ -53,9 +53,6 @@ Initialization initialization; } // anonymous namespace -constexpr char Group::g_class_name_prefix[]; -constexpr size_t Group::g_class_name_prefix_len; - Group::Group() : m_local_alloc(new SlabAlloc) , m_alloc(*m_local_alloc) // Throws diff --git a/src/realm/group.hpp b/src/realm/group.hpp index 1c3ebf654f8..15c11e4a0d6 100644 --- a/src/realm/group.hpp +++ b/src/realm/group.hpp @@ -283,17 +283,19 @@ class Group : public ArrayParent { bool table_is_public(TableKey key) const; static StringData table_name_to_class_name(StringData table_name) { - REALM_ASSERT(table_name.begins_with(StringData(g_class_name_prefix, g_class_name_prefix_len))); - return table_name.substr(g_class_name_prefix_len); + if (table_name.begins_with(g_class_name_prefix)) { + return table_name.substr(g_class_name_prefix.size()); + } + return table_name; } using TableNameBuffer = std::array; static StringData class_name_to_table_name(StringData class_name, TableNameBuffer& buffer) { - char* p = std::copy_n(g_class_name_prefix, g_class_name_prefix_len, buffer.data()); - size_t len = std::min(class_name.size(), buffer.size() - g_class_name_prefix_len); + char* p = std::copy_n(g_class_name_prefix.data(), g_class_name_prefix.size(), buffer.data()); + size_t len = std::min(class_name.size(), buffer.size() - g_class_name_prefix.size()); std::copy_n(class_name.data(), len, p); - return StringData(buffer.data(), g_class_name_prefix_len + len); + return StringData(buffer.data(), g_class_name_prefix.size() + len); } TableRef get_table(TableKey key); @@ -532,8 +534,7 @@ class Group : public ArrayParent { } private: - static constexpr char g_class_name_prefix[] = "class_"; - static constexpr size_t g_class_name_prefix_len = 6; + static constexpr StringData g_class_name_prefix = "class_"; struct ToDeleteRef { ToDeleteRef(TableKey tk, ObjKey k) @@ -926,7 +927,7 @@ inline StringData Group::get_table_name(TableKey key) const inline bool Group::table_is_public(TableKey key) const { - return get_table_name(key).begins_with(StringData(g_class_name_prefix, g_class_name_prefix_len)); + return get_table_name(key).begins_with(g_class_name_prefix); } inline bool Group::has_table(StringData name) const noexcept diff --git a/src/realm/keys.hpp b/src/realm/keys.hpp index 0cc261af8f9..2c9e1ff47b5 100644 --- a/src/realm/keys.hpp +++ b/src/realm/keys.hpp @@ -95,10 +95,7 @@ struct ColKey { unsigned val; }; - constexpr ColKey() noexcept - : value(null_value) - { - } + constexpr ColKey() noexcept = default; constexpr explicit ColKey(int64_t val) noexcept : value(val) { @@ -164,7 +161,7 @@ struct ColKey { { return (value >> 30) & 0xFFFFFFFFUL; } - int64_t value; + int64_t value = null_value; }; static_assert(ColKey::null_value == 0x7fffffffffffffff, "Fix this"); @@ -254,8 +251,8 @@ class ObjKeys : public std::vector { struct ObjLink { public: - ObjLink() {} - ObjLink(TableKey table_key, ObjKey obj_key) + constexpr ObjLink() = default; + constexpr ObjLink(TableKey table_key, ObjKey obj_key) : m_obj_key(obj_key) , m_table_key(table_key) { diff --git a/src/realm/obj.cpp b/src/realm/obj.cpp index 00d333dbbb1..d59a7025114 100644 --- a/src/realm/obj.cpp +++ b/src/realm/obj.cpp @@ -2070,21 +2070,20 @@ struct EmbeddedObjectLinkMigrator : public LinkTranslator { void Obj::handle_multiple_backlinks_during_schema_migration() { REALM_ASSERT(!m_table->get_primary_key_column()); - auto copy_links = [this](ColKey col) { - auto embedded_obj_tracker = std::make_shared(); + converters::EmbeddedObjectConverter embedded_obj_tracker; + auto copy_links = [&](ColKey col) { auto opposite_table = m_table->get_opposite_table(col); auto opposite_column = m_table->get_opposite_column(col); auto backlinks = get_all_backlinks(col); for (auto backlink : backlinks) { // create a new obj auto obj = m_table->create_object(); - embedded_obj_tracker->track(*this, obj); + embedded_obj_tracker.track(*this, obj); auto linking_obj = opposite_table->get_object(backlink); // change incoming links to point to the newly created object - EmbeddedObjectLinkMigrator migrator{linking_obj, opposite_column, *this, obj}; - migrator.run(); + EmbeddedObjectLinkMigrator{linking_obj, opposite_column, *this, obj}.run(); } - embedded_obj_tracker->process_pending(); + embedded_obj_tracker.process_pending(); return IteratorControl::AdvanceToNext; }; m_table->for_each_backlink_column(copy_links); diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp index 3b432b488a8..b542df8cbd1 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -232,6 +232,10 @@ class Obj { // new object and link it. (To Be Implemented) Obj clear_linked_object(ColKey col_key); Obj& set_any(ColKey col_key, Mixed value, bool is_default = false); + Obj& set_any(StringData col_name, Mixed value, bool is_default = false) + { + return set_any(get_column_key(col_name), value, is_default); + } template Obj& set(StringData col_name, U value, bool is_default = false) @@ -310,6 +314,7 @@ class Obj { template SetPtr get_set_ptr(ColKey col_key) const; LnkSet get_linkset(ColKey col_key) const; + LnkSet get_linkset(StringData col_name) const; LnkSetPtr get_linkset_ptr(ColKey col_key) const; SetBasePtr get_setbase_ptr(ColKey col_key) const; Dictionary get_dictionary(ColKey col_key) const; diff --git a/src/realm/object-store/c_api/config.cpp b/src/realm/object-store/c_api/config.cpp index fc3ed7ef31b..557b88b7cb0 100644 --- a/src/realm/object-store/c_api/config.cpp +++ b/src/realm/object-store/c_api/config.cpp @@ -226,5 +226,5 @@ RLM_API bool realm_config_get_cached(realm_config_t* realm_config) noexcept RLM_API void realm_config_set_automatic_backlink_handling(realm_config_t* realm_config, bool enable_automatic_handling) noexcept { - realm_config->automatic_handle_backlicks_in_migrations = enable_automatic_handling; + realm_config->automatically_handle_backlinks_in_migrations = enable_automatic_handling; } diff --git a/src/realm/object-store/object_schema.cpp b/src/realm/object-store/object_schema.cpp index b5244e4e7af..672691f9f5d 100644 --- a/src/realm/object-store/object_schema.cpp +++ b/src/realm/object-store/object_schema.cpp @@ -448,4 +448,17 @@ bool operator==(ObjectSchema const& a, ObjectSchema const& b) noexcept return std::tie(a.name, a.table_type, a.primary_key, a.persisted_properties, a.computed_properties) == std::tie(b.name, b.table_type, b.primary_key, b.persisted_properties, b.computed_properties); } + +std::ostream& operator<<(std::ostream& o, ObjectSchema::ObjectType table_type) +{ + switch (table_type) { + case ObjectSchema::ObjectType::TopLevel: + return o << "TopLevel"; + case ObjectSchema::ObjectType::Embedded: + return o << "Embedded"; + case ObjectSchema::ObjectType::TopLevelAsymmetric: + return o << "TopLevelAsymmetric"; + } + return o << "Invalid table type: " << uint8_t(table_type); +} } // namespace realm diff --git a/src/realm/object-store/object_schema.hpp b/src/realm/object-store/object_schema.hpp index 4486cfb7b2e..aef4692aada 100644 --- a/src/realm/object-store/object_schema.hpp +++ b/src/realm/object-store/object_schema.hpp @@ -93,18 +93,7 @@ class ObjectSchema { void set_primary_key_property() noexcept; }; -inline std::ostream& operator<<(std::ostream& o, ObjectSchema::ObjectType table_type) -{ - switch (table_type) { - case ObjectSchema::ObjectType::TopLevel: - return o << "TopLevel"; - case ObjectSchema::ObjectType::Embedded: - return o << "Embedded"; - case ObjectSchema::ObjectType::TopLevelAsymmetric: - return o << "TopLevelAsymmetric"; - } - return o << "Invalid table type: " << uint8_t(table_type); -} +std::ostream& operator<<(std::ostream& o, ObjectSchema::ObjectType table_type); } // namespace realm diff --git a/src/realm/object-store/object_store.cpp b/src/realm/object-store/object_store.cpp index acf9d628451..4bdc4b871e1 100644 --- a/src/realm/object-store/object_store.cpp +++ b/src/realm/object-store/object_store.cpp @@ -605,7 +605,7 @@ static void create_initial_tables(Group& group, std::vector const& // downside. void operator()(ChangeTableType op) { - table(op.object).set_table_type(static_cast(*op.new_table_type)); + table(op.object).set_table_type(static_cast(*op.new_table_type), false); } void operator()(AddProperty op) { @@ -826,51 +826,14 @@ static void apply_post_migration_changes(Group& group, std::vector void operator()(ChangeTableType op) { - if (handle_backlinks_automatically) - post_migration_embedded_objects_backlinks_handling(op.object); - table(op.object).set_table_type(static_cast(*op.new_table_type)); + table(op.object).set_table_type(static_cast(*op.new_table_type), + handle_backlinks_automatically); } void operator()(RemoveTable) {} void operator()(ChangePropertyType) {} void operator()(MakePropertyNullable) {} void operator()(MakePropertyRequired) {} void operator()(AddProperty) {} - - void post_migration_embedded_objects_backlinks_handling(const ObjectSchema* object_schema) - { - // check if we are doing a migration from TopLevel Object to Embedded. - // check back link count. - // 1. if object is an orphan (no backlicks, then delete it if instructed to do so) - // 2. if object has multiple backlicks, then just clone N times the object (for each backlick) and assign - // to each a different parent - // object_schema->handle_automatically_backlinks_for_embedded_object = true; - - if (object_schema->table_type == ObjectSchema::ObjectType::Embedded) { - auto original_object_schema = initial_schema.find(object_schema->name); - if (original_object_schema != initial_schema.end() && - (original_object_schema->table_type == ObjectSchema::ObjectType::TopLevel || - original_object_schema->table_type == ObjectSchema::ObjectType::TopLevelAsymmetric)) { - auto table = table_for_object_schema(group, *original_object_schema); - std::vector objects_to_erase; - std::vector objects_to_fix_backlinks; - for (auto& object : *table) { - size_t backlink_count = object.get_backlink_count(); - if (backlink_count == 0) - objects_to_erase.push_back(object); - else if (backlink_count > 1) - objects_to_fix_backlinks.push_back(object); - } - - for (auto& object : objects_to_erase) - object.remove(); - - for (auto& object : objects_to_fix_backlinks) { - object.handle_multiple_backlinks_during_schema_migration(); - object.remove(); - } - } - } - } } applier{group, initial_schema, did_reread_schema, handle_backlinks_automatically}; for (auto& change : changes) { @@ -1065,9 +1028,11 @@ void ObjectStore::rename_property(Group& group, Schema& target_schema, StringDat } } -InvalidSchemaVersionException::InvalidSchemaVersionException(uint64_t old_version, uint64_t new_version) - : logic_error( - util::format("Provided schema version %1 is less than last set version %2.", new_version, old_version)) +InvalidSchemaVersionException::InvalidSchemaVersionException(uint64_t old_version, uint64_t new_version, + bool must_exactly_equal) + : logic_error(util::format(must_exactly_equal ? "Provided schema version %1 does not equal last set version %2." + : "Provided schema version %1 is less than last set version %2.", + new_version, old_version)) , m_old_version(old_version) , m_new_version(new_version) { diff --git a/src/realm/object-store/object_store.hpp b/src/realm/object-store/object_store.hpp index f0e071a26b4..a5078f1b60d 100644 --- a/src/realm/object-store/object_store.hpp +++ b/src/realm/object-store/object_store.hpp @@ -117,7 +117,7 @@ class ObjectStore { class InvalidSchemaVersionException : public std::logic_error { public: - InvalidSchemaVersionException(uint64_t old_version, uint64_t new_version); + InvalidSchemaVersionException(uint64_t old_version, uint64_t new_version, bool must_exactly_equal); uint64_t old_version() const { return m_old_version; diff --git a/src/realm/object-store/shared_realm.cpp b/src/realm/object-store/shared_realm.cpp index abeb40efc38..3fdef563202 100644 --- a/src/realm/object-store/shared_realm.cpp +++ b/src/realm/object-store/shared_realm.cpp @@ -285,12 +285,12 @@ bool Realm::schema_change_needs_write_transaction(Schema& schema, std::vector converters; while (!embedded_pending.empty()) { EmbeddedToCheck pending = embedded_pending.back(); embedded_pending.pop_back(); - TableRef src_table = pending.embedded_in_src.get_table(); + TableRef dst_table = pending.embedded_in_dst.get_table(); TableKey dst_table_key = dst_table->get_key(); - auto it_with_did_insert = - converters.insert({dst_table_key, InterRealmObjectConverter{src_table, dst_table, shared_from_this()}}); - InterRealmObjectConverter& converter = it_with_did_insert.first->second; + auto it = converters.find(dst_table_key); + if (it == converters.end()) { + TableRef src_table = pending.embedded_in_src.get_table(); + it = converters.insert({dst_table_key, InterRealmObjectConverter{src_table, dst_table, this}}).first; + } + InterRealmObjectConverter& converter = it->second; converter.copy(pending.embedded_in_src, pending.embedded_in_dst, nullptr); } } InterRealmValueConverter::InterRealmValueConverter(ConstTableRef src_table, ColKey src_col, ConstTableRef dst_table, - ColKey dst_col, std::shared_ptr ec) + ColKey dst_col, EmbeddedObjectConverter* ec) : m_src_table(src_table) , m_dst_table(dst_table) , m_src_col(src_col) @@ -333,7 +331,7 @@ InterRealmValueConverter::InterRealmValueConverter(ConstTableRef src_table, ColK } } -void InterRealmValueConverter::track_new_embedded(Obj src, Obj dst) +void InterRealmValueConverter::track_new_embedded(const Obj& src, const Obj& dst) { m_embedded_converter->track(src, dst); } @@ -371,17 +369,17 @@ int InterRealmValueConverter::cmp_src_to_dst(Mixed src, Mixed dst, ConversionRes } else { Obj dst_link; - if (m_opposite_of_src->get_primary_key_column()) { - Mixed src_link_pk = m_opposite_of_src->get_primary_key(src_link_key); - dst_link = m_opposite_of_dst->create_object_with_primary_key(src_link_pk, did_update_out); + if (m_opposite_of_dst == m_opposite_of_src) { + // if this is the same Realm, we can use the ObjKey + dst_link = m_opposite_of_dst->get_object(src_link_key); } else { - if (m_opposite_of_dst == m_opposite_of_src) { - // if this is the same Realm, we can use the ObjKey - dst_link = m_opposite_of_dst->get_object(src_link_key); + // in different Realms we create a new object + if (m_opposite_of_src->get_primary_key_column()) { + Mixed src_link_pk = m_opposite_of_src->get_primary_key(src_link_key); + dst_link = m_opposite_of_dst->create_object_with_primary_key(src_link_pk, did_update_out); } else { - // in different Realms we create a new object dst_link = m_opposite_of_dst->create_object(); } } @@ -436,22 +434,9 @@ int InterRealmValueConverter::cmp_src_to_dst(Mixed src, Mixed dst, ConversionRes } InterRealmObjectConverter::InterRealmObjectConverter(ConstTableRef table_src, TableRef table_dst, - std::shared_ptr embedded_tracker) + EmbeddedObjectConverter* embedded_tracker) : m_embedded_tracker(embedded_tracker) { - populate_columns_from_table(table_src, table_dst); -} - -void InterRealmObjectConverter::copy(const Obj& src, Obj& dst, bool* update_out) -{ - for (auto& column : m_columns_cache) { - column.copy_value(src, dst, update_out); - } -} - -void InterRealmObjectConverter::populate_columns_from_table(ConstTableRef table_src, ConstTableRef table_dst) -{ - m_columns_cache.clear(); m_columns_cache.reserve(table_src->get_column_count()); ColKey pk_col = table_src->get_primary_key_column(); for (ColKey col_key_src : table_src->get_column_keys()) { @@ -460,8 +445,14 @@ void InterRealmObjectConverter::populate_columns_from_table(ConstTableRef table_ StringData col_name = table_src->get_column_name(col_key_src); ColKey col_key_dst = table_dst->get_column_key(col_name); REALM_ASSERT(col_key_dst); - m_columns_cache.emplace_back( - InterRealmValueConverter(table_src, col_key_src, table_dst, col_key_dst, m_embedded_tracker)); + m_columns_cache.emplace_back(table_src, col_key_src, table_dst, col_key_dst, m_embedded_tracker); + } +} + +void InterRealmObjectConverter::copy(const Obj& src, Obj& dst, bool* update_out) +{ + for (auto& column : m_columns_cache) { + column.copy_value(src, dst, update_out); } } diff --git a/src/realm/object_converter.hpp b/src/realm/object_converter.hpp index 53c3ea4176f..caec6c4bbc4 100644 --- a/src/realm/object_converter.hpp +++ b/src/realm/object_converter.hpp @@ -22,12 +22,10 @@ #include #include -#include - namespace realm::converters { -struct EmbeddedObjectConverter : std::enable_shared_from_this { - void track(Obj e_src, Obj e_dst); +struct EmbeddedObjectConverter { + void track(const Obj& e_src, const Obj& e_dst); void process_pending(); private: @@ -40,8 +38,8 @@ struct EmbeddedObjectConverter : std::enable_shared_from_this ec); - void track_new_embedded(Obj src, Obj dst); + EmbeddedObjectConverter* ec); + void track_new_embedded(const Obj& src, const Obj& dst); struct ConversionResult { Mixed converted_value; bool requires_new_embedded_object = false; @@ -66,19 +64,17 @@ struct InterRealmValueConverter { ColKey m_dst_col; TableRef m_opposite_of_src; TableRef m_opposite_of_dst; - std::shared_ptr m_embedded_converter; + EmbeddedObjectConverter* m_embedded_converter; bool m_is_embedded_link; const bool m_primitive_types_only; }; struct InterRealmObjectConverter { - InterRealmObjectConverter(ConstTableRef table_src, TableRef table_dst, - std::shared_ptr embedded_tracker); + InterRealmObjectConverter(ConstTableRef table_src, TableRef table_dst, EmbeddedObjectConverter* embedded_tracker); void copy(const Obj& src, Obj& dst, bool* update_out); private: - void populate_columns_from_table(ConstTableRef table_src, ConstTableRef table_dst); - std::shared_ptr m_embedded_tracker; + EmbeddedObjectConverter* m_embedded_tracker; std::vector m_columns_cache; }; diff --git a/src/realm/set.hpp b/src/realm/set.hpp index bfef7686746..e7e382a79bc 100644 --- a/src/realm/set.hpp +++ b/src/realm/set.hpp @@ -572,6 +572,11 @@ inline LnkSet Obj::get_linkset(ColKey col_key) const return LnkSet{*this, col_key}; } +inline LnkSet Obj::get_linkset(StringData col_name) const +{ + return get_linkset(get_column_key(col_name)); +} + inline LnkSetPtr Obj::get_linkset_ptr(ColKey col_key) const { return std::make_unique(*this, col_key); diff --git a/src/realm/string_data.hpp b/src/realm/string_data.hpp index 2ae547d219d..8e5db15593d 100644 --- a/src/realm/string_data.hpp +++ b/src/realm/string_data.hpp @@ -84,10 +84,10 @@ uint_least64_t cityhash_64(const unsigned char* data, size_t len) noexcept; class StringData { public: /// Construct a null reference. - StringData() noexcept; + constexpr StringData() noexcept = default; /// If \a external_data is 'null', \a data_size must be zero. - StringData(const char* external_data, size_t data_size) noexcept; + constexpr StringData(const char* external_data, size_t data_size) noexcept; template StringData(const std::basic_string&); @@ -98,18 +98,18 @@ class StringData { template StringData(const util::Optional>&); - StringData(std::string_view sv); + constexpr StringData(std::string_view sv); - StringData(const null&) noexcept; + constexpr StringData(const null&) noexcept; /// Initialize from a zero terminated C style string. Pass null to construct /// a null reference. - StringData(const char* c_str) noexcept; + constexpr StringData(const char* c_str) noexcept; - char operator[](size_t i) const noexcept; + constexpr char operator[](size_t i) const noexcept; - const char* data() const noexcept; - size_t size() const noexcept; + constexpr const char* data() const noexcept; + constexpr size_t size() const noexcept; /// Is this a null reference? /// @@ -127,7 +127,7 @@ class StringData { /// of the result of calling this function. In other words, a StringData /// object is converted to true if it is not the null reference, otherwise /// it is converted to false. - bool is_null() const noexcept; + constexpr bool is_null() const noexcept; friend bool operator==(const StringData&, const StringData&) noexcept; friend bool operator!=(const StringData&, const StringData&) noexcept; @@ -140,9 +140,9 @@ class StringData { friend bool operator>=(const StringData&, const StringData&) noexcept; //@} - bool begins_with(StringData) const noexcept; - bool ends_with(StringData) const noexcept; - bool contains(StringData) const noexcept; + constexpr bool begins_with(StringData) const noexcept; + constexpr bool ends_with(StringData) const noexcept; + constexpr bool contains(StringData) const noexcept; bool contains(StringData d, const std::array &charmap) const noexcept; // Wildcard matching ('?' for single char, '*' for zero or more chars) @@ -152,10 +152,10 @@ class StringData { //@{ /// Undefined behavior if \a n, \a i, or i+n is greater than /// size(). - StringData prefix(size_t n) const noexcept; - StringData suffix(size_t n) const noexcept; - StringData substr(size_t i, size_t n) const noexcept; - StringData substr(size_t i) const noexcept; + constexpr StringData prefix(size_t n) const noexcept; + constexpr StringData suffix(size_t n) const noexcept; + constexpr StringData substr(size_t i, size_t n) const noexcept; + constexpr StringData substr(size_t i) const noexcept; //@} template @@ -172,8 +172,8 @@ class StringData { size_t hash() const noexcept; private: - const char* m_data; - size_t m_size; + const char* m_data = nullptr; + size_t m_size = 0; static bool matchlike(const StringData& text, const StringData& pattern) noexcept; static bool matchlike_ins(const StringData& text, const StringData& pattern_upper, @@ -186,20 +186,14 @@ class StringData { // Implementation: -inline StringData::StringData() noexcept - : m_data(nullptr) - , m_size(0) -{ -} - -inline StringData::StringData(const char* external_data, size_t data_size) noexcept +constexpr inline StringData::StringData(const char* external_data, size_t data_size) noexcept : m_data(external_data) , m_size(data_size) { REALM_ASSERT_DEBUG(external_data || data_size == 0); } -inline StringData::StringData(std::string_view sv) +constexpr inline StringData::StringData(std::string_view sv) : m_data(sv.data()) , m_size(sv.size()) { @@ -225,13 +219,9 @@ inline StringData::StringData(const util::Optional { } -inline StringData::StringData(const null&) noexcept - : m_data(nullptr) - , m_size(0) -{ -} +constexpr inline StringData::StringData(const null&) noexcept {} -inline StringData::StringData(const char* c_str) noexcept +constexpr inline StringData::StringData(const char* c_str) noexcept : m_data(c_str) , m_size(0) { @@ -239,22 +229,22 @@ inline StringData::StringData(const char* c_str) noexcept m_size = std::char_traits::length(c_str); } -inline char StringData::operator[](size_t i) const noexcept +constexpr inline char StringData::operator[](size_t i) const noexcept { return m_data[i]; } -inline const char* StringData::data() const noexcept +constexpr inline const char* StringData::data() const noexcept { return m_data; } -inline size_t StringData::size() const noexcept +constexpr inline size_t StringData::size() const noexcept { return m_size; } -inline bool StringData::is_null() const noexcept +constexpr inline bool StringData::is_null() const noexcept { return !m_data; } @@ -294,21 +284,21 @@ inline bool operator>=(const StringData& a, const StringData& b) noexcept return !(a < b); } -inline bool StringData::begins_with(StringData d) const noexcept +constexpr inline bool StringData::begins_with(StringData d) const noexcept { if (is_null() && !d.is_null()) return false; return d.m_size <= m_size && safe_equal(m_data, m_data + d.m_size, d.m_data); } -inline bool StringData::ends_with(StringData d) const noexcept +constexpr inline bool StringData::ends_with(StringData d) const noexcept { if (is_null() && !d.is_null()) return false; return d.m_size <= m_size && safe_equal(m_data + m_size - d.m_size, m_data + m_size, d.m_data); } -inline bool StringData::contains(StringData d) const noexcept +constexpr inline bool StringData::contains(StringData d) const noexcept { if (is_null() && !d.is_null()) return false; @@ -362,22 +352,22 @@ inline bool StringData::like(StringData d) const noexcept return matchlike(*this, d); } -inline StringData StringData::prefix(size_t n) const noexcept +constexpr inline StringData StringData::prefix(size_t n) const noexcept { return substr(0, n); } -inline StringData StringData::suffix(size_t n) const noexcept +constexpr inline StringData StringData::suffix(size_t n) const noexcept { return substr(m_size - n); } -inline StringData StringData::substr(size_t i, size_t n) const noexcept +constexpr inline StringData StringData::substr(size_t i, size_t n) const noexcept { return StringData(m_data + i, n); } -inline StringData StringData::substr(size_t i) const noexcept +constexpr inline StringData StringData::substr(size_t i) const noexcept { return substr(i, m_size - i); } diff --git a/src/realm/sync/noinst/client_reset.cpp b/src/realm/sync/noinst/client_reset.cpp index cc07f4729ac..0d04a36f272 100644 --- a/src/realm/sync/noinst/client_reset.cpp +++ b/src/realm/sync/noinst/client_reset.cpp @@ -329,8 +329,7 @@ void transfer_group(const Transaction& group_src, Transaction& group_dst, util:: } } - std::shared_ptr embedded_tracker = - std::make_shared(); + converters::EmbeddedObjectConverter embedded_tracker; // Now src and dst have identical schemas and no extraneous objects from dst. // There may be missing object from src and the values of existing objects may @@ -354,7 +353,7 @@ void transfer_group(const Transaction& group_src, Transaction& group_dst, util:: table_name, table_src->size(), table_src->get_column_count(), pk_col.get_index().val, pk_col.get_type()); - converters::InterRealmObjectConverter converter(table_src, table_dst, embedded_tracker); + converters::InterRealmObjectConverter converter(table_src, table_dst, &embedded_tracker); for (const Obj& src : *table_src) { auto src_pk = src.get_primary_key(); @@ -368,7 +367,7 @@ void transfer_group(const Transaction& group_src, Transaction& group_dst, util:: logger.debug(" updating %1", src_pk); } } - embedded_tracker->process_pending(); + embedded_tracker.process_pending(); } } diff --git a/src/realm/sync/noinst/client_reset_recovery.cpp b/src/realm/sync/noinst/client_reset_recovery.cpp index 17386d53507..d94b5fd64eb 100644 --- a/src/realm/sync/noinst/client_reset_recovery.cpp +++ b/src/realm/sync/noinst/client_reset_recovery.cpp @@ -456,7 +456,7 @@ void RecoverLocalChangesetsHandler::copy_lists_with_unrecoverable_changes() // IDEA: if a unique id were associated with each list element, we could recover lists correctly because // we would know where list elements ended up or if they were deleted by the server. using namespace realm::converters; - std::shared_ptr embedded_object_tracker = std::make_shared(); + EmbeddedObjectConverter embedded_object_tracker; for (auto& it : m_lists) { if (!it.second.requires_manual_copy()) continue; @@ -470,11 +470,11 @@ void RecoverLocalChangesetsHandler::copy_lists_with_unrecoverable_changes() Obj local_obj = local_list.get_obj(); Obj remote_obj = remote_list.get_obj(); InterRealmValueConverter value_converter(local_table, local_col_key, remote_table, remote_col_key, - embedded_object_tracker); + &embedded_object_tracker); m_logger.debug("Recovery overwrites list for '%1' size: %2 -> %3", path_str, remote_list.size(), local_list.size()); value_converter.copy_value(local_obj, remote_obj, nullptr); - embedded_object_tracker->process_pending(); + embedded_object_tracker.process_pending(); }); if (!did_translate) { // object no longer exists in the local state, ignore and continue @@ -483,7 +483,7 @@ void RecoverLocalChangesetsHandler::copy_lists_with_unrecoverable_changes() path_str); } } - embedded_object_tracker->process_pending(); + embedded_object_tracker.process_pending(); m_lists.clear(); } diff --git a/src/realm/table.cpp b/src/realm/table.cpp index 9fa330d4d92..b77daee7372 100644 --- a/src/realm/table.cpp +++ b/src/realm/table.cpp @@ -316,6 +316,19 @@ const char* get_data_type_name(DataType type) noexcept } return "unknown"; } + +std::ostream& operator<<(std::ostream& o, Table::Type table_type) +{ + switch (table_type) { + case Table::Type::TopLevel: + return o << "TopLevel"; + case Table::Type::Embedded: + return o << "Embedded"; + case Table::Type::TopLevelAsymmetric: + return o << "TopLevelAsymmetric"; + } + return o << "Invalid table type: " << uint8_t(table_type); +} } // namespace realm void LinkChain::add(ColKey ck) @@ -1069,7 +1082,7 @@ Query Table::where(const DictionaryLinkValues& dictionary_of_links) const return Query(m_own_ref, dictionary_of_links); } -void Table::set_table_type(Type table_type) +void Table::set_table_type(Type table_type, bool handle_backlinks) { if (table_type == m_table_type) { return; @@ -1080,17 +1093,11 @@ void Table::set_table_type(Type table_type) } REALM_ASSERT_EX(table_type == Type::TopLevel || table_type == Type::Embedded, table_type); - set_embedded(table_type == Type::Embedded); + set_embedded(table_type == Type::Embedded, handle_backlinks); } -void Table::set_embedded(bool embedded) +void Table::set_embedded(bool embedded, bool handle_backlinks) { - if (Replication* repl = get_repl()) { - if (repl->get_history_type() == Replication::HistoryType::hist_SyncClient) { - throw std::logic_error(util::format("Cannot change '%1' to embedded when using Sync.", get_name())); - } - } - if (embedded == false) { do_set_table_type(Type::TopLevel); return; @@ -1098,52 +1105,102 @@ void Table::set_embedded(bool embedded) // Embedded objects cannot have a primary key. if (get_primary_key_column()) { - throw std::logic_error(util::format("Cannot change '%1' to embedded when using a primary key.", get_name())); + throw std::logic_error( + util::format("Cannot change '%1' to embedded when using a primary key.", get_class_name())); } - // `has_backlink_columns` indicates if the table is embedded in any other table. - bool has_backlink_columns = false; - for_each_backlink_column([&has_backlink_columns](ColKey) { - has_backlink_columns = true; - return IteratorControl::Stop; - }); - if (!has_backlink_columns) { - throw std::logic_error( - util::format("Cannot change '%1' to embedded without backlink columns. Objects must be embedded in " - "at least one other class.", - get_name())); - } - else if (size() > 0) { - for (auto object : *this) { - size_t backlink_count = 0; - for_each_backlink_column([&](ColKey backlink_col_key) { - size_t cur_backlinks = object.get_backlink_cnt(backlink_col_key); - if (cur_backlinks > 0) { - // Make sure this link is not an untyped ObjLink which lacks core support in many places - ColKey source_col = get_opposite_column(backlink_col_key); - REALM_ASSERT(source_col); // backlink columns should always have a source - TableRef source_table = get_opposite_table(backlink_col_key); - ColKey forward_col_mapped = source_table->get_opposite_column(source_col); - if (!forward_col_mapped) { - throw std::logic_error(util::format("There is a dynamic/untyped link from a Mixed property " - "'%1.%2' which prevents migrating class '%3' to embedded", - source_table->get_name(), - source_table->get_column_name(source_col), get_name())); + if (size() == 0) { + do_set_table_type(Type::Embedded); + return; + } + + // Check all of the objects for invalid incoming links. Each embedded object + // must have exactly one incoming link, and it must be from a non-Mixed property. + // Objects with no incoming links are either deleted or an error (depending + // on `handle_backlinks`), and objects with multiple incoming links are either + // cloned for each of the incoming links or an error (again depending on `handle_backlinks`). + // Incoming links from a Mixed property are always an error, as those can't + // link to embedded objects + ArrayInteger leaf(get_alloc()); + enum class LinkCount : int8_t { None, One, Multiple }; + std::vector incoming_link_count; + std::vector orphans; + std::vector multiple_incoming_links; + traverse_clusters([&](const Cluster* cluster) { + size_t size = cluster->node_size(); + incoming_link_count.assign(size, LinkCount::None); + + for_each_backlink_column([&](ColKey col) { + cluster->init_leaf(col, &leaf); + // Width zero means all the values are zero and there can't be any backlinks + if (leaf.get_width() == 0) { + return IteratorControl::AdvanceToNext; + } + + for (size_t i = 0, size = leaf.size(); i < size; ++i) { + auto value = leaf.get_as_ref_or_tagged(i); + if (value.is_ref() && value.get_as_ref() == 0) { + // ref of zero means there's no backlinks + continue; + } + + if (value.is_ref()) { + // Any other ref indicates an array of backlinks, which will + // always have more than one entry + incoming_link_count[i] = LinkCount::Multiple; + } + else { + // Otherwise it's a tagged ref to the single linking object + if (incoming_link_count[i] == LinkCount::None) { + incoming_link_count[i] = LinkCount::One; + } + else if (incoming_link_count[i] == LinkCount::One) { + incoming_link_count[i] = LinkCount::Multiple; } } - backlink_count += cur_backlinks; - if (backlink_count > 1) { - throw std::logic_error( - util::format("At least one object in '%1' does have multiple backlinks.", get_name())); + + auto source_col = get_opposite_column(col); + if (source_col.get_type() == col_type_Mixed) { + auto source_table = get_opposite_table(col); + throw std::logic_error(util::format( + "Cannot convert '%1' to embedded: there is an incoming link from the Mixed property '%2.%3', " + "which does not support linking to embedded objects.", + get_class_name(), source_table->get_class_name(), source_table->get_column_name(source_col))); } - return IteratorControl::AdvanceToNext; // continue - }); + } + return IteratorControl::AdvanceToNext; + }); - if (backlink_count == 0) { - throw std::logic_error(util::format( - "At least one object in '%1' does not have a backlink (data would get lost).", get_name())); + for (size_t i = 0; i < size; ++i) { + if (incoming_link_count[i] == LinkCount::None) { + if (!handle_backlinks) { + throw std::logic_error(util::format("Cannot convert '%1' to embedded: at least one object has no " + "incoming links and would be deleted.", + get_class_name())); + } + orphans.push_back(cluster->get_real_key(i)); + } + else if (incoming_link_count[i] == LinkCount::Multiple) { + if (!handle_backlinks) { + throw std::logic_error(util::format( + "Cannot convert '%1' to embedded: at least one object has more than one incoming link.", + get_class_name())); + } + multiple_incoming_links.push_back(cluster->get_real_key(i)); } } + + return IteratorControl::AdvanceToNext; + }); + + // orphans and multiple_incoming_links will always be empty if `handle_backlinks = false` + for (auto key : orphans) { + remove_object(key); + } + for (auto key : multiple_incoming_links) { + auto obj = get_object(key); + obj.handle_multiple_backlinks_during_schema_migration(); + obj.remove(); } do_set_table_type(Type::Embedded); @@ -1895,6 +1952,11 @@ StringData Table::get_name() const noexcept return static_cast(parent)->get_table_name(get_key()); } +StringData Table::get_class_name() const noexcept +{ + return Group::table_name_to_class_name(get_name()); +} + const char* Table::get_state() const noexcept { switch (m_cookie) { @@ -3482,7 +3544,7 @@ bool Table::contains_unique_values(ColKey col) const void Table::validate_column_is_unique(ColKey col) const { if (!contains_unique_values(col)) { - throw DuplicatePrimaryKeyValueException(get_name(), get_column_name(col)); + throw DuplicatePrimaryKeyValueException(get_class_name(), get_column_name(col)); } } diff --git a/src/realm/table.hpp b/src/realm/table.hpp index 91e64e53b15..bb6ee475e72 100644 --- a/src/realm/table.hpp +++ b/src/realm/table.hpp @@ -110,6 +110,9 @@ class Table { /// string. StringData get_name() const noexcept; + // Get table name with class prefix removed + StringData get_class_name() const noexcept; + const char* get_state() const noexcept; /// If this table is a group-level table, the parent group is returned, @@ -174,8 +177,7 @@ class Table { throw LogicError(LogicError::column_does_not_exist); } // Change the type of a table. Only allowed to switch to/from TopLevel from/to Embedded. - // Called only when updating the schema. - void set_table_type(Type table_tpe); + void set_table_type(Type new_type, bool handle_backlinks = false); //@} /// True for `col_type_Link` and `col_type_LinkList`. @@ -512,7 +514,7 @@ class Table { Obj create_linked_object(GlobalKey = {}); // Change the embedded property of a table. If switching to being embedded, the table must // not have a primary key and all objects must have exactly 1 backlink. - void set_embedded(bool embedded); + void set_embedded(bool embedded, bool handle_backlinks); /// Changes type unconditionally. Called only from Group::do_get_or_add_table() void do_set_table_type(Type table_type); @@ -869,18 +871,7 @@ class Table { friend class AggregateHelper; }; -inline std::ostream& operator<<(std::ostream& o, Table::Type table_type) -{ - switch (table_type) { - case Table::Type::TopLevel: - return o << "TopLevel"; - case Table::Type::Embedded: - return o << "Embedded"; - case Table::Type::TopLevelAsymmetric: - return o << "TopLevelAsymmetric"; - } - return o << "Invalid table type: " << uint8_t(table_type); -} +std::ostream& operator<<(std::ostream& o, Table::Type table_type); class ColKeyIterator { public: diff --git a/src/realm/timestamp.hpp b/src/realm/timestamp.hpp index c4c95acfc07..0e4d8bf288c 100644 --- a/src/realm/timestamp.hpp +++ b/src/realm/timestamp.hpp @@ -64,7 +64,7 @@ class Timestamp { // +1.1 seconds (1100 milliseconds after the epoch) is constructed by Timestamp(1, 100000000) // -1.1 seconds (1100 milliseconds before the epoch) is constructed by Timestamp(-1, -100000000) // - Timestamp(int64_t seconds, int32_t nanoseconds) + constexpr Timestamp(int64_t seconds, int32_t nanoseconds) : m_seconds(seconds) , m_nanoseconds(nanoseconds) , m_is_null(false) @@ -74,46 +74,40 @@ class Timestamp { const bool both_non_positive = seconds <= 0 && nanoseconds <= 0; REALM_ASSERT_EX(both_non_negative || both_non_positive, both_non_negative, both_non_positive); } - Timestamp(realm::null) - : m_is_null(true) - { - } - Timestamp(const Timestamp&) = default; + constexpr Timestamp() = default; + constexpr Timestamp(realm::null) {} + + constexpr Timestamp(const Timestamp&) = default; + constexpr Timestamp& operator=(const Timestamp&) = default; template - Timestamp(std::chrono::time_point tp) + constexpr Timestamp(std::chrono::time_point tp) : m_is_null(false) { int64_t native_nano = std::chrono::duration_cast(tp.time_since_epoch()).count(); m_seconds = native_nano / nanoseconds_per_second; m_nanoseconds = static_cast(native_nano % nanoseconds_per_second); } - Timestamp() - : Timestamp(null{}) - { - } - - Timestamp& operator=(const Timestamp& rhs) = default; - bool is_null() const + constexpr bool is_null() const { return m_is_null; } - int64_t get_seconds() const noexcept + constexpr int64_t get_seconds() const noexcept { REALM_ASSERT(!m_is_null); return m_seconds; } - int32_t get_nanoseconds() const noexcept + constexpr int32_t get_nanoseconds() const noexcept { REALM_ASSERT(!m_is_null); return m_nanoseconds; } template - std::chrono::time_point get_time_point() const + constexpr std::chrono::time_point get_time_point() const { REALM_ASSERT(!m_is_null); @@ -124,12 +118,12 @@ class Timestamp { } template - explicit operator std::chrono::time_point() const + constexpr explicit operator std::chrono::time_point() const { return get_time_point(); } - bool operator==(const Timestamp& rhs) const + constexpr bool operator==(const Timestamp& rhs) const { if (is_null() && rhs.is_null()) return true; @@ -139,11 +133,11 @@ class Timestamp { return m_seconds == rhs.m_seconds && m_nanoseconds == rhs.m_nanoseconds; } - bool operator!=(const Timestamp& rhs) const + constexpr bool operator!=(const Timestamp& rhs) const { return !(*this == rhs); } - bool operator>(const Timestamp& rhs) const + constexpr bool operator>(const Timestamp& rhs) const { if (is_null()) { return false; @@ -153,7 +147,7 @@ class Timestamp { } return (m_seconds > rhs.m_seconds) || (m_seconds == rhs.m_seconds && m_nanoseconds > rhs.m_nanoseconds); } - bool operator<(const Timestamp& rhs) const + constexpr bool operator<(const Timestamp& rhs) const { if (rhs.is_null()) { return false; @@ -163,7 +157,7 @@ class Timestamp { } return (m_seconds < rhs.m_seconds) || (m_seconds == rhs.m_seconds && m_nanoseconds < rhs.m_nanoseconds); } - bool operator<=(const Timestamp& rhs) const + constexpr bool operator<=(const Timestamp& rhs) const { if (is_null()) { return true; @@ -173,7 +167,7 @@ class Timestamp { } return *this < rhs || *this == rhs; } - bool operator>=(const Timestamp& rhs) const + constexpr bool operator>=(const Timestamp& rhs) const { if (rhs.is_null()) { return true; @@ -184,16 +178,19 @@ class Timestamp { return *this > rhs || *this == rhs; } - size_t hash() const noexcept; + constexpr size_t hash() const noexcept + { + return size_t(m_seconds) ^ size_t(m_nanoseconds); + } template friend std::basic_ostream& operator<<(std::basic_ostream& out, const Timestamp&); static constexpr int32_t nanoseconds_per_second = 1000000000; private: - int64_t m_seconds; - int32_t m_nanoseconds; - bool m_is_null; + int64_t m_seconds = 0; + int32_t m_nanoseconds = 0; + bool m_is_null = true; }; // LCOV_EXCL_START @@ -224,26 +221,21 @@ inline std::basic_ostream& operator<<(std::basic_ostream& out, const } // LCOV_EXCL_STOP -inline size_t Timestamp::hash() const noexcept -{ - return size_t(m_seconds) ^ size_t(m_nanoseconds); -} - } // namespace realm namespace std { template <> struct numeric_limits { static constexpr bool is_integer = false; - static realm::Timestamp min() + static constexpr realm::Timestamp min() { return realm::Timestamp(numeric_limits::min(), 0); } - static realm::Timestamp lowest() + static constexpr realm::Timestamp lowest() { return realm::Timestamp(numeric_limits::lowest(), 0); } - static realm::Timestamp max() + static constexpr realm::Timestamp max() { return realm::Timestamp(numeric_limits::max(), 0); } diff --git a/test/object-store/migrations.cpp b/test/object-store/migrations.cpp index 044f69dc4ab..1f60bdb5506 100644 --- a/test/object-store/migrations.cpp +++ b/test/object-store/migrations.cpp @@ -51,6 +51,15 @@ using util::any_cast; REQUIRE_NOTHROW((r).update_schema(s, version)); \ VERIFY_SCHEMA(r, false); \ REQUIRE((r).schema() == s); \ + REQUIRE((r).schema_version() == version); \ + } while (0) + +#define REQUIRE_MIGRATION_SUCCEEDS(r, s, version, fn) \ + do { \ + REQUIRE_NOTHROW((r).update_schema(s, version, fn)); \ + VERIFY_SCHEMA(r, false); \ + REQUIRE((r).schema() == s); \ + REQUIRE((r).schema_version() == version); \ } while (0) #define REQUIRE_NO_MIGRATION_NEEDED(r, schema1, schema2) \ @@ -59,10 +68,10 @@ using util::any_cast; REQUIRE_UPDATE_SUCCEEDS(r, schema2, 0); \ } while (0) -#define REQUIRE_MIGRATION_NEEDED(r, schema1, schema2) \ +#define REQUIRE_MIGRATION_NEEDED(r, schema1, schema2, msg) \ do { \ REQUIRE_UPDATE_SUCCEEDS(r, schema1, 0); \ - REQUIRE_THROWS((r).update_schema(schema2)); \ + REQUIRE_THROWS_CONTAINING((r).update_schema(schema2), msg); \ REQUIRE((r).schema() == schema1); \ REQUIRE_UPDATE_SUCCEEDS(r, schema2, 1); \ } while (0) @@ -81,9 +90,14 @@ void verify_schema(Realm& r, int line, bool in_migration) auto col = table->get_primary_key_column(); primary_key = col ? table->get_column_name(col) : ""; REQUIRE(primary_key == object_schema.primary_key); + REQUIRE(table->get_table_type() == Table::Type(object_schema.table_type)); } else { primary_key = object_schema.primary_key; + // Tables are not changed to embedded until after the migration block completes + if (object_schema.table_type != ObjectType::Embedded) { + REQUIRE(table->get_table_type() == Table::Type(object_schema.table_type)); + } } for (auto&& prop : object_schema.persisted_properties) { auto col = table->get_column_key(prop.name); @@ -278,13 +292,10 @@ TEST_CASE("migration: Automatic") { auto realm = Realm::get_shared_realm(config); Schema schema1 = { - {"object", - { - {"col1", PropertyType::Int}, - }}, + {"object", {{"col1", PropertyType::Int}}}, }; auto schema2 = add_property(schema1, "object", {"col2", PropertyType::Int}); - REQUIRE_MIGRATION_NEEDED(*realm, schema1, schema2); + REQUIRE_MIGRATION_NEEDED(*realm, schema1, schema2, "Property 'object.col2' has been added."); } SECTION("remove property from existing object schema") { @@ -296,7 +307,8 @@ TEST_CASE("migration: Automatic") { {"col2", PropertyType::Int}, }}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, remove_property(schema, "object", "col2")); + REQUIRE_MIGRATION_NEEDED(*realm, schema, remove_property(schema, "object", "col2"), + "Property 'object.col2' has been removed."); } SECTION("migratation which replaces a persisted property with a computed one") { @@ -318,91 +330,73 @@ TEST_CASE("migration: Automatic") { schema2.find("object")->computed_properties.emplace_back(new_property); REQUIRE_UPDATE_SUCCEEDS(*realm, schema1, 0); - REQUIRE_THROWS((*realm).update_schema(schema2)); + REQUIRE_THROWS_CONTAINING((*realm).update_schema(schema2), "Property 'object.link' has been removed."); REQUIRE((*realm).schema() == schema1); - REQUIRE_NOTHROW((*realm).update_schema( - schema2, 1, [](SharedRealm, SharedRealm, Schema&) { /* empty but present migration handler */ })); - VERIFY_SCHEMA(*realm, false); - REQUIRE((*realm).schema() == schema2); + REQUIRE_MIGRATION_SUCCEEDS(*realm, schema2, 1, [](auto, auto, auto&) {}); } SECTION("change property type") { auto realm = Realm::get_shared_realm(config); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_type(schema, "object", "value", PropertyType::Float)); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_type(schema, "object", "value", PropertyType::Float), + "Property 'object.value' has been changed from 'int' to 'float'."); } SECTION("make property nullable") { auto realm = Realm::get_shared_realm(config); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_optional(schema, "object", "value", true)); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_optional(schema, "object", "value", true), + "Property 'object.value' has been made optional."); } SECTION("make property required") { auto realm = Realm::get_shared_realm(config); Schema schema = { - {"object", - { - {"value", PropertyType::Int | PropertyType::Nullable}, - }}, + {"object", {{"value", PropertyType::Int | PropertyType::Nullable}}}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_optional(schema, "object", "value", false)); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_optional(schema, "object", "value", false), + "Property 'object.value' has been made required."); } SECTION("change link target") { auto realm = Realm::get_shared_realm(config); Schema schema = { - {"target 1", - { - {"value", PropertyType::Int}, - }}, - {"target 2", - { - {"value", PropertyType::Int}, - }}, + {"target 1", {{"value", PropertyType::Int}}}, + {"target 2", {{"value", PropertyType::Int}}}, {"origin", { {"value", PropertyType::Object | PropertyType::Nullable, "target 1"}, }}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_target(schema, "origin", "value", "target 2")); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_target(schema, "origin", "value", "target 2"), + "Property 'origin.value' has been changed from '' to ''"); } SECTION("add pk") { auto realm = Realm::get_shared_realm(config); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_primary_key(schema, "object", "value")); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_primary_key(schema, "object", "value"), + "Primary Key for class 'object' has been added."); } SECTION("remove pk") { auto realm = Realm::get_shared_realm(config); Schema schema = { - {"object", - { - {"value", PropertyType::Int, Property::IsPrimary{true}}, - }}, + {"object", {{"value", PropertyType::Int, Property::IsPrimary{true}}}}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_primary_key(schema, "object", "")); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_primary_key(schema, "object", ""), + "Primary Key for class 'object' has been removed."); } SECTION("adding column and table in same migration doesn't add duplicate columns") { @@ -427,10 +421,7 @@ TEST_CASE("migration: Automatic") { auto realm = Realm::get_shared_realm(config); Schema schema1 = { - {"object", - { - {"col1", PropertyType::Int}, - }}, + {"object", {{"col1", PropertyType::Int}}}, }; auto schema2 = add_table( add_property(schema1, "object", {"link", PropertyType::Object | PropertyType::Nullable, "object2"}), @@ -444,13 +435,10 @@ TEST_CASE("migration: Automatic") { Schema schema = { {"top", {{"link", PropertyType::Object | PropertyType::Nullable, "object"}}}, - {"object", - ObjectType::Embedded, - { - {"value", PropertyType::Int}, - }}, + {"object", ObjectType::Embedded, {{"value", PropertyType::Int}}}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_table_type(schema, "object", ObjectType::TopLevel)); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_table_type(schema, "object", ObjectType::TopLevel), + "Class 'object' has been changed from Embedded to TopLevel."); } SECTION("change table from top-level to embedded without version bump") { @@ -458,22 +446,17 @@ TEST_CASE("migration: Automatic") { Schema schema = { {"top", {{"link", PropertyType::Object | PropertyType::Nullable, "object"}}}, - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; - REQUIRE_MIGRATION_NEEDED(*realm, schema, set_table_type(schema, "object", ObjectType::Embedded)); + REQUIRE_MIGRATION_NEEDED(*realm, schema, set_table_type(schema, "object", ObjectType::Embedded), + "Class 'object' has been changed from TopLevel to Embedded."); } } SECTION("migration block invocations") { SECTION("not called for initial creation of schema") { Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; auto realm = Realm::get_shared_realm(config); realm->update_schema(schema, 5, [](SharedRealm, SharedRealm, Schema&) { @@ -483,15 +466,9 @@ TEST_CASE("migration: Automatic") { SECTION("not called when schema version is unchanged even if there are schema changes") { Schema schema1 = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; - Schema schema2 = add_table(schema1, {"second object", - { - {"value", PropertyType::Int}, - }}); + Schema schema2 = add_table(schema1, {"second object", {{"value", PropertyType::Int}}}); auto realm = Realm::get_shared_realm(config); realm->update_schema(schema1, 1); realm->update_schema(schema2, 1, [](SharedRealm, SharedRealm, Schema&) { @@ -501,13 +478,10 @@ TEST_CASE("migration: Automatic") { SECTION("called when schema version is bumped even if there are no schema changes") { Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); bool called = false; realm->update_schema(schema, 5, [&](SharedRealm, SharedRealm, Schema&) { called = true; @@ -521,34 +495,33 @@ TEST_CASE("migration: Automatic") { auto realm = Realm::get_shared_realm(config); realm->update_schema({}, 1); realm->update_schema({}, 2); - REQUIRE_THROWS(realm->update_schema({}, 0)); + REQUIRE_THROWS_CONTAINING(realm->update_schema({}, 0), + "Provided schema version 0 is less than last set version 2."); } SECTION("insert duplicate keys for existing PK during migration") { Schema schema = { - {"object", - { - {"value", PropertyType::Int, Property::IsPrimary{true}}, - }}, + {"object", {{"value", PropertyType::Int, Property::IsPrimary{true}}}}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - REQUIRE_THROWS(realm->update_schema(schema, 2, [](SharedRealm, SharedRealm realm, Schema&) { - auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); - table->create_object_with_primary_key(1); - table->create_object_with_primary_key(2).set("value", 1); - })); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + REQUIRE_THROWS_CONTAINING( + realm->update_schema(schema, 2, + [](SharedRealm, SharedRealm realm, Schema&) { + auto table = + ObjectStore::table_for_object_type(realm->read_group(), "object"); + table->create_object_with_primary_key(1); + table->create_object_with_primary_key(2).set("value", 1); + }), + "Primary key property 'object.value' has duplicate values after migration."); } SECTION("add pk to existing table with duplicate keys") { Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); realm->begin_transaction(); auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); @@ -556,25 +529,26 @@ TEST_CASE("migration: Automatic") { realm->commit_transaction(); schema = set_primary_key(schema, "object", "value"); - REQUIRE_THROWS(realm->update_schema(schema, 2, nullptr)); + REQUIRE_THROWS_CONTAINING(realm->update_schema(schema, 2, nullptr), + "Primary key property 'object.value' has duplicate values after migration."); } SECTION("throwing an exception from migration function rolls back all changes") { Schema schema1 = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; Schema schema2 = add_property(schema1, "object", {"value2", PropertyType::Int}); auto realm = Realm::get_shared_realm(config); realm->update_schema(schema1, 1); - REQUIRE_THROWS(realm->update_schema(schema2, 2, [](SharedRealm, SharedRealm realm, Schema&) { - auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); - table->create_object(); - throw 5; - })); + REQUIRE_THROWS_AS(realm->update_schema(schema2, 2, + [](SharedRealm, SharedRealm realm, Schema&) { + auto table = ObjectStore::table_for_object_type( + realm->read_group(), "object"); + table->create_object(); + throw 5; + }), + int); auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); REQUIRE(table->size() == 0); @@ -582,27 +556,21 @@ TEST_CASE("migration: Automatic") { REQUIRE(realm->schema() == schema1); } - SECTION("change table to embedded - table has primary key") { + SECTION("changing a table to embedded does not require a migration block") { Schema schema = { - {"child_table", - { - {"value", PropertyType::Int, Property::IsPrimary{true}}, - }}, - {"parent_table", + {"object", {{"value", PropertyType::Int}}}, + {"parent", { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, + {"link", PropertyType::Object | PropertyType::Nullable, "object"}, }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - REQUIRE_FALSE(child_table->is_embedded()); - - REQUIRE_THROWS( - realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, nullptr)); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, set_table_type(schema, "object", ObjectType::Embedded), 2); } - SECTION("change table to embedded - no migration block") { + SECTION("changing a table to embedded fails if there are any objects in the table and there are no incoming " + "links to the object type") { Schema schema = { {"object", { @@ -610,191 +578,144 @@ TEST_CASE("migration: Automatic") { }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "object"); - REQUIRE_FALSE(child_table->is_embedded()); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); + realm->begin_transaction(); + table->create_object(); + realm->commit_transaction(); + + auto new_schema = set_table_type(schema, "object", ObjectType::Embedded); + REQUIRE_THROWS_CONTAINING(realm->update_schema(new_schema, 2), + "Cannot convert 'object' to embedded: at least one object has no incoming " + "links and would be deleted."); - REQUIRE_THROWS(realm->update_schema(set_table_type(schema, "object", ObjectType::Embedded), 2, nullptr)); + REQUIRE_MIGRATION_SUCCEEDS(*realm, new_schema, 2, [](auto, auto realm, auto&) { + ObjectStore::table_for_object_type(realm->read_group(), "object")->clear(); + }); } - SECTION("change table to embedded - table has no backlinks") { + SECTION("changing table to embedded with zero incoming links fails") { Schema schema = { - {"object", + {"child", { {"value", PropertyType::Int}, }}, + {"parent", + { + {"link", PropertyType::Object | PropertyType::Nullable, "child"}, + }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "object"); - REQUIRE_FALSE(child_table->is_embedded()); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + + realm->begin_transaction(); + ObjectStore::table_for_object_type(realm->read_group(), "child")->create_object(); + realm->commit_transaction(); - REQUIRE_THROWS(realm->update_schema(set_table_type(schema, "object", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); + REQUIRE_THROWS_WITH(realm->update_schema(set_table_type(schema, "child", ObjectType::Embedded), 2), + "Cannot convert 'child' to embedded: at least one object has no incoming links and " + "would be deleted."); } - SECTION("change table to embedded - multiple incoming link per object") { + SECTION("changing table to embedded with multiple incoming links fails") { Schema schema = { - {"child_table", + {"child", { {"value", PropertyType::Int}, }}, - {"parent_table", + {"parent", { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, + {"link", PropertyType::Object | PropertyType::Nullable, "child"}, }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key = child_object.get_key(); - parent_table->create_object().set_all(child_object_key); - parent_table->create_object().set_all(child_object_key); + auto child = ObjectStore::table_for_object_type(realm->read_group(), "child"); + auto parent = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + auto child_obj = child->create_object().get_key(); + parent->create_object().set_all(child_obj); + parent->create_object().set_all(child_obj); realm->commit_transaction(); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_THROWS( - realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, nullptr)); + REQUIRE_THROWS_WITH( + realm->update_schema(set_table_type(schema, "child", ObjectType::Embedded), 2), + "Cannot convert 'child' to embedded: at least one object has more than one incoming link."); } - SECTION("change table to embedded - adding more links in migration block") { + SECTION("changing table to embedded fails if more links are added inside the migratioon block") { Schema schema = { - {"child_table", + {"child", { {"value", PropertyType::Int}, }}, - {"parent_table", + {"parent", { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, + {"link", PropertyType::Object | PropertyType::Nullable, "child"}, }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key = child_object.get_key(); - parent_table->create_object().set_all(child_object_key); + ObjectStore::table_for_object_type(realm->read_group(), "child")->create_object(); realm->commit_transaction(); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - - REQUIRE_THROWS(realm->update_schema( - set_table_type(schema, "child_table", ObjectType::Embedded), 2, [](auto, auto new_realm, auto&) { - Object child_object(new_realm, "child_table", 0); - auto parent_table = ObjectStore::table_for_object_type(new_realm->read_group(), "parent_table"); - Obj parent_obj = parent_table->create_object(); - Object parent_object(new_realm, parent_obj); - CppContext context(new_realm); - parent_object.set_property_value(context, "child_property", std::any(child_object)); - })); - } - - SECTION("Migrations to embedded object with untyped Mixed links") { - auto setup_mixed_link = [&](PropertyType type) -> SharedRealm { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - { - "child_table", - {{"value", PropertyType::Int}}, - }, - {"parent_table", - { - {"child_property", type}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto parent_object = parent_table->create_object(); - auto child_object_key = child_object.get_key(); - ColKey child_col_key = parent_table->get_column_key("child_property"); - - REALM_ASSERT(child_col_key.get_type() == col_type_Mixed); - Mixed child_link = ObjLink{child_table->get_key(), child_object_key}; - if (child_col_key.is_set()) { - auto set = parent_object.get_set(child_col_key); - set.insert(child_link); - } - else if (child_col_key.is_list()) { - auto list = parent_object.get_list(child_col_key); - list.insert(0, child_link); - list.insert(1, child_link); - } - else if (child_col_key.is_dictionary()) { - auto dict = parent_object.get_dictionary(child_col_key); - dict.insert("foo", child_link); - dict.insert("bar", child_link); - } - else { - REALM_ASSERT(!child_col_key.is_collection()); - parent_object.set_any(child_col_key, child_link); - } - realm->commit_transaction(); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - return realm; - }; - auto post_check_failed_migration = [](SharedRealm realm) { - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - REQUIRE(realm->schema_version() == 1); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 1); - REQUIRE(!child_table->is_embedded()); + + REQUIRE_THROWS_WITH( + realm->update_schema(set_table_type(schema, "child", ObjectType::Embedded), 2, + [](auto, auto new_realm, auto) { + auto child = + ObjectStore::table_for_object_type(new_realm->read_group(), "child"); + auto parent = + ObjectStore::table_for_object_type(new_realm->read_group(), "parent"); + parent->create_object().set_all(child->get_object(0).get_key()); + parent->create_object().set_all(child->get_object(0).get_key()); + }), + "Cannot convert 'child' to embedded: at least one object has more than one incoming link."); + } + + SECTION("changing table to embedded fails if there are incoming Mixed linkes") { + auto type = GENERATE(PropertyType::Array, PropertyType::Set, PropertyType::Dictionary, PropertyType::Int); + type |= PropertyType::Mixed | PropertyType::Nullable; + + InMemoryTestFile config; + config.automatically_handle_backlinks_in_migrations = true; + Schema schema = { + {"child", {{"value", PropertyType::Int}}}, + {"parent", {{"link", type}}}, }; - const std::string expected_message = - "There is a dynamic/untyped link from a Mixed property 'class_parent_table.child_property' which " - "prevents migrating class 'class_child_table' to embedded"; - - SECTION("List") { - auto realm = setup_mixed_link(PropertyType::Mixed | PropertyType::Nullable | PropertyType::Array); - REQUIRE_THROWS_CONTAINING( - realm->update_schema(set_table_type(realm->schema(), "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {}), - expected_message); - post_check_failed_migration(realm); + auto realm = Realm::get_shared_realm(config); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + + realm->begin_transaction(); + auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child"); + auto child = child_table->create_object().set_all(42).get_key(); + auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + auto parent_object = parent_table->create_object(); + ColKey link_col = parent_table->get_column_key("link"); + + REALM_ASSERT(link_col.get_type() == col_type_Mixed); + Mixed child_link = ObjLink{child_table->get_key(), child}; + if (link_col.is_set()) { + parent_object.get_set(link_col).insert(child_link); } - SECTION("Set") { - auto realm = setup_mixed_link(PropertyType::Mixed | PropertyType::Nullable | PropertyType::Set); - REQUIRE_THROWS_CONTAINING( - realm->update_schema(set_table_type(realm->schema(), "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {}), - expected_message); - post_check_failed_migration(realm); + else if (link_col.is_list()) { + parent_object.get_list(link_col).add(child_link); } - SECTION("Dictionary") { - auto realm = - setup_mixed_link(PropertyType::Mixed | PropertyType::Nullable | PropertyType::Dictionary); - REQUIRE_THROWS_CONTAINING( - realm->update_schema(set_table_type(realm->schema(), "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {}), - expected_message); - post_check_failed_migration(realm); + else if (link_col.is_dictionary()) { + parent_object.get_dictionary(link_col).insert("foo", child_link); } - SECTION("Mixed property") { - auto realm = setup_mixed_link(PropertyType::Mixed | PropertyType::Nullable); - REQUIRE_THROWS_CONTAINING( - realm->update_schema(set_table_type(realm->schema(), "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {}), - expected_message); - post_check_failed_migration(realm); + else { + REALM_ASSERT(!link_col.is_collection()); + parent_object.set_any(link_col, child_link); } + realm->commit_transaction(); + + REQUIRE_THROWS_CONTAINING( + realm->update_schema(set_table_type(realm->schema(), "child", ObjectType::Embedded), 2), + "Cannot convert 'child' to embedded: there is an incoming link from the Mixed property " + "'parent.link', which does not support linking to embedded objects."); } } @@ -807,15 +728,14 @@ TEST_CASE("migration: Automatic") { }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); realm->begin_transaction(); auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); create_objects(*table, 10); realm->commit_transaction(); - schema = set_type(schema, "object", "value", PropertyType::Float); - realm->update_schema(schema, 2); + REQUIRE_UPDATE_SUCCEEDS(*realm, set_type(schema, "object", "value", PropertyType::Float), 2); REQUIRE(table->size() == 10); } @@ -827,7 +747,7 @@ TEST_CASE("migration: Automatic") { }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); realm->begin_transaction(); auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); @@ -837,7 +757,7 @@ TEST_CASE("migration: Automatic") { table->get_object(i).set(key, i); realm->commit_transaction(); - realm->update_schema(set_optional(schema, "object", "value", true), 2); + REQUIRE_UPDATE_SUCCEEDS(*realm, set_optional(schema, "object", "value", true), 2); key = table->get_column_key("value"); for (int i = 0; i < 10; ++i) REQUIRE(table->get_object(i).get>(key) == i); @@ -851,7 +771,7 @@ TEST_CASE("migration: Automatic") { }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); realm->begin_transaction(); auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); @@ -861,7 +781,7 @@ TEST_CASE("migration: Automatic") { table->get_object(i).set(key, i); realm->commit_transaction(); - realm->update_schema(set_optional(schema, "object", "value", false), 2); + REQUIRE_UPDATE_SUCCEEDS(*realm, set_optional(schema, "object", "value", false), 2); key = table->get_column_key("value"); for (size_t i = 0; i < 10; ++i) REQUIRE(table->get_object(i).get(key) == 0); @@ -875,9 +795,9 @@ TEST_CASE("migration: Automatic") { }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); - realm->update_schema({}, 2, [](SharedRealm, SharedRealm realm, Schema&) { + REQUIRE_MIGRATION_SUCCEEDS(*realm, Schema{}, 2, [](SharedRealm, SharedRealm realm, Schema&) { ObjectStore::delete_data_for_object(realm->read_group(), "object"); }); REQUIRE_FALSE(ObjectStore::table_for_object_type(realm->read_group(), "object")); @@ -891,13 +811,13 @@ TEST_CASE("migration: Automatic") { }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); realm->begin_transaction(); ObjectStore::table_for_object_type(realm->read_group(), "object")->create_object(); realm->commit_transaction(); - realm->update_schema(schema, 2, [](SharedRealm, SharedRealm realm, Schema&) { + REQUIRE_MIGRATION_SUCCEEDS(*realm, schema, 2, [](SharedRealm, SharedRealm realm, Schema&) { ObjectStore::delete_data_for_object(realm->read_group(), "object"); }); auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); @@ -913,817 +833,218 @@ TEST_CASE("migration: Automatic") { }}, }; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); - REQUIRE_NOTHROW(realm->update_schema({}, 2, [](SharedRealm, SharedRealm realm, Schema&) { + REQUIRE_MIGRATION_SUCCEEDS(*realm, Schema{}, 2, [](SharedRealm, SharedRealm realm, Schema&) { ObjectStore::delete_data_for_object(realm->read_group(), "foo"); - })); + }); } - SECTION("change empty table from top-level to embedded") { - Schema schema = { - {"child_table", - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - REQUIRE_FALSE(child_table->is_embedded()); - - REQUIRE_NOTHROW( - realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, nullptr)); - - REQUIRE(realm->schema_version() == 2); - REQUIRE(child_table->is_embedded()); - } + const Schema basic_link_schema = { + {"child", + { + {"value", PropertyType::Int}, + }}, + {"parent", + { + {"link", PropertyType::Object | PropertyType::Nullable, "child"}, + }}, + }; + const auto basic_embedded_schema = set_table_type(basic_link_schema, "child", ObjectType::Embedded); - SECTION("change empty table from embedded to top-level") { - Schema schema = { - {"child_table", - ObjectType::Embedded, - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; + SECTION("changing empty table from top-level to embedded requires a migration") { auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - REQUIRE(child_table->is_embedded()); - - REQUIRE_NOTHROW( - realm->update_schema(set_table_type(schema, "child_table", ObjectType::TopLevel), 2, nullptr)); - - REQUIRE(realm->schema_version() == 2); - REQUIRE_FALSE(child_table->is_embedded()); + REQUIRE_MIGRATION_NEEDED(*realm, basic_link_schema, basic_embedded_schema, + "Class 'child' has been changed from TopLevel to Embedded."); } - SECTION("re-apply embedded flag to table") { - Schema schema = { - {"child_table", - ObjectType::Embedded, - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; + SECTION("changing empty table from embedded to top-level requires a migration") { auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - REQUIRE(child_table->is_embedded()); - - REQUIRE_NOTHROW( - realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, nullptr)); - - REQUIRE(realm->schema_version() == 2); - REQUIRE(child_table->is_embedded()); + REQUIRE_MIGRATION_NEEDED(*realm, basic_embedded_schema, basic_link_schema, + "Class 'child' has been changed from Embedded to TopLevel."); } - SECTION("change table to embedded - one incoming link per object") { - Schema schema = { - {"child_table", - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; + SECTION("changing table to embedded with exactly one incoming link per object works") { auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, basic_link_schema, 1); + realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object1 = child_table->create_object(); - child_object1.set("value", 42); - Obj child_object2 = child_table->create_object(); - child_object2.set("value", 43); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key1 = child_object1.get_key(); - auto child_object_key2 = child_object2.get_key(); - parent_table->create_object().set_all(child_object_key1); - parent_table->create_object().set_all(child_object_key2); + auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child"); + ObjKey child1 = child_table->create_object().set_all(42).get_key(); + ObjKey child2 = child_table->create_object().set_all(43).get_key(); + auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + parent_table->create_object().set_all(child1); + parent_table->create_object().set_all(child2); realm->commit_transaction(); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 2); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_NOTHROW( - realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, nullptr)); + REQUIRE_UPDATE_SUCCEEDS(*realm, basic_embedded_schema, 2); - REQUIRE(realm->schema_version() == 2); REQUIRE(parent_table->size() == 2); REQUIRE(child_table->size() == 2); - REQUIRE(child_table->is_embedded()); - for (int i = 0; i < 2; i++) { - Object parent_object(realm, "parent_table", i); - CppContext context(realm); - Object child_object = - util::any_cast(parent_object.get_property_value(context, "child_property")); - Int value = util::any_cast(child_object.get_property_value(context, "value")); - REQUIRE(value == 42 + i); + int64_t expected = 42; + for (auto parent : *parent_table) { + auto child = child_table->get_object(parent.get("link")); + REQUIRE(child.get("value") == expected++); } } - SECTION("change table to embedded - multiple incoming links per object resolved by removing a column") { - Schema schema = { - {"child_table", - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - {"child_property_duplicate", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; - Schema schema2 = { - {"child_table", - ObjectType::Embedded, - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; - + SECTION("changing table to embedded works if duplicate links were from a removed column") { auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS( + *realm, + add_property(basic_link_schema, "parent", + Property{"link 2", PropertyType::Object | PropertyType::Nullable, "child"}), + 1); + realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object1 = child_table->create_object(); - child_object1.set("value", 42); - Obj child_object2 = child_table->create_object(); - child_object2.set("value", 43); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key1 = child_object1.get_key(); - auto child_object_key2 = child_object2.get_key(); - parent_table->create_object().set_all(child_object_key1, child_object_key1); - parent_table->create_object().set_all(child_object_key2, child_object_key2); + auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child"); + ObjKey child_object1 = child_table->create_object().set_all(42).get_key(); + ObjKey child_object2 = child_table->create_object().set_all(43).get_key(); + auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + parent_table->create_object().set_all(child_object1, child_object2); + parent_table->create_object().set_all(child_object2, child_object1); realm->commit_transaction(); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 2); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_NOTHROW(realm->update_schema(schema2, 2, nullptr)); + REQUIRE_UPDATE_SUCCEEDS(*realm, basic_embedded_schema, 2); - REQUIRE(realm->schema_version() == 2); REQUIRE(parent_table->size() == 2); REQUIRE(child_table->size() == 2); - REQUIRE(child_table->is_embedded()); - CppContext context(realm); - for (int i = 0; i < 2; i++) { - Object parent_object(realm, "parent_table", i); - Object child_object = - util::any_cast(parent_object.get_property_value(context, "child_property")); - Int value = util::any_cast(child_object.get_property_value(context, "value")); - REQUIRE(value == 42 + i); + int64_t expected = 42; + for (auto parent : *parent_table) { + auto child = child_table->get_object(parent.get("link")); + REQUIRE(child.get("value") == expected++); } } - SECTION("change table to embedded - multiple incoming links - resolved in migration block") { - Schema schema = { - {"child_table", - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; + SECTION("changing table to embedded works if duplicate links are resolved in migration block") { auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, basic_link_schema, 1); + realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key = child_object.get_key(); - parent_table->create_object().set_all(child_object_key); - parent_table->create_object().set_all(child_object_key); + auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child"); + ObjKey child_object = child_table->create_object().set_all(42).get_key(); + auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + parent_table->create_object().set_all(child_object); + parent_table->create_object().set_all(child_object); realm->commit_transaction(); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_NOTHROW(realm->update_schema( - set_table_type(schema, "child_table", ObjectType::Embedded), 2, [](auto, auto new_realm, auto&) { - Object parent_object1(new_realm, "parent_table", 0); - CppContext context(new_realm); - Object child_object1 = util::any_cast( - parent_object1.get_property_value(context, "child_property")); - Int value = util::any_cast(child_object1.get_property_value(context, "value")); - - auto child_table = ObjectStore::table_for_object_type(new_realm->read_group(), "child_table"); - Obj child_object2 = child_table->create_object(); - child_object2.set("value", value); - - Object parent_object2(new_realm, "parent_table", 1); - parent_object2.set_property_value(context, "child_property", std::any(child_object2)); - })); - - REQUIRE(realm->schema_version() == 2); + REQUIRE_THROWS_CONTAINING( + realm->update_schema(basic_embedded_schema, 2), + "Cannot convert 'child' to embedded: at least one object has more than one incoming link."); + REQUIRE_MIGRATION_SUCCEEDS(*realm, basic_embedded_schema, 2, [](auto, auto new_realm, auto&) { + auto child = ObjectStore::table_for_object_type(new_realm->read_group(), "child"); + auto parent = ObjectStore::table_for_object_type(new_realm->read_group(), "parent"); + parent->get_object(1).set("link", child->create_object().set_all(42).get_key()); + }); + REQUIRE(parent_table->size() == 2); REQUIRE(child_table->size() == 2); - REQUIRE(child_table->is_embedded()); - for (int i = 0; i < 2; i++) { - Object parent_object(realm, "parent_table", i); - CppContext context(realm); - Object child_object = - util::any_cast(parent_object.get_property_value(context, "child_property")); - Int value = util::any_cast(child_object.get_property_value(context, "value")); - REQUIRE(value == 42); + for (auto parent : *parent_table) { + auto child = child_table->get_object(parent.get("link")); + REQUIRE(child.get("value") == 42); } } - SECTION( - "change table from top-level to embedded, delete objects with 0 incoming links, resolved automatically") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - {"child_table", + + SECTION("changing table to embedded works if there are backlink columns from a Mixed property but currently " + "no incoming links") { + const Schema schema = { + {"child", { {"value", PropertyType::Int}, }}, - {"parent_table", + {"parent", { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, + {"link", PropertyType::Object | PropertyType::Nullable, "child"}, + {"mixed", PropertyType::Mixed | PropertyType::Nullable}, + {"list", PropertyType::Mixed | PropertyType::Nullable | PropertyType::Array}, + {"set", PropertyType::Mixed | PropertyType::Nullable | PropertyType::Set}, + {"dictionary", PropertyType::Mixed | PropertyType::Nullable | PropertyType::Dictionary}, }}, }; + auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); + realm->begin_transaction(); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); + auto child = ObjectStore::table_for_object_type(realm->read_group(), "child"); + auto parent = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + ObjLink child_obj{child->get_key(), child->create_object().get_key()}; + auto parent_obj = parent->create_object().set_all(child_obj.get_obj_key(), Mixed(child_obj)); + parent_obj.get_list("list").add(child_obj); + parent_obj.get_set("set").insert(child_obj); + parent_obj.get_dictionary("dictionary").insert("foo", child_obj); realm->commit_transaction(); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE(child_table->size() == 1); - REQUIRE(parent_table->size() == 0); - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); + auto embedded_schema = set_table_type(schema, "child", ObjectType::Embedded); + REQUIRE_THROWS_WITH(realm->update_schema(embedded_schema, 2), + "Cannot convert 'child' to embedded: there is an incoming link from the Mixed " + "property 'parent.mixed', which does not support linking to embedded objects."); - REQUIRE(realm->schema_version() == 2); - REQUIRE(child_table->is_embedded()); - REQUIRE(child_table->size() == 0); - REQUIRE(parent_table->size() == 0); + realm->begin_transaction(); + parent_obj.set_any("mixed", Mixed()); + parent_obj.get_list("list").clear(); + parent_obj.get_set("set").clear(); + parent_obj.get_dictionary("dictionary").clear(); + realm->commit_transaction(); + + REQUIRE_UPDATE_SUCCEEDS(*realm, embedded_schema, 2); } - SECTION("change table from top-level to embedded, migration allowed, embedded object with 1 incoming link. " - "Resolve automatic " - "should not be triggered") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - {"child_table", - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - }; + + SECTION("automatic migration to embedded deletes objects with no incoming links") { + config.automatically_handle_backlinks_in_migrations = true; + config.schema = basic_link_schema; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); + + auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child"); realm->begin_transaction(); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - auto child_object_key = child_object.get_key(); - parent_table->create_object().set_all(child_object_key); + child_table->create_object(); realm->commit_transaction(); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE(child_table->size() == 1); - REQUIRE(parent_table->size() == 1); - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); + REQUIRE_UPDATE_SUCCEEDS(*realm, basic_embedded_schema, 1); + REQUIRE(child_table->size() == 0); + } - REQUIRE(realm->schema_version() == 2); - REQUIRE(child_table->is_embedded()); - REQUIRE(child_table->size() == 1); - REQUIRE(parent_table->size() == 1); - } - SECTION("change table to embedded - multiple incoming links - resolved automatically + copy array of mixed " - "verification") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - { - "child_table", - { - {"mixed_array", PropertyType::Mixed | PropertyType::Array | PropertyType::Nullable}, - }, - }, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - {"target", - { - {"value", PropertyType::Int}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - ColKey col_mixed_array = child_table->get_column_key("mixed_array"); - auto target_table = ObjectStore::table_for_object_type(realm->read_group(), "target"); - Obj target_object = target_table->create_object(); - target_object.set("value", 10); - List list(realm, child_object, col_mixed_array); - list.insert(0, Mixed{10}); - list.insert(1, Mixed{10.10}); - list.insert(2, Mixed{ObjLink{target_table->get_key(), target_object.get_key()}}); - list.insert(3, Mixed{ObjLink{target_table->get_key(), target_object.get_key()}}); - - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key = child_object.get_key(); - auto o1 = parent_table->create_object(); - auto o2 = parent_table->create_object(); - o1.set_all(child_object_key); - o2.set_all(child_object_key); - realm->commit_transaction(); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 1); - REQUIRE(target_table->size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - REQUIRE_FALSE(target_table->is_embedded()); - - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); - - REQUIRE(realm->schema_version() == 2); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 2); - REQUIRE(target_table->size() == 1); - REQUIRE(child_table->is_embedded()); - REQUIRE_FALSE(target_table->is_embedded()); - - for (int i = 0; i < 2; i++) { - Object parent_object(realm, "parent_table", i); - CppContext context(realm); - Object child_object = - util::any_cast(parent_object.get_property_value(context, "child_property")); - auto mixed_array = - util::any_cast(child_object.get_property_value(context, "mixed_array")); - REQUIRE(mixed_array.size() == 4); - REQUIRE(mixed_array.get_any(0).get() == 10); - REQUIRE(mixed_array.get_any(1).get() == 10.10); - REQUIRE(mixed_array.get_any(2).get().get_table_key() == - target_object.get_table()->get_key()); - REQUIRE(mixed_array.get_any(2).get().get_obj_key() == target_object.get_key()); - REQUIRE(mixed_array.get_any(3).get() == target_object.get_key()); - } - } - SECTION("change table to embedded - multiple incoming links - resolved automatically + copy set, dictionary, " - "any array " - "verification") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - { - "child_table", - { - {"value", PropertyType::Int}, - {"value_dict", PropertyType::Dictionary | PropertyType::Int}, - {"links_dict", PropertyType::Dictionary | PropertyType::Object | PropertyType::Nullable, - "target"}, - {"value_set", PropertyType::Set | PropertyType::Int}, - {"links_set", PropertyType::Set | PropertyType::Object, "target"}, - }, - }, - {"parent_table", - {{"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - {"mixed_links", PropertyType::Dictionary | PropertyType::Mixed | PropertyType::Nullable}}}, - {"target", - { - {"value", PropertyType::Int}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - ColKey col_dict_value = child_table->get_column_key("value_dict"); - ColKey col_dict_links = child_table->get_column_key("links_dict"); - ColKey col_set_value = child_table->get_column_key("value_set"); - ColKey col_set_links = child_table->get_column_key("links_set"); - object_store::Dictionary dict_vals(realm, child_object, col_dict_value); - dict_vals.insert("test", 10); - object_store::Set set_vals(realm, child_object, col_set_value); - set_vals.insert(10); - set_vals.insert(11); - set_vals.insert(9); - - auto target_table = ObjectStore::table_for_object_type(realm->read_group(), "target"); - Obj target_object = target_table->create_object(); - target_object.set("value", 10); - object_store::Dictionary dict_links(realm, child_object, col_dict_links); - dict_links.insert("link", target_object.get_key()); - object_store::Set set_links(realm, child_object, col_set_links); - set_links.insert(target_object.get_key()); - - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key = child_object.get_key(); - auto o1 = parent_table->create_object(); - auto o2 = parent_table->create_object(); - ColKey col_mixed_links = parent_table->get_column_key("mixed_links"); - object_store::Dictionary mixed_links_o1(realm, o1, col_mixed_links); - mixed_links_o1.insert("ref_mixed_link", ObjLink{target_table->get_key(), target_object.get_key()}); - object_store::Dictionary mixed_links_o2(realm, o2, col_mixed_links); - mixed_links_o2.insert("ref_mixed_link", ObjLink{target_table->get_key(), target_object.get_key()}); - o1.set_all(child_object_key); - o2.set_all(child_object_key); - realm->commit_transaction(); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 1); - REQUIRE(target_table->size() == 1); - REQUIRE(dict_vals.size() == 1); - REQUIRE(dict_links.size() == 1); - REQUIRE(set_vals.size() == 3); - REQUIRE(set_links.size() == 1); - REQUIRE(mixed_links_o1.size() == 1); - REQUIRE(mixed_links_o2.size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - REQUIRE_FALSE(target_table->is_embedded()); - - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); - - REQUIRE(realm->schema_version() == 2); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 2); - REQUIRE(target_table->size() == 1); - REQUIRE(child_table->is_embedded()); - REQUIRE_FALSE(target_table->is_embedded()); - - for (int i = 0; i < 2; i++) { - Object parent_object(realm, "parent_table", i); - CppContext context(realm); - Object child_object = - util::any_cast(parent_object.get_property_value(context, "child_property")); - Int value = util::any_cast(child_object.get_property_value(context, "value")); - REQUIRE(value == 42); - auto value_dictionary = util::any_cast( - child_object.get_property_value(context, "value_dict")); - REQUIRE(value_dictionary.size() == 1); - auto pair_val = value_dictionary.get_pair(0); - REQUIRE(pair_val.first == "test"); - REQUIRE(pair_val.second == 10); - auto links_dictionary = util::any_cast( - child_object.get_property_value(context, "links_dict")); - REQUIRE(links_dictionary.size() == 1); - auto pair_link = links_dictionary.get_pair(0); - REQUIRE(pair_link.first == "link"); - REQUIRE_FALSE(pair_link.second.is_unresolved_link()); - REQUIRE(pair_link.second.get() == target_object.get_key()); - - auto mixed_links = util::any_cast( - parent_object.get_property_value(context, "mixed_links")); - REQUIRE(mixed_links.size() == 1); - auto pair_mixed_link = mixed_links.get_pair(0); - REQUIRE(pair_mixed_link.first == "ref_mixed_link"); - REQUIRE_FALSE(pair_mixed_link.second.is_unresolved_link()); - REQUIRE(pair_mixed_link.second.get() == target_object.get_key()); - - - auto value_set = util::any_cast( - child_object.get_property_value(context, "value_set")); - REQUIRE(value_set.size() == 3); - REQUIRE(value_set.get_any(0) == 9); - REQUIRE(value_set.get_any(1) == 10); - REQUIRE(value_set.get_any(2) == 11); - auto links_set = util::any_cast( - child_object.get_property_value(context, "links_set")); - REQUIRE(links_set.size() == 1); - REQUIRE(links_set.get_any(0).get() == target_object.get_key()); - } - } - SECTION("change table to embedded - multiple links stored in a dictionary") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - { - "child_table", - {{"value", PropertyType::Int}}, - }, - {"parent_table", - { - {"child_property", PropertyType::Dictionary | PropertyType::Object | PropertyType::Nullable, - "child_table"}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto parent_object = parent_table->create_object(); - ColKey col_links = parent_table->get_column_key("child_property"); - auto child_object_key = child_object.get_key(); - object_store::Dictionary dict_links(realm, parent_object, col_links); - dict_links.insert("ref", child_object_key); - dict_links.insert("ref1", child_object_key); - realm->commit_transaction(); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); - - REQUIRE(realm->schema_version() == 2); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 2); - REQUIRE(child_table->is_embedded()); - - for (int i = 0; i < 1; i++) { - Object parent_object(realm, "parent_table", i); - CppContext context(realm); - object_store::Dictionary links_dictionary = util::any_cast( - parent_object.get_property_value(context, "child_property")); - REQUIRE(links_dictionary.size() == dict_links.size()); - for (size_t i = 0; i < 2; ++i) { - const auto& [key, value] = links_dictionary.get_pair(i); - const auto& [key1, value1] = dict_links.get_pair(i); - REQUIRE(key == key1); - REQUIRE(value == value1); - } - } - } - SECTION("change table to embedded - incoming links stored in a set") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - { - "child_table", - {{"value", PropertyType::Int}}, - }, - {"parent_table", - { - {"child_property", PropertyType::Set | PropertyType::Object, "child_table"}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto parent_object = parent_table->create_object(); - ColKey col_links = parent_table->get_column_key("child_property"); - auto child_object_key = child_object.get_key(); - object_store::Set set_links(realm, parent_object, col_links); - set_links.insert(child_object_key); - // this should not create a new ref (set does not allow dups) - set_links.insert(child_object_key); - realm->commit_transaction(); + SECTION("automatic migration to embedded does not modify valid objects") { + config.automatically_handle_backlinks_in_migrations = true; + config.schema = basic_link_schema; + auto realm = Realm::get_shared_realm(config); + + auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child"); + realm->begin_transaction(); + Obj child_object = child_table->create_object().set_all(42); + auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + parent_table->create_object().set_all(child_object.get_key()); + realm->commit_transaction(); + + REQUIRE_UPDATE_SUCCEEDS(*realm, basic_embedded_schema, 1); REQUIRE(parent_table->size() == 1); REQUIRE(child_table->size() == 1); - REQUIRE(set_links.size() == 1); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - - REQUIRE_THROWS(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); + // Verifies that the existing accessor is still valid + REQUIRE(child_object.get("value") == 42); } - SECTION("change table to embedded - multiple links stored in linked list") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - { - "child_table", - {{"value", PropertyType::Int}}, - }, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Array, "child_table"}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key = child_object.get_key(); - auto parent_object = parent_table->create_object(); - auto list = parent_object.get_linklist("child_property"); - list.insert(0, child_object_key); - list.insert(1, child_object_key); - realm->commit_transaction(); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 1); - REQUIRE(list.size() == 2); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); - REQUIRE(realm->schema_version() == 2); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 2); - REQUIRE(child_table->is_embedded()); - auto linklist = parent_object.get_linklist("child_property"); - REQUIRE(linklist.size() == 2); - for (size_t i = 1; i < linklist.size(); ++i) { - REQUIRE(linklist.get(i - 1) != linklist.get(i)); - } - } - SECTION("change table to embedded - convert the whole list of linking embedded objects") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - {"child_table", - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable, "child_table"}, - }}, - {"origin_table", - { - {"parent_property", PropertyType::Object | PropertyType::Nullable, "parent_table"}, - }}, - }; + SECTION("automatic migration to embedded duplicates objects with multiple incoming links") { + config.automatically_handle_backlinks_in_migrations = true; + config.schema = basic_link_schema; auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - realm->begin_transaction(); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_table"); - Obj child_object = child_table->create_object(); - child_object.set("value", 42); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - auto child_object_key = child_object.get_key(); - auto p1 = parent_table->create_object(); - auto p2 = parent_table->create_object(); - p1.set_all(child_object_key); - p2.set_all(child_object_key); - auto origin_table = ObjectStore::table_for_object_type(realm->read_group(), "origin_table"); - origin_table->create_object().set_all(p1.get_key()); - origin_table->create_object().set_all(p2.get_key()); - realm->commit_transaction(); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 1); - REQUIRE(origin_table->size() == 2); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - REQUIRE_FALSE(origin_table->is_embedded()); - - for (auto& obj : *child_table) { - REQUIRE(obj.get_backlink_count() == 2); - } - for (auto& obj : *parent_table) { - REQUIRE(obj.get_backlink_count() == 1); - } - for (auto& obj : *origin_table) { - REQUIRE(obj.get_backlink_count() == 0); - } - - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "parent_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); - REQUIRE(realm->schema_version() == 2); - REQUIRE_FALSE(child_table->is_embedded()); - REQUIRE(parent_table->is_embedded()); - REQUIRE_FALSE(origin_table->is_embedded()); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 1); - REQUIRE(origin_table->size() == 2); - - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "child_table", ObjectType::Embedded), 3, - [](auto, auto, auto&) {})); - - REQUIRE(realm->schema_version() == 3); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 2); - REQUIRE(origin_table->size() == 2); - - for (auto& obj : *child_table) { - REQUIRE(obj.get_backlink_count() == 1); - } - for (auto& obj : *parent_table) { - REQUIRE(obj.get_backlink_count() == 1); - } - for (auto& obj : *origin_table) { - REQUIRE(obj.get_backlink_count() == 0); - } - - std::vector obj_children; - for (auto& child_obj : *child_table) { - obj_children.push_back(child_obj.get_key()); - } - for (int i = 0; i < 2; i++) { - Object parent_object(realm, "parent_table", i); - CppContext context(realm); - Object child_object = - util::any_cast(parent_object.get_property_value(context, "child_property")); - REQUIRE(child_object.obj().get_key() == obj_children[i]); - } - } - SECTION("change table to embedded - violate embedded object constraints") { - InMemoryTestFile config; - config.automatic_handle_backlicks_in_migrations = true; - Schema schema = { - {"child_embedded_table", - ObjectType::Embedded, - { - {"value", PropertyType::Int}, - }}, - {"parent_table", - { - {"child_property", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary, - "child_embedded_table"}, - }}, - {"origin_table", - { - {"parent_property", PropertyType::Object | PropertyType::Nullable, "parent_table"}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); + auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child"); realm->begin_transaction(); - - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "child_embedded_table"); - auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent_table"); - Obj parent_object = parent_table->create_object(); - ColKey col_link = parent_table->get_column_key("child_property"); - object_store::Dictionary dict_link(realm, parent_object, col_link); - auto child_obj = dict_link.insert_embedded("Ref"); - child_obj.set("value", 42); - - auto origin_table = ObjectStore::table_for_object_type(realm->read_group(), "origin_table"); - origin_table->create_object().set_all(parent_object.get_key()); - origin_table->create_object().set_all(parent_object.get_key()); + Obj child_object = child_table->create_object().set_all(42); + auto parent_table = ObjectStore::table_for_object_type(realm->read_group(), "parent"); + parent_table->create_object().set_all(child_object.get_key()); + parent_table->create_object().set_all(child_object.get_key()); + parent_table->create_object().set_all(child_object.get_key()); realm->commit_transaction(); - REQUIRE(parent_table->size() == 1); - REQUIRE(child_table->size() == 1); - REQUIRE(origin_table->size() == 2); - REQUIRE(child_table->is_embedded()); - REQUIRE_FALSE(parent_table->is_embedded()); - REQUIRE_FALSE(origin_table->is_embedded()); - for (auto& obj : *child_table) { - REQUIRE(obj.get_backlink_count() == 1); - } - for (auto& obj : *parent_table) { - REQUIRE(obj.get_backlink_count() == 2); - } - for (auto& obj : *origin_table) { - REQUIRE(obj.get_backlink_count() == 0); - } + REQUIRE_UPDATE_SUCCEEDS(*realm, basic_embedded_schema, 1); + REQUIRE(parent_table->size() == 3); + REQUIRE(child_table->size() == 3); - REQUIRE_NOTHROW(realm->update_schema(set_table_type(schema, "parent_table", ObjectType::Embedded), 2, - [](auto, auto, auto&) {})); - REQUIRE(realm->schema_version() == 2); - REQUIRE(child_table->is_embedded()); - REQUIRE(parent_table->is_embedded()); - REQUIRE_FALSE(origin_table->is_embedded()); - REQUIRE(parent_table->size() == 2); - REQUIRE(child_table->size() == 2); - REQUIRE(origin_table->size() == 2); - - for (int i = 0; i < 2; i++) { - Object parent_object(realm, "parent_table", i); - CppContext context(realm); - object_store::Dictionary dictionary_to_embedded_object = util::any_cast( - parent_object.get_property_value(context, "child_property")); - auto child = dictionary_to_embedded_object.get_any("Ref"); - ObjLink link = child.get_link(); - Object child_value(realm, link); - REQUIRE(child_value.get_column_value("value") == 42); + // The existing accessor is no longer valid because we delete the original object + REQUIRE_FALSE(child_object.is_valid()); + for (auto obj : *parent_table) { + REQUIRE(child_table->get_object(obj.get("link")).get("value") == 42); } } } @@ -1752,7 +1073,7 @@ TEST_CASE("migration: Automatic") { {"optional", PropertyType::Int | PropertyType::Nullable}, }}, }; - realm->update_schema(schema); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); #define VERIFY_SCHEMA_IN_MIGRATION(target_schema) \ do { \ @@ -1993,7 +1314,8 @@ TEST_CASE("migration: Automatic") { CppContext ctx2(new_realm); obj = Object::get_for_primary_key(ctx, new_realm, "all types", std::any(INT64_C(1))); REQUIRE(obj.is_valid()); - REQUIRE_THROWS(obj.get_property_value(ctx, "bool")); + REQUIRE_THROWS_CONTAINING(obj.get_property_value(ctx, "bool"), + "Property 'all types.bool' does not exist"); }); } @@ -2002,25 +1324,27 @@ TEST_CASE("migration: Automatic") { CppContext ctx(old_realm); Object obj = Object::get_for_primary_key(ctx, old_realm, "all types", std::any(INT64_C(1))); REQUIRE(obj.is_valid()); - REQUIRE_THROWS(obj.set_property_value(ctx, "bool", std::any(false))); - REQUIRE_THROWS(old_realm->begin_transaction()); + REQUIRE_THROWS_CONTAINING(obj.set_property_value(ctx, "bool", std::any(false)), + "Cannot modify managed objects outside of a write transaction."); + REQUIRE_THROWS_CONTAINING(old_realm->begin_transaction(), + "Can't perform transactions on read-only Realms."); }); } SECTION("cannot read values for removed properties from new realm") { Schema schema{ - {"all types", - { - {"pk", PropertyType::Int, Property::IsPrimary{true}}, - }}, + {"all types", {{"pk", PropertyType::Int, Property::IsPrimary{true}}}}, }; realm->update_schema(schema, 2, [](auto, auto new_realm, Schema&) { CppContext ctx(new_realm); Object obj = Object::get_for_primary_key(ctx, new_realm, "all types", std::any(INT64_C(1))); REQUIRE(obj.is_valid()); - REQUIRE_THROWS(obj.get_property_value(ctx, "bool")); - REQUIRE_THROWS(obj.get_property_value(ctx, "object")); - REQUIRE_THROWS(obj.get_property_value(ctx, "array")); + REQUIRE_THROWS_CONTAINING(obj.get_property_value(ctx, "bool"), + "Property 'all types.bool' does not exist"); + REQUIRE_THROWS_CONTAINING(obj.get_property_value(ctx, "object"), + "Property 'all types.object' does not exist"); + REQUIRE_THROWS_CONTAINING(obj.get_property_value(ctx, "array"), + "Property 'all types.array' does not exist"); }); } @@ -2030,7 +1354,6 @@ TEST_CASE("migration: Automatic") { Object obj = Object::get_for_primary_key(ctx, new_realm, "all types", std::any(INT64_C(1))); REQUIRE(obj.is_valid()); - auto link = util::any_cast(obj.get_property_value(ctx, "object")); REQUIRE(link.is_valid()); REQUIRE(util::any_cast(link.get_property_value(ctx, "value")) == 10); @@ -2362,10 +1685,7 @@ TEST_CASE("migration: Automatic") { } while (false) Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; SECTION("table does not exist in old schema") { @@ -2416,14 +1736,8 @@ TEST_CASE("migration: Automatic") { SECTION("different link targets") { Schema schema = { - {"target", - { - {"value", PropertyType::Int}, - }}, - {"origin", - { - {"link", PropertyType::Object | PropertyType::Nullable, "target"}, - }}, + {"target", {{"value", PropertyType::Int}}}, + {"origin", {{"link", PropertyType::Object | PropertyType::Nullable, "target"}}}, }; auto schema2 = set_target(schema, "origin", "link", "origin"); schema2.find("origin")->property_for_name("link")->name = "new"; @@ -2435,14 +1749,8 @@ TEST_CASE("migration: Automatic") { SECTION("different linklist targets") { Schema schema = { - {"target", - { - {"value", PropertyType::Int}, - }}, - {"origin", - { - {"link", PropertyType::Array | PropertyType::Object, "target"}, - }}, + {"target", {{"value", PropertyType::Int}}}, + {"origin", {{"link", PropertyType::Array | PropertyType::Object, "target"}}}, }; auto schema2 = set_target(schema, "origin", "link", "origin"); schema2.find("origin")->property_for_name("link")->name = "new"; @@ -2454,14 +1762,8 @@ TEST_CASE("migration: Automatic") { SECTION("different object set targets") { Schema schema = { - {"target", - { - {"value", PropertyType::Int}, - }}, - {"origin", - { - {"link", PropertyType::Set | PropertyType::Object, "target"}, - }}, + {"target", {{"value", PropertyType::Int}}}, + {"origin", {{"link", PropertyType::Set | PropertyType::Object, "target"}}}, }; auto schema2 = set_target(schema, "origin", "link", "origin"); schema2.find("origin")->property_for_name("link")->name = "new"; @@ -2607,40 +1909,23 @@ TEST_CASE("migration: Immutable") { SECTION("extra tables") { auto realm = realm_with_schema({ - {"object", - { - {"value", PropertyType::Int}, - }}, - {"object 2", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, + {"object 2", {{"value", PropertyType::Int}}}, }); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; REQUIRE_NOTHROW(realm->update_schema(schema)); + REQUIRE(realm->schema() == schema); } SECTION("missing tables") { auto realm = realm_with_schema({ - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, - {"second object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, + {"second object", {{"value", PropertyType::Int}}}, }; REQUIRE_NOTHROW(realm->update_schema(schema)); REQUIRE(realm->schema() == schema); @@ -2663,34 +1948,27 @@ TEST_CASE("migration: Immutable") { }}, }); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; REQUIRE_NOTHROW(realm->update_schema(schema)); + REQUIRE(realm->schema() == schema); } SECTION("differing embeddedness") { auto realm = realm_with_schema({ {"top", {{"link", PropertyType::Object | PropertyType::Nullable, "object"}}}, - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }); Schema schema = set_table_type(realm->schema(), "object", ObjectType::Embedded); REQUIRE_NOTHROW(realm->update_schema(schema)); + REQUIRE(realm->schema() == schema); } } SECTION("disallowed mismatches") { SECTION("missing columns in table") { auto realm = realm_with_schema({ - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }); Schema schema = { {"object", @@ -2699,18 +1977,16 @@ TEST_CASE("migration: Immutable") { {"value 2", PropertyType::Int}, }}, }; - REQUIRE_THROWS(realm->update_schema(schema)); + REQUIRE_THROWS_CONTAINING(realm->update_schema(schema), "Property 'object.value 2' has been added."); } SECTION("bump schema version") { Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; auto realm = realm_with_schema(schema); - REQUIRE_THROWS(realm->update_schema(schema, 1)); + REQUIRE_THROWS_CONTAINING(realm->update_schema(schema, 1), + "Provided schema version 1 does not equal last set version 0."); } } } @@ -2721,7 +1997,7 @@ TEST_CASE("migration: ReadOnly") { auto realm_with_schema = [&](Schema schema) { { auto realm = Realm::get_shared_realm(config); - realm->update_schema(std::move(schema)); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); } config.schema_mode = SchemaMode::ReadOnly; return Realm::get_shared_realm(config); @@ -2744,25 +2020,15 @@ TEST_CASE("migration: ReadOnly") { }}, }; REQUIRE_NOTHROW(realm->update_schema(schema)); - REQUIRE(realm->schema() == schema); } SECTION("extra tables") { auto realm = realm_with_schema({ - {"object", - { - {"value", PropertyType::Int}, - }}, - {"object 2", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, + {"object 2", {{"value", PropertyType::Int}}}, }); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; REQUIRE_NOTHROW(realm->update_schema(schema)); } @@ -2776,40 +2042,25 @@ TEST_CASE("migration: ReadOnly") { }}, }); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; REQUIRE_NOTHROW(realm->update_schema(schema)); } SECTION("missing tables") { auto realm = realm_with_schema({ - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }); Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, - {"second object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, + {"second object", {{"value", PropertyType::Int}}}, }; REQUIRE_NOTHROW(realm->update_schema(schema)); } SECTION("bump schema version") { Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; auto realm = realm_with_schema(schema); REQUIRE_NOTHROW(realm->update_schema(schema, 1)); @@ -2818,10 +2069,7 @@ TEST_CASE("migration: ReadOnly") { SECTION("differing embeddedness") { Schema schema = { {"top", {{"link", PropertyType::Object | PropertyType::Nullable, "object"}}}, - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }; auto realm = realm_with_schema(schema); REQUIRE_NOTHROW(realm->update_schema(set_table_type(realm->schema(), "object", ObjectType::Embedded))); @@ -2829,13 +2077,9 @@ TEST_CASE("migration: ReadOnly") { } SECTION("disallowed mismatches") { - SECTION("missing columns in table") { auto realm = realm_with_schema({ - {"object", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, }); Schema schema = { {"object", @@ -2844,7 +2088,7 @@ TEST_CASE("migration: ReadOnly") { {"value 2", PropertyType::Int}, }}, }; - REQUIRE_THROWS(realm->update_schema(schema)); + REQUIRE_THROWS_CONTAINING(realm->update_schema(schema), "Property 'object.value 2' has been added."); } } } @@ -2854,14 +2098,8 @@ TEST_CASE("migration: SoftResetFile") { config.schema_mode = SchemaMode::SoftResetFile; Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, - {"object 2", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, + {"object 2", {{"value", PropertyType::Int}}}, }; // To verify that the file has actually be deleted and recreated, on @@ -2894,7 +2132,7 @@ TEST_CASE("migration: SoftResetFile") { { auto realm = Realm::get_shared_realm(config); auto ino = get_fileid(); - realm->update_schema(schema); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); REQUIRE(ino == get_fileid()); realm->begin_transaction(); ObjectStore::table_for_object_type(realm->read_group(), "object")->create_object(); @@ -2904,29 +2142,26 @@ TEST_CASE("migration: SoftResetFile") { auto ino = get_fileid(); SECTION("file is reset when schema version increases") { - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 0); REQUIRE(ino != get_fileid()); } SECTION("file is reset when an existing table is modified") { - realm->update_schema(add_property(schema, "object", {"value 2", PropertyType::Int})); + REQUIRE_UPDATE_SUCCEEDS(*realm, add_property(schema, "object", {"value 2", PropertyType::Int}), 0); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 0); REQUIRE(ino != get_fileid()); } SECTION("file is not reset when adding a new table") { - realm->update_schema(add_table(schema, {"object 3", - { - {"value", PropertyType::Int}, - }})); + REQUIRE_UPDATE_SUCCEEDS(*realm, add_table(schema, {"object 3", {{"value", PropertyType::Int}}}), 0); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 1); REQUIRE(realm->schema().size() == 3); REQUIRE(ino == get_fileid()); } SECTION("file is not reset when removing a table") { - realm->update_schema(remove_table(schema, "object 2")); + REQUIRE_UPDATE_SUCCEEDS(*realm, remove_table(schema, "object 2"), 0); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 1); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object 2")); REQUIRE(realm->schema().size() == 1); @@ -2934,14 +2169,14 @@ TEST_CASE("migration: SoftResetFile") { } SECTION("file is not reset when adding an index") { - realm->update_schema(set_indexed(schema, "object", "value", true)); + REQUIRE_UPDATE_SUCCEEDS(*realm, set_indexed(schema, "object", "value", true), 0); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 1); REQUIRE(ino == get_fileid()); } SECTION("file is not reset when removing an index") { - realm->update_schema(set_indexed(schema, "object", "value", true)); - realm->update_schema(schema); + REQUIRE_UPDATE_SUCCEEDS(*realm, set_indexed(schema, "object", "value", true), 0); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 1); REQUIRE(ino == get_fileid()); } @@ -2951,14 +2186,8 @@ TEST_CASE("migration: HardResetFile") { TestFile config; Schema schema = { - {"object", - { - {"value", PropertyType::Int}, - }}, - {"object 2", - { - {"value", PropertyType::Int}, - }}, + {"object", {{"value", PropertyType::Int}}}, + {"object 2", {{"value", PropertyType::Int}}}, }; // To verify that the file has actually be deleted and recreated, on @@ -2991,7 +2220,7 @@ TEST_CASE("migration: HardResetFile") { { auto realm = Realm::get_shared_realm(config); auto ino = get_fileid(); - realm->update_schema(schema); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); REQUIRE(ino == get_fileid()); realm->begin_transaction(); ObjectStore::table_for_object_type(realm->read_group(), "object")->create_object(); @@ -3002,7 +2231,7 @@ TEST_CASE("migration: HardResetFile") { auto ino = get_fileid(); SECTION("file is reset when schema version increases") { - realm->update_schema(schema, 1); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 0); REQUIRE(ino != get_fileid()); } @@ -3014,16 +2243,13 @@ TEST_CASE("migration: HardResetFile") { } SECTION("file is reset when adding a new table") { - realm->update_schema(add_table(schema, {"object 3", - { - {"value", PropertyType::Int}, - }})); + realm->update_schema(add_table(schema, {"object 3", {{"value", PropertyType::Int}}})); REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->size() == 0); REQUIRE(ino != get_fileid()); } } -TEST_CASE("migration: AdditiveDiscovered") { +TEST_CASE("migration: Additive") { Schema schema = { {"object", { @@ -3032,280 +2258,278 @@ TEST_CASE("migration: AdditiveDiscovered") { }}, }; - std::vector additive_modes = {SchemaMode::AdditiveDiscovered, SchemaMode::AdditiveExplicit}; - - for (auto mode : additive_modes) { - TestFile config; - config.cache = false; - config.schema = schema; - config.schema_mode = mode; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema); - std::string mode_string = util::format( - " with mode: %1", mode == SchemaMode::AdditiveDiscovered ? "AdditiveDiscovered" : "AdditiveExplicit"); - - DYNAMIC_SECTION("can add new properties to existing tables" << mode_string) { - REQUIRE_NOTHROW(realm->update_schema(add_property(schema, "object", {"value 3", PropertyType::Int}))); - REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->get_column_count() == 3); - } - - DYNAMIC_SECTION("can add new tables" << mode_string) { - REQUIRE_NOTHROW(realm->update_schema(add_table(schema, {"object 2", - { - {"value", PropertyType::Int}, - }}))); - REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")); - REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object 2")); - } - - DYNAMIC_SECTION("embedded orphan types" << mode_string) { - if (mode == SchemaMode::AdditiveDiscovered) { - // in discovered mode, adding embedded orphan types is allowed but ignored - REQUIRE_NOTHROW(realm->update_schema( - add_table(schema, {"origin", - ObjectType::Embedded, - {{"link", PropertyType::Object | PropertyType::Nullable, "object"}}}))); - REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")); - REQUIRE(!ObjectStore::table_for_object_type(realm->read_group(), "origin")); - } - else { - // explicitly included embedded orphan types is an error - REQUIRE_THROWS(realm->update_schema( - add_table(schema, {"origin", - ObjectType::Embedded, - {{"link", PropertyType::Object | PropertyType::Nullable, "object"}}}))); - } - } - - DYNAMIC_SECTION("cannot change existing table type" << mode_string) { - REQUIRE_THROWS(realm->update_schema(set_table_type(schema, "object", ObjectType::Embedded))); - } - - DYNAMIC_SECTION("indexes are updated when schema version is bumped" << mode_string) { - auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); - auto col_keys = table->get_column_keys(); - REQUIRE(table->has_search_index(col_keys[0])); - REQUIRE(!table->has_search_index(col_keys[1])); - - REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value", false), 1)); - REQUIRE(!table->has_search_index(col_keys[0])); - - REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value 2", true), 2)); - REQUIRE(table->has_search_index(col_keys[1])); - } - - DYNAMIC_SECTION("indexes are not updated when schema version is not bumped" << mode_string) { - auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); - auto col_keys = table->get_column_keys(); - REQUIRE(table->has_search_index(col_keys[0])); - REQUIRE(!table->has_search_index(col_keys[1])); - - REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value", false))); - REQUIRE(table->has_search_index(col_keys[0])); - - REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value 2", true))); - REQUIRE(!table->has_search_index(col_keys[1])); - } + TestFile config; + config.cache = false; + config.schema = schema; + config.schema_mode = GENERATE(SchemaMode::AdditiveDiscovered, SchemaMode::AdditiveExplicit); + auto realm = Realm::get_shared_realm(config); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); - DYNAMIC_SECTION("can remove properties from existing tables, but column is not removed" << mode_string) { - auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); - REQUIRE_NOTHROW(realm->update_schema(remove_property(schema, "object", "value"))); - REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->get_column_count() == 2); - auto const& properties = realm->schema().find("object")->persisted_properties; - REQUIRE(properties.size() == 1); - auto col_keys = table->get_column_keys(); - REQUIRE(col_keys.size() == 2); - REQUIRE(properties[0].column_key == col_keys[1]); - } + INFO((config.schema_mode == SchemaMode::AdditiveDiscovered ? "AdditiveDiscovered" : "AdditiveExplicit")); - DYNAMIC_SECTION("cannot change existing property types" << mode_string) { - REQUIRE_THROWS(realm->update_schema(set_type(schema, "object", "value", PropertyType::Float))); - } + SECTION("can add new properties to existing tables") { + REQUIRE_NOTHROW(realm->update_schema(add_property(schema, "object", {"value 3", PropertyType::Int}))); + REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->get_column_count() == 3); + } - DYNAMIC_SECTION("cannot change existing property nullability" << mode_string) { - REQUIRE_THROWS(realm->update_schema(set_optional(schema, "object", "value", true))); - REQUIRE_THROWS(realm->update_schema(set_optional(schema, "object", "value 2", false))); - } + SECTION("can add new tables") { + REQUIRE_NOTHROW(realm->update_schema(add_table(schema, {"object 2", + { + {"value", PropertyType::Int}, + }}))); + REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")); + REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object 2")); + } - DYNAMIC_SECTION("cannot change existing link targets" << mode_string) { + SECTION("embedded orphan types") { + if (config.schema_mode == SchemaMode::AdditiveDiscovered) { + // in discovered mode, adding embedded orphan types is allowed but ignored REQUIRE_NOTHROW(realm->update_schema( - add_table(schema, {"object 2", - { - {"link", PropertyType::Object | PropertyType::Nullable, "object"}, - }}))); - REQUIRE_THROWS(realm->update_schema(set_target(realm->schema(), "object 2", "link", "object 2"))); + add_table(schema, {"origin", + ObjectType::Embedded, + {{"link", PropertyType::Object | PropertyType::Nullable, "object"}}}))); + REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")); + REQUIRE(!ObjectStore::table_for_object_type(realm->read_group(), "origin")); } + } - DYNAMIC_SECTION("cannot change primary keys" << mode_string) { - REQUIRE_THROWS(realm->update_schema(set_primary_key(schema, "object", "value"))); + SECTION("cannot change existing table type") { + Schema schema = { + {"child", {{"value", PropertyType::Int}}}, + {"parent", {{"link", PropertyType::Object | PropertyType::Nullable, "child"}}}, + }; + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); + REQUIRE_THROWS_CONTAINING(realm->update_schema(set_table_type(schema, "child", ObjectType::Embedded)), + "Class 'child' has been changed from TopLevel to Embedded."); + } - REQUIRE_NOTHROW( - realm->update_schema(add_table(schema, {"object 2", - { - {"pk", PropertyType::Int, Property::IsPrimary{true}}, - }}))); + SECTION("indexes are updated when schema version is bumped") { + auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); + auto col_keys = table->get_column_keys(); + REQUIRE(table->has_search_index(col_keys[0])); + REQUIRE(!table->has_search_index(col_keys[1])); - REQUIRE_THROWS(realm->update_schema(set_primary_key(realm->schema(), "object 2", ""))); - } + REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value", false), 1)); + REQUIRE(!table->has_search_index(col_keys[0])); - DYNAMIC_SECTION("schema version is allowed to go down" << mode_string) { - REQUIRE_NOTHROW(realm->update_schema(schema, 1)); - REQUIRE(realm->schema_version() == 1); - REQUIRE_NOTHROW(realm->update_schema(schema, 0)); - REQUIRE(realm->schema_version() == 1); - } + REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value 2", true), 2)); + REQUIRE(table->has_search_index(col_keys[1])); + } - DYNAMIC_SECTION("migration function is not used" << mode_string) { - REQUIRE_NOTHROW(realm->update_schema(schema, 1, [&](SharedRealm, SharedRealm, Schema&) { - REQUIRE(false); - })); - } + SECTION("indexes are not updated when schema version is not bumped") { + auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); + auto col_keys = table->get_column_keys(); + REQUIRE(table->has_search_index(col_keys[0])); + REQUIRE(!table->has_search_index(col_keys[1])); - DYNAMIC_SECTION("add new columns from different SG" << mode_string) { - auto realm2 = Realm::get_shared_realm(config); - auto& group = realm2->read_group(); - realm2->begin_transaction(); - auto table = ObjectStore::table_for_object_type(group, "object"); - auto col_keys = table->get_column_keys(); - table->add_column(type_Int, "new column"); - realm2->commit_transaction(); + REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value", false))); + REQUIRE(table->has_search_index(col_keys[0])); - REQUIRE_NOTHROW(realm->refresh()); - REQUIRE(realm->schema() == schema); - REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); - REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); - } - - DYNAMIC_SECTION("opening new Realms uses the correct schema after an external change" << mode_string) { - auto realm2 = Realm::get_shared_realm(config); - auto& group = realm2->read_group(); - realm2->begin_transaction(); - auto table = ObjectStore::table_for_object_type(group, "object"); - auto col_keys = table->get_column_keys(); - table->add_column(type_Double, "newcol"); - realm2->commit_transaction(); + REQUIRE_NOTHROW(realm->update_schema(set_indexed(schema, "object", "value 2", true))); + REQUIRE(!table->has_search_index(col_keys[1])); + } - REQUIRE_NOTHROW(realm->refresh()); - REQUIRE(realm->schema() == schema); - REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); - REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); + SECTION("can remove properties from existing tables, but column is not removed") { + auto table = ObjectStore::table_for_object_type(realm->read_group(), "object"); + REQUIRE_NOTHROW(realm->update_schema(remove_property(schema, "object", "value"))); + REQUIRE(ObjectStore::table_for_object_type(realm->read_group(), "object")->get_column_count() == 2); + auto const& properties = realm->schema().find("object")->persisted_properties; + REQUIRE(properties.size() == 1); + auto col_keys = table->get_column_keys(); + REQUIRE(col_keys.size() == 2); + REQUIRE(properties[0].column_key == col_keys[1]); + } - // Gets the schema from the RealmCoordinator - auto realm3 = Realm::get_shared_realm(config); - REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); - REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); + SECTION("cannot change existing property types") { + REQUIRE_THROWS_CONTAINING(realm->update_schema(set_type(schema, "object", "value", PropertyType::String)), + "Property 'object.value' has been changed from 'int' to 'string'."); + } - // Close and re-open the file entirely so that the coordinator is recreated - realm.reset(); - realm2.reset(); - realm3.reset(); + SECTION("cannot change existing property nullability") { + REQUIRE_THROWS_CONTAINING(realm->update_schema(set_optional(schema, "object", "value", true)), + "Property 'object.value' has been made optional."); + REQUIRE_THROWS_CONTAINING(realm->update_schema(set_optional(schema, "object", "value 2", false)), + "Property 'object.value 2' has been made required."); + } - realm = Realm::get_shared_realm(config); - REQUIRE(realm->schema() == schema); - REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); - REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); - } + SECTION("cannot change existing link targets") { + REQUIRE_NOTHROW(realm->update_schema( + add_table(schema, {"object 2", + { + {"link", PropertyType::Object | PropertyType::Nullable, "object"}, + }}))); + REQUIRE_THROWS_CONTAINING(realm->update_schema(set_target(realm->schema(), "object 2", "link", "object 2")), + "Property 'object 2.link' has been changed from '' to ''."); + } - DYNAMIC_SECTION("can have different subsets of columns in different Realm instances" << mode_string) { - Realm::Config config2 = config; - config2.schema = add_property(schema, "object", {"value 3", PropertyType::Int}); - Realm::Config config3 = config; - config3.schema = remove_property(schema, "object", "value 2"); + SECTION("cannot change primary keys") { + REQUIRE_THROWS_CONTAINING(realm->update_schema(set_primary_key(schema, "object", "value")), + "Primary Key for class 'object' has been added."); - Realm::Config config4 = config; - config4.schema = util::none; + REQUIRE_NOTHROW( + realm->update_schema(add_table(schema, {"object 2", + { + {"pk", PropertyType::Int, Property::IsPrimary{true}}, + }}))); - auto realm2 = Realm::get_shared_realm(config2); - auto realm3 = Realm::get_shared_realm(config3); - REQUIRE(realm->schema().find("object")->persisted_properties.size() == 2); - REQUIRE(realm2->schema().find("object")->persisted_properties.size() == 3); - REQUIRE(realm3->schema().find("object")->persisted_properties.size() == 1); + REQUIRE_THROWS_CONTAINING(realm->update_schema(set_primary_key(realm->schema(), "object 2", "")), + "Primary Key for class 'object 2' has been removed."); + } - realm->refresh(); - realm2->refresh(); - REQUIRE(realm->schema().find("object")->persisted_properties.size() == 2); - REQUIRE(realm2->schema().find("object")->persisted_properties.size() == 3); + SECTION("schema version is allowed to go down") { + REQUIRE_NOTHROW(realm->update_schema(schema, 1)); + REQUIRE(realm->schema_version() == 1); + REQUIRE_NOTHROW(realm->update_schema(schema, 0)); + REQUIRE(realm->schema_version() == 1); + } - // No schema specified; should see all of them - auto realm4 = Realm::get_shared_realm(config4); - REQUIRE(realm4->schema().find("object")->persisted_properties.size() == 3); - } + SECTION("migration function is not used") { + REQUIRE_NOTHROW(realm->update_schema(schema, 1, [&](SharedRealm, SharedRealm, Schema&) { + REQUIRE(false); + })); + } - DYNAMIC_SECTION("updating a schema to include already-present column" << mode_string) { - Realm::Config config2 = config; - config2.schema = add_property(schema, "object", {"value 3", PropertyType::Int}); - auto realm2 = Realm::get_shared_realm(config2); - auto& properties2 = realm2->schema().find("object")->persisted_properties; + SECTION("add new columns from different SG") { + auto realm2 = Realm::get_shared_realm(config); + auto& group = realm2->read_group(); + realm2->begin_transaction(); + auto table = ObjectStore::table_for_object_type(group, "object"); + auto col_keys = table->get_column_keys(); + table->add_column(type_Int, "new column"); + realm2->commit_transaction(); + + REQUIRE_NOTHROW(realm->refresh()); + REQUIRE(realm->schema() == schema); + REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); + REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); + } - REQUIRE_NOTHROW(realm->update_schema(*config2.schema)); - REQUIRE(realm->schema().find("object")->persisted_properties.size() == 3); - auto& properties = realm->schema().find("object")->persisted_properties; - REQUIRE(properties[0].column_key == properties2[0].column_key); - REQUIRE(properties[1].column_key == properties2[1].column_key); - REQUIRE(properties[2].column_key == properties2[2].column_key); - } + SECTION("opening new Realms uses the correct schema after an external change") { + auto realm2 = Realm::get_shared_realm(config); + auto& group = realm2->read_group(); + realm2->begin_transaction(); + auto table = ObjectStore::table_for_object_type(group, "object"); + auto col_keys = table->get_column_keys(); + table->add_column(type_Double, "newcol"); + realm2->commit_transaction(); + + REQUIRE_NOTHROW(realm->refresh()); + REQUIRE(realm->schema() == schema); + REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); + REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); + + // Gets the schema from the RealmCoordinator + auto realm3 = Realm::get_shared_realm(config); + REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); + REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); + + // Close and re-open the file entirely so that the coordinator is recreated + realm.reset(); + realm2.reset(); + realm3.reset(); + + realm = Realm::get_shared_realm(config); + REQUIRE(realm->schema() == schema); + REQUIRE(realm->schema().find("object")->persisted_properties[0].column_key == col_keys[0]); + REQUIRE(realm->schema().find("object")->persisted_properties[1].column_key == col_keys[1]); + } - DYNAMIC_SECTION("increasing schema version without modifying schema properly leaves the schema untouched" - << mode_string) { - TestFile config1; - config1.schema = schema; - config1.schema_mode = SchemaMode::AdditiveDiscovered; - config1.schema_version = 0; + SECTION("can have different subsets of columns in different Realm instances") { + Realm::Config config2 = config; + config2.schema = add_property(schema, "object", {"value 3", PropertyType::Int}); + Realm::Config config3 = config; + config3.schema = remove_property(schema, "object", "value 2"); + + Realm::Config config4 = config; + config4.schema = util::none; + + auto realm2 = Realm::get_shared_realm(config2); + auto realm3 = Realm::get_shared_realm(config3); + REQUIRE(realm->schema().find("object")->persisted_properties.size() == 2); + REQUIRE(realm2->schema().find("object")->persisted_properties.size() == 3); + REQUIRE(realm3->schema().find("object")->persisted_properties.size() == 1); + + realm->refresh(); + realm2->refresh(); + REQUIRE(realm->schema().find("object")->persisted_properties.size() == 2); + REQUIRE(realm2->schema().find("object")->persisted_properties.size() == 3); + + // No schema specified; should see all of them + auto realm4 = Realm::get_shared_realm(config4); + REQUIRE(realm4->schema().find("object")->persisted_properties.size() == 3); + } - auto realm1 = Realm::get_shared_realm(config1); - REQUIRE(realm1->schema().size() == 1); - Schema schema1 = realm1->schema(); - realm1->close(); + SECTION("updating a schema to include already-present column") { + Realm::Config config2 = config; + config2.schema = add_property(schema, "object", {"value 3", PropertyType::Int}); + auto realm2 = Realm::get_shared_realm(config2); + auto& properties2 = realm2->schema().find("object")->persisted_properties; + + REQUIRE_NOTHROW(realm->update_schema(*config2.schema)); + REQUIRE(realm->schema().find("object")->persisted_properties.size() == 3); + auto& properties = realm->schema().find("object")->persisted_properties; + REQUIRE(properties[0].column_key == properties2[0].column_key); + REQUIRE(properties[1].column_key == properties2[1].column_key); + REQUIRE(properties[2].column_key == properties2[2].column_key); + } - Realm::Config config2 = config1; - config2.schema_version = 1; - auto realm2 = Realm::get_shared_realm(config2); - REQUIRE(realm2->schema() == schema1); - } + SECTION("increasing schema version without modifying schema properly leaves the schema untouched") { + TestFile config1; + config1.schema = schema; + config1.schema_mode = SchemaMode::AdditiveDiscovered; + config1.schema_version = 0; + + auto realm1 = Realm::get_shared_realm(config1); + REQUIRE(realm1->schema().size() == 1); + Schema schema1 = realm1->schema(); + realm1->close(); + + Realm::Config config2 = config1; + config2.schema_version = 1; + auto realm2 = Realm::get_shared_realm(config2); + REQUIRE(realm2->schema() == schema1); + } - DYNAMIC_SECTION("invalid schema update leaves the schema untouched" << mode_string) { - Realm::Config config2 = config; - config2.schema = add_property(schema, "object", {"value 3", PropertyType::Int}); - auto realm2 = Realm::get_shared_realm(config2); + SECTION("invalid schema update leaves the schema untouched") { + Realm::Config config2 = config; + config2.schema = add_property(schema, "object", {"value 3", PropertyType::Int}); + auto realm2 = Realm::get_shared_realm(config2); - REQUIRE_THROWS(realm->update_schema(add_property(schema, "object", {"value 3", PropertyType::Float}))); - REQUIRE(realm->schema().find("object")->persisted_properties.size() == 2); - } + REQUIRE_THROWS_CONTAINING( + realm->update_schema(add_property(schema, "object", {"value 3", PropertyType::Float})), + "Property 'object.value 3' has been changed from 'int' to 'float'."); + REQUIRE(realm->schema().find("object")->persisted_properties.size() == 2); + } - DYNAMIC_SECTION("update_schema() does not begin a write transaction when extra columns are present" - << mode_string) { - realm->begin_transaction(); + SECTION("update_schema() does not begin a write transaction when extra columns are present") { + realm->begin_transaction(); - auto realm2 = Realm::get_shared_realm(config); - // will deadlock if it tries to start a write transaction - realm2->update_schema(remove_property(schema, "object", "value")); - } + auto realm2 = Realm::get_shared_realm(config); + // will deadlock if it tries to start a write transaction + realm2->update_schema(remove_property(schema, "object", "value")); + } - DYNAMIC_SECTION( - "update_schema() does not begin a write transaction when indexes are changed without bumping schema " - "version" - << mode_string) { - realm->begin_transaction(); + SECTION("update_schema() does not begin a write transaction when indexes are changed without bumping schema " + "version") { + realm->begin_transaction(); - auto realm2 = Realm::get_shared_realm(config); - // will deadlock if it tries to start a write transaction - realm->update_schema(set_indexed(schema, "object", "value 2", true)); - } + auto realm2 = Realm::get_shared_realm(config); + // will deadlock if it tries to start a write transaction + realm->update_schema(set_indexed(schema, "object", "value 2", true)); + } - DYNAMIC_SECTION("update_schema() does not begin a write transaction for invalid schema changes" - << mode_string) { - realm->begin_transaction(); + SECTION("update_schema() does not begin a write transaction for invalid schema changes") { + realm->begin_transaction(); - auto realm2 = Realm::get_shared_realm(config); - auto new_schema = - add_property(remove_property(schema, "object", "value"), "object", {"value", PropertyType::Float}); - // will deadlock if it tries to start a write transaction - REQUIRE_THROWS(realm2->update_schema(new_schema)); - } + auto realm2 = Realm::get_shared_realm(config); + auto new_schema = + add_property(remove_property(schema, "object", "value"), "object", {"value", PropertyType::Float}); + // will deadlock if it tries to start a write transaction + REQUIRE_THROWS_CONTAINING(realm2->update_schema(new_schema), + "Property 'object.value' has been changed from 'int' to 'float'."); } } - TEST_CASE("migration: Manual") { TestFile config; config.schema_mode = SchemaMode::Manual; @@ -3323,110 +2547,137 @@ TEST_CASE("migration: Manual") { {"object", PropertyType::Object | PropertyType::Nullable, "object"}, {"array", PropertyType::Array | PropertyType::Object, "object"}, }}}; - realm->update_schema(schema); + REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 0); auto col_keys = realm->read_group().get_table("class_object")->get_column_keys(); -#define REQUIRE_MIGRATION(schema, migration) \ +#define REQUIRE_MIGRATION(schema, migration, msg) \ do { \ Schema new_schema = (schema); \ - REQUIRE_THROWS(realm->update_schema(new_schema)); \ + REQUIRE_THROWS_CONTAINING(realm->update_schema(new_schema), msg); \ REQUIRE(realm->schema_version() == 0); \ - REQUIRE_THROWS(realm->update_schema(new_schema, 1, [](SharedRealm, SharedRealm, Schema&) {})); \ + REQUIRE_THROWS_CONTAINING(realm->update_schema(new_schema, 1, [](SharedRealm, SharedRealm, Schema&) {}), \ + msg); \ REQUIRE(realm->schema_version() == 0); \ REQUIRE_NOTHROW(realm->update_schema(new_schema, 1, migration)); \ REQUIRE(realm->schema_version() == 1); \ } while (false) SECTION("add new table") { - REQUIRE_MIGRATION(add_table(schema, {"new table", - { - {"value", PropertyType::Int}, - }}), - [](SharedRealm, SharedRealm realm, Schema&) { - realm->read_group().add_table("class_new table")->add_column(type_Int, "value"); - }); + REQUIRE_MIGRATION( + add_table(schema, {"new table", {{"value", PropertyType::Int}}}), + [](SharedRealm, SharedRealm realm, Schema&) { + realm->read_group().add_table("class_new table")->add_column(type_Int, "value"); + }, + "Class 'new table' has been added."); } SECTION("add property to table") { - REQUIRE_MIGRATION(add_property(schema, "object", {"new", PropertyType::Int}), - [&](SharedRealm, SharedRealm realm, Schema&) { - get_table(realm, "object")->add_column(type_Int, "new"); - }); + REQUIRE_MIGRATION( + add_property(schema, "object", {"new", PropertyType::Int}), + [&](SharedRealm, SharedRealm realm, Schema&) { + get_table(realm, "object")->add_column(type_Int, "new"); + }, + "Property 'object.new' has been added."); } SECTION("remove property from table") { - REQUIRE_MIGRATION(remove_property(schema, "object", "value"), [&](SharedRealm, SharedRealm realm, Schema&) { - get_table(realm, "object")->remove_column(col_keys[1]); - }); + REQUIRE_MIGRATION( + remove_property(schema, "object", "value"), + [&](SharedRealm, SharedRealm realm, Schema&) { + get_table(realm, "object")->remove_column(col_keys[1]); + }, + "Property 'object.value' has been removed."); } SECTION("add primary key to table") { - REQUIRE_MIGRATION(set_primary_key(schema, "link origin", "not a pk"), - [&](SharedRealm, SharedRealm realm, Schema&) { - auto table = get_table(realm, "link origin"); - table->set_primary_key_column(table->get_column_key("not a pk")); - }); + REQUIRE_MIGRATION( + set_primary_key(schema, "link origin", "not a pk"), + [&](SharedRealm, SharedRealm realm, Schema&) { + auto table = get_table(realm, "link origin"); + table->set_primary_key_column(table->get_column_key("not a pk")); + }, + "Primary Key for class 'link origin' has been added."); } SECTION("remove primary key from table") { - REQUIRE_MIGRATION(set_primary_key(schema, "object", ""), [&](SharedRealm, SharedRealm realm, Schema&) { - get_table(realm, "object")->set_primary_key_column({}); - }); + REQUIRE_MIGRATION( + set_primary_key(schema, "object", ""), + [&](SharedRealm, SharedRealm realm, Schema&) { + get_table(realm, "object")->set_primary_key_column({}); + }, + "Primary Key for class 'object' has been removed."); } SECTION("change primary key") { - REQUIRE_MIGRATION(set_primary_key(schema, "object", "value"), [&](SharedRealm, SharedRealm realm, Schema&) { - get_table(realm, "object")->set_primary_key_column(col_keys[1]); - }); + REQUIRE_MIGRATION( + set_primary_key(schema, "object", "value"), + [&](SharedRealm, SharedRealm realm, Schema&) { + get_table(realm, "object")->set_primary_key_column(col_keys[1]); + }, + "Primary Key for class 'object' has changed from 'pk' to 'value'."); } SECTION("change property type") { - REQUIRE_MIGRATION(set_type(schema, "object", "value", PropertyType::Date), - [&](SharedRealm, SharedRealm realm, Schema&) { - auto table = get_table(realm, "object"); - table->remove_column(col_keys[1]); - auto col = table->add_column(type_Timestamp, "value"); - table->add_search_index(col); - }); + REQUIRE_MIGRATION( + set_type(schema, "object", "value", PropertyType::Date), + [&](SharedRealm, SharedRealm realm, Schema&) { + auto table = get_table(realm, "object"); + table->remove_column(col_keys[1]); + auto col = table->add_column(type_Timestamp, "value"); + table->add_search_index(col); + }, + "Property 'object.value' has been changed from 'int' to 'date'."); } SECTION("change link target") { - REQUIRE_MIGRATION(set_target(schema, "link origin", "object", "link origin"), - [&](SharedRealm, SharedRealm realm, Schema&) { - auto table = get_table(realm, "link origin"); - table->remove_column(table->get_column_keys()[1]); - table->add_column(*table, "object"); - }); + REQUIRE_MIGRATION( + set_target(schema, "link origin", "object", "link origin"), + [&](SharedRealm, SharedRealm realm, Schema&) { + auto table = get_table(realm, "link origin"); + table->remove_column(table->get_column_keys()[1]); + table->add_column(*table, "object"); + }, + "Property 'link origin.object' has been changed from '' to ''."); } SECTION("change linklist target") { - REQUIRE_MIGRATION(set_target(schema, "link origin", "array", "link origin"), - [&](SharedRealm, SharedRealm realm, Schema&) { - auto table = get_table(realm, "link origin"); - table->remove_column(table->get_column_keys()[2]); - table->add_column_list(*table, "array"); - }); + REQUIRE_MIGRATION( + set_target(schema, "link origin", "array", "link origin"), + [&](SharedRealm, SharedRealm realm, Schema&) { + auto table = get_table(realm, "link origin"); + table->remove_column(table->get_column_keys()[2]); + table->add_column_list(*table, "array"); + }, + "Property 'link origin.array' has been changed from 'array' to 'array'."); } SECTION("make property optional") { - REQUIRE_MIGRATION(set_optional(schema, "object", "value", true), - [&](SharedRealm, SharedRealm realm, Schema&) { - auto table = get_table(realm, "object"); - table->remove_column(col_keys[1]); - auto col = table->add_column(type_Int, "value", true); - table->add_search_index(col); - }); + REQUIRE_MIGRATION( + set_optional(schema, "object", "value", true), + [&](SharedRealm, SharedRealm realm, Schema&) { + auto table = get_table(realm, "object"); + table->remove_column(col_keys[1]); + auto col = table->add_column(type_Int, "value", true); + table->add_search_index(col); + }, + "Property 'object.value' has been made optional."); } SECTION("make property required") { - REQUIRE_MIGRATION(set_optional(schema, "object", "optional", false), - [&](SharedRealm, SharedRealm realm, Schema&) { - auto table = get_table(realm, "object"); - table->remove_column(col_keys[2]); - table->add_column(type_Int, "optional", false); - }); + REQUIRE_MIGRATION( + set_optional(schema, "object", "optional", false), + [&](SharedRealm, SharedRealm realm, Schema&) { + auto table = get_table(realm, "object"); + table->remove_column(col_keys[2]); + table->add_column(type_Int, "optional", false); + }, + "Property 'object.optional' has been made required."); } SECTION("add index") { - REQUIRE_MIGRATION(set_indexed(schema, "object", "optional", true), - [&](SharedRealm, SharedRealm realm, Schema&) { - get_table(realm, "object")->add_search_index(col_keys[2]); - }); + REQUIRE_MIGRATION( + set_indexed(schema, "object", "optional", true), + [&](SharedRealm, SharedRealm realm, Schema&) { + get_table(realm, "object")->add_search_index(col_keys[2]); + }, + "Property 'object.optional' has been made indexed."); } SECTION("remove index") { - REQUIRE_MIGRATION(set_indexed(schema, "object", "value", false), - [&](SharedRealm, SharedRealm realm, Schema&) { - get_table(realm, "object")->remove_search_index(col_keys[1]); - }); + REQUIRE_MIGRATION( + set_indexed(schema, "object", "value", false), + [&](SharedRealm, SharedRealm realm, Schema&) { + get_table(realm, "object")->remove_search_index(col_keys[1]); + }, + "Property 'object.value' has been made unindexed."); } SECTION("reorder properties") { auto schema2 = schema; @@ -3438,7 +2689,8 @@ TEST_CASE("migration: Manual") { SECTION("cannot lower schema version") { REQUIRE_NOTHROW(realm->update_schema(schema, 1, [](SharedRealm, SharedRealm, Schema&) {})); REQUIRE(realm->schema_version() == 1); - REQUIRE_THROWS(realm->update_schema(schema, 0, [](SharedRealm, SharedRealm, Schema&) {})); + REQUIRE_THROWS_CONTAINING(realm->update_schema(schema, 0, [](SharedRealm, SharedRealm, Schema&) {}), + "Provided schema version 0 is less than last set version 1."); REQUIRE(realm->schema_version() == 1); } @@ -3448,7 +2700,8 @@ TEST_CASE("migration: Manual") { auto realm2 = Realm::get_shared_realm(config); // will deadlock if it tries to start a write transaction REQUIRE_NOTHROW(realm2->update_schema(schema)); - REQUIRE_THROWS(realm2->update_schema(remove_property(schema, "object", "value"))); + REQUIRE_THROWS_CONTAINING(realm2->update_schema(remove_property(schema, "object", "value")), + "Property 'object.value' has been removed."); } SECTION("null migration callback should throw SchemaMismatchException") { @@ -3456,111 +2709,3 @@ TEST_CASE("migration: Manual") { REQUIRE_THROWS_AS(realm->update_schema(new_schema, 1, nullptr), SchemaMismatchException); } } - -#if REALM_ENABLE_AUTH_TESTS - -TEST_CASE("migrations with asymmetric tables") { - realm::app::FLXSyncTestHarness harness("asymmetric_sync_migrations"); - SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{}); - config.automatic_change_notifications = false; - - SECTION("migration: Automatic") { - config.schema_mode = SchemaMode::Automatic; - - SECTION("add asymmetric object schema") { - auto realm = Realm::get_shared_realm(config); - - Schema schema1 = {}; - Schema schema2 = add_table(schema1, {"object", - ObjectType::TopLevelAsymmetric, - {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, - {"value", PropertyType::Int}}}); - Schema schema3 = - add_table(schema2, {"object2", - ObjectType::TopLevelAsymmetric, - {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, - {"link", PropertyType::Object | PropertyType::Array, "embedded2"}}}); - schema3 = add_table(schema3, {"embedded2", ObjectType::Embedded, {{"value", PropertyType::Int}}}); - REQUIRE_UPDATE_SUCCEEDS(*realm, schema1, 1); - REQUIRE_UPDATE_SUCCEEDS(*realm, schema2, 1); - REQUIRE_UPDATE_SUCCEEDS(*realm, schema3, 1); - } - - SECTION("cannot change table from top-level to top-level asymmetric without version bump") { - auto realm = Realm::get_shared_realm(config); - - Schema schema = { - {"object", - { - {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, - {"value", PropertyType::Int}, - }}, - }; - REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); - REQUIRE_THROWS_CONTAINING( - realm->update_schema(set_table_type(schema, "object", ObjectType::TopLevelAsymmetric), 1), - "Class 'object' has been changed from TopLevel to TopLevelAsymmetric."); - } - - SECTION("cannot change table from top-level asymmetric to top-level without version bump") { - auto realm = Realm::get_shared_realm(config); - - Schema schema = { - {"object", - ObjectType::TopLevelAsymmetric, - { - {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, - {"value", PropertyType::Int}, - }}, - }; - REQUIRE_UPDATE_SUCCEEDS(*realm, schema, 1); - REQUIRE_THROWS_CONTAINING(realm->update_schema(set_table_type(schema, "object", ObjectType::TopLevel), 1), - "Class 'object' has been changed from TopLevelAsymmetric to TopLevel."); - } - - SECTION("cannot change empty table from top-level to top-level asymmetric") { - Schema schema = { - {"table", - { - {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, - {"value", PropertyType::Int}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "table"); - REQUIRE(child_table->get_table_type() == Table::Type::TopLevel); - - REQUIRE_THROWS_CONTAINING( - realm->update_schema(set_table_type(schema, "table", ObjectType::TopLevelAsymmetric), 2, nullptr), - "Cannot change 'class_table' to/from asymmetric."); - - REQUIRE(realm->schema_version() == 1); - REQUIRE(child_table->get_table_type() == Table::Type::TopLevel); - } - - SECTION("cannot change empty table from top-level asymmetric to top-level") { - Schema schema = { - {"table", - ObjectType::TopLevelAsymmetric, - { - {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, - {"value", PropertyType::Int}, - }}, - }; - auto realm = Realm::get_shared_realm(config); - realm->update_schema(schema, 1); - auto child_table = ObjectStore::table_for_object_type(realm->read_group(), "table"); - REQUIRE(child_table->get_table_type() == Table::Type::TopLevelAsymmetric); - - REQUIRE_THROWS_CONTAINING( - realm->update_schema(set_table_type(schema, "table", ObjectType::TopLevel), 2, nullptr), - "Cannot change 'class_table' to/from asymmetric."); - - REQUIRE(realm->schema_version() == 1); - REQUIRE(child_table->get_table_type() == Table::Type::TopLevelAsymmetric); - } - } -} - -#endif // REALM_ENABLE_AUTH_TESTS diff --git a/test/object-store/util/test_file.cpp b/test/object-store/util/test_file.cpp index 14087bdeed7..3d48fe514b0 100644 --- a/test/object-store/util/test_file.cpp +++ b/test/object-store/util/test_file.cpp @@ -235,7 +235,9 @@ static std::error_code wait_for_session(Realm& realm, bool completed = cv.wait_for(lock, timeout, [&]() { return wait_flag == true; }); - REALM_ASSERT_RELEASE(completed); + if (!completed) { + throw std::runtime_error("wait_For_session() timed out"); + } return ec; } diff --git a/test/test_group.cpp b/test/test_group.cpp index e9e96cbfbb3..04a46f50d54 100644 --- a/test/test_group.cpp +++ b/test/test_group.cpp @@ -1342,9 +1342,9 @@ TEST(Group_ChangeEmbeddedness) p2.set(col, obj2.get_key()); // obj3 has no owner, so we can't make the table embedded - std::string message; - CHECK_THROW_ANY_GET_MESSAGE(t->set_table_type(Table::Type::Embedded), message); - CHECK_EQUAL(message, "At least one object in 'table' does not have a backlink (data would get lost)."); + CHECK_THROW_CONTAINING_MESSAGE( + t->set_table_type(Table::Type::Embedded), + "Cannot convert 'table' to embedded: at least one object has no incoming links and would be deleted."); CHECK_NOT(t->is_embedded()); // Now it has owner @@ -1358,11 +1358,481 @@ TEST(Group_ChangeEmbeddedness) // Now obj2 has 2 parents CHECK_EQUAL(obj2.get_backlink_count(), 2); - CHECK_THROW_ANY_GET_MESSAGE(t->set_table_type(Table::Type::Embedded), message); - CHECK_EQUAL(message, "At least one object in 'table' does have multiple backlinks."); + CHECK_THROW_CONTAINING_MESSAGE( + t->set_table_type(Table::Type::Embedded), + "Cannot convert 'table' to embedded: at least one object has more than one incoming link."); CHECK_NOT(t->is_embedded()); } +TEST(Group_MakeEmbedded_PrimaryKey) +{ + Group g; + TableRef t = g.add_table_with_primary_key("child", type_Int, "_id"); + CHECK_THROW_CONTAINING_MESSAGE(t->set_table_type(Table::Type::Embedded), + "Cannot change 'child' to embedded when using a primary key."); + CHECK_NOT(t->is_embedded()); +} + +TEST(Group_MakeEmbedded_NoIncomingLinks_Empty) +{ + Group g; + TableRef t = g.add_table("child"); + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 0); +} + +TEST(Group_MakeEmbedded_NoIncomingLinks_Nonempty) +{ + Group g; + TableRef t = g.add_table("child"); + auto child_obj = t->create_object(); + CHECK_THROW_CONTAINING_MESSAGE( + t->set_table_type(Table::Type::Embedded), + "Cannot convert 'child' to embedded: at least one object has no incoming links and would be deleted."); + CHECK_NOT(t->is_embedded()); + + // Add an incoming link and it should now work + TableRef parent = g.add_table("parent"); + parent->add_column(*t, "child"); + parent->create_object().set_all(child_obj.get_key()); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 1); +} + +TEST(Group_MakeEmbedded_MultipleIncomingLinks_OneProperty) +{ + Group g; + TableRef t = g.add_table("child"); + TableRef parent = g.add_table("parent"); + + auto child_obj = t->create_object(); + + parent->add_column(*t, "child"); + parent->create_object().set_all(child_obj.get_key()); + parent->create_object().set_all(child_obj.get_key()); + + CHECK_THROW_CONTAINING_MESSAGE( + t->set_table_type(Table::Type::Embedded), + "Cannot convert 'child' to embedded: at least one object has more than one incoming link."); + CHECK_NOT(t->is_embedded()); + + // Should work after deleting one of the incoming links + parent->begin()->remove(); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 1); +} + +TEST(Group_MakeEmbedded_MultipleIncomingLinks_MultipleProperties) +{ + Group g; + TableRef t = g.add_table("child"); + TableRef parent = g.add_table("parent"); + + auto child_obj = t->create_object(); + + parent->add_column(*t, "link 1"); + parent->add_column(*t, "link 2"); + parent->create_object().set_all(child_obj.get_key(), child_obj.get_key()); + + CHECK_THROW_CONTAINING_MESSAGE( + t->set_table_type(Table::Type::Embedded), + "Cannot convert 'child' to embedded: at least one object has more than one incoming link."); + CHECK_NOT(t->is_embedded()); + + // Should work after deleting one of the incoming links + parent->begin()->set_all(ObjKey()); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 1); +} + +TEST(Group_MakeEmbedded_IncomingMixed_Single) +{ + Group g; + TableRef t = g.add_table("child"); + auto child_obj = t->create_object(); + + TableRef parent = g.add_table("parent"); + parent->add_column(type_Mixed, "mixed"); + parent->add_column(*t, "link"); + parent->create_object().set_all(Mixed(child_obj.get_link()), child_obj.get_key()); + + CHECK_THROW_CONTAINING_MESSAGE(t->set_table_type(Table::Type::Embedded), + "Cannot convert 'child' to embedded: there is an incoming link from the Mixed " + "property 'parent.mixed', which does not support linking to embedded objects."); + CHECK_NOT(t->is_embedded()); + + // Should work after removing the Mixed link + parent->begin()->set_all(Mixed()); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 1); +} + +TEST(Group_MakeEmbedded_IncomingMixed_List) +{ + Group g; + TableRef t = g.add_table("child"); + auto child_obj = t->create_object(); + + TableRef parent = g.add_table("parent"); + parent->add_column(*t, "link"); + auto col = parent->add_column_list(type_Mixed, "mixed"); + auto obj = parent->create_object().set_all(child_obj.get_key()); + obj.get_list(col).add(child_obj.get_link()); + + CHECK_THROW_CONTAINING_MESSAGE(t->set_table_type(Table::Type::Embedded), + "Cannot convert 'child' to embedded: there is an incoming link from the Mixed " + "property 'parent.mixed', which does not support linking to embedded objects."); + CHECK_NOT(t->is_embedded()); + + // Should work after removing the Mixed link + obj.get_list(col).clear(); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 1); +} + +TEST(Group_MakeEmbedded_IncomingMixed_Set) +{ + Group g; + TableRef t = g.add_table("child"); + auto child_obj = t->create_object(); + + TableRef parent = g.add_table("parent"); + parent->add_column(*t, "link"); + auto col = parent->add_column_set(type_Mixed, "mixed"); + auto obj = parent->create_object().set_all(child_obj.get_key()); + obj.get_set(col).insert(child_obj.get_link()); + + CHECK_THROW_CONTAINING_MESSAGE(t->set_table_type(Table::Type::Embedded), + "Cannot convert 'child' to embedded: there is an incoming link from the Mixed " + "property 'parent.mixed', which does not support linking to embedded objects."); + CHECK_NOT(t->is_embedded()); + + // Should work after removing the Mixed link + obj.get_set(col).clear(); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 1); +} + +TEST(Group_MakeEmbedded_IncomingMixed_Dictionary) +{ + Group g; + TableRef t = g.add_table("child"); + auto child_obj = t->create_object(); + + TableRef parent = g.add_table("parent"); + parent->add_column(*t, "link"); + auto col = parent->add_column_dictionary(type_Mixed, "mixed"); + auto obj = parent->create_object().set_all(child_obj.get_key()); + obj.get_dictionary(col).insert("foo", child_obj.get_link()); + + CHECK_THROW_CONTAINING_MESSAGE(t->set_table_type(Table::Type::Embedded), + "Cannot convert 'child' to embedded: there is an incoming link from the Mixed " + "property 'parent.mixed', which does not support linking to embedded objects."); + CHECK_NOT(t->is_embedded()); + + // Should work after removing the Mixed link + obj.get_dictionary(col).clear(); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded)); + CHECK(t->is_embedded()); + CHECK(t->size() == 1); +} + +TEST(Group_MakeEmbedded_DeleteOrphans) +{ + Group g; + TableRef t = g.add_table("child"); + auto col = t->add_column(type_Int, "value"); + std::vector children; + t->create_objects(10000, children); + + TableRef parent = g.add_table("parent"); + parent->add_column(*t, "link"); + for (int i = 0; i < 100; ++i) { + t->get_object(children[i * 100]).set_all(i + 1); + parent->create_object().set_all(children[i * 100]); + } + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded, true)); + CHECK(t->is_embedded()); + CHECK(t->size() == 100); + for (int i = 0; i < 100; ++i) { + Obj obj; + if (CHECK_NOTHROW(obj = t->get_object(children[i * 100]))) { + CHECK_EQUAL(obj.get(col), i + 1); + } + } +} + +TEST(Group_MakeEmbedded_DuplicateObjectsForMultipleParentObjects) +{ + Group g; + TableRef t = g.add_table("child"); + auto col = t->add_column(type_Int, "value"); + auto child = t->create_object().set_all(10); + + TableRef parent = g.add_table("parent"); + auto link_col = parent->add_column(*t, "link"); + for (int i = 0; i < 10000; ++i) { + parent->create_object().set_all(child.get_key()); + } + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded, true)); + CHECK(t->is_embedded()); + CHECK(t->size() == 10000); + CHECK(parent->size() == 10000); + CHECK_NOT(child.is_valid()); // original object should have been deleted + + for (auto parent_obj : *parent) { + CHECK_EQUAL(t->get_object(parent_obj.get(link_col)).get(col), 10); + } +} + +TEST(Group_MakeEmbedded_DuplicateObjectsForMultipleColumns) +{ + Group g; + TableRef t = g.add_table("child"); + auto col = t->add_column(type_Int, "value"); + auto child = t->create_object().set_all(10); + + TableRef parent = g.add_table("parent"); + auto parent_obj = parent->create_object(); + for (int i = 0; i < 10; ++i) { + auto col = parent->add_column(*t, util::format("link %1", i)); + parent_obj.set(col, child.get_key()); + } + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded, true)); + CHECK(t->is_embedded()); + CHECK(t->size() == 10); + CHECK(parent->size() == 1); + CHECK_NOT(child.is_valid()); // original object should have been deleted + + for (auto link_col : parent->get_column_keys()) { + CHECK_EQUAL(t->get_object(parent_obj.get(link_col)).get(col), 10); + } +} + +TEST(Group_MakeEmbedded_DuplicateObjectsForList) +{ + Group g; + TableRef t = g.add_table("child"); + auto col = t->add_column(type_Int, "value"); + auto child = t->create_object().set_all(10); + + TableRef parent = g.add_table("parent"); + auto parent_obj = parent->create_object(); + auto list = parent_obj.get_linklist(parent->add_column_list(*t, "link")); + for (int i = 0; i < 10; ++i) + list.add(child.get_key()); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded, true)); + CHECK(t->is_embedded()); + CHECK(t->size() == 10); + CHECK(parent->size() == 1); + CHECK_NOT(child.is_valid()); // original object should have been deleted + + CHECK_EQUAL(list.size(), 10); + for (int i = 0; i < 10; ++i) + CHECK_EQUAL(t->get_object(list.get(i)).get(col), 10); +} + +TEST(Group_MakeEmbedded_DuplicateObjectsForDictionary) +{ + Group g; + TableRef t = g.add_table("child"); + auto col = t->add_column(type_Int, "value"); + auto child = t->create_object().set_all(10); + + TableRef parent = g.add_table("parent"); + auto parent_obj = parent->create_object(); + auto dict = parent_obj.get_dictionary(parent->add_column_dictionary(*t, "link")); + for (int i = 0; i < 10; ++i) + dict.insert(util::to_string(i), child.get_link()); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded, true)); + CHECK(t->is_embedded()); + CHECK(t->size() == 10); + CHECK(parent->size() == 1); + CHECK_NOT(child.is_valid()); // original object should have been deleted + + CHECK_EQUAL(dict.size(), 10); + for (auto [key, value] : dict) + CHECK_EQUAL(g.get_object(value.get_link()).get(col), 10); +} + +TEST(Group_MakeEmbedded_DeepCopy) +{ + Group g; + TableRef t = g.add_table("table"); + auto obj = t->create_object(); + + // Single/List/Set/Dictionary of a primitive value + obj.set(t->add_column(type_Int, "int"), 1); + auto int_list = obj.get_list(t->add_column_list(type_Int, "int list")); + int_list.add(1); + int_list.add(2); + int_list.add(3); + auto int_set = obj.get_set(t->add_column_set(type_Int, "int set")); + int_set.insert(4); + int_set.insert(5); + int_set.insert(6); + auto int_dict = obj.get_dictionary(t->add_column_dictionary(type_Int, "int dict")); + int_dict.insert("a", 7); + int_dict.insert("b", 8); + int_dict.insert("c", 9); + + // Single/List/Set/Dictionary of a link to a top-level object + TableRef target = g.add_table("target"); + target->add_column(type_Int, "value"); + auto target_obj1 = target->create_object().set_all(123); + auto target_obj2 = target->create_object().set_all(456); + auto target_obj3 = target->create_object().set_all(789); + auto target_objkey1 = target_obj1.get_key(); + auto target_objkey2 = target_obj2.get_key(); + auto target_objkey3 = target_obj3.get_key(); + + obj.set(t->add_column(*target, "link"), target_objkey1); + auto obj_list = obj.get_linklist(t->add_column_list(*target, "obj list")); + obj_list.add(target_objkey1); + obj_list.add(target_objkey2); + obj_list.add(target_objkey3); + obj_list.add(target_objkey1); + obj_list.add(target_objkey2); + obj_list.add(target_objkey3); + auto obj_set = obj.get_linkset(t->add_column_set(*target, "obj set")); + obj_set.insert(target_objkey1); + obj_set.insert(target_objkey2); + obj_set.insert(target_objkey3); + auto obj_dict = obj.get_dictionary(t->add_column_dictionary(*target, "obj dict")); + obj_dict.insert("a", target_objkey1); + obj_dict.insert("b", target_objkey2); + obj_dict.insert("c", target_objkey3); + obj_dict.insert("d", target_objkey1); + obj_dict.insert("e", target_objkey2); + obj_dict.insert("f", target_objkey3); + + // Single/List/Dictionary of a link to an embedded object + TableRef embedded = g.add_table("embedded", Table::Type::Embedded); + embedded->add_column(type_Int, "value"); + + obj.create_and_set_linked_object(t->add_column(*embedded, "embedded link")).set_all(1); + auto embedded_list = obj.get_linklist(t->add_column_list(*embedded, "embedded list")); + embedded_list.create_and_insert_linked_object(0).set_all(2); + embedded_list.create_and_insert_linked_object(1).set_all(3); + embedded_list.create_and_insert_linked_object(2).set_all(4); + auto embedded_dict = obj.get_dictionary(t->add_column_dictionary(*embedded, "embedded dict")); + embedded_dict.create_and_insert_linked_object("a").set_all(5); + embedded_dict.create_and_insert_linked_object("b").set_all(6); + embedded_dict.create_and_insert_linked_object("c").set_all(7); + + // Parent object type for the table which'll be made embedded + auto parent = g.add_table("parent"); + parent->add_column(*t, "link"); + parent->create_object().set_all(obj.get_key()); + parent->create_object().set_all(obj.get_key()); + + // Pre-conversion sanity check + CHECK_EQUAL(t->size(), 1); + CHECK_EQUAL(target->size(), 3); + CHECK_EQUAL(embedded->size(), 7); + CHECK_EQUAL(parent->get_object(0).get("link"), parent->get_object(1).get("link")); + + CHECK_NOTHROW(t->set_table_type(Table::Type::Embedded, true)); + + CHECK_EQUAL(t->size(), 2); + CHECK_EQUAL(target->size(), 3); + CHECK_EQUAL(embedded->size(), 14); + CHECK_NOT_EQUAL(parent->get_object(0).get("link"), parent->get_object(1).get("link")); + + // The original objects should be deleted, except for target since that's still top-level + CHECK_NOT(obj.is_valid()); + CHECK(target_obj1.is_valid()); + CHECK(target_obj2.is_valid()); + CHECK(target_obj3.is_valid()); + CHECK_NOT(int_list.is_attached()); + CHECK_NOT(int_set.is_attached()); + CHECK_NOT(int_dict.is_attached()); + CHECK_NOT(obj_list.is_attached()); + CHECK_NOT(obj_set.is_attached()); + CHECK_NOT(obj_dict.is_attached()); + CHECK_NOT(embedded_list.is_attached()); + CHECK_NOT(embedded_dict.is_attached()); + + for (auto parent_obj : *parent) { + auto obj = parent_obj.get_linked_object("link"); + CHECK_EQUAL(obj.get("int"), 1); + + auto int_list = obj.get_list("int list"); + CHECK_EQUAL(int_list.size(), 3); + CHECK_EQUAL(int_list.get(0), 1); + CHECK_EQUAL(int_list.get(1), 2); + CHECK_EQUAL(int_list.get(2), 3); + + auto int_set = obj.get_set("int set"); + CHECK_EQUAL(int_set.size(), 3); + CHECK_NOT_EQUAL(int_set.find(4), -1); + CHECK_NOT_EQUAL(int_set.find(5), -1); + CHECK_NOT_EQUAL(int_set.find(6), -1); + + auto int_dict = obj.get_dictionary("int dict"); + CHECK_EQUAL(int_dict.size(), 3); + CHECK_EQUAL(int_dict.get("a"), 7); + CHECK_EQUAL(int_dict.get("b"), 8); + CHECK_EQUAL(int_dict.get("c"), 9); + + CHECK_EQUAL(obj.get("link"), target_objkey1); + + auto obj_list = obj.get_linklist("obj list"); + CHECK_EQUAL(obj_list.size(), 6); + CHECK_EQUAL(obj_list.get(0), target_objkey1); + CHECK_EQUAL(obj_list.get(1), target_objkey2); + CHECK_EQUAL(obj_list.get(2), target_objkey3); + CHECK_EQUAL(obj_list.get(3), target_objkey1); + CHECK_EQUAL(obj_list.get(4), target_objkey2); + CHECK_EQUAL(obj_list.get(5), target_objkey3); + + auto obj_set = obj.get_linkset("obj set"); + CHECK_EQUAL(obj_set.size(), 3); + CHECK_NOT_EQUAL(obj_set.find(target_objkey1), -1); + CHECK_NOT_EQUAL(obj_set.find(target_objkey2), -1); + CHECK_NOT_EQUAL(obj_set.find(target_objkey3), -1); + + auto obj_dict = obj.get_dictionary("obj dict"); + CHECK_EQUAL(obj_dict.size(), 6); + CHECK_EQUAL(obj_dict.get("a"), target_obj1.get_link()); + CHECK_EQUAL(obj_dict.get("b"), target_obj2.get_link()); + CHECK_EQUAL(obj_dict.get("c"), target_obj3.get_link()); + CHECK_EQUAL(obj_dict.get("d"), target_obj1.get_link()); + CHECK_EQUAL(obj_dict.get("e"), target_obj2.get_link()); + CHECK_EQUAL(obj_dict.get("f"), target_obj3.get_link()); + + CHECK_EQUAL(obj.get_linked_object("embedded link").get("value"), 1); + + auto embedded_list = obj.get_linklist("embedded list"); + CHECK_EQUAL(embedded_list[0].get("value"), 2); + CHECK_EQUAL(embedded_list[1].get("value"), 3); + CHECK_EQUAL(embedded_list[2].get("value"), 4); + + auto embedded_dict = obj.get_dictionary("embedded dict"); + CHECK_EQUAL(embedded_dict.get_object("a").get("value"), 5); + CHECK_EQUAL(embedded_dict.get_object("b").get("value"), 6); + CHECK_EQUAL(embedded_dict.get_object("c").get("value"), 7); + } +} TEST(Group_WriteEmpty) { diff --git a/test/test_string_data.cpp b/test/test_string_data.cpp index 92c28f3ead6..d5748908a6a 100644 --- a/test/test_string_data.cpp +++ b/test/test_string_data.cpp @@ -261,7 +261,6 @@ TEST(StringData_Like) StringData empty(""); StringData f("f"); StringData foo("foo"); - StringData bar("bar"); StringData foobar("foobar"); StringData foofoo("foofoo"); StringData foobarfoo("foobarfoo"); @@ -319,7 +318,6 @@ TEST(StringData_Like_CaseInsensitive) StringData empty(""); StringData f("f"); StringData foo("FoO"); - StringData bar("bAr"); StringData foobar("FOOBAR"); StringData foofoo("FOOfoo"); StringData foobarfoo("FoObArFoO"); diff --git a/test/test_table.cpp b/test/test_table.cpp index e4b2b8e1702..43667ec55fc 100644 --- a/test/test_table.cpp +++ b/test/test_table.cpp @@ -689,7 +689,6 @@ TEST(Table_AggregateFuzz) ObjKey key; size_t cnt; int64_t i; - Timestamp ts; Mixed m; // Test methods on Table diff --git a/test/util/unit_test.cpp b/test/util/unit_test.cpp index 6c158eab6c4..00b545d128a 100644 --- a/test/util/unit_test.cpp +++ b/test/util/unit_test.cpp @@ -909,6 +909,14 @@ void TestContext::throw_any_failed(const char* file, long line, const char* expr check_failed(file, line, out.str()); } +bool TestContext::check_string_contains(std::string_view a, std::string_view b, const char* file, long line, + const char* a_text, const char* b_text) +{ + bool cond = a.find(b) != a.npos; + return check_compare(cond, a, b, file, line, "CHECK_STRING_CONTAINS", a_text, b_text); +} + + namespace { std::locale locale_classic = std::locale::classic(); } diff --git a/test/util/unit_test.hpp b/test/util/unit_test.hpp index 0b9b21fbb5b..3ed83e2fd4e 100644 --- a/test/util/unit_test.hpp +++ b/test/util/unit_test.hpp @@ -89,6 +89,8 @@ #define CHECK_GREATER_EQUAL(a, b) test_context.check_greater_equal((a), (b), __FILE__, __LINE__, #a, #b) +#define CHECK_STRING_CONTAINS(a, b) test_context.check_string_contains((a), (b), __FILE__, __LINE__, #a, #b) + #define CHECK_OR_RETURN(cond) \ do { \ if (!CHECK(cond)) \ @@ -144,12 +146,23 @@ (expr); \ test_context.throw_any_failed(__FILE__, __LINE__, #expr); \ } \ - catch (std::exception & e) { \ + catch (const std::exception& e) { \ test_context.check_succeeded(); \ message = e.what(); \ } \ }()) +#define CHECK_THROW_CONTAINING_MESSAGE(expr, message) \ + ([&] { \ + try { \ + (expr); \ + test_context.throw_any_failed(__FILE__, __LINE__, #expr); \ + } \ + catch (const std::exception& e) { \ + CHECK_STRING_CONTAINS(e.what(), message); \ + } \ + }()) + #define CHECK_NOTHROW(expr) \ ([&] { \ try { \ @@ -505,6 +518,9 @@ class TestContext { bool check_greater_equal(const A& a, const B& b, const char* file, long line, const char* a_text, const char* b_text); + bool check_string_contains(std::string_view a, std::string_view b, const char* file, long line, + const char* a_text, const char* b_text); + bool check_approximately_equal(long double a, long double b, long double eps, const char* file, long line, const char* a_text, const char* b_text, const char* eps_text);