diff --git a/bindgen/spec.yml b/bindgen/spec.yml index 91fac6394fa..80304576ca2 100644 --- a/bindgen/spec.yml +++ b/bindgen/spec.yml @@ -440,6 +440,9 @@ records: schema_mode: type: SchemaMode default: SchemaMode::Automatic + flexible_schema: + type: bool + default: false disable_format_upgrade: type: bool default: false diff --git a/src/realm.h b/src/realm.h index 68e39b80111..d043fb8b3e8 100644 --- a/src/realm.h +++ b/src/realm.h @@ -925,6 +925,11 @@ RLM_API bool realm_config_get_cached(realm_config_t*) RLM_API_NOEXCEPT; */ RLM_API void realm_config_set_automatic_backlink_handling(realm_config_t*, bool) RLM_API_NOEXCEPT; +/** + * Allow realm objects in the realm to have additional properties that are not defined in the schema. + */ +RLM_API void realm_config_set_flexible_schema(realm_config_t*, bool) RLM_API_NOEXCEPT; + /** * Create a custom scheduler object from callback functions. * @@ -1640,6 +1645,13 @@ RLM_API realm_object_t* realm_object_from_thread_safe_reference(const realm_t*, */ RLM_API bool realm_get_value(const realm_object_t*, realm_property_key_t, realm_value_t* out_value); +/** + * Get the value for a property. + * + * @return True if no exception occurred. + */ +RLM_API bool realm_get_value_by_name(const realm_object_t*, const char* property_name, realm_value_t* out_value); + /** * Get the values for several properties. * @@ -1675,6 +1687,41 @@ RLM_API bool realm_get_values(const realm_object_t*, size_t num_values, const re */ RLM_API bool realm_set_value(realm_object_t*, realm_property_key_t, realm_value_t new_value, bool is_default); +/** + * Set the value for a property. Property need not be defined in schema if flexible + * schema is enabled in configuration + * + * @param property_name The name of the property. + * @param new_value The new value for the property. + * @return True if no exception occurred. + */ +RLM_API bool realm_set_value_by_name(realm_object_t*, const char* property_name, realm_value_t new_value); + +/** + * Examines if the object has a property with the given name. + * @param out_has_property will be true if the property exists. + * @return True if no exception occurred. + */ +RLM_API bool realm_has_property(realm_object_t*, const char* property_name, bool* out_has_property); + +/** + * Get a list of properties set on the object that are not defined in the schema. + * + * @param out_prop_names A pointer to an array of const char* of size @a max. If the pointer is NULL, + * no names will be copied, but @a out_n will be set to the required size. + * @param max size of @a out_prop_names + * @param out_n number of names actually returned. + */ +RLM_API void realm_get_additional_properties(realm_object_t*, const char** out_prop_names, size_t max, size_t* out_n); + +/** + * Erases a property from an object. You can't erase a property that is defined in the current schema. + * + * @param property_name The name of the property. + * @return True if the property was removed. + */ +RLM_API bool realm_erase_additional_property(realm_object_t*, const char* property_name); + /** * Assign a JSON formatted string to a Mixed property. Underlying structures will be created as needed * @@ -1696,6 +1743,8 @@ RLM_API realm_object_t* realm_set_embedded(realm_object_t*, realm_property_key_t */ RLM_API realm_list_t* realm_set_list(realm_object_t*, realm_property_key_t); RLM_API realm_dictionary_t* realm_set_dictionary(realm_object_t*, realm_property_key_t); +RLM_API realm_list_t* realm_set_list_by_name(realm_object_t*, const char* property_name); +RLM_API realm_dictionary_t* realm_set_dictionary_by_name(realm_object_t*, const char* property_name); /** Return the object linked by the given property * @@ -1748,6 +1797,15 @@ RLM_API bool realm_set_values(realm_object_t*, size_t num_values, const realm_pr */ RLM_API realm_list_t* realm_get_list(realm_object_t*, realm_property_key_t); +/** + * Get a list instance for the property of an object by name. + * + * Note: It is up to the caller to call `realm_release()` on the returned list. + * + * @return A non-null pointer if no exception occurred. + */ +RLM_API realm_list_t* realm_get_list_by_name(realm_object_t*, const char*); + /** * Create a `realm_list_t` from a pointer to a `realm::List`, copy-constructing * the internal representation. @@ -2253,6 +2311,15 @@ RLM_API realm_set_t* realm_set_from_thread_safe_reference(const realm_t*, realm_ */ RLM_API realm_dictionary_t* realm_get_dictionary(realm_object_t*, realm_property_key_t); +/** + * Get a dictionary instance for the property of an object by name. + * + * Note: It is up to the caller to call `realm_release()` on the returned dictionary. + * + * @return A non-null pointer if no exception occurred. + */ +RLM_API realm_dictionary_t* realm_get_dictionary_by_name(realm_object_t*, const char*); + /** * Create a `realm_dictionary_t` from a pointer to a `realm::object_store::Dictionary`, * copy-constructing the internal representation. diff --git a/src/realm/db.cpp b/src/realm/db.cpp index f6f258616d2..6198cac10ae 100644 --- a/src/realm/db.cpp +++ b/src/realm/db.cpp @@ -2806,7 +2806,8 @@ void DB::async_request_write_mutex(TransactionRef& tr, util::UniqueFunction { SharedInfo* m_info = nullptr; bool m_wait_for_change_enabled = true; // Initially wait_for_change is enabled bool m_write_transaction_open GUARDED_BY(m_mutex) = false; + bool m_allow_flexible_schema; std::string m_db_path; int m_file_format_version = 0; util::InterprocessMutex m_writemutex; diff --git a/src/realm/db_options.hpp b/src/realm/db_options.hpp index a11eded2352..68e2c8098f3 100644 --- a/src/realm/db_options.hpp +++ b/src/realm/db_options.hpp @@ -106,6 +106,9 @@ struct DBOptions { /// will clear and reinitialize the file. bool clear_on_invalid_file = false; + /// Allow setting properties not supported by a specific column on an object + bool allow_flexible_schema = false; + /// sys_tmp_dir will be used if the temp_dir is empty when creating DBOptions. /// It must be writable and allowed to create pipe/fifo file on it. /// set_sys_tmp_dir is not a thread-safe call and it is only supposed to be called once diff --git a/src/realm/group.cpp b/src/realm/group.cpp index b6703b3af53..f0c941725c7 100644 --- a/src/realm/group.cpp +++ b/src/realm/group.cpp @@ -69,12 +69,13 @@ Group::Group() } -Group::Group(const std::string& file_path, const char* encryption_key) +Group::Group(const std::string& file_path, const char* encryption_key, bool allow_additional_properties) : m_local_alloc(new SlabAlloc) // Throws , m_alloc(*m_local_alloc) , m_top(m_alloc) , m_tables(m_alloc) , m_table_names(m_alloc) + , m_allow_additional_properties(allow_additional_properties) { init_array_parents(); @@ -760,6 +761,10 @@ Table* Group::do_add_table(StringData name, Table::Type table_type, bool do_repl Table* table = create_table_accessor(j); table->do_set_table_type(table_type); + if (m_allow_additional_properties && name.begins_with(g_class_name_prefix)) { + table->do_add_additional_prop_column(); + } + return table; } diff --git a/src/realm/group.hpp b/src/realm/group.hpp index 352c5bd25fb..a28509ae135 100644 --- a/src/realm/group.hpp +++ b/src/realm/group.hpp @@ -117,7 +117,8 @@ class Group : public ArrayParent { /// types that are derived from FileAccessError, the /// derived exception type is thrown. Note that InvalidDatabase is /// among these derived exception types. - explicit Group(const std::string& file, const char* encryption_key = nullptr); + explicit Group(const std::string& file, const char* encryption_key = nullptr, + bool allow_additional_properties = false); /// Attach this Group instance to the specified memory buffer. /// @@ -599,6 +600,7 @@ class Group : public ArrayParent { mutable int m_num_tables = 0; bool m_attached = false; bool m_is_writable = true; + bool m_allow_additional_properties = false; static std::optional fake_target_file_format; util::UniqueFunction m_notify_handler; diff --git a/src/realm/impl/array_writer.hpp b/src/realm/impl/array_writer.hpp index 4096805e0fa..1b2694a8e60 100644 --- a/src/realm/impl/array_writer.hpp +++ b/src/realm/impl/array_writer.hpp @@ -30,9 +30,7 @@ class ArrayWriterBase { bool only_modified = true; bool compress = true; const Table* table; - virtual ~ArrayWriterBase() - { - } + virtual ~ArrayWriterBase() {} /// Write the specified array data and its checksum into free /// space. diff --git a/src/realm/obj.cpp b/src/realm/obj.cpp index 8d968971fc7..5b95813fbda 100644 --- a/src/realm/obj.cpp +++ b/src/realm/obj.cpp @@ -631,6 +631,30 @@ BinaryData Obj::_get(ColKey::Idx col_ndx) const return ArrayBinary::get(alloc.translate(ref), m_row_ndx, alloc); } +bool Obj::has_property(StringData prop_name) const +{ + if (m_table->get_column_key(prop_name)) + return true; + if (auto ck = m_table->m_additional_prop_col) { + Dictionary dict(*this, ck); + return dict.contains(prop_name); + } + return false; +} + +std::vector Obj::get_additional_properties() const +{ + std::vector ret; + + if (auto ck = m_table->m_additional_prop_col) { + Dictionary dict(*this, ck); + dict.for_all_keys([&ret](StringData key) { + ret.push_back(key); + }); + } + return ret; +} + Mixed Obj::get_any(ColKey col_key) const { m_table->check_column(col_key); @@ -676,6 +700,19 @@ Mixed Obj::get_any(ColKey col_key) const return {}; } +Mixed Obj::get_additional_prop(StringData prop_name) const +{ + if (auto ck = m_table->m_additional_prop_col) { + Dictionary dict(*this, ck); + if (auto val = dict.try_get(prop_name)) { + return *val; + } + } + throw InvalidArgument(ErrorCodes::InvalidProperty, + util::format("Property '%1.%2' does not exist", m_table->get_class_name(), prop_name)); + return {}; +} + Mixed Obj::get_primary_key() const { auto col = m_table->get_primary_key_column(); @@ -1107,7 +1144,8 @@ StablePath Obj::get_stable_path() const noexcept void Obj::add_index(Path& path, const CollectionParent::Index& index) const { if (path.empty()) { - path.emplace_back(get_table()->get_column_key(index)); + auto ck = m_table->get_column_key(index); + path.emplace_back(ck); } else { StringData col_name = get_table()->get_column_name(index); @@ -1229,6 +1267,32 @@ Obj& Obj::set(ColKey col_key, Mixed value, bool is_default) return *this; } +Obj& Obj::erase_additional_prop(StringData prop_name) +{ + bool erased = false; + if (auto ck = m_table->m_additional_prop_col) { + Dictionary dict(*this, ck); + erased = dict.try_erase(prop_name); + } + if (!erased) { + throw InvalidArgument(ErrorCodes::InvalidProperty, util::format("Could not erase property: %1", prop_name)); + } + return *this; +} + +Obj& Obj::set_additional_prop(StringData prop_name, const Mixed& value) +{ + if (auto ck = m_table->m_additional_prop_col) { + Dictionary dict(*this, ck); + dict.insert(prop_name, value); + } + else { + throw InvalidArgument(ErrorCodes::InvalidProperty, + util::format("Property '%1.%2' does not exist", m_table->get_class_name(), prop_name)); + } + return *this; +} + Obj& Obj::set_any(ColKey col_key, Mixed value, bool is_default) { if (value.is_null()) { @@ -1983,7 +2047,6 @@ Dictionary Obj::get_dictionary(ColKey col_key) const Obj& Obj::set_collection(ColKey col_key, CollectionType type) { - REALM_ASSERT(col_key.get_type() == col_type_Mixed); if ((col_key.is_dictionary() && type == CollectionType::Dictionary) || (col_key.is_list() && type == CollectionType::List)) { return *this; @@ -1991,11 +2054,34 @@ Obj& Obj::set_collection(ColKey col_key, CollectionType type) if (type == CollectionType::Set) { throw IllegalOperation("Set nested in Mixed is not supported"); } + if (col_key.get_type() != col_type_Mixed) { + throw IllegalOperation("Collection can only be nested in Mixed"); + } set(col_key, Mixed(0, type)); return *this; } +Obj& Obj::set_collection(StringData prop_name, CollectionType type) +{ + if (auto ck = get_column_key(prop_name)) { + return set_collection(ck, type); + } + return set_additional_collection(prop_name, type); +} + +Obj& Obj::set_additional_collection(StringData prop_name, CollectionType type) +{ + if (auto ck = m_table->m_additional_prop_col) { + Dictionary dict(*this, ck); + dict.insert_collection(prop_name, type); + } + else { + throw InvalidArgument(ErrorCodes::InvalidProperty, util::format("Property not found: %1", prop_name)); + } + return *this; +} + DictionaryPtr Obj::get_dictionary_ptr(ColKey col_key) const { return std::make_shared(get_dictionary(col_key)); @@ -2011,14 +2097,33 @@ Dictionary Obj::get_dictionary(StringData col_name) const return get_dictionary(get_column_key(col_name)); } -CollectionPtr Obj::get_collection_ptr(const Path& path) const +CollectionBasePtr Obj::get_collection_ptr(const Path& path) const { REALM_ASSERT(path.size() > 0); // First element in path must be column name auto col_key = path[0].is_col_key() ? path[0].get_col_key() : m_table->get_column_key(path[0].get_key()); - REALM_ASSERT(col_key); + + CollectionBasePtr collection; size_t level = 1; - CollectionBasePtr collection = get_collection_ptr(col_key); + if (col_key) { + collection = get_collection_ptr(col_key); + } + else { + if (auto ck = m_table->m_additional_prop_col) { + auto prop_name = path[0].get_key(); + Dictionary dict(*this, ck); + auto ref = dict.get(prop_name); + if (ref.is_type(type_List)) { + collection = dict.get_list(prop_name); + } + else if (ref.is_type(type_Dictionary)) { + collection = dict.get_dictionary(prop_name); + } + else { + throw InvalidArgument("Wrong path"); + } + } + } while (level < path.size()) { auto& path_elem = path[level]; @@ -2044,7 +2149,7 @@ CollectionPtr Obj::get_collection_ptr(const Path& path) const return collection; } -CollectionPtr Obj::get_collection_by_stable_path(const StablePath& path) const +CollectionBasePtr Obj::get_collection_by_stable_path(const StablePath& path) const { // First element in path is phony column key ColKey col_key = m_table->get_column_key(path[0]); @@ -2108,7 +2213,7 @@ CollectionBasePtr Obj::get_collection_ptr(ColKey col_key) const CollectionBasePtr Obj::get_collection_ptr(StringData col_name) const { - return get_collection_ptr(get_column_key(col_name)); + return get_collection_ptr(Path{{col_name}}); } LinkCollectionPtr Obj::get_linkcollection_ptr(ColKey col_key) const diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp index 67c82a0cada..ac4cdcf7c4b 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -117,17 +117,29 @@ class Obj { template U get(ColKey col_key) const; + bool has_property(StringData prop_name) const; + + std::vector get_additional_properties() const; + Mixed get_any(ColKey col_key) const; Mixed get_any(StringData col_name) const { - return get_any(get_column_key(col_name)); + if (auto ck = get_column_key(col_name)) { + return get_any(ck); + } + return get_additional_prop(col_name); } + Mixed get_additional_prop(StringData col_name) const; + Mixed get_primary_key() const; template U get(StringData col_name) const { - return get(get_column_key(col_name)); + if (auto ck = get_column_key(col_name)) { + return get(ck); + } + return get_additional_prop(col_name).get(); } bool is_unresolved(ColKey col_key) const; @@ -187,17 +199,26 @@ class Obj { // default state. If the object does not exist, create a // new object and link it. (To Be Implemented) Obj clear_linked_object(ColKey col_key); + + Obj& erase_additional_prop(StringData prop_name); 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); + if (auto ck = get_column_key(col_name)) { + return set_any(ck, value, is_default); + } + return set_additional_prop(col_name, value); } template Obj& set(StringData col_name, U value, bool is_default = false) { - return set(get_column_key(col_name), value, is_default); + if (auto ck = get_column_key(col_name)) { + return set(ck, value, is_default); + } + return set_additional_prop(col_name, Mixed(value)); } + Obj& set_additional_prop(StringData prop_name, const Mixed& value); Obj& set_null(ColKey col_key, bool is_default = false); Obj& set_null(StringData col_name, bool is_default = false) @@ -206,6 +227,7 @@ class Obj { } Obj& set_json(ColKey col_key, StringData json); + Obj& add_int(ColKey col_key, int64_t value); Obj& add_int(StringData col_name, int64_t value) { @@ -248,6 +270,11 @@ class Obj { { return std::dynamic_pointer_cast>(get_collection_ptr(path)); } + template + std::shared_ptr> get_list_ptr(StringData prop_name) const + { + return get_list_ptr(Path{prop_name}); + } template Lst get_list(StringData col_name) const @@ -285,17 +312,24 @@ class Obj { 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; Dictionary get_dictionary(StringData col_name) const; Obj& set_collection(ColKey col_key, CollectionType type); + Obj& set_collection(StringData, CollectionType type); + Obj& set_additional_collection(StringData, CollectionType type); DictionaryPtr get_dictionary_ptr(ColKey col_key) const; DictionaryPtr get_dictionary_ptr(const Path& path) const; + DictionaryPtr get_dictionary_ptr(StringData prop_name) const + { + return get_dictionary_ptr(Path{prop_name}); + } CollectionBasePtr get_collection_ptr(ColKey col_key) const; CollectionBasePtr get_collection_ptr(StringData col_name) const; - CollectionPtr get_collection_ptr(const Path& path) const; - CollectionPtr get_collection_by_stable_path(const StablePath& path) const; + CollectionBasePtr get_collection_ptr(const Path& path) const; + CollectionBasePtr get_collection_by_stable_path(const StablePath& path) const; LinkCollectionPtr get_linkcollection_ptr(ColKey col_key) const; void assign_pk_and_backlinks(Obj& other); diff --git a/src/realm/object-store/c_api/config.cpp b/src/realm/object-store/c_api/config.cpp index 7b1b00498c2..2d76bd9368f 100644 --- a/src/realm/object-store/c_api/config.cpp +++ b/src/realm/object-store/c_api/config.cpp @@ -243,3 +243,8 @@ RLM_API void realm_config_set_automatic_backlink_handling(realm_config_t* realm_ { realm_config->automatically_handle_backlinks_in_migrations = enable_automatic_handling; } + +RLM_API void realm_config_set_flexible_schema(realm_config_t* realm_config, bool flexible_schema) noexcept +{ + realm_config->flexible_schema = flexible_schema; +} diff --git a/src/realm/object-store/c_api/object.cpp b/src/realm/object-store/c_api/object.cpp index 44bc55ab548..4971a818c66 100644 --- a/src/realm/object-store/c_api/object.cpp +++ b/src/realm/object-store/c_api/object.cpp @@ -253,31 +253,31 @@ RLM_API realm_object_t* realm_object_from_thread_safe_reference(const realm_t* r }); } -RLM_API bool realm_get_value(const realm_object_t* obj, realm_property_key_t col, realm_value_t* out_value) +RLM_API bool realm_get_value(const realm_object_t* object, realm_property_key_t col, realm_value_t* out_value) { - return realm_get_values(obj, 1, &col, out_value); + return realm_get_values(object, 1, &col, out_value); } -RLM_API bool realm_get_values(const realm_object_t* obj, size_t num_values, const realm_property_key_t* properties, +RLM_API bool realm_get_values(const realm_object_t* object, size_t num_values, const realm_property_key_t* properties, realm_value_t* out_values) { return wrap_err([&]() { - obj->verify_attached(); + object->verify_attached(); - auto o = obj->get_obj(); + auto obj = object->get_obj(); for (size_t i = 0; i < num_values; ++i) { auto col_key = ColKey(properties[i]); if (col_key.is_collection()) { - auto table = o.get_table(); - auto& schema = schema_for_table(obj->get_realm(), table->get_key()); + auto table = obj.get_table(); + auto& schema = schema_for_table(object->get_realm(), table->get_key()); throw PropertyTypeMismatch{schema.name, table->get_column_name(col_key)}; } - auto val = o.get_any(col_key); + auto val = obj.get_any(col_key); if (out_values) { - auto converted = objkey_to_typed_link(val, col_key, *o.get_table()); + auto converted = objkey_to_typed_link(val, col_key, *obj.get_table()); out_values[i] = to_capi(converted); } } @@ -286,18 +286,34 @@ RLM_API bool realm_get_values(const realm_object_t* obj, size_t num_values, cons }); } -RLM_API bool realm_set_value(realm_object_t* obj, realm_property_key_t col, realm_value_t new_value, bool is_default) +RLM_API bool realm_get_value_by_name(const realm_object_t* object, const char* property_name, + realm_value_t* out_value) +{ + return wrap_err([&]() { + object->verify_attached(); + + auto obj = object->get_obj(); + auto val = obj.get_any(property_name); + if (out_value) { + *out_value = to_capi(val); + } + return true; + }); +} + +RLM_API bool realm_set_value(realm_object_t* object, realm_property_key_t col, realm_value_t new_value, + bool is_default) { - return realm_set_values(obj, 1, &col, &new_value, is_default); + return realm_set_values(object, 1, &col, &new_value, is_default); } -RLM_API bool realm_set_values(realm_object_t* obj, size_t num_values, const realm_property_key_t* properties, +RLM_API bool realm_set_values(realm_object_t* object, size_t num_values, const realm_property_key_t* properties, const realm_value_t* values, bool is_default) { return wrap_err([&]() { - obj->verify_attached(); - auto o = obj->get_obj(); - auto table = o.get_table(); + object->verify_attached(); + auto obj = object->get_obj(); + auto table = obj.get_table(); // Perform validation up front to avoid partial updates. This is // unlikely to incur performance overhead because the object itself is @@ -308,12 +324,12 @@ RLM_API bool realm_set_values(realm_object_t* obj, size_t num_values, const real table->check_column(col_key); if (col_key.is_collection()) { - auto& schema = schema_for_table(obj->get_realm(), table->get_key()); + auto& schema = schema_for_table(object->get_realm(), table->get_key()); throw PropertyTypeMismatch{schema.name, table->get_column_name(col_key)}; } auto val = from_capi(values[i]); - check_value_assignable(obj->get_realm(), *table, col_key, val); + check_value_assignable(object->get_realm(), *table, col_key, val); } // Actually write the properties. @@ -321,36 +337,94 @@ RLM_API bool realm_set_values(realm_object_t* obj, size_t num_values, const real for (size_t i = 0; i < num_values; ++i) { auto col_key = ColKey(properties[i]); auto val = from_capi(values[i]); - o.set_any(col_key, val, is_default); + obj.set_any(col_key, val, is_default); } return true; }); } -RLM_API bool realm_set_json(realm_object_t* obj, realm_property_key_t col, const char* json_string) +RLM_API bool realm_set_value_by_name(realm_object_t* object, const char* property_name, realm_value_t new_value) { return wrap_err([&]() { - obj->verify_attached(); - auto o = obj->get_obj(); + object->verify_attached(); + auto obj = object->get_obj(); + obj.set_any(property_name, from_capi(new_value)); + return true; + }); +} + +RLM_API bool realm_has_property(realm_object_t* object, const char* property_name, bool* out_has_property) +{ + return wrap_err([&]() { + object->verify_attached(); + if (out_has_property) { + auto obj = object->get_obj(); + *out_has_property = obj.has_property(property_name); + } + return true; + }); +} + +RLM_API void realm_get_additional_properties(realm_object_t* object, const char** out_prop_names, size_t max, + size_t* out_n) +{ + size_t copied = 0; + wrap_err([&]() { + object->verify_attached(); + auto obj = object->get_obj(); + auto vec = obj.get_additional_properties(); + copied = vec.size(); + if (out_prop_names) { + if (max < copied) { + copied = max; + } + auto it = vec.begin(); + auto to_copy = copied; + while (to_copy--) { + *out_prop_names++ = (*it++).data(); + } + } + return true; + }); + if (out_n) { + *out_n = copied; + } +} + +RLM_API bool realm_erase_additional_property(realm_object_t* object, const char* property_name) +{ + return wrap_err([&]() { + object->verify_attached(); + auto obj = object->get_obj(); + obj.erase_additional_prop(property_name); + return true; + }); +} + +RLM_API bool realm_set_json(realm_object_t* object, realm_property_key_t col, const char* json_string) +{ + return wrap_err([&]() { + object->verify_attached(); + auto obj = object->get_obj(); ColKey col_key(col); if (col_key.get_type() != col_type_Mixed) { - auto table = o.get_table(); - auto& schema = schema_for_table(obj->get_realm(), table->get_key()); + auto table = obj.get_table(); + auto& schema = schema_for_table(object->get_realm(), table->get_key()); throw PropertyTypeMismatch{schema.name, table->get_column_name(col_key)}; } - o.set_json(ColKey(col), json_string); + obj.set_json(ColKey(col), json_string); return true; }); } -RLM_API realm_object_t* realm_set_embedded(realm_object_t* obj, realm_property_key_t col) +RLM_API realm_object_t* realm_set_embedded(realm_object_t* object, realm_property_key_t col) { return wrap_err([&]() { - obj->verify_attached(); - auto& o = obj->get_obj(); - return new realm_object_t({obj->get_realm(), o.create_and_set_linked_object(ColKey(col))}); + object->verify_attached(); + auto& obj = object->get_obj(); + return new realm_object_t({object->get_realm(), obj.create_and_set_linked_object(ColKey(col))}); }); } @@ -380,12 +454,35 @@ RLM_API realm_dictionary_t* realm_set_dictionary(realm_object_t* object, realm_p }); } -RLM_API realm_object_t* realm_get_linked_object(realm_object_t* obj, realm_property_key_t col) +RLM_API realm_list_t* realm_set_list_by_name(realm_object_t* object, const char* property_name) { return wrap_err([&]() { - obj->verify_attached(); - const auto& o = obj->get_obj().get_linked_object(ColKey(col)); - return o ? new realm_object_t({obj->get_realm(), o}) : nullptr; + object->verify_attached(); + + auto& obj = object->get_obj(); + obj.set_collection(property_name, CollectionType::List); + return new realm_list_t{List{object->get_realm(), obj.get_list_ptr(property_name)}}; + }); +} + +RLM_API realm_dictionary_t* realm_set_dictionary_by_name(realm_object_t* object, const char* property_name) +{ + return wrap_err([&]() { + object->verify_attached(); + + auto& obj = object->get_obj(); + obj.set_collection(property_name, CollectionType::Dictionary); + return new realm_dictionary_t{ + object_store::Dictionary{object->get_realm(), obj.get_dictionary_ptr(property_name)}}; + }); +} + +RLM_API realm_object_t* realm_get_linked_object(realm_object_t* object, realm_property_key_t col) +{ + return wrap_err([&]() { + object->verify_attached(); + const auto& obj = object->get_obj().get_linked_object(ColKey(col)); + return obj ? new realm_object_t({object->get_realm(), obj}) : nullptr; }); } @@ -408,6 +505,20 @@ RLM_API realm_list_t* realm_get_list(realm_object_t* object, realm_property_key_ }); } +RLM_API realm_list_t* realm_get_list_by_name(realm_object_t* object, const char* prop_name) +{ + return wrap_err([&]() -> realm_list_t* { + object->verify_attached(); + + const auto& obj = object->get_obj(); + auto collection = obj.get_collection_ptr(StringData(prop_name)); + if (collection->get_collection_type() == CollectionType::List) { + return new realm_list_t{List{object->get_realm(), std::move(collection)}}; + } + return nullptr; + }); +} + RLM_API realm_set_t* realm_get_set(realm_object_t* object, realm_property_key_t key) { return wrap_err([&]() { @@ -445,6 +556,20 @@ RLM_API realm_dictionary_t* realm_get_dictionary(realm_object_t* object, realm_p }); } +RLM_API realm_dictionary_t* realm_get_dictionary_by_name(realm_object_t* object, const char* prop_name) +{ + return wrap_err([&]() -> realm_dictionary_t* { + object->verify_attached(); + + const auto& obj = object->get_obj(); + auto collection = obj.get_collection_ptr(StringData(prop_name)); + if (collection->get_collection_type() == CollectionType::Dictionary) { + return new realm_dictionary_t{object_store::Dictionary{object->get_realm(), std::move(collection)}}; + } + return nullptr; + }); +} + RLM_API char* realm_object_to_string(realm_object_t* object) { return wrap_err([&]() { diff --git a/src/realm/object-store/impl/realm_coordinator.cpp b/src/realm/object-store/impl/realm_coordinator.cpp index 3d9f2a95034..06a47e6041c 100644 --- a/src/realm/object-store/impl/realm_coordinator.cpp +++ b/src/realm/object-store/impl/realm_coordinator.cpp @@ -478,6 +478,7 @@ bool RealmCoordinator::open_db() options.durability = m_config.in_memory ? DBOptions::Durability::MemOnly : DBOptions::Durability::Full; options.is_immutable = m_config.immutable(); options.logger = util::Logger::get_default_logger(); + options.allow_flexible_schema = m_config.flexible_schema; if (!m_config.fifo_files_fallback_path.empty()) { options.temp_dir = util::normalize_dir(m_config.fifo_files_fallback_path); diff --git a/src/realm/object-store/object.hpp b/src/realm/object-store/object.hpp index 53fa43a2653..8d5a7527f7d 100644 --- a/src/realm/object-store/object.hpp +++ b/src/realm/object-store/object.hpp @@ -206,7 +206,12 @@ class Object { void set_property_value_impl(ContextType& ctx, const Property& property, ValueType value, CreatePolicy policy, bool is_default); template + void set_additional_property_value_impl(ContextType& ctx, StringData prop_name, ValueType value, + CreatePolicy policy); + template ValueType get_property_value_impl(ContextType& ctx, const Property& property) const; + template + ValueType get_additional_property_value_impl(ContextType& ctx, StringData prop_name) const; template static ObjKey get_for_primary_key_in_migration(ContextType& ctx, Table const& table, const Property& primary_prop, diff --git a/src/realm/object-store/object_accessor.hpp b/src/realm/object-store/object_accessor.hpp index 824ee437204..868325c55ce 100644 --- a/src/realm/object-store/object_accessor.hpp +++ b/src/realm/object-store/object_accessor.hpp @@ -42,9 +42,13 @@ namespace realm { template void Object::set_property_value(ContextType& ctx, StringData prop_name, ValueType value, CreatePolicy policy) { - auto& property = property_for_name(prop_name); - validate_property_for_setter(property); - set_property_value_impl(ctx, property, value, policy, false); + if (auto prop = m_object_schema->property_for_name(prop_name)) { + validate_property_for_setter(*prop); + set_property_value_impl(ctx, *prop, value, policy, false); + } + else { + set_additional_property_value_impl(ctx, prop_name, value, policy); + } } template @@ -62,7 +66,12 @@ ValueType Object::get_property_value(ContextType& ctx, const Property& property) template ValueType Object::get_property_value(ContextType& ctx, StringData prop_name) const { - return get_property_value_impl(ctx, property_for_name(prop_name)); + if (auto prop = m_object_schema->property_for_name(prop_name)) { + return get_property_value_impl(ctx, *prop); + } + else { + return get_additional_property_value_impl(ctx, prop_name); + } } namespace { @@ -205,6 +214,28 @@ void Object::set_property_value_impl(ContextType& ctx, const Property& property, ctx.did_change(); } +template +void Object::set_additional_property_value_impl(ContextType& ctx, StringData prop_name, ValueType value, + CreatePolicy policy) +{ + Mixed new_val = ctx.template unbox(value, policy); + if (new_val.is_type(type_Dictionary)) { + m_obj.set_additional_collection(prop_name, CollectionType::Dictionary); + object_store::Dictionary dict(m_realm, m_obj.get_collection_ptr(prop_name)); + dict.assign(ctx, value, policy); + ctx.did_change(); + return; + } + if (new_val.is_type(type_List)) { + m_obj.set_additional_collection(prop_name, CollectionType::List); + List list(m_realm, m_obj.get_collection_ptr(prop_name)); + list.assign(ctx, value, policy); + ctx.did_change(); + return; + } + m_obj.set_additional_prop(prop_name, new_val); +} + template ValueType Object::get_property_value_impl(ContextType& ctx, const Property& property) const { @@ -269,6 +300,20 @@ ValueType Object::get_property_value_impl(ContextType& ctx, const Property& prop } } +template +ValueType Object::get_additional_property_value_impl(ContextType& ctx, StringData prop_name) const +{ + verify_attached(); + auto value = m_obj.get_additional_prop(prop_name); + if (value.is_type(type_Dictionary)) { + return ctx.box(object_store::Dictionary(m_realm, m_obj.get_collection_ptr(prop_name))); + } + if (value.is_type(type_List)) { + return ctx.box(List(m_realm, m_obj.get_collection_ptr(prop_name))); + } + return ctx.box(value); +} + template Object Object::create(ContextType& ctx, std::shared_ptr const& realm, StringData object_type, ValueType value, CreatePolicy policy, ObjKey current_obj, Obj* out_row) diff --git a/src/realm/object-store/shared_realm.hpp b/src/realm/object-store/shared_realm.hpp index 5e98c96149f..23b79b1ea9a 100644 --- a/src/realm/object-store/shared_realm.hpp +++ b/src/realm/object-store/shared_realm.hpp @@ -104,6 +104,7 @@ struct RealmConfig { std::string fifo_files_fallback_path; bool in_memory = false; + bool flexible_schema = false; SchemaMode schema_mode = SchemaMode::Automatic; SchemaSubsetMode schema_subset_mode = SchemaSubsetMode::Strict; diff --git a/src/realm/sync/instruction_applier.cpp b/src/realm/sync/instruction_applier.cpp index 3106858f890..50bf5112ed5 100644 --- a/src/realm/sync/instruction_applier.cpp +++ b/src/realm/sync/instruction_applier.cpp @@ -1495,8 +1495,13 @@ InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resol InstructionApplier::PathResolver::Status InstructionApplier::PathResolver::resolve_field(Obj& obj, InternString field) { auto field_name = get_string(field); - ColKey col = obj.get_table()->get_column_key(field_name); + auto table = obj.get_table().unchecked_ptr(); + ColKey col = table->get_column_key(field_name); if (!col) { + if (auto ck = table->get_additional_prop_col()) { + auto dict = obj.get_dictionary(ck); + return resolve_dictionary_element(dict, field); + } on_error(util::format("%1: No such field: '%2' in class '%3'", m_instr_name, field_name, obj.get_table()->get_name())); return Status::DidNotResolve; diff --git a/src/realm/sync/instruction_replication.cpp b/src/realm/sync/instruction_replication.cpp index c845bd9c0bf..190a4c92dd3 100644 --- a/src/realm/sync/instruction_replication.cpp +++ b/src/realm/sync/instruction_replication.cpp @@ -611,51 +611,62 @@ void SyncReplication::set_clear(const CollectionBase& set) } } -void SyncReplication::dictionary_update(const CollectionBase& dict, const Mixed& key, const Mixed& value) +void SyncReplication::dictionary_update(const CollectionBase& dict, const Mixed& key, const Mixed* value) { // If link is unresolved, it should not be communicated. - if (value.is_unresolved_link()) { + if (value && value->is_unresolved_link()) { return; } - if (select_collection(dict)) { - Instruction::Update instr; - REALM_ASSERT(key.get_type() == type_String); - populate_path_instr(instr, dict); + Instruction::Update instr; + REALM_ASSERT(key.get_type() == type_String); + + const Table* source_table = dict.get_table().unchecked_ptr(); + auto col = dict.get_col_key(); + ObjKey obj_key = dict.get_owner_key(); + if (source_table->is_additional_props_col(col)) { + // Here we have to fake it and pretend we are setting/erasing a property on the object + if (!select_table(*source_table)) { + return; + } + + populate_path_instr(instr, *source_table, obj_key, {key.get_string()}); + } + else { + if (!select_collection(dict)) { + return; + } + + populate_path_instr(instr, *source_table, obj_key, dict.get_short_path()); StringData key_value = key.get_string(); instr.path.push_back(m_encoder.intern_string(key_value)); - instr.value = as_payload(dict, value); - instr.is_default = false; - emit(instr); } + if (value) { + instr.value = as_payload(*source_table, col, *value); + } + else { + instr.value = Instruction::Payload::Erased{}; + } + instr.is_default = false; + emit(instr); } void SyncReplication::dictionary_insert(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value) { Replication::dictionary_insert(dict, ndx, key, value); - dictionary_update(dict, key, value); + dictionary_update(dict, key, &value); } void SyncReplication::dictionary_set(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value) { Replication::dictionary_set(dict, ndx, key, value); - dictionary_update(dict, key, value); + dictionary_update(dict, key, &value); } void SyncReplication::dictionary_erase(const CollectionBase& dict, size_t ndx, Mixed key) { Replication::dictionary_erase(dict, ndx, key); - - if (select_collection(dict)) { - Instruction::Update instr; - REALM_ASSERT(key.get_type() == type_String); - populate_path_instr(instr, dict); - StringData key_value = key.get_string(); - instr.path.push_back(m_encoder.intern_string(key_value)); - instr.value = Instruction::Payload::Erased{}; - instr.is_default = false; - emit(instr); - } + dictionary_update(dict, key, nullptr); } void SyncReplication::dictionary_clear(const CollectionBase& dict) @@ -750,7 +761,25 @@ void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, c { REALM_ASSERT(key); // The first path entry will be the column key - REALM_ASSERT(path[0].is_col_key()); + std::string field_name; + if (path[0].is_col_key()) { + auto ck = path[0].get_col_key(); + if (table.is_additional_props_col(ck)) { + // We are modifying a collection nested in an additional property + REALM_ASSERT(path.size() > 1); + field_name = path[1].get_key(); + // Erase the "__additional" part of the path + path.erase(path.begin()); + } + else { + field_name = table.get_column_name(ck); + } + } + else { + // In the case of an additional property directly on an object, + // the first element is a string. + field_name = path[0].get_key(); + } if (table.is_embedded()) { // For embedded objects, Obj::traverse_path() yields the top object @@ -760,7 +789,7 @@ void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, c // Populate top object in the normal way. auto top_table = table.get_parent_group()->get_table(full_path.top_table); - full_path.path_from_top.emplace_back(table.get_column_name(path[0].get_col_key())); + full_path.path_from_top.emplace_back(field_name); for (auto it = path.begin() + 1; it != path.end(); ++it) { full_path.path_from_top.emplace_back(std::move(*it)); @@ -782,8 +811,6 @@ void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, c m_last_primary_key = instr.object; } - StringData field_name = table.get_column_name(path[0].get_col_key()); - if (m_last_field_name == field_name) { instr.field = m_last_interned_field_name; } diff --git a/src/realm/sync/instruction_replication.hpp b/src/realm/sync/instruction_replication.hpp index ff7fbd62de4..46f683dcec6 100644 --- a/src/realm/sync/instruction_replication.hpp +++ b/src/realm/sync/instruction_replication.hpp @@ -132,13 +132,13 @@ class SyncReplication : public Replication { void populate_path_instr(Instruction::PathInstruction&, const CollectionBase&); void populate_path_instr(Instruction::PathInstruction&, const CollectionBase&, uint32_t ndx); - void dictionary_update(const CollectionBase&, const Mixed& key, const Mixed& val); + void dictionary_update(const CollectionBase&, const Mixed& key, const Mixed* val); // Cache information for the purpose of avoiding excessive string comparisons / interning // lookups. const Table* m_last_table = nullptr; ObjKey m_last_object; - StringData m_last_field_name; + std::string m_last_field_name; InternString m_last_class_name; util::Optional m_last_primary_key; InternString m_last_interned_field_name; diff --git a/src/realm/sync/noinst/server/server_file_access_cache.hpp b/src/realm/sync/noinst/server/server_file_access_cache.hpp index 24af8a4adc4..51186f5b2ba 100644 --- a/src/realm/sync/noinst/server/server_file_access_cache.hpp +++ b/src/realm/sync/noinst/server/server_file_access_cache.hpp @@ -229,6 +229,7 @@ inline DBOptions ServerFileAccessCache::Slot::make_shared_group_options() const options.encryption_key = m_cache.m_encryption_key->data(); if (m_disable_sync_to_disk) options.durability = DBOptions::Durability::Unsafe; + options.allow_flexible_schema = true; return options; } diff --git a/src/realm/table.cpp b/src/realm/table.cpp index a21820997d6..fb910594ec5 100644 --- a/src/realm/table.cpp +++ b/src/realm/table.cpp @@ -262,6 +262,7 @@ using namespace realm; using namespace realm::util; Replication* Table::g_dummy_replication = nullptr; +static const StringData additional_properties_colum_name{"__additional"}; bool TableVersions::operator==(const TableVersions& other) const { @@ -634,12 +635,18 @@ void Table::init(ref_type top_ref, ArrayParent* parent, size_t ndx_in_parent, bo auto rot_pk_key = m_top.get_as_ref_or_tagged(top_position_for_pk_col); m_primary_key_col = rot_pk_key.is_tagged() ? ColKey(rot_pk_key.get_as_int()) : ColKey(); + m_additional_prop_col = ColKey(); if (m_top.size() <= top_position_for_flags) { m_table_type = Type::TopLevel; } else { uint64_t flags = m_top.get_as_ref_or_tagged(top_position_for_flags).get_as_int(); m_table_type = Type(flags & table_type_mask); + if (flags & additional_prop_mask) { + // If we have an additional properties column, it will always be first + REALM_ASSERT(m_spec.m_names.size() > 0 && m_spec.m_names.get(0) == additional_properties_colum_name); + m_additional_prop_col = ColKey(m_spec.m_keys.get(0)); + } } m_has_any_embedded_objects.reset(); @@ -2952,6 +2959,23 @@ void Table::do_set_primary_key_column(ColKey col_key) m_primary_key_col = col_key; } +void Table::do_add_additional_prop_column() +{ + ColumnAttrMask attr; + attr.set(col_attr_Dictionary); + attr.set(col_attr_Nullable); + ColKey col_key = generate_col_key(col_type_Mixed, attr); + + uint64_t flags = m_top.get_as_ref_or_tagged(top_position_for_flags).get_as_int(); + flags |= additional_prop_mask; + m_top.set(top_position_for_flags, RefOrTagged::make_tagged(flags)); + + m_additional_prop_col = + do_insert_root_column(col_key, col_type_Mixed, additional_properties_colum_name, type_String); + // Be sure that it will always be first + REALM_ASSERT(m_additional_prop_col.get_index().val == 0); +} + bool Table::contains_unique_values(ColKey col) const { if (search_index_type(col) == IndexType::General) { diff --git a/src/realm/table.hpp b/src/realm/table.hpp index ac0faa5140a..278f756041c 100644 --- a/src/realm/table.hpp +++ b/src/realm/table.hpp @@ -93,6 +93,7 @@ class Table { /// . enum class Type : uint8_t { TopLevel = 0, Embedded = 0x1, TopLevelAsymmetric = 0x2 }; constexpr static uint8_t table_type_mask = 0x3; + constexpr static uint8_t additional_prop_mask = 0x4; /// Construct a new freestanding top-level table with static /// lifetime. For debugging only. @@ -146,6 +147,15 @@ class Table { DataType get_dictionary_key_type(ColKey column_key) const noexcept; ColKey get_column_key(StringData name) const noexcept; ColKey get_column_key(StableIndex) const noexcept; + bool is_additional_props_col(ColKey ck) const + { + return ck == m_additional_prop_col; + } + ColKey get_additional_prop_col() const + { + return m_additional_prop_col; + } + ColKeys get_column_keys() const; typedef util::Optional> BacklinkOrigin; BacklinkOrigin find_backlink_origin(StringData origin_table_name, StringData origin_col_name) const noexcept; @@ -738,6 +748,7 @@ class Table { Array m_opposite_column; // 8th slot in m_top std::vector> m_index_accessors; ColKey m_primary_key_col; + ColKey m_additional_prop_col; Replication* const* m_repl; static Replication* g_dummy_replication; bool m_is_frozen = false; @@ -793,6 +804,7 @@ class Table { ColKey find_backlink_column(ColKey origin_col_key, TableKey origin_table) const; ColKey find_or_add_backlink_column(ColKey origin_col_key, TableKey origin_table); void do_set_primary_key_column(ColKey col_key); + void do_add_additional_prop_column(); void validate_column_is_unique(ColKey col_key) const; ObjKey get_next_valid_key(); @@ -954,6 +966,7 @@ class ColKeys { public: ColKeys(ConstTableRef&& t) : m_table(std::move(t)) + , m_offset(m_table->get_additional_prop_col() ? 1 : 0) { } @@ -964,7 +977,7 @@ class ColKeys { size_t size() const { - return m_table->get_column_count(); + return m_table->get_column_count() - m_offset; } bool empty() const { @@ -972,19 +985,20 @@ class ColKeys { } ColKey operator[](size_t p) const { - return ColKeyIterator(m_table, p).operator*(); + return ColKeyIterator(m_table, p + m_offset).operator*(); } ColKeyIterator begin() const { - return ColKeyIterator(m_table, 0); + return ColKeyIterator(m_table, m_offset); } ColKeyIterator end() const { - return ColKeyIterator(m_table, size()); + return ColKeyIterator(m_table, m_table->get_column_count()); } private: ConstTableRef m_table; + unsigned m_offset = 0; }; // Class used to collect a chain of links when building up a Query following links. diff --git a/src/realm/transaction.cpp b/src/realm/transaction.cpp index 9e93875923f..a1f62970e50 100644 --- a/src/realm/transaction.cpp +++ b/src/realm/transaction.cpp @@ -170,6 +170,7 @@ Transaction::Transaction(DBRef _db, SlabAlloc* alloc, DB::ReadLockInfo& rli, DB: { bool writable = stage == DB::transact_Writing; m_transact_stage = DB::transact_Ready; + m_allow_additional_properties = db->m_allow_flexible_schema; set_transact_stage(stage); attach_shared(m_read_lock.m_top_ref, m_read_lock.m_file_size, writable, VersionID{rli.m_version, rli.m_reader_idx}); diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index 3d94bb112d7..59b554dfc23 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -2603,6 +2603,12 @@ TEST_CASE("C API - properties", "[c_api]") { CHECK(strings.get() != list2.get()); } + SECTION("get_by_name") { + auto list2 = cptr_checked(realm_get_list_by_name(obj2.get(), "strings")); + CHECK(realm_equals(strings.get(), list2.get())); + CHECK(strings.get() != list2.get()); + } + SECTION("insert, then get") { write([&]() { CHECK(checked(realm_list_insert(strings.get(), 0, a))); @@ -3712,6 +3718,12 @@ TEST_CASE("C API - properties", "[c_api]") { CHECK(strings.get() != dict2.get()); } + SECTION("get by name") { + auto dict2 = cptr_checked(realm_get_dictionary_by_name(obj1.get(), "nullable_string_dict")); + CHECK(realm_equals(strings.get(), dict2.get())); + CHECK(strings.get() != dict2.get()); + } + SECTION("insert, then get, then erase") { write([&]() { bool inserted = false; @@ -5814,6 +5826,94 @@ TEST_CASE("C API: convert", "[c_api]") { realm_release(realm); } +TEST_CASE("C API: flexible schema", "[c_api]") { + TestFile test_file; + ObjectSchema object_schema = {"Foo", {{"_id", PropertyType::Int, Property::IsPrimary{true}}}}; + + auto config = make_config(test_file.path.c_str(), false); + config->schema = Schema{object_schema}; + config->schema_version = 0; + config->flexible_schema = 1; + auto realm = realm_open(config.get()); + realm_class_info_t class_foo; + bool found = false; + CHECK(checked(realm_find_class(realm, "Foo", &found, &class_foo))); + REQUIRE(found); + + SECTION("Simple set/get/delete") { + checked(realm_begin_write(realm)); + + realm_value_t pk = rlm_int_val(42); + auto obj1 = cptr_checked(realm_object_create_with_primary_key(realm, class_foo.key, pk)); + checked(realm_set_value_by_name(obj1.get(), "age", rlm_int_val(23))); + const char* prop_names[10]; + size_t actual; + realm_get_additional_properties(obj1.get(), prop_names, 10, &actual); + REQUIRE(actual == 1); + CHECK(prop_names[0] == std::string_view("age")); + realm_has_property(obj1.get(), "age", &found); + REQUIRE(found); + realm_has_property(obj1.get(), "_id", &found); + REQUIRE(found); + realm_has_property(obj1.get(), "weight", &found); + REQUIRE(!found); + + realm_value_t value; + CHECK(checked(realm_get_value_by_name(obj1.get(), "age", &value))); + CHECK(value.type == RLM_TYPE_INT); + CHECK(value.integer == 23); + + checked(realm_erase_additional_property(obj1.get(), "age")); + realm_get_additional_properties(obj1.get(), nullptr, 0, &actual); + REQUIRE(actual == 0); + realm_commit(realm); + } + + SECTION("Set/get nested list") { + checked(realm_begin_write(realm)); + + realm_value_t pk = rlm_int_val(42); + auto obj1 = cptr_checked(realm_object_create_with_primary_key(realm, class_foo.key, pk)); + auto list = cptr_checked(realm_set_list_by_name(obj1.get(), "scores")); + REQUIRE(list); + realm_has_property(obj1.get(), "scores", &found); + REQUIRE(found); + + realm_value_t value; + CHECK(checked(realm_get_value_by_name(obj1.get(), "scores", &value))); + CHECK(value.type == RLM_TYPE_LIST); + + auto list1 = cptr_checked(realm_get_list_by_name(obj1.get(), "scores")); + REQUIRE(list1); + + realm_commit(realm); + } + + SECTION("Set/get nested dictionary") { + checked(realm_begin_write(realm)); + + realm_value_t pk = rlm_int_val(42); + auto obj1 = cptr_checked(realm_object_create_with_primary_key(realm, class_foo.key, pk)); + auto dict = cptr_checked(realm_set_dictionary_by_name(obj1.get(), "properties")); + REQUIRE(dict); + realm_has_property(obj1.get(), "properties", &found); + REQUIRE(found); + + realm_value_t value; + CHECK(checked(realm_get_value_by_name(obj1.get(), "properties", &value))); + CHECK(value.type == RLM_TYPE_DICTIONARY); + + auto dict1 = cptr_checked(realm_get_dictionary_by_name(obj1.get(), "properties")); + REQUIRE(dict1); + + realm_commit(realm); + } + + realm_close(realm); + REQUIRE(realm_is_closed(realm)); + realm_release(realm); +} + struct Userdata { std::atomic called{false}; bool has_error; diff --git a/test/test_sync.cpp b/test/test_sync.cpp index 954dca2cb7f..b4d6f89208c 100644 --- a/test/test_sync.cpp +++ b/test/test_sync.cpp @@ -6207,6 +6207,70 @@ TEST(Sync_DeleteCollectionInCollection) } } +TEST(Sync_AdditionalProperties) +{ + DBOptions options; + options.allow_flexible_schema = true; + SHARED_GROUP_TEST_PATH(db_1_path); + SHARED_GROUP_TEST_PATH(db_2_path); + auto db_1 = DB::create(make_client_replication(), db_1_path, options); + auto db_2 = DB::create(make_client_replication(), db_2_path, options); + + TEST_DIR(dir); + fixtures::ClientServerFixture fixture{dir, test_context}; + fixture.start(); + + Session session_1 = fixture.make_session(db_1, "/test"); + Session session_2 = fixture.make_session(db_2, "/test"); + + write_transaction(db_1, [&](WriteTransaction& tr) { + auto& g = tr.get_group(); + auto table = g.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto foo = table->create_object_with_primary_key(123); + foo.set_any(col_any, "FooBar"); + foo.set("age", 10); + foo.set_collection("scores", CollectionType::List); + auto list = foo.get_list_ptr("scores"); + list->add(4.6); + }); + + session_1.wait_for_upload_complete_or_client_stopped(); + session_2.wait_for_download_complete_or_client_stopped(); + + write_transaction(db_2, [&](WriteTransaction& tr) { + auto table = tr.get_table("class_Table"); + CHECK_EQUAL(table->size(), 1); + + auto obj = table->get_object_with_primary_key(123); + auto col_keys = table->get_column_keys(); + CHECK_EQUAL(col_keys.size(), 2); + CHECK_EQUAL(table->get_column_name(col_keys[0]), "id"); + CHECK_EQUAL(table->get_column_name(col_keys[1]), "any"); + auto props = obj.get_additional_properties(); + CHECK_EQUAL(props.size(), 2); + CHECK_EQUAL(obj.get("age"), 10); + CHECK_EQUAL(obj.get_any("any"), Mixed("FooBar")); + auto list = obj.get_list_ptr("scores"); + CHECK_EQUAL(list->get(0), Mixed(4.6)); + CHECK_THROW_ANY(obj.get_any("some")); + CHECK_THROW_ANY(obj.erase_additional_prop("any")); + obj.erase_additional_prop("age"); + }); + + session_2.wait_for_upload_complete_or_client_stopped(); + session_1.wait_for_download_complete_or_client_stopped(); + + write_transaction(db_1, [&](WriteTransaction& tr) { + auto table = tr.get_table("class_Table"); + CHECK_EQUAL(table->size(), 1); + + auto obj = table->get_object_with_primary_key(123); + auto props = obj.get_additional_properties(); + CHECK_EQUAL(props.size(), 1); + }); +} + TEST(Sync_NestedCollectionClear) { TEST_CLIENT_DB(db_1);