diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4f380d674..8c3e480b407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,18 @@ # NEXT RELEASE ### Enhancements -* (PR [#????](https://github.com/realm/realm-core/pull/????)) +* Add support to synchronize collections embedded in Mixed properties and other collections (except sets) ([PR #7353](https://github.com/realm/realm-core/pull/7353)). +* Improve performance of change notifications on nested collections somewhat ([PR #7402](https://github.com/realm/realm-core/pull/7402)). * Added Resumption delay configuration to SyncClientTimeouts. ([PR #7441](https://github.com/realm/realm-core/pull/7441)) ### Fixed -* ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) -* None. +* Fixed conflict resolution bug which may result in an crash when the AddInteger instruction on Mixed properties is merged against updates to a non-integer type ([PR #7353](https://github.com/realm/realm-core/pull/7353)). +* Fix a spurious crash related to opening a Realm on background thread while the process was in the middle of exiting ([#7420](https://github.com/realm/realm-core/issues/7420jj)) +* Fix a data race in change notification delivery when running at debug log level ([PR #7402](https://github.com/realm/realm-core/pull/7402), since v14.0.0). +* Fix a 10-15% performance regression when reading data from the Realm resulting from Obj being made a non-trivial type ([PR #7402](https://github.com/realm/realm-core/pull/7402), since v14.0.0). ### Breaking changes -* None. +* Remove `realm_scheduler_set_default_factory()` and `realm_scheduler_has_default_factory()`, and change the `Scheduler` factory function to a bare function pointer rather than a `UniqueFunction` so that it does not have a non-trivial destructor. ### Compatibility * Fileformat: Generates files with format v24. Reads and automatically upgrade from fileformat v10. If you want to upgrade from an earlier file format version you will have to use RealmCore v13.x.y or earlier. @@ -17,7 +20,8 @@ ----------- ### Internals -* None. +* The CMake option `REALM_MONGODB_ENDPOINT` for running the object-store-tests against baas has been deprecated in favor of an environment variable of the same name ([PR #7423](https://github.com/realm/realm-core/pull/7423)). +* The object-store-tests test suite can now launch baas containers on its own by specifying a `BAASAAS_API_KEY` in the environment ([PR #7423](https://github.com/realm/realm-core/pull/7423)). ---------------------------------------------- diff --git a/dependencies.yml b/dependencies.yml index 04130370b31..8037f32918f 100644 --- a/dependencies.yml +++ b/dependencies.yml @@ -3,5 +3,5 @@ VERSION: 14.2.0 OPENSSL_VERSION: 3.2.0 ZLIB_VERSION: 1.2.13 # https://github.com/10gen/baas/commits -# dd016 is 2024 Feb 22 -BAAS_VERSION: dd01629d83b86292af9c59ebe2a28673c2e559cf +# acb71d0 is 2024 Mar 12 +BAAS_VERSION: acb71d0183b33eb304bb496390567efcfb8a6e60 diff --git a/evergreen/config.yml b/evergreen/config.yml index 49b952002d4..2b4831daf87 100644 --- a/evergreen/config.yml +++ b/evergreen/config.yml @@ -78,7 +78,6 @@ functions: if [ -z "${disable_tests_against_baas|}" ]; then scheme="http" set_cmake_var baas_vars REALM_ENABLE_AUTH_TESTS BOOL On - set_cmake_var baas_vars REALM_MONGODB_ENDPOINT STRING "$scheme://localhost:9090" if [ -n "${baas_admin_port|}" ]; then set_cmake_var baas_vars REALM_ADMIN_ENDPOINT STRING "$scheme://localhost:${baas_admin_port}" fi @@ -156,7 +155,7 @@ functions: if [ -n "${curl_base|}" ]; then set_cmake_var curl_vars CURL_LIBRARY PATH "$(./evergreen/abspath.sh ${curl_base}/lib/libcurl.dll.a)" set_cmake_var curl_vars CURL_INCLUDE_DIR PATH "$(./evergreen/abspath.sh ${curl_base}/include)" - set_cmake_var baas_vars REALM_CURL_CACERTS PATH "$(./evergreen/abspath.sh "${curl_base}/bin/cacert.pem")" + set_cmake_var baas_vars REALM_CURL_CACERTS PATH "$(./evergreen/abspath.sh "${curl_base}/bin/curl-ca-bundle.crt")" fi set_cmake_var realm_vars REALM_NO_TESTS BOOL ${no_tests|Off} @@ -208,10 +207,17 @@ functions: file: './realm-core/benchmark_results/results.latest.json' "run tests": + - command: expansions.update + params: + file: realm-core/dependencies.yml - command: shell.exec params: working_dir: realm-core shell: bash + env: + BAASAAS_API_KEY: "${baasaas_api_key}" + BAASAAS_REF_SPEC: "${BAAS_VERSION}" + BAASAAS_START_MODE: "githash" script: |- set -o errexit set -o verbose @@ -244,6 +250,12 @@ functions: TEST_FLAGS="--no-tests=error $TEST_FLAGS ${test_flags|}" + if [[ -n "${disable_tests_against_baas|}" ]]; then + unset BAASAAS_API_KEY + unset BAASAAS_REF_SPEC + unset BAASAAS_START_MODE + fi + if [[ -n "${llvm_symbolizer}" ]]; then export ASAN_SYMBOLIZER_PATH="$(./evergreen/abspath.sh ${llvm_symbolizer})" fi @@ -977,6 +989,7 @@ tasks: test_label: objstore-local test_executable_name: "realm-object-store-tests" verbose_test_output: true + disable_tests_against_baas: true - func: "check branch state" # These are baas object store tests that run against baas running on a remote host @@ -984,11 +997,9 @@ tasks: tags: [ "test_suite", "for_pull_requests", "requires_baas" ] exec_timeout_secs: 3600 commands: - - func: "launch remote baas" - func: "compile" vars: target_to_build: ObjectStoreTests - - func: "wait for remote baas to start" - func: "run tests" vars: test_label: objstore-baas diff --git a/how-to-build.md b/how-to-build.md index f394c7ddb1e..92bd693cee7 100644 --- a/how-to-build.md +++ b/how-to-build.md @@ -145,39 +145,41 @@ These are the available variables: testing process as soon as a check fails or an unexpected exception is thrown in a test. -## Running [app] tests against a local MongoDB BAAS - -Due to MongoDB security policies, running baas requires company issued AWS account credentials. -These are for MongoDB employees only, if you do not have these, reach out to #realm-core. -Once you have them, they need to be set in the shell environment. - -First, log in to aws using their command line tool. On mac this requries `brew install awscli`. -Then login using `aws configure` and input your access key and secret acess key. The other -configuration options can be left as none. This creates a correctly formatted file locally at -`~/.aws/credentials` which we will use later. - -If you do not want to install the aws command line tools, you can also create the aws file -manually in the correct location (`~/.aws/credentials`) with the following contents: +## Running [app] tests against a local BAAS instance +If you already have a baas instance running, you can specify that directly via the +`BAAS_BASE_URL` environment variable. You can run baas in a local docker container using +instructions from [the wiki](https://wiki.corp.mongodb.com/display/10GEN/%28Device+Sync%29+Using+Docker+to+run+a+BAAS+server+instance). ``` -AWS_ACCESS_KEY_ID = -AWS_SECRET_ACCESS_KEY = +export BAAS_BASE_URL=http://localhost:9090 +mkdir build.sync.ninja +cmake -B build.sync.ninja -G Ninja -DREALM_ENABLE_AUTH_TESTS=1 +cmake --build build.sync.ninja --target realm-object-store-tests +./build.sync.ninja/test/object-store/realm-object-store-tests -d=1 ``` -We use a script to fetch the dependencies for and run baas locally. Use the `-b sha` to use a particular version from https://github.com/10gen/baas/ -The script uses the configuration from https://github.com/10gen/baas/blob/master/etc/configs/test_rcore_config.json +## Running [app] tests against an on-demand BAASAAS container +Due to MongoDB security policies, running baas requires company issued credentials. +These are for MongoDB employees only, if you do not have these, reach out to +#appx-device-sync-internal. Once you have a baasaas API key, it needs to be set +in the shell environment. ``` -./evergreen/install_baas.sh -w baas -``` - -To run the [app] tests against the local baas, you need to configure a build with some cmake options to tell the tests where to point to. -``` +export BAASAAS_API_KEY= mkdir build.sync.ninja -cmake -B build.sync.ninja -G Ninja -DREALM_ENABLE_AUTH_TESTS=1 -DREALM_MONGODB_ENDPOINT=http://localhost:9090 +cmake -B build.sync.ninja -G Ninja -DREALM_ENABLE_AUTH_TESTS=1 cmake --build build.sync.ninja --target realm-object-store-tests ./build.sync.ninja/test/object-store/realm-object-store-tests -d=1 ``` +You can tell the object-store tests to use a specific version of baas with the +`BAASAAS_START_MODE` environment variable, which can either be `githash`, `patchid`, +or `branch`. If you specify a start mode, you need to tell it which githash or +branch name to start with via the `BAASAAS_REF_SPEC` environment variable. Omitting +these will use the latest available commit from the main branch of baas. + +If you've started a baasaas container already via the baasaas CLI, you can tell +the object-store tests to use that with the `BAASAAS_INSTANCE_ID` environment variable. + ### Developing inside a container diff --git a/src/realm.h b/src/realm.h index f105baca6a5..5cd5c24c5b0 100644 --- a/src/realm.h +++ b/src/realm.h @@ -953,33 +953,6 @@ RLM_API realm_scheduler_t* realm_scheduler_make_default(void); */ RLM_API const realm_scheduler_t* realm_scheduler_get_frozen(void); -/** - * Returns true if there is a default scheduler implementation for the current - * platform, or one has been set with `realm_scheduler_set_default_factory()`. - * - * If there is no default factory, and no scheduler is provided in the config, - * `realm_open()` will fail. Note that `realm_scheduler_get_frozen()` always - * returns a valid scheduler. - * - * This function is thread-safe, and cannot fail. - */ -RLM_API bool realm_scheduler_has_default_factory(void); - -/** - * For platforms with no default scheduler implementation, register a factory - * function which can produce custom schedulers. If there is a platform-specific - * scheduler, this function will fail. If a custom scheduler is desired for - * platforms that already have a default scheduler implementation, the caller - * must call `realm_open()` with a config that indicates the desired scheduler. - * - * The provided callback may produce a scheduler by calling - * `realm_scheduler_new()`. - * - * This function is thread-safe, but should generally only be called once. - */ -RLM_API bool realm_scheduler_set_default_factory(realm_userdata_t userdata, realm_free_userdata_func_t userdata_free, - realm_scheduler_default_factory_func_t); - /** * Open a Realm file. * diff --git a/src/realm/collection.cpp b/src/realm/collection.cpp index f43e6baf589..99553ec53ee 100644 --- a/src/realm/collection.cpp +++ b/src/realm/collection.cpp @@ -237,4 +237,25 @@ void Collection::get_any(QueryCtrlBlock& ctrl, Mixed val, size_t index) } } +UpdateStatus CollectionBase::do_init_from_parent(BPlusTreeBase* tree, ref_type ref, bool allow_create) +{ + if (ref) { + tree->init_from_ref(ref); + } + else { + if (tree->init_from_parent()) { + // All is well + return UpdateStatus::Updated; + } + if (!allow_create) { + tree->detach(); + return UpdateStatus::Detached; + } + // The ref in the column was NULL, create the tree in place. + tree->create(); + REALM_ASSERT(tree->is_attached()); + } + return UpdateStatus::Updated; +} + } // namespace realm diff --git a/src/realm/collection.hpp b/src/realm/collection.hpp index 20de7e448a4..6dbcebc7eaf 100644 --- a/src/realm/collection.hpp +++ b/src/realm/collection.hpp @@ -53,18 +53,18 @@ class DummyParent : public CollectionParent { { return m_obj; } + uint32_t parent_version() const noexcept final + { + return 0; + } protected: Obj m_obj; ref_type m_ref; - UpdateStatus update_if_needed_with_status() const final + UpdateStatus update_if_needed() const final { return UpdateStatus::Updated; } - bool update_if_needed() const final - { - return true; - } ref_type get_collection_ref(Index, CollectionType) const final { return m_ref; @@ -255,6 +255,7 @@ class CollectionBase : public Collection { CollectionBase& operator=(CollectionBase&&) noexcept = default; void validate_index(const char* msg, size_t index, size_t size) const; + static UpdateStatus do_init_from_parent(BPlusTreeBase* tree, ref_type ref, bool allow_create); }; inline std::string_view collection_type_name(CollectionType col_type, bool uppercase = false) @@ -492,7 +493,7 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { if (m_parent) { try { // Update the parent. Will throw if parent is not existing. - switch (m_parent->update_if_needed_with_status()) { + switch (m_parent->update_if_needed()) { case UpdateStatus::Updated: // Make sure to update next time around m_content_version = 0; @@ -524,7 +525,7 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { { try { // `has_changed()` sneakily modifies internal state. - update_if_needed_with_status(); + update_if_needed(); if (m_last_content_version != m_content_version) { m_last_content_version = m_content_version; return true; @@ -563,6 +564,11 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { m_content_version = 0; } + CollectionParent* get_owner() const noexcept + { + return m_parent; + } + void to_json(std::ostream&, JSONOutputMode, util::FunctionRef) const override; using Interface::get_owner_key; @@ -570,17 +576,15 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { using Interface::get_target_table; protected: - Obj m_obj_mem; + ObjCollectionParent m_obj_mem; std::shared_ptr m_col_parent; CollectionParent::Index m_index; - mutable size_t m_my_version = 0; ColKey m_col_key; - bool m_nullable = false; - mutable uint_fast64_t m_content_version = 0; - // Content version used by `has_changed()`. mutable uint_fast64_t m_last_content_version = 0; + mutable uint32_t m_parent_version = 0; + bool m_nullable = false; CollectionBaseImpl() = default; CollectionBaseImpl(const CollectionBaseImpl& other) @@ -650,13 +654,14 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { UpdateStatus get_update_status() const { - UpdateStatus status = m_parent ? m_parent->update_if_needed_with_status() : UpdateStatus::Detached; + UpdateStatus status = m_parent ? m_parent->update_if_needed() : UpdateStatus::Detached; if (status != UpdateStatus::Detached) { auto content_version = m_alloc->get_content_version(); - if (content_version != m_content_version || m_my_version != m_parent->m_parent_version) { + auto parent_version = m_parent->parent_version(); + if (content_version != m_content_version || m_parent_version != parent_version) { m_content_version = content_version; - m_my_version = m_parent->m_parent_version; + m_parent_version = parent_version; status = UpdateStatus::Updated; } } @@ -667,18 +672,14 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { /// Refresh the parent object (if needed) and compare version numbers. /// Return true if the collection should initialize from parent /// Throws if the owning object no longer exists. - bool should_update() + bool should_update() const { check_parent(); - bool changed = m_parent->update_if_needed(); // Throws if the object does not exist. - auto content_version = m_alloc->get_content_version(); - - if (changed || content_version != m_content_version || m_my_version != m_parent->m_parent_version) { - m_content_version = content_version; - m_my_version = m_parent->m_parent_version; - return true; + auto status = get_update_status(); + if (status == UpdateStatus::Detached) { + throw StaleAccessor("Parent no longer exists"); } - return false; + return status == UpdateStatus::Updated; } void bump_content_version() @@ -728,19 +729,19 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { void set_backlink(ColKey col_key, ObjLink new_link) const { check_parent(); - m_parent->set_backlink(col_key, new_link); + m_parent->get_object().set_backlink(col_key, new_link); } // Used when replacing a link, return true if CascadeState contains objects to remove bool replace_backlink(ColKey col_key, ObjLink old_link, ObjLink new_link, CascadeState& state) const { check_parent(); - return m_parent->replace_backlink(col_key, old_link, new_link, state); + return m_parent->get_object().replace_backlink(col_key, old_link, new_link, state); } // Used when removing a backlink, return true if CascadeState contains objects to remove bool remove_backlink(ColKey col_key, ObjLink old_link, CascadeState& state) const { check_parent(); - return m_parent->remove_backlink(col_key, old_link, state); + return m_parent->get_object().remove_backlink(col_key, old_link, state); } /// Reset the accessor's tracking of the content version. Derived classes @@ -796,7 +797,7 @@ class CollectionBaseImpl : public Interface, protected ArrayParent { /// /// If no change has happened to the data, this function returns /// `UpdateStatus::NoChange`, and the caller is allowed to not do anything. - virtual UpdateStatus update_if_needed_with_status() const = 0; + virtual UpdateStatus update_if_needed() const = 0; }; namespace _impl { @@ -884,7 +885,7 @@ class ObjCollectionBase : public Interface, public _impl::ObjListProxy { /// `BPlusTree`. virtual BPlusTree* get_mutable_tree() const = 0; - /// Implements update_if_needed() in a way that ensures the consistency of + /// Implements `update_if_needed()` in a way that ensures the consistency of /// the unresolved list. Derived classes should call this instead of calling /// `update_if_needed()` on their inner accessor. UpdateStatus update_if_needed() const diff --git a/src/realm/collection_parent.cpp b/src/realm/collection_parent.cpp index b54ac869e25..dc3ad806bc3 100644 --- a/src/realm/collection_parent.cpp +++ b/src/realm/collection_parent.cpp @@ -66,14 +66,7 @@ bool StablePath::is_prefix_of(const StablePath& other) const noexcept { if (size() > other.size()) return false; - - auto it = other.begin(); - for (auto& p : *this) { - if (!(p == *it)) - return false; - ++it; - } - return true; + return std::equal(begin(), end(), other.begin()); } /***************************** CollectionParent ******************************/ @@ -86,212 +79,81 @@ void CollectionParent::check_level() const throw LogicError(ErrorCodes::LimitExceeded, "Max nesting level reached"); } } -void CollectionParent::set_backlink(ColKey col_key, ObjLink new_link) const -{ - if (new_link && new_link.get_obj_key()) { - auto t = get_table(); - auto target_table = t->get_parent_group()->get_table(new_link.get_table_key()); - ColKey backlink_col_key; - auto type = col_key.get_type(); - if (type == col_type_TypedLink || type == col_type_Mixed || col_key.is_dictionary()) { - // This may modify the target table - backlink_col_key = target_table->find_or_add_backlink_column(col_key, t->get_key()); - // it is possible that this was a link to the same table and that adding a backlink column has - // caused the need to update this object as well. - update_if_needed(); - } - else { - backlink_col_key = t->get_opposite_column(col_key); - } - auto obj_key = new_link.get_obj_key(); - auto target_obj = obj_key.is_unresolved() ? target_table->try_get_tombstone(obj_key) - : target_table->try_get_object(obj_key); - if (!target_obj) { - throw InvalidArgument(ErrorCodes::KeyNotFound, "Target object not found"); - } - target_obj.add_backlink(backlink_col_key, get_object().get_key()); - } -} - -bool CollectionParent::replace_backlink(ColKey col_key, ObjLink old_link, ObjLink new_link, CascadeState& state) const -{ - bool recurse = remove_backlink(col_key, old_link, state); - set_backlink(col_key, new_link); - - return recurse; -} - -bool CollectionParent::remove_backlink(ColKey col_key, ObjLink old_link, CascadeState& state) const -{ - if (old_link && old_link.get_obj_key()) { - auto t = get_table(); - REALM_ASSERT(t->valid_column(col_key)); - ObjKey old_key = old_link.get_obj_key(); - auto target_obj = t->get_parent_group()->get_object(old_link); - TableRef target_table = target_obj.get_table(); - ColKey backlink_col_key; - auto type = col_key.get_type(); - if (type == col_type_TypedLink || type == col_type_Mixed || col_key.is_dictionary()) { - backlink_col_key = target_table->find_or_add_backlink_column(col_key, t->get_key()); - } - else { - backlink_col_key = t->get_opposite_column(col_key); - } - - bool strong_links = target_table->is_embedded(); - bool is_unres = old_key.is_unresolved(); - - bool last_removed = target_obj.remove_one_backlink(backlink_col_key, get_object().get_key()); // Throws - if (is_unres) { - if (last_removed) { - // Check is there are more backlinks - if (!target_obj.has_backlinks(false)) { - // Tombstones can be erased right away - there is no cascading effect - target_table->m_tombstones->erase(old_key, state); - } - } - } - else { - return state.enqueue_for_cascade(target_obj, strong_links, last_removed); - } - } - - return false; -} -LstBasePtr CollectionParent::get_listbase_ptr(ColKey col_key) const +template typename Collection, typename LinkCol> +std::unique_ptr create_collection(ColKey col_key, size_t level) { - auto table = get_table(); - auto attr = table->get_column_attr(col_key); - REALM_ASSERT(attr.test(col_attr_List) || attr.test(col_attr_Nullable)); - bool nullable = attr.test(col_attr_Nullable); - - switch (table->get_column_type(col_key)) { - case type_Int: { + bool nullable = col_key.get_attrs().test(col_attr_Nullable); + switch (col_key.get_type()) { + case col_type_Int: if (nullable) - return std::make_unique>>(col_key); - else - return std::make_unique>(col_key); - } - case type_Bool: { + return std::make_unique>>(col_key); + return std::make_unique>(col_key); + case col_type_Bool: if (nullable) - return std::make_unique>>(col_key); - else - return std::make_unique>(col_key); - } - case type_Float: { + return std::make_unique>>(col_key); + return std::make_unique>(col_key); + case col_type_Float: if (nullable) - return std::make_unique>>(col_key); - else - return std::make_unique>(col_key); - } - case type_Double: { + return std::make_unique>>(col_key); + return std::make_unique>(col_key); + case col_type_Double: if (nullable) - return std::make_unique>>(col_key); - else - return std::make_unique>(col_key); - } - case type_String: { - return std::make_unique>(col_key); - } - case type_Binary: { - return std::make_unique>(col_key); - } - case type_Timestamp: { - return std::make_unique>(col_key); - } - case type_Decimal: { - return std::make_unique>(col_key); - } - case type_ObjectId: { + return std::make_unique>>(col_key); + return std::make_unique>(col_key); + case col_type_String: + return std::make_unique>(col_key); + case col_type_Binary: + return std::make_unique>(col_key); + case col_type_Timestamp: + return std::make_unique>(col_key); + case col_type_Decimal: + return std::make_unique>(col_key); + case col_type_ObjectId: if (nullable) - return std::make_unique>>(col_key); - else - return std::make_unique>(col_key); - } - case type_UUID: { + return std::make_unique>>(col_key); + return std::make_unique>(col_key); + case col_type_UUID: if (nullable) - return std::make_unique>>(col_key); - else - return std::make_unique>(col_key); - } - case type_TypedLink: { - return std::make_unique>(col_key); - } - case type_Mixed: { - return std::make_unique>(col_key, get_level() + 1); - } - case type_Link: - return std::make_unique(col_key); + return std::make_unique>>(col_key); + return std::make_unique>(col_key); + case col_type_TypedLink: + return std::make_unique>(col_key); + case col_type_Mixed: + return std::make_unique>(col_key, level + 1); + case col_type_Link: + return std::make_unique(col_key); + default: + REALM_TERMINATE("Unsupported column type."); } - REALM_TERMINATE("Unsupported column type"); } -SetBasePtr CollectionParent::get_setbase_ptr(ColKey col_key) const +LstBasePtr CollectionParent::get_listbase_ptr(ColKey col_key, size_t level) { - auto table = get_table(); - auto attr = table->get_column_attr(col_key); - REALM_ASSERT(attr.test(col_attr_Set)); - bool nullable = attr.test(col_attr_Nullable); + REALM_ASSERT(col_key.get_attrs().test(col_attr_List) || col_key.get_type() == col_type_Mixed); + return create_collection(col_key, level); +} - switch (table->get_column_type(col_key)) { - case type_Int: - if (nullable) - return std::make_unique>>(col_key); - return std::make_unique>(col_key); - case type_Bool: - if (nullable) - return std::make_unique>>(col_key); - return std::make_unique>(col_key); - case type_Float: - if (nullable) - return std::make_unique>>(col_key); - return std::make_unique>(col_key); - case type_Double: - if (nullable) - return std::make_unique>>(col_key); - return std::make_unique>(col_key); - case type_String: - return std::make_unique>(col_key); - case type_Binary: - return std::make_unique>(col_key); - case type_Timestamp: - return std::make_unique>(col_key); - case type_Decimal: - return std::make_unique>(col_key); - case type_ObjectId: - if (nullable) - return std::make_unique>>(col_key); - return std::make_unique>(col_key); - case type_UUID: - if (nullable) - return std::make_unique>>(col_key); - return std::make_unique>(col_key); - case type_TypedLink: - return std::make_unique>(col_key); - case type_Mixed: - return std::make_unique>(col_key); - case type_Link: - return std::make_unique(col_key); - } - REALM_TERMINATE("Unsupported column type."); +SetBasePtr CollectionParent::get_setbase_ptr(ColKey col_key, size_t level) +{ + REALM_ASSERT(col_key.get_attrs().test(col_attr_Set)); + return create_collection(col_key, level); } -CollectionBasePtr CollectionParent::get_collection_ptr(ColKey col_key) const +CollectionBasePtr CollectionParent::get_collection_ptr(ColKey col_key, size_t level) { if (col_key.is_list()) { - return get_listbase_ptr(col_key); + return get_listbase_ptr(col_key, level); } else if (col_key.is_set()) { - return get_setbase_ptr(col_key); + return get_setbase_ptr(col_key, level); } else if (col_key.is_dictionary()) { - return std::make_unique(col_key, get_level() + 1); + return std::make_unique(col_key, level + 1); } return {}; } - int64_t CollectionParent::generate_key(size_t sz) { static std::mt19937 gen32; @@ -314,5 +176,13 @@ int64_t CollectionParent::generate_key(size_t sz) return key; } +void CollectionParent::set_key(BPlusTreeMixed& tree, size_t index) +{ + int64_t key = generate_key(tree.size()); + while (tree.find_key(key) != realm::not_found) { + key++; + } + tree.set_key(index, key); +} } // namespace realm diff --git a/src/realm/collection_parent.hpp b/src/realm/collection_parent.hpp index d7a6f9d1e06..53fe3ecb711 100644 --- a/src/realm/collection_parent.hpp +++ b/src/realm/collection_parent.hpp @@ -20,15 +20,16 @@ #define REALM_COLLECTION_PARENT_HPP #include -#include -#include #include +#include +#include namespace realm { class Obj; class Replication; class CascadeState; +class BPlusTreeMixed; class Collection; class CollectionBase; @@ -74,7 +75,7 @@ class CollectionParent : public std::enable_shared_from_this { using Index = StableIndex; // Return the nesting level of the parent - size_t get_level() const noexcept + uint8_t get_level() const noexcept { return m_level; } @@ -95,6 +96,13 @@ class CollectionParent : public std::enable_shared_from_this { /// Get table of owning object virtual TableRef get_table() const noexcept = 0; + static LstBasePtr get_listbase_ptr(ColKey col_key, size_t level); + static SetBasePtr get_setbase_ptr(ColKey col_key, size_t level); + static CollectionBasePtr get_collection_ptr(ColKey col_key, size_t level); + + static int64_t generate_key(size_t sz); + static void set_key(BPlusTreeMixed& tree, size_t index); + protected: friend class Collection; template @@ -106,10 +114,9 @@ class CollectionParent : public std::enable_shared_from_this { #else static constexpr size_t s_max_level = 100; #endif - size_t m_level = 0; - mutable size_t m_parent_version = 0; + uint8_t m_level = 0; - constexpr CollectionParent(size_t level = 0) + constexpr CollectionParent(uint8_t level = 0) : m_level(level) { } @@ -117,10 +124,7 @@ class CollectionParent : public std::enable_shared_from_this { virtual ~CollectionParent(); /// Update the accessor (and return `UpdateStatus::Detached` if the // collection is not initialized. - virtual UpdateStatus update_if_needed_with_status() const = 0; - /// Check if the storage version has changed and update if it has - /// Return true if the object was updated - virtual bool update_if_needed() const = 0; + virtual UpdateStatus update_if_needed() const = 0; /// Get owning object virtual const Obj& get_object() const noexcept = 0; /// Get the top ref from pareht @@ -133,18 +137,8 @@ class CollectionParent : public std::enable_shared_from_this { /// Set the top ref in parent virtual void set_collection_ref(Index, ref_type ref, CollectionType) = 0; - // Used when inserting a new link. You will not remove existing links in this process - void set_backlink(ColKey col_key, ObjLink new_link) const; - // Used when replacing a link, return true if CascadeState contains objects to remove - bool replace_backlink(ColKey col_key, ObjLink old_link, ObjLink new_link, CascadeState& state) const; - // Used when removing a backlink, return true if CascadeState contains objects to remove - bool remove_backlink(ColKey col_key, ObjLink old_link, CascadeState& state) const; - - LstBasePtr get_listbase_ptr(ColKey col_key) const; - SetBasePtr get_setbase_ptr(ColKey col_key) const; - CollectionBasePtr get_collection_ptr(ColKey col_key) const; - - static int64_t generate_key(size_t sz); + /// Get the counter which is incremented whenever the root Obj is updated. + virtual uint32_t parent_version() const noexcept = 0; }; } // namespace realm diff --git a/src/realm/dictionary.cpp b/src/realm/dictionary.cpp index 854d51cfc6f..dbc9da3aba9 100644 --- a/src/realm/dictionary.cpp +++ b/src/realm/dictionary.cpp @@ -441,11 +441,7 @@ void Dictionary::insert_collection(const PathElement& path_elem, CollectionType if (!old_val || *old_val != new_val) { m_values->ensure_keys(); auto [it, inserted] = insert(path_elem.get_key(), new_val); - int64_t key = generate_key(size()); - while (m_values->find_key(key) != realm::not_found) { - key++; - } - m_values->set_key(it.index(), key); + set_key(*m_values, it.index()); } } @@ -651,10 +647,9 @@ size_t Dictionary::find_index(const Index& index) const return m_values->find_key(index.get_salt()); } -UpdateStatus Dictionary::update_if_needed_with_status() const +UpdateStatus Dictionary::do_update_if_needed(bool allow_create) const { - auto status = Base::get_update_status(); - switch (status) { + switch (get_update_status()) { case UpdateStatus::Detached: { m_dictionary_top.reset(); return UpdateStatus::Detached; @@ -667,27 +662,23 @@ UpdateStatus Dictionary::update_if_needed_with_status() const // perform lazy initialization by treating it as an update. [[fallthrough]]; } - case UpdateStatus::Updated: { - // Try to initialize. If the dictionary is not initialized - // the function will return false; - bool attached = init_from_parent(false); - Base::update_content_version(); - CollectionParent::m_parent_version++; - return attached ? UpdateStatus::Updated : UpdateStatus::Detached; - } + case UpdateStatus::Updated: + return init_from_parent(allow_create); } REALM_UNREACHABLE(); } +UpdateStatus Dictionary::update_if_needed() const +{ + constexpr bool allow_create = false; + return do_update_if_needed(allow_create); +} + void Dictionary::ensure_created() { - if (Base::should_update() || !(m_dictionary_top && m_dictionary_top->is_attached())) { - // When allow_create is true, init_from_parent will always succeed - // In case of errors, an exception is thrown. - constexpr bool allow_create = true; - init_from_parent(allow_create); // Throws - CollectionParent::m_parent_version++; - Base::update_content_version(); + constexpr bool allow_create = true; + if (do_update_if_needed(allow_create) == UpdateStatus::Detached) { + throw StaleAccessor("Dictionary no longer exists"); } } @@ -707,7 +698,6 @@ bool Dictionary::try_erase(Mixed key) return true; } - void Dictionary::erase(Mixed key) { if (!try_erase(key)) { @@ -837,8 +827,9 @@ void Dictionary::clear() } } -bool Dictionary::init_from_parent(bool allow_create) const +UpdateStatus Dictionary::init_from_parent(bool allow_create) const { + Base::update_content_version(); try { auto ref = Base::get_collection_ref(); if ((ref || allow_create) && !m_dictionary_top) { @@ -871,7 +862,7 @@ bool Dictionary::init_from_parent(bool allow_create) const // dictionary detached if (!allow_create) { m_dictionary_top.reset(); - return false; + return UpdateStatus::Detached; } // Create dictionary @@ -881,7 +872,7 @@ bool Dictionary::init_from_parent(bool allow_create) const m_dictionary_top->update_parent(); } - return true; + return UpdateStatus::Updated; } catch (...) { m_dictionary_top.reset(); @@ -1179,7 +1170,6 @@ ref_type Dictionary::get_collection_ref(Index index, CollectionType type) const throw realm::IllegalOperation(util::format("Not a %1", type)); } throw StaleAccessor("This collection is no more"); - return 0; } bool Dictionary::check_collection_ref(Index index, CollectionType type) const noexcept @@ -1200,15 +1190,6 @@ void Dictionary::set_collection_ref(Index index, ref_type ref, CollectionType ty m_values->set(ndx, Mixed(ref, type)); } -bool Dictionary::update_if_needed() const -{ - auto status = update_if_needed_with_status(); - if (status == UpdateStatus::Detached) { - throw StaleAccessor("CollectionList no longer exists"); - } - return status == UpdateStatus::Updated; -} - /************************* DictionaryLinkValues *************************/ DictionaryLinkValues::DictionaryLinkValues(const Obj& obj, ColKey col_key) diff --git a/src/realm/dictionary.hpp b/src/realm/dictionary.hpp index 3cf08e9b91c..ee313c9453a 100644 --- a/src/realm/dictionary.hpp +++ b/src/realm/dictionary.hpp @@ -39,10 +39,7 @@ class Dictionary final : public CollectionBaseImpl, public Colle using Base = CollectionBaseImpl; class Iterator; - Dictionary() - : CollectionParent(0) - { - } + Dictionary() = default; ~Dictionary(); Dictionary(const Obj& obj, ColKey col_key) @@ -213,12 +210,15 @@ class Dictionary final : public CollectionBaseImpl, public Colle { return get_obj().get_table(); } - UpdateStatus update_if_needed_with_status() const override; - bool update_if_needed() const override; + UpdateStatus update_if_needed() const override; const Obj& get_object() const noexcept override { return get_obj(); } + uint32_t parent_version() const noexcept override + { + return m_parent_version; + } ref_type get_collection_ref(Index, CollectionType) const override; bool check_collection_ref(Index, CollectionType) const noexcept override; void set_collection_ref(Index, ref_type ref, CollectionType) override; @@ -227,6 +227,8 @@ class Dictionary final : public CollectionBaseImpl, public Colle void to_json(std::ostream&, JSONOutputMode, util::FunctionRef) const override; private: + using Base::set_collection; + template friend class CollectionColumnAggregate; friend class DictionaryLinkValues; @@ -239,7 +241,7 @@ class Dictionary final : public CollectionBaseImpl, public Colle Dictionary(Allocator& alloc, ColKey col_key, ref_type ref); - bool init_from_parent(bool allow_create) const; + UpdateStatus init_from_parent(bool allow_create) const; Mixed do_get(size_t ndx) const; void do_erase(size_t ndx, Mixed key); Mixed do_get_key(size_t ndx) const; @@ -261,12 +263,14 @@ class Dictionary final : public CollectionBaseImpl, public Colle void do_accumulate(size_t* return_ndx, AggregateType& agg) const; void ensure_created(); - inline bool update() const + bool update() const { - return update_if_needed_with_status() != UpdateStatus::Detached; + return update_if_needed() != UpdateStatus::Detached; } void verify() const; void get_key_type(); + + UpdateStatus do_update_if_needed(bool allow_create) const; }; class Dictionary::Iterator { @@ -459,7 +463,7 @@ class DictionaryLinkValues final : public ObjCollectionBase { // Overrides of ObjCollectionBase: UpdateStatus do_update_if_needed() const final { - return m_source.update_if_needed_with_status(); + return m_source.update_if_needed(); } BPlusTree* get_mutable_tree() const final { diff --git a/src/realm/list.cpp b/src/realm/list.cpp index 2bd3ffff183..5a5482b70dc 100644 --- a/src/realm/list.cpp +++ b/src/realm/list.cpp @@ -347,45 +347,30 @@ void Lst::do_remove(size_t ndx) /******************************** Lst *********************************/ -bool Lst::init_from_parent(bool allow_create) const +UpdateStatus Lst::init_from_parent(bool allow_create) const { + Base::update_content_version(); + if (!m_tree) { m_tree.reset(new BPlusTreeMixed(get_alloc())); const ArrayParent* parent = this; m_tree->set_parent(const_cast(parent), 0); } try { - auto ref = Base::get_collection_ref(); - if (ref) { - m_tree->init_from_ref(ref); - } - else { - if (!allow_create) { - m_tree->detach(); - return false; - } - - // The ref in the column was NULL, create the tree in place. - m_tree->create(); - REALM_ASSERT(m_tree->is_attached()); - } + return do_init_from_parent(m_tree.get(), Base::get_collection_ref(), allow_create); } catch (...) { m_tree->detach(); throw; } - - return true; } -UpdateStatus Lst::update_if_needed_with_status() const +UpdateStatus Lst::update_if_needed() const { - auto status = Base::get_update_status(); - switch (status) { - case UpdateStatus::Detached: { + switch (get_update_status()) { + case UpdateStatus::Detached: m_tree.reset(); return UpdateStatus::Detached; - } case UpdateStatus::NoChange: if (m_tree && m_tree->is_attached()) { return UpdateStatus::NoChange; @@ -393,12 +378,8 @@ UpdateStatus Lst::update_if_needed_with_status() const // The tree has not been initialized yet for this accessor, so // perform lazy initialization by treating it as an update. [[fallthrough]]; - case UpdateStatus::Updated: { - bool attached = init_from_parent(false); - Base::update_content_version(); - CollectionParent::m_parent_version++; - return attached ? UpdateStatus::Updated : UpdateStatus::Detached; - } + case UpdateStatus::Updated: + return init_from_parent(false); } REALM_UNREACHABLE(); } @@ -553,11 +534,7 @@ void Lst::insert_collection(const PathElement& path_elem, CollectionType check_level(); m_tree->ensure_keys(); insert(path_elem.get_ndx(), Mixed(0, dict_or_list)); - int64_t key = generate_key(size()); - while (m_tree->find_key(key) != realm::not_found) { - key++; - } - m_tree->set_key(path_elem.get_ndx(), key); + set_key(*m_tree, path_elem.get_ndx()); bump_content_version(); } @@ -579,11 +556,7 @@ void Lst::set_collection(const PathElement& path_elem, CollectionType dic set(ndx, new_val); int64_t key = m_tree->get_key(ndx); if (key == 0) { - key = generate_key(size()); - while (m_tree->find_key(key) != realm::not_found) { - key++; - } - m_tree->set_key(ndx, key); + set_key(*m_tree, path_elem.get_ndx()); } bump_content_version(); } @@ -898,15 +871,6 @@ bool Lst::remove_backlinks(CascadeState& state) const return recurse; } -bool Lst::update_if_needed() const -{ - auto status = update_if_needed_with_status(); - if (status == UpdateStatus::Detached) { - throw StaleAccessor("CollectionList no longer exists"); - } - return status == UpdateStatus::Updated; -} - /********************************** LnkLst ***********************************/ Obj LnkLst::create_and_insert_linked_object(size_t ndx) diff --git a/src/realm/list.hpp b/src/realm/list.hpp index d3bca568116..fec6505c030 100644 --- a/src/realm/list.hpp +++ b/src/realm/list.hpp @@ -183,7 +183,7 @@ class Lst final : public CollectionBaseImpl { return *m_tree; } - UpdateStatus update_if_needed_with_status() const final + UpdateStatus update_if_needed() const final { auto status = Base::get_update_status(); switch (status) { @@ -199,9 +199,7 @@ class Lst final : public CollectionBaseImpl { // perform lazy initialization by treating it as an update. [[fallthrough]]; case UpdateStatus::Updated: { - bool attached = init_from_parent(false); - Base::update_content_version(); - return attached ? UpdateStatus::Updated : UpdateStatus::Detached; + return init_from_parent(false); } } REALM_UNREACHABLE(); @@ -214,14 +212,13 @@ class Lst final : public CollectionBaseImpl { // In case of errors, an exception is thrown. constexpr bool allow_create = true; init_from_parent(allow_create); // Throws - Base::update_content_version(); } } /// Update the accessor and return true if it is attached after the update. inline bool update() const { - return update_if_needed_with_status() != UpdateStatus::Detached; + return update_if_needed() != UpdateStatus::Detached; } size_t translate_index(size_t ndx) const noexcept override @@ -255,28 +252,15 @@ class Lst final : public CollectionBaseImpl { using Base::m_col_key; using Base::m_nullable; - bool init_from_parent(bool allow_create) const + UpdateStatus init_from_parent(bool allow_create) const { if (!m_tree) { m_tree.reset(new BPlusTree(get_alloc())); const ArrayParent* parent = this; m_tree->set_parent(const_cast(parent), 0); } - - if (m_tree->init_from_parent()) { - // All is well - return true; - } - - if (!allow_create) { - m_tree->detach(); - return false; - } - - // The ref in the column was NULL, create the tree in place. - m_tree->create(); - REALM_ASSERT(m_tree->is_attached()); - return true; + Base::update_content_version(); + return do_init_from_parent(m_tree.get(), 0, allow_create); } template @@ -448,7 +432,7 @@ class Lst final : public CollectionBaseImpl, public CollectionPa return *m_tree; } - UpdateStatus update_if_needed_with_status() const final; + UpdateStatus update_if_needed() const final; void ensure_created() { @@ -457,15 +441,13 @@ class Lst final : public CollectionBaseImpl, public CollectionPa // In case of errors, an exception is thrown. constexpr bool allow_create = true; init_from_parent(allow_create); // Throws - Base::update_content_version(); - CollectionParent::m_parent_version++; } } /// Update the accessor and return true if it is attached after the update. inline bool update() const { - return update_if_needed_with_status() != UpdateStatus::Detached; + return update_if_needed() != UpdateStatus::Detached; } // Overriding members in CollectionParent @@ -500,11 +482,14 @@ class Lst final : public CollectionBaseImpl, public CollectionPa { return get_obj().get_table(); } - bool update_if_needed() const override; const Obj& get_object() const noexcept override { return get_obj(); } + uint32_t parent_version() const noexcept override + { + return m_parent_version; + } ref_type get_collection_ref(Index, CollectionType) const override; bool check_collection_ref(Index, CollectionType) const noexcept override; void set_collection_ref(Index, ref_type ref, CollectionType) override; @@ -527,7 +512,7 @@ class Lst final : public CollectionBaseImpl, public CollectionPa using Base::m_col_key; using Base::m_nullable; - bool init_from_parent(bool allow_create) const; + UpdateStatus init_from_parent(bool allow_create) const; template void find_all_mixed_unresolved_links(Func&& func) const @@ -644,7 +629,7 @@ class LnkLst final : public ObjCollectionBase { } void add(const Obj& obj) { - if (get_target_table()->get_key() != obj.get_table_key()) { + if (get_target_table() != obj.get_table()) { throw InvalidArgument("LnkLst::add: Wrong object type"); } add(obj.get_key()); @@ -800,7 +785,7 @@ class LnkLst final : public ObjCollectionBase { UpdateStatus do_update_if_needed() const final { - return m_list.update_if_needed_with_status(); + return m_list.update_if_needed(); } BPlusTree* get_mutable_tree() const final diff --git a/src/realm/obj.cpp b/src/realm/obj.cpp index 7c5177a0514..d0e1c3c0bd2 100644 --- a/src/realm/obj.cpp +++ b/src/realm/obj.cpp @@ -385,7 +385,7 @@ bool Obj::update() const if (changes) { m_mem = new_obj.m_mem; m_row_ndx = new_obj.m_row_ndx; - CollectionParent::m_parent_version++; + ++m_version_counter; } // Always update versions m_storage_version = new_obj.m_storage_version; @@ -402,14 +402,14 @@ inline bool Obj::_update_if_needed() const return false; } -UpdateStatus Obj::update_if_needed_with_status() const +UpdateStatus Obj::update_if_needed() const { if (!m_table) { // Table deleted return UpdateStatus::Detached; } - auto current_version = get_alloc().get_storage_version(); + auto current_version = _get_alloc().get_storage_version(); if (current_version != m_storage_version) { ClusterNode::State state = get_tree_top()->try_get(m_key); @@ -423,13 +423,21 @@ UpdateStatus Obj::update_if_needed_with_status() const if ((m_mem.get_addr() != state.mem.get_addr()) || (m_row_ndx != state.index)) { m_mem = state.mem; m_row_ndx = state.index; - CollectionParent::m_parent_version++; + ++m_version_counter; return UpdateStatus::Updated; } } return UpdateStatus::NoChange; } +void Obj::checked_update_if_needed() const +{ + if (update_if_needed() == UpdateStatus::Detached) { + m_table.check(); + get_tree_top()->get(m_key); // should always throw + } +} + template T Obj::get(ColKey col_key) const { @@ -704,7 +712,7 @@ Obj Obj::_get_linked_object(ColKey link_col_key, Mixed link) const Obj Obj::get_parent_object() const { Obj obj; - update_if_needed(); + checked_update_if_needed(); if (!m_table->is_embedded()) { throw LogicError(ErrorCodes::TopLevelObject, "Object is not embedded"); @@ -747,7 +755,7 @@ size_t Obj::get_link_count(ColKey col_key) const bool Obj::is_null(ColKey col_key) const { - update_if_needed(); + checked_update_if_needed(); ColumnAttrMask attr = col_key.get_attrs(); ColKey::Idx col_ndx = col_key.get_index(); if (attr.test(col_attr_Nullable) && !attr.test(col_attr_Collection)) { @@ -802,7 +810,7 @@ bool Obj::has_backlinks(bool only_strong_links) const size_t Obj::get_backlink_count() const { - update_if_needed(); + checked_update_if_needed(); size_t cnt = 0; m_table->for_each_backlink_column([&](ColKey backlink_col_key) { @@ -814,7 +822,7 @@ size_t Obj::get_backlink_count() const size_t Obj::get_backlink_count(const Table& origin, ColKey origin_col_key) const { - update_if_needed(); + checked_update_if_needed(); size_t cnt = 0; if (TableKey origin_table_key = origin.get_key()) { @@ -867,7 +875,7 @@ ObjKey Obj::get_backlink(ColKey backlink_col, size_t backlink_ndx) const std::vector Obj::get_all_backlinks(ColKey backlink_col) const { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(backlink_col); Allocator& alloc = get_alloc(); @@ -1099,7 +1107,7 @@ StablePath Obj::get_stable_path() const noexcept return {}; } -void Obj::add_index(Path& path, const Index& index) const +void Obj::add_index(Path& path, const CollectionParent::Index& index) const { if (path.empty()) { path.emplace_back(get_table()->get_column_key(index)); @@ -1151,7 +1159,7 @@ REALM_FORCEINLINE void Obj::sync(Node& arr) template <> Obj& Obj::set(ColKey col_key, Mixed value, bool is_default) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); auto type = col_key.get_type(); auto col_ndx = col_key.get_index(); @@ -1284,7 +1292,7 @@ Obj& Obj::set_any(ColKey col_key, Mixed value, bool is_default) template <> Obj& Obj::set(ColKey col_key, int64_t value, bool is_default) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); auto col_ndx = col_key.get_index(); @@ -1328,7 +1336,7 @@ Obj& Obj::set(ColKey col_key, int64_t value, bool is_default) Obj& Obj::add_int(ColKey col_key, int64_t value) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); auto col_ndx = col_key.get_index(); @@ -1406,7 +1414,7 @@ Obj& Obj::add_int(ColKey col_key, int64_t value) template <> Obj& Obj::set(ColKey col_key, ObjKey target_key, bool is_default) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); ColKey::Idx col_ndx = col_key.get_index(); ColumnType type = col_key.get_type(); @@ -1460,7 +1468,7 @@ Obj& Obj::set(ColKey col_key, ObjKey target_key, bool is_default) template <> Obj& Obj::set(ColKey col_key, ObjLink target_link, bool is_default) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); ColKey::Idx col_ndx = col_key.get_index(); ColumnType type = col_key.get_type(); @@ -1504,7 +1512,7 @@ Obj& Obj::set(ColKey col_key, ObjLink target_link, bool is_default) Obj Obj::create_and_set_linked_object(ColKey col_key, bool is_default) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); ColKey::Idx col_ndx = col_key.get_index(); ColumnType type = col_key.get_type(); @@ -1601,7 +1609,7 @@ inline void Obj::set_spec(ArrayString& values, ColKey col_key) template <> Obj& Obj::set(ColKey col_key, Geospatial value, bool) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); auto type = col_key.get_type(); @@ -1621,7 +1629,7 @@ Obj& Obj::set(ColKey col_key, Geospatial value, bool) template <> Obj& Obj::set(ColKey col_key, std::optional value, bool) { - update_if_needed(); + checked_update_if_needed(); auto table = get_table(); table->check_column(col_key); auto type = col_key.get_type(); @@ -1652,7 +1660,7 @@ Obj& Obj::set(ColKey col_key, std::optional value, bool) template Obj& Obj::set(ColKey col_key, T value, bool is_default) { - update_if_needed(); + checked_update_if_needed(); get_table()->check_column(col_key); auto type = col_key.get_type(); auto attrs = col_key.get_attrs(); @@ -1660,7 +1668,7 @@ Obj& Obj::set(ColKey col_key, T value, bool is_default) if (type != ColumnTypeTraits::column_id) throw InvalidArgument(ErrorCodes::TypeMismatch, - util::format("Property not a %1", ColumnTypeTraits::column_id)); + util::format("Property not a %1", ColumnTypeTraits::column_id)); if (value_is_null(value) && !attrs.test(col_attr_Nullable)) throw NotNullable(Group::table_name_to_class_name(m_table->get_name()), m_table->get_column_name(col_key)); @@ -1705,7 +1713,7 @@ INSTANTIATE_OBJ_SET(UUID); void Obj::set_int(ColKey::Idx col_ndx, int64_t value) { - update_if_needed(); + checked_update_if_needed(); Allocator& alloc = get_alloc(); alloc.bump_content_version(); @@ -1722,7 +1730,7 @@ void Obj::set_int(ColKey::Idx col_ndx, int64_t value) void Obj::set_ref(ColKey::Idx col_ndx, ref_type value, CollectionType type) { - update_if_needed(); + checked_update_if_needed(); Allocator& alloc = get_alloc(); alloc.bump_content_version(); @@ -1957,14 +1965,14 @@ void Obj::handle_multiple_backlinks_during_schema_migration() LstBasePtr Obj::get_listbase_ptr(ColKey col_key) const { - auto list = CollectionParent::get_listbase_ptr(col_key); + auto list = CollectionParent::get_listbase_ptr(col_key, 0); list->set_owner(*this, col_key); return list; } SetBasePtr Obj::get_setbase_ptr(ColKey col_key) const { - auto set = CollectionParent::get_setbase_ptr(col_key); + auto set = CollectionParent::get_setbase_ptr(col_key, 0); set->set_owner(*this, col_key); return set; } @@ -1972,7 +1980,7 @@ SetBasePtr Obj::get_setbase_ptr(ColKey col_key) const Dictionary Obj::get_dictionary(ColKey col_key) const { REALM_ASSERT(col_key.is_dictionary() || col_key.get_type() == col_type_Mixed); - update_if_needed(); + checked_update_if_needed(); return Dictionary(Obj(*this), col_key); } @@ -1983,7 +1991,7 @@ Obj& Obj::set_collection(ColKey col_key, CollectionType type) (col_key.is_list() && type == CollectionType::List)) { return *this; } - update_if_needed(); + checked_update_if_needed(); Mixed new_val(0, type); if (type == CollectionType::Set) { @@ -2023,7 +2031,7 @@ Obj& Obj::set_collection(ColKey col_key, CollectionType type) values.init_from_parent(); values.set(m_row_ndx, new_val); - values.set_key(m_row_ndx, generate_key(0x10)); + values.set_key(m_row_ndx, CollectionParent::generate_key(0x10)); sync(fields); @@ -2131,7 +2139,7 @@ CollectionPtr Obj::get_collection_by_stable_path(const StablePath& path) const CollectionBasePtr Obj::get_collection_ptr(ColKey col_key) const { if (col_key.is_collection()) { - auto collection = CollectionParent::get_collection_ptr(col_key); + auto collection = CollectionParent::get_collection_ptr(col_key, 0); collection->set_owner(*this, col_key); return collection; } @@ -2357,7 +2365,7 @@ Obj& Obj::set_null(ColKey col_key, bool is_default) m_table->get_column_name(col_key)); } - update_if_needed(); + checked_update_if_needed(); SearchIndex* index = m_table->get_search_index(col_key); if (index && !m_key.is_unresolved()) { @@ -2431,7 +2439,7 @@ ref_type Obj::Internal::get_ref(const Obj& obj, ColKey col_key) return to_ref(obj._get(col_key.get_index())); } -ref_type Obj::get_collection_ref(Index index, CollectionType type) const +ref_type Obj::get_collection_ref(StableIndex index, CollectionType type) const { if (index.is_collection()) { return to_ref(_get(index.get_index())); @@ -2446,7 +2454,7 @@ ref_type Obj::get_collection_ref(Index index, CollectionType type) const throw StaleAccessor("This collection is no more"); } -bool Obj::check_collection_ref(Index index, CollectionType type) const noexcept +bool Obj::check_collection_ref(StableIndex index, CollectionType type) const noexcept { if (index.is_collection()) { return true; @@ -2457,7 +2465,7 @@ bool Obj::check_collection_ref(Index index, CollectionType type) const noexcept return false; } -void Obj::set_collection_ref(Index index, ref_type ref, CollectionType type) +void Obj::set_collection_ref(StableIndex index, ref_type ref, CollectionType type) { if (index.is_collection()) { set_int(index.get_index(), from_ref(ref)); @@ -2466,4 +2474,78 @@ void Obj::set_collection_ref(Index index, ref_type ref, CollectionType type) set_ref(index.get_index(), ref, type); } +void Obj::set_backlink(ColKey col_key, ObjLink new_link) const +{ + if (!new_link) { + return; + } + + auto target_table = m_table->get_parent_group()->get_table(new_link.get_table_key()); + ColKey backlink_col_key; + auto type = col_key.get_type(); + if (type == col_type_TypedLink || type == col_type_Mixed || col_key.is_dictionary()) { + // This may modify the target table + backlink_col_key = target_table->find_or_add_backlink_column(col_key, m_table->get_key()); + // it is possible that this was a link to the same table and that adding a backlink column has + // caused the need to update this object as well. + update_if_needed(); + } + else { + backlink_col_key = m_table->get_opposite_column(col_key); + } + auto obj_key = new_link.get_obj_key(); + auto target_obj = + obj_key.is_unresolved() ? target_table->try_get_tombstone(obj_key) : target_table->try_get_object(obj_key); + if (!target_obj) { + throw InvalidArgument(ErrorCodes::KeyNotFound, "Target object not found"); + } + target_obj.add_backlink(backlink_col_key, m_key); +} + +bool Obj::replace_backlink(ColKey col_key, ObjLink old_link, ObjLink new_link, CascadeState& state) const +{ + bool recurse = remove_backlink(col_key, old_link, state); + set_backlink(col_key, new_link); + return recurse; +} + +bool Obj::remove_backlink(ColKey col_key, ObjLink old_link, CascadeState& state) const +{ + if (!old_link) { + return false; + } + + REALM_ASSERT(m_table->valid_column(col_key)); + ObjKey old_key = old_link.get_obj_key(); + auto target_obj = m_table->get_parent_group()->get_object(old_link); + TableRef target_table = target_obj.get_table(); + ColKey backlink_col_key; + auto type = col_key.get_type(); + if (type == col_type_TypedLink || type == col_type_Mixed || col_key.is_dictionary()) { + backlink_col_key = target_table->find_or_add_backlink_column(col_key, m_table->get_key()); + } + else { + backlink_col_key = m_table->get_opposite_column(col_key); + } + + bool strong_links = target_table->is_embedded(); + bool is_unres = old_key.is_unresolved(); + + bool last_removed = target_obj.remove_one_backlink(backlink_col_key, m_key); // Throws + if (is_unres) { + if (last_removed) { + // Check is there are more backlinks + if (!target_obj.has_backlinks(false)) { + // Tombstones can be erased right away - there is no cascading effect + target_table->m_tombstones->erase(old_key, state); + } + } + } + else { + return state.enqueue_for_cascade(target_obj, strong_links, last_removed); + } + + return false; +} + } // namespace realm diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp index edf9db3aa60..c7c25399500 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -57,45 +57,30 @@ class DeepChangeChecker; } // 'Object' would have been a better name, but it clashes with a class in ObjectStore -class Obj : public CollectionParent { +class Obj { public: - constexpr Obj() - : m_table(nullptr) - , m_row_ndx(size_t(-1)) - , m_storage_version(-1) - , m_valid(false) - { - } + constexpr Obj() = default; Obj(TableRef table, MemRef mem, ObjKey key, size_t row_ndx); - // Overriding members of CollectionParent: - UpdateStatus update_if_needed_with_status() const final; + // CollectionParent implementation + UpdateStatus update_if_needed() const; // Get the path in a minimal format without including object accessors. // If you need to obtain additional information for each object in the path, // you should use get_fat_path() or traverse_path() instead (see below). - FullPath get_path() const final; + FullPath get_path() const; std::string get_id() const; - Path get_short_path() const noexcept final; - ColKey get_col_key() const noexcept final; - StablePath get_stable_path() const noexcept final; - void add_index(Path& path, const Index& ndx) const final; - size_t find_index(const Index&) const final - { - return realm::npos; - } + Path get_short_path() const noexcept; + ColKey get_col_key() const noexcept; + StablePath get_stable_path() const noexcept; + void add_index(Path& path, const CollectionParent::Index& ndx) const; - bool update_if_needed() const final; - TableRef get_table() const noexcept final + TableRef get_table() const noexcept { return m_table.cast_away_const(); } - const Obj& get_object() const noexcept final - { - return *this; - } - ref_type get_collection_ref(Index, CollectionType) const final; - bool check_collection_ref(Index, CollectionType) const noexcept final; - void set_collection_ref(Index, ref_type, CollectionType) final; + ref_type get_collection_ref(CollectionParent::Index, CollectionType) const; + bool check_collection_ref(CollectionParent::Index, CollectionType) const noexcept; + void set_collection_ref(CollectionParent::Index, ref_type, CollectionType); StableIndex build_index(ColKey) const; bool check_index(StableIndex) const; @@ -325,29 +310,26 @@ class Obj : public CollectionParent { friend class ArrayBacklink; friend class CascadeState; friend class Cluster; + friend class CollectionParent; friend class ColumnListBase; - friend class CollectionBase; + friend class LinkCount; + friend class LinkMap; + friend class Lst; + friend class ObjCollectionParent; + friend class Table; friend class TableView; template friend class CollectionBaseImpl; template - friend class Lst; - friend class LnkLst; - friend class LinkCount; - friend class Dictionary; - friend class LinkMap; - template friend class Set; - friend class Table; - friend class Transaction; - friend class CollectionParent; mutable TableRef m_table; ObjKey m_key; mutable MemRef m_mem; - mutable size_t m_row_ndx; - mutable uint64_t m_storage_version; - mutable bool m_valid; + mutable size_t m_row_ndx = -1; + mutable uint64_t m_storage_version = -1; + mutable uint32_t m_version_counter = 0; + mutable bool m_valid = false; Allocator& _get_alloc() const noexcept; @@ -356,6 +338,7 @@ class Obj : public CollectionParent { /// reflect new changes to the underlying state. bool update() const; bool _update_if_needed() const; // no check, use only when already checked + void checked_update_if_needed() const; template bool do_is_null(ColKey::Idx col_ndx) const; @@ -418,6 +401,82 @@ class Obj : public CollectionParent { bool compare_values(Mixed, Mixed, ColKey, Obj, StringData) const; bool compare_list_in_mixed(Lst&, Lst&, ColKey, Obj, StringData) const; bool compare_dict_in_mixed(Dictionary&, Dictionary&, ColKey, Obj, StringData) const; + + // Used when inserting a new link. You will not remove existing links in this process + void set_backlink(ColKey col_key, ObjLink new_link) const; + // Used when replacing a link, return true if CascadeState contains objects to remove + bool replace_backlink(ColKey col_key, ObjLink old_link, ObjLink new_link, CascadeState& state) const; + // Used when removing a backlink, return true if CascadeState contains objects to remove + bool remove_backlink(ColKey col_key, ObjLink old_link, CascadeState& state) const; +}; +static_assert(std::is_trivially_destructible_v); + +class ObjCollectionParent final : public Obj, public CollectionParent { +public: + ObjCollectionParent() = default; + ObjCollectionParent(const Obj& obj) noexcept + : Obj(obj) + { + } + ObjCollectionParent& operator=(const Obj& obj) noexcept + { + static_cast(*this) = obj; + return *this; + } + +private: + FullPath get_path() const override + { + return Obj::get_path(); + } + Path get_short_path() const override + { + return Obj::get_short_path(); + } + ColKey get_col_key() const noexcept override + { + return Obj::get_col_key(); + } + StablePath get_stable_path() const override + { + return Obj::get_stable_path(); + } + void add_index(Path& path, const Index& ndx) const override + { + Obj::add_index(path, ndx); + } + size_t find_index(const Index&) const override + { + return realm::npos; + } + TableRef get_table() const noexcept override + { + return Obj::get_table(); + } + UpdateStatus update_if_needed() const override + { + return Obj::update_if_needed(); + } + const Obj& get_object() const noexcept override + { + return *this; + } + uint32_t parent_version() const noexcept override + { + return m_version_counter; + } + ref_type get_collection_ref(Index index, CollectionType type) const override + { + return Obj::get_collection_ref(index, type); + } + bool check_collection_ref(Index index, CollectionType type) const noexcept override + { + return Obj::check_collection_ref(index, type); + } + void set_collection_ref(Index index, ref_type ref, CollectionType type) override + { + Obj::set_collection_ref(index, ref, type); + } }; std::ostream& operator<<(std::ostream&, const Obj& obj); @@ -589,15 +648,6 @@ inline Obj& Obj::set_all(Head v, Tail... tail) return _set_all(start_index, v, tail...); } -inline bool Obj::update_if_needed() const -{ - auto current_version = get_alloc().get_storage_version(); - if (current_version != m_storage_version) { - return update(); - } - return false; -} - inline int_fast64_t Obj::bump_content_version() { Allocator& alloc = get_alloc(); diff --git a/src/realm/object-store/c_api/list.cpp b/src/realm/object-store/c_api/list.cpp index 58c6ff32759..6455c4f9bb3 100644 --- a/src/realm/object-store/c_api/list.cpp +++ b/src/realm/object-store/c_api/list.cpp @@ -112,7 +112,6 @@ RLM_API realm_dictionary_t* realm_list_set_dictionary(realm_list_t* list, size_t return wrap_err([&]() { list->set_collection(index, CollectionType::Dictionary); return new realm_dictionary_t{list->get_dictionary(index)}; - ; }); } diff --git a/src/realm/object-store/c_api/scheduler.cpp b/src/realm/object-store/c_api/scheduler.cpp index bcac4917678..3a7a0b0766b 100644 --- a/src/realm/object-store/c_api/scheduler.cpp +++ b/src/realm/object-store/c_api/scheduler.cpp @@ -165,44 +165,6 @@ RLM_API const realm_scheduler_t* realm_scheduler_get_frozen() }); } -// FIXME: Move this into `GenericScheduler` (i.e. make `Scheduler::set_default_factory()` thread-safe). -static std::mutex s_default_factory_mutex; -static bool s_default_factory_set = false; - -RLM_API bool realm_scheduler_has_default_factory() -{ -#if REALM_HAS_DEFAULT_SCHEDULER - return true; -#else - return s_default_factory_set; -#endif -} - -RLM_API bool realm_scheduler_set_default_factory(realm_userdata_t userdata, realm_free_userdata_func_t free_func, - realm_scheduler_default_factory_func_t factory_func) -{ - return wrap_err([&]() { -#if REALM_HAS_DEFAULT_SCHEDULER - static_cast(userdata); - static_cast(free_func); - static_cast(factory_func); - static_cast(s_default_factory_mutex); - static_cast(s_default_factory_set); - throw IllegalOperation{"This platform already has a default scheduler implementation"}; - return true; -#else - std::unique_lock lock{s_default_factory_mutex}; - if (s_default_factory_set) { - throw IllegalOperation{"A default scheduler factory has already been registered"}; - } - DefaultFactory factory{userdata, free_func, factory_func}; - Scheduler::set_default_factory(std::move(factory)); - s_default_factory_set = true; - return true; -#endif - }); -} - } // namespace realm::c_api // LCOV_EXCL_STOP diff --git a/src/realm/object-store/collection_notifications.hpp b/src/realm/object-store/collection_notifications.hpp index 88648271e52..d484e73cd02 100644 --- a/src/realm/object-store/collection_notifications.hpp +++ b/src/realm/object-store/collection_notifications.hpp @@ -106,7 +106,7 @@ struct CollectionChangeSet { // Per-column version of `modifications` std::unordered_map columns; - std::set paths; + std::set paths; bool empty() const noexcept { diff --git a/src/realm/object-store/impl/collection_notifier.cpp b/src/realm/object-store/impl/collection_notifier.cpp index a4c9c21ae31..2dbf9c8d41e 100644 --- a/src/realm/object-store/impl/collection_notifier.cpp +++ b/src/realm/object-store/impl/collection_notifier.cpp @@ -307,8 +307,9 @@ void CollectionNotifier::prepare_handover() REALM_ASSERT(m_change.empty()); m_has_run = true; -#ifdef REALM_DEBUG util::CheckedLockGuard lock(m_callback_mutex); + m_run_time_point = std::chrono::steady_clock::now(); +#ifdef REALM_DEBUG for (auto& callback : m_callbacks) REALM_ASSERT(!callback.skip_next); #endif @@ -330,10 +331,59 @@ void CollectionNotifier::before_advance() }); } +static void log_changeset(util::Logger* logger, const CollectionChangeSet& changes, std::string_view description, + std::chrono::microseconds elapsed) +{ + if (!logger) { + return; + } + + logger->log(util::LogCategory::notification, util::Logger::Level::debug, + "Delivering notifications for %1 after %2 us", description, elapsed.count()); + if (!logger->would_log(util::Logger::Level::trace)) { + return; + } + if (changes.empty()) { + logger->log(util::LogCategory::notification, util::Logger::Level::trace, " No changes"); + } + else { + if (changes.collection_root_was_deleted) { + logger->log(util::LogCategory::notification, util::Logger::Level::trace, " collection deleted"); + } + else if (changes.collection_was_cleared) { + logger->log(util::LogCategory::notification, util::Logger::Level::trace, " collection cleared"); + } + else { + auto log = [logger](const char* change, const IndexSet& index_set) { + if (auto cnt = index_set.count()) { + std::ostringstream ostr; + bool first = true; + for (auto [a, b] : index_set) { + if (!first) + ostr << ','; + if (b > a + 1) { + ostr << '[' << a << ',' << b - 1 << ']'; + } + else { + ostr << a; + } + first = false; + } + logger->log(util::LogCategory::notification, util::Logger::Level::trace, " %1 %2: %3", cnt, + change, ostr.str().c_str()); + } + }; + log("deletions", changes.deletions); + log("insertions", changes.insertions); + log("modifications", changes.modifications); + } + } +} + void CollectionNotifier::after_advance() { using namespace std::chrono; - auto t1 = steady_clock::now(); + auto now = steady_clock::now(); for_each_callback([&](auto& lock, auto& callback) { if (callback.initial_delivered && callback.changes_to_deliver.empty()) { @@ -346,51 +396,9 @@ void CollectionNotifier::after_advance() // acquire a local reference to the callback so that removing the // callback from within it can't result in a dangling pointer auto cb = callback.fn; + auto elapsed = duration_cast(now - m_run_time_point); lock.unlock_unchecked(); - if (m_logger) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, - "Delivering notifications for %1 after %2 us", m_description, - duration_cast(t1 - m_run_time_point).count()); - if (m_logger->would_log(util::Logger::Level::trace)) { - if (changes.empty()) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::trace, " No changes"); - } - else { - if (changes.collection_root_was_deleted) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::trace, - " collection deleted"); - } - else if (changes.collection_was_cleared) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::trace, - " collection cleared"); - } - else { - auto log = [this](const char* change, const IndexSet& index_set) { - if (auto cnt = index_set.count()) { - std::ostringstream ostr; - bool first = true; - for (auto [a, b] : index_set) { - if (!first) - ostr << ','; - if (b > a + 1) { - ostr << '[' << a << ',' << b - 1 << ']'; - } - else { - ostr << a; - } - first = false; - } - m_logger->log(util::LogCategory::notification, util::Logger::Level::trace, - " %1 %2: %3", cnt, change, ostr.str().c_str()); - } - }; - log("deletions", changes.deletions); - log("insertions", changes.insertions); - log("modifications", changes.modifications); - } - } - } - } + log_changeset(m_logger.get(), changes, m_description, elapsed); cb.after(changes); }); } @@ -529,3 +537,24 @@ void NotifierPackage::after_advance() for (auto& notifier : m_notifiers) notifier->after_advance(); } + +NotifierRunLogger::NotifierRunLogger(util::Logger* logger, std::string_view name, std::string_view description) + : m_logger(logger) + , m_name(name) + , m_description(description) +{ + if (logger && logger->would_log(util::Logger::Level::debug)) { + m_logger = logger; + m_start = std::chrono::steady_clock::now(); + } +} + +NotifierRunLogger::~NotifierRunLogger() +{ + using namespace std::chrono; + if (m_logger) { + auto now = steady_clock::now(); + m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, "%1 %2 ran in %3 us", m_name, + m_description, duration_cast(now - m_start).count()); + } +} diff --git a/src/realm/object-store/impl/collection_notifier.hpp b/src/realm/object-store/impl/collection_notifier.hpp index 88e8b6f9cf3..c333203c645 100644 --- a/src/realm/object-store/impl/collection_notifier.hpp +++ b/src/realm/object-store/impl/collection_notifier.hpp @@ -381,6 +381,18 @@ class NotifierPackage { RealmCoordinator* m_coordinator = nullptr; }; +class NotifierRunLogger { +public: + NotifierRunLogger(util::Logger* logger, std::string_view name, std::string_view description); + ~NotifierRunLogger(); + +private: + util::Logger* m_logger; + std::string_view m_name; + std::string_view m_description; + std::chrono::steady_clock::time_point m_start; +}; + } // namespace realm::_impl #endif /* REALM_BACKGROUND_COLLECTION_HPP */ diff --git a/src/realm/object-store/impl/list_notifier.cpp b/src/realm/object-store/impl/list_notifier.cpp index 0de163fa7fa..ebcfb797d77 100644 --- a/src/realm/object-store/impl/list_notifier.cpp +++ b/src/realm/object-store/impl/list_notifier.cpp @@ -33,7 +33,7 @@ ListNotifier::ListNotifier(std::shared_ptr realm, CollectionBase const& l , m_prev_size(list.size()) { attach(list); - if (m_logger) { + if (m_logger && m_logger->would_log(util::Logger::Level::debug)) { auto path = m_list->get_short_path(); auto prop_name = m_list->get_table()->get_column_name(path[0].get_col_key()); path[0] = PathElement(prop_name); @@ -62,9 +62,11 @@ void ListNotifier::attach(CollectionBase const& src) if (auto obj = tr.get_table(src.get_table()->get_key())->try_get_object(src.get_owner_key())) { auto path = src.get_stable_path(); m_list = std::static_pointer_cast(obj.get_collection_by_stable_path(path)); + m_collection_parent = dynamic_cast(m_list.get()); } else { m_list = nullptr; + m_collection_parent = nullptr; } } @@ -73,14 +75,9 @@ bool ListNotifier::do_add_required_change_info(TransactionChangeInfo& info) if (!m_list || !m_list->is_attached()) return false; // origin row was deleted after the notification was added - // We need to have the collections with the shortest paths first StablePath this_path = m_list->get_stable_path(); - auto it = std::lower_bound(info.collections.begin(), info.collections.end(), this_path.size(), - [](const CollectionChangeInfo& info, size_t sz) { - return info.path.size() < sz; - }); - info.collections.insert( - it, {m_list->get_table()->get_key(), m_list->get_owner_key(), std::move(this_path), &m_change}); + info.collections.push_back( + {m_list->get_table()->get_key(), m_list->get_owner_key(), std::move(this_path), &m_change}); m_info = &info; @@ -97,16 +94,7 @@ bool ListNotifier::do_add_required_change_info(TransactionChangeInfo& info) void ListNotifier::run() { - using namespace std::chrono; - auto t1 = steady_clock::now(); - util::ScopeExit cleanup([&]() noexcept { - m_run_time_point = steady_clock::now(); - if (m_logger) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, - "ListNotifier %1 did run in %2 us", m_description, - duration_cast(m_run_time_point - t1).count()); - } - }); + NotifierRunLogger log(m_logger.get(), "ListNotifier", m_description); if (!m_list || !m_list->is_attached()) { // List was deleted, so report all of the rows being removed if this is @@ -142,14 +130,14 @@ void ListNotifier::run() } } + // Modifications to nested values in Mixed are recorded in replication as + // StableIndex and we have to look up the actual index afterwards if (m_change.paths.size()) { - if (auto coll = dynamic_cast(m_list.get())) { - for (auto& p : m_change.paths) { - // Report changes in substructure as modifications on this list - auto ndx = coll->find_index(p[0]); - if (ndx != realm::not_found) - m_change.modifications.add(ndx); // OK to insert same index again - } + REALM_ASSERT(m_collection_parent); + REALM_ASSERT(m_type == PropertyType::Mixed); + for (auto& p : m_change.paths) { + if (auto ndx = m_collection_parent->find_index(p); ndx != realm::not_found) + m_change.modifications.add(ndx); } } } diff --git a/src/realm/object-store/impl/list_notifier.hpp b/src/realm/object-store/impl/list_notifier.hpp index ec22e2c54fe..44f955cb7c1 100644 --- a/src/realm/object-store/impl/list_notifier.hpp +++ b/src/realm/object-store/impl/list_notifier.hpp @@ -34,6 +34,7 @@ class ListNotifier : public CollectionNotifier { private: PropertyType m_type; CollectionBasePtr m_list; + CollectionParent* m_collection_parent = nullptr; // The last-seen size of the collection so that when the parent of the collection // is deleted we can report each row as being deleted diff --git a/src/realm/object-store/impl/object_notifier.cpp b/src/realm/object-store/impl/object_notifier.cpp index 86feba916d0..676680017d8 100644 --- a/src/realm/object-store/impl/object_notifier.cpp +++ b/src/realm/object-store/impl/object_notifier.cpp @@ -64,16 +64,7 @@ void ObjectNotifier::run() { if (!m_table || !m_info) return; - using namespace std::chrono; - auto t1 = steady_clock::now(); - util::ScopeExit cleanup([&]() noexcept { - m_run_time_point = steady_clock::now(); - if (m_logger) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, - "ObjectNotifier %1 did run in %2 us", m_description, - duration_cast(m_run_time_point - t1).count()); - } - }); + NotifierRunLogger log(m_logger.get(), "ObjectNotifier", m_description); auto it = m_info->tables.find(m_table->get_key()); if (it != m_info->tables.end() && it->second.deletions_contains(m_obj_key)) { diff --git a/src/realm/object-store/impl/results_notifier.cpp b/src/realm/object-store/impl/results_notifier.cpp index 92ad8292f57..b60029e58cd 100644 --- a/src/realm/object-store/impl/results_notifier.cpp +++ b/src/realm/object-store/impl/results_notifier.cpp @@ -65,12 +65,12 @@ ResultsNotifier::ResultsNotifier(Results& target) , m_target_is_in_table_order(target.is_in_table_order()) { if (m_logger) { - m_description = std::string("'") + std::string(m_query->get_table()->get_class_name()) + std::string("'"); + m_description = "'" + std::string(m_query->get_table()->get_class_name()) + "'"; if (m_query->has_conditions()) { m_description += " where \""; m_description += m_query->get_description_safe() + "\""; } - m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, "Creating ResultsNotifier for ", + m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, "Creating ResultsNotifier for %1", m_description); } reattach(); @@ -151,20 +151,10 @@ void ResultsNotifier::calculate_changes() void ResultsNotifier::run() { - using namespace std::chrono; + NotifierRunLogger log(m_logger.get(), "ResultsNotifier", m_description); REALM_ASSERT(m_info || !has_run()); - auto t1 = steady_clock::now(); - util::ScopeExit cleanup([&]() noexcept { - m_run_time_point = steady_clock::now(); - if (m_logger) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, - "ResultsNotifier %1 did run in %2 us", m_description, - duration_cast(m_run_time_point - t1).count()); - } - }); - // Table's been deleted, so report all objects as deleted if (!m_query->get_table()) { m_change = {}; @@ -278,7 +268,7 @@ ListResultsNotifier::ListResultsNotifier(Results& target) auto path = m_list->get_short_path(); auto prop_name = m_list->get_table()->get_column_name(path[0].get_col_key()); path[0] = PathElement(prop_name); - std::string sort_order; + std::string_view sort_order = ""; if (m_sort_order) { sort_order = *m_sort_order ? " sorted ascending" : " sorted descending"; } @@ -358,17 +348,6 @@ void ListResultsNotifier::calculate_changes() void ListResultsNotifier::run() { - using namespace std::chrono; - auto t1 = steady_clock::now(); - util::ScopeExit cleanup([&]() noexcept { - m_run_time_point = steady_clock::now(); - if (m_logger) { - m_logger->log(util::LogCategory::notification, util::Logger::Level::debug, - "ListResultsNotifier %1 did run in %2 us", m_description, - duration_cast(m_run_time_point - t1).count()); - } - }); - if (!m_list || !m_list->is_attached()) { // List was deleted, so report all of the rows being removed m_change = {}; @@ -379,10 +358,11 @@ void ListResultsNotifier::run() } if (!need_to_run()) { - cleanup.cancel(); return; } + NotifierRunLogger log(m_logger.get(), "ListResultsNotifier", m_description); + m_run_indices = std::vector(); if (m_distinct) m_list->distinct(*m_run_indices, m_sort_order); @@ -393,13 +373,13 @@ void ListResultsNotifier::run() std::iota(m_run_indices->begin(), m_run_indices->end(), 0); } + // Modifications to nested values in Mixed are recorded in replication as + // StableIndex and we have to look up the actual index afterwards if (m_change.paths.size()) { if (auto coll = dynamic_cast(m_list.get())) { for (auto& p : m_change.paths) { - // Report changes in substructure as modifications on this list - auto ndx = coll->find_index(p[0]); - if (ndx != realm::not_found) - m_change.modifications.add(ndx); // OK to insert same index again + if (auto ndx = coll->find_index(p); ndx != realm::not_found) + m_change.modifications.add(ndx); } } } diff --git a/src/realm/object-store/impl/transact_log_handler.cpp b/src/realm/object-store/impl/transact_log_handler.cpp index be021dd2ec4..93377cfaa06 100644 --- a/src/realm/object-store/impl/transact_log_handler.cpp +++ b/src/realm/object-store/impl/transact_log_handler.cpp @@ -368,21 +368,19 @@ class TransactLogObserver : public TransactLogValidationMixin { { modify_object(col, obj); auto table = current_table(); + m_active_collection = nullptr; for (auto& c : m_info.collections) { - if (c.table_key == table && c.obj_key == obj && c.path.is_prefix_of(path)) { if (c.path.size() != path.size()) { - StablePath sub_path; - sub_path.insert(sub_path.begin(), path.begin() + c.path.size(), path.end()); - c.changes->paths.insert(std::move(sub_path)); + c.changes->paths.insert(path[c.path.size()]); } - else { + // If there are multiple exact matches for this collection we + // use the first and then propagate the data to the others later + else if (!m_active_collection) { m_active_collection = c.changes; - return true; } } } - m_active_collection = nullptr; return true; } diff --git a/src/realm/object-store/util/scheduler.cpp b/src/realm/object-store/util/scheduler.cpp index ac0a3f55f2e..3807b20e8d1 100644 --- a/src/realm/object-store/util/scheduler.cpp +++ b/src/realm/object-store/util/scheduler.cpp @@ -41,7 +41,7 @@ namespace realm::util { namespace { -util::UniqueFunction()> s_factory = &Scheduler::make_platform_default; +std::shared_ptr (*s_factory)() = Scheduler::make_platform_default; class FrozenScheduler : public util::Scheduler { public: @@ -108,7 +108,7 @@ void InvocationQueue::invoke_all() Scheduler::~Scheduler() = default; -void Scheduler::set_default_factory(util::UniqueFunction()> factory) +void Scheduler::set_default_factory(std::shared_ptr (*factory)()) { s_factory = std::move(factory); } diff --git a/src/realm/object-store/util/scheduler.hpp b/src/realm/object-store/util/scheduler.hpp index be43450a75d..247283d4aec 100644 --- a/src/realm/object-store/util/scheduler.hpp +++ b/src/realm/object-store/util/scheduler.hpp @@ -125,8 +125,9 @@ class Scheduler { #endif /// Register a factory function which can produce custom schedulers when - /// `Scheduler::make_default()` is called. - static void set_default_factory(util::UniqueFunction()>); + /// `Scheduler::make_default()` is called. This function is not thread-safe + /// and must be called before any schedulers are created. + static void set_default_factory(std::shared_ptr (*factory)()); }; // A thread-safe queue of functions to invoke, used in the implemenation of @@ -141,7 +142,6 @@ class InvocationQueue { std::vector> m_functions; }; - } // namespace realm::util #endif // REALM_OS_UTIL_SCHEDULER diff --git a/src/realm/object_converter.cpp b/src/realm/object_converter.cpp index 20797abbaa8..a88a18248d6 100644 --- a/src/realm/object_converter.cpp +++ b/src/realm/object_converter.cpp @@ -327,10 +327,10 @@ void InterRealmValueConverter::copy_value(const Obj& src_obj, Obj& dst_obj, bool } // -// Handle collections in mixed. A collection can have N nested levels (expect for Sets). And these levels can be +// Handle collections in mixed. A collection can have N nested levels (except for Sets). And these levels can be // nested in arbitrary way (eg a List within a Dictionary or viceversa). In order to try to merge server changes with -// client changes, the algorithm needs to go throw each single element in the collection, check its type and perform -// the most appropriate action in order to miminize the number of notificiations triggered. +// client changes, the algorithm needs to go through each single element in the collection, check its type and perform +// the most appropriate action in order to miminize the number of notifications triggered. // void InterRealmValueConverter::handle_list_in_mixed(const Lst& src_list, Lst& dst_list) const { diff --git a/src/realm/path.hpp b/src/realm/path.hpp index 08ead2dbae9..6124590271c 100644 --- a/src/realm/path.hpp +++ b/src/realm/path.hpp @@ -271,54 +271,45 @@ class ExtendedColumnKey { */ class StableIndex { public: - StableIndex() - { - value.raw = 0; - } + StableIndex() = default; StableIndex(ColKey col_key, int64_t salt) { - value.col_index = col_key.get_index().val; - value.is_collection = col_key.is_collection(); - value.is_column = true; - value.salt = int32_t(salt); + m_col_index = col_key.get_index().val; + m_is_collection = col_key.is_collection(); + m_is_column = true; + m_salt = int32_t(salt); } StableIndex(int64_t salt) { - value.raw = 0; - value.salt = int32_t(salt); + m_salt = int32_t(salt); } int64_t get_salt() const { - return value.salt; + return m_salt; } ColKey::Idx get_index() const noexcept { - return {unsigned(value.col_index)}; + return {unsigned(m_col_index)}; } bool is_collection() const noexcept { - return value.is_collection; + return m_is_collection; } bool operator==(const StableIndex& other) const noexcept { - return value.is_column ? value.col_index == other.value.col_index : value.salt == other.value.salt; + return m_is_column ? m_col_index == other.m_col_index : m_salt == other.m_salt; } bool operator<(const StableIndex& other) const noexcept { - return value.is_column ? value.col_index < other.value.col_index : value.salt < other.value.salt; + return m_is_column ? m_col_index < other.m_col_index : m_salt < other.m_salt; } private: - union { - struct { - bool is_column; - bool is_collection; - int16_t col_index; - int32_t salt; - }; - int64_t raw; - } value; + bool m_is_column = false; + bool m_is_collection = false; + int16_t m_col_index = 0; + int32_t m_salt = 0; }; static_assert(sizeof(StableIndex) == 8); diff --git a/src/realm/replication.cpp b/src/realm/replication.cpp index 35bd32d8b3a..cfc60c6580b 100644 --- a/src/realm/replication.cpp +++ b/src/realm/replication.cpp @@ -177,7 +177,7 @@ void Replication::remove_object(const Table* t, ObjKey key) m_encoder.remove_object(key); // Throws } -inline void Replication::select_obj(ObjKey key) +void Replication::select_obj(ObjKey key) { if (key == m_selected_obj) { return; diff --git a/src/realm/set.cpp b/src/realm/set.cpp index 7ef6b07d7b1..74d4e51c0f6 100644 --- a/src/realm/set.cpp +++ b/src/realm/set.cpp @@ -273,32 +273,6 @@ void CollectionBaseImpl::to_json(std::ostream& out, JSONOutputMode outp } } -bool SetBase::do_init_from_parent(ref_type ref, bool allow_create) const -{ - try { - if (ref) { - m_tree->init_from_ref(ref); - } - else { - if (m_tree->init_from_parent()) { - // All is well - return true; - } - if (!allow_create) { - return false; - } - // The ref in the column was NULL, create the tree in place. - m_tree->create(); - REALM_ASSERT(m_tree->is_attached()); - } - } - catch (...) { - m_tree->detach(); - throw; - } - return true; -} - void SetBase::resort_range(size_t start, size_t end) { if (end > size()) { diff --git a/src/realm/set.hpp b/src/realm/set.hpp index d9d989ea1ed..e3d7fac3d60 100644 --- a/src/realm/set.hpp +++ b/src/realm/set.hpp @@ -57,7 +57,6 @@ class SetBase : public CollectionBase { void erase_repl(Replication* repl, size_t index, Mixed value) const; void clear_repl(Replication* repl) const; static std::vector convert_to_mixed_set(const CollectionBase& rhs); - bool do_init_from_parent(ref_type ref, bool allow_create) const; void resort_range(size_t from, size_t to); @@ -104,7 +103,7 @@ class Set final : public CollectionBaseImpl { this->set_owner(owner, col_key); } - Set(ColKey col_key) + Set(ColKey col_key, size_t = 0) : Base(col_key) { if (!col_key.is_set()) { @@ -195,7 +194,7 @@ class Set final : public CollectionBaseImpl { return tree(); } - UpdateStatus update_if_needed_with_status() const final; + UpdateStatus update_if_needed() const final; void ensure_created(); void migrate(); @@ -216,12 +215,12 @@ class Set final : public CollectionBaseImpl { return static_cast&>(*m_tree); } - bool init_from_parent(bool allow_create) const; + UpdateStatus init_from_parent(bool allow_create) const; /// Update the accessor and return true if it is attached after the update. inline bool update() const { - return update_if_needed_with_status() != UpdateStatus::Detached; + return update_if_needed() != UpdateStatus::Detached; } // `do_` methods here perform the action after preconditions have been @@ -377,7 +376,7 @@ class LnkSet final : public ObjCollectionBase { // Overriding members of ObjCollectionBase: UpdateStatus do_update_if_needed() const final { - return m_set.update_if_needed_with_status(); + return m_set.update_if_needed(); } BPlusTree* get_mutable_tree() const final @@ -494,10 +493,9 @@ inline Set& Set::operator=(Set&& other) noexcept } template -UpdateStatus Set::update_if_needed_with_status() const +UpdateStatus Set::update_if_needed() const { - auto status = Base::get_update_status(); - switch (status) { + switch (get_update_status()) { case UpdateStatus::Detached: { m_tree.reset(); return UpdateStatus::Detached; @@ -509,11 +507,8 @@ UpdateStatus Set::update_if_needed_with_status() const // The tree has not been initialized yet for this accessor, so // perform lazy initialization by treating it as an update. [[fallthrough]]; - case UpdateStatus::Updated: { - bool attached = init_from_parent(false); - Base::update_content_version(); - return attached ? UpdateStatus::Updated : UpdateStatus::Detached; - } + case UpdateStatus::Updated: + return init_from_parent(false); } REALM_UNREACHABLE(); } @@ -526,19 +521,19 @@ void Set::ensure_created() // In case of errors, an exception is thrown. constexpr bool allow_create = true; init_from_parent(allow_create); // Throws - Base::update_content_version(); } } template -bool Set::init_from_parent(bool allow_create) const +UpdateStatus Set::init_from_parent(bool allow_create) const { + Base::update_content_version(); if (!m_tree) { m_tree.reset(new BPlusTree(get_alloc())); const ArrayParent* parent = this; m_tree->set_parent(const_cast(parent), 0); } - return do_init_from_parent(Base::get_collection_ref(), allow_create); + return do_init_from_parent(m_tree.get(), Base::get_collection_ref(), allow_create); } template diff --git a/src/realm/sync/instruction_applier.cpp b/src/realm/sync/instruction_applier.cpp index 22eb69d28d2..4082e3e3e7d 100644 --- a/src/realm/sync/instruction_applier.cpp +++ b/src/realm/sync/instruction_applier.cpp @@ -886,10 +886,51 @@ void InstructionApplier::operator()(const Instruction::Clear& instr) { list.clear(); } + Status on_list_index(LstBase& list, uint32_t index) override + { + REALM_ASSERT(dynamic_cast*>(&list)); + auto& mixed_list = static_cast&>(list); + if (index >= mixed_list.size()) { + m_applier->bad_transaction_log("Clear: Index out of bounds (%1 > %2)", index, + mixed_list.size()); // Throws + return Status::DidNotResolve; + } + auto val = mixed_list.get(index); + if (val.is_type(type_Dictionary)) { + Dictionary d(mixed_list, mixed_list.get_key(index)); + d.clear(); + return Status::Pending; + } + if (val.is_type(type_List)) { + Lst l(mixed_list, mixed_list.get_key(index)); + l.clear(); + return Status::Pending; + } + m_applier->bad_transaction_log("Clear: Item (%1) at index %2 is not a collection", val.get_type(), + index); // Throws + return Status::DidNotResolve; + } void on_dictionary(Dictionary& dict) override { dict.clear(); } + Status on_dictionary_key(Dictionary& dict, Mixed key) override + { + auto val = dict.get(key); + if (val.is_type(type_Dictionary)) { + Dictionary d(dict, dict.build_index(key)); + d.clear(); + return Status::Pending; + } + if (val.is_type(type_List)) { + Lst l(dict, dict.build_index(key)); + l.clear(); + return Status::Pending; + } + m_applier->bad_transaction_log("Clear: Item (%1) at key '%2' is not a collection", val.get_type(), + key); // Throws + return Status::DidNotResolve; + } void on_set(SetBase& set) override { set.clear(); diff --git a/src/realm/sync/instruction_replication.cpp b/src/realm/sync/instruction_replication.cpp index b92f87f2a72..5767ba8daf9 100644 --- a/src/realm/sync/instruction_replication.cpp +++ b/src/realm/sync/instruction_replication.cpp @@ -81,18 +81,12 @@ Instruction::Payload SyncReplication::as_payload(Mixed value) } } if (type == type_Dictionary) { - if (!SYNC_SUPPORTS_NESTED_COLLECTIONS) - throw IllegalOperation("Cannot sync nested dictionary"); return Instruction::Payload(Instruction::Payload::Dictionary()); } else if (type == type_List) { - if (!SYNC_SUPPORTS_NESTED_COLLECTIONS) - throw IllegalOperation("Cannot sync nested list"); return Instruction::Payload(Instruction::Payload::List()); } else if (type == type_Set) { - if (!SYNC_SUPPORTS_NESTED_COLLECTIONS) - throw IllegalOperation("Cannot sync nested set"); return Instruction::Payload(Instruction::Payload::Set()); } return Instruction::Payload{}; @@ -810,11 +804,11 @@ void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, c } } -void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, const CollectionBase& list) +void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, const CollectionBase& collection) { - ConstTableRef source_table = list.get_table(); - ObjKey source_obj = list.get_owner_key(); - populate_path_instr(instr, *source_table, source_obj, list.get_short_path()); + ConstTableRef source_table = collection.get_table(); + ObjKey source_obj = collection.get_owner_key(); + populate_path_instr(instr, *source_table, source_obj, collection.get_short_path()); } void SyncReplication::populate_path_instr(Instruction::PathInstruction& instr, const CollectionBase& list, diff --git a/src/realm/sync/instruction_replication.hpp b/src/realm/sync/instruction_replication.hpp index b9008b9609b..ff7fbd62de4 100644 --- a/src/realm/sync/instruction_replication.hpp +++ b/src/realm/sync/instruction_replication.hpp @@ -198,8 +198,6 @@ class TempShortCircuitReplication { bool m_was_short_circuited; }; -constexpr bool SYNC_SUPPORTS_NESTED_COLLECTIONS = false; - } // namespace sync } // namespace realm diff --git a/src/realm/sync/instructions.hpp b/src/realm/sync/instructions.hpp index 1f399c19273..ea6ab97563b 100644 --- a/src/realm/sync/instructions.hpp +++ b/src/realm/sync/instructions.hpp @@ -176,7 +176,7 @@ struct Payload { struct List {}; /// Create an empty dictionary in-place (does not clear an existing dictionary). struct Dictionary {}; - /// Create an empty set in-place (does not clear an existing dictionary). + /// Create an empty set in-place (does not clear an existing set). struct Set {}; /// Sentinel value for an erased dictionary element. struct Erased {}; diff --git a/src/realm/sync/transform.cpp b/src/realm/sync/transform.cpp index 6b661f3c3bf..ab8e06d7504 100644 --- a/src/realm/sync/transform.cpp +++ b/src/realm/sync/transform.cpp @@ -1038,39 +1038,6 @@ struct MergeUtils { // shorter path than the right, and the entire left path is the initial // sequence of the right. - bool is_prefix_of(const Instruction::AddTable& left, const Instruction::TableInstruction& right) const noexcept - { - return same_table(left, right); - } - - bool is_prefix_of(const Instruction::EraseTable& left, const Instruction::TableInstruction& right) const noexcept - { - return same_table(left, right); - } - - bool is_prefix_of(const Instruction::AddColumn&, const Instruction::TableInstruction&) const noexcept - { - // Right side is a schema instruction. - return false; - } - - bool is_prefix_of(const Instruction::EraseColumn&, const Instruction::TableInstruction&) const noexcept - { - // Right side is a schema instruction. - return false; - } - - bool is_prefix_of(const Instruction::AddColumn& left, const Instruction::ObjectInstruction& right) const noexcept - { - return same_column(left, right); - } - - bool is_prefix_of(const Instruction::EraseColumn& left, - const Instruction::ObjectInstruction& right) const noexcept - { - return same_column(left, right); - } - bool is_prefix_of(const Instruction::ObjectInstruction&, const Instruction::TableInstruction&) const noexcept { // Right side is a schema instruction. @@ -1083,25 +1050,31 @@ struct MergeUtils { return same_object(left, right); } - bool is_prefix_of(const Instruction::PathInstruction&, const Instruction::TableInstruction&) const noexcept + // Returns the next path element if the first path is a parent of the second path. + // Example: + // * is_prefix_of(field1.123.field2, field1.123.field2.456) = 456 + // * is_prefix_of(field1.123.field2, field1.123.field3.456) = {} + + std::optional is_prefix_of(const Instruction::PathInstruction&, + const Instruction::TableInstruction&) const noexcept { // Path instructions can never be full prefixes of table-level instructions. Note that this also covers // ObjectInstructions. - return false; + return {}; } - bool is_prefix_of(const Instruction::PathInstruction& left, - const Instruction::PathInstruction& right) const noexcept + std::optional is_prefix_of(const Instruction::PathInstruction& left, + const Instruction::PathInstruction& right) const noexcept { if (left.path.size() < right.path.size() && same_field(left, right)) { for (size_t i = 0; i < left.path.size(); ++i) { if (!same_path_element(left.path[i], right.path[i])) { - return false; + return {}; } } - return true; + return right.path[left.path.size()]; } - return false; + return {}; } // True if the left side is an instruction that touches a container within @@ -1382,7 +1355,7 @@ DEFINE_MERGE_NOOP(Instruction::SetErase, Instruction::AddTable); DEFINE_NESTED_MERGE(Instruction::EraseTable) { - if (is_prefix_of(outer, inner)) { + if (same_table(outer, inner)) { inner_side.discard(); } } @@ -1499,15 +1472,23 @@ DEFINE_NESTED_MERGE(Instruction::Update) { using Type = Instruction::Payload::Type; - if (outer.value.type == Type::ObjectValue || outer.value.type == Type::Dictionary) { - // Creating an embedded object or a dictionary is an idempotent - // operation, and should not eliminate updates to the subtree. + if (outer.value.type == Type::ObjectValue) { + // Creating an embedded object is an idempotent operation, and should + // not eliminate updates to the subtree. return; } // Setting a value higher up in the hierarchy overwrites any modification to // the inner value, regardless of when this happened. - if (is_prefix_of(outer, inner)) { + if (auto next_element = is_prefix_of(outer, inner)) { + // If this is a collection in mixed, we will allow the inner instruction + // to pass so long as it references the proper type (list or dictionary). + if (outer.value.type == Type::List && mpark::holds_alternative(*next_element)) { + return; + } + else if (outer.value.type == Type::Dictionary && mpark::holds_alternative(*next_element)) { + return; + } inner_side.discard(); } } @@ -1537,19 +1518,27 @@ DEFINE_MERGE(Instruction::Update, Instruction::Update) } if (left.value.type != right.value.type) { - // Embedded object / dictionary creation should always lose to an - // Update(value), because these structures are nested, and we need to - // discard any update inside the structure. - if (left.value.type == Type::Dictionary || left.value.type == Type::ObjectValue) { + // Embedded object creation should always lose to an Update(value), + // because these structures are nested, and we need to discard any + // update inside the structure. + if (left.value.type == Type::ObjectValue) { left_side.discard(); return; } - else if (right.value.type == Type::Dictionary || right.value.type == Type::ObjectValue) { + else if (right.value.type == Type::ObjectValue) { right_side.discard(); return; } } + // Updates to List or Dictionary are idempotent. If both sides are setting to the same value, + // let them both pass through. It is important that the instruction application rules reflect this. + // If it is not two lists or dictionaries, then the normal last-writer-wins rules will take effect below. + if (left.value.type == right.value.type && + (left.value.type == Type::List || left.value.type == Type::Dictionary)) { + return; + } + // CONFLICT: Two updates of the same element. // // RESOLUTION: Suppress the effect of the UPDATE operation with the lower @@ -1585,22 +1574,36 @@ DEFINE_MERGE(Instruction::AddInteger, Instruction::Update) // RESOLUTION: If the Add was later than the Set, add its value to // the payload of the Set instruction. Otherwise, discard it. - if (!(right.value.type == Instruction::Payload::Type::Int || right.value.is_null())) { - bad_merge(right_side, right, - "Merge error: right.value.type == Instruction::Payload::Type::Int || right.value.is_null()"); - } - bool right_is_default = !right.is_array_update() && right.is_default; + // Five Cases Here: + // 1. AddInteger is after Update and Update is of a non-integer type + // - Discard the AddInteger; AddInteger to a mixed field is a no-op + // 2: AddInteger is after the Update and the Update instruction contains an integer payload: + // - We increment the Update instruction payload + // 3: AddInteger is after Update and Update is null: + // - No conflict + // 4: Update is after AddInteger and Update.default is false + // - Discard the AddInteger + // 5: Update is after AddInteger and Update.default is true + // - Treat the Update as if it were before the AddInteger instruction + // Note: AddInteger survives SetDefault, regardless of timestamp. if (right_side.timestamp() < left_side.timestamp() || right_is_default) { if (right.value.is_null()) { - // The AddInteger happened "after" the Set(null). This becomes a - // no-op, but if the server later integrates a Set(int) that + // The AddInteger happened "after" the Update(null). This becomes a + // no-op, but if the server later integrates a Update(int) that // came-before the AddInteger, it will be taken into account again. return; } + // The AddInteger happened after an Update with a non int type + // This must be operating on a mixed field. Discard the AddInteger + if (right.value.type != Instruction::Payload::Type::Int) { + left_side.discard(); + return; + } + // Wrapping add uint64_t ua = uint64_t(right.value.data.integer); uint64_t ub = uint64_t(left.value); @@ -1689,15 +1692,34 @@ DEFINE_MERGE(Instruction::ArrayErase, Instruction::Update) } } +DEFINE_MERGE(Instruction::Clear, Instruction::Update) +{ + using Type = Instruction::Payload::Type; + + // The two instructions are at the same level of nesting. + if (same_path(left, right)) { + // TODO: We could make it so a Clear instruction does not win against setting a property or + // collection item to a different collection. + if (right.value.type != Type::List && right.value.type != Type::Dictionary) { + left_side.discard(); + } + } +} + // Handled by nested rule -DEFINE_MERGE_NOOP(Instruction::Clear, Instruction::Update); DEFINE_MERGE_NOOP(Instruction::SetInsert, Instruction::Update); DEFINE_MERGE_NOOP(Instruction::SetErase, Instruction::Update); /// AddInteger rules -DEFINE_NESTED_MERGE_NOOP(Instruction::AddInteger); +DEFINE_NESTED_MERGE(Instruction::AddInteger) +{ + if (is_prefix_of(outer, inner)) { + inner_side.discard(); + } +} + DEFINE_MERGE_NOOP(Instruction::AddInteger, Instruction::AddInteger); DEFINE_MERGE_NOOP(Instruction::AddColumn, Instruction::AddInteger); DEFINE_MERGE_NOOP(Instruction::EraseColumn, Instruction::AddInteger); diff --git a/src/realm/transaction.cpp b/src/realm/transaction.cpp index 6f246e36e2d..1cddd8fdce3 100644 --- a/src/realm/transaction.cpp +++ b/src/realm/transaction.cpp @@ -419,7 +419,7 @@ _impl::History* Transaction::get_history() const Obj Transaction::import_copy_of(const Obj& original) { if (bool(original) && original.is_valid()) { - TableKey tk = original.get_table_key(); + TableKey tk = original.get_table()->get_key(); ObjKey rk = original.get_key(); auto table = get_table(tk); if (table->is_valid(rk)) diff --git a/src/realm/util/logger.hpp b/src/realm/util/logger.hpp index a438cf60179..017cf8284f3 100644 --- a/src/realm/util/logger.hpp +++ b/src/realm/util/logger.hpp @@ -497,6 +497,12 @@ class LocalThresholdLogger : public Logger { std::shared_ptr m_chained_logger; }; +/// A logger that performs a noop when logging functions are called +class NullLogger : public Logger { + // Since we don't want to log anything, do_log() does nothing + void do_log(const LogCategory&, Level, const std::string&) override {} +}; + // Implementation diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2191efd77ca..bdcd394b5c8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -201,6 +201,7 @@ if(REALM_ENABLE_SYNC) test_sync_subscriptions.cpp test_sync_pending_bootstraps.cpp test_sync_error_backoff.cpp + test_transform_collections_mixed.cpp test_transform.cpp test_util_buffer_stream.cpp test_util_circular_buffer.cpp diff --git a/test/object-store/CMakeLists.txt b/test/object-store/CMakeLists.txt index b23946ec353..406f018f2aa 100644 --- a/test/object-store/CMakeLists.txt +++ b/test/object-store/CMakeLists.txt @@ -92,6 +92,12 @@ endif() target_link_libraries(ObjectStoreTestLib Catch2::Catch2 ObjectStore RealmFFIStatic TestUtil) enable_stdfilesystem(ObjectStoreTestLib) +if(REALM_CURL_CACERTS) + target_compile_definitions(ObjectStoreTestLib PRIVATE + REALM_CURL_CACERTS="${REALM_CURL_CACERTS}" + ) +endif() + add_executable(ObjectStoreTests main.cpp ${RESOURCES}) set_target_properties(ObjectStoreTests PROPERTIES OUTPUT_NAME realm-object-store-tests) target_link_libraries(ObjectStoreTests ObjectStoreTestLib TestUtil) @@ -131,16 +137,17 @@ if(REALM_ENABLE_SYNC) target_link_libraries(ObjectStoreTestLib SyncServer) option(REALM_ENABLE_AUTH_TESTS "" OFF) if(REALM_ENABLE_AUTH_TESTS) - if(NOT REALM_MONGODB_ENDPOINT) - message(FATAL_ERROR "REALM_MONGODB_ENDPOINT must be set when specifying REALM_ENABLE_AUTH_TESTS.") - endif() - - message(STATUS "Auth tests enabled: ${REALM_MONGODB_ENDPOINT}") target_compile_definitions(ObjectStoreTestLib PRIVATE REALM_ENABLE_AUTH_TESTS=1 - REALM_MONGODB_ENDPOINT="${REALM_MONGODB_ENDPOINT}" ) + if(REALM_MONGODB_ENDPOINT) + message(STATUS "Auth tests enabled: ${REALM_MONGODB_ENDPOINT}") + target_compile_definitions(ObjectStoreTestLib PRIVATE + REALM_MONGODB_ENDPOINT="${REALM_MONGODB_ENDPOINT}" + ) + endif() + if(REALM_ADMIN_ENDPOINT) message(STATUS "BAAS admin endpoint: ${REALM_ADMIN_ENDPOINT}") target_compile_definitions(ObjectStoreTests PRIVATE diff --git a/test/object-store/audit.cpp b/test/object-store/audit.cpp index a0b5ffef2da..97886421c45 100644 --- a/test/object-store/audit.cpp +++ b/test/object-store/audit.cpp @@ -52,18 +52,11 @@ using namespace std::string_literals; using Catch::Matchers::StartsWith; using nlohmann::json; -namespace { -class NullLogger : public util::Logger { - // Since we don't want to log anything, do_log() does nothing - void do_log(const util::LogCategory&, Level, const std::string&) override {} -}; -} // namespace - static auto audit_logger = #ifdef AUDIT_LOG_LEVEL std::make_shared(AUDIT_LOG_LEVEL); #else - std::make_shared(); + std::make_shared(); #endif namespace { diff --git a/test/object-store/benchmarks/client_reset.cpp b/test/object-store/benchmarks/client_reset.cpp index b232eb1cf3d..3fea181b669 100644 --- a/test/object-store/benchmarks/client_reset.cpp +++ b/test/object-store/benchmarks/client_reset.cpp @@ -139,10 +139,7 @@ struct BenchmarkLocalClientReset : public reset_utils::TestClientReset { Transaction& wt_local = (Transaction&)m_local->read_group(); VersionID current_local_version = wt_local.get_version_of_current_transaction(); - class NullLogger : public util::Logger { - // Since we don't want to log anything, do_log() does nothing - void do_log(const util::LogCategory&, Level, const std::string&) override {} - } logger; + util::NullLogger logger; if (m_mode == ClientResyncMode::Recover) { auto history_local = dynamic_cast(wt_local.get_replication()->_get_history_write()); diff --git a/test/object-store/benchmarks/main.cpp b/test/object-store/benchmarks/main.cpp index daedc1e4ede..447a9984815 100644 --- a/test/object-store/benchmarks/main.cpp +++ b/test/object-store/benchmarks/main.cpp @@ -51,7 +51,7 @@ int main(int argc, char** argv) #endif #if TEST_SCHEDULER_UV - realm::util::Scheduler::set_default_factory([]() { + realm::util::Scheduler::set_default_factory([]() -> std::shared_ptr { return std::make_shared(); }); #endif diff --git a/test/object-store/dictionary.cpp b/test/object-store/dictionary.cpp index f72791b6ed8..0ae18396843 100644 --- a/test/object-store/dictionary.cpp +++ b/test/object-store/dictionary.cpp @@ -63,9 +63,7 @@ struct StringMaker { namespace cf = realm::collection_fixtures; TEST_CASE("nested dictionary in mixed", "[dictionary]") { - InMemoryTestFile config; - config.cache = false; config.automatic_change_notifications = false; config.schema = Schema{{"any_collection", {{"any", PropertyType::Mixed | PropertyType::Nullable}}}}; diff --git a/test/object-store/nested_collections.cpp b/test/object-store/nested_collections.cpp index cd94a94b078..99962d25337 100644 --- a/test/object-store/nested_collections.cpp +++ b/test/object-store/nested_collections.cpp @@ -36,7 +36,6 @@ using namespace realm; TEST_CASE("nested-list-mixed", "[nested-collections]") { InMemoryTestFile config; - config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({{ diff --git a/test/object-store/object.cpp b/test/object-store/object.cpp index b370234616a..ea779201724 100644 --- a/test/object-store/object.cpp +++ b/test/object-store/object.cpp @@ -160,7 +160,6 @@ TEST_CASE("object") { _impl::RealmCoordinator::assert_no_open_realms(); InMemoryTestFile config; - config.cache = false; config.automatic_change_notifications = false; config.schema_mode = SchemaMode::AdditiveExplicit; config.schema = Schema{ diff --git a/test/object-store/primitive_list.cpp b/test/object-store/primitive_list.cpp index 3160c88cc3c..9bab1b1deea 100644 --- a/test/object-store/primitive_list.cpp +++ b/test/object-store/primitive_list.cpp @@ -983,7 +983,6 @@ TEST_CASE("list of mixed links", "[primitives]") { TEST_CASE("list of strings - with index", "[primitives]") { InMemoryTestFile config; - config.cache = false; config.automatic_change_notifications = false; config.schema = Schema{ {"object", diff --git a/test/object-store/sync/app.cpp b/test/object-store/sync/app.cpp index 181ddb15858..92e13b89a2d 100644 --- a/test/object-store/sync/app.cpp +++ b/test/object-store/sync/app.cpp @@ -3924,7 +3924,6 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { std::unique_ptr app_session; auto redir_transport = std::make_shared(); AutoVerifiedEmailCredentials creds; - util::LogCategory::realm.set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); auto logger = util::Logger::get_default_logger(); App::Config app_config = {"fake-app-id"}; diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index ee76f011bfb..b44720796b5 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -4336,10 +4336,6 @@ TEST_CASE("client reset with nested collection", "[client reset][local][nested c if (!util::EventLoop::has_implementation()) return; - // remove this check once sync is ready - if (!realm::sync::SYNC_SUPPORTS_NESTED_COLLECTIONS) - return; - OfflineAppSession oas; SyncTestFile config(oas, "default"); config.cache = false; @@ -4440,7 +4436,7 @@ TEST_CASE("client reset with nested collection", "[client reset][local][nested c auto obj = table->get_object(0); auto col = table->get_column_key("any_mixed"); List list{local, obj, col}; - REQUIRE(list.size() == 4); + REQUIRE(list.size() == 3); auto mixed = list.get_any(0); REQUIRE(mixed.get_int() == 42); auto nlist = list.get_list(1); @@ -4491,7 +4487,7 @@ TEST_CASE("client reset with nested collection", "[client reset][local][nested c auto obj = table->get_object(0); auto col = table->get_column_key("any_mixed"); object_store::Dictionary dict{local, obj, col}; - REQUIRE(dict.size() == 4); + REQUIRE(dict.size() == 3); auto mixed = dict.get_any("Scalar"); REQUIRE(mixed.get_int() == 42); auto nlist = dict.get_list("List"); @@ -4505,55 +4501,6 @@ TEST_CASE("client reset with nested collection", "[client reset][local][nested c }) ->run(); } - SECTION("add nested collection both locally and remotely List vs Set") { - ObjectId pk_val = ObjectId::gen(); - SyncTestFile config2(oas.app()->current_user(), "default"); - config2.schema = config.schema; - auto test_reset = reset_utils::make_fake_local_client_reset(config, config2); - test_reset - ->make_local_changes([&](SharedRealm local) { - advance_and_notify(*local); - auto table = get_table(*local, "TopLevel"); - auto obj = table->create_object_with_primary_key(pk_val); - auto col = table->get_column_key("any_mixed"); - obj.set_collection(col, CollectionType::List); - List list{local, obj, col}; - list.insert(0, Mixed{30}); - REQUIRE(list.size() == 1); - }) - ->make_remote_changes([&](SharedRealm remote_realm) { - advance_and_notify(*remote_realm); - auto table = get_table(*remote_realm, "TopLevel"); - auto obj = table->create_object_with_primary_key(pk_val); - auto col = table->get_column_key("any_mixed"); - obj.set_collection(col, CollectionType::Set); - object_store::Set set{remote_realm, obj, col}; - set.insert(Mixed{40}); - REQUIRE(set.size() == 1); - }) - ->on_post_reset([&](SharedRealm local_realm) { - advance_and_notify(*local_realm); - if (test_mode == ClientResyncMode::DiscardLocal) { - TableRef table = get_table(*local_realm, "TopLevel"); - REQUIRE(table->size() == 1); - auto obj = table->get_object(0); - auto col = table->get_column_key("any_mixed"); - object_store::Set set{local_realm, obj, col}; - REQUIRE(set.size() == 1); - REQUIRE(set.get_any(0).get_int() == 40); - } - else { - TableRef table = get_table(*local_realm, "TopLevel"); - REQUIRE(table->size() == 1); - auto obj = table->get_object(0); - auto col = table->get_column_key("any_mixed"); - List list{local_realm, obj, col}; - REQUIRE(list.size() == 1); - REQUIRE(list.get_any(0).get_int() == 30); - } - }) - ->run(); - } SECTION("add nested collection both locally and remotely List vs Dictionary") { ObjectId pk_val = ObjectId::gen(); SyncTestFile config2(oas.app()->current_user(), "default"); diff --git a/test/object-store/test_runner.cpp b/test/object-store/test_runner.cpp index dce3b7734ac..448e428ba3e 100644 --- a/test/object-store/test_runner.cpp +++ b/test/object-store/test_runner.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #if TEST_SCHEDULER_UV @@ -38,6 +39,76 @@ #include #include +#ifndef TEST_ENABLE_LOGGING +#define TEST_ENABLE_LOGGING 0 // change to 1 to enable trace-level logging +#endif + +#ifndef TEST_LOGGING_LEVEL +#if TEST_ENABLE_LOGGING +#define TEST_LOGGING_LEVEL all +#else +#define TEST_LOGGING_LEVEL off +#endif // TEST_ENABLE_LOGGING +#endif // TEST_LOGGING_LEVEL + +#define TEST_LOGGING_LEVEL_STORAGE off +#define TEST_LOGGING_LEVEL_SERVER off +/* +#define TEST_LOGGING_LEVEL_SYNC off +#define TEST_LOGGING_LEVEL_RESET trace +#define TEST_LOGGING_LEVEL_APP off +*/ + +static std::vector> default_log_levels = { + {"Realm", realm::util::Logger::Level::TEST_LOGGING_LEVEL}, +#ifdef TEST_LOGGING_LEVEL_STORAGE + {"Realm.Storage", realm::util::Logger::Level::TEST_LOGGING_LEVEL_STORAGE}, +#endif +#ifdef TEST_LOGGING_LEVEL_TRANSACTION + {"Realm.Storage.Transaction", realm::util::Logger::Level::TEST_LOGGING_LEVEL_TRANSACTION}, +#endif +#ifdef TEST_LOGGING_LEVEL_QUERY + {"Realm.Storage.Query", realm::util::Logger::Level::TEST_LOGGING_LEVEL_QUERY}, +#endif +#ifdef TEST_LOGGING_LEVEL_OBJECT + {"Realm.Storage.Object", realm::util::Logger::Level::TEST_LOGGING_LEVEL_OBJECT}, +#endif +#ifdef TEST_LOGGING_LEVEL_NOTIFICATION + {"Realm.Storage.Notification", realm::util::Logger::Level::TEST_LOGGING_LEVEL_NOTIFICATION}, +#endif +#ifdef TEST_LOGGING_LEVEL_SYNC + {"Realm.Sync", realm::util::Logger::Level::TEST_LOGGING_LEVEL_SYNC}, +#endif +#ifdef TEST_LOGGING_LEVEL_CLIENT + {"Realm.Sync.Client", realm::util::Logger::Level::TEST_LOGGING_LEVEL_CLIENT}, +#endif +#ifdef TEST_LOGGING_LEVEL_SESSION + {"Realm.Sync.Client.Session", realm::util::Logger::Level::TEST_LOGGING_LEVEL_SESSION}, +#endif +#ifdef TEST_LOGGING_LEVEL_CHANGESET + {"Realm.Sync.Client.Changeset", realm::util::Logger::Level::TEST_LOGGING_LEVEL_CHANGESET}, +#endif +#ifdef TEST_LOGGING_LEVEL_NETWORK + {"Realm.Sync.Client.Network", realm::util::Logger::Level::TEST_LOGGING_LEVEL_NETWORK}, +#endif +#ifdef TEST_LOGGING_LEVEL_RESET + {"Realm.Sync.Client.Reset", realm::util::Logger::Level::TEST_LOGGING_LEVEL_RESET}, +#endif +#ifdef TEST_LOGGING_LEVEL_SERVER + {"Realm.Sync.Server", realm::util::Logger::Level::TEST_LOGGING_LEVEL_SERVER}, +#endif +#ifdef TEST_LOGGING_LEVEL_APP + {"Realm.App", realm::util::Logger::Level::TEST_LOGGING_LEVEL_APP}, +#endif +}; + +static void set_default_level_thresholds() +{ + for (auto [cat, level] : default_log_levels) { + realm::util::LogCategory::get_category(cat).set_default_level_threshold(level); + } +} + int run_object_store_tests(int argc, const char** argv); @@ -86,11 +157,13 @@ int run_object_store_tests(int argc, const char** argv) #endif #if TEST_SCHEDULER_UV - realm::util::Scheduler::set_default_factory([]() { + realm::util::Scheduler::set_default_factory([]() -> std::shared_ptr { return std::make_shared(); }); #endif + set_default_level_thresholds(); + Catch::Session session; session.useConfigData(config); int result = session.run(argc, argv); diff --git a/test/object-store/util/sync/baas_admin_api.cpp b/test/object-store/util/sync/baas_admin_api.cpp index eaf27468612..41dd4337f8d 100644 --- a/test/object-store/util/sync/baas_admin_api.cpp +++ b/test/object-store/util/sync/baas_admin_api.cpp @@ -248,17 +248,17 @@ size_t curl_write_cb(char* ptr, size_t size, size_t nmemb, std::string* response size_t curl_header_cb(char* buffer, size_t size, size_t nitems, std::map* response_headers) { REALM_ASSERT(response_headers); - std::string combined(buffer, size * nitems); + std::string_view combined(buffer, size * nitems); if (auto pos = combined.find(':'); pos != std::string::npos) { - std::string key = combined.substr(0, pos); - std::string value = combined.substr(pos + 1); - while (value.size() > 0 && value[0] == ' ') { - value = value.substr(1); + std::string_view key = combined.substr(0, pos); + std::string_view value = combined.substr(pos + 1); + if (auto first_not_space = value.find_first_not_of(' '); first_not_space != std::string::npos) { + value = value.substr(first_not_space); } - while (value.size() > 0 && (value[value.size() - 1] == '\r' || value[value.size() - 1] == '\n')) { - value = value.substr(0, value.size() - 1); + if (auto last_not_nl = value.find_last_not_of("\r\n"); last_not_nl != std::string::npos) { + value = value.substr(0, last_not_nl + 1); } - response_headers->insert({key, value}); + response_headers->insert({std::string{key}, std::string{value}}); } else { if (combined.size() > 5 && combined.substr(0, 5) != "HTTP/") { // ignore for now HTTP/1.1 ... @@ -268,6 +268,15 @@ size_t curl_header_cb(char* buffer, size_t size, size_t nitems, std::maperror("curl_easy_perform() failed when sending request to '%1' with body '%2': %3", request.url, + request.body, curl_easy_strerror(response_code)); } + if (logger->would_log(util::Logger::Level::trace)) { + std::string coid = [&] { + auto coid_header = response_headers.find("X-Appservices-Request-Id"); + if (coid_header == response_headers.end()) { + return std::string{}; + } + return util::format("BaaS Coid: \"%1\"", coid_header->second); + }(); + + logger->trace("Baas API %1 request to %2 took %3 %4\n", app::httpmethod_to_string(request.method), + request.url, std::chrono::duration_cast(total_time), coid); + } + int http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); return { @@ -341,6 +372,252 @@ app::Response do_http_request(const app::Request& request) }; } +class Baasaas { +public: + enum class StartMode { Default, GitHash, Branch, PatchId }; + explicit Baasaas(std::string api_key, StartMode mode, std::string ref_spec) + : m_api_key(std::move(api_key)) + , m_base_url(get_baasaas_base_url()) + , m_externally_managed_instance(false) + { + auto logger = util::Logger::get_default_logger(); + std::string url_path = "startContainer"; + if (mode == StartMode::GitHash) { + url_path = util::format("startContainer?githash=%1", ref_spec); + logger->info("Starting baasaas container with githash of %1", ref_spec); + } + else if (mode == StartMode::Branch) { + url_path = util::format("startContainer?branch=%1", ref_spec); + logger->info("Starting baasaas container on branch %1", ref_spec); + } + else if (mode == StartMode::PatchId) { + url_path = util::format("startContainer?patchId=%1", ref_spec); + logger->info("Starting baasaas container for patch id %1", ref_spec); + } + else { + logger->info("Starting baasaas container"); + } + + auto resp = do_request(std::move(url_path), app::HttpMethod::post); + m_container_id = resp["id"].get(); + logger->info("Baasaas container started with id \"%1\"", m_container_id); + auto lock_file = util::File(std::string{s_baasaas_lock_file_name}, util::File::mode_Write); + lock_file.write(m_container_id); + } + + explicit Baasaas(std::string api_key, std::string baasaas_instance_id) + : m_api_key(std::move(api_key)) + , m_base_url(get_baasaas_base_url()) + , m_container_id(std::move(baasaas_instance_id)) + , m_externally_managed_instance(true) + { + auto logger = util::Logger::get_default_logger(); + logger->info("Using externally managed baasaas instance \"%1\"", m_container_id); + } + + Baasaas(const Baasaas&) = delete; + Baasaas(Baasaas&&) = delete; + Baasaas& operator=(const Baasaas&) = delete; + Baasaas& operator=(Baasaas&&) = delete; + + ~Baasaas() + { + stop(); + } + + void poll() + { + if (!m_http_endpoint.empty() || m_container_id.empty()) { + return; + } + + auto logger = util::Logger::get_default_logger(); + auto poll_start_at = std::chrono::system_clock::now(); + std::string http_endpoint; + std::string mongo_endpoint; + bool logged = false; + while (std::chrono::system_clock::now() - poll_start_at < std::chrono::minutes(2) && + m_http_endpoint.empty()) { + if (http_endpoint.empty()) { + auto status_obj = + do_request(util::format("containerStatus?id=%1", m_container_id), app::HttpMethod::get); + if (!status_obj["httpUrl"].is_null()) { + http_endpoint = status_obj["httpUrl"].get(); + mongo_endpoint = status_obj["mongoUrl"].get(); + } + } + else { + app::Request baas_req; + baas_req.url = util::format("%1/api/private/v1.0/version", http_endpoint); + baas_req.method = app::HttpMethod::get; + baas_req.headers.insert_or_assign("Content-Type", "application/json"); + auto baas_resp = do_http_request(baas_req); + if (baas_resp.http_status_code >= 200 && baas_resp.http_status_code < 300) { + m_http_endpoint = http_endpoint; + m_mongo_endpoint = mongo_endpoint; + break; + } + } + + if (!logged) { + logger->info("Waiting for baasaas container \"%1\" to be ready", m_container_id); + logged = true; + } + std::this_thread::sleep_for(std::chrono::seconds(3)); + } + + if (m_http_endpoint.empty()) { + throw std::runtime_error( + util::format("Failed to launch baasaas container %1 within 2 minutes", m_container_id)); + } + } + + void stop() + { + if (m_externally_managed_instance) { + return; + } + auto container_id = std::move(m_container_id); + if (container_id.empty()) { + return; + } + + auto logger = util::Logger::get_default_logger(); + logger->info("Stopping baasaas container with id \"%1\"", container_id); + do_request(util::format("stopContainer?id=%1", container_id), app::HttpMethod::post); + auto lock_file = util::File(std::string{s_baasaas_lock_file_name}, util::File::mode_Write); + lock_file.resize(0); + lock_file.close(); + util::File::remove(lock_file.get_path()); + } + + const std::string& http_endpoint() + { + poll(); + return m_http_endpoint; + } + + const std::string& mongo_endpoint() + { + poll(); + return m_mongo_endpoint; + } + +private: + nlohmann::json do_request(std::string api_path, app::HttpMethod method) + { + app::Request request; + + request.url = util::format("%1/%2", m_base_url, api_path); + request.method = method; + request.headers.insert_or_assign("apiKey", m_api_key); + request.headers.insert_or_assign("Content-Type", "application/json"); + auto response = do_http_request(request); + REALM_ASSERT_EX(response.http_status_code >= 200 && response.http_status_code < 300, + util::format("Baasaas api response code: %1 Response body: %2", response.http_status_code, + response.body)); + return nlohmann::json::parse(response.body); + } + + static std::string get_baasaas_base_url() + { + auto env_value = getenv_sv("BAASAAS_BASE_URL"); + if (env_value.empty()) { + // This is the current default endpoint for baasaas maintained by the sync team. + // You can reach out for help in #appx-device-sync-internal if there are problems. + return "https://us-east-1.aws.data.mongodb-api.com/app/baas-container-service-autzb/endpoint"; + } + + return unquote_string(env_value); + } + + constexpr static std::string_view s_baasaas_lock_file_name = "baasaas_instance.lock"; + + std::string m_api_key; + std::string m_base_url; + std::string m_container_id; + bool m_externally_managed_instance; + std::string m_http_endpoint; + std::string m_mongo_endpoint; +}; + +class BaasaasLauncher : public Catch::EventListenerBase { +public: + static std::optional& get_baasaas_holder() + { + static std::optional global_baasaas = std::nullopt; + return global_baasaas; + } + + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) override + { + std::string_view api_key(getenv_sv("BAASAAS_API_KEY")); + if (api_key.empty()) { + return; + } + + // Allow overriding the baas base url at runtime via an environment variable, even if BAASAAS_API_KEY + // is also specified. + if (!getenv_sv("BAAS_BASE_URL").empty()) { + return; + } + + // If we've started a baasaas container outside of running the tests, then use that instead of + // figuring out how to start our own. + if (auto baasaas_instance = getenv_sv("BAASAAS_INSTANCE_ID"); !baasaas_instance.empty()) { + auto& baasaas_holder = get_baasaas_holder(); + REALM_ASSERT(!baasaas_holder); + baasaas_holder.emplace(std::string{api_key}, std::string{baasaas_instance}); + return; + } + + std::string_view ref_spec(getenv_sv("BAASAAS_REF_SPEC")); + std::string_view mode_spec(getenv_sv("BAASAAS_START_MODE")); + Baasaas::StartMode mode = Baasaas::StartMode::Default; + if (mode_spec == "branch") { + if (ref_spec.empty()) { + throw std::runtime_error("Expected branch name in BAASAAS_REF_SPEC env variable, but it was empty"); + } + mode = Baasaas::StartMode::Branch; + } + else if (mode_spec == "githash") { + if (ref_spec.empty()) { + throw std::runtime_error("Expected git hash in BAASAAS_REF_SPEC env variable, but it was empty"); + } + mode = Baasaas::StartMode::GitHash; + } + else if (mode_spec == "patchid") { + if (ref_spec.empty()) { + throw std::runtime_error("Expected patch id in BAASAAS_REF_SPEC env variable, but it was empty"); + } + mode = Baasaas::StartMode::PatchId; + } + else { + if (!mode_spec.empty()) { + throw std::runtime_error("Excepted BAASAAS_START_MODE to be \"githash\", \"patchid\", or \"branch\""); + } + ref_spec = {}; + } + + auto& baasaas_holder = get_baasaas_holder(); + REALM_ASSERT(!baasaas_holder); + baasaas_holder.emplace(std::string{api_key}, mode, std::string{ref_spec}); + + get_runtime_app_session(); + } + + void testRunEnded(Catch::TestRunStats const&) override + { + if (auto& baasaas_holder = get_baasaas_holder(); baasaas_holder.has_value()) { + baasaas_holder->stop(); + } + } +}; + +CATCH_REGISTER_LISTENER(BaasaasLauncher) + AdminAPIEndpoint AdminAPIEndpoint::operator[](StringData name) const { return AdminAPIEndpoint(util::format("%1/%2", m_url, name), m_access_token); @@ -581,7 +858,7 @@ AdminAPISession::Service AdminAPISession::get_sync_service(const std::string& ap void AdminAPISession::trigger_client_reset(const std::string& app_id, int64_t file_ident) const { - auto endpoint = apps(APIFamily::Private)[app_id]["sync"]["force_reset"]; + auto endpoint = apps(APIFamily::Admin)[app_id]["sync"]["force_reset"]; endpoint.put_json(nlohmann::json{{"file_ident", file_ident}}); } @@ -874,6 +1151,43 @@ realm::Schema get_default_schema() return realm::Schema({dog_schema, cat_schema, person_schema}); } +std::string get_base_url() +{ + if (auto baas_url = getenv_sv("BAAS_BASE_URL"); !baas_url.empty()) { + return std::string{baas_url}; + } + if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) { + return baasaas_holder->http_endpoint(); + } + + return get_compile_time_base_url(); +} + +std::string get_admin_url() +{ + if (auto baas_admin_url = getenv_sv("BAAS_ADMIN_URL"); !baas_admin_url.empty()) { + return std::string{baas_admin_url}; + } + if (auto compile_url = get_compile_time_admin_url(); !compile_url.empty()) { + return compile_url; + } + + return get_base_url(); +} + +std::string get_mongodb_server() +{ + if (auto baas_url = getenv_sv("BAAS_MONGO_URL"); !baas_url.empty()) { + return std::string{baas_url}; + } + + if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) { + return baasaas_holder->mongo_endpoint(); + } + return "mongodb://localhost:26000"; +} + + AppCreateConfig default_app_config() { ObjectId id = ObjectId::gen(); @@ -1252,9 +1566,11 @@ AppSession create_app(const AppCreateConfig& config) return object_schema.table_type == ObjectSchema::ObjectType::TopLevel; }); if (any_sync_types) { - timed_sleeping_wait_for([&] { - return session.is_initial_sync_complete(app_id); - }); + timed_sleeping_wait_for( + [&] { + return session.is_initial_sync_complete(app_id); + }, + std::chrono::seconds(30), std::chrono::seconds(1)); } return {client_app_id, app_id, session, config}; @@ -1269,10 +1585,6 @@ AppSession get_runtime_app_session() return cached_app_session; } -std::string get_mongodb_server() -{ - return "mongodb://localhost:26000"; -} #ifdef REALM_MONGODB_ENDPOINT TEST_CASE("app: baas admin api", "[sync][app][admin api][baas]") { diff --git a/test/object-store/util/sync/baas_admin_api.hpp b/test/object-store/util/sync/baas_admin_api.hpp index ce85d8ecc22..faa6cd51623 100644 --- a/test/object-store/util/sync/baas_admin_api.hpp +++ b/test/object-store/util/sync/baas_admin_api.hpp @@ -289,6 +289,8 @@ class SynchronousTestTransport : public app::GenericNetworkTransport { AppSession get_runtime_app_session(); std::string get_mongodb_server(); +std::string get_base_url(); +std::string get_admin_url(); template inline app::App::Config get_config(Factory factory, const AppSession& app_session) diff --git a/test/object-store/util/sync/sync_test_utils.cpp b/test/object-store/util/sync/sync_test_utils.cpp index 52fd33d6026..ddcea52d4bd 100644 --- a/test/object-store/util/sync/sync_test_utils.cpp +++ b/test/object-store/util/sync/sync_test_utils.cpp @@ -175,6 +175,23 @@ ExpectedRealmPaths::ExpectedRealmPaths(const std::string& base_path, const std:: legacy_sync_path = (dir_builder / cleaned_partition).string(); } +std::string unquote_string(std::string_view possibly_quoted_string) +{ + if (possibly_quoted_string.size() > 0) { + auto check_char = possibly_quoted_string.front(); + if (check_char == '"' || check_char == '\'') { + possibly_quoted_string.remove_prefix(1); + } + } + if (possibly_quoted_string.size() > 0) { + auto check_char = possibly_quoted_string.back(); + if (check_char == '"' || check_char == '\'') { + possibly_quoted_string.remove_suffix(1); + } + } + return std::string{possibly_quoted_string}; +} + #if REALM_ENABLE_SYNC void subscribe_to_all_and_bootstrap(Realm& realm) @@ -205,37 +222,23 @@ void wait_for_sessions_to_close(const TestAppSession& test_app_session) std::chrono::minutes(5), std::chrono::milliseconds(100)); } -static std::string unquote_string(std::string_view possibly_quoted_string) +std::string get_compile_time_base_url() { - if (possibly_quoted_string.size() > 0) { - auto check_char = possibly_quoted_string.front(); - if (check_char == '"' || check_char == '\'') { - possibly_quoted_string.remove_prefix(1); - } - } - if (possibly_quoted_string.size() > 0) { - auto check_char = possibly_quoted_string.back(); - if (check_char == '"' || check_char == '\'') { - possibly_quoted_string.remove_suffix(1); - } - } - return std::string{possibly_quoted_string}; -} - #ifdef REALM_MONGODB_ENDPOINT -std::string get_base_url() -{ // allows configuration with or without quotes return unquote_string(REALM_QUOTE(REALM_MONGODB_ENDPOINT)); +#else + return {}; +#endif } -std::string get_admin_url() +std::string get_compile_time_admin_url() { #ifdef REALM_ADMIN_ENDPOINT // allows configuration with or without quotes return unquote_string(REALM_QUOTE(REALM_ADMIN_ENDPOINT)); #else - return get_base_url(); + return {}; #endif } #endif // REALM_MONGODB_ENDPOINT @@ -316,8 +319,6 @@ void async_open_realm(const Realm::Config& config, finish(std::move(tsr), err); } -#endif // REALM_ENABLE_SYNC - class TestHelper { public: static DBRef& get_db(SharedRealm const& shared_realm) @@ -561,9 +562,11 @@ struct BaasClientReset : public TestClientReset { // // So just don't try to do anything until initial sync is done and we're sure the server is in a stable // state. - timed_sleeping_wait_for([&] { - return app_session.admin_api.is_initial_sync_complete(app_session.server_app_id); - }); + timed_sleeping_wait_for( + [&] { + return app_session.admin_api.is_initial_sync_complete(app_session.server_app_id); + }, + std::chrono::seconds(30), std::chrono::seconds(1)); auto realm = Realm::get_shared_realm(m_local_config); auto session = sync_manager->get_existing_session(realm->config().path); diff --git a/test/object-store/util/sync/sync_test_utils.hpp b/test/object-store/util/sync/sync_test_utils.hpp index 9649f7f3517..c58c58de52d 100644 --- a/test/object-store/util/sync/sync_test_utils.hpp +++ b/test/object-store/util/sync/sync_test_utils.hpp @@ -128,6 +128,10 @@ struct ExpectedRealmPaths { std::vector legacy_sync_directories_to_make; }; +// Takes a string_view of a possibly quoted string (i.e. the string begins with '"' and ends with '"') +// and returns an owned string without the quotes. +std::string unquote_string(std::string_view possibly_quoted_string); + #if REALM_ENABLE_SYNC template @@ -140,10 +144,8 @@ void subscribe_to_all_and_bootstrap(Realm& realm); #if REALM_ENABLE_AUTH_TESTS void wait_for_sessions_to_close(const TestAppSession& test_app_session); -#ifdef REALM_MONGODB_ENDPOINT -std::string get_base_url(); -std::string get_admin_url(); -#endif +std::string get_compile_time_base_url(); +std::string get_compile_time_admin_url(); #endif // REALM_ENABLE_AUTH_TESTS struct AutoVerifiedEmailCredentials : app::AppCredentials { diff --git a/test/object-store/util/test_file.cpp b/test/object-store/util/test_file.cpp index 5c4983678a5..048130cf40a 100644 --- a/test/object-store/util/test_file.cpp +++ b/test/object-store/util/test_file.cpp @@ -65,62 +65,11 @@ inline static int mkstemp(char* _template) using namespace realm; -static std::vector> default_log_levels = { - {"Realm", realm::util::Logger::Level::TEST_LOGGING_LEVEL}, -#ifdef TEST_LOGGING_LEVEL_STORAGE - {"Realm.Storage", realm::util::Logger::Level::TEST_LOGGING_LEVEL_STORAGE}, -#endif -#ifdef TEST_LOGGING_LEVEL_TRANSACTION - {"Realm.Storage.Transaction", realm::util::Logger::Level::TEST_LOGGING_LEVEL_TRANSACTION}, -#endif -#ifdef TEST_LOGGING_LEVEL_QUERY - {"Realm.Storage.Query", realm::util::Logger::Level::TEST_LOGGING_LEVEL_QUERY}, -#endif -#ifdef TEST_LOGGING_LEVEL_OBJECT - {"Realm.Storage.Object", realm::util::Logger::Level::TEST_LOGGING_LEVEL_OBJECT}, -#endif -#ifdef TEST_LOGGING_LEVEL_NOTIFICATION - {"Realm.Storage.Notification", realm::util::Logger::Level::TEST_LOGGING_LEVEL_NOTIFICATION}, -#endif -#ifdef TEST_LOGGING_LEVEL_SYNC - {"Realm.Sync", realm::util::Logger::Level::TEST_LOGGING_LEVEL_SYNC}, -#endif -#ifdef TEST_LOGGING_LEVEL_CLIENT - {"Realm.Sync.Client", realm::util::Logger::Level::TEST_LOGGING_LEVEL_CLIENT}, -#endif -#ifdef TEST_LOGGING_LEVEL_SESSION - {"Realm.Sync.Client.Session", realm::util::Logger::Level::TEST_LOGGING_LEVEL_SESSION}, -#endif -#ifdef TEST_LOGGING_LEVEL_CHANGESET - {"Realm.Sync.Client.Changeset", realm::util::Logger::Level::TEST_LOGGING_LEVEL_CHANGESET}, -#endif -#ifdef TEST_LOGGING_LEVEL_NETWORK - {"Realm.Sync.Client.Network", realm::util::Logger::Level::TEST_LOGGING_LEVEL_NETWORK}, -#endif -#ifdef TEST_LOGGING_LEVEL_RESET - {"Realm.Sync.Client.Reset", realm::util::Logger::Level::TEST_LOGGING_LEVEL_RESET}, -#endif -#ifdef TEST_LOGGING_LEVEL_SERVER - {"Realm.Sync.Server", realm::util::Logger::Level::TEST_LOGGING_LEVEL_SERVER}, -#endif -#ifdef TEST_LOGGING_LEVEL_APP - {"Realm.App", realm::util::Logger::Level::TEST_LOGGING_LEVEL_APP}, -#endif -}; - -static void set_default_level_thresholds() -{ - for (auto [cat, level] : default_log_levels) { - realm::util::LogCategory::get_category(cat).set_default_level_threshold(level); - } -} - TestFile::TestFile() { disable_sync_to_disk(); m_temp_dir = util::make_temp_dir(); path = (fs::path(m_temp_dir) / "realm.XXXXXX").string(); - set_default_level_thresholds(); if (const char* crypt_key = test_util::crypt_key()) { encryption_key = std::vector(crypt_key, crypt_key + 64); } @@ -167,7 +116,6 @@ InMemoryTestFile::InMemoryTestFile() in_memory = true; schema_version = 0; encryption_key = std::vector(); - set_default_level_thresholds(); } DBOptions InMemoryTestFile::options() const @@ -390,7 +338,6 @@ TestAppSession::TestAppSession(AppSession session, if (!m_transport) m_transport = instance_of; auto app_config = get_config(m_transport, *m_app_session); - set_default_level_thresholds(); set_app_config_defaults(app_config, m_transport); util::try_make_dir(m_base_file_path); @@ -472,10 +419,7 @@ std::vector TestAppSession::get_documents(SyncUser& user, co // MARK: - TestSyncManager -TestSyncManager::Config::Config() -{ - set_default_level_thresholds(); -} +TestSyncManager::Config::Config() {} TestSyncManager::TestSyncManager(const Config& config, const SyncServer::Config& sync_server_config) : m_sync_server(sync_server_config) @@ -546,8 +490,6 @@ OfflineAppSession::OfflineAppSession(OfflineAppSession::Config config) sc_config.metadata_mode = config.metadata_mode; sc_config.socket_provider = config.socket_provider; - set_default_level_thresholds(); - m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); } diff --git a/test/object-store/util/test_file.hpp b/test/object-store/util/test_file.hpp index 4d3e01c2891..d3b50438e25 100644 --- a/test/object-store/util/test_file.hpp +++ b/test/object-store/util/test_file.hpp @@ -98,26 +98,6 @@ struct InMemoryTestFile : realm::Realm::Config { void advance_and_notify(realm::Realm& realm); void on_change_but_no_notify(realm::Realm& realm); -#ifndef TEST_ENABLE_LOGGING -#define TEST_ENABLE_LOGGING 0 // change to 1 to enable trace-level logging -#endif - -#ifndef TEST_LOGGING_LEVEL -#if TEST_ENABLE_LOGGING -#define TEST_LOGGING_LEVEL all -#else -#define TEST_LOGGING_LEVEL off -#endif // TEST_ENABLE_LOGGING -#endif // TEST_LOGGING_LEVEL - -#define TEST_LOGGING_LEVEL_STORAGE off -#define TEST_LOGGING_LEVEL_SERVER off -/* -#define TEST_LOGGING_LEVEL_SYNC off -#define TEST_LOGGING_LEVEL_RESET trace -#define TEST_LOGGING_LEVEL_APP off -*/ - #if REALM_ENABLE_SYNC using StartImmediately = realm::util::TaggedBool; @@ -198,7 +178,6 @@ class TestSyncManager { std::string base_path; realm::SyncManager::MetadataMode metadata_mode = realm::SyncManager::MetadataMode::NoMetadata; bool should_teardown_test_directory = true; - realm::util::Logger::Level log_level = realm::util::Logger::Level::TEST_LOGGING_LEVEL; bool start_sync_client = true; }; diff --git a/test/test_all.cpp b/test/test_all.cpp index 6363776a887..a2f6426901a 100644 --- a/test/test_all.cpp +++ b/test/test_all.cpp @@ -128,6 +128,7 @@ const char* file_order[] = { "large_tests*.cpp", "test_crypto.cpp", + "test_transform_collections_mixed.cpp", "test_transform.cpp", "test_array.cpp", "test_lang_bind_helper_sync.cpp", diff --git a/test/test_client_reset.cpp b/test/test_client_reset.cpp index f5e910b71c6..c6beed04601 100644 --- a/test/test_client_reset.cpp +++ b/test/test_client_reset.cpp @@ -1477,13 +1477,6 @@ TEST(ClientReset_Recover_RecoverableChangesOnListsAfterUnrecoverableAreNotDuplic CHECK_EQUAL(changes.size(), 1); } -namespace { -class NullLogger : public util::Logger { - // Since we don't want to log anything, do_log() does nothing - void do_log(const util::LogCategory&, Level, const std::string&) override {} -}; -} // namespace - // Apply uploaded changes in src to dst as if they had been exchanged by sync void apply_changes(DB& src, DB& dst) { @@ -1515,7 +1508,7 @@ void apply_changes(DB& src, DB& dst) dst_progress.download.server_version += remote_changesets.size(); dst_progress.latest_server_version.version += remote_changesets.size(); - NullLogger logger; + util::NullLogger logger; VersionInfo new_version; dst_history.integrate_server_changesets(dst_progress, nullptr, remote_changesets, new_version, DownloadBatchState::SteadyState, logger, dst.start_read()); diff --git a/test/test_list.cpp b/test/test_list.cpp index 2bfa0232873..2e88514780e 100644 --- a/test/test_list.cpp +++ b/test/test_list.cpp @@ -1071,3 +1071,177 @@ TEST(List_Nested_Replication) Dictionary dict3(*dict, dict2_index); CHECK_EQUAL(dict3.get_col_key(), col_any); } + +namespace realm { +static std::ostream& operator<<(std::ostream& os, UpdateStatus status) +{ + switch (status) { + case UpdateStatus::Detached: + os << "Detatched"; + break; + case UpdateStatus::Updated: + os << "Updated"; + break; + case UpdateStatus::NoChange: + os << "NoChange"; + break; + } + return os; +} +} // namespace realm + +TEST(List_UpdateIfNeeded) +{ + SHARED_GROUP_TEST_PATH(path); + DBRef db = DB::create(make_in_realm_history(), path); + auto tr = db->start_write(); + auto table = tr->add_table("table"); + auto col = table->add_column(type_Mixed, "mixed"); + auto col2 = table->add_column(type_Mixed, "col2"); + auto leading_obj = table->create_object(); + Obj obj = table->create_object(); + obj.set_collection(col, CollectionType::List); + + auto list_1 = obj.get_list(col); + auto list_2 = obj.get_list(col); + + // The underlying object starts out up-to-date + CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::NoChange); + + // Attempt to initialize the accessor and fail because the list is empty, + // leaving it detached (only size() can be called on an empty list) + CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Detached); + CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Detached); + + list_1.add(Mixed()); + + // First accessor was used to create the list so it's already up to date, + // but the second is updated + CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::NoChange); + CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated); + + // The list is now non-empty, so a new accessor can initialize + auto list_3 = obj.get_list(col); + CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::NoChange); + + // A copy of a list is lazily initialized, so it's updated on first call + // even if the source was up-to-date + auto list_4 = std::make_shared>(list_3); + CHECK_EQUAL(list_4->update_if_needed(), UpdateStatus::Updated); + + // Nested lists work the same way as top-level ones + list_4->insert_collection(1, CollectionType::List); + auto list_4_1 = list_4->get_list(1); + auto list_4_2 = list_4->get_list(1); + list_4_1->add(Mixed()); + // FIXME: this should be NoChange + CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated); + + // Update the row index of the parent object, forcing it to update + leading_obj.remove(); + + // Updating the base object directly first doesn't change the result of + // updating the list + CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Updated); + + CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated); + + // These two lists share the same parent, so the first updates due to the + // parent returning Updated, and the second updates due to seeing that the + // parent version has changed + CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated); + + tr->commit_and_continue_as_read(); + + // Committing the write transaction changes the obj's ref, so everything + // has to update + CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated); + + // Perform a write which does not result in obj changing + { + auto tr2 = db->start_write(); + tr2->add_table("other table"); + tr2->commit(); + } + tr->advance_read(); + + // The obj's storage version has changed, but nothing needs to update + CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::NoChange); + CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::NoChange); + CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::NoChange); + CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::NoChange); + CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::NoChange); + CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::NoChange); + + // Perform a write which does modify obj + { + auto tr2 = db->start_write(); + tr2->get_table("table")->get_object(obj.get_key()).set_any(col2, "value"); + tr2->commit(); + } + tr->advance_read(); + + // Everything needs to update even though the allocator's content version is unchanged + CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Updated); + CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Updated); + + // Everything updates to detached when the object is removed + tr->promote_to_write(); + obj.remove(); + + CHECK_EQUAL(list_1.get_obj().update_if_needed(), UpdateStatus::Detached); + CHECK_EQUAL(list_1.update_if_needed(), UpdateStatus::Detached); + CHECK_EQUAL(list_2.update_if_needed(), UpdateStatus::Detached); + CHECK_EQUAL(list_3.update_if_needed(), UpdateStatus::Detached); + CHECK_EQUAL(list_4_1->update_if_needed(), UpdateStatus::Detached); + CHECK_EQUAL(list_4_2->update_if_needed(), UpdateStatus::Detached); +} + +TEST(List_AsCollectionParent) +{ + Group g; + auto table = g.add_table("table"); + auto col = table->add_column(type_Mixed, "mixed"); + + Obj obj = table->create_object(); + obj.set_collection(col, CollectionType::List); + auto list_1 = obj.get_list(col); + list_1.insert_collection(0, CollectionType::List); + + // list_1 is stack allocated, so we have to create a new object which can + // serve as the owner. This object is not reused for multiple calls. + auto list_1_1 = list_1.get_list(0); + auto list_1_2 = list_1.get_list(0); + CHECK_NOT_EQUAL(list_1_1->get_owner(), &list_1); + CHECK_NOT_EQUAL(list_1_1->get_owner(), list_1_2->get_owner()); + + // list_2 is heap allocated but not owned by a shared_ptr, so we have to + // create a new object which can serve as the owner. This object is not + // reused for multiple calls. + auto list_2 = obj.get_list_ptr(col); + auto list_2_1 = list_2->get_list(0); + auto list_2_2 = list_2->get_list(0); + CHECK_NOT_EQUAL(list_2_1->get_owner(), list_2.get()); + CHECK_NOT_EQUAL(list_2_1->get_owner(), list_2_2->get_owner()); + + // list_3 is owned by a shared_ptr, so we can just use it as the owner directly + auto list_3 = std::shared_ptr{std::move(list_2)}; + auto list_3_1 = list_3->get_list(0); + auto list_3_2 = list_3->get_list(0); + CHECK_EQUAL(list_3_1->get_owner(), list_3.get()); + CHECK_EQUAL(list_3_1->get_owner(), list_3_2->get_owner()); +} diff --git a/test/test_parser.cpp b/test/test_parser.cpp index 2fd69da5a6c..d60d5c1d215 100644 --- a/test/test_parser.cpp +++ b/test/test_parser.cpp @@ -5843,8 +5843,8 @@ TEST(Parser_CollectionLinks) Obj charlie = persons->create_object_with_primary_key("charlie"); Obj david = persons->create_object_with_primary_key("david"); - Obj elisabeth = persons->create_object_with_primary_key("elisabeth"); - Obj felix = persons->create_object_with_primary_key("felix"); + persons->create_object_with_primary_key("elisabeth"); + persons->create_object_with_primary_key("felix"); Obj gary = persons->create_object_with_primary_key("gary"); Obj hutch = persons->create_object_with_primary_key("hutch"); diff --git a/test/test_sync.cpp b/test/test_sync.cpp index e36efcb1183..03565b1514f 100644 --- a/test/test_sync.cpp +++ b/test/test_sync.cpp @@ -6107,7 +6107,7 @@ TEST(Sync_Dictionary) } } -TEST_IF(Sync_CollectionInMixed, sync::SYNC_SUPPORTS_NESTED_COLLECTIONS) +TEST(Sync_CollectionInMixed) { TEST_CLIENT_DB(db_1); TEST_CLIENT_DB(db_2); @@ -6266,7 +6266,7 @@ TEST_IF(Sync_CollectionInMixed, sync::SYNC_SUPPORTS_NESTED_COLLECTIONS) } } -TEST_IF(Sync_CollectionInCollection, SYNC_SUPPORTS_NESTED_COLLECTIONS) +TEST(Sync_CollectionInCollection) { TEST_CLIENT_DB(db_1); TEST_CLIENT_DB(db_2); diff --git a/test/test_transform.cpp b/test/test_transform.cpp index 95754ce92a9..7a98a187517 100644 --- a/test/test_transform.cpp +++ b/test/test_transform.cpp @@ -1805,6 +1805,194 @@ TEST(Transform_AddIntegerSurvivesSetDefault_NoRegularSets) }); } +TEST(Transform_AddIntegerBeforeUpdateString) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set(col_any, Mixed{0}); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_1->history.set_time(1); + client_2->history.set_time(2); + + client_1->transaction([&](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.add_int(col_any, 42); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.set(col_any, Mixed{"test"}); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + CHECK_EQUAL(table->get_object_with_primary_key(1).get_any(col_any), "test"); +} + +TEST(Transform_AddIntegerAfterUpdateString) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set(col_any, Mixed{0}); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_2->history.set_time(1); + client_1->history.set_time(2); + + client_1->transaction([&](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.add_int(col_any, 42); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.set(col_any, Mixed{"test"}); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + CHECK_EQUAL(table->get_object_with_primary_key(1).get_any(col_any), "test"); +} + +TEST(Transform_AddIntegerVsCreateArray) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set(col_any, Mixed{0}); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_1->history.set_time(1); + client_2->history.set_time(2); + + client_1->transaction([&](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.add_int(col_any, 42); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list_ptr(col_any); + list->add(1); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + auto list = table->get_object_with_primary_key(1).get_list_ptr(col_any); + CHECK_EQUAL(list->size(), 1); + CHECK_EQUAL(list->get(0), 1); +} + +TEST(Transform_AddIntegerVsCreateDictionary) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set(col_any, Mixed{0}); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_1->history.set_time(1); + client_2->history.set_time(2); + + client_1->transaction([&](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.add_int(col_any, 42); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", 1); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + auto dict = table->get_object_with_primary_key(1).get_dictionary_ptr(col_any); + CHECK_EQUAL(dict->size(), 1); + CHECK_EQUAL(dict->get("key1"), 1); +} + TEST(Transform_DanglingLinks) { auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); @@ -2616,4 +2804,211 @@ TEST(Transform_ArrayClearVersusClearRegression) server->integrate_next_changeset_from(*client_2); } +TEST(Transform_CreateDictionaryVsArrayInsert_DiscardArray) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict->get_dictionary("A"); + dict2->insert_collection("B", CollectionType::List); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_1->history.set_time(1); + client_2->history.set_time(2); + + client_1->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + auto dict = obj.get_dictionary_ptr({col_any, "A"}); + dict->insert_collection("B", CollectionType::Dictionary); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + auto list = obj.get_list_ptr({col_any, "A", "B"}); + list->add(1); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + CHECK(table->get_object_with_primary_key(1).get_dictionary_ptr({col_any, "A", "B"})->is_empty()); +} + +TEST(Transform_CreateArrayVsArrayInsert_NoConflict) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict->get_dictionary("A"); + dict2->insert_collection("B", CollectionType::Dictionary); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_1->history.set_time(1); + client_2->history.set_time(2); + + client_1->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + auto dict = obj.get_dictionary_ptr({col_any, "A"}); + dict->insert_collection("B", CollectionType::List); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + auto dict = obj.get_dictionary_ptr({col_any, "A"}); + dict->insert_collection("B", CollectionType::List); + auto list = dict->get_list("B"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert("C", "some value"); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + CHECK_EQUAL(table->get_object_with_primary_key(1).get_dictionary_ptr({col_any, "A", "B", 0})->get("C"), + Mixed("some value")); +} + +TEST(Transform_CreateArrayVsDictionaryInsert_DiscardDictionary) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict->get_dictionary("A"); + dict2->insert_collection("B", CollectionType::Dictionary); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_1->history.set_time(1); + client_2->history.set_time(2); + + client_1->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + auto dict = obj.get_dictionary_ptr({col_any, "A"}); + dict->insert_collection("B", CollectionType::List); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + auto dict = obj.get_dictionary_ptr({col_any, "A", "B"}); + dict->insert("C", "some value"); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + CHECK(table->get_object_with_primary_key(1).get_list_ptr({col_any, "A", "B"})->is_empty()); +} + +TEST(Transform_ClearArrayVsUpdateInt) +{ + auto changeset_dump_dir_gen = get_changeset_dump_dir_generator(test_context); + auto server = Peer::create_server(test_context, changeset_dump_dir_gen.get()); + auto client_1 = Peer::create_client(test_context, 2, changeset_dump_dir_gen.get()); + auto client_2 = Peer::create_client(test_context, 3, changeset_dump_dir_gen.get()); + + // Create baseline + client_1->transaction([](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.add(1); + list.add(2); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + client_1->history.set_time(1); + client_2->history.set_time(2); + + client_1->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + auto list = obj.get_list(col_any); + list.clear(); + }); + + client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.set(col_any, Mixed{42}); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + ReadTransaction read_server(server->shared_group); + ReadTransaction read_client_1(client_1->shared_group); + ReadTransaction read_client_2(client_2->shared_group); + CHECK(compare_groups(read_server, read_client_1)); + CHECK(compare_groups(read_server, read_client_2, *test_context.logger)); + auto table = read_server.get_table("class_Table"); + auto col_any = table->get_column_key("any"); + CHECK_EQUAL(table->get_object_with_primary_key(1).get_any(col_any), 42); +} + } // unnamed namespace diff --git a/test/test_transform_collections_mixed.cpp b/test/test_transform_collections_mixed.cpp new file mode 100644 index 00000000000..8c0ad328e6b --- /dev/null +++ b/test/test_transform_collections_mixed.cpp @@ -0,0 +1,1457 @@ +#include "peer.hpp" +#include "util/dump_changesets.hpp" + +using namespace realm; +using namespace realm::test_util; + +struct TransformTestHarness { + enum ConflictOrdering { ClientOneBeforeTwo, ClientTwoBeforeOne, SameTime }; + + template + explicit TransformTestHarness(unit_test::TestContext& test_context, ConflictOrdering ordering, + Func&& baseline_func) + : TransformTestHarness(test_context) + { + client_1->transaction([&](Peer& c) { + auto& tr = *c.group; + TableRef table = tr.add_table_with_primary_key("class_Table", type_Int, "id"); + auto col_any = table->add_column(type_Mixed, "any"); + auto obj = table->create_object_with_primary_key(1); + baseline_func(obj, col_any); + }); + + synchronize(server.get(), {client_1.get(), client_2.get()}); + + switch (ordering) { + case SameTime: + break; + case ClientOneBeforeTwo: + client_1->history.set_time(1); + client_2->history.set_time(2); + break; + case ClientTwoBeforeOne: + client_2->history.set_time(1); + client_1->history.set_time(2); + break; + } + } + + explicit TransformTestHarness(unit_test::TestContext& test_context) + : test_context(test_context) + , changeset_dump_dir_gen(get_changeset_dump_dir_generator(test_context)) + , server(Peer::create_server(test_context, changeset_dump_dir_gen.get())) + , client_1(Peer::create_client(test_context, 2, changeset_dump_dir_gen.get())) + , client_2(Peer::create_client(test_context, 3, changeset_dump_dir_gen.get())) + { + } + + template + void transaction(const std::unique_ptr& p, Func&& func) + { + p->transaction([&](Peer& p) { + auto col_any = p.table("class_Table")->get_column_key("any"); + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + func(obj, col_any); + }); + } + + template + void check_merge_result(Func&& func) + { + synchronize(server.get(), {client_1.get(), client_2.get()}); + + auto read_server = server->shared_group->start_read(); + auto read_client_1 = client_1->shared_group->start_read(); + auto read_client_2 = client_2->shared_group->start_read(); + + CHECK(compare_groups(*read_server, *read_client_1)); + CHECK(compare_groups(*read_server, *read_client_2, *test_context.logger)); + auto table = read_server->get_table("class_Table"); + auto col_any = table->get_column_key("any"); + func(table->get_object_with_primary_key(Mixed{1}), col_any); + } + + unit_test::TestContext& test_context; + std::unique_ptr changeset_dump_dir_gen; + std::unique_ptr server; + std::unique_ptr client_1; + std::unique_ptr client_2; +}; + +// Test merging instructions at different level of nesting. + +TEST(Transform_CreateArrayVsArrayInsert) +{ + // Baseline: set property 'any' to Dictionary + // {id: 1, any: {}} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + }); + + // Client 1 sets property 'any' from Dictionary to List + // {id: 1, any: []} + h.client_1->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 2 sets property 'any' from Dictionary to List and inserts one integer in the list + // {id: 1, any: [42]} + h.client_2->transaction([](Peer& p) { + auto obj = p.table("class_Table")->get_object_with_primary_key(1); + auto col_any = p.table("class_Table")->get_column_key("any"); + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.add(42); + }); + + // Result: Both changes are accepted - both clients are setting the same type (idempotent), + // and the insert is on the same list + // {id: 1, any: [42]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr(col_any); + CHECK_EQUAL(list->size(), 1); + CHECK_EQUAL(list->get(0), 42); + }); +} + +TEST(Transform_Nested_CreateArrayVsArrayInsert) +{ + // Baseline: set 'any.A.0' to List and insert one integer in the list + // {id: 1, any: {{"A": [[42]]}}} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::List); + auto list2 = list->get_list(0); + list2->insert(0, 42); + }); + + // Client 2 sets 'any.A.0.0' from integer to List + // {id: 1, any: {{"A": [[[]]]}}} + h.transaction(h.client_2, [&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0}); + list->set_collection(0, CollectionType::List); + }); + + synchronize(h.server.get(), {h.client_2.get()}); + + // Client 1 sets 'any.A.0.0' from integer to List + // {id: 1, any: {{"A": [[[]]]}}} + h.transaction(h.client_1, [&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0}); + list->set_collection(0, CollectionType::List); + }); + + // Client 2 inserts one integer in list at 'any.A.0.0' + // {id: 1, any: {{"A": [[[42]]]}}} + h.transaction(h.client_2, [&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0, 0}); + list->add(42); + }); + + // Result: Both changes are accepted - both clients are setting the same type (idempotent), + // and the insert is on the same type + // {id: 1, any: {{"A": [[[42]]]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0, 0}); + CHECK_EQUAL(list->size(), 1); + CHECK_EQUAL(list->get(0), 42); + }); +} + +TEST(Transform_CreateArrayVsDictionaryInsert) +{ + // Baseline: set property 'any' to Dictionary + // {id: 1, any: {}} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + }); + + // Client 1 sets property 'any' from Dictionary to List + // {id: 1, any: []} + h.transaction(h.client_1, [&](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 2 inserts one integer in dictionary at 'any' + // {id: 1, any: {{"key": 42}}} + h.transaction(h.client_2, [&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert("key", 42); + }); + + // Result: Client 1 wins because its update is higher up in the path + // {id: 1, any: []} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK(obj.get_list_ptr(col_any)->is_empty()); + }); +} + +TEST(Transform_Nested_CreateArrayVsDictionaryInsert) +{ + // Baseline: set 'any.A.0.B' to Dictionary + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert_collection("B", CollectionType::Dictionary); + }); + + // Client 1 sets 'any.A.0.B' from Dictionary to List + // {id: 1, any: {{"A": [{{"B": []}}]}}} + h.transaction(h.client_1, [&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::List); + }); + + // Client 2 inserts one integer in dictionary at 'any.A.0.B' + // {id: 1, any: {{"A": [{{"B": {{"key": 42}}}}]}}} + h.transaction(h.client_2, [&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0, "B"}); + dict->insert("key", 42); + }); + + // Result: Client 1 wins because its update is higher up in the path + // {id: 1, any: {{"A": [{{"B": []}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK(obj.get_list_ptr({col_any, "A", 0, "B"})->is_empty()); + }); +} + +TEST(Transform_CreateDictionaryVsDictionaryInsert) +{ + // Baseline: set property 'any' to List + // {id: 1, any: []} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + auto set_nested_dictionary = [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + }; + + // Client 2 sets property 'any' from List to Dictionary + // {id: 1, any: []} + h.transaction(h.client_2, set_nested_dictionary); + + synchronize(h.server.get(), {h.client_2.get()}); + + // Client 1 sets property 'any' from List to Dictionary + // {id: 1, any: []} + h.transaction(h.client_1, set_nested_dictionary); + + // Client 2 inserts one integer in dictionary at 'any' + // {id: 1, any: {{"key": 42}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert("key", 42); + }); + + // Result: Both changes are accepted - both clients are setting the same type (idempotent), + // and the insert is on the same type + // {id: 1, any: {{"key": 42}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + CHECK_EQUAL(dict.size(), 1); + CHECK_EQUAL(dict.get("key"), 42); + }); +} + +TEST(Transform_Nested_CreateDictionaryVsDictionaryInsert) +{ + // Baseline: set 'any.A.0.B' to List + // {id: 1, any: {{"A": [{{"B": []}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert_collection("B", CollectionType::List); + }); + + auto set_nested_dictionary = [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::Dictionary); + }; + + // Client 2 sets 'any.A.0.B' from List to Dictionary + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + h.transaction(h.client_2, set_nested_dictionary); + + synchronize(h.server.get(), {h.client_2.get()}); + + // Client 1 sets 'any.A.0.B' from List to Dictionary + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + h.transaction(h.client_1, set_nested_dictionary); + + // Client 2 inserts one integer in dictionary at 'any.A.0.B' + // {id: 1, any: {{"A": [{{"B": {{"key": 42}}}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0, "B"}); + dict->insert("key", 42); + }); + + // Result: Both changes are accepted - both clients are setting the same type (idempotent), + // and the insert is on the same type + // {id: 1, any: {{"A": [{{"B": {{"key": 42}}}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0, "B"}); + CHECK_EQUAL(dict->size(), 1); + CHECK_EQUAL(dict->get("key"), 42); + }); +} + +TEST(Transform_CreateDictionaryVsArrayInsert) +{ + // Baseline: set property 'any' to List + // {id: 1, any: []} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 2 sets property 'any' from List to Dictionary + // {id: 1, any: {}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + }); + + // Client 2 inserts one integer in list at 'any' + // {id: 1, any: [42]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.add(42); + }); + + // Result: Client 1 wins because its update is higher up in the path + // {id: 1, any: {}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK(obj.get_dictionary(col_any).is_empty()); + }); +} + +TEST(Transform_Nested_CreateDictionaryVsArrayInsert) +{ + // Baseline: set 'any.A.0.B' to List + // {id: 1, any: {{"A": [{{"B": []}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::SameTime, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert_collection("B", CollectionType::List); + }); + + // Client 1 sets 'any.A.0.B' from List to Dictionary + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::Dictionary); + }); + + // Client 2 inserts one integer in list at 'any.A.0.B' + // {id: 1, any: {{"A": [{{"B": [42]}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0, "B"}); + list->add(42); + }); + + // Result: Client 1 wins because its update is higher up in the path + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK(obj.get_dictionary_ptr({col_any, "A", 0, "B"})->is_empty()); + }); +} + +TEST(Transform_ArrayInsertVsUpdateString) +{ + // Baseline: set property 'any' to List and insert two integers in the list + // {id: 1, any: [1, 2]} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.add(1); + list.add(2); + }); + + // Client 1 inserts one integer in the list at 'any' + // {id: 1, any: [1, 2, 3]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.add(3); + }); + + // Client 2 sets property 'any' from List to a string value + // {id: 1, any: "value"} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set(col_any, Mixed{"value"}); + }); + + // Client 2 wins because its update is higher up in the path + // {id: 1, any: "value"} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK_EQUAL(obj.get_any(col_any), "value"); + }); +} + +TEST(Transform_ClearArrayVsDictionaryInsert) +{ + // Baseline: set property 'any' to List + // {id: 1, any: []} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 1 inserts two integers in the list at 'any', clears the list, and + // inserts one integer in the list + // {id: 1, any: [3]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.add(1); + list.add(2); + list.clear(); + list.add(3); + }); + + // Client 2 sets property 'any' from List to Dictionary and inserts one integer in the dictionary + // {id: 1, any: {{"key1": 42}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", 42); + }); + + // Result: Client 2 wins because changing the collection type is higher up in the path than updating the + // collection. Also, Clear wins against any updates on the collection from the other users + // {id: 1, any: {}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + CHECK(dict.is_empty()); + }); +} + +// Test merging instructions at same level of nesting (both on Mixed properties and nested collections). + +TEST(Transform_CreateArrayBeforeUpdateInt) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to List + // {id: 1, any: []} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 2 sets property 'any' to an integer value (after Client 1's change) + // {id: 1, any: 42} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_any(col_any, 42); + }); + + // Result: Client 2 wins because its change has a higher timestamp + // {id: 1, any: 42} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK_EQUAL(obj.get_any(col_any), 42); + }); +} + +TEST(Transform_CreateArrayAfterUpdateInt) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientTwoBeforeOne, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to List + // {id: 1, any: []} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 2 sets property 'any' to an integer value (before Client 1's change) + // {id: 1, any: 42} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_any(col_any, 42); + }); + + // Result: Client 1 wins because its change has a higher timestamp + // {id: 1, any: []} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK(obj.get_list(col_any).is_empty()); + }); +} + +TEST(Transform_Nested_CreateArrayBeforeUpdateInt) +{ + // Baseline: set 'any.A.0' as Dictionary and insert one string in the dictionary + // {id: 1, any: {{"A": [{{"B": "some value"}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert("B", "some value"); + }); + + // Client 1 sets 'any.A.0.B' from string to List + // {id: 1, any: {{"A": [{{"B": []}}]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::List); + }); + + // Client 2 sets 'any.A.0.B' from string to integer (after Client 1's change) + // {id: 1, any: {{"A": [{{"B": 42}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert("B", 42); + }); + + // Result: Client 2 wins because its change has a higher timestamp + // {id: 1, any: {{"A": [{{"B": 42}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + CHECK_EQUAL(dict->size(), 1); + CHECK_EQUAL(dict->get("B"), 42); + }); +} + +TEST(Transform_CreateDictionaryBeforeUpdateInt) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to Dictionary + // {id: 1, any: {}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + }); + + // Client 2 sets property 'any' to an integer value (after Client 1's change) + // {id: 1, any: 42} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_any(col_any, 42); + }); + + // Result: Client 2 wins because its change has a higher timestamp + // {id: 1, any: 42} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK_EQUAL(obj.get_any(col_any), 42); + }); +} + +TEST(Transform_CreateDictionaryAfterUpdateInt) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientTwoBeforeOne, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to Dictionary + // {id: 1, any: {}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + }); + + // Client 2 sets property 'any' to an integer value (before Client 1's change) + // {id: 1, any: 42} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_any(col_any, 42); + }); + + // Result: Client 1 wins because its change has a higher timestamp + // {id: 1, any: {}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK(obj.get_dictionary(col_any).is_empty()); + }); +} + +TEST(Transform_Nested_CreateDictionaryAfterUpdateInt) +{ + // Baseline: set 'any.A.0' as Dictionary and insert one string in the dictionary + // {id: 1, any: {{"A": [{{"B": "some value"}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientTwoBeforeOne, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert("B", "some value"); + }); + + // Client 1 sets 'any.A.0.B' from string to Dictionary + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::Dictionary); + }); + + // Client 2 sets 'any.A.0.B' from string to integer (before Client 1's change) + // {id: 1, any: {{"A": [{{"B": 42}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert("B", 42); + }); + + // Result: Client 1 wins because its change has a higher timestamp + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + CHECK(obj.get_dictionary_ptr({col_any, "A", 0, "B"})->is_empty()); + }); +} + +TEST(Transform_MergeArrays) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to List and inserts two strings in the list + // {id: 1, any: ["a", "b"]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert(0, "a"); + list.insert(1, "b"); + }); + + // Client 2 sets property 'any' to List and inserts two strings in the list + // {id: 1, any: ["c", "d"]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert(0, "c"); + list.insert(1, "d"); + }); + + // Result: Both changes are accepted and the lists are merged - both clients are setting the same type + // (idempotent), and inserts are all in the same list + // {id: 1, any: ["a", "b", "c", "d"]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr(col_any); + CHECK_EQUAL(list->size(), 4); + CHECK_EQUAL(list->get(0), "a"); + CHECK_EQUAL(list->get(1), "b"); + CHECK_EQUAL(list->get(2), "c"); + CHECK_EQUAL(list->get(3), "d"); + }); +} + +TEST(Transform_Nested_MergeArrays) +{ + // Baseline: set 'any.A.0.B' to Dictionary + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert_collection("B", CollectionType::Dictionary); + }); + + // Client 1 sets 'any.A.0.B' from Dictionary to List and inserts two strings in the list + // {id: 1, any: {{"A": [{{"B": ["a", "b"]}}]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::List); + auto list = dict->get_list("B"); + list->insert(0, "a"); + list->insert(1, "b"); + }); + + // Client 2 sets 'any.A.0.B' from Dictionary to List and inserts two strings in the list + // {id: 1, any: {{"A": [{{"B": ["c", "d"]}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::List); + auto list = dict->get_list("B"); + list->insert(0, "c"); + list->insert(1, "d"); + }); + + // Result: Both changes are accepted and the lists are merged - both clients are setting the same type + // (idempotent), and inserts are all in the same list + // {id: 1, any: {{"A": [{{"B": ["a", "b", "c", "d"]}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0, "B"}); + CHECK_EQUAL(list->size(), 4); + CHECK_EQUAL(list->get(0), "a"); + CHECK_EQUAL(list->get(1), "b"); + CHECK_EQUAL(list->get(2), "c"); + CHECK_EQUAL(list->get(3), "d"); + }); +} + +TEST(Transform_Nested_MergeArrays_CorrectOrder) +{ + // Baseline: set 'any.A.0.B' to Dictionary + // {id: 1, any: {{"A": [{{"B": {}}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientTwoBeforeOne, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert_collection("B", CollectionType::Dictionary); + }); + + // Client 1 sets 'any.A.0.B' from Dictionary to List and inserts two strings in the list + // {id: 1, any: {{"A": [{{"B": ["a", "b"]}}]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::List); + auto list = dict->get_list("B"); + list->insert(0, "a"); + list->insert(1, "b"); + }); + + // Client 2 sets 'any.A.0.B' from Dictionary to List and inserts two strings in the list (before Client 1's + // changes) + // {id: 1, any: {{"A": [{{"B": ["c", "d"]}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::List); + auto list = dict->get_list("B"); + list->insert(0, "c"); + list->insert(1, "d"); + }); + + // Result: Both changes are accepted and the lists are merged - both clients are setting the same type + // (idempotent), and inserts are all in the same list (but Client 2's changes come first) + // {id: 1, any: {{"A": [{{"B": ["c", "d", "a", "b"]}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0, "B"}); + CHECK_EQUAL(list->size(), 4); + CHECK_EQUAL(list->get(0), "c"); + CHECK_EQUAL(list->get(1), "d"); + CHECK_EQUAL(list->get(2), "a"); + CHECK_EQUAL(list->get(3), "b"); + }); +} + +TEST(Transform_MergeDictionaries) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to Dictionary and inserts two strings in the dictionary + // {id: 1, any: {{"key1": "a"}, {"key2": "b"}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto list = obj.get_dictionary(col_any); + list.insert("key1", "a"); + list.insert("key2", "b"); + }); + + // Client 1 sets property 'any' to Dictionary and inserts two strings in the dictionary + // {id: 1, any: {{"key2": "y"}, {"key3": "z"}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto list = obj.get_dictionary(col_any); + list.insert("key2", "y"); + list.insert("key3", "z"); + }); + + // Result: Both changes are accepted and the dictionaries are merged - both clients are setting the same type + // (idempotent), and inserts are all in the same dictionary. Client 2 overwrites the value of key "key2" because + // its change has a higher timestamp. + // {id: 1, any: {{"key1": "a"}, {"key2": "y"}, {"key3": "z"}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + CHECK_EQUAL(dict.size(), 3); + CHECK_EQUAL(dict.get("key1"), "a"); + CHECK_EQUAL(dict.get("key2"), "y"); + CHECK_EQUAL(dict.get("key3"), "z"); + }); +} + +TEST(Transform_Nested_MergeDictionaries) +{ + // Baseline: set 'any.A.0.B' to List + // {id: 1, any: {{"A": [{{"B": []}}]}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + auto dict2 = list->get_dictionary(0); + dict2->insert_collection("B", CollectionType::List); + }); + + // Client 1 sets 'any.A.0.B' from List to Dictionary and inserts two strings in the dictionary + // {id: 1, any: {{"A": [{{"B": {{"key1": "a"}, {"key2": "b"}}}}]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::Dictionary); + auto dict2 = dict->get_dictionary("B"); + dict2->insert("key1", "a"); + dict2->insert("key2", "b"); + }); + + // Client 1 sets 'any.A.0.B' from List to Dictionary and inserts two strings in the dictionary + // {id: 1, any: {{"A": [{{"B": {{"key2": "y"}, {"key3": "z"}}}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::Dictionary); + auto dict2 = dict->get_dictionary("B"); + dict2->insert("key2", "y"); + dict2->insert("key3", "z"); + }); + + // Result: Both changes are accepted and the dictionaries are merged - both clients are setting the same type + // (idempotent), and inserts are all in the same dictionary. Client 2 overwrites the value of key "key2" because + // its change has a higher timestamp. + // {id: 1, any: {{"A": [{{"B": {{"key1": "a"}, {"key2": "y"}, {"key3": "z"}}}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0, "B"}); + CHECK_EQUAL(dict->size(), 3); + CHECK_EQUAL(dict->get("key1"), "a"); + CHECK_EQUAL(dict->get("key2"), "y"); + CHECK_EQUAL(dict->get("key3"), "z"); + }); +} + +TEST(Transform_CreateArrayAfterCreateDictionary) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientTwoBeforeOne, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to List and inserts two strings in the list + // {id: 1, any: ["a", "b"]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert(0, "a"); + list.insert(1, "b"); + }); + + // Client 2 sets property 'any' to Dictionary and inserts two strings in the dictionary (before Client 1's change) + // {id: 1, any: {{"key1": "a"}, {"key2": "b"}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", "a"); + dict.insert("key2", "b"); + }); + + // Result: Client 1 wins because its change has a higher timestamp + // {id: 1, any: ["a", "b"]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr(col_any); + CHECK_EQUAL(list->size(), 2); + CHECK_EQUAL(list->get(0), "a"); + CHECK_EQUAL(list->get(1), "b"); + }); +} + +TEST(Transform_CreateArrayBeforeCreateDictionary) +{ + // Baseline: property 'any' is not set to any type + // {id: 1, any: null} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj, ColKey) {}); + + // Client 1 sets property 'any' to List and inserts two strings in the list + // {id: 1, any: ["a", "b"]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert(0, "a"); + list.insert(1, "b"); + }); + + // Client 2 sets property 'any' to Dictionary and inserts two strings in the dictionary (after Client 1's change) + // {id: 1, any: {{"key1": "a"}, {"key2": "b"}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", "a"); + dict.insert("key2", "b"); + }); + + // Result: Client 2 wins because its change has a higher timestamp + // {id: 1, any: {{"key1": "a"}, {"key2": "b"}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr(col_any); + CHECK_EQUAL(dict->size(), 2); + CHECK_EQUAL(dict->get("key1"), "a"); + CHECK_EQUAL(dict->get("key2"), "b"); + }); +} + +TEST(Transform_Nested_CreateArrayAfterCreateDictionary) +{ + // Baseline: set 'any.A.0' to Dictionary + // {id: 1, any: {{"A": [{}]}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientTwoBeforeOne, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->insert_collection(0, CollectionType::Dictionary); + }); + + // Client 1 inserts a List in dictionary at 'any.A.0' and then inserts two strings in the list + // {id: 1, any: {{"A": [{{"B": ["a", "b"]}}]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::List); + auto list = dict->get_list("B"); + list->insert(0, "a"); + list->insert(1, "b"); + }); + + // Client 2 inserts a Dictionary in dictionary at 'any.A.0' and then inserts two strings in the dictionary (before + // Client 1's changes) + // {id: 1, any: {{"A": [{{"B": {{"key1": "a", {"key2": "b"}}}}}]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A", 0}); + dict->insert_collection("B", CollectionType::Dictionary); + auto dict2 = dict->get_dictionary("B"); + dict2->insert("key1", "a"); + dict2->insert("key2", "b"); + }); + + // Result: Client 1 wins because its change has a higher timestamp + // {id: 1, any: {{"A": [{{"B": ["a", "b"]}}]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A", 0, "B"}); + CHECK_EQUAL(list->size(), 2); + CHECK_EQUAL(list->get(0), "a"); + CHECK_EQUAL(list->get(1), "b"); + }); +} + +TEST(Transform_Nested_ClearArrayVsUpdateString) +{ + // Baseline: set 'any.A' to List and insert two integers in the list + // {id: 1, any: {{"A": [1, 2]}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert_collection("A", CollectionType::List); + auto list = dict->get_list("A"); + list->add(1); + list->add(2); + }); + + // Client 1 clears the list at 'any.A' and inserts one integer in the list + // {id: 1, any: {{"A": [3]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A"}); + list->clear(); + list->add(3); + }); + + // Client 2 sets 'any.A' from List to a string value + // {id: 1, any: {{"A", "some value"}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr(col_any); + dict->insert("A", "some value"); + }); + + // Result: Client 2 wins - setting a property or item to a non-collections type wins against any updates + // (including clear) on that collection {id: 1, any: {{"A", "some value"}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr(col_any); + CHECK_EQUAL(dict->size(), 1); + CHECK_EQUAL(dict->get("A"), "some value"); + }); +} + +TEST(Transform_ClearArrayVsCreateArray) +{ + // Baseline: set property 'any' to Dictionary and insert two integers in the dictionary + // {id: 1, any: {{"key1": 1}, {"key2": 2}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", 1); + dict.insert("key2", 2); + }); + + // Client 1 sets property 'any' from Dictionary to List, inserts one integer in the list, clears the list, and + // inserts one integer in the list + // {id: 1, any: [2]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.add(1); + list.clear(); + list.add(2); + }); + + // Client 2 sets property 'any' from Dictionary to List and inserts one integer in the list + // {id: 1, any: [4]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.add(4); + }); + + // Result: Client 1 wins - Clear wins against the insertion from Client 2 + // {id: 1, any: [2]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr(col_any); + CHECK_EQUAL(list->size(), 1); + CHECK_EQUAL(list->get(0), 2); + }); +} + +TEST(Transform_ClearArrayInsideArrayVsCreateArray) +{ + // Baseline: set 'any.0' to Dictionary and insert two integers in the dictionary + // {id: 1, any: [{{"key1": 1}, {"key2": 2}}]} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert_collection(0, CollectionType::Dictionary); + auto dict = list.get_dictionary(0); + dict->insert("key1", 1); + dict->insert("key2", 2); + }); + + // Client 1 sets 'any.0' from Dictionary to List, inserts one integer in the list, clears the list, and inserts + // one integer in the list + // {id: 1, any: [2]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.set_collection(0, CollectionType::List); + auto list2 = list.get_list(0); + list2->add(1); + list2->clear(); + list2->add(2); + }); + + // Client 2 sets 'any.0' from Dictionary to List and inserts one integer in the list + // {id: 1, any: [4]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.set_collection(0, CollectionType::List); + auto list2 = list.get_list(0); + list2->add(4); + }); + + // Result: Client 1 wins - Clear wins against the insertion from Client 2 + // {id: 1, any: [2]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, 0}); + CHECK_EQUAL(list->size(), 1); + CHECK_EQUAL(list->get(0), 2); + }); +} + +TEST(Transform_ClearArrayInsideDictionaryVsCreateArray) +{ + // Baseline: set 'any.A' to Dictionary and insert two integers in the dictionary + // {id: 1, any: {{"A": {{{"key1": 1}, {"key2": 2}}}}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict.get_dictionary("A"); + dict2->insert("key1", 1); + dict2->insert("key2", 2); + }); + + // Client 1 sets 'any.A' from Dictionary to List, inserts one integer in the list, clears the list, and inserts + // one integer in the list + // {id: 1, any: {{"A": [2]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::List); + auto list = dict.get_list("A"); + list->add(1); + list->clear(); + list->add(2); + }); + + // Client 2 sets 'any.A' from Dictionary to List and inserts one integer in the list + // {id: 1, any: {{"A": [4]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::List); + auto list = dict.get_list("A"); + list->add(4); + }); + + // Result: Client 1 wins - Clear wins against the insertion from Client 2 + // {id: 1, any: {{"A": [2]}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A"}); + CHECK_EQUAL(list->size(), 1); + CHECK_EQUAL(list->get(0), 2); + }); +} + +TEST(Transform_ClearArrayVsCreateDictionary) +{ + // Baseline: set property 'any' to List + // {id: 1, any: []} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 1 inserts two integers in list at 'any', clears the list, and inserts one integer in the list + // {id: 1, any: [3]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.add(1); + list.add(2); + list.clear(); + list.add(3); + }); + + // Client 2 sets property 'any' from List to Dictionary and inserts one integer in the dictionary + // {id: 1, any: {{"key1": 42}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", 42); + }); + + // Result: Client 2 wins because changing the collection type is higher up in the path than updating the + // collection. Also, Clear wins against any updates on the collection from the other users + // {id: 1, any: {}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + CHECK(dict.is_empty()); + }); +} + +TEST(Transform_ClearArrayInsideArrayVsCreateDictionary) +{ + // Baseline: set property 'any' to List and insert one integer in the list + // {id: 1, any: [42]} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert(0, 42); + }); + + // Client 1 sets 'any.0' from integer to List, inserts one integer in the list, clears the list, and inserts one + // integer in the list + // {id: 1, any: [[2]]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.set_collection(0, CollectionType::List); + auto list2 = list.get_list(0); + list2->add(1); + list2->clear(); + list2->add(2); + }); + + // Client 2 sets 'any.0' from integer to Dictionary and inserts one string in the dictionary (after Client 1's + // changes) + // {id: 1, any: [{{"key1": "some value"}}]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.set_collection(0, CollectionType::Dictionary); + auto dict = list.get_dictionary(0); + dict->insert("key1", "some value"); + }); + + // Result: Client 2 wins because its change has a higher timestamp + // Also, Clear wins against any updates on the collection from the other users + // {id: 1, any: [{}]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, 0}); + CHECK(dict->is_empty()); + }); +} + +TEST(Transform_ClearArrayInsideDictionaryVsCreateDictionary) +{ + // Baseline: set property 'any' to Dictionary and insert one string in the dictionary + // {id: 1, any: {{"A": "some value"}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("A", "some value"); + }); + + // Client 1 sets 'any.A' from string to List, inserts one integer in the list, clears the list, and inserts one + // integer in the list + // {id: 1, any: {{"A": [2]}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::List); + auto list = dict.get_list("A"); + list->add(1); + list->clear(); + list->add(2); + }); + + // Client 2 sets 'any.A' from string to Dictionary and inserts one string in the dictionary (after Client 1's + // changes) + // {id: 1, any: {{"A": {{"key1": "some value"}}}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict.get_dictionary("A"); + dict2->insert("key1", "some other value"); + }); + + // Result: Client 2 wins because its change has a higher timestamp + // Also, Clear wins against any updates on the collection from the other users + // {id: 1, any: {{"A": {}}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A"}); + CHECK(dict->is_empty()); + }); +} + +TEST(Transform_ClearDictionaryVsCreateArray) +{ + // Baseline: set property 'any' to Dictionary and insert two integers in the dictionary + // {id: 1, any: {{"key1": 1}, {"key2": 2}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", 1); + dict.insert("key2", 2); + }); + + // Client 1 insert one integer in dictionary at 'any', clears the dictionary, and inserts one integer in the + // dictionary + // {id: 1, any: {{"key4": 4}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert("key3", 3); + dict.clear(); + dict.insert("key4", 4); + }); + + // Client 2 sets property 'any' from Dictionary to List and inserts one integer in the list + // {id: 1, any: [1]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.add(1); + }); + + // Result: Client 2 wins because changing the collection type is higher up in the path than updating the + // collection. Also, Clear wins against any updates on the collection from the other users + // {id: 1, any: []} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr(col_any); + CHECK(list->is_empty()); + }); +} + +TEST(Transform_ClearDictionaryInsideArrayVsCreateArray) +{ + // Baseline: set 'any.0' to Dictionary and insert two integers in the dictionary + // {id: 1, any: [{{"key1": 1}, {"key2": 2}}]} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert_collection(0, CollectionType::Dictionary); + auto dict = list.get_dictionary(0); + dict->insert("key1", 1); + dict->insert("key2", 2); + }); + + // Client 1 inserts one integer in dictionary at 'any.0', clears it, and insert one integer in the dictionary + // {id: 1, any: [{{"key4": 4}}]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, 0}); + dict->insert("key3", 3); + dict->clear(); + dict->insert("key4", 4); + }); + + // Client 2 sets 'any.0' from Dictionary to List and inserts one integer in the list + // {id: 1, any: [[4]]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.set_collection(0, CollectionType::List); + auto list2 = list.get_list(0); + list2->add(4); + }); + + // Result: Client 2 wins because changing the collection type is higher up in the path than updating the + // collection. Also, Clear wins against any updates on the collection from the other users + // {id: 1, any: [[]]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, 0}); + CHECK(list->is_empty()); + }); +} + +TEST(Transform_ClearDictionaryInsideDictionaryVsCreateArray) +{ + // Baseline: set 'any.A' to Dictionary and insert two integers in the dictionary + // {id: 1, any: {{"A": {{"key1": 1}, {"key2": 2}}}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict.get_dictionary("A"); + dict2->insert("key1", 1); + dict2->insert("key2", 2); + }); + + // Client 1 inserts one integer in dictionary at 'any.A', clear the dictionary, and inserts one integer in the + // dictionary + // {id: 1, any: {{"A": {{"key4": 4}}}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A"}); + dict->insert("key3", 3); + dict->clear(); + dict->insert("key4", 4); + }); + + // Client 2 sets 'any.A' from Dictionary to List and inserts one integer in the list + // {id: 1, any: {{"A": [4]}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::List); + auto list = dict.get_list("A"); + list->add(4); + }); + + // Result: Client 2 wins because changing the collection type is higher up in the path than updating the + // collection. Also, Clear wins against any updates on the collection from the other users + // {id: 1, any: {{"A": []}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto list = obj.get_list_ptr({col_any, "A"}); + CHECK(list->is_empty()); + }); +} + +TEST(Transform_ClearDictionaryVsCreateDictionary) +{ + // Baseline: set property 'any' to List + // {id: 1, any: []} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + }); + + // Client 1 sets property 'any' from List to Dictionary, inserts one integer in the dictionary, clears the + // dictionary, and inserts one integer in the dictionary + // {id: 1, any: {{"key2": 2}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key1", 1); + dict.clear(); + dict.insert("key2", 2); + }); + + // Client 2 sets property 'any' from List to Dictionary and inserts one integer in the dictionary + // {id: 1, any: {{"key3": 3}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("key3", 3); + }); + + // Result: Client 1 wins - Clear wins against the insertion from Client 2 + // {id: 1, any: {{"key2": 2}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + CHECK_EQUAL(dict.size(), 1); + CHECK_EQUAL(dict.get("key2"), 2); + }); +} + +TEST(Transform_ClearDictionaryInsideArrayVsCreateDictionary) +{ + // Baseline: set property 'any' to List and insert one integer in the list + // {id: 1, any: [42]} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::List); + auto list = obj.get_list(col_any); + list.insert(0, 42); + }); + + // Client 1 sets 'any.0' from integer to Dictionary, inserts one integer in the dictionary, clears the + // dictionary, and inserts one integer in the dictionary + // {id: 1, any: [{{"key2": 2}}]} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.insert_collection(0, CollectionType::Dictionary); + auto dict = list.get_dictionary(0); + dict->insert("key1", 1); + dict->clear(); + dict->insert("key2", 2); + }); + + // Client 2 sets 'any.0' from integer to Dictionary and inserts one integer in the dictionary + // {id: 1, any: [{{"key3": 3}}]} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto list = obj.get_list(col_any); + list.set_collection(0, CollectionType::Dictionary); + auto dict = list.get_dictionary(0); + dict->insert("key3", 3); + }); + + // Result: Client 1 wins - Clear wins against the insertion from Client 2 + // {id: 1, any: [{{"key2": 2}}]} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, 0}); + CHECK_EQUAL(dict->size(), 1); + CHECK_EQUAL(dict->get("key2"), 2); + }); +} + +TEST(Transform_ClearDictionaryInsideDictionaryVsCreateDictionary) +{ + // Baseline: set property 'any' to Dictionary and insert one string in the dictionary + // {id: 1, any: {{"A": "some value"}}} + TransformTestHarness h(test_context, TransformTestHarness::ClientOneBeforeTwo, [](Obj obj, ColKey col_any) { + obj.set_collection(col_any, CollectionType::Dictionary); + auto dict = obj.get_dictionary(col_any); + dict.insert("A", "some value"); + }); + + // Client 1 sets 'any.A' from string to Dictionary, inserts one integer in the dictionary, clears the dictionary, + // and inserts one integer in the dictionary + // {id: 1, any: {{"A": {{"key2": 2}}}}} + h.transaction(h.client_1, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict.get_dictionary("A"); + dict2->insert("key1", 1); + dict2->clear(); + dict2->insert("key2", 2); + }); + + // Client 2 sets 'any.A' from string to Dictionary and inserts one integer in the dictionary + // {id: 1, any: {{"A": {{"key3": 3}}}}} + h.transaction(h.client_2, [](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary(col_any); + dict.insert_collection("A", CollectionType::Dictionary); + auto dict2 = dict.get_dictionary("A"); + dict2->insert("key3", 3); + }); + + // Result: Client 1 wins - Clear wins against the insertion from Client 2 + // {id: 1, any: {{"A": {{"key2": 2}}}}} + h.check_merge_result([&](Obj obj, ColKey col_any) { + auto dict = obj.get_dictionary_ptr({col_any, "A"}); + CHECK_EQUAL(dict->size(), 1); + CHECK_EQUAL(dict->get("key2"), 2); + }); +}