diff --git a/CMake/CodeCoverage.cmake b/CMake/CodeCoverage.cmake new file mode 100644 index 00000000..b8308286 --- /dev/null +++ b/CMake/CodeCoverage.cmake @@ -0,0 +1,49 @@ +find_program(LCOV_PATH lcov) +find_program(GENHTML_PATH genhtml) +find_program(GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/tests) + +set(CMAKE_CXX_FLAGS_COVERAGE "-g -O0 -fprofile-arcs -ftest-coverage" + CACHE STRING "Flags used by the C++ compiler during coverage builds.") +mark_as_advanced(CMAKE_CXX_FLAGS_COVERAGE) + +if(CMAKE_BUILD_TYPE STREQUAL "Coverage") + if(NOT (LCOV_PATH AND GENHTML_PATH AND GCOVR_PATH)) + message(FATAL_ERROR "Generating a coverage report requires lcov and gcovr") + endif() + + function(create_coverage_target targetname testrunner) + add_custom_target(${targetname} + # Clear previous coverage information + COMMAND ${LCOV_PATH} --directory . --zerocounters + + # Run the tests + COMMAND ${testrunner} + + # Generate new coverage report + COMMAND ${LCOV_PATH} --directory . --capture --output-file coverage.info + COMMAND ${LCOV_PATH} --extract coverage.info '${CMAKE_SOURCE_DIR}/src/*' --output-file coverage.info.cleaned + COMMAND ${GENHTML_PATH} -o coverage coverage.info.cleaned + COMMAND ${CMAKE_COMMAND} -E remove coverage.info coverage.info.cleaned + + COMMAND echo Open coverage/index.html in your browser to view the coverage report. + + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + + add_custom_target(${targetname}-cobertura + COMMAND ${testrunner} + COMMAND ${GCOVR_PATH} -x -r ${CMAKE_SOURCE_DIR}/src -o coverage.xml + COMMAND echo Code coverage report written to coverage.xml + + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + endfunction() +else() + function(create_coverage_target targetname testrunner) + add_custom_target(${targetname} + COMMAND echo "Configure with -DCMAKE_BUILD_TYPE=Coverage to generate coverage reports") + + add_custom_target(${targetname}-cobertura + COMMAND echo "Configure with -DCMAKE_BUILD_TYPE=Coverage to generate coverage reports") + endfunction() +endif() diff --git a/CMake/CompilerFlags.cmake b/CMake/CompilerFlags.cmake index d45392e4..c214b625 100644 --- a/CMake/CompilerFlags.cmake +++ b/CMake/CompilerFlags.cmake @@ -3,6 +3,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED on) set(CMAKE_CXX_EXTENSIONS off) add_compile_options(-Wall -DREALM_HAVE_CONFIG) add_compile_options("$<$:-DREALM_DEBUG>") +add_compile_options("$<$:-DREALM_DEBUG>") if(${CMAKE_GENERATOR} STREQUAL "Ninja") if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") diff --git a/CMake/RealmCore.cmake b/CMake/RealmCore.cmake index 90256b98..d4d02e9a 100644 --- a/CMake/RealmCore.cmake +++ b/CMake/RealmCore.cmake @@ -60,6 +60,7 @@ function(download_realm_core core_version) add_library(realm STATIC IMPORTED) add_dependencies(realm realm-core) set_property(TARGET realm PROPERTY IMPORTED_LOCATION_DEBUG ${core_library_debug}) + set_property(TARGET realm PROPERTY IMPORTED_LOCATION_COVERAGE ${core_library_debug}) set_property(TARGET realm PROPERTY IMPORTED_LOCATION_RELEASE ${core_library_release}) set_property(TARGET realm PROPERTY IMPORTED_LOCATION ${core_library_release}) @@ -81,6 +82,7 @@ macro(define_built_realm_core_target core_directory) add_dependencies(realm realm-core) set_property(TARGET realm PROPERTY IMPORTED_LOCATION_DEBUG ${core_library_debug}) + set_property(TARGET realm PROPERTY IMPORTED_LOCATION_COVERAGE ${core_library_debug}) set_property(TARGET realm PROPERTY IMPORTED_LOCATION_RELEASE ${core_library_release}) set_property(TARGET realm PROPERTY IMPORTED_LOCATION ${core_library_release}) diff --git a/CMakeLists.txt b/CMakeLists.txt index b4c55f65..286e6ff0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ project(realm-object-store) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake") +include(CodeCoverage) include(CompilerFlags) include(Sanitizers) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2e1bd2b..8f849049 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,5 @@ set(SOURCES + collection_notifications.cpp index_set.cpp list.cpp object_schema.cpp @@ -6,24 +7,34 @@ set(SOURCES results.cpp schema.cpp shared_realm.cpp - impl/async_query.cpp + impl/collection_change_builder.cpp + impl/collection_notifier.cpp + impl/list_notifier.cpp impl/realm_coordinator.cpp + impl/results_notifier.cpp impl/transact_log_handler.cpp parser/parser.cpp parser/query_builder.cpp) set(HEADERS + collection_notifications.hpp index_set.hpp list.hpp object_schema.hpp object_store.hpp + property.hpp results.hpp schema.hpp shared_realm.hpp + impl/collection_change_builder.hpp + impl/collection_notifier.hpp + impl/external_commit_helper.hpp + impl/list_notifier.hpp + impl/realm_coordinator.hpp + impl/results_notifier.hpp + impl/transact_log_handler.hpp impl/weak_realm_notifier.hpp impl/weak_realm_notifier_base.hpp - impl/external_commit_helper.hpp - impl/transact_log_handler.hpp parser/parser.hpp parser/query_builder.hpp util/atomic_shared_ptr.hpp) diff --git a/src/collection_notifications.cpp b/src/collection_notifications.cpp new file mode 100644 index 00000000..83dc89e1 --- /dev/null +++ b/src/collection_notifications.cpp @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include "collection_notifications.hpp" + +#include "impl/collection_notifier.hpp" + +using namespace realm; +using namespace realm::_impl; + +NotificationToken::NotificationToken(std::shared_ptr<_impl::CollectionNotifier> notifier, size_t token) +: m_notifier(std::move(notifier)), m_token(token) +{ +} + +NotificationToken::~NotificationToken() +{ + // m_notifier itself (and not just the pointed-to thing) needs to be accessed + // atomically to ensure that there are no data races when the token is + // destroyed after being modified on a different thread. + // This is needed despite the token not being thread-safe in general as + // users find it very surprising for obj-c objects to care about what + // thread they are deallocated on. + if (auto notifier = m_notifier.exchange({})) { + notifier->remove_callback(m_token); + } +} + +NotificationToken::NotificationToken(NotificationToken&& rgt) = default; + +NotificationToken& NotificationToken::operator=(realm::NotificationToken&& rgt) +{ + if (this != &rgt) { + if (auto notifier = m_notifier.exchange({})) { + notifier->remove_callback(m_token); + } + m_notifier = std::move(rgt.m_notifier); + m_token = rgt.m_token; + } + return *this; +} diff --git a/src/collection_notifications.hpp b/src/collection_notifications.hpp new file mode 100644 index 00000000..ed75c315 --- /dev/null +++ b/src/collection_notifications.hpp @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_COLLECTION_NOTIFICATIONS_HPP +#define REALM_COLLECTION_NOTIFICATIONS_HPP + +#include "index_set.hpp" +#include "util/atomic_shared_ptr.hpp" + +#include +#include +#include +#include + +namespace realm { +namespace _impl { + class CollectionNotifier; +} + +// A token which keeps an asynchronous query alive +struct NotificationToken { + NotificationToken() = default; + NotificationToken(std::shared_ptr<_impl::CollectionNotifier> notifier, size_t token); + ~NotificationToken(); + + NotificationToken(NotificationToken&&); + NotificationToken& operator=(NotificationToken&&); + + NotificationToken(NotificationToken const&) = delete; + NotificationToken& operator=(NotificationToken const&) = delete; + +private: + util::AtomicSharedPtr<_impl::CollectionNotifier> m_notifier; + size_t m_token; +}; + +struct CollectionChangeSet { + struct Move { + size_t from; + size_t to; + + bool operator==(Move m) const { return from == m.from && to == m.to; } + }; + + IndexSet deletions; + IndexSet insertions; + IndexSet modifications; + std::vector moves; + + bool empty() const { return deletions.empty() && insertions.empty() && modifications.empty() && moves.empty(); } +}; + +using CollectionChangeCallback = std::function; +} // namespace realm + +#endif // REALM_COLLECTION_NOTIFICATIONS_HPP diff --git a/src/impl/apple/external_commit_helper.cpp b/src/impl/apple/external_commit_helper.cpp index db04a1de..5195a3e3 100644 --- a/src/impl/apple/external_commit_helper.cpp +++ b/src/impl/apple/external_commit_helper.cpp @@ -35,7 +35,7 @@ using namespace realm::_impl; namespace { // Write a byte to a pipe to notify anyone waiting for data on the pipe -void notify_fd(int fd) +void notify_fd(int fd, int read_fd) { while (true) { char c = 0; @@ -50,7 +50,7 @@ void notify_fd(int fd) // write. assert(ret == -1 && errno == EAGAIN); char buff[1024]; - read(fd, buff, sizeof buff); + read(read_fd, buff, sizeof buff); } } } // anonymous namespace @@ -94,6 +94,7 @@ ExternalCommitHelper::ExternalCommitHelper(RealmCoordinator& parent) throw std::system_error(errno, std::system_category()); } +#if !TARGET_OS_TV auto path = parent.get_path() + ".note"; // Create and open the named pipe @@ -129,15 +130,29 @@ ExternalCommitHelper::ExternalCommitHelper(RealmCoordinator& parent) throw std::system_error(errno, std::system_category()); } - // Create the anonymous pipe - int pipeFd[2]; - ret = pipe(pipeFd); +#else // !TARGET_OS_TV + + // tvOS does not support named pipes, so use an anonymous pipe instead + int notification_pipe[2]; + int ret = pipe(notification_pipe); if (ret == -1) { throw std::system_error(errno, std::system_category()); } - m_shutdown_read_fd = pipeFd[0]; - m_shutdown_write_fd = pipeFd[1]; + m_notify_fd = notification_pipe[0]; + m_notify_fd_write = notification_pipe[1]; + +#endif // TARGET_OS_TV + + // Create the anonymous pipe for shutdown notifications + int shutdown_pipe[2]; + ret = pipe(shutdown_pipe); + if (ret == -1) { + throw std::system_error(errno, std::system_category()); + } + + m_shutdown_read_fd = shutdown_pipe[0]; + m_shutdown_write_fd = shutdown_pipe[1]; m_thread = std::async(std::launch::async, [=] { try { @@ -158,7 +173,7 @@ ExternalCommitHelper::ExternalCommitHelper(RealmCoordinator& parent) ExternalCommitHelper::~ExternalCommitHelper() { - notify_fd(m_shutdown_write_fd); + notify_fd(m_shutdown_write_fd, m_shutdown_read_fd); m_thread.wait(); // Wait for the thread to exit } @@ -202,5 +217,10 @@ void ExternalCommitHelper::listen() void ExternalCommitHelper::notify_others() { - notify_fd(m_notify_fd); + if (m_notify_fd_write != -1) { + notify_fd(m_notify_fd_write, m_notify_fd); + } + else { + notify_fd(m_notify_fd, m_notify_fd); + } } diff --git a/src/impl/apple/external_commit_helper.hpp b/src/impl/apple/external_commit_helper.hpp index a39876ce..c87d8b24 100644 --- a/src/impl/apple/external_commit_helper.hpp +++ b/src/impl/apple/external_commit_helper.hpp @@ -61,16 +61,20 @@ private: // The listener thread std::future m_thread; - // Read-write file descriptor for the named pipe which is waited on for - // changes and written to when a commit is made + // Pipe which is waited on for changes and written to when there is a new + // commit to notify others of. When using a named pipe m_notify_fd is + // read-write and m_notify_fd_write is unused; when using an anonymous pipe + // (on tvOS) m_notify_fd is read-only and m_notify_fd_write is write-only. FdHolder m_notify_fd; + FdHolder m_notify_fd_write; + // File descriptor for the kqueue FdHolder m_kq; + // The two ends of an anonymous pipe used to notify the kqueue() thread that // it should be shut down. FdHolder m_shutdown_read_fd; FdHolder m_shutdown_write_fd; }; - } // namespace _impl } // namespace realm diff --git a/src/impl/async_query.cpp b/src/impl/async_query.cpp deleted file mode 100644 index ecee53c8..00000000 --- a/src/impl/async_query.cpp +++ /dev/null @@ -1,290 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2015 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#include "impl/async_query.hpp" - -#include "impl/realm_coordinator.hpp" -#include "results.hpp" - -using namespace realm; -using namespace realm::_impl; - -AsyncQuery::AsyncQuery(Results& target) -: m_target_results(&target) -, m_realm(target.get_realm()) -, m_sort(target.get_sort()) -, m_sg_version(Realm::Internal::get_shared_group(*m_realm).get_version_of_current_transaction()) -{ - Query q = target.get_query(); - m_query_handover = Realm::Internal::get_shared_group(*m_realm).export_for_handover(q, MutableSourcePayload::Move); -} - -AsyncQuery::~AsyncQuery() -{ - // unregister() may have been called from a different thread than we're being - // destroyed on, so we need to synchronize access to the interesting fields - // modified there - std::lock_guard lock(m_target_mutex); - m_realm = nullptr; -} - -size_t AsyncQuery::add_callback(std::function callback) -{ - m_realm->verify_thread(); - - auto next_token = [=] { - size_t token = 0; - for (auto& callback : m_callbacks) { - if (token <= callback.token) { - token = callback.token + 1; - } - } - return token; - }; - - std::lock_guard lock(m_callback_mutex); - auto token = next_token(); - m_callbacks.push_back({std::move(callback), token, -1ULL}); - if (m_callback_index == npos) { // Don't need to wake up if we're already sending notifications - Realm::Internal::get_coordinator(*m_realm).send_commit_notifications(); - m_have_callbacks = true; - } - return token; -} - -void AsyncQuery::remove_callback(size_t token) -{ - Callback old; - { - std::lock_guard lock(m_callback_mutex); - REALM_ASSERT(m_error || m_callbacks.size() > 0); - - auto it = find_if(begin(m_callbacks), end(m_callbacks), - [=](const auto& c) { return c.token == token; }); - // We should only fail to find the callback if it was removed due to an error - REALM_ASSERT(m_error || it != end(m_callbacks)); - if (it == end(m_callbacks)) { - return; - } - - size_t idx = distance(begin(m_callbacks), it); - if (m_callback_index != npos && m_callback_index >= idx) { - --m_callback_index; - } - - old = std::move(*it); - m_callbacks.erase(it); - - m_have_callbacks = !m_callbacks.empty(); - } -} - -void AsyncQuery::unregister() noexcept -{ - std::lock_guard lock(m_target_mutex); - m_target_results = nullptr; - m_realm = nullptr; -} - -void AsyncQuery::release_query() noexcept -{ - { - std::lock_guard lock(m_target_mutex); - REALM_ASSERT(!m_realm && !m_target_results); - } - - m_query = nullptr; -} - -bool AsyncQuery::is_alive() const noexcept -{ - std::lock_guard lock(m_target_mutex); - return m_target_results != nullptr; -} - -// Most of the inter-thread synchronization for run(), prepare_handover(), -// attach_to(), detach(), release_query() and deliver() is done by -// RealmCoordinator external to this code, which has some potentially -// non-obvious results on which members are and are not safe to use without -// holding a lock. -// -// attach_to(), detach(), run(), prepare_handover(), and release_query() are -// all only ever called on a single thread. call_callbacks() and deliver() are -// called on the same thread. Calls to prepare_handover() and deliver() are -// guarded by a lock. -// -// In total, this means that the safe data flow is as follows: -// - prepare_handover(), attach_to(), detach() and release_query() can read -// members written by each other -// - deliver() can read members written to in prepare_handover(), deliver(), -// and call_callbacks() -// - call_callbacks() and read members written to in deliver() -// -// Separately from this data flow for the query results, all uses of -// m_target_results, m_callbacks, and m_callback_index must be done with the -// appropriate mutex held to avoid race conditions when the Results object is -// destroyed while the background work is running, and to allow removing -// callbacks from any thread. - -void AsyncQuery::run() -{ - REALM_ASSERT(m_sg); - - { - std::lock_guard target_lock(m_target_mutex); - // Don't run the query if the results aren't actually going to be used - if (!m_target_results || (!m_have_callbacks && !m_target_results->wants_background_updates())) { - return; - } - } - - REALM_ASSERT(!m_tv.is_attached()); - - // If we've run previously, check if we need to rerun - if (m_initial_run_complete) { - // Make an empty tableview from the query to get the table version, since - // Query doesn't expose it - if (m_query->find_all(0, 0, 0).sync_if_needed() == m_handed_over_table_version) { - return; - } - } - - m_tv = m_query->find_all(); - if (m_sort) { - m_tv.sort(m_sort.columnIndices, m_sort.ascending); - } -} - -void AsyncQuery::prepare_handover() -{ - m_sg_version = m_sg->get_version_of_current_transaction(); - - if (!m_tv.is_attached()) { - return; - } - - REALM_ASSERT(m_tv.is_in_sync()); - - m_initial_run_complete = true; - m_handed_over_table_version = m_tv.sync_if_needed(); - m_tv_handover = m_sg->export_for_handover(m_tv, MutableSourcePayload::Move); - - // detach the TableView as we won't need it again and keeping it around - // makes advance_read() much more expensive - m_tv = TableView(); -} - -bool AsyncQuery::deliver(SharedGroup& sg, std::exception_ptr err) -{ - if (!is_for_current_thread()) { - return false; - } - - std::lock_guard target_lock(m_target_mutex); - - // Target results being null here indicates that it was destroyed while we - // were in the process of advancing the Realm version and preparing for - // delivery, i.e. it was destroyed from the "wrong" thread - if (!m_target_results) { - return false; - } - - // We can get called before the query has actually had the chance to run if - // we're added immediately before a different set of async results are - // delivered - if (!m_initial_run_complete && !err) { - return false; - } - - if (err) { - m_error = err; - return m_have_callbacks; - } - - REALM_ASSERT(!m_query_handover); - - auto realm_sg_version = Realm::Internal::get_shared_group(*m_realm).get_version_of_current_transaction(); - if (m_sg_version != realm_sg_version) { - // Realm version can be newer if a commit was made on our thread or the - // user manually called refresh(), or older if a commit was made on a - // different thread and we ran *really* fast in between the check for - // if the shared group has changed and when we pick up async results - return false; - } - - if (m_tv_handover) { - m_tv_handover->version = m_sg_version; - Results::Internal::set_table_view(*m_target_results, - std::move(*sg.import_from_handover(std::move(m_tv_handover)))); - m_delivered_table_version = m_handed_over_table_version; - - } - REALM_ASSERT(!m_tv_handover); - return m_have_callbacks; -} - -void AsyncQuery::call_callbacks() -{ - REALM_ASSERT(is_for_current_thread()); - - while (auto fn = next_callback()) { - fn(m_error); - } - - if (m_error) { - // Remove all the callbacks as we never need to call anything ever again - // after delivering an error - std::lock_guard callback_lock(m_callback_mutex); - m_callbacks.clear(); - } -} - -std::function AsyncQuery::next_callback() -{ - std::lock_guard callback_lock(m_callback_mutex); - for (++m_callback_index; m_callback_index < m_callbacks.size(); ++m_callback_index) { - auto& callback = m_callbacks[m_callback_index]; - if (m_error || callback.delivered_version != m_delivered_table_version) { - callback.delivered_version = m_delivered_table_version; - return callback.fn; - } - } - - m_callback_index = npos; - return nullptr; -} - -void AsyncQuery::attach_to(realm::SharedGroup& sg) -{ - REALM_ASSERT(!m_sg); - REALM_ASSERT(m_query_handover); - - m_query = sg.import_from_handover(std::move(m_query_handover)); - m_sg = &sg; -} - -void AsyncQuery::detatch() -{ - REALM_ASSERT(m_sg); - REALM_ASSERT(m_query); - REALM_ASSERT(!m_tv.is_attached()); - - m_query_handover = m_sg->export_for_handover(*m_query, MutableSourcePayload::Move); - m_sg = nullptr; - m_query = nullptr; -} diff --git a/src/impl/async_query.hpp b/src/impl/async_query.hpp deleted file mode 100644 index cf70b463..00000000 --- a/src/impl/async_query.hpp +++ /dev/null @@ -1,122 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2015 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#ifndef REALM_ASYNC_QUERY_HPP -#define REALM_ASYNC_QUERY_HPP - -#include "results.hpp" - -#include - -#include -#include -#include -#include -#include - -namespace realm { -namespace _impl { -class AsyncQuery { -public: - AsyncQuery(Results& target); - ~AsyncQuery(); - - size_t add_callback(std::function); - void remove_callback(size_t token); - - void unregister() noexcept; - void release_query() noexcept; - - // Run/rerun the query if needed - void run(); - // Prepare the handover object if run() did update the TableView - void prepare_handover(); - // Update the target results from the handover - // Returns if any callbacks need to be invoked - bool deliver(SharedGroup& sg, std::exception_ptr err); - void call_callbacks(); - - // Attach the handed-over query to `sg` - void attach_to(SharedGroup& sg); - // Create a new query handover object and stop using the previously attached - // SharedGroup - void detatch(); - - Realm& get_realm() { return *m_target_results->get_realm(); } - // Get the version of the current handover object - SharedGroup::VersionID version() const noexcept { return m_sg_version; } - - bool is_alive() const noexcept; - -private: - // Target Results to update and a mutex which guards it - mutable std::mutex m_target_mutex; - Results* m_target_results; - - std::shared_ptr m_realm; - const SortOrder m_sort; - const std::thread::id m_thread_id = std::this_thread::get_id(); - - // The source Query, in handover form iff m_sg is null - std::unique_ptr> m_query_handover; - std::unique_ptr m_query; - - // The TableView resulting from running the query. Will be detached unless - // the query was (re)run since the last time the handover object was created - TableView m_tv; - std::unique_ptr> m_tv_handover; - SharedGroup::VersionID m_sg_version; - std::exception_ptr m_error; - - struct Callback { - std::function fn; - size_t token; - uint_fast64_t delivered_version; - }; - - // Currently registered callbacks and a mutex which must always be held - // while doing anything with them or m_callback_index - std::mutex m_callback_mutex; - std::vector m_callbacks; - - SharedGroup* m_sg = nullptr; - - uint_fast64_t m_handed_over_table_version = -1; - uint_fast64_t m_delivered_table_version = -1; - - // Iteration variable for looping over callbacks - // remove_callback() updates this when needed - size_t m_callback_index = npos; - - bool m_initial_run_complete = false; - - // Cached value for if m_callbacks is empty, needed to avoid deadlocks in - // run() due to lock-order inversion between m_callback_mutex and m_target_mutex - // It's okay if this value is stale as at worst it'll result in us doing - // some extra work. - std::atomic m_have_callbacks = {false}; - - bool is_for_current_thread() const { return m_thread_id == std::this_thread::get_id(); } - - std::function next_callback(); -}; - -} // namespace _impl -} // namespace realm - -#endif /* REALM_ASYNC_QUERY_HPP */ diff --git a/src/impl/collection_change_builder.cpp b/src/impl/collection_change_builder.cpp new file mode 100644 index 00000000..da0ff5ab --- /dev/null +++ b/src/impl/collection_change_builder.cpp @@ -0,0 +1,659 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include "impl/collection_change_builder.hpp" + +#include + +using namespace realm; +using namespace realm::_impl; + +CollectionChangeBuilder::CollectionChangeBuilder(IndexSet deletions, + IndexSet insertions, + IndexSet modifications, + std::vector moves) +: CollectionChangeSet({std::move(deletions), std::move(insertions), std::move(modifications), std::move(moves)}) +{ + for (auto&& move : this->moves) { + this->deletions.add(move.from); + this->insertions.add(move.to); + } +} + +void CollectionChangeBuilder::merge(CollectionChangeBuilder&& c) +{ + if (c.empty()) + return; + if (empty()) { + *this = std::move(c); + return; + } + + verify(); + c.verify(); + + // First update any old moves + if (!c.moves.empty() || !c.deletions.empty() || !c.insertions.empty()) { + auto it = remove_if(begin(moves), end(moves), [&](auto& old) { + // Check if the moved row was moved again, and if so just update the destination + auto it = find_if(begin(c.moves), end(c.moves), [&](auto const& m) { + return old.to == m.from; + }); + if (it != c.moves.end()) { + if (modifications.contains(it->from)) + c.modifications.add(it->to); + old.to = it->to; + *it = c.moves.back(); + c.moves.pop_back(); + ++it; + return false; + } + + // Check if the destination was deleted + // Removing the insert for this move will happen later + if (c.deletions.contains(old.to)) + return true; + + // Update the destination to adjust for any new insertions and deletions + old.to = c.insertions.shift(c.deletions.unshift(old.to)); + return false; + }); + moves.erase(it, end(moves)); + } + + // Ignore new moves of rows which were previously inserted (the implicit + // delete from the move will remove the insert) + if (!insertions.empty() && !c.moves.empty()) { + c.moves.erase(remove_if(begin(c.moves), end(c.moves), + [&](auto const& m) { return insertions.contains(m.from); }), + end(c.moves)); + } + + // Ensure that any previously modified rows which were moved are still modified + if (!modifications.empty() && !c.moves.empty()) { + for (auto const& move : c.moves) { + if (modifications.contains(move.from)) + c.modifications.add(move.to); + } + } + + // Update the source position of new moves to compensate for the changes made + // in the old changeset + if (!deletions.empty() || !insertions.empty()) { + for (auto& move : c.moves) + move.from = deletions.shift(insertions.unshift(move.from)); + } + + moves.insert(end(moves), begin(c.moves), end(c.moves)); + + // New deletion indices have been shifted by the insertions, so unshift them + // before adding + deletions.add_shifted_by(insertions, c.deletions); + + // Drop any inserted-then-deleted rows, then merge in new insertions + insertions.erase_at(c.deletions); + insertions.insert_at(c.insertions); + + clean_up_stale_moves(); + + modifications.erase_at(c.deletions); + modifications.shift_for_insert_at(c.insertions); + modifications.add(c.modifications); + + c = {}; + verify(); +} + +void CollectionChangeBuilder::clean_up_stale_moves() +{ + // Look for moves which are now no-ops, and remove them plus the associated + // insert+delete. Note that this isn't just checking for from == to due to + // that rows can also be shifted by other inserts and deletes + moves.erase(remove_if(begin(moves), end(moves), [&](auto const& move) { + if (move.from - deletions.count(0, move.from) != move.to - insertions.count(0, move.to)) + return false; + deletions.remove(move.from); + insertions.remove(move.to); + return true; + }), end(moves)); +} + +void CollectionChangeBuilder::parse_complete() +{ + moves.reserve(m_move_mapping.size()); + for (auto move : m_move_mapping) { + REALM_ASSERT_DEBUG(deletions.contains(move.second)); + REALM_ASSERT_DEBUG(insertions.contains(move.first)); + moves.push_back({move.second, move.first}); + } + m_move_mapping.clear(); + std::sort(begin(moves), end(moves), + [](auto const& a, auto const& b) { return a.from < b.from; }); +} + +void CollectionChangeBuilder::modify(size_t ndx) +{ + modifications.add(ndx); +} + +void CollectionChangeBuilder::insert(size_t index, size_t count, bool track_moves) +{ + modifications.shift_for_insert_at(index, count); + if (!track_moves) + return; + + insertions.insert_at(index, count); + + for (auto& move : moves) { + if (move.to >= index) + ++move.to; + } +} + +void CollectionChangeBuilder::erase(size_t index) +{ + modifications.erase_at(index); + size_t unshifted = insertions.erase_or_unshift(index); + if (unshifted != IndexSet::npos) + deletions.add_shifted(unshifted); + + for (size_t i = 0; i < moves.size(); ++i) { + auto& move = moves[i]; + if (move.to == index) { + moves.erase(moves.begin() + i); + --i; + } + else if (move.to > index) + --move.to; + } +} + +void CollectionChangeBuilder::clear(size_t old_size) +{ + if (old_size != std::numeric_limits::max()) { + for (auto range : deletions) + old_size += range.second - range.first; + for (auto range : insertions) + old_size -= range.second - range.first; + } + + modifications.clear(); + insertions.clear(); + moves.clear(); + m_move_mapping.clear(); + deletions.set(old_size); +} + +void CollectionChangeBuilder::move(size_t from, size_t to) +{ + REALM_ASSERT(from != to); + + bool updated_existing_move = false; + for (auto& move : moves) { + if (move.to != from) { + // Shift other moves if this row is moving from one side of them + // to the other + if (move.to >= to && move.to < from) + ++move.to; + else if (move.to <= to && move.to > from) + --move.to; + continue; + } + REALM_ASSERT(!updated_existing_move); + + // Collapse A -> B, B -> C into a single A -> C move + move.to = to; + updated_existing_move = true; + + insertions.erase_at(from); + insertions.insert_at(to); + } + + if (!updated_existing_move) { + auto shifted_from = insertions.erase_or_unshift(from); + insertions.insert_at(to); + + // Don't report deletions/moves for newly inserted rows + if (shifted_from != IndexSet::npos) { + shifted_from = deletions.add_shifted(shifted_from); + moves.push_back({shifted_from, to}); + } + } + + bool modified = modifications.contains(from); + modifications.erase_at(from); + + if (modified) + modifications.insert_at(to); + else + modifications.shift_for_insert_at(to); +} + +void CollectionChangeBuilder::move_over(size_t row_ndx, size_t last_row, bool track_moves) +{ + REALM_ASSERT(row_ndx <= last_row); + REALM_ASSERT(insertions.empty() || prev(insertions.end())->second - 1 <= last_row); + REALM_ASSERT(modifications.empty() || prev(modifications.end())->second - 1 <= last_row); + + if (row_ndx == last_row) { + if (track_moves) { + auto shifted_from = insertions.erase_or_unshift(row_ndx); + if (shifted_from != IndexSet::npos) + deletions.add_shifted(shifted_from); + m_move_mapping.erase(row_ndx); + } + modifications.remove(row_ndx); + return; + } + + bool modified = modifications.contains(last_row); + if (modified) { + modifications.remove(last_row); + modifications.add(row_ndx); + } + else + modifications.remove(row_ndx); + + if (!track_moves) + return; + + bool row_is_insertion = insertions.contains(row_ndx); + bool last_is_insertion = !insertions.empty() && prev(insertions.end())->second == last_row + 1; + REALM_ASSERT_DEBUG(insertions.empty() || prev(insertions.end())->second <= last_row + 1); + + // Collapse A -> B, B -> C into a single A -> C move + bool last_was_already_moved = false; + if (last_is_insertion) { + auto it = m_move_mapping.find(last_row); + if (it != m_move_mapping.end() && it->first == last_row) { + m_move_mapping[row_ndx] = it->second; + m_move_mapping.erase(it); + last_was_already_moved = true; + } + } + + // Remove moves to the row being deleted + if (row_is_insertion && !last_was_already_moved) { + auto it = m_move_mapping.find(row_ndx); + if (it != m_move_mapping.end() && it->first == row_ndx) + m_move_mapping.erase(it); + } + + // Don't report deletions/moves if last_row is newly inserted + if (last_is_insertion) { + insertions.remove(last_row); + } + // If it was previously moved, the unshifted source row has already been marked as deleted + else if (!last_was_already_moved) { + auto shifted_last_row = insertions.unshift(last_row); + shifted_last_row = deletions.add_shifted(shifted_last_row); + m_move_mapping[row_ndx] = shifted_last_row; + } + + // Don't mark the moved-over row as deleted if it was a new insertion + if (!row_is_insertion) { + deletions.add_shifted(insertions.unshift(row_ndx)); + insertions.add(row_ndx); + } + verify(); +} + +void CollectionChangeBuilder::verify() +{ +#ifdef REALM_DEBUG + for (auto&& move : moves) { + REALM_ASSERT(deletions.contains(move.from)); + REALM_ASSERT(insertions.contains(move.to)); + } +#endif +} + +namespace { +struct RowInfo { + size_t row_index; + size_t prev_tv_index; + size_t tv_index; + size_t shifted_tv_index; +}; + +void calculate_moves_unsorted(std::vector& new_rows, IndexSet& removed, CollectionChangeSet& changeset) +{ + size_t expected = 0; + for (auto& row : new_rows) { + // With unsorted queries rows only move due to move_last_over(), which + // inherently can only move a row to earlier in the table. + REALM_ASSERT(row.shifted_tv_index >= expected); + if (row.shifted_tv_index == expected) { + ++expected; + continue; + } + + // This row isn't just the row after the previous one, but it still may + // not be a move if there were rows deleted between the two, so next + // calcuate what row should be here taking those in to account + size_t calc_expected = row.tv_index - changeset.insertions.count(0, row.tv_index) + removed.count(0, row.prev_tv_index); + if (row.shifted_tv_index == calc_expected) { + expected = calc_expected + 1; + continue; + } + + // The row still isn't the expected one, so it's a move + changeset.moves.push_back({row.prev_tv_index, row.tv_index}); + changeset.insertions.add(row.tv_index); + removed.add(row.prev_tv_index); + } +} + +class LongestCommonSubsequenceCalculator { +public: + // A pair of an index in the table and an index in the table view + struct Row { + size_t row_index; + size_t tv_index; + }; + + struct Match { + // The index in `a` at which this match begins + size_t i; + // The index in `b` at which this match begins + size_t j; + // The length of this match + size_t size; + // The number of rows in this block which were modified + size_t modified; + }; + std::vector m_longest_matches; + + LongestCommonSubsequenceCalculator(std::vector& a, std::vector& b, + size_t start_index, + IndexSet const& modifications) + : m_modified(modifications) + , a(a), b(b) + { + find_longest_matches(start_index, a.size(), + start_index, b.size()); + m_longest_matches.push_back({a.size(), b.size(), 0}); + } + +private: + IndexSet const& m_modified; + + // The two arrays of rows being diffed + // a is sorted by tv_index, b is sorted by row_index + std::vector &a, &b; + + // Find the longest matching range in (a + begin1, a + end1) and (b + begin2, b + end2) + // "Matching" is defined as "has the same row index"; the TV index is just + // there to let us turn an index in a/b into an index which can be reported + // in the output changeset. + // + // This is done with the O(N) space variant of the dynamic programming + // algorithm for longest common subsequence, where N is the maximum number + // of the most common row index (which for everything but linkview-derived + // TVs will be 1). + Match find_longest_match(size_t begin1, size_t end1, size_t begin2, size_t end2) + { + struct Length { + size_t j, len; + }; + // The length of the matching block for each `j` for the previously checked row + std::vector prev; + // The length of the matching block for each `j` for the row currently being checked + std::vector cur; + + // Calculate the length of the matching block *ending* at b[j], which + // is 1 if b[j - 1] did not match, and b[j - 1] + 1 otherwise. + auto length = [&](size_t j) -> size_t { + for (auto const& pair : prev) { + if (pair.j + 1 == j) + return pair.len + 1; + } + return 1; + }; + + // Iterate over each `j` which has the same row index as a[i] and falls + // within the range begin2 <= j < end2 + auto for_each_b_match = [&](size_t i, auto&& f) { + size_t ai = a[i].row_index; + // Find the TV indicies at which this row appears in the new results + // There should always be at least one (or it would have been + // filtered out earlier), but there can be multiple if there are dupes + auto it = lower_bound(begin(b), end(b), ai, + [](auto lft, auto rgt) { return lft.row_index < rgt; }); + REALM_ASSERT(it != end(b) && it->row_index == ai); + for (; it != end(b) && it->row_index == ai; ++it) { + size_t j = it->tv_index; + if (j < begin2) + continue; + if (j >= end2) + break; // b is sorted by tv_index so this can't transition from false to true + f(j); + } + }; + + Match best = {begin1, begin2, 0, 0}; + for (size_t i = begin1; i < end1; ++i) { + // prev = std::move(cur), but avoids discarding prev's heap allocation + cur.swap(prev); + cur.clear(); + + for_each_b_match(i, [&](size_t j) { + size_t size = length(j); + + cur.push_back({j, size}); + + // If the matching block ending at a[i] and b[j] is longer than + // the previous one, select it as the best + if (size > best.size) + best = {i - size + 1, j - size + 1, size, IndexSet::npos}; + // Given two equal-length matches, prefer the one with fewer modified rows + else if (size == best.size) { + if (best.modified == IndexSet::npos) + best.modified = m_modified.count(best.j - size + 1, best.j + 1); + auto count = m_modified.count(j - size + 1, j + 1); + if (count < best.modified) + best = {i - size + 1, j - size + 1, size, count}; + } + + // The best block should always fall within the range being searched + REALM_ASSERT(best.i >= begin1 && best.i + best.size <= end1); + REALM_ASSERT(best.j >= begin2 && best.j + best.size <= end2); + }); + } + return best; + } + + void find_longest_matches(size_t begin1, size_t end1, size_t begin2, size_t end2) + { + // FIXME: recursion could get too deep here + // recursion depth worst case is currently O(N) and each recursion uses 320 bytes of stack + // could reduce worst case to O(sqrt(N)) (and typical case to O(log N)) + // biasing equal selections towards the middle, but that's still + // insufficient for Android's 8 KB stacks + auto m = find_longest_match(begin1, end1, begin2, end2); + if (!m.size) + return; + if (m.i > begin1 && m.j > begin2) + find_longest_matches(begin1, m.i, begin2, m.j); + m_longest_matches.push_back(m); + if (m.i + m.size < end2 && m.j + m.size < end2) + find_longest_matches(m.i + m.size, end1, m.j + m.size, end2); + } +}; + +void calculate_moves_sorted(std::vector& rows, CollectionChangeSet& changeset) +{ + // The RowInfo array contains information about the old and new TV indices of + // each row, which we need to turn into two sequences of rows, which we'll + // then find matches in + std::vector a, b; + + a.reserve(rows.size()); + for (auto& row : rows) { + a.push_back({row.row_index, row.prev_tv_index}); + } + std::sort(begin(a), end(a), [](auto lft, auto rgt) { + return std::tie(lft.tv_index, lft.row_index) < std::tie(rgt.tv_index, rgt.row_index); + }); + + // Before constructing `b`, first find the first index in `a` which will + // actually differ in `b`, and skip everything else if there aren't any + size_t first_difference = IndexSet::npos; + for (size_t i = 0; i < a.size(); ++i) { + if (a[i].row_index != rows[i].row_index) { + first_difference = i; + break; + } + } + if (first_difference == IndexSet::npos) + return; + + // Note that `b` is sorted by row_index, while `a` is sorted by tv_index + b.reserve(rows.size()); + for (size_t i = 0; i < rows.size(); ++i) + b.push_back({rows[i].row_index, i}); + std::sort(begin(b), end(b), [](auto lft, auto rgt) { + return std::tie(lft.row_index, lft.tv_index) < std::tie(rgt.row_index, rgt.tv_index); + }); + + // Calculate the LCS of the two sequences + auto matches = LongestCommonSubsequenceCalculator(a, b, first_difference, + changeset.modifications).m_longest_matches; + + // And then insert and delete rows as needed to align them + size_t i = first_difference, j = first_difference; + for (auto match : matches) { + for (; i < match.i; ++i) + changeset.deletions.add(a[i].tv_index); + for (; j < match.j; ++j) + changeset.insertions.add(rows[j].tv_index); + i += match.size; + j += match.size; + } +} + +} // Anonymous namespace + +CollectionChangeBuilder CollectionChangeBuilder::calculate(std::vector const& prev_rows, + std::vector const& next_rows, + std::function row_did_change, + bool rows_are_in_table_order) +{ + REALM_ASSERT_DEBUG(!rows_are_in_table_order || std::is_sorted(begin(next_rows), end(next_rows))); + + CollectionChangeBuilder ret; + + size_t deleted = 0; + std::vector old_rows; + old_rows.reserve(prev_rows.size()); + for (size_t i = 0; i < prev_rows.size(); ++i) { + if (prev_rows[i] == IndexSet::npos) { + ++deleted; + ret.deletions.add(i); + } + else + old_rows.push_back({prev_rows[i], IndexSet::npos, i, i - deleted}); + } + std::sort(begin(old_rows), end(old_rows), [](auto& lft, auto& rgt) { + return lft.row_index < rgt.row_index; + }); + + std::vector new_rows; + new_rows.reserve(next_rows.size()); + for (size_t i = 0; i < next_rows.size(); ++i) { + new_rows.push_back({next_rows[i], IndexSet::npos, i, 0}); + } + std::sort(begin(new_rows), end(new_rows), [](auto& lft, auto& rgt) { + return lft.row_index < rgt.row_index; + }); + + // Don't add rows which were modified to not match the query to `deletions` + // immediately because the unsorted move logic needs to be able to + // distinguish them from rows which were outright deleted + IndexSet removed; + + // Now that our old and new sets of rows are sorted by row index, we can + // iterate over them and either record old+new TV indices for rows present + // in both, or mark them as inserted/deleted if they appear only in one + size_t i = 0, j = 0; + while (i < old_rows.size() && j < new_rows.size()) { + auto old_index = old_rows[i]; + auto new_index = new_rows[j]; + if (old_index.row_index == new_index.row_index) { + new_rows[j].prev_tv_index = old_rows[i].tv_index; + new_rows[j].shifted_tv_index = old_rows[i].shifted_tv_index; + ++i; + ++j; + } + else if (old_index.row_index < new_index.row_index) { + removed.add(old_index.tv_index); + ++i; + } + else { + ret.insertions.add(new_index.tv_index); + ++j; + } + } + + for (; i < old_rows.size(); ++i) + removed.add(old_rows[i].tv_index); + for (; j < new_rows.size(); ++j) + ret.insertions.add(new_rows[j].tv_index); + + // Filter out the new insertions since we don't need them for any of the + // further calculations + new_rows.erase(std::remove_if(begin(new_rows), end(new_rows), + [](auto& row) { return row.prev_tv_index == IndexSet::npos; }), + end(new_rows)); + std::sort(begin(new_rows), end(new_rows), + [](auto& lft, auto& rgt) { return lft.tv_index < rgt.tv_index; }); + + for (auto& row : new_rows) { + if (row_did_change(row.row_index)) { + ret.modifications.add(row.tv_index); + } + } + + if (!rows_are_in_table_order) { + calculate_moves_sorted(new_rows, ret); + } + else { + calculate_moves_unsorted(new_rows, removed, ret); + } + ret.deletions.add(removed); + ret.verify(); + +#ifdef REALM_DEBUG + { // Verify that applying the calculated change to prev_rows actually produces next_rows + auto rows = prev_rows; + auto it = util::make_reverse_iterator(ret.deletions.end()); + auto end = util::make_reverse_iterator(ret.deletions.begin()); + for (; it != end; ++it) { + rows.erase(rows.begin() + it->first, rows.begin() + it->second); + } + + for (auto i : ret.insertions.as_indexes()) { + rows.insert(rows.begin() + i, next_rows[i]); + } + + REALM_ASSERT(rows == next_rows); + } +#endif + + return ret; +} diff --git a/src/impl/collection_change_builder.hpp b/src/impl/collection_change_builder.hpp new file mode 100644 index 00000000..6e9f78c1 --- /dev/null +++ b/src/impl/collection_change_builder.hpp @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_COLLECTION_CHANGE_BUILDER_HPP +#define REALM_COLLECTION_CHANGE_BUILDER_HPP + +#include "collection_notifications.hpp" + +#include + +namespace realm { +namespace _impl { +class CollectionChangeBuilder : public CollectionChangeSet { +public: + CollectionChangeBuilder(CollectionChangeBuilder const&) = default; + CollectionChangeBuilder(CollectionChangeBuilder&&) = default; + CollectionChangeBuilder& operator=(CollectionChangeBuilder const&) = default; + CollectionChangeBuilder& operator=(CollectionChangeBuilder&&) = default; + + CollectionChangeBuilder(IndexSet deletions = {}, + IndexSet insertions = {}, + IndexSet modification = {}, + std::vector moves = {}); + + // Calculate where rows need to be inserted or deleted from old_rows to turn + // it into new_rows, and check all matching rows for modifications + static CollectionChangeBuilder calculate(std::vector const& old_rows, + std::vector const& new_rows, + std::function row_did_change, + bool sort); + + void merge(CollectionChangeBuilder&&); + void clean_up_stale_moves(); + + void insert(size_t ndx, size_t count=1, bool track_moves=true); + void modify(size_t ndx); + void erase(size_t ndx); + void move_over(size_t ndx, size_t last_ndx, bool track_moves=true); + void clear(size_t old_size); + void move(size_t from, size_t to); + + void parse_complete(); + +private: + std::unordered_map m_move_mapping; + + void verify(); +}; +} // namespace _impl +} // namespace realm + +#endif // REALM_COLLECTION_CHANGE_BUILDER_HPP diff --git a/src/impl/collection_notifier.cpp b/src/impl/collection_notifier.cpp new file mode 100644 index 00000000..7751c3ac --- /dev/null +++ b/src/impl/collection_notifier.cpp @@ -0,0 +1,344 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include "impl/collection_notifier.hpp" + +#include "impl/realm_coordinator.hpp" +#include "shared_realm.hpp" + +#include + +using namespace realm; +using namespace realm::_impl; + +std::function +CollectionNotifier::get_modification_checker(TransactionChangeInfo const& info, + Table const& root_table) +{ + // First check if any of the tables accessible from the root table were + // actually modified. This can be false if there were only insertions, or + // deletions which were not linked to by any row in the linking table + auto table_modified = [&](auto& tbl) { + return tbl.table_ndx < info.tables.size() + && !info.tables[tbl.table_ndx].modifications.empty(); + }; + if (!any_of(begin(m_related_tables), end(m_related_tables), table_modified)) { + return [](size_t) { return false; }; + } + + return DeepChangeChecker(info, root_table, m_related_tables); +} + +void DeepChangeChecker::find_related_tables(std::vector& out, Table const& table) +{ + auto table_ndx = table.get_index_in_group(); + if (any_of(begin(out), end(out), [=](auto& tbl) { return tbl.table_ndx == table_ndx; })) + return; + + // We need to add this table to `out` before recurring so that the check + // above works, but we can't store a pointer to the thing being populated + // because the recursive calls may resize `out`, so instead look it up by + // index every time + size_t out_index = out.size(); + out.push_back({table_ndx, {}}); + + for (size_t i = 0, count = table.get_column_count(); i != count; ++i) { + auto type = table.get_column_type(i); + if (type == type_Link || type == type_LinkList) { + out[out_index].links.push_back({i, type == type_LinkList}); + find_related_tables(out, *table.get_link_target(i)); + } + } +} + +DeepChangeChecker::DeepChangeChecker(TransactionChangeInfo const& info, + Table const& root_table, + std::vector const& related_tables) +: m_info(info) +, m_root_table(root_table) +, m_root_table_ndx(root_table.get_index_in_group()) +, m_root_modifications(m_root_table_ndx < info.tables.size() ? &info.tables[m_root_table_ndx].modifications : nullptr) +, m_related_tables(related_tables) +{ +} + +bool DeepChangeChecker::check_outgoing_links(size_t table_ndx, + Table const& table, + size_t row_ndx, size_t depth) +{ + auto it = find_if(begin(m_related_tables), end(m_related_tables), + [&](auto&& tbl) { return tbl.table_ndx == table_ndx; }); + if (it == m_related_tables.end()) + return false; + + // Check if we're already checking if the destination of the link is + // modified, and if not add it to the stack + auto already_checking = [&](size_t col) { + for (auto p = m_current_path.begin(); p < m_current_path.begin() + depth; ++p) { + if (p->table == table_ndx && p->row == row_ndx && p->col == col) + return true; + } + m_current_path[depth] = {table_ndx, row_ndx, col, false}; + return false; + }; + + for (auto const& link : it->links) { + if (already_checking(link.col_ndx)) + continue; + if (!link.is_list) { + if (table.is_null_link(link.col_ndx, row_ndx)) + continue; + auto dst = table.get_link(link.col_ndx, row_ndx); + return check_row(*table.get_link_target(link.col_ndx), dst, depth + 1); + } + + auto& target = *table.get_link_target(link.col_ndx); + auto lvr = table.get_linklist(link.col_ndx, row_ndx); + for (size_t j = 0, size = lvr->size(); j < size; ++j) { + size_t dst = lvr->get(j).get_index(); + if (check_row(target, dst, depth + 1)) + return true; + } + } + + return false; +} + +bool DeepChangeChecker::check_row(Table const& table, size_t idx, size_t depth) +{ + // Arbitrary upper limit on the maximum depth to search + if (depth >= m_current_path.size()) { + // Don't mark any of the intermediate rows checked along the path as + // not modified, as a search starting from them might hit a modification + for (size_t i = 1; i < m_current_path.size(); ++i) + m_current_path[i].depth_exceeded = true; + return false; + } + + size_t table_ndx = table.get_index_in_group(); + if (depth > 0 && table_ndx < m_info.tables.size() && m_info.tables[table_ndx].modifications.contains(idx)) + return true; + + if (m_not_modified.size() <= table_ndx) + m_not_modified.resize(table_ndx + 1); + if (m_not_modified[table_ndx].contains(idx)) + return false; + + bool ret = check_outgoing_links(table_ndx, table, idx, depth); + if (!ret && !m_current_path[depth].depth_exceeded) + m_not_modified[table_ndx].add(idx); + return ret; +} + +bool DeepChangeChecker::operator()(size_t ndx) +{ + if (m_root_modifications && m_root_modifications->contains(ndx)) + return true; + return check_row(m_root_table, ndx, 0); +} + +CollectionNotifier::CollectionNotifier(std::shared_ptr realm) +: m_realm(std::move(realm)) +, m_sg_version(Realm::Internal::get_shared_group(*m_realm).get_version_of_current_transaction()) +{ +} + +CollectionNotifier::~CollectionNotifier() +{ + // Need to do this explicitly to ensure m_realm is destroyed with the mutex + // held to avoid potential double-deletion + unregister(); +} + +size_t CollectionNotifier::add_callback(CollectionChangeCallback callback) +{ + m_realm->verify_thread(); + + auto next_token = [=] { + size_t token = 0; + for (auto& callback : m_callbacks) { + if (token <= callback.token) { + token = callback.token + 1; + } + } + return token; + }; + + std::lock_guard lock(m_callback_mutex); + auto token = next_token(); + m_callbacks.push_back({std::move(callback), token, false}); + if (m_callback_index == npos) { // Don't need to wake up if we're already sending notifications + Realm::Internal::get_coordinator(*m_realm).send_commit_notifications(); + m_have_callbacks = true; + } + return token; +} + +void CollectionNotifier::remove_callback(size_t token) +{ + Callback old; + { + std::lock_guard lock(m_callback_mutex); + REALM_ASSERT(m_error || m_callbacks.size() > 0); + + auto it = find_if(begin(m_callbacks), end(m_callbacks), + [=](const auto& c) { return c.token == token; }); + // We should only fail to find the callback if it was removed due to an error + REALM_ASSERT(m_error || it != end(m_callbacks)); + if (it == end(m_callbacks)) { + return; + } + + size_t idx = distance(begin(m_callbacks), it); + if (m_callback_index != npos && m_callback_index >= idx) { + --m_callback_index; + } + + old = std::move(*it); + m_callbacks.erase(it); + + m_have_callbacks = !m_callbacks.empty(); + } +} + +void CollectionNotifier::unregister() noexcept +{ + std::lock_guard lock(m_realm_mutex); + m_realm = nullptr; +} + +bool CollectionNotifier::is_alive() const noexcept +{ + std::lock_guard lock(m_realm_mutex); + return m_realm != nullptr; +} + +std::unique_lock CollectionNotifier::lock_target() +{ + return std::unique_lock{m_realm_mutex}; +} + +void CollectionNotifier::set_table(Table const& table) +{ + m_related_tables.clear(); + DeepChangeChecker::find_related_tables(m_related_tables, table); +} + +void CollectionNotifier::add_required_change_info(TransactionChangeInfo& info) +{ + if (!do_add_required_change_info(info)) { + return; + } + + auto max = max_element(begin(m_related_tables), end(m_related_tables), + [](auto&& a, auto&& b) { return a.table_ndx < b.table_ndx; }); + + if (max->table_ndx >= info.table_modifications_needed.size()) + info.table_modifications_needed.resize(max->table_ndx + 1, false); + for (auto& tbl : m_related_tables) { + info.table_modifications_needed[tbl.table_ndx] = true; + } +} + +void CollectionNotifier::prepare_handover() +{ + REALM_ASSERT(m_sg); + m_sg_version = m_sg->get_version_of_current_transaction(); + do_prepare_handover(*m_sg); +} + +bool CollectionNotifier::deliver(Realm& realm, SharedGroup& sg, std::exception_ptr err) +{ + { + std::lock_guard lock(m_realm_mutex); + if (m_realm.get() != &realm) { + return false; + } + } + + if (err) { + m_error = err; + return have_callbacks(); + } + + auto realm_sg_version = sg.get_version_of_current_transaction(); + if (version() != realm_sg_version) { + // Realm version can be newer if a commit was made on our thread or the + // user manually called refresh(), or older if a commit was made on a + // different thread and we ran *really* fast in between the check for + // if the shared group has changed and when we pick up async results + return false; + } + + bool should_call_callbacks = do_deliver(sg); + m_changes_to_deliver = std::move(m_accumulated_changes); + + // fixup modifications to be source rows rather than dest rows + // FIXME: the actual change calculations should be updated to just calculate + // the correct thing instead + m_changes_to_deliver.modifications.erase_at(m_changes_to_deliver.insertions); + m_changes_to_deliver.modifications.shift_for_insert_at(m_changes_to_deliver.deletions); + + return should_call_callbacks && have_callbacks(); +} + +void CollectionNotifier::call_callbacks() +{ + while (auto fn = next_callback()) { + fn(m_changes_to_deliver, m_error); + } + + if (m_error) { + // Remove all the callbacks as we never need to call anything ever again + // after delivering an error + std::lock_guard callback_lock(m_callback_mutex); + m_callbacks.clear(); + } +} + +CollectionChangeCallback CollectionNotifier::next_callback() +{ + std::lock_guard callback_lock(m_callback_mutex); + + for (++m_callback_index; m_callback_index < m_callbacks.size(); ++m_callback_index) { + auto& callback = m_callbacks[m_callback_index]; + if (!m_error && callback.initial_delivered && m_changes_to_deliver.empty()) { + continue; + } + callback.initial_delivered = true; + return callback.fn; + } + + m_callback_index = npos; + return nullptr; +} + +void CollectionNotifier::attach_to(SharedGroup& sg) +{ + REALM_ASSERT(!m_sg); + + m_sg = &sg; + do_attach_to(sg); +} + +void CollectionNotifier::detach() +{ + REALM_ASSERT(m_sg); + do_detach_from(*m_sg); + m_sg = nullptr; +} diff --git a/src/impl/collection_notifier.hpp b/src/impl/collection_notifier.hpp new file mode 100644 index 00000000..211d5ced --- /dev/null +++ b/src/impl/collection_notifier.hpp @@ -0,0 +1,207 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_BACKGROUND_COLLECTION_HPP +#define REALM_BACKGROUND_COLLECTION_HPP + +#include "impl/collection_change_builder.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace realm { +class Realm; + +namespace _impl { +struct ListChangeInfo { + size_t table_ndx; + size_t row_ndx; + size_t col_ndx; + CollectionChangeBuilder* changes; +}; + +struct TransactionChangeInfo { + std::vector table_modifications_needed; + std::vector table_moves_needed; + std::vector lists; + std::vector tables; +}; + +class DeepChangeChecker { +public: + struct OutgoingLink { + size_t col_ndx; + bool is_list; + }; + struct RelatedTable { + size_t table_ndx; + std::vector links; + }; + + DeepChangeChecker(TransactionChangeInfo const& info, Table const& root_table, + std::vector const& related_tables); + + bool operator()(size_t row_ndx); + + // Recursively add `table` and all tables it links to to `out`, along with + // information about the links from them + static void find_related_tables(std::vector& out, Table const& table); + +private: + TransactionChangeInfo const& m_info; + Table const& m_root_table; + const size_t m_root_table_ndx; + IndexSet const* const m_root_modifications; + std::vector m_not_modified; + std::vector const& m_related_tables; + + struct Path { + size_t table; + size_t row; + size_t col; + bool depth_exceeded; + }; + std::array m_current_path; + + bool check_row(Table const& table, size_t row_ndx, size_t depth = 0); + bool check_outgoing_links(size_t table_ndx, Table const& table, + size_t row_ndx, size_t depth = 0); +}; + +// A base class for a notifier that keeps a collection up to date and/or +// generates detailed change notifications on a background thread. This manages +// most of the lifetime-management issues related to sharing an object between +// the worker thread and the collection on the target thread, along with the +// thread-safe callback collection. +class CollectionNotifier { +public: + CollectionNotifier(std::shared_ptr); + virtual ~CollectionNotifier(); + + // ------------------------------------------------------------------------ + // Public API for the collections using this to get notifications: + + // Stop receiving notifications from this background worker + // This must be called in the destructor of the collection + void unregister() noexcept; + + // Add a callback to be called each time the collection changes + // This can only be called from the target collection's thread + // Returns a token which can be passed to remove_callback() + size_t add_callback(CollectionChangeCallback callback); + // Remove a previously added token. The token is no longer valid after + // calling this function and must not be used again. This function can be + // called from any thread. + void remove_callback(size_t token); + + // ------------------------------------------------------------------------ + // API for RealmCoordinator to manage running things and calling callbacks + + Realm* get_realm() const noexcept { return m_realm.get(); } + + // Get the SharedGroup version which this collection can attach to (if it's + // in handover mode), or can deliver to (if it's been handed over to the BG worker alredad) + SharedGroup::VersionID version() const noexcept { return m_sg_version; } + + // Release references to all core types + // This is called on the worker thread to ensure that non-thread-safe things + // can be destroyed on the correct thread, even if the last reference to the + // CollectionNotifier is released on a different thread + virtual void release_data() noexcept = 0; + + // Call each of the currently registered callbacks, if there have been any + // changes since the last time each of those callbacks was called + void call_callbacks(); + + bool is_alive() const noexcept; + + // Attach the handed-over query to `sg`. Must not be already attached to a SharedGroup. + void attach_to(SharedGroup& sg); + // Create a new query handover object and stop using the previously attached + // SharedGroup + void detach(); + + // Set `info` as the new ChangeInfo that will be populated by the next + // transaction advance, and register all required information in it + void add_required_change_info(TransactionChangeInfo& info); + + virtual void run() = 0; + void prepare_handover(); + bool deliver(Realm&, SharedGroup&, std::exception_ptr); + +protected: + bool have_callbacks() const noexcept { return m_have_callbacks; } + void add_changes(CollectionChangeBuilder change) { m_accumulated_changes.merge(std::move(change)); } + void set_table(Table const& table); + std::unique_lock lock_target(); + + std::function get_modification_checker(TransactionChangeInfo const&, Table const&); + +private: + virtual void do_attach_to(SharedGroup&) = 0; + virtual void do_detach_from(SharedGroup&) = 0; + virtual void do_prepare_handover(SharedGroup&) = 0; + virtual bool do_deliver(SharedGroup&) { return true; } + virtual bool do_add_required_change_info(TransactionChangeInfo&) = 0; + + mutable std::mutex m_realm_mutex; + std::shared_ptr m_realm; + + SharedGroup::VersionID m_sg_version; + SharedGroup* m_sg = nullptr; + + std::exception_ptr m_error; + CollectionChangeBuilder m_accumulated_changes; + CollectionChangeSet m_changes_to_deliver; + + std::vector m_related_tables; + + struct Callback { + CollectionChangeCallback fn; + size_t token; + bool initial_delivered; + }; + + // Currently registered callbacks and a mutex which must always be held + // while doing anything with them or m_callback_index + std::mutex m_callback_mutex; + std::vector m_callbacks; + + // Cached value for if m_callbacks is empty, needed to avoid deadlocks in + // run() due to lock-order inversion between m_callback_mutex and m_target_mutex + // It's okay if this value is stale as at worst it'll result in us doing + // some extra work. + std::atomic m_have_callbacks = {false}; + + // Iteration variable for looping over callbacks + // remove_callback() updates this when needed + size_t m_callback_index = npos; + + CollectionChangeCallback next_callback(); +}; + +} // namespace _impl +} // namespace realm + +#endif /* REALM_BACKGROUND_COLLECTION_HPP */ diff --git a/src/impl/list_notifier.cpp b/src/impl/list_notifier.cpp new file mode 100644 index 00000000..64bf6daf --- /dev/null +++ b/src/impl/list_notifier.cpp @@ -0,0 +1,122 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include "impl/list_notifier.hpp" + +#include "shared_realm.hpp" + +#include + +using namespace realm; +using namespace realm::_impl; + +ListNotifier::ListNotifier(LinkViewRef lv, std::shared_ptr realm) +: CollectionNotifier(std::move(realm)) +, m_prev_size(lv->size()) +{ + // Find the lv's column, since that isn't tracked directly + size_t row_ndx = lv->get_origin_row_index(); + m_col_ndx = not_found; + auto& table = lv->get_origin_table(); + for (size_t i = 0, count = table.get_column_count(); i != count; ++i) { + if (table.get_column_type(i) == type_LinkList && table.get_linklist(i, row_ndx) == lv) { + m_col_ndx = i; + break; + } + } + REALM_ASSERT(m_col_ndx != not_found); + + set_table(lv->get_target_table()); + + auto& sg = Realm::Internal::get_shared_group(*get_realm()); + m_lv_handover = sg.export_linkview_for_handover(lv); +} + +void ListNotifier::release_data() noexcept +{ + m_lv.reset(); +} + +void ListNotifier::do_attach_to(SharedGroup& sg) +{ + REALM_ASSERT(m_lv_handover); + REALM_ASSERT(!m_lv); + m_lv = sg.import_linkview_from_handover(std::move(m_lv_handover)); +} + +void ListNotifier::do_detach_from(SharedGroup& sg) +{ + REALM_ASSERT(!m_lv_handover); + if (m_lv) { + m_lv_handover = sg.export_linkview_for_handover(m_lv); + m_lv = {}; + } +} + +bool ListNotifier::do_add_required_change_info(TransactionChangeInfo& info) +{ + REALM_ASSERT(!m_lv_handover); + if (!m_lv || !m_lv->is_attached()) { + return false; // origin row was deleted after the notification was added + } + + size_t row_ndx = m_lv->get_origin_row_index(); + auto& table = m_lv->get_origin_table(); + info.lists.push_back({table.get_index_in_group(), row_ndx, m_col_ndx, &m_change}); + + m_info = &info; + return true; +} + +void ListNotifier::run() +{ + if (!m_lv || !m_lv->is_attached()) { + // LV was deleted, so report all of the rows being removed if this is + // the first run after that + if (m_prev_size) { + m_change.deletions.set(m_prev_size); + m_prev_size = 0; + } + else { + m_change = {}; + } + return; + } + + auto row_did_change = get_modification_checker(*m_info, m_lv->get_target_table()); + for (size_t i = 0; i < m_lv->size(); ++i) { + if (m_change.modifications.contains(i)) + continue; + if (row_did_change(m_lv->get(i).get_index())) + m_change.modifications.add(i); + } + + for (auto const& move : m_change.moves) { + if (m_change.modifications.contains(move.to)) + continue; + if (row_did_change(m_lv->get(move.to).get_index())) + m_change.modifications.add(move.to); + } + + m_prev_size = m_lv->size(); +} + +void ListNotifier::do_prepare_handover(SharedGroup&) +{ + add_changes(std::move(m_change)); +} diff --git a/src/impl/list_notifier.hpp b/src/impl/list_notifier.hpp new file mode 100644 index 00000000..82b4e414 --- /dev/null +++ b/src/impl/list_notifier.hpp @@ -0,0 +1,62 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_LIST_NOTIFIER_HPP +#define REALM_LIST_NOTIFIER_HPP + +#include "impl/collection_notifier.hpp" + +#include + +namespace realm { +namespace _impl { +class ListNotifier : public CollectionNotifier { +public: + ListNotifier(LinkViewRef lv, std::shared_ptr realm); + +private: + // The linkview, in handover form if this has not been attached to the main + // SharedGroup yet + LinkViewRef m_lv; + std::unique_ptr> m_lv_handover; + + // The last-seen size of the LinkView so that we can report row deletions + // when the LinkView itself is deleted + size_t m_prev_size; + + // The column index of the LinkView + size_t m_col_ndx; + + // The actual change, calculated in run() and delivered in prepare_handover() + CollectionChangeBuilder m_change; + TransactionChangeInfo* m_info; + + void run() override; + + void do_prepare_handover(SharedGroup&) override; + + void do_attach_to(SharedGroup& sg) override; + void do_detach_from(SharedGroup& sg) override; + + void release_data() noexcept override; + bool do_add_required_change_info(TransactionChangeInfo& info) override; +}; +} +} + +#endif // REALM_LIST_NOTIFIER_HPP diff --git a/src/impl/realm_coordinator.cpp b/src/impl/realm_coordinator.cpp index d9b89e0b..86b118a8 100644 --- a/src/impl/realm_coordinator.cpp +++ b/src/impl/realm_coordinator.cpp @@ -18,20 +18,18 @@ #include "impl/realm_coordinator.hpp" -#include "impl/async_query.hpp" -#include "impl/weak_realm_notifier.hpp" +#include "impl/collection_notifier.hpp" #include "impl/external_commit_helper.hpp" #include "impl/transact_log_handler.hpp" +#include "impl/weak_realm_notifier.hpp" #include "object_store.hpp" #include "schema.hpp" #include #include #include -#include -#include +#include -#include #include using namespace realm; @@ -238,30 +236,34 @@ void RealmCoordinator::pin_version(uint_fast64_t version, uint_fast32_t index) m_advancer_history = nullptr; } } - else if (m_new_queries.empty()) { - // If this is the first query then we don't already have a read transaction + else if (m_new_notifiers.empty()) { + // If this is the first notifier then we don't already have a read transaction + REALM_ASSERT_3(m_advancer_sg->get_transact_stage(), ==, SharedGroup::transact_Ready); m_advancer_sg->begin_read(versionid); } - else if (versionid < m_advancer_sg->get_version_of_current_transaction()) { - // Ensure we're holding a readlock on the oldest version we have a - // handover object for, as handover objects don't - m_advancer_sg->end_read(); - m_advancer_sg->begin_read(versionid); + else { + REALM_ASSERT_3(m_advancer_sg->get_transact_stage(), ==, SharedGroup::transact_Reading); + if (versionid < m_advancer_sg->get_version_of_current_transaction()) { + // Ensure we're holding a readlock on the oldest version we have a + // handover object for, as handover objects don't + m_advancer_sg->end_read(); + m_advancer_sg->begin_read(versionid); + } } } -void RealmCoordinator::register_query(std::shared_ptr query) +void RealmCoordinator::register_notifier(std::shared_ptr notifier) { - auto version = query->version(); - auto& self = Realm::Internal::get_coordinator(query->get_realm()); + auto version = notifier->version(); + auto& self = Realm::Internal::get_coordinator(*notifier->get_realm()); { - std::lock_guard lock(self.m_query_mutex); + std::lock_guard lock(self.m_notifier_mutex); self.pin_version(version.version, version.index); - self.m_new_queries.push_back(std::move(query)); + self.m_new_notifiers.push_back(std::move(notifier)); } } -void RealmCoordinator::clean_up_dead_queries() +void RealmCoordinator::clean_up_dead_notifiers() { auto swap_remove = [&](auto& container) { bool did_remove = false; @@ -269,9 +271,9 @@ void RealmCoordinator::clean_up_dead_queries() if (container[i]->is_alive()) continue; - // Ensure the query is destroyed here even if there's lingering refs - // to the async query elsewhere - container[i]->release_query(); + // Ensure the notifier is destroyed here even if there's lingering refs + // to the async notifier elsewhere + container[i]->release_data(); if (container.size() > i + 1) container[i] = std::move(container.back()); @@ -282,16 +284,18 @@ void RealmCoordinator::clean_up_dead_queries() return did_remove; }; - if (swap_remove(m_queries)) { + if (swap_remove(m_notifiers)) { // Make sure we aren't holding on to read versions needlessly if there - // are no queries left, but don't close them entirely as opening shared + // are no notifiers left, but don't close them entirely as opening shared // groups is expensive - if (m_queries.empty() && m_query_sg) { - m_query_sg->end_read(); + if (m_notifiers.empty() && m_notifier_sg) { + REALM_ASSERT_3(m_notifier_sg->get_transact_stage(), ==, SharedGroup::transact_Reading); + m_notifier_sg->end_read(); } } - if (swap_remove(m_new_queries)) { - if (m_new_queries.empty() && m_advancer_sg) { + if (swap_remove(m_new_notifiers)) { + REALM_ASSERT_3(m_advancer_sg->get_transact_stage(), ==, SharedGroup::transact_Reading); + if (m_new_notifiers.empty() && m_advancer_sg) { m_advancer_sg->end_read(); } } @@ -299,7 +303,7 @@ void RealmCoordinator::clean_up_dead_queries() void RealmCoordinator::on_change() { - run_async_queries(); + run_async_notifiers(); std::lock_guard lock(m_realm_mutex); for (auto& realm : m_weak_realm_notifiers) { @@ -307,13 +311,108 @@ void RealmCoordinator::on_change() } } -void RealmCoordinator::run_async_queries() +namespace { +class IncrementalChangeInfo { +public: + IncrementalChangeInfo(SharedGroup& sg, + std::vector>& notifiers) + : m_sg(sg) + { + if (notifiers.empty()) + return; + + auto cmp = [&](auto&& lft, auto&& rgt) { + return lft->version() < rgt->version(); + }; + + // Sort the notifiers by their source version so that we can pull them + // all forward to the latest version in a single pass over the transaction log + std::sort(notifiers.begin(), notifiers.end(), cmp); + + // Preallocate the required amount of space in the vector so that we can + // safely give out pointers to within the vector + size_t count = 1; + for (auto it = notifiers.begin(), next = it + 1; next != notifiers.end(); ++it, ++next) { + if (cmp(*it, *next)) + ++count; + } + m_info.reserve(count); + m_info.resize(1); + m_current = &m_info[0]; + } + + TransactionChangeInfo& current() const { return *m_current; } + + bool advance_incremental(SharedGroup::VersionID version) + { + if (version != m_sg.get_version_of_current_transaction()) { + transaction::advance(m_sg, *m_current, version); + m_info.push_back({ + m_current->table_modifications_needed, + m_current->table_moves_needed, + std::move(m_current->lists)}); + m_current = &m_info.back(); + return true; + } + return false; + } + + void advance_to_final(SharedGroup::VersionID version) + { + if (!m_current) { + transaction::advance(m_sg, nullptr, version); + return; + } + + transaction::advance(m_sg, *m_current, version); + + // We now need to combine the transaction change info objects so that all of + // the notifiers see the complete set of changes from their first version to + // the most recent one + for (size_t i = m_info.size() - 1; i > 0; --i) { + auto& cur = m_info[i]; + if (cur.tables.empty()) + continue; + auto& prev = m_info[i - 1]; + if (prev.tables.empty()) { + prev.tables = cur.tables; + continue; + } + + for (size_t j = 0; j < prev.tables.size() && j < cur.tables.size(); ++j) { + prev.tables[j].merge(CollectionChangeBuilder{cur.tables[j]}); + } + prev.tables.reserve(cur.tables.size()); + while (prev.tables.size() < cur.tables.size()) { + prev.tables.push_back(cur.tables[prev.tables.size()]); + } + } + + // Copy the list change info if there are multiple LinkViews for the same LinkList + auto id = [](auto const& list) { return std::tie(list.table_ndx, list.col_ndx, list.row_ndx); }; + for (size_t i = 1; i < m_current->lists.size(); ++i) { + for (size_t j = i; j > 0; --j) { + if (id(m_current->lists[i]) == id(m_current->lists[j - 1])) { + m_current->lists[j - 1].changes->merge(CollectionChangeBuilder{*m_current->lists[i].changes}); + } + } + } + } + +private: + std::vector m_info; + TransactionChangeInfo* m_current = nullptr; + SharedGroup& m_sg; +}; +} // anonymous namespace + +void RealmCoordinator::run_async_notifiers() { - std::unique_lock lock(m_query_mutex); + std::unique_lock lock(m_notifier_mutex); - clean_up_dead_queries(); + clean_up_dead_notifiers(); - if (m_queries.empty() && m_new_queries.empty()) { + if (m_notifiers.empty() && m_new_notifiers.empty()) { return; } @@ -322,103 +421,114 @@ void RealmCoordinator::run_async_queries() } if (m_async_error) { - move_new_queries_to_main(); + std::move(m_new_notifiers.begin(), m_new_notifiers.end(), std::back_inserter(m_notifiers)); + m_new_notifiers.clear(); return; } - advance_helper_shared_group_to_latest(); + SharedGroup::VersionID version; - // Make a copy of the queries vector so that we can release the lock while - // we run the queries - auto queries_to_run = m_queries; + // Advance all of the new notifiers to the most recent version, if any + auto new_notifiers = std::move(m_new_notifiers); + IncrementalChangeInfo new_notifier_change_info(*m_advancer_sg, new_notifiers); + + if (!new_notifiers.empty()) { + REALM_ASSERT_3(m_advancer_sg->get_transact_stage(), ==, SharedGroup::transact_Reading); + REALM_ASSERT_3(m_advancer_sg->get_version_of_current_transaction().version, + <=, new_notifiers.front()->version().version); + + // The advancer SG can be at an older version than the oldest new notifier + // if a notifier was added and then removed before it ever got the chance + // to run, as we don't move the pin forward when removing dead notifiers + transaction::advance(*m_advancer_sg, nullptr, new_notifiers.front()->version()); + + // Advance each of the new notifiers to the latest version, attaching them + // to the SG at their handover version. This requires a unique + // TransactionChangeInfo for each source version, so that things don't + // see changes from before the version they were handed over from. + // Each Info has all of the changes between that source version and the + // next source version, and they'll be merged together later after + // releasing the lock + for (auto& notifier : new_notifiers) { + new_notifier_change_info.advance_incremental(notifier->version()); + notifier->attach_to(*m_advancer_sg); + notifier->add_required_change_info(new_notifier_change_info.current()); + } + new_notifier_change_info.advance_to_final(SharedGroup::VersionID{}); + + for (auto& notifier : new_notifiers) { + notifier->detach(); + } + version = m_advancer_sg->get_version_of_current_transaction(); + m_advancer_sg->end_read(); + } + REALM_ASSERT_3(m_advancer_sg->get_transact_stage(), ==, SharedGroup::transact_Ready); + + // Make a copy of the notifiers vector and then release the lock to avoid + // blocking other threads trying to register or unregister notifiers while we run them + auto notifiers = m_notifiers; lock.unlock(); - for (auto& query : queries_to_run) { - query->run(); + // Advance the non-new notifiers to the same version as we advanced the new + // ones to (or the latest if there were no new ones) + IncrementalChangeInfo change_info(*m_notifier_sg, notifiers); + for (auto& notifier : notifiers) { + notifier->add_required_change_info(change_info.current()); + } + change_info.advance_to_final(version); + + // Attach the new notifiers to the main SG and move them to the main list + for (auto& notifier : new_notifiers) { + notifier->attach_to(*m_notifier_sg); + } + std::move(new_notifiers.begin(), new_notifiers.end(), std::back_inserter(notifiers)); + + // Change info is now all ready, so the notifiers can now perform their + // background work + for (auto& notifier : notifiers) { + notifier->run(); } // Reacquire the lock while updating the fields that are actually read on // other threads - { - lock.lock(); - for (auto& query : queries_to_run) { - query->prepare_handover(); - } + lock.lock(); + for (auto& notifier : notifiers) { + notifier->prepare_handover(); } - - clean_up_dead_queries(); + m_notifiers = std::move(notifiers); + clean_up_dead_notifiers(); } void RealmCoordinator::open_helper_shared_group() { - if (!m_query_sg) { + if (!m_notifier_sg) { try { std::unique_ptr read_only_group; - Realm::open_with_config(m_config, m_query_history, m_query_sg, read_only_group); + Realm::open_with_config(m_config, m_notifier_history, m_notifier_sg, read_only_group); REALM_ASSERT(!read_only_group); - m_query_sg->begin_read(); + m_notifier_sg->begin_read(); } catch (...) { - // Store the error to be passed to the async queries + // Store the error to be passed to the async notifiers m_async_error = std::current_exception(); - m_query_sg = nullptr; - m_query_history = nullptr; + m_notifier_sg = nullptr; + m_notifier_history = nullptr; } } - else if (m_queries.empty()) { - m_query_sg->begin_read(); + else if (m_notifiers.empty()) { + m_notifier_sg->begin_read(); } } -void RealmCoordinator::move_new_queries_to_main() -{ - m_queries.reserve(m_queries.size() + m_new_queries.size()); - std::move(m_new_queries.begin(), m_new_queries.end(), std::back_inserter(m_queries)); - m_new_queries.clear(); -} - -void RealmCoordinator::advance_helper_shared_group_to_latest() -{ - if (m_new_queries.empty()) { - LangBindHelper::advance_read(*m_query_sg); - return; - } - - // Sort newly added queries by their source version so that we can pull them - // all forward to the latest version in a single pass over the transaction log - std::sort(m_new_queries.begin(), m_new_queries.end(), [](auto const& lft, auto const& rgt) { - return lft->version() < rgt->version(); - }); - - // Import all newly added queries to our helper SG - for (auto& query : m_new_queries) { - LangBindHelper::advance_read(*m_advancer_sg, query->version()); - query->attach_to(*m_advancer_sg); - } - - // Advance both SGs to the newest version - LangBindHelper::advance_read(*m_advancer_sg); - LangBindHelper::advance_read(*m_query_sg, m_advancer_sg->get_version_of_current_transaction()); - - // Transfer all new queries over to the main SG - for (auto& query : m_new_queries) { - query->detatch(); - query->attach_to(*m_query_sg); - } - - move_new_queries_to_main(); - m_advancer_sg->end_read(); -} - void RealmCoordinator::advance_to_ready(Realm& realm) { - decltype(m_queries) queries; + decltype(m_notifiers) notifiers; auto& sg = Realm::Internal::get_shared_group(realm); - auto get_query_version = [&] { - for (auto& query : m_queries) { - auto version = query->version(); + auto get_notifier_version = [&] { + for (auto& notifier : m_notifiers) { + auto version = notifier->version(); if (version != SharedGroup::VersionID{}) { return version; } @@ -428,11 +538,11 @@ void RealmCoordinator::advance_to_ready(Realm& realm) SharedGroup::VersionID version; { - std::lock_guard lock(m_query_mutex); - version = get_query_version(); + std::lock_guard lock(m_notifier_mutex); + version = get_notifier_version(); } - // no async queries; just advance to latest + // no async notifiers; just advance to latest if (version.version == std::numeric_limits::max()) { transaction::advance(sg, realm.m_binding_context.get()); return; @@ -448,44 +558,44 @@ void RealmCoordinator::advance_to_ready(Realm& realm) // may end up calling user code (in did_change() notifications) transaction::advance(sg, realm.m_binding_context.get(), version); - // Reacquire the lock and recheck the query version, as the queries may + // Reacquire the lock and recheck the notifier version, as the notifiers may // have advanced to a later version while we didn't hold the lock. If // so, we need to release the lock and re-advance - std::lock_guard lock(m_query_mutex); - version = get_query_version(); + std::lock_guard lock(m_notifier_mutex); + version = get_notifier_version(); if (version.version == std::numeric_limits::max()) return; if (version != sg.get_version_of_current_transaction()) continue; // Query version now matches the SG version, so we can deliver them - for (auto& query : m_queries) { - if (query->deliver(sg, m_async_error)) { - queries.push_back(query); + for (auto& notifier : m_notifiers) { + if (notifier->deliver(realm, sg, m_async_error)) { + notifiers.push_back(notifier); } } break; } - for (auto& query : queries) { - query->call_callbacks(); + for (auto& notifier : notifiers) { + notifier->call_callbacks(); } } void RealmCoordinator::process_available_async(Realm& realm) { auto& sg = Realm::Internal::get_shared_group(realm); - decltype(m_queries) queries; + decltype(m_notifiers) notifiers; { - std::lock_guard lock(m_query_mutex); - for (auto& query : m_queries) { - if (query->deliver(sg, m_async_error)) { - queries.push_back(query); + std::lock_guard lock(m_notifier_mutex); + for (auto& notifier : m_notifiers) { + if (notifier->deliver(realm, sg, m_async_error)) { + notifiers.push_back(notifier); } } } - for (auto& query : queries) { - query->call_callbacks(); + for (auto& notifier : notifiers) { + notifier->call_callbacks(); } } diff --git a/src/impl/realm_coordinator.hpp b/src/impl/realm_coordinator.hpp index 05cf6d6c..2a6f74b2 100644 --- a/src/impl/realm_coordinator.hpp +++ b/src/impl/realm_coordinator.hpp @@ -21,20 +21,18 @@ #include "shared_realm.hpp" -#include +#include namespace realm { -class AsyncQueryCallback; class Replication; -class Results; class Schema; class SharedGroup; -struct AsyncQueryCancelationToken; +class StringData; namespace _impl { -class AsyncQuery; -class WeakRealmNotifier; +class CollectionNotifier; class ExternalCommitHelper; +class WeakRealmNotifier; // RealmCoordinator manages the weak cache of Realm instances and communication // between per-thread Realm instances for a given file @@ -83,7 +81,7 @@ public: // Update the schema in the cached config void update_schema(Schema const& new_schema); - static void register_query(std::shared_ptr query); + static void register_notifier(std::shared_ptr notifier); // Advance the Realm to the most recent transaction version which all async // work is complete for @@ -96,32 +94,31 @@ private: std::mutex m_realm_mutex; std::vector m_weak_realm_notifiers; - std::mutex m_query_mutex; - std::vector> m_new_queries; - std::vector> m_queries; + std::mutex m_notifier_mutex; + std::vector> m_new_notifiers; + std::vector> m_notifiers; - // SharedGroup used for actually running async queries - // Will have a read transaction iff m_queries is non-empty - std::unique_ptr m_query_history; - std::unique_ptr m_query_sg; + // SharedGroup used for actually running async notifiers + // Will have a read transaction iff m_notifiers is non-empty + std::unique_ptr m_notifier_history; + std::unique_ptr m_notifier_sg; - // SharedGroup used to advance queries in m_new_queries to the main shared + // SharedGroup used to advance notifiers in m_new_notifiers to the main shared // group's transaction version - // Will have a read transaction iff m_new_queries is non-empty + // Will have a read transaction iff m_new_notifiers is non-empty std::unique_ptr m_advancer_history; std::unique_ptr m_advancer_sg; std::exception_ptr m_async_error; std::unique_ptr<_impl::ExternalCommitHelper> m_notifier; - // must be called with m_query_mutex locked + // must be called with m_notifier_mutex locked void pin_version(uint_fast64_t version, uint_fast32_t index); - void run_async_queries(); + void run_async_notifiers(); void open_helper_shared_group(); - void move_new_queries_to_main(); void advance_helper_shared_group_to_latest(); - void clean_up_dead_queries(); + void clean_up_dead_notifiers(); }; } // namespace _impl diff --git a/src/impl/results_notifier.cpp b/src/impl/results_notifier.cpp new file mode 100644 index 00000000..6e91f3d0 --- /dev/null +++ b/src/impl/results_notifier.cpp @@ -0,0 +1,211 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include "impl/results_notifier.hpp" + +#include "results.hpp" + +using namespace realm; +using namespace realm::_impl; + +ResultsNotifier::ResultsNotifier(Results& target) +: CollectionNotifier(target.get_realm()) +, m_target_results(&target) +, m_sort(target.get_sort()) +, m_target_is_in_table_order(target.is_in_table_order()) +{ + Query q = target.get_query(); + set_table(*q.get_table()); + m_query_handover = Realm::Internal::get_shared_group(*get_realm()).export_for_handover(q, MutableSourcePayload::Move); +} + +void ResultsNotifier::release_data() noexcept +{ + m_query = nullptr; +} + +// Most of the inter-thread synchronization for run(), prepare_handover(), +// attach_to(), detach(), release_data() and deliver() is done by +// RealmCoordinator external to this code, which has some potentially +// non-obvious results on which members are and are not safe to use without +// holding a lock. +// +// add_required_change_info(), attach_to(), detach(), run(), +// prepare_handover(), and release_data() are all only ever called on a single +// background worker thread. call_callbacks() and deliver() are called on the +// target thread. Calls to prepare_handover() and deliver() are guarded by a +// lock. +// +// In total, this means that the safe data flow is as follows: +// - add_Required_change_info(), prepare_handover(), attach_to(), detach() and +// release_data() can read members written by each other +// - deliver() can read members written to in prepare_handover(), deliver(), +// and call_callbacks() +// - call_callbacks() and read members written to in deliver() +// +// Separately from the handover data flow, m_target_results is guarded by the target lock + +bool ResultsNotifier::do_add_required_change_info(TransactionChangeInfo& info) +{ + REALM_ASSERT(m_query); + m_info = &info; + + auto table_ndx = m_query->get_table()->get_index_in_group(); + if (info.table_moves_needed.size() <= table_ndx) + info.table_moves_needed.resize(table_ndx + 1); + info.table_moves_needed[table_ndx] = true; + + return m_initial_run_complete && have_callbacks(); +} + +bool ResultsNotifier::need_to_run() +{ + REALM_ASSERT(m_info); + REALM_ASSERT(!m_tv.is_attached()); + + { + auto lock = lock_target(); + // Don't run the query if the results aren't actually going to be used + if (!get_realm() || (!have_callbacks() && !m_target_results->wants_background_updates())) { + return false; + } + } + + // If we've run previously, check if we need to rerun + if (m_initial_run_complete && m_query->sync_view_if_needed() == m_last_seen_version) { + return false; + } + + return true; +} + +void ResultsNotifier::calculate_changes() +{ + size_t table_ndx = m_query->get_table()->get_index_in_group(); + if (m_initial_run_complete) { + auto changes = table_ndx < m_info->tables.size() ? &m_info->tables[table_ndx] : nullptr; + + std::vector next_rows; + next_rows.reserve(m_tv.size()); + for (size_t i = 0; i < m_tv.size(); ++i) + next_rows.push_back(m_tv[i].get_index()); + + if (changes) { + auto const& moves = changes->moves; + for (auto& idx : m_previous_rows) { + auto it = lower_bound(begin(moves), end(moves), idx, + [](auto const& a, auto b) { return a.from < b; }); + if (it != moves.end() && it->from == idx) + idx = it->to; + else if (changes->deletions.contains(idx)) + idx = npos; + else + REALM_ASSERT_DEBUG(!changes->insertions.contains(idx)); + } + } + + m_changes = CollectionChangeBuilder::calculate(m_previous_rows, next_rows, + get_modification_checker(*m_info, *m_query->get_table()), + m_target_is_in_table_order && !m_sort); + + m_previous_rows = std::move(next_rows); + } + else { + m_previous_rows.resize(m_tv.size()); + for (size_t i = 0; i < m_tv.size(); ++i) + m_previous_rows[i] = m_tv[i].get_index(); + } +} + +void ResultsNotifier::run() +{ + if (!need_to_run()) + return; + + m_query->sync_view_if_needed(); + m_tv = m_query->find_all(); + if (m_sort) { + m_tv.sort(m_sort.column_indices, m_sort.ascending); + } + m_last_seen_version = m_tv.sync_if_needed(); + + calculate_changes(); +} + +void ResultsNotifier::do_prepare_handover(SharedGroup& sg) +{ + if (!m_tv.is_attached()) { + return; + } + + REALM_ASSERT(m_tv.is_in_sync()); + + m_initial_run_complete = true; + m_tv_handover = sg.export_for_handover(m_tv, MutableSourcePayload::Move); + + add_changes(std::move(m_changes)); + REALM_ASSERT(m_changes.empty()); + + // detach the TableView as we won't need it again and keeping it around + // makes advance_read() much more expensive + m_tv = {}; +} + +bool ResultsNotifier::do_deliver(SharedGroup& sg) +{ + auto lock = lock_target(); + + // Target realm being null here indicates that we were unregistered while we + // were in the process of advancing the Realm version and preparing for + // delivery, i.e. the results was destroyed from the "wrong" thread + if (!get_realm()) { + return false; + } + + // We can get called before the query has actually had the chance to run if + // we're added immediately before a different set of async results are + // delivered + if (!m_initial_run_complete) { + return false; + } + + REALM_ASSERT(!m_query_handover); + + if (m_tv_handover) { + m_tv_handover->version = version(); + Results::Internal::set_table_view(*m_target_results, + std::move(*sg.import_from_handover(std::move(m_tv_handover)))); + } + REALM_ASSERT(!m_tv_handover); + return true; +} + +void ResultsNotifier::do_attach_to(SharedGroup& sg) +{ + REALM_ASSERT(m_query_handover); + m_query = sg.import_from_handover(std::move(m_query_handover)); +} + +void ResultsNotifier::do_detach_from(SharedGroup& sg) +{ + REALM_ASSERT(m_query); + REALM_ASSERT(!m_tv.is_attached()); + + m_query_handover = sg.export_for_handover(*m_query, MutableSourcePayload::Move); + m_query = nullptr; +} diff --git a/src/impl/results_notifier.hpp b/src/impl/results_notifier.hpp new file mode 100644 index 00000000..8028fbd0 --- /dev/null +++ b/src/impl/results_notifier.hpp @@ -0,0 +1,81 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_RESULTS_NOTIFIER_HPP +#define REALM_RESULTS_NOTIFIER_HPP + +#include "collection_notifier.hpp" +#include "results.hpp" + +#include + +namespace realm { +namespace _impl { +class ResultsNotifier : public CollectionNotifier { +public: + ResultsNotifier(Results& target); + +private: + // Target Results to update + // Can only be used with lock_target() held + Results* m_target_results; + + const SortOrder m_sort; + bool m_target_is_in_table_order; + + // The source Query, in handover form iff m_sg is null + std::unique_ptr> m_query_handover; + std::unique_ptr m_query; + + // The TableView resulting from running the query. Will be detached unless + // the query was (re)run since the last time the handover object was created + TableView m_tv; + std::unique_ptr> m_tv_handover; + + // The table version from the last time the query was run. Used to avoid + // rerunning the query when there's no chance of it changing. + uint_fast64_t m_last_seen_version = -1; + + // The rows from the previous run of the query, for calculating diffs + std::vector m_previous_rows; + + // The changeset calculated during run() and delivered in do_prepare_handover() + CollectionChangeBuilder m_changes; + TransactionChangeInfo* m_info = nullptr; + + // Flag for whether or not the query has been run at all, as goofy timing + // can lead to deliver() being called before that + bool m_initial_run_complete = false; + + bool need_to_run(); + void calculate_changes(); + + void run() override; + void do_prepare_handover(SharedGroup&) override; + bool do_deliver(SharedGroup& sg) override; + bool do_add_required_change_info(TransactionChangeInfo& info) override; + + void release_data() noexcept override; + void do_attach_to(SharedGroup& sg) override; + void do_detach_from(SharedGroup& sg) override; +}; + +} // namespace _impl +} // namespace realm + +#endif /* REALM_RESULTS_NOTIFIER_HPP */ diff --git a/src/impl/transact_log_handler.cpp b/src/impl/transact_log_handler.cpp index d95aa32d..e0880bee 100644 --- a/src/impl/transact_log_handler.cpp +++ b/src/impl/transact_log_handler.cpp @@ -19,17 +19,39 @@ #include "impl/transact_log_handler.hpp" #include "binding_context.hpp" +#include "impl/collection_notifier.hpp" +#include "index_set.hpp" -#include #include #include using namespace realm; namespace { -// A transaction log handler that just validates that all operations made are -// ones supported by the object store -class TransactLogValidator { +template +struct MarkDirtyMixin { + bool mark_dirty(size_t row, size_t col) { static_cast(this)->mark_dirty(row, col); return true; } + + bool set_int(size_t col, size_t row, int_fast64_t) { return mark_dirty(row, col); } + bool set_bool(size_t col, size_t row, bool) { return mark_dirty(row, col); } + bool set_float(size_t col, size_t row, float) { return mark_dirty(row, col); } + bool set_double(size_t col, size_t row, double) { return mark_dirty(row, col); } + bool set_string(size_t col, size_t row, StringData) { return mark_dirty(row, col); } + bool set_binary(size_t col, size_t row, BinaryData) { return mark_dirty(row, col); } + bool set_olddatetime(size_t col, size_t row, OldDateTime) { return mark_dirty(row, col); } + bool set_timestamp(size_t col, size_t row, Timestamp) { return mark_dirty(row, col); } + bool set_table(size_t col, size_t row) { return mark_dirty(row, col); } + bool set_mixed(size_t col, size_t row, const Mixed&) { return mark_dirty(row, col); } + bool set_link(size_t col, size_t row, size_t, size_t) { return mark_dirty(row, col); } + bool set_null(size_t col, size_t row) { return mark_dirty(row, col); } + bool nullify_link(size_t col, size_t row, size_t) { return mark_dirty(row, col); } + bool set_int_unique(size_t col, size_t row, size_t, int_fast64_t) { return mark_dirty(row, col); } + bool set_string_unique(size_t col, size_t row, size_t, StringData) { return mark_dirty(row, col); } + bool insert_substring(size_t col, size_t row, size_t, StringData) { return mark_dirty(row, col); } + bool erase_substring(size_t col, size_t row, size_t, size_t) { return mark_dirty(row, col); } +}; + +class TransactLogValidationMixin { // Index of currently selected table size_t m_current_table = 0; @@ -78,7 +100,6 @@ public: } bool insert_column(size_t, DataType, StringData, bool) { return schema_error_unless_new_table(); } bool insert_link_column(size_t, DataType, StringData, size_t, size_t) { return schema_error_unless_new_table(); } - bool add_primary_key(size_t) { return schema_error_unless_new_table(); } bool set_link_type(size_t, LinkType) { return schema_error_unless_new_table(); } // Removing or renaming things while a Realm is open is never supported @@ -87,7 +108,6 @@ public: bool erase_column(size_t) { schema_error(); } bool erase_link_column(size_t, size_t, size_t) { schema_error(); } bool rename_column(size_t, StringData) { schema_error(); } - bool remove_primary_key() { schema_error(); } bool move_column(size_t, size_t) { schema_error(); } bool move_group_level_table(size_t, size_t) { schema_error(); } @@ -118,30 +138,20 @@ public: bool link_list_clear(size_t) { return true; } bool link_list_move(size_t, size_t) { return true; } bool link_list_swap(size_t, size_t) { return true; } - bool set_int(size_t, size_t, int_fast64_t) { return true; } - bool set_bool(size_t, size_t, bool) { return true; } - bool set_float(size_t, size_t, float) { return true; } - bool set_double(size_t, size_t, double) { return true; } - bool set_string(size_t, size_t, StringData) { return true; } - bool set_binary(size_t, size_t, BinaryData) { return true; } - bool set_olddatetime(size_t, size_t, OldDateTime) { return true; } - bool set_timestamp(size_t, size_t, Timestamp) { return true; } - bool set_table(size_t, size_t) { return true; } - bool set_mixed(size_t, size_t, const Mixed&) { return true; } - bool set_link(size_t, size_t, size_t, size_t) { return true; } - bool set_null(size_t, size_t) { return true; } - bool nullify_link(size_t, size_t, size_t) { return true; } - bool insert_substring(size_t, size_t, size_t, StringData) { return true; } - bool erase_substring(size_t, size_t, size_t, size_t) { return true; } - bool optimize_table() { return true; } - bool set_int_unique(size_t, size_t, size_t, int_fast64_t) { return true; } - bool set_string_unique(size_t, size_t, size_t, StringData) { return true; } bool change_link_targets(size_t, size_t) { return true; } + bool optimize_table() { return true; } +}; + + +// A transaction log handler that just validates that all operations made are +// ones supported by the object store +struct TransactLogValidator : public TransactLogValidationMixin, public MarkDirtyMixin { + void mark_dirty(size_t, size_t) { } }; // Extends TransactLogValidator to also track changes and report it to the // binding context if any properties are being observed -class TransactLogObserver : public TransactLogValidator { +class TransactLogObserver : public TransactLogValidationMixin, public MarkDirtyMixin { using ColumnInfo = BindingContext::ColumnInfo; using ObserverState = BindingContext::ObserverState; @@ -180,16 +190,6 @@ class TransactLogObserver : public TransactLogValidator { } } - // Mark the given row/col as needing notifications sent - bool mark_dirty(size_t row_ndx, size_t col_ndx) - { - auto it = lower_bound(begin(m_observers), end(m_observers), ObserverState{current_table(), row_ndx, nullptr}); - if (it != end(m_observers) && it->table_ndx == current_table() && it->row_ndx == row_ndx) { - get_change(*it, col_ndx).changed = true; - } - return true; - } - // Remove the given observer from the list of observed objects and add it // to the listed of invalidated objects void invalidate(ObserverState *o) @@ -205,10 +205,7 @@ public: { if (!context) { if (validate_schema_changes) { - // The handler functions are non-virtual, so the parent class's - // versions are called if we don't need to track changes to observed - // objects - func(static_cast(*this)); + func(TransactLogValidator()); } else { func(); @@ -220,7 +217,7 @@ public: if (m_observers.empty()) { auto old_version = sg.get_version_of_current_transaction(); if (validate_schema_changes) { - func(static_cast(*this)); + func(TransactLogValidator()); } else { func(); @@ -235,6 +232,15 @@ public: context->did_change(m_observers, invalidated); } + // Mark the given row/col as needing notifications sent + void mark_dirty(size_t row_ndx, size_t col_ndx) + { + auto it = lower_bound(begin(m_observers), end(m_observers), ObserverState{current_table(), row_ndx, nullptr}); + if (it != end(m_observers) && it->table_ndx == current_table() && it->row_ndx == row_ndx) { + get_change(*it, col_ndx).changed = true; + } + } + // Called at the end of the transaction log immediately before the version // is advanced void parse_complete() @@ -248,7 +254,7 @@ public: if (observer.table_ndx >= table_ndx) ++observer.table_ndx; } - TransactLogValidator::insert_group_level_table(table_ndx, prior_size, name); + TransactLogValidationMixin::insert_group_level_table(table_ndx, prior_size, name); return true; } @@ -329,7 +335,7 @@ public: } else { // Array KVO can only send a single kind of change at a time, so - // if there's multiple just give up and send "Set" + // if there are multiple just give up and send "Set" o->indices.set(0); o->kind = ColumnInfo::Kind::SetAll; } @@ -374,9 +380,9 @@ public: } if (o->kind == ColumnInfo::Kind::Remove) - old_size += o->indices.size(); + old_size += o->indices.count(); else if (o->kind == ColumnInfo::Kind::Insert) - old_size -= o->indices.size(); + old_size -= o->indices.count(); o->indices.set(old_size); @@ -409,25 +415,156 @@ public: } return true; } +}; - // Things that just mark the field as modified - bool set_int(size_t col, size_t row, int_fast64_t) { return mark_dirty(row, col); } - bool set_bool(size_t col, size_t row, bool) { return mark_dirty(row, col); } - bool set_float(size_t col, size_t row, float) { return mark_dirty(row, col); } - bool set_double(size_t col, size_t row, double) { return mark_dirty(row, col); } - bool set_string(size_t col, size_t row, StringData) { return mark_dirty(row, col); } - bool set_binary(size_t col, size_t row, BinaryData) { return mark_dirty(row, col); } - bool set_olddatetime(size_t col, size_t row, OldDateTime) { return mark_dirty(row, col); } - bool set_timestamp(size_t col, size_t row, Timestamp) { return mark_dirty(row, col); } - bool set_table(size_t col, size_t row) { return mark_dirty(row, col); } - bool set_mixed(size_t col, size_t row, const Mixed&) { return mark_dirty(row, col); } - bool set_link(size_t col, size_t row, size_t, size_t) { return mark_dirty(row, col); } - bool set_null(size_t col, size_t row) { return mark_dirty(row, col); } - bool nullify_link(size_t col, size_t row, size_t) { return mark_dirty(row, col); } - bool set_int_unique(size_t col, size_t row, size_t, int_fast64_t) { return mark_dirty(row, col); } - bool set_string_unique(size_t col, size_t row, size_t, StringData) { return mark_dirty(row, col); } - bool insert_substring(size_t col, size_t row, size_t, StringData) { return mark_dirty(row, col); } - bool erase_substring(size_t col, size_t row, size_t, size_t) { return mark_dirty(row, col); } +// Extends TransactLogValidator to track changes made to LinkViews +class LinkViewObserver : public TransactLogValidationMixin, public MarkDirtyMixin { + _impl::TransactionChangeInfo& m_info; + _impl::CollectionChangeBuilder* m_active = nullptr; + + _impl::CollectionChangeBuilder* get_change() + { + auto tbl_ndx = current_table(); + if (tbl_ndx >= m_info.table_modifications_needed.size() || !m_info.table_modifications_needed[tbl_ndx]) + return nullptr; + if (m_info.tables.size() <= tbl_ndx) { + m_info.tables.resize(std::max(m_info.tables.size() * 2, tbl_ndx + 1)); + } + return &m_info.tables[tbl_ndx]; + } + + bool need_move_info() const + { + auto tbl_ndx = current_table(); + return tbl_ndx < m_info.table_moves_needed.size() && m_info.table_moves_needed[tbl_ndx]; + } + +public: + LinkViewObserver(_impl::TransactionChangeInfo& info) + : m_info(info) { } + + void mark_dirty(size_t row, __unused size_t col) + { + if (auto change = get_change()) + change->modify(row); + } + + void parse_complete() + { + for (auto& table : m_info.tables) { + table.parse_complete(); + } + for (auto& list : m_info.lists) { + list.changes->clean_up_stale_moves(); + } + } + + bool select_link_list(size_t col, size_t row, size_t) + { + mark_dirty(row, col); + + m_active = nullptr; + // When there are multiple source versions there could be multiple + // change objects for a single LinkView, in which case we need to use + // the last one + for (auto it = m_info.lists.rbegin(), end = m_info.lists.rend(); it != end; ++it) { + if (it->table_ndx == current_table() && it->row_ndx == row && it->col_ndx == col) { + m_active = it->changes; + break; + } + } + return true; + } + + bool link_list_set(size_t index, size_t) + { + if (m_active) + m_active->modify(index); + return true; + } + + bool link_list_insert(size_t index, size_t) + { + if (m_active) + m_active->insert(index); + return true; + } + + bool link_list_erase(size_t index) + { + if (m_active) + m_active->erase(index); + return true; + } + + bool link_list_nullify(size_t index) + { + return link_list_erase(index); + } + + bool link_list_swap(size_t index1, size_t index2) + { + link_list_set(index1, 0); + link_list_set(index2, 0); + return true; + } + + bool link_list_clear(size_t old_size) + { + if (m_active) + m_active->clear(old_size); + return true; + } + + bool link_list_move(size_t from, size_t to) + { + if (m_active) + m_active->move(from, to); + return true; + } + + bool insert_empty_rows(size_t row_ndx, size_t num_rows_to_insert, size_t, bool unordered) + { + REALM_ASSERT(!unordered); + if (auto change = get_change()) + change->insert(row_ndx, num_rows_to_insert, need_move_info()); + + return true; + } + + bool erase_rows(size_t row_ndx, size_t, size_t prior_num_rows, bool unordered) + { + REALM_ASSERT(unordered); + size_t last_row = prior_num_rows - 1; + + for (auto it = begin(m_info.lists); it != end(m_info.lists); ) { + if (it->table_ndx == current_table()) { + if (it->row_ndx == row_ndx) { + *it = std::move(m_info.lists.back()); + m_info.lists.pop_back(); + continue; + } + if (it->row_ndx == last_row - 1) + it->row_ndx = row_ndx; + } + ++it; + } + + if (auto change = get_change()) + change->move_over(row_ndx, last_row, need_move_info()); + return true; + } + + bool clear_table() + { + auto tbl_ndx = current_table(); + auto it = remove_if(begin(m_info.lists), end(m_info.lists), + [&](auto const& lv) { return lv.table_ndx == tbl_ndx; }); + m_info.lists.erase(it, end(m_info.lists)); + if (auto change = get_change()) + change->clear(std::numeric_limits::max()); + return true; + } }; } // anonymous namespace @@ -437,7 +574,7 @@ namespace transaction { void advance(SharedGroup& sg, BindingContext* context, SharedGroup::VersionID version) { TransactLogObserver(context, sg, [&](auto&&... args) { - LangBindHelper::advance_read(sg, std::move(args)...); + LangBindHelper::advance_read(sg, std::move(args)..., version); }, true); } @@ -464,6 +601,19 @@ void cancel(SharedGroup& sg, BindingContext* context) }, false); } +void advance(SharedGroup& sg, + TransactionChangeInfo& info, + SharedGroup::VersionID version) +{ + if (info.table_modifications_needed.empty() && info.lists.empty()) { + LangBindHelper::advance_read(sg, version); + } + else { + LangBindHelper::advance_read(sg, LinkViewObserver(info), version); + } + +} + } // namespace transaction } // namespace _impl } // namespace realm diff --git a/src/impl/transact_log_handler.hpp b/src/impl/transact_log_handler.hpp index 4249f8f3..96dbbfda 100644 --- a/src/impl/transact_log_handler.hpp +++ b/src/impl/transact_log_handler.hpp @@ -23,9 +23,10 @@ namespace realm { class BindingContext; -class SharedGroup; namespace _impl { +struct TransactionChangeInfo; + namespace transaction { // Advance the read transaction version, with change notifications sent to delegate // Must not be called from within a write transaction. @@ -44,6 +45,11 @@ void commit(SharedGroup& sg, BindingContext* binding_context); // Cancel a write transaction and roll back all changes, with change notifications // for reverting to the old values sent to delegate void cancel(SharedGroup& sg, BindingContext* binding_context); + +// Advance the read transaction version, with change information gathered in info +void advance(SharedGroup& sg, + TransactionChangeInfo& info, + SharedGroup::VersionID version=SharedGroup::VersionID{}); } // namespace transaction } // namespace _impl } // namespace realm diff --git a/src/index_set.cpp b/src/index_set.cpp index c244f76a..a5c30c2b 100644 --- a/src/index_set.cpp +++ b/src/index_set.cpp @@ -18,15 +18,329 @@ #include "index_set.hpp" +#include + +#include + using namespace realm; +using namespace realm::_impl; + +const size_t IndexSet::npos; + +template +void MutableChunkedRangeVectorIterator::set(size_t front, size_t back) +{ + this->m_outer->count -= this->m_inner->second - this->m_inner->first; + if (this->offset() == 0) { + this->m_outer->begin = front; + } + if (this->m_inner == &this->m_outer->data.back()) { + this->m_outer->end = back; + } + this->m_outer->count += back - front; + this->m_inner->first = front; + this->m_inner->second = back; +} + +template +void MutableChunkedRangeVectorIterator::adjust(ptrdiff_t front, ptrdiff_t back) +{ + if (this->offset() == 0) { + this->m_outer->begin += front; + } + if (this->m_inner == &this->m_outer->data.back()) { + this->m_outer->end += back; + } + this->m_outer->count += -front + back; + this->m_inner->first += front; + this->m_inner->second += back; +} + +template +void MutableChunkedRangeVectorIterator::shift(ptrdiff_t distance) +{ + if (this->offset() == 0) { + this->m_outer->begin += distance; + } + if (this->m_inner == &this->m_outer->data.back()) { + this->m_outer->end += distance; + } + this->m_inner->first += distance; + this->m_inner->second += distance; +} + +void ChunkedRangeVector::push_back(value_type value) +{ + if (!empty() && m_data.back().data.size() < max_size) { + auto& range = m_data.back(); + REALM_ASSERT(range.end <= value.first); + + range.data.push_back(value); + range.count += value.second - value.first; + range.end = value.second; + } + else { + m_data.push_back({{std::move(value)}, value.first, value.second, value.second - value.first}); + } + verify(); +} + +ChunkedRangeVector::iterator ChunkedRangeVector::insert(iterator pos, value_type value) +{ + if (pos.m_outer == m_data.end()) { + push_back(std::move(value)); + return std::prev(end()); + } + + pos = ensure_space(pos); + auto& chunk = *pos.m_outer; + pos.m_inner = &*chunk.data.insert(pos.m_outer->data.begin() + pos.offset(), value); + chunk.count += value.second - value.first; + chunk.begin = std::min(chunk.begin, value.first); + chunk.end = std::max(chunk.end, value.second); + + verify(); + return pos; +} + +ChunkedRangeVector::iterator ChunkedRangeVector::ensure_space(iterator pos) +{ + if (pos.m_outer->data.size() + 1 <= max_size) + return pos; + + auto offset = pos.offset(); + + // Split the chunk in half to make space for the new insertion + auto new_pos = m_data.insert(pos.m_outer + 1, Chunk{}); + auto prev = new_pos - 1; + auto to_move = max_size / 2; + new_pos->data.reserve(to_move); + new_pos->data.assign(prev->data.end() - to_move, prev->data.end()); + prev->data.resize(prev->data.size() - to_move); + + size_t moved_count = 0; + for (auto range : new_pos->data) + moved_count += range.second - range.first; + + prev->end = prev->data.back().second; + prev->count -= moved_count; + new_pos->begin = new_pos->data.front().first; + new_pos->end = new_pos->data.back().second; + new_pos->count = moved_count; + + if (offset >= to_move) { + pos.m_outer = new_pos; + offset -= to_move; + } + else { + pos.m_outer = prev; + } + pos.m_end = m_data.end(); + pos.m_inner = &pos.m_outer->data[offset]; + verify(); + return pos; +} + +ChunkedRangeVector::iterator ChunkedRangeVector::erase(iterator pos) +{ + auto offset = pos.offset(); + auto& chunk = *pos.m_outer; + chunk.count -= pos->second - pos->first; + chunk.data.erase(chunk.data.begin() + offset); + + if (chunk.data.size() == 0) { + pos.m_outer = m_data.erase(pos.m_outer); + pos.m_end = m_data.end(); + pos.m_inner = pos.m_outer == m_data.end() ? nullptr : &pos.m_outer->data.front(); + verify(); + return pos; + } + + chunk.begin = chunk.data.front().first; + chunk.end = chunk.data.back().second; + if (offset < chunk.data.size()) + pos.m_inner = &chunk.data[offset]; + else { + ++pos.m_outer; + pos.m_inner = pos.m_outer == pos.m_end ? nullptr : &pos.m_outer->data.front(); + } + + verify(); + return pos; +} + +void ChunkedRangeVector::verify() const noexcept +{ +#ifdef REALM_DEBUG + size_t prev_end = -1; + for (auto range : *this) { + REALM_ASSERT(range.first < range.second); + REALM_ASSERT(prev_end == size_t(-1) || range.first > prev_end); + prev_end = range.second; + } + + for (auto& chunk : m_data) { + REALM_ASSERT(!chunk.data.empty()); + REALM_ASSERT(chunk.data.front().first == chunk.begin); + REALM_ASSERT(chunk.data.back().second == chunk.end); + REALM_ASSERT(chunk.count <= chunk.end - chunk.begin); + size_t count = 0; + for (auto range : chunk.data) + count += range.second - range.first; + REALM_ASSERT(count == chunk.count); + } +#endif +} + +namespace { +class ChunkedRangeVectorBuilder { +public: + using value_type = std::pair; + + ChunkedRangeVectorBuilder(ChunkedRangeVector const& expected); + void push_back(size_t index); + void push_back(std::pair range); + std::vector finalize(); +private: + std::vector m_data; + size_t m_outer_pos = 0; +}; + +ChunkedRangeVectorBuilder::ChunkedRangeVectorBuilder(ChunkedRangeVector const& expected) +{ + size_t size = 0; + for (auto const& chunk : expected.m_data) + size += chunk.data.size(); + m_data.resize(size / ChunkedRangeVector::max_size + 1); + for (size_t i = 0; i < m_data.size() - 1; ++i) + m_data[i].data.reserve(ChunkedRangeVector::max_size); +} + +void ChunkedRangeVectorBuilder::push_back(size_t index) +{ + push_back({index, index + 1}); +} + +void ChunkedRangeVectorBuilder::push_back(std::pair range) +{ + auto& chunk = m_data[m_outer_pos]; + if (chunk.data.empty()) { + chunk.data.push_back(range); + chunk.count = range.second - range.first; + chunk.begin = range.first; + } + else if (range.first == chunk.data.back().second) { + chunk.data.back().second = range.second; + chunk.count += range.second - range.first; + } + else if (chunk.data.size() < ChunkedRangeVector::max_size) { + chunk.data.push_back(range); + chunk.count += range.second - range.first; + } + else { + chunk.end = chunk.data.back().second; + ++m_outer_pos; + if (m_outer_pos >= m_data.size()) + m_data.push_back({{range}, range.first, 0, 1}); + else { + auto& chunk = m_data[m_outer_pos]; + chunk.data.push_back(range); + chunk.begin = range.first; + chunk.count = range.second - range.first; + } + } +} + +std::vector ChunkedRangeVectorBuilder::finalize() +{ + if (!m_data.empty()) { + m_data.resize(m_outer_pos + 1); + if (m_data.back().data.empty()) + m_data.pop_back(); + else + m_data.back().end = m_data.back().data.back().second; + } + return std::move(m_data); +} +} + +IndexSet::IndexSet(std::initializer_list values) +{ + for (size_t v : values) + add(v); +} + +bool IndexSet::contains(size_t index) const +{ + auto it = const_cast(this)->find(index); + return it != end() && it->first <= index; +} + +size_t IndexSet::count(size_t start_index, size_t end_index) const +{ + auto it = const_cast(this)->find(start_index); + const auto end = this->end(); + if (it == end || it->first >= end_index) { + return 0; + } + if (it->second >= end_index) + return std::min(it->second, end_index) - std::max(it->first, start_index); + + size_t ret = 0; + + if (start_index > it->first || it.offset() != 0) { + // Start index is in the middle of a chunk, so start by counting the + // rest of that chunk + ret = it->second - std::max(it->first, start_index); + for (++it; it != end && it->second < end_index && it.offset() != 0; ++it) { + ret += it->second - it->first; + } + if (it != end && it->first < end_index && it.offset() != 0) + ret += end_index - it->first; + if (it == end || it->second >= end_index) + return ret; + } + + // Now count all complete chunks that fall within the range + while (it != end && it.outer()->end <= end_index) { + REALM_ASSERT_DEBUG(it.offset() == 0); + ret += it.outer()->count; + it.next_chunk(); + } + + // Cound all complete ranges within the last chunk + while (it != end && it->second <= end_index) { + ret += it->second - it->first; + ++it; + } + + // And finally add in the partial last range + if (it != end && it->first < end_index) + ret += end_index - it->first; + return ret; +} IndexSet::iterator IndexSet::find(size_t index) { - for (auto it = m_ranges.begin(), end = m_ranges.end(); it != end; ++it) { - if (it->second > index) - return it; - } - return m_ranges.end(); + return find(index, begin()); +} + +IndexSet::iterator IndexSet::find(size_t index, iterator begin) +{ + auto it = std::find_if(begin.outer(), m_data.end(), + [&](auto const& lft) { return lft.end > index; }); + if (it == m_data.end()) + return end(); + if (index < it->begin) + return iterator(it, m_data.end(), &it->data[0]); + auto inner_begin = it->data.begin(); + if (it == begin.outer()) + inner_begin += begin.offset(); + auto inner = std::lower_bound(inner_begin, it->data.end(), index, + [&](auto const& lft, auto) { return lft.second <= index; }); + REALM_ASSERT_DEBUG(inner != it->data.end()); + + return iterator(it, m_data.end(), &*inner); } void IndexSet::add(size_t index) @@ -34,60 +348,360 @@ void IndexSet::add(size_t index) do_add(find(index), index); } -void IndexSet::do_add(iterator it, size_t index) +void IndexSet::add(IndexSet const& other) { - bool more_before = it != m_ranges.begin(), valid = it != m_ranges.end(); - if (valid && it->first <= index && it->second > index) { - // index is already in set + auto it = begin(); + for (size_t index : other.as_indexes()) { + it = do_add(find(index, it), index); } - else if (more_before && (it - 1)->second == index) { - // index is immediately after an existing range - ++(it - 1)->second; +} - if (valid && (it - 1)->second == it->first) { - // index joins two existing ranges - (it - 1)->second = it->second; - m_ranges.erase(it); +size_t IndexSet::add_shifted(size_t index) +{ + iterator it = begin(), end = this->end(); + + // Shift for any complete chunks before the target + for (; it != end && it.outer()->end <= index; it.next_chunk()) + index += it.outer()->count; + + // And any ranges within the last partial chunk + for (; it != end && it->first <= index; ++it) + index += it->second - it->first; + + do_add(it, index); + return index; +} + +void IndexSet::add_shifted_by(IndexSet const& shifted_by, IndexSet const& values) +{ + if (values.empty()) + return; + +#ifdef REALM_DEBUG + size_t expected = std::distance(as_indexes().begin(), as_indexes().end()); + for (auto index : values.as_indexes()) { + if (!shifted_by.contains(index)) + ++expected; + } +#endif + + ChunkedRangeVectorBuilder builder(*this); + + auto old_it = cbegin(), old_end = cend(); + auto shift_it = shifted_by.cbegin(), shift_end = shifted_by.cend(); + + size_t skip_until = 0; + size_t old_shift = 0; + size_t new_shift = 0; + for (size_t index : values.as_indexes()) { + for (; shift_it != shift_end && shift_it->first <= index; ++shift_it) { + new_shift += shift_it->second - shift_it->first; + skip_until = shift_it->second; } + if (index < skip_until) + continue; + + for (; old_it != old_end && old_it->first <= index - new_shift + old_shift; ++old_it) { + for (size_t i = old_it->first; i < old_it->second; ++i) + builder.push_back(i); + old_shift += old_it->second - old_it->first; + } + + REALM_ASSERT(index >= new_shift); + builder.push_back(index - new_shift + old_shift); } - else if (valid && it->first == index + 1) { - // index is immediately before an existing range - --it->first; - } - else { - // index is not next to an existing range - m_ranges.insert(it, {index, index + 1}); - } + + copy(old_it, old_end, std::back_inserter(builder)); + m_data = builder.finalize(); + +#ifdef REALM_DEBUG + REALM_ASSERT((size_t)std::distance(as_indexes().begin(), as_indexes().end()) == expected); +#endif } void IndexSet::set(size_t len) { - m_ranges.clear(); + clear(); if (len) { - m_ranges.push_back({0, len}); + push_back({0, len}); } } -void IndexSet::insert_at(size_t index) +void IndexSet::insert_at(size_t index, size_t count) { + REALM_ASSERT(count > 0); + auto pos = find(index); - if (pos != m_ranges.end()) { - if (pos->first >= index) - ++pos->first; - ++pos->second; - for (auto it = pos + 1; it != m_ranges.end(); ++it) { - ++it->first; - ++it->second; + auto end = this->end(); + bool in_existing = false; + if (pos != end) { + if (pos->first <= index) { + in_existing = true; + pos.adjust(0, count); + } + else { + pos.shift(count); + } + for (auto it = std::next(pos); it != end; ++it) + it.shift(count); + } + if (!in_existing) { + for (size_t i = 0; i < count; ++i) + pos = std::next(do_add(pos, index + i)); + } + + verify(); +} + +void IndexSet::insert_at(IndexSet const& positions) +{ + if (positions.empty()) + return; + if (empty()) { + *this = positions; + return; + } + + IndexIterator begin1 = cbegin(), begin2 = positions.cbegin(); + IndexIterator end1 = cend(), end2 = positions.cend(); + + ChunkedRangeVectorBuilder builder(*this); + size_t shift = 0; + while (begin1 != end1 && begin2 != end2) { + if (*begin1 + shift < *begin2) { + builder.push_back(*begin1++ + shift); + } + else { + ++shift; + builder.push_back(*begin2++); } } - do_add(pos, index); + for (; begin1 != end1; ++begin1) + builder.push_back(*begin1 + shift); + for (; begin2 != end2; ++begin2) + builder.push_back(*begin2); + + m_data = builder.finalize(); } -void IndexSet::add_shifted(size_t index) +void IndexSet::shift_for_insert_at(size_t index, size_t count) { - auto it = m_ranges.begin(); - for (auto end = m_ranges.end(); it != end && it->first <= index; ++it) { - index += it->second - it->first; + REALM_ASSERT(count > 0); + + auto it = find(index); + if (it == end()) + return; + + for (auto pos = it, end = this->end(); pos != end; ++pos) + pos.shift(count); + + // If the range contained the insertion point, split the range and move + // the part of it before the insertion point back + if (it->first < index + count) { + auto old_second = it->second; + it.set(it->first - count, index); + insert(std::next(it), {index + count, old_second}); } - do_add(it, index); + verify(); +} + +void IndexSet::shift_for_insert_at(realm::IndexSet const& values) +{ + if (empty() || values.empty()) + return; + if (values.m_data.front().begin >= m_data.back().end) + return; + + IndexIterator begin1 = cbegin(), begin2 = values.cbegin(); + IndexIterator end1 = cend(), end2 = values.cend(); + + ChunkedRangeVectorBuilder builder(*this); + size_t shift = 0; + while (begin1 != end1 && begin2 != end2) { + if (*begin1 + shift < *begin2) { + builder.push_back(*begin1++ + shift); + } + else { + ++shift; + begin2++; + } + } + for (; begin1 != end1; ++begin1) + builder.push_back(*begin1 + shift); + + m_data = builder.finalize(); +} + +void IndexSet::erase_at(size_t index) +{ + auto it = find(index); + if (it != end()) + do_erase(it, index); +} + +void IndexSet::erase_at(IndexSet const& positions) +{ + if (empty() || positions.empty()) + return; + + ChunkedRangeVectorBuilder builder(*this); + + IndexIterator begin1 = cbegin(), begin2 = positions.cbegin(); + IndexIterator end1 = cend(), end2 = positions.cend(); + + size_t shift = 0; + while (begin1 != end1 && begin2 != end2) { + if (*begin1 < *begin2) { + builder.push_back(*begin1++ - shift); + } + else if (*begin1 == *begin2) { + ++shift; + ++begin1; + ++begin2; + } + else { + ++shift; + ++begin2; + } + } + for (; begin1 != end1; ++begin1) + builder.push_back(*begin1 - shift); + + m_data = builder.finalize(); +} + +size_t IndexSet::erase_or_unshift(size_t index) +{ + auto shifted = index; + iterator it = begin(), end = this->end(); + + // Shift for any complete chunks before the target + for (; it != end && it.outer()->end <= index; it.next_chunk()) + shifted -= it.outer()->count; + + // And any ranges within the last partial chunk + for (; it != end && it->second <= index; ++it) + shifted -= it->second - it->first; + + if (it == end) + return shifted; + + if (it->first <= index) + shifted = npos; + + do_erase(it, index); + + return shifted; +} + +void IndexSet::do_erase(iterator it, size_t index) +{ + if (it->first <= index) { + if (it->first + 1 == it->second) { + it = erase(it); + } + else { + it.adjust(0, -1); + ++it; + } + } + else if (it != begin() && std::prev(it)->second + 1 == it->first) { + std::prev(it).adjust(0, it->second - it->first); + it = erase(it); + } + + for (; it != end(); ++it) + it.shift(-1); +} + +IndexSet::iterator IndexSet::do_remove(iterator it, size_t begin, size_t end) +{ + for (it = find(begin, it); it != this->end() && it->first < end; it = find(begin, it)) { + // Trim off any part of the range to remove that's before the matching range + begin = std::max(it->first, begin); + + // If the matching range extends to both sides of the range to remove, + // split it on the range to remove + if (it->first < begin && it->second > end) { + auto old_second = it->second; + it.set(it->first, begin); + it = std::prev(insert(std::next(it), {end, old_second})); + } + // Range to delete now coverages (at least) one end of the matching range + else if (begin == it->first && end >= it->second) + it = erase(it); + else if (begin == it->first) + it.set(end, it->second); + else + it.set(it->first, begin); + } + return it; +} + +void IndexSet::remove(size_t index, size_t count) +{ + do_remove(find(index), index, index + count); +} + +void IndexSet::remove(realm::IndexSet const& values) +{ + auto it = begin(); + for (auto range : values) { + it = do_remove(it, range.first, range.second); + if (it == end()) + return; + } +} + +size_t IndexSet::shift(size_t index) const +{ + // FIXME: optimize + for (auto range : *this) { + if (range.first > index) + break; + index += range.second - range.first; + } + return index; +} + +size_t IndexSet::unshift(size_t index) const +{ + REALM_ASSERT_DEBUG(!contains(index)); + return index - count(0, index); +} + +void IndexSet::clear() +{ + m_data.clear(); +} + +IndexSet::iterator IndexSet::do_add(iterator it, size_t index) +{ + verify(); + bool more_before = it != begin(), valid = it != end(); + REALM_ASSERT(!more_before || index >= std::prev(it)->second); + if (valid && it->first <= index && it->second > index) { + // index is already in set + return it; + } + if (more_before && std::prev(it)->second == index) { + auto prev = std::prev(it); + // index is immediately after an existing range + prev.adjust(0, 1); + + if (valid && prev->second == it->first) { + // index joins two existing ranges + prev.adjust(0, it->second - it->first); + return std::prev(erase(it)); + } + return prev; + } + if (valid && it->first == index + 1) { + // index is immediately before an existing range + it.adjust(-1, 0); + return it; + } + + // index is not next to an existing range + return insert(it, {index, index + 1}); } diff --git a/src/index_set.hpp b/src/index_set.hpp index 9988b10f..0cf00fd9 100644 --- a/src/index_set.hpp +++ b/src/index_set.hpp @@ -19,24 +19,142 @@ #ifndef REALM_INDEX_SET_HPP #define REALM_INDEX_SET_HPP +#include #include +#include +#include +#include +#include #include -#include namespace realm { -class IndexSet { -public: - using value_type = std::pair; - using iterator = std::vector::iterator; - using const_iterator = std::vector::const_iterator; +namespace _impl { +template +class MutableChunkedRangeVectorIterator; - const_iterator begin() const { return m_ranges.begin(); } - const_iterator end() const { return m_ranges.end(); } - bool empty() const { return m_ranges.empty(); } - size_t size() const { return m_ranges.size(); } +// An iterator for ChunkedRangeVector, templated on the vector iterator/const_iterator +template +class ChunkedRangeVectorIterator { +public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = typename std::remove_referencedata.begin())>::type; + using difference_type = ptrdiff_t; + using pointer = const value_type*; + using reference = const value_type&; + + ChunkedRangeVectorIterator(OuterIterator outer, OuterIterator end, value_type* inner) + : m_outer(outer), m_end(end), m_inner(inner) { } + + reference operator*() const { return *m_inner; } + pointer operator->() const { return m_inner; } + + template bool operator==(Other const& it) const; + template bool operator!=(Other const& it) const; + + ChunkedRangeVectorIterator& operator++(); + ChunkedRangeVectorIterator operator++(int); + + ChunkedRangeVectorIterator& operator--(); + ChunkedRangeVectorIterator operator--(int); + + // Advance directly to the next outer block + void next_chunk(); + + OuterIterator outer() const { return m_outer; } + size_t offset() const { return m_inner - &m_outer->data[0]; } + +private: + OuterIterator m_outer; + OuterIterator m_end; + value_type* m_inner; + friend struct ChunkedRangeVector; + friend class MutableChunkedRangeVectorIterator; +}; + +// A mutable iterator that adds some invariant-preserving mutation methods +template +class MutableChunkedRangeVectorIterator : public ChunkedRangeVectorIterator { +public: + using ChunkedRangeVectorIterator::ChunkedRangeVectorIterator; + + // Set this iterator to the given range and update the parent if needed + void set(size_t begin, size_t end); + // Adjust the begin and end of this iterator by the given amounts and + // update the parent if needed + void adjust(ptrdiff_t front, ptrdiff_t back); + // Shift this iterator by the given amount and update the parent if needed + void shift(ptrdiff_t distance); +}; + +// A vector which stores ranges in chunks with a maximum size +struct ChunkedRangeVector { + struct Chunk { + std::vector> data; + size_t begin; + size_t end; + size_t count; + }; + std::vector m_data; + + using value_type = std::pair; + using iterator = MutableChunkedRangeVectorIterator; + using const_iterator = ChunkedRangeVectorIterator; + +#ifdef REALM_DEBUG + static const size_t max_size = 4; +#else + static const size_t max_size = 4096 / sizeof(std::pair); +#endif + + iterator begin() { return empty() ? end() : iterator(m_data.begin(), m_data.end(), &m_data[0].data[0]); } + iterator end() { return iterator(m_data.end(), m_data.end(), nullptr); } + const_iterator begin() const { return cbegin(); } + const_iterator end() const { return cend(); } + const_iterator cbegin() const { return empty() ? cend() : const_iterator(m_data.cbegin(), m_data.end(), &m_data[0].data[0]); } + const_iterator cend() const { return const_iterator(m_data.end(), m_data.end(), nullptr); } + + bool empty() const noexcept { return m_data.empty(); } + + iterator insert(iterator pos, value_type value); + iterator erase(iterator pos); + void push_back(value_type value); + iterator ensure_space(iterator pos); + + void verify() const noexcept; +}; +} // namespace _impl + +class IndexSet : private _impl::ChunkedRangeVector { +public: + static const size_t npos = -1; + + using ChunkedRangeVector::value_type; + using ChunkedRangeVector::iterator; + using ChunkedRangeVector::const_iterator; + using ChunkedRangeVector::begin; + using ChunkedRangeVector::end; + using ChunkedRangeVector::empty; + using ChunkedRangeVector::verify; + + IndexSet() = default; + IndexSet(std::initializer_list); + + // Check if the index set contains the given index + bool contains(size_t index) const; + + // Counts the number of indices in the set in the given range + size_t count(size_t start_index=0, size_t end_index=-1) const; // Add an index to the set, doing nothing if it's already present void add(size_t index); + void add(IndexSet const& is); + + // Add an index which has had all of the ranges in the set before it removed + // Returns the unshifted index + size_t add_shifted(size_t index); + // Add indexes which have had the ranges in `shifted_by` added and the ranges + // in the current set removed + void add_shifted_by(IndexSet const& shifted_by, IndexSet const& values); // Remove all indexes from the set and then add a single range starting from // zero with the given length @@ -44,21 +162,165 @@ public: // Insert an index at the given position, shifting existing indexes at or // after that point back by one - void insert_at(size_t index); + void insert_at(size_t index, size_t count=1); + void insert_at(IndexSet const&); - // Add an index which has had all of the ranges in the set before it removed - void add_shifted(size_t index); + // Shift indexes at or after the given point back by one + void shift_for_insert_at(size_t index, size_t count=1); + void shift_for_insert_at(IndexSet const&); + + // Delete an index at the given position, shifting indexes after that point + // forward by one + void erase_at(size_t index); + void erase_at(IndexSet const&); + + // If the given index is in the set remove it and return npos; otherwise unshift() it + size_t erase_or_unshift(size_t index); + + // Remove the indexes at the given index from the set, without shifting + void remove(size_t index, size_t count=1); + void remove(IndexSet const&); + + // Shift an index by inserting each of the indexes in this set + size_t shift(size_t index) const; + // Shift an index by deleting each of the indexes in this set + size_t unshift(size_t index) const; + + // Remove all indexes from the set + void clear(); + + // An iterator over the individual indices in the set rather than the ranges + class IndexIterator : public std::iterator { + public: + IndexIterator(IndexSet::const_iterator it) : m_iterator(it) { } + size_t operator*() const { return m_iterator->first + m_offset; } + bool operator==(IndexIterator const& it) const { return m_iterator == it.m_iterator; } + bool operator!=(IndexIterator const& it) const { return m_iterator != it.m_iterator; } + + IndexIterator& operator++() + { + ++m_offset; + if (m_iterator->first + m_offset == m_iterator->second) { + ++m_iterator; + m_offset = 0; + } + return *this; + } + + IndexIterator operator++(int) + { + auto value = *this; + ++*this; + return value; + } + + private: + IndexSet::const_iterator m_iterator; + size_t m_offset = 0; + }; + + class IndexIteratableAdaptor { + public: + using value_type = size_t; + using iterator = IndexIterator; + using const_iterator = iterator; + + const_iterator begin() const { return m_index_set.begin(); } + const_iterator end() const { return m_index_set.end(); } + + IndexIteratableAdaptor(IndexSet const& is) : m_index_set(is) { } + private: + IndexSet const& m_index_set; + }; + + IndexIteratableAdaptor as_indexes() const { return *this; } private: - std::vector m_ranges; - // Find the range which contains the index, or the first one after it if // none do iterator find(size_t index); + iterator find(size_t index, iterator it); // Insert the index before the given position, combining existing ranges as // applicable - void do_add(iterator pos, size_t index); + // returns inserted position + iterator do_add(iterator pos, size_t index); + void do_erase(iterator it, size_t index); + iterator do_remove(iterator it, size_t index, size_t count); + + void shift_until_end_by(iterator begin, ptrdiff_t shift); }; + +namespace util { +// This was added in C++14 but is missing from libstdc++ 4.9 +template +std::reverse_iterator make_reverse_iterator(Iterator it) +{ + return std::reverse_iterator(it); +} +} // namespace util + + +namespace _impl { +template +template +inline bool ChunkedRangeVectorIterator::operator==(OtherIterator const& it) const +{ + return m_outer == it.outer() && m_inner == it.operator->(); +} + +template +template +inline bool ChunkedRangeVectorIterator::operator!=(OtherIterator const& it) const +{ + return !(*this == it); +} + +template +inline ChunkedRangeVectorIterator& ChunkedRangeVectorIterator::operator++() +{ + ++m_inner; + if (offset() == m_outer->data.size()) + next_chunk(); + return *this; +} + +template +inline ChunkedRangeVectorIterator ChunkedRangeVectorIterator::operator++(int) +{ + auto value = *this; + ++*this; + return value; +} + +template +inline ChunkedRangeVectorIterator& ChunkedRangeVectorIterator::operator--() +{ + if (!m_inner || m_inner == &m_outer->data.front()) { + --m_outer; + m_inner = &m_outer->data.back(); + } + else { + --m_inner; + } + return *this; +} + +template +inline ChunkedRangeVectorIterator ChunkedRangeVectorIterator::operator--(int) +{ + auto value = *this; + --*this; + return value; +} + +template +inline void ChunkedRangeVectorIterator::next_chunk() +{ + ++m_outer; + m_inner = m_outer != m_end ? &m_outer->data[0] : nullptr; +} +} // namespace _impl + } // namespace realm #endif // REALM_INDEX_SET_HPP diff --git a/src/list.cpp b/src/list.cpp index 78464125..fec20772 100644 --- a/src/list.cpp +++ b/src/list.cpp @@ -17,15 +17,25 @@ //////////////////////////////////////////////////////////////////////////// #include "list.hpp" + +#include "impl/list_notifier.hpp" +#include "impl/realm_coordinator.hpp" #include "results.hpp" +#include #include #include using namespace realm; +using namespace realm::_impl; List::List() noexcept = default; -List::~List() = default; +List::~List() +{ + if (m_notifier) { + m_notifier->unregister(); + } +} List::List(std::shared_ptr r, const ObjectSchema& s, LinkViewRef l) noexcept : m_realm(std::move(r)) @@ -151,7 +161,14 @@ void List::delete_all() Results List::sort(SortOrder order) { - return Results(m_realm, *m_object_schema, get_query(), std::move(order)); + verify_attached(); + return Results(m_realm, *m_object_schema, m_link_view, util::none, std::move(order)); +} + +Results List::filter(Query q) +{ + verify_attached(); + return Results(m_realm, *m_object_schema, m_link_view, get_query().and_query(std::move(q))); } // These definitions rely on that LinkViews are interned by core @@ -166,3 +183,13 @@ size_t hash::operator()(realm::List const& list) const return std::hash()(list.m_link_view.get()); } } + +NotificationToken List::add_notification_callback(CollectionChangeCallback cb) +{ + verify_attached(); + if (!m_notifier) { + m_notifier = std::make_shared(m_link_view, m_realm); + RealmCoordinator::register_notifier(m_notifier); + } + return {m_notifier, m_notifier->add_callback(std::move(cb))}; +} diff --git a/src/list.hpp b/src/list.hpp index 464c3bcd..8abd601d 100644 --- a/src/list.hpp +++ b/src/list.hpp @@ -19,19 +19,27 @@ #ifndef REALM_LIST_HPP #define REALM_LIST_HPP -#include +#include "collection_notifications.hpp" +#include +#include + +#include #include namespace realm { -template class BasicRowExpr; using RowExpr = BasicRowExpr; class ObjectSchema; +class Query; class Realm; class Results; struct SortOrder; +namespace _impl { + class BackgroundCollection; +} + class List { public: List() noexcept; @@ -61,9 +69,12 @@ public: void delete_all(); Results sort(SortOrder order); + Results filter(Query q); bool operator==(List const& rgt) const noexcept; + NotificationToken add_notification_callback(CollectionChangeCallback cb); + // These are implemented in object_accessor.hpp template void add(ContextType ctx, ValueType value); @@ -78,6 +89,7 @@ private: std::shared_ptr m_realm; const ObjectSchema* m_object_schema; LinkViewRef m_link_view; + std::shared_ptr<_impl::CollectionNotifier> m_notifier; void verify_valid_row(size_t row_ndx, bool insertion = false) const; diff --git a/src/object_accessor.hpp b/src/object_accessor.hpp index 41757e5f..c20282b6 100644 --- a/src/object_accessor.hpp +++ b/src/object_accessor.hpp @@ -26,6 +26,7 @@ #include "shared_realm.hpp" #include +#include namespace realm { @@ -154,7 +155,7 @@ namespace realm { "Setting invalid property '" + prop_name + "' on object '" + m_object_schema->name + "'."); } set_property_value_impl(ctx, *prop, value, try_update); - }; + } template inline ValueType Object::get_property_value(ContextType ctx, std::string prop_name) @@ -165,7 +166,7 @@ namespace realm { "Getting invalid property '" + prop_name + "' on object '" + m_object_schema->name + "'."); } return get_property_value_impl(ctx, *prop); - }; + } template inline void Object::set_property_value_impl(ContextType ctx, const Property &property, ValueType value, bool try_update) @@ -180,7 +181,7 @@ namespace realm { size_t column = property.table_column; if (property.is_nullable && Accessor::is_null(ctx, value)) { - if (property.type == PropertyTypeObject) { + if (property.type == PropertyType::Object) { m_row.nullify_link(column); } else { @@ -190,33 +191,33 @@ namespace realm { } switch (property.type) { - case PropertyTypeBool: + case PropertyType::Bool: m_row.set_bool(column, Accessor::to_bool(ctx, value)); break; - case PropertyTypeInt: + case PropertyType::Int: m_row.set_int(column, Accessor::to_long(ctx, value)); break; - case PropertyTypeFloat: + case PropertyType::Float: m_row.set_float(column, Accessor::to_float(ctx, value)); break; - case PropertyTypeDouble: + case PropertyType::Double: m_row.set_double(column, Accessor::to_double(ctx, value)); break; - case PropertyTypeString: { + case PropertyType::String: { auto string_value = Accessor::to_string(ctx, value); m_row.set_string(column, string_value); break; - } - case PropertyTypeData: + } + case PropertyType::Data: m_row.set_binary(column, BinaryData(Accessor::to_binary(ctx, value))); break; - case PropertyTypeAny: + case PropertyType::Any: m_row.set_mixed(column, Accessor::to_mixed(ctx, value)); break; - case PropertyTypeDate: + case PropertyType::Date: m_row.set_timestamp(column, Accessor::to_timestamp(ctx, value)); break; - case PropertyTypeObject: { + case PropertyType::Object: { if (Accessor::is_null(ctx, value)) { m_row.nullify_link(column); } @@ -225,7 +226,7 @@ namespace realm { } break; } - case PropertyTypeArray: { + case PropertyType::Array: { realm::LinkViewRef link_view = m_row.get_linklist(column); link_view->clear(); if (!Accessor::is_null(ctx, value)) { @@ -253,23 +254,23 @@ namespace realm { } switch (property.type) { - case PropertyTypeBool: + case PropertyType::Bool: return Accessor::from_bool(ctx, m_row.get_bool(column)); - case PropertyTypeInt: + case PropertyType::Int: return Accessor::from_long(ctx, m_row.get_int(column)); - case PropertyTypeFloat: + case PropertyType::Float: return Accessor::from_float(ctx, m_row.get_float(column)); - case PropertyTypeDouble: + case PropertyType::Double: return Accessor::from_double(ctx, m_row.get_double(column)); - case PropertyTypeString: + case PropertyType::String: return Accessor::from_string(ctx, m_row.get_string(column)); - case PropertyTypeData: + case PropertyType::Data: return Accessor::from_binary(ctx, m_row.get_binary(column)); - case PropertyTypeAny: + case PropertyType::Any: throw "Any not supported"; - case PropertyTypeDate: + case PropertyType::Date: return Accessor::from_timestamp(ctx, m_row.get_timestamp(column)); - case PropertyTypeObject: { + case PropertyType::Object: { auto linkObjectSchema = m_realm->config().schema->find(property.object_type); TableRef table = ObjectStore::table_for_object_type(m_realm->read_group(), linkObjectSchema->name); if (m_row.is_null_link(property.table_column)) { @@ -277,7 +278,7 @@ namespace realm { } return Accessor::from_object(ctx, std::move(Object(m_realm, *linkObjectSchema, table->get(m_row.get_link(column))))); } - case PropertyTypeArray: { + case PropertyType::Array: { auto arrayObjectSchema = m_realm->config().schema->find(property.object_type); return Accessor::from_list(ctx, std::move(List(m_realm, *arrayObjectSchema, static_cast(m_row.get_linklist(column))))); } @@ -303,7 +304,7 @@ namespace realm { if (primary_prop) { // search for existing object based on primary key type ValueType primary_value = Accessor::dict_value_for_key(ctx, value, object_schema.primary_key); - if (primary_prop->type == PropertyTypeString) { + if (primary_prop->type == PropertyType::String) { auto primary_string = Accessor::to_string(ctx, primary_value); row_index = table->find_first_string(primary_prop->table_column, primary_string); } @@ -335,7 +336,7 @@ namespace realm { if (Accessor::has_default_value_for_property(ctx, realm.get(), object_schema, prop.name)) { object.set_property_value_impl(ctx, prop, Accessor::default_value_for_property(ctx, realm.get(), object_schema, prop.name), try_update); } - else if (prop.is_nullable || prop.type == PropertyTypeArray) { + else if (prop.is_nullable || prop.type == PropertyType::Array) { object.set_property_value_impl(ctx, prop, Accessor::null_value(ctx), try_update); } else { diff --git a/src/object_schema.cpp b/src/object_schema.cpp index f6d7ae2f..f794c34a 100644 --- a/src/object_schema.cpp +++ b/src/object_schema.cpp @@ -27,7 +27,7 @@ using namespace realm; #define ASSERT_PROPERTY_TYPE_VALUE(property, type) \ - static_assert(static_cast(PropertyType##property) == type_##type, \ + static_assert(static_cast(PropertyType::property) == type_##type, \ "PropertyType and DataType must have the same values") ASSERT_PROPERTY_TYPE_VALUE(Int, Int); @@ -40,6 +40,7 @@ ASSERT_PROPERTY_TYPE_VALUE(Any, Mixed); ASSERT_PROPERTY_TYPE_VALUE(Object, Link); ASSERT_PROPERTY_TYPE_VALUE(Array, LinkList); +ObjectSchema::ObjectSchema() = default; ObjectSchema::~ObjectSchema() = default; ObjectSchema::ObjectSchema(std::string name, std::string primary_key, std::initializer_list properties) @@ -61,9 +62,9 @@ ObjectSchema::ObjectSchema(const Group *group, const std::string &name) : name(n property.type = (PropertyType)table->get_column_type(col); property.is_indexed = table->has_search_index(col); property.is_primary = false; - property.is_nullable = table->is_nullable(col) || property.type == PropertyTypeObject; + property.is_nullable = table->is_nullable(col) || property.type == PropertyType::Object; property.table_column = col; - if (property.type == PropertyTypeObject || property.type == PropertyTypeArray) { + if (property.type == PropertyType::Object || property.type == PropertyType::Array) { // set link type for objects and arrays ConstTableRef linkTable = table->get_link_target(col); property.object_type = ObjectStore::object_type_for_table_name(linkTable->get_name().data()); diff --git a/src/object_schema.hpp b/src/object_schema.hpp index 10a2e555..f18a5a2d 100644 --- a/src/object_schema.hpp +++ b/src/object_schema.hpp @@ -25,35 +25,35 @@ #include namespace realm { - class Group; - struct Property; +class Group; +struct Property; - class ObjectSchema { - public: - ObjectSchema() = default; - ObjectSchema(std::string name, std::string primary_key, std::initializer_list properties); - ~ObjectSchema(); +class ObjectSchema { +public: + ObjectSchema(); + ObjectSchema(std::string name, std::string primary_key, std::initializer_list properties); + ~ObjectSchema(); - // create object schema from existing table - // if no table is provided it is looked up in the group - ObjectSchema(const Group *group, const std::string &name); + // create object schema from existing table + // if no table is provided it is looked up in the group + ObjectSchema(const Group *group, const std::string &name); - std::string name; - std::vector properties; - std::string primary_key; + std::string name; + std::vector properties; + std::string primary_key; - Property *property_for_name(StringData name); - const Property *property_for_name(StringData name) const; - Property *primary_key_property() { - return property_for_name(primary_key); - } - const Property *primary_key_property() const { - return property_for_name(primary_key); - } + Property *property_for_name(StringData name); + const Property *property_for_name(StringData name) const; + Property *primary_key_property() { + return property_for_name(primary_key); + } + const Property *primary_key_property() const { + return property_for_name(primary_key); + } - private: - void set_primary_key_property(); - }; +private: + void set_primary_key_property(); +}; } #endif /* defined(REALM_OBJECT_SCHEMA_HPP) */ diff --git a/src/object_store.cpp b/src/object_store.cpp index 7be70789..f777faa8 100644 --- a/src/object_store.cpp +++ b/src/object_store.cpp @@ -171,7 +171,7 @@ void ObjectStore::verify_schema(Schema const& actual_schema, Schema& target_sche errors.insert(errors.end(), more_errors.begin(), more_errors.end()); } if (errors.size()) { - throw SchemaUpdateValidationException(errors); + throw SchemaMismatchException(errors); } } @@ -224,25 +224,25 @@ static void copy_property_values(const Property& old_property, const Property& n static void copy_property_values(const Property& source, const Property& destination, Table& table) { switch (destination.type) { - case PropertyTypeInt: + case PropertyType::Int: copy_property_values(source, destination, table, &Table::get_int, &Table::set_int); break; - case PropertyTypeBool: + case PropertyType::Bool: copy_property_values(source, destination, table, &Table::get_bool, &Table::set_bool); break; - case PropertyTypeFloat: + case PropertyType::Float: copy_property_values(source, destination, table, &Table::get_float, &Table::set_float); break; - case PropertyTypeDouble: + case PropertyType::Double: copy_property_values(source, destination, table, &Table::get_double, &Table::set_double); break; - case PropertyTypeString: + case PropertyType::String: copy_property_values(source, destination, table, &Table::get_string, &Table::set_string); break; - case PropertyTypeData: + case PropertyType::Data: copy_property_values(source, destination, table, &Table::get_binary, &Table::set_binary); break; - case PropertyTypeDate: + case PropertyType::Date: copy_property_values(source, destination, table, &Table::get_timestamp, &Table::set_timestamp); break; default: @@ -319,8 +319,8 @@ void ObjectStore::create_tables(Group *group, Schema &target_schema, bool update if (!current_prop || current_prop->table_column == npos) { switch (target_prop.type) { // for objects and arrays, we have to specify target table - case PropertyTypeObject: - case PropertyTypeArray: { + case PropertyType::Object: + case PropertyType::Array: { TableRef link_table = ObjectStore::table_for_object_type(group, target_prop.object_type); REALM_ASSERT(link_table); target_prop.table_column = table->add_column_link(DataType(target_prop.type), target_prop.name, *link_table); @@ -524,17 +524,16 @@ DuplicatePrimaryKeyValueException::DuplicatePrimaryKeyValueException(std::string SchemaValidationException::SchemaValidationException(std::vector const& errors) : m_validation_errors(errors) { - m_what ="The following errors were encountered during schema validation: "; + m_what = "Schema validation failed due to the following errors: "; for (auto const& error : errors) { m_what += std::string("\n- ") + error.what(); } } - -SchemaUpdateValidationException::SchemaUpdateValidationException(std::vector const& errors) : - SchemaValidationException(errors) +SchemaMismatchException::SchemaMismatchException(std::vector const& errors) : +m_validation_errors(errors) { - m_what ="Migration is required due to the following errors: "; + m_what = "Migration is required due to the following errors: "; for (auto const& error : errors) { m_what += std::string("\n- ") + error.what(); } @@ -561,7 +560,7 @@ MissingPropertyException::MissingPropertyException(std::string const& object_typ InvalidNullabilityException::InvalidNullabilityException(std::string const& object_type, Property const& property) : ObjectSchemaPropertyException(object_type, property) { - if (property.type == PropertyTypeObject) { + if (property.type == PropertyType::Object) { m_what = "'Object' property '" + property.name + "' must be nullable."; } else { diff --git a/src/object_store.hpp b/src/object_store.hpp index e538fdc7..508b43be 100644 --- a/src/object_store.hpp +++ b/src/object_store.hpp @@ -22,14 +22,12 @@ #include "schema.hpp" #include "property.hpp" +#include + #include -#include -#include - -#include - namespace realm { + class Group; class ObjectSchemaValidationException; class Schema; @@ -165,6 +163,14 @@ namespace realm { SchemaUpdateValidationException(std::vector const& errors); }; + class SchemaMismatchException : public ObjectStoreException { + public: + SchemaMismatchException(std::vector const& errors); + std::vector const& validation_errors() const { return m_validation_errors; } + private: + std::vector m_validation_errors; + }; + class ObjectSchemaValidationException : public ObjectStoreException { public: ObjectSchemaValidationException(std::string const& object_type) : m_object_type(object_type) {} diff --git a/src/parser/query_builder.cpp b/src/parser/query_builder.cpp index f116b9b2..26f195ee 100644 --- a/src/parser/query_builder.cpp +++ b/src/parser/query_builder.cpp @@ -94,7 +94,7 @@ struct PropertyExpression KeyPath key_path = key_path_from_string(key_path_string); for (size_t index = 0; index < key_path.size(); index++) { if (prop) { - precondition(prop->type == PropertyTypeObject || prop->type == PropertyTypeArray, + precondition(prop->type == PropertyType::Object || prop->type == PropertyType::Array, (std::string)"Property '" + key_path[index] + "' is not a link in object of type '" + desc->name + "'"); indexes.push_back(prop->table_column); @@ -384,36 +384,36 @@ void do_add_comparison_to_query(Query &query, const Schema &schema, const Object { auto type = expr.prop->type; switch (type) { - case PropertyTypeBool: + case PropertyType::Bool: add_bool_constraint_to_query(query, cmp.op, value_of_type_for_query(expr.table_getter, lhs, args), value_of_type_for_query(expr.table_getter, rhs, args)); break; - case PropertyTypeDate: + case PropertyType::Date: add_numeric_constraint_to_query(query, cmp.op, value_of_type_for_query(expr.table_getter, lhs, args), value_of_type_for_query(expr.table_getter, rhs, args)); break; - case PropertyTypeDouble: + case PropertyType::Double: add_numeric_constraint_to_query(query, cmp.op, value_of_type_for_query(expr.table_getter, lhs, args), value_of_type_for_query(expr.table_getter, rhs, args)); break; - case PropertyTypeFloat: + case PropertyType::Float: add_numeric_constraint_to_query(query, cmp.op, value_of_type_for_query(expr.table_getter, lhs, args), value_of_type_for_query(expr.table_getter, rhs, args)); break; - case PropertyTypeInt: + case PropertyType::Int: add_numeric_constraint_to_query(query, cmp.op, value_of_type_for_query(expr.table_getter, lhs, args), value_of_type_for_query(expr.table_getter, rhs, args)); break; - case PropertyTypeString: + case PropertyType::String: add_string_constraint_to_query(query, cmp, value_of_type_for_query(expr.table_getter, lhs, args), value_of_type_for_query(expr.table_getter, rhs, args)); break; - case PropertyTypeData: + case PropertyType::Data: add_binary_constraint_to_query(query, cmp.op, value_of_type_for_query(expr.table_getter, lhs, args), value_of_type_for_query(expr.table_getter, rhs, args)); break; - case PropertyTypeObject: - case PropertyTypeArray: + case PropertyType::Object: + case PropertyType::Array: add_link_constraint_to_query(query, cmp.op, expr, link_argument(lhs, rhs, args)); break; default: { @@ -476,31 +476,31 @@ void do_add_null_comparison_to_query(Query &query, const Schema &schema, const O { auto type = expr.prop->type; switch (type) { - case PropertyTypeBool: + case realm::PropertyType::Bool: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeDate: + case realm::PropertyType::Date: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeDouble: + case realm::PropertyType::Double: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeFloat: + case realm::PropertyType::Float: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeInt: + case realm::PropertyType::Int: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeString: + case realm::PropertyType::String: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeData: + case realm::PropertyType::Data: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeObject: + case realm::PropertyType::Object: do_add_null_comparison_to_query(query, cmp.op, expr, args); break; - case PropertyTypeArray: + case realm::PropertyType::Array: throw std::runtime_error((std::string)"Comparing Lists to 'null' is not supported"); break; default: { diff --git a/src/parser/query_builder.hpp b/src/parser/query_builder.hpp index bccc2e13..f9dba1bd 100644 --- a/src/parser/query_builder.hpp +++ b/src/parser/query_builder.hpp @@ -50,7 +50,7 @@ class Arguments { template class ArgumentConverter : public Arguments { public: - ArgumentConverter(ContextType context, std::vector arguments) : m_arguments(arguments), m_ctx(context) {}; + ArgumentConverter(ContextType context, std::vector arguments) : m_arguments(arguments), m_ctx(context) {} using Accessor = realm::NativeAccessor; virtual bool bool_for_argument(size_t argument_index) { return Accessor::to_bool(m_ctx, argument_at(argument_index)); } diff --git a/src/property.hpp b/src/property.hpp index 25a3bda7..edd22a75 100644 --- a/src/property.hpp +++ b/src/property.hpp @@ -22,17 +22,17 @@ #include namespace realm { - enum PropertyType { - PropertyTypeInt = 0, - PropertyTypeBool = 1, - PropertyTypeFloat = 9, - PropertyTypeDouble = 10, - PropertyTypeString = 2, - PropertyTypeData = 4, - PropertyTypeAny = 6, // deprecated and will be removed in the future - PropertyTypeDate = 8, - PropertyTypeObject = 12, - PropertyTypeArray = 13, + enum class PropertyType { + Int = 0, + Bool = 1, + Float = 9, + Double = 10, + String = 2, + Data = 4, + Any = 6, // deprecated and will be removed in the future + Date = 8, + Object = 12, + Array = 13, }; struct Property { @@ -47,35 +47,53 @@ namespace realm { bool requires_index() const { return is_primary || is_indexed; } bool is_indexable() const { - return type == PropertyTypeInt - || type == PropertyTypeBool - || type == PropertyTypeDate - || type == PropertyTypeString; + return type == PropertyType::Int + || type == PropertyType::Bool + || type == PropertyType::Date + || type == PropertyType::String; } + +#if __GNUC__ < 5 + // GCC 4.9 does not support C++14 braced-init with NSDMIs + Property(std::string name="", PropertyType type=PropertyType::Int, std::string object_type="", + bool is_primary=false, bool is_indexed=false, bool is_nullable=false) + : name(std::move(name)) + , type(type) + , object_type(std::move(object_type)) + , is_primary(is_primary) + , is_indexed(is_indexed) + , is_nullable(is_nullable) + { + } +#endif }; static inline const char *string_for_property_type(PropertyType type) { switch (type) { - case PropertyTypeString: + case PropertyType::String: return "string"; - case PropertyTypeInt: + case PropertyType::Int: return "int"; - case PropertyTypeBool: + case PropertyType::Bool: return "bool"; - case PropertyTypeDate: + case PropertyType::Date: return "date"; - case PropertyTypeData: + case PropertyType::Data: return "data"; - case PropertyTypeDouble: + case PropertyType::Double: return "double"; - case PropertyTypeFloat: + case PropertyType::Float: return "float"; - case PropertyTypeAny: + case PropertyType::Any: return "any"; - case PropertyTypeObject: + case PropertyType::Object: return "object"; - case PropertyTypeArray: + case PropertyType::Array: return "array"; +#if __GNUC__ + default: + __builtin_unreachable(); +#endif } } } diff --git a/src/results.cpp b/src/results.cpp index 540aac54..39621e8d 100644 --- a/src/results.cpp +++ b/src/results.cpp @@ -18,8 +18,8 @@ #include "results.hpp" -#include "impl/async_query.hpp" #include "impl/realm_coordinator.hpp" +#include "impl/results_notifier.hpp" #include "object_store.hpp" #include @@ -46,6 +46,7 @@ Results::Results(SharedRealm r, const ObjectSchema &o, Query q, SortOrder s) , m_sort(std::move(s)) , m_mode(Mode::Query) { + REALM_ASSERT(m_sort.column_indices.size() == m_sort.ascending.size()); } Results::Results(SharedRealm r, const ObjectSchema &o, Table& table) @@ -56,10 +57,36 @@ Results::Results(SharedRealm r, const ObjectSchema &o, Table& table) { } +Results::Results(SharedRealm r, const ObjectSchema& o, LinkViewRef lv, util::Optional q, SortOrder s) +: m_realm(std::move(r)) +, m_object_schema(&o) +, m_link_view(lv) +, m_table(&lv->get_target_table()) +, m_sort(std::move(s)) +, m_mode(Mode::LinkView) +{ + REALM_ASSERT(m_sort.column_indices.size() == m_sort.ascending.size()); + if (q) { + m_query = std::move(*q); + m_mode = Mode::Query; + } +} + +Results::Results(SharedRealm r, const ObjectSchema& o, TableView tv, SortOrder s) +: m_realm(std::move(r)) +, m_object_schema(&o) +, m_table_view(std::move(tv)) +, m_table(&m_table_view.get_parent()) +, m_sort(std::move(s)) +, m_mode(Mode::TableView) +{ + REALM_ASSERT(m_sort.column_indices.size() == m_sort.ascending.size()); +} + Results::~Results() { - if (m_background_query) { - m_background_query->unregister(); + if (m_notifier) { + m_notifier->unregister(); } } @@ -69,7 +96,9 @@ void Results::validate_read() const m_realm->verify_thread(); if (m_table && !m_table->is_attached()) throw InvalidatedException(); - if (m_mode == Mode::TableView && !m_table_view.is_attached()) + if (m_mode == Mode::TableView && (!m_table_view.is_attached() || m_table_view.depends_on_deleted_object())) + throw InvalidatedException(); + if (m_mode == Mode::LinkView && !m_link_view->is_attached()) throw InvalidatedException(); } @@ -97,9 +126,12 @@ size_t Results::size() { validate_read(); switch (m_mode) { - case Mode::Empty: return 0; - case Mode::Table: return m_table->size(); - case Mode::Query: return m_query.count(); + case Mode::Empty: return 0; + case Mode::Table: return m_table->size(); + case Mode::LinkView: return m_link_view->size(); + case Mode::Query: + m_query.sync_view_if_needed(); + return m_query.count(); case Mode::TableView: update_tableview(); return m_table_view.size(); @@ -121,12 +153,21 @@ RowExpr Results::get(size_t row_ndx) if (row_ndx < m_table->size()) return m_table->get(row_ndx); break; + case Mode::LinkView: + if (update_linkview()) { + if (row_ndx < m_link_view->size()) + return m_link_view->get(row_ndx); + break; + } + REALM_FALLTHROUGH; case Mode::Query: case Mode::TableView: update_tableview(); - if (row_ndx < m_table_view.size()) - return (!m_live && !m_table_view.is_row_attached(row_ndx)) ? RowExpr() : m_table_view.get(row_ndx); - break; + if (row_ndx >= m_table_view.size()) + break; + if (!m_live && !m_table_view.is_row_attached(row_ndx)) + return {}; + return m_table_view.get(row_ndx); } throw OutOfBoundsIndexException{row_ndx, size()}; @@ -140,6 +181,10 @@ util::Optional Results::first() return none; case Mode::Table: return m_table->size() == 0 ? util::none : util::make_optional(m_table->front()); + case Mode::LinkView: + if (update_linkview()) + return m_link_view->size() == 0 ? util::none : util::make_optional(m_link_view->get(0)); + REALM_FALLTHROUGH; case Mode::Query: case Mode::TableView: update_tableview(); @@ -156,6 +201,10 @@ util::Optional Results::last() return none; case Mode::Table: return m_table->size() == 0 ? util::none : util::make_optional(m_table->back()); + case Mode::LinkView: + if (update_linkview()) + return m_link_view->size() == 0 ? util::none : util::make_optional(m_link_view->get(m_link_view->size() - 1)); + REALM_FALLTHROUGH; case Mode::Query: case Mode::TableView: update_tableview(); @@ -164,17 +213,30 @@ util::Optional Results::last() REALM_UNREACHABLE(); } +bool Results::update_linkview() +{ + if (m_sort) { + m_query = get_query(); + m_mode = Mode::Query; + update_tableview(); + return false; + } + return true; +} + void Results::update_tableview() { validate_read(); switch (m_mode) { case Mode::Empty: case Mode::Table: + case Mode::LinkView: return; case Mode::Query: + m_query.sync_view_if_needed(); m_table_view = m_query.find_all(); if (m_sort) { - m_table_view.sort(m_sort.columnIndices, m_sort.ascending); + m_table_view.sort(m_sort.column_indices, m_sort.ascending); } m_mode = Mode::TableView; break; @@ -182,9 +244,9 @@ void Results::update_tableview() if (!m_live) { return; } - if (!m_background_query && !m_realm->is_in_transaction() && m_realm->can_deliver_notifications()) { - m_background_query = std::make_shared<_impl::AsyncQuery>(*this); - _impl::RealmCoordinator::register_query(m_background_query); + if (!m_notifier && !m_realm->is_in_transaction() && m_realm->can_deliver_notifications()) { + m_notifier = std::make_shared<_impl::ResultsNotifier>(*this); + _impl::RealmCoordinator::register_notifier(m_notifier); } m_has_used_table_view = true; m_table_view.sync_if_needed(); @@ -215,6 +277,10 @@ size_t Results::index_of(size_t row_ndx) return not_found; case Mode::Table: return row_ndx; + case Mode::LinkView: + if (update_linkview()) + return m_link_view->find(row_ndx); + REALM_FALLTHROUGH; case Mode::Query: case Mode::TableView: update_tableview(); @@ -242,6 +308,10 @@ util::Optional Results::aggregate(size_t column, bool return_none_for_emp if (return_none_for_empty && m_table->size() == 0) return none; return util::Optional(getter(*m_table)); + case Mode::LinkView: + m_query = this->get_query(); + m_mode = Mode::Query; + REALM_FALLTHROUGH; case Mode::Query: case Mode::TableView: this->update_tableview(); @@ -316,6 +386,10 @@ void Results::clear() update_tableview(); m_table_view.clear(RemoveMode::unordered); break; + case Mode::LinkView: + validate_write(); + m_link_view->remove_all_target_rows(); + break; } } @@ -326,8 +400,21 @@ Query Results::get_query() const case Mode::Empty: case Mode::Query: return m_query; - case Mode::TableView: - return m_table_view.get_query(); + case Mode::TableView: { + // A TableView has an associated Query if it was produced by Query::find_all. This is indicated + // by TableView::get_query returning a Query with a non-null table. + Query query = m_table_view.get_query(); + if (query.get_table()) { + return query; + } + + // The TableView has no associated query so create one with no conditions that is restricted + // to the rows in the TableView. + m_table_view.sync_if_needed(); + return Query(*m_table, std::make_unique(m_table_view)); + } + case Mode::LinkView: + return m_table->where(m_link_view); case Mode::Table: return m_table->where(); } @@ -340,6 +427,10 @@ TableView Results::get_tableview() switch (m_mode) { case Mode::Empty: return {}; + case Mode::LinkView: + if (update_linkview()) + return m_table->where(m_link_view).find_all(); + REALM_FALLTHROUGH; case Mode::Query: case Mode::TableView: update_tableview(); @@ -352,15 +443,16 @@ TableView Results::get_tableview() Results Results::sort(realm::SortOrder&& sort) const { - return Results(m_realm, get_object_schema(), get_query(), std::move(sort)); + REALM_ASSERT(sort.column_indices.size() == sort.ascending.size()); + return Results(m_realm, *m_object_schema, get_query(), std::move(sort)); } Results Results::filter(Query&& q) const { - return Results(m_realm, get_object_schema(), get_query().and_query(std::move(q)), get_sort()); + return Results(m_realm, *m_object_schema, get_query().and_query(std::move(q)), m_sort); } -AsyncQueryCancelationToken Results::async(std::function target) +void Results::prepare_async() { if (m_realm->config().read_only) { throw InvalidTransactionException("Cannot create asynchronous query for read-only Realms"); @@ -369,11 +461,38 @@ AsyncQueryCancelationToken Results::async(std::function(*this); - _impl::RealmCoordinator::register_query(m_background_query); + if (!m_notifier) { + m_notifier = std::make_shared<_impl::ResultsNotifier>(*this); + _impl::RealmCoordinator::register_notifier(m_notifier); + } +} + +NotificationToken Results::async(std::function target) +{ + prepare_async(); + auto wrap = [=](CollectionChangeSet, std::exception_ptr e) { target(e); }; + return {m_notifier, m_notifier->add_callback(wrap)}; +} + +NotificationToken Results::add_notification_callback(CollectionChangeCallback cb) +{ + prepare_async(); + return {m_notifier, m_notifier->add_callback(std::move(cb))}; +} + +bool Results::is_in_table_order() const +{ + switch (m_mode) { + case Mode::Empty: + case Mode::Table: + return true; + case Mode::LinkView: + return false; + case Mode::Query: + return m_query.produces_results_in_table_order() && !m_sort; + case Mode::TableView: + return m_table_view.is_in_table_order(); } - return {m_background_query, m_background_query->add_callback(std::move(target))}; } void Results::Internal::set_table_view(Results& results, realm::TableView &&tv) @@ -388,6 +507,7 @@ void Results::Internal::set_table_view(Results& results, realm::TableView &&tv) results.m_mode = Mode::TableView; results.m_has_used_table_view = false; REALM_ASSERT(results.m_table_view.is_in_sync()); + REALM_ASSERT(results.m_table_view.is_attached()); } Results::UnsupportedColumnTypeException::UnsupportedColumnTypeException(size_t column, const Table* table) @@ -397,35 +517,3 @@ Results::UnsupportedColumnTypeException::UnsupportedColumnTypeException(size_t c , column_type(table->get_column_type(column)) { } - -AsyncQueryCancelationToken::AsyncQueryCancelationToken(std::shared_ptr<_impl::AsyncQuery> query, size_t token) -: m_query(std::move(query)), m_token(token) -{ -} - -AsyncQueryCancelationToken::~AsyncQueryCancelationToken() -{ - // m_query itself (and not just the pointed-to thing) needs to be accessed - // atomically to ensure that there are no data races when the token is - // destroyed after being modified on a different thread. - // This is needed despite the token not being thread-safe in general as - // users find it very surpringing for obj-c objects to care about what - // thread they are deallocated on. - if (auto query = m_query.exchange({})) { - query->remove_callback(m_token); - } -} - -AsyncQueryCancelationToken::AsyncQueryCancelationToken(AsyncQueryCancelationToken&& rgt) = default; - -AsyncQueryCancelationToken& AsyncQueryCancelationToken::operator=(realm::AsyncQueryCancelationToken&& rgt) -{ - if (this != &rgt) { - if (auto query = m_query.exchange({})) { - query->remove_callback(m_token); - } - m_query = std::move(rgt.m_query); - m_token = rgt.m_token; - } - return *this; -} diff --git a/src/results.hpp b/src/results.hpp index c7501179..6b25982a 100644 --- a/src/results.hpp +++ b/src/results.hpp @@ -19,11 +19,10 @@ #ifndef REALM_RESULTS_HPP #define REALM_RESULTS_HPP +#include "collection_notifications.hpp" #include "shared_realm.hpp" -#include "util/atomic_shared_ptr.hpp" #include -#include #include #include @@ -31,38 +30,17 @@ namespace realm { template class BasicRowExpr; using RowExpr = BasicRowExpr
; class Mixed; -class Results; class ObjectSchema; namespace _impl { - class AsyncQuery; + class ResultsNotifier; } -// A token which keeps an asynchronous query alive -struct AsyncQueryCancelationToken { - AsyncQueryCancelationToken() = default; - AsyncQueryCancelationToken(std::shared_ptr<_impl::AsyncQuery> query, size_t token); - ~AsyncQueryCancelationToken(); - - AsyncQueryCancelationToken(AsyncQueryCancelationToken&&); - AsyncQueryCancelationToken& operator=(AsyncQueryCancelationToken&&); - - AsyncQueryCancelationToken(AsyncQueryCancelationToken const&) = delete; - AsyncQueryCancelationToken& operator=(AsyncQueryCancelationToken const&) = delete; - -private: - util::AtomicSharedPtr<_impl::AsyncQuery> m_query; - size_t m_token; -}; - struct SortOrder { - std::vector columnIndices; + std::vector column_indices; std::vector ascending; - explicit operator bool() const - { - return !columnIndices.empty(); - } + explicit operator bool() const { return !column_indices.empty(); } }; class Results { @@ -73,6 +51,8 @@ public: Results() = default; Results(SharedRealm r, const ObjectSchema& o, Table& table); Results(SharedRealm r, const ObjectSchema& o, Query q, SortOrder s = {}); + Results(SharedRealm r, const ObjectSchema& o, TableView tv, SortOrder s); + Results(SharedRealm r, const ObjectSchema& o, LinkViewRef lv, util::Optional q = {}, SortOrder s = {}); ~Results(); // Results is copyable and moveable @@ -100,6 +80,9 @@ public: // Get the object type which will be returned by get() StringData get_object_type() const noexcept; + // Get the LinkView this Results is derived from, if any + LinkViewRef get_linkview() const { return m_link_view; } + // Set whether the TableView should sync if needed before accessing results void set_live(bool live); @@ -145,6 +128,7 @@ public: Empty, // Backed by nothing (for missing tables) Table, // Backed directly by a Table Query, // Backed by a query that has not yet been turned into a TableView + LinkView, // Backed directly by a LinkView TableView // Backed by a TableView created from a Query }; // Get the currrent mode of the Results @@ -191,19 +175,21 @@ public: UnsupportedColumnTypeException(size_t column, const Table* table); }; - void update_tableview(); - // Create an async query from this Results // The query will be run on a background thread and delivered to the callback, // and then rerun after each commit (if needed) and redelivered if it changed - AsyncQueryCancelationToken async(std::function target); + NotificationToken async(std::function target); + NotificationToken add_notification_callback(CollectionChangeCallback cb); bool wants_background_updates() const { return m_wants_background_updates; } - // Helper type to let AsyncQuery update the tableview without giving access + // Returns whether the rows are guaranteed to be in table order. + bool is_in_table_order() const; + + // Helper type to let ResultsNotifier update the tableview without giving access // to any other privates or letting anyone else do so class Internal { - friend class _impl::AsyncQuery; + friend class _impl::ResultsNotifier; static void set_table_view(Results& results, TableView&& tv); }; @@ -212,19 +198,25 @@ private: const ObjectSchema *m_object_schema; Query m_query; TableView m_table_view; + LinkViewRef m_link_view; Table* m_table = nullptr; SortOrder m_sort; bool m_live = true; - std::shared_ptr<_impl::AsyncQuery> m_background_query; + std::shared_ptr<_impl::ResultsNotifier> m_notifier; Mode m_mode = Mode::Empty; bool m_has_used_table_view = false; bool m_wants_background_updates = true; + void update_tableview(); + bool update_linkview(); + void validate_read() const; void validate_write() const; + void prepare_async(); + template util::Optional aggregate(size_t column, bool return_none_for_empty, Int agg_int, Float agg_float, diff --git a/src/schema.cpp b/src/schema.cpp index 3d3988d7..2d036041 100644 --- a/src/schema.cpp +++ b/src/schema.cpp @@ -18,16 +18,19 @@ #include "schema.hpp" -#include "object_schema.hpp" #include "object_store.hpp" #include "property.hpp" +#include + using namespace realm; static bool compare_by_name(ObjectSchema const& lft, ObjectSchema const& rgt) { return lft.name < rgt.name; } +Schema::Schema(std::initializer_list types) : Schema(base(types)) { } + Schema::Schema(base types) : base(std::move(types)) { std::sort(begin(), end(), compare_by_name); } @@ -71,11 +74,11 @@ void Schema::validate() const // check nullablity if (prop.is_nullable) { - if (prop.type == PropertyTypeArray || prop.type == PropertyTypeAny) { + if (prop.type == PropertyType::Array || prop.type == PropertyType::Any) { exceptions.emplace_back(InvalidNullabilityException(object.name, prop)); } } - else if (prop.type == PropertyTypeObject) { + else if (prop.type == PropertyType::Object) { exceptions.emplace_back(InvalidNullabilityException(object.name, prop)); } diff --git a/src/schema.hpp b/src/schema.hpp index 52c03975..3a4e026f 100644 --- a/src/schema.hpp +++ b/src/schema.hpp @@ -20,21 +20,18 @@ #define REALM_SCHEMA_HPP #include "object_schema.hpp" -#include "property.hpp" #include #include namespace realm { -class ObjectSchema; - class Schema : private std::vector { private: using base = std::vector; public: // Create a schema from a vector of ObjectSchema Schema(base types); - Schema(std::initializer_list types) : Schema(base(types)) { } + Schema(std::initializer_list types); // find an ObjectSchema by name iterator find(std::string const& name); diff --git a/src/shared_realm.cpp b/src/shared_realm.cpp index ae73a9a8..ff731ce3 100644 --- a/src/shared_realm.cpp +++ b/src/shared_realm.cpp @@ -19,7 +19,6 @@ #include "shared_realm.hpp" #include "binding_context.hpp" -#include "impl/external_commit_helper.hpp" #include "impl/realm_coordinator.hpp" #include "impl/transact_log_handler.hpp" #include "object_store.hpp" @@ -28,8 +27,6 @@ #include #include -#include - using namespace realm; using namespace realm::_impl; diff --git a/src/shared_realm.hpp b/src/shared_realm.hpp index 75b67694..2526d198 100644 --- a/src/shared_realm.hpp +++ b/src/shared_realm.hpp @@ -21,28 +21,25 @@ #include "schema.hpp" -#include - #include -#include #include #include #include -#include namespace realm { class BindingContext; - class Replication; class Group; class Realm; - class RealmDelegate; + class Replication; class SharedGroup; typedef std::shared_ptr SharedRealm; typedef std::weak_ptr WeakRealm; namespace _impl { - class AsyncQuery; + class CollectionNotifier; + class ListNotifier; class RealmCoordinator; + class ResultsNotifier; } class Realm : public std::enable_shared_from_this { @@ -66,7 +63,7 @@ namespace realm { bool in_memory = false; // The following are intended for internal/testing purposes and - // should not be publically exposed in binding APIs + // should not be publicly exposed in binding APIs // If false, always return a new Realm instance, and don't return // that Realm instance for other requests for a cached Realm. Useful @@ -146,16 +143,18 @@ namespace realm { // Expose some internal functionality to other parts of the ObjectStore // without making it public to everyone class Internal { - friend class _impl::AsyncQuery; + friend class _impl::CollectionNotifier; + friend class _impl::ListNotifier; friend class _impl::RealmCoordinator; + friend class _impl::ResultsNotifier; - // AsyncQuery needs access to the SharedGroup to be able to call the - // handover functions, which are not very wrappable + // ResultsNotifier and ListNotifier need access to the SharedGroup + // to be able to call the handover functions, which are not very wrappable static SharedGroup& get_shared_group(Realm& realm) { return *realm.m_shared_group; } - // AsyncQuery needs to be able to access the owning coordinator to - // wake up the worker thread when a callback is added, and - // coordinators need to be able to get themselves from a Realm + // CollectionNotifier needs to be able to access the owning + // coordinator to wake up the worker thread when a callback is + // added, and coordinators need to be able to get themselves from a Realm static _impl::RealmCoordinator& get_coordinator(Realm& realm) { return *realm.m_coordinator; } }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0d61f0e9..6e055b4d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,18 +1,26 @@ include_directories(../external/catch/single_include .) set(HEADERS + util/index_helpers.hpp util/test_file.hpp ) set(SOURCES + collection_change_indices.cpp index_set.cpp + list.cpp main.cpp parser.cpp results.cpp + transaction_log_parsing.cpp util/test_file.cpp ) add_executable(tests ${SOURCES} ${HEADERS}) target_link_libraries(tests realm-object-store) +create_coverage_target(generate-coverage tests) + add_custom_target(run-tests USES_TERMINAL DEPENDS tests COMMAND ./tests) + +add_subdirectory(notifications-fuzzer) diff --git a/tests/collection_change_indices.cpp b/tests/collection_change_indices.cpp new file mode 100644 index 00000000..d0f9ec92 --- /dev/null +++ b/tests/collection_change_indices.cpp @@ -0,0 +1,986 @@ +#include "catch.hpp" + +#include "impl/collection_notifier.hpp" + +#include "util/index_helpers.hpp" + +#include + +using namespace realm; + +TEST_CASE("[collection_change] insert()") { + _impl::CollectionChangeBuilder c; + + SECTION("adds the row to the insertions set") { + c.insert(5); + c.insert(8); + REQUIRE_INDICES(c.insertions, 5, 8); + } + + SECTION("shifts previous insertions and modifications") { + c.insert(5); + c.modify(8); + + c.insert(1); + REQUIRE_INDICES(c.insertions, 1, 6); + REQUIRE_INDICES(c.modifications, 9); + } + + SECTION("does not shift previous deletions") { + c.erase(8); + c.erase(3); + c.insert(5); + + REQUIRE_INDICES(c.insertions, 5); + REQUIRE_INDICES(c.deletions, 3, 8); + } + + SECTION("shifts destination of previous moves after the insertion point") { + c.moves = {{10, 5}, {10, 2}, {3, 10}}; + c.insert(4); + REQUIRE_MOVES(c, {10, 6}, {10, 2}, {3, 11}); + } +} + +TEST_CASE("[collection_change] modify()") { + _impl::CollectionChangeBuilder c; + + SECTION("marks the row as modified") { + c.modify(5); + REQUIRE_INDICES(c.modifications, 5); + } + + SECTION("also marks newly inserted rows as modified") { + c.insert(5); + c.modify(5); + REQUIRE_INDICES(c.modifications, 5); + } + + SECTION("is idempotent") { + c.modify(5); + c.modify(5); + c.modify(5); + c.modify(5); + REQUIRE_INDICES(c.modifications, 5); + } +} + +TEST_CASE("[collection_change] erase()") { + _impl::CollectionChangeBuilder c; + + SECTION("adds the row to the deletions set") { + c.erase(5); + REQUIRE_INDICES(c.deletions, 5); + } + + SECTION("is shifted for previous deletions") { + c.erase(5); + c.erase(6); + REQUIRE_INDICES(c.deletions, 5, 7); + } + + SECTION("is shifted for previous insertions") { + c.insert(5); + c.erase(6); + REQUIRE_INDICES(c.deletions, 5); + } + + SECTION("removes previous insertions") { + c.insert(5); + c.erase(5); + REQUIRE(c.insertions.empty()); + REQUIRE(c.deletions.empty()); + } + + SECTION("removes previous modifications") { + c.modify(5); + c.erase(5); + REQUIRE(c.modifications.empty()); + REQUIRE_INDICES(c.deletions, 5); + } + + SECTION("shifts previous modifications") { + c.modify(5); + c.erase(4); + REQUIRE_INDICES(c.modifications, 4); + REQUIRE_INDICES(c.deletions, 4); + } + + SECTION("removes previous moves to the row being erased") { + c.moves = {{10, 5}}; + c.erase(5); + REQUIRE(c.moves.empty()); + } + + SECTION("shifts the destination of previous moves") { + c.moves = {{10, 5}, {10, 2}, {3, 10}}; + c.erase(4); + REQUIRE_MOVES(c, {10, 4}, {10, 2}, {3, 9}); + } +} + +TEST_CASE("[collection_change] move_over()") { + _impl::CollectionChangeBuilder c; + + SECTION("is just erase when row == last_row") { + c.move_over(10, 10); + c.parse_complete(); + + REQUIRE_INDICES(c.deletions, 10); + REQUIRE(c.insertions.empty()); + REQUIRE(c.moves.empty()); + } + + SECTION("is just erase when row + 1 == last_row") { + c.move_over(0, 6); + c.move_over(4, 5); + c.move_over(0, 4); + c.move_over(2, 3); + c.parse_complete(); + c.clean_up_stale_moves(); + + REQUIRE_INDICES(c.deletions, 0, 2, 4, 5, 6); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_MOVES(c, {5, 0}); + } + + SECTION("marks the old last row as moved") { + c.move_over(5, 8); + c.parse_complete(); + REQUIRE_MOVES(c, {8, 5}); + } + + SECTION("does not mark the old last row as moved if it was newly inserted") { + c.insert(8); + c.move_over(5, 8); + c.parse_complete(); + REQUIRE(c.moves.empty()); + } + + SECTION("removes previous modifications for the removed row") { + c.modify(5); + c.move_over(5, 8); + c.parse_complete(); + REQUIRE(c.modifications.empty()); + } + + SECTION("updates previous insertions for the old last row") { + c.insert(5); + c.move_over(3, 5); + c.parse_complete(); + REQUIRE_INDICES(c.insertions, 3); + } + + SECTION("updates previous modifications for the old last row") { + c.modify(5); + c.move_over(3, 5); + c.parse_complete(); + REQUIRE_INDICES(c.modifications, 3); + } + + SECTION("removes moves to the target") { + c.move_over(5, 10); + c.move_over(5, 8); + c.parse_complete(); + REQUIRE_MOVES(c, {8, 5}); + } + + SECTION("updates moves to the source") { + c.move_over(8, 10); + c.move_over(5, 8); + c.parse_complete(); + REQUIRE_MOVES(c, {10, 5}); + } + + SECTION("removes moves to the row when row == last_row") { + c.move_over(0, 1); + c.move_over(0, 0); + c.parse_complete(); + + REQUIRE_INDICES(c.deletions, 0, 1); + REQUIRE(c.insertions.empty()); + REQUIRE(c.moves.empty()); + } + + SECTION("is not shifted by previous calls to move_over()") { + c.move_over(5, 10); + c.move_over(6, 9); + c.parse_complete(); + REQUIRE_INDICES(c.deletions, 5, 6, 9, 10); + REQUIRE_INDICES(c.insertions, 5, 6); + REQUIRE_MOVES(c, {9, 6}, {10, 5}); + } + + SECTION("marks the moved-over row as deleted when chaining moves") { + c.move_over(5, 10); + c.move_over(0, 5); + c.parse_complete(); + + REQUIRE_INDICES(c.deletions, 0, 5, 10); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_MOVES(c, {10, 0}); + } +} + +TEST_CASE("[collection_change] clear()") { + _impl::CollectionChangeBuilder c; + + SECTION("removes all insertions") { + c.insertions = {1, 2, 3}; + c.clear(0); + REQUIRE(c.insertions.empty()); + } + + SECTION("removes all modifications") { + c.modifications = {1, 2, 3}; + c.clear(0); + REQUIRE(c.modifications.empty()); + } + + SECTION("removes all moves") { + c.moves = {{1, 3}}; + c.clear(0); + REQUIRE(c.moves.empty()); + } + + SECTION("sets deletions to the number of rows before any changes") { + c.insertions = {1, 2, 3}; + c.clear(5); + REQUIRE_INDICES(c.deletions, 0, 1); + + c.deletions = {1, 2, 3}; + c.clear(5); + REQUIRE_INDICES(c.deletions, 0, 1, 2, 3, 4, 5, 6, 7); + } + + SECTION("sets deletions SIZE_T_MAX if that if the given previous size") { + c.insertions = {1, 2, 3}; + c.clear(std::numeric_limits::max()); + REQUIRE(!c.deletions.empty()); + REQUIRE(++c.deletions.begin() == c.deletions.end()); + REQUIRE(c.deletions.begin()->first == 0); + REQUIRE(c.deletions.begin()->second == std::numeric_limits::max()); + } +} + +TEST_CASE("[collection_change] move()") { + _impl::CollectionChangeBuilder c; + + SECTION("adds the move to the list of moves") { + c.move(5, 6); + REQUIRE_MOVES(c, {5, 6}); + } + + SECTION("updates previous moves to the source of this move") { + c.move(5, 6); + c.move(6, 7); + REQUIRE_MOVES(c, {5, 7}); + } + + SECTION("shifts previous moves and is shifted by them") { + c.move(5, 10); + c.move(6, 12); + REQUIRE_MOVES(c, {5, 9}, {7, 12}); + + c.move(10, 0); + REQUIRE_MOVES(c, {5, 10}, {7, 12}, {11, 0}); + } + + SECTION("does not report a move if the source is newly inserted") { + c.insert(5); + c.move(5, 10); + REQUIRE_INDICES(c.insertions, 10); + REQUIRE(c.moves.empty()); + } + + SECTION("shifts previous insertions and modifications") { + c.insert(5); + c.modify(6); + c.move(10, 0); + REQUIRE_INDICES(c.insertions, 0, 6); + REQUIRE_INDICES(c.modifications, 7); + REQUIRE_MOVES(c, {9, 0}); + } + + SECTION("marks the target row as modified if the source row was") { + c.modify(5); + + c.move(5, 10); + REQUIRE_INDICES(c.modifications, 10); + + c.move(6, 12); + REQUIRE_INDICES(c.modifications, 9); + } + + SECTION("bumps previous moves to the same location") { + c.move(5, 10); + c.move(7, 10); + REQUIRE_MOVES(c, {5, 9}, {8, 10}); + + c = {}; + c.move(5, 10); + c.move(15, 10); + REQUIRE_MOVES(c, {5, 11}, {15, 10}); + } + + SECTION("collapses redundant swaps of adjacent rows to a no-op") { + c.move(7, 8); + c.move(7, 8); + c.clean_up_stale_moves(); + REQUIRE(c.empty()); + } +} + +TEST_CASE("[collection_change] calculate() unsorted") { + _impl::CollectionChangeBuilder c; + + auto all_modified = [](size_t) { return true; }; + auto none_modified = [](size_t) { return false; }; + const auto npos = size_t(-1); + + SECTION("returns an empty set when input and output are identical") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2, 3}, none_modified, true); + REQUIRE(c.empty()); + } + + SECTION("marks all as inserted when prev is empty") { + c = _impl::CollectionChangeBuilder::calculate({}, {1, 2, 3}, all_modified, true); + REQUIRE_INDICES(c.insertions, 0, 1, 2); + } + + SECTION("marks all as deleted when new is empty") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {}, all_modified, true); + REQUIRE_INDICES(c.deletions, 0, 1, 2); + } + + SECTION("marks npos rows in prev as deleted") { + c = _impl::CollectionChangeBuilder::calculate({npos, 1, 2, 3, npos}, {1, 2, 3}, all_modified, true); + REQUIRE_INDICES(c.deletions, 0, 4); + } + + SECTION("marks modified rows which do not move as modified") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2, 3}, all_modified, true); + REQUIRE_INDICES(c.modifications, 0, 1, 2); + } + + SECTION("does not mark unmodified rows as modified") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2, 3}, none_modified, true); + REQUIRE(c.modifications.empty()); + } + + SECTION("marks newly added rows as insertions") { + c = _impl::CollectionChangeBuilder::calculate({2, 3}, {1, 2, 3}, all_modified, true); + REQUIRE_INDICES(c.insertions, 0); + + c = _impl::CollectionChangeBuilder::calculate({1, 3}, {1, 2, 3}, all_modified, true); + REQUIRE_INDICES(c.insertions, 1); + + c = _impl::CollectionChangeBuilder::calculate({1, 2}, {1, 2, 3}, all_modified, true); + REQUIRE_INDICES(c.insertions, 2); + } + + SECTION("marks removed rows as deleted") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2}, all_modified, true); + REQUIRE_INDICES(c.deletions, 2); + + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 3}, all_modified, true); + REQUIRE_INDICES(c.deletions, 1); + + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {2, 3}, all_modified, true); + REQUIRE_INDICES(c.deletions, 0); + } + + SECTION("marks rows as both inserted and deleted") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 3, 4}, all_modified, true); + REQUIRE_INDICES(c.deletions, 1); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE(c.moves.empty()); + } + + SECTION("marks rows as modified even if they moved") { + c = _impl::CollectionChangeBuilder::calculate({5, 3}, {3, 5}, all_modified, true); + REQUIRE_MOVES(c, {1, 0}); + REQUIRE_INDICES(c.modifications, 0, 1); + } + + SECTION("does not mark rows as modified if they are new") { + c = _impl::CollectionChangeBuilder::calculate({3}, {3, 5}, all_modified, true); + REQUIRE_INDICES(c.modifications, 0); + } + + SECTION("reports moves which can be produced by move_last_over()") { + auto calc = [&](std::vector values) { + return _impl::CollectionChangeBuilder::calculate(values, {1, 2, 3}, none_modified, true); + }; + + REQUIRE(calc({1, 2, 3}).empty()); + REQUIRE_MOVES(calc({1, 3, 2}), {2, 1}); + REQUIRE_MOVES(calc({2, 1, 3}), {1, 0}); + REQUIRE_MOVES(calc({2, 3, 1}), {2, 0}); + REQUIRE_MOVES(calc({3, 1, 2}), {1, 0}, {2, 1}); + REQUIRE_MOVES(calc({3, 2, 1}), {2, 0}, {1, 1}); + } +} + +TEST_CASE("[collection_change] calculate() sorted") { + _impl::CollectionChangeBuilder c; + + auto all_modified = [](size_t) { return true; }; + auto none_modified = [](size_t) { return false; }; + const auto npos = size_t(-1); + + SECTION("returns an empty set when input and output are identical") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2, 3}, none_modified, false); + REQUIRE(c.empty()); + } + + SECTION("marks all as inserted when prev is empty") { + c = _impl::CollectionChangeBuilder::calculate({}, {1, 2, 3}, all_modified, false); + REQUIRE_INDICES(c.insertions, 0, 1, 2); + } + + SECTION("marks all as deleted when new is empty") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {}, all_modified, false); + REQUIRE_INDICES(c.deletions, 0, 1, 2); + } + + SECTION("marks npos rows in prev as deleted") { + c = _impl::CollectionChangeBuilder::calculate({npos, 1, 2, 3, npos}, {1, 2, 3}, all_modified, false); + REQUIRE_INDICES(c.deletions, 0, 4); + } + + SECTION("marks modified rows which do not move as modified") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2, 3}, all_modified, false); + REQUIRE_INDICES(c.modifications, 0, 1, 2); + } + + SECTION("does not mark unmodified rows as modified") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2, 3}, none_modified, false); + REQUIRE(c.modifications.empty()); + } + + SECTION("marks newly added rows as insertions") { + c = _impl::CollectionChangeBuilder::calculate({2, 3}, {1, 2, 3}, all_modified, false); + REQUIRE_INDICES(c.insertions, 0); + + c = _impl::CollectionChangeBuilder::calculate({1, 3}, {1, 2, 3}, all_modified, false); + REQUIRE_INDICES(c.insertions, 1); + + c = _impl::CollectionChangeBuilder::calculate({1, 2}, {1, 2, 3}, all_modified, false); + REQUIRE_INDICES(c.insertions, 2); + } + + SECTION("marks removed rows as deleted") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 2}, all_modified, false); + REQUIRE_INDICES(c.deletions, 2); + + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 3}, all_modified, false); + REQUIRE_INDICES(c.deletions, 1); + + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {2, 3}, all_modified, false); + REQUIRE_INDICES(c.deletions, 0); + } + + SECTION("marks rows as both inserted and deleted") { + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 3, 4}, all_modified, false); + REQUIRE_INDICES(c.deletions, 1); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE(c.moves.empty()); + } + + SECTION("marks rows as modified even if they moved") { + c = _impl::CollectionChangeBuilder::calculate({3, 5}, {5, 3}, all_modified, false); + REQUIRE_INDICES(c.deletions, 1); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.modifications, 0, 1); + } + + SECTION("does not mark rows as modified if they are new") { + c = _impl::CollectionChangeBuilder::calculate({3}, {3, 5}, all_modified, false); + REQUIRE_INDICES(c.modifications, 0); + } + + SECTION("reports inserts/deletes for simple reorderings") { + auto calc = [&](std::vector old_rows, std::vector new_rows) { + return _impl::CollectionChangeBuilder::calculate(old_rows, new_rows, none_modified, false); + }; + + c = calc({1, 2, 3}, {1, 2, 3}); + REQUIRE(c.insertions.empty()); + REQUIRE(c.deletions.empty()); + + c = calc({1, 2, 3}, {1, 3, 2}); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({1, 2, 3}, {2, 1, 3}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 1); + + c = calc({1, 2, 3}, {2, 3, 1}); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE_INDICES(c.deletions, 0); + + c = calc({1, 2, 3}, {3, 1, 2}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({1, 2, 3}, {3, 2, 1}); + REQUIRE_INDICES(c.insertions, 0, 1); + REQUIRE_INDICES(c.deletions, 1, 2); + + c = calc({1, 3, 2}, {1, 2, 3}); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({1, 3, 2}, {1, 3, 2}); + REQUIRE(c.insertions.empty()); + REQUIRE(c.deletions.empty()); + + c = calc({1, 3, 2}, {2, 1, 3}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({1, 3, 2}, {2, 3, 1}); + REQUIRE_INDICES(c.insertions, 0, 1); + REQUIRE_INDICES(c.deletions, 1, 2); + + c = calc({1, 3, 2}, {3, 1, 2}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 1); + + c = calc({1, 3, 2}, {3, 2, 1}); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE_INDICES(c.deletions, 0); + + c = calc({2, 1, 3}, {1, 2, 3}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 1); + + c = calc({2, 1, 3}, {1, 3, 2}); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE_INDICES(c.deletions, 0); + + c = calc({2, 1, 3}, {2, 1, 3}); + REQUIRE(c.insertions.empty()); + REQUIRE(c.deletions.empty()); + + c = calc({2, 1, 3}, {2, 3, 1}); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({2, 1, 3}, {3, 1, 2}); + REQUIRE_INDICES(c.insertions, 0, 1); + REQUIRE_INDICES(c.deletions, 1, 2); + + c = calc({2, 1, 3}, {3, 2, 1}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({2, 3, 1}, {1, 2, 3}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({2, 3, 1}, {1, 3, 2}); + REQUIRE_INDICES(c.insertions, 0, 1); + REQUIRE_INDICES(c.deletions, 1, 2); + + c = calc({2, 3, 1}, {2, 1, 3}); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({2, 3, 1}, {2, 3, 1}); + REQUIRE(c.insertions.empty()); + REQUIRE(c.deletions.empty()); + + c = calc({2, 3, 1}, {3, 1, 2}); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE_INDICES(c.deletions, 0); + + c = calc({2, 3, 1}, {3, 2, 1}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 1); + + c = calc({3, 1, 2}, {1, 2, 3}); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE_INDICES(c.deletions, 0); + + c = calc({3, 1, 2}, {1, 3, 2}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 1); + + c = calc({3, 1, 2}, {2, 1, 3}); + REQUIRE_INDICES(c.insertions, 0, 1); + REQUIRE_INDICES(c.deletions, 1, 2); + + c = calc({3, 1, 2}, {2, 3, 1}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({3, 1, 2}, {3, 1, 2}); + REQUIRE(c.insertions.empty()); + REQUIRE(c.deletions.empty()); + + c = calc({3, 1, 2}, {3, 2, 1}); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({3, 2, 1}, {1, 2, 3}); + REQUIRE_INDICES(c.insertions, 0, 1); + REQUIRE_INDICES(c.deletions, 1, 2); + + c = calc({3, 2, 1}, {1, 3, 2}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({3, 2, 1}, {2, 1, 3}); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE_INDICES(c.deletions, 0); + + c = calc({3, 2, 1}, {2, 3, 1}); + REQUIRE_INDICES(c.insertions, 0); + REQUIRE_INDICES(c.deletions, 1); + + c = calc({3, 2, 1}, {3, 1, 2}); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.deletions, 2); + + c = calc({3, 2, 1}, {3, 2, 1}); + REQUIRE(c.insertions.empty()); + REQUIRE(c.deletions.empty()); + } + + SECTION("prefers to produce diffs where modified rows are the ones to move when it is ambiguous") { + auto two_modified = [](size_t ndx) { return ndx == 2; }; + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 3, 2}, two_modified, false); + REQUIRE_INDICES(c.deletions, 1); + REQUIRE_INDICES(c.insertions, 2); + + auto three_modified = [](size_t ndx) { return ndx == 3; }; + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {1, 3, 2}, three_modified, false); + REQUIRE_INDICES(c.deletions, 2); + REQUIRE_INDICES(c.insertions, 1); + } + + SECTION("prefers smaller diffs over larger diffs moving only modified rows") { + auto two_modified = [](size_t ndx) { return ndx == 2; }; + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, {2, 3, 1}, two_modified, false); + REQUIRE_INDICES(c.deletions, 0); + REQUIRE_INDICES(c.insertions, 2); + } + + SECTION("supports duplicate indices") { + c = _impl::CollectionChangeBuilder::calculate({1, 1, 2, 2, 3, 3}, + {1, 2, 3, 1, 2, 3}, + all_modified, false); + REQUIRE_INDICES(c.deletions, 3, 5); + REQUIRE_INDICES(c.insertions, 1, 2); + } + + SECTION("deletes and inserts the last option when any in a range could be deleted") { + c = _impl::CollectionChangeBuilder::calculate({3, 2, 1, 1, 2, 3}, + {1, 1, 2, 2, 3, 3}, + all_modified, false); + REQUIRE_INDICES(c.deletions, 0, 1); + REQUIRE_INDICES(c.insertions, 3, 5); + } + + SECTION("reports insertions/deletions when the number of duplicate entries changes") { + c = _impl::CollectionChangeBuilder::calculate({1, 1, 1, 1, 2, 3}, + {1, 2, 3, 1}, + all_modified, false); + REQUIRE_INDICES(c.deletions, 1, 2, 3); + REQUIRE_INDICES(c.insertions, 3); + + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3, 1}, + {1, 1, 1, 1, 2, 3}, + all_modified, false); + REQUIRE_INDICES(c.deletions, 3); + REQUIRE_INDICES(c.insertions, 1, 2, 3); + } + + SECTION("properly recurses into smaller subblocks") { + std::vector prev = {10, 1, 2, 11, 3, 4, 5, 12, 6, 7, 13}; + std::vector next = {13, 1, 2, 12, 3, 4, 5, 11, 6, 7, 10}; + c = _impl::CollectionChangeBuilder::calculate(prev, next, all_modified, false); + REQUIRE_INDICES(c.deletions, 0, 3, 7, 10); + REQUIRE_INDICES(c.insertions, 0, 3, 7, 10); + } + + SECTION("produces diffs which let merge collapse insert -> move -> delete to no-op") { + auto four_modified = [](size_t ndx) { return ndx == 4; }; + for (int insert_pos = 0; insert_pos < 4; ++insert_pos) { + for (int move_to_pos = 0; move_to_pos < 4; ++move_to_pos) { + if (insert_pos == move_to_pos) + continue; + CAPTURE(insert_pos); + CAPTURE(move_to_pos); + + std::vector after_insert = {1, 2, 3}; + after_insert.insert(after_insert.begin() + insert_pos, 4); + c = _impl::CollectionChangeBuilder::calculate({1, 2, 3}, after_insert, four_modified, false); + + std::vector after_move = {1, 2, 3}; + after_move.insert(after_move.begin() + move_to_pos, 4); + c.merge(_impl::CollectionChangeBuilder::calculate(after_insert, after_move, four_modified, false)); + + c.merge(_impl::CollectionChangeBuilder::calculate(after_move, {1, 2, 3}, four_modified, false)); + REQUIRE(c.empty()); + } + } + } +} + +TEST_CASE("[collection_change] merge()") { + _impl::CollectionChangeBuilder c; + + SECTION("is a no-op if the new set is empty") { + c = {{1, 2, 3}, {4, 5}, {6, 7}, {{8, 9}}}; + c.merge({}); + REQUIRE_INDICES(c.deletions, 1, 2, 3, 8); + REQUIRE_INDICES(c.insertions, 4, 5, 9); + REQUIRE_INDICES(c.modifications, 6, 7); + REQUIRE_MOVES(c, {8, 9}); + } + + SECTION("replaces the set with the new set if the old set is empty") { + c.merge({{1, 2, 3}, {4, 5}, {6, 7}, {{8, 9}}}); + REQUIRE_INDICES(c.deletions, 1, 2, 3, 8); + REQUIRE_INDICES(c.insertions, 4, 5, 9); + REQUIRE_INDICES(c.modifications, 6, 7); + REQUIRE_MOVES(c, {8, 9}); + } + + SECTION("shifts deletions by previous deletions") { + c = {{5}, {}, {}, {}}; + c.merge({{3}, {}, {}, {}}); + REQUIRE_INDICES(c.deletions, 3, 5); + + c = {{5}, {}, {}, {}}; + c.merge({{4}, {}, {}, {}}); + REQUIRE_INDICES(c.deletions, 4, 5); + + c = {{5}, {}, {}, {}}; + c.merge({{5}, {}, {}, {}}); + REQUIRE_INDICES(c.deletions, 5, 6); + + c = {{5}, {}, {}, {}}; + c.merge({{6}, {}, {}, {}}); + REQUIRE_INDICES(c.deletions, 5, 7); + } + + SECTION("shifts deletions by previous insertions") { + c = {{}, {5}, {}, {}}; + c.merge({{4}, {}, {}, {}}); + REQUIRE_INDICES(c.deletions, 4); + + c = {{}, {5}, {}, {}}; + c.merge({{6}, {}, {}, {}}); + REQUIRE_INDICES(c.deletions, 5); + } + + SECTION("shifts previous insertions by deletions") { + c = {{}, {2, 3}, {}, {}}; + c.merge({{1}, {}, {}, {}}); + REQUIRE_INDICES(c.insertions, 1, 2); + } + + SECTION("removes previous insertions for newly deleted rows") { + c = {{}, {1, 2}, {}, {}}; + c.merge({{2}, {}, {}, {}}); + REQUIRE_INDICES(c.insertions, 1); + } + + SECTION("removes previous modifications for newly deleted rows") { + c = {{}, {}, {2, 3}, {}}; + c.merge({{2}, {}, {}, {}}); + REQUIRE_INDICES(c.modifications, 2); + } + + SECTION("shifts previous modifications for deletions of other rows") { + c = {{}, {}, {2, 3}, {}}; + c.merge({{1}, {}, {}, {}}); + REQUIRE_INDICES(c.modifications, 1, 2); + } + + SECTION("removes moves to rows which have been deleted") { + c = {{}, {}, {}, {{2, 3}}}; + c.merge({{3}, {}, {}, {}}); + REQUIRE(c.moves.empty()); + } + + SECTION("shifts destinations of previous moves to reflect new deletions") { + c = {{}, {}, {}, {{2, 5}}}; + c.merge({{3}, {}, {}, {}}); + REQUIRE_MOVES(c, {2, 4}); + } + + SECTION("does not modify old deletions based on new insertions") { + c = {{1, 3}, {}, {}, {}}; + c.merge({{}, {1, 2, 3}, {}, {}}); + REQUIRE_INDICES(c.deletions, 1, 3); + REQUIRE_INDICES(c.insertions, 1, 2, 3); + } + + SECTION("shifts previous insertions to reflect new insertions") { + c = {{}, {1, 5}, {}, {}}; + c.merge({{}, {1, 4}, {}, {}}); + REQUIRE_INDICES(c.insertions, 1, 2, 4, 7); + } + + SECTION("shifts previous modifications to reflect new insertions") { + c = {{}, {}, {1, 5}, {}}; + c.merge({{}, {1, 4}, {}, {}}); + REQUIRE_INDICES(c.modifications, 2, 7); + REQUIRE_INDICES(c.insertions, 1, 4); + } + + SECTION("shifts previous move destinations to reflect new insertions") { + c = {{}, {}, {}, {{2, 5}}}; + c.merge({{}, {3}, {}}); + REQUIRE_MOVES(c, {2, 6}); + } + + SECTION("does not modify old deletions based on new modifications") { + c = {{1, 2, 3}, {}, {}, {}}; + c.merge({{}, {}, {2}}); + REQUIRE_INDICES(c.deletions, 1, 2, 3); + REQUIRE_INDICES(c.modifications, 2); + } + + SECTION("tracks modifications made to previously inserted rows") { + c = {{}, {2}, {}, {}}; + c.merge({{}, {}, {1, 2, 3}}); + REQUIRE_INDICES(c.insertions, 2); + REQUIRE_INDICES(c.modifications, 1, 2, 3); + } + + SECTION("unions modifications with old modifications") { + c = {{}, {}, {2}, {}}; + c.merge({{}, {}, {1, 2, 3}}); + REQUIRE_INDICES(c.modifications, 1, 2, 3); + } + + SECTION("tracks modifications for previous moves") { + c = {{}, {}, {}, {{1, 2}}}; + c.merge({{}, {}, {2, 3}}); + REQUIRE_INDICES(c.modifications, 2, 3); + } + + SECTION("updates new move sources to reflect previous inserts and deletes") { + c = {{1}, {}, {}, {}}; + c.merge({{}, {}, {}, {{2, 3}}}); + REQUIRE_MOVES(c, {3, 3}); + + c = {{}, {1}, {}, {}}; + c.merge({{}, {}, {}, {{2, 3}}}); + REQUIRE_MOVES(c, {1, 3}); + + c = {{2}, {4}, {}, {}}; + c.merge({{}, {}, {}, {{5, 10}}}); + REQUIRE_MOVES(c, {5, 10}); + } + + SECTION("updates the row modified for rows moved after a modification") { + c = {{}, {}, {1}, {}}; + c.merge({{}, {}, {}, {{1, 3}}}); + REQUIRE_INDICES(c.modifications, 3); + REQUIRE_MOVES(c, {1, 3}); + } + + SECTION("updates the row modified for chained moves") { + c = {{}, {}, {1}, {}}; + c.merge({{}, {}, {}, {{1, 3}}}); + c.merge({{}, {}, {}, {{3, 5}}}); + REQUIRE_INDICES(c.modifications, 5); + REQUIRE_MOVES(c, {1, 5}); + } + + SECTION("updates the row inserted for moves of previously new rows") { + c = {{}, {1}, {}, {}}; + c.merge({{}, {}, {}, {{1, 3}}}); + REQUIRE(c.moves.empty()); + REQUIRE_INDICES(c.insertions, 3); + } + + SECTION("updates old moves when the destination is moved again") { + c = {{}, {}, {}, {{1, 3}}}; + c.merge({{}, {}, {}, {{3, 5}}}); + REQUIRE_MOVES(c, {1, 5}); + } + + SECTION("shifts destination of previous moves to reflect new moves like an insert/delete pair would") { + c = {{}, {}, {}, {{1, 3}}}; + c.merge({{}, {}, {}, {{2, 5}}}); + REQUIRE_MOVES(c, {1, 2}, {3, 5}); + + c = {{}, {}, {}, {{1, 10}}}; + c.merge({{}, {}, {}, {{2, 5}}}); + REQUIRE_MOVES(c, {1, 10}, {3, 5}); + + c = {{}, {}, {}, {{5, 10}}}; + c.merge({{}, {}, {}, {{12, 2}}}); + REQUIRE_MOVES(c, {5, 11}, {12, 2}); + } + + SECTION("moves shift previous inserts like an insert/delete pair would") { + c = {{}, {5}}; + c.merge({{}, {}, {}, {{2, 6}}}); + REQUIRE_INDICES(c.insertions, 4, 6); + } + + SECTION("moves shift previous modifications like an insert/delete pair would") { + c = {{}, {}, {5}}; + c.merge({{}, {}, {}, {{2, 6}}}); + REQUIRE_INDICES(c.modifications, 4); + } + + SECTION("moves are shifted by previous deletions like an insert/delete pair would") { + c = {{5}}; + c.merge({{}, {}, {}, {{2, 6}}}); + REQUIRE_MOVES(c, {2, 6}); + + c = {{5}}; + c.merge({{}, {}, {}, {{6, 2}}}); + REQUIRE_MOVES(c, {7, 2}); + } + + SECTION("leapfrogging rows collapse to an empty changeset") { + c = {{1}, {0}, {}, {{1, 0}}}; + c.merge({{1}, {0}, {}, {{1, 0}}}); + REQUIRE(c.empty()); + } + + SECTION("modify -> move -> unmove leaves row marked as modified") { + c = {{}, {}, {1}}; + c.merge({{1}, {2}, {}, {{1, 2}}}); + c.merge({{1}}); + + REQUIRE_INDICES(c.deletions, 2); + REQUIRE(c.insertions.empty()); + REQUIRE(c.moves.empty()); + REQUIRE_INDICES(c.modifications, 1); + } + + SECTION("modifying a previously moved row which stops being a move due to more deletions") { + // Make it stop being a move in the same transaction as the modify + c = {{1, 2}, {0, 1}, {}, {{1, 0}, {2, 1}}}; + c.merge({{0, 2}, {1}, {0}, {}}); + + REQUIRE_INDICES(c.deletions, 0, 1); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.modifications, 0); + REQUIRE(c.moves.empty()); + + // Same net change, but make it no longer a move in the transaction after the modify + c = {{1, 2}, {0, 1}, {}, {{1, 0}, {2, 1}}}; + c.merge({{}, {}, {1}, {}}); + c.merge({{0, 2}, {0}, {}, {{2, 0}}}); + c.merge({{0}, {1}, {}, {}}); + + REQUIRE_INDICES(c.deletions, 0, 1); + REQUIRE_INDICES(c.insertions, 1); + REQUIRE_INDICES(c.modifications, 0); + REQUIRE(c.moves.empty()); + } +} diff --git a/tests/index_set.cpp b/tests/index_set.cpp index 50ae889c..ff62a05d 100644 --- a/tests/index_set.cpp +++ b/tests/index_set.cpp @@ -1,153 +1,592 @@ -#include "index_set.hpp" - -#include - -// Catch doesn't have an overload for std::pair, so define one ourselves -// The declaration needs to be before catch.hpp is included for it to be used, -// but the definition needs to be after since it uses Catch's toString() -namespace Catch { -template -std::string toString(std::pair const& value); -} - #include "catch.hpp" -namespace Catch { -template - std::string toString(std::pair const& value) { - return "{" + toString(value.first) + ", " + toString(value.second) + "}"; -} +#include "index_set.hpp" + +#include "util/index_helpers.hpp" + +TEST_CASE("[index_set] contains()") { + SECTION("returns false if the index is before the first entry in the set") { + realm::IndexSet set = {1, 2, 5}; + REQUIRE_FALSE(set.contains(0)); + } + + SECTION("returns false if the index is after the last entry in the set") { + realm::IndexSet set = {1, 2, 5}; + REQUIRE_FALSE(set.contains(6)); + } + + SECTION("returns false if the index is between ranges in the set") { + realm::IndexSet set = {1, 2, 5}; + REQUIRE_FALSE(set.contains(4)); + } + + SECTION("returns true if the index is in the set") { + realm::IndexSet set = {1, 2, 5}; + REQUIRE(set.contains(1)); + REQUIRE(set.contains(2)); + REQUIRE(set.contains(5)); + } } -#define REQUIRE_RANGES(index_set, ...) do { \ - std::initializer_list> expected = {__VA_ARGS__}; \ - REQUIRE(index_set.size() == expected.size()); \ - auto begin = index_set.begin(), end = index_set.end(); \ - for (auto range : expected) { \ - REQUIRE(*begin++ == range); \ - } \ -} while (0) +TEST_CASE("[index_set] count()") { + SECTION("returns the number of indices in the set in the given range") { + realm::IndexSet set = {1, 2, 3, 5}; + REQUIRE(set.count(0, 6) == 4); + REQUIRE(set.count(0, 5) == 3); + REQUIRE(set.count(0, 4) == 3); + REQUIRE(set.count(0, 3) == 2); + REQUIRE(set.count(0, 2) == 1); + REQUIRE(set.count(0, 1) == 0); + REQUIRE(set.count(0, 0) == 0); -TEST_CASE("index set") { + REQUIRE(set.count(0, 6) == 4); + REQUIRE(set.count(1, 6) == 4); + REQUIRE(set.count(2, 6) == 3); + REQUIRE(set.count(3, 6) == 2); + REQUIRE(set.count(4, 6) == 1); + REQUIRE(set.count(5, 6) == 1); + REQUIRE(set.count(6, 6) == 0); + } + + SECTION("includes full ranges in the middle") { + realm::IndexSet set = {1, 3, 4, 5, 10}; + REQUIRE(set.count(0, 11) == 5); + } + + SECTION("truncates ranges at the beginning and end") { + realm::IndexSet set = {1, 2, 3, 5, 6, 7, 8, 9}; + REQUIRE(set.count(3, 9) == 5); + } + + SECTION("handles full chunks well") { + size_t count = realm::_impl::ChunkedRangeVector::max_size * 4; + realm::IndexSet set; + for (size_t i = 0; i < count; ++i) { + set.add(i * 3); + set.add(i * 3 + 1); + } + + for (size_t i = 0; i < count * 3; ++i) { + REQUIRE(set.count(i) == 2 * count - (i + 1) * 2 / 3); + REQUIRE(set.count(0, i) == (i + 1) / 3 + (i + 2) / 3); + } + } +} + +TEST_CASE("[index_set] add()") { realm::IndexSet set; - SECTION("add() extends existing ranges") { + SECTION("extends existing ranges when next to an edge") { set.add(1); - REQUIRE_RANGES(set, {1, 2}); + REQUIRE_INDICES(set, 1); set.add(2); - REQUIRE_RANGES(set, {1, 3}); + REQUIRE_INDICES(set, 1, 2); set.add(0); - REQUIRE_RANGES(set, {0, 3}); + REQUIRE_INDICES(set, 0, 1, 2); } - SECTION("add() with gaps") { + SECTION("does not extend ranges over gaps") { set.add(0); - REQUIRE_RANGES(set, {0, 1}); + REQUIRE_INDICES(set, 0); set.add(2); - REQUIRE_RANGES(set, {0, 1}, {2, 3}); + REQUIRE_INDICES(set, 0, 2); } - SECTION("add() is idempotent") { + SECTION("does nothing when the index is already in the set") { set.add(0); set.add(0); - REQUIRE_RANGES(set, {0, 1}); + REQUIRE_INDICES(set, 0); } - SECTION("add() merges existing ranges") { - set.add(0); - set.add(2); - set.add(4); + SECTION("merges existing ranges when adding the index between them") { + set = {0, 2, 4}; set.add(1); - REQUIRE_RANGES(set, {0, 3}, {4, 5}); + REQUIRE_INDICES(set, 0, 1, 2, 4); } - SECTION("set() from empty") { - set.set(5); - REQUIRE_RANGES(set, {0, 5}); + SECTION("combines multiple index sets without any shifting") { + set = {0, 2, 6}; + + set.add({1, 4, 5}); + REQUIRE_INDICES(set, 0, 1, 2, 4, 5, 6); } - SECTION("set() discards existing data") { - set.add(8); - set.add(9); - - set.set(5); - REQUIRE_RANGES(set, {0, 5}); + SECTION("handles front additions of ranges") { + for (size_t i = 20; i > 0; i -= 2) + set.add(i); + REQUIRE_INDICES(set, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20); } - SECTION("insert_at() extends ranges containing the target index") { - set.add(5); - set.add(6); - - set.insert_at(5); - REQUIRE_RANGES(set, {5, 8}); - - set.insert_at(4); - REQUIRE_RANGES(set, {4, 5}, {6, 9}); - - set.insert_at(9); - REQUIRE_RANGES(set, {4, 5}, {6, 10}); - } - - SECTION("insert_at() does not modify ranges entirely before it") { - set.add(5); - set.add(6); - - set.insert_at(8); - REQUIRE_RANGES(set, {5, 7}, {8, 9}); - } - - SECTION("insert_at() shifts ranges after it") { - set.add(5); - set.add(6); - - set.insert_at(3); - REQUIRE_RANGES(set, {3, 4}, {6, 8}); - } - - SECTION("insert_at() cannot join ranges") { - set.add(5); - set.add(7); - - set.insert_at(6); - REQUIRE_RANGES(set, {5, 7}, {8, 9}); - } - - SECTION("add_shifted() on an empty set is just add()") { - set.add_shifted(5); - REQUIRE_RANGES(set, {5, 6}); - } - - SECTION("add_shifted() before the first range is just add()") { - set.add(10); - set.add_shifted(5); - REQUIRE_RANGES(set, {5, 6}, {10, 11}); - } - - SECTION("add_shifted() on first index of range extends range") { - set.add(5); - set.add_shifted(5); - REQUIRE_RANGES(set, {5, 7}); - - set.add_shifted(5); - REQUIRE_RANGES(set, {5, 8}); - - set.add_shifted(6); - REQUIRE_RANGES(set, {5, 8}, {9, 10}); - } - - SECTION("add_shifted() after ranges shifts by the size of those ranges") { - set.add(5); - set.add_shifted(6); - REQUIRE_RANGES(set, {5, 6}, {7, 8}); - - set.add_shifted(6); // bumped into second range - REQUIRE_RANGES(set, {5, 6}, {7, 9}); - - set.add_shifted(8); - REQUIRE_RANGES(set, {5, 6}, {7, 9}, {11, 12}); + SECTION("merges ranges even when they are in different chunks") { + realm::IndexSet set2; + for (int i = 0; i < 20; ++i) { + set.add(i * 2); + set2.add(i); + set2.add(i * 2); + } + set.add(set2); + REQUIRE(set.count() == 30); + } +} + +TEST_CASE("[index_set] add_shifted()") { + realm::IndexSet set; + + SECTION("on an empty set is just add()") { + set.add_shifted(5); + REQUIRE_INDICES(set, 5); + } + + SECTION("before the first range is just add()") { + set = {10}; + set.add_shifted(5); + REQUIRE_INDICES(set, 5, 10); + } + + SECTION("on first index of a range extends the range") { + set = {5}; + + set.add_shifted(5); + REQUIRE_INDICES(set, 5, 6); + + set.add_shifted(5); + REQUIRE_INDICES(set, 5, 6, 7); + } + + SECTION("in the middle of a range is shifted by that range") { + set = {5, 6, 7}; + set.add_shifted(6); + REQUIRE_INDICES(set, 5, 6, 7, 9); + } + + SECTION("after the last range adds the total count to the index to be added") { + set = {5}; + + set.add_shifted(6); + REQUIRE_INDICES(set, 5, 7); + + set.add_shifted(10); + REQUIRE_INDICES(set, 5, 7, 12); + } + + SECTION("in between ranges can be bumped into the next range") { + set = {5, 7}; + set.add_shifted(6); + REQUIRE_INDICES(set, 5, 7, 8); + } +} + +TEST_CASE("[index_set] add_shifted_by()") { + realm::IndexSet set; + + SECTION("does nothing given an empty set to add") { + set = {5, 6, 7}; + set.add_shifted_by({5, 6}, {}); + REQUIRE_INDICES(set, 5, 6, 7); + } + + SECTION("does nothing if values is a subset of shifted_by") { + set = {5, 6, 7}; + set.add_shifted_by({3, 4}, {3, 4}); + REQUIRE_INDICES(set, 5, 6, 7); + } + + SECTION("just adds the indices when they are all before the old indices and the shifted-by set is empty") { + set = {5, 6}; + set.add_shifted_by({}, {3, 4}); + REQUIRE_INDICES(set, 3, 4, 5, 6); + } + + SECTION("adds the indices shifted by the old count when they are all after the old indices and the shifted-by set is empty") { + set = {5, 6}; + set.add_shifted_by({}, {7, 9, 11, 13}); + REQUIRE_INDICES(set, 5, 6, 9, 11, 13, 15); + } + + SECTION("acts like bulk add_shifted() when shifted_by is empty") { + set = {5, 10, 15, 20, 25}; + set.add_shifted_by({}, {4, 5, 11}); + REQUIRE_INDICES(set, 4, 5, 6, 10, 13, 15, 20, 25); + } + + SECTION("shifts indices in values back by the number of indices in shifted_by before them") { + set = {5}; + set.add_shifted_by({0, 2, 3}, {6}); + REQUIRE_INDICES(set, 3, 5); + + set = {5}; + set.add_shifted_by({1, 3}, {4}); + REQUIRE_INDICES(set, 2, 5); + } + + SECTION("discards indices in both shifted_by and values") { + set = {5}; + set.add_shifted_by({2}, {2, 4}); + REQUIRE_INDICES(set, 3, 5); + } +} + +TEST_CASE("[index_set] set()") { + realm::IndexSet set; + + SECTION("clears the existing indices and replaces with the range [0, value)") { + set = {8, 9}; + set.set(5); + REQUIRE_INDICES(set, 0, 1, 2, 3, 4); + } +} + +TEST_CASE("[index_set] insert_at()") { + realm::IndexSet set; + + SECTION("on an empty set is add()") { + set.insert_at(5); + REQUIRE_INDICES(set, 5); + + set = {}; + set.insert_at({1, 3, 5}); + REQUIRE_INDICES(set, 1, 3, 5); + } + + SECTION("with an empty set is a no-op") { + set = {5, 6}; + set.insert_at(realm::IndexSet{}); + REQUIRE_INDICES(set, 5, 6); + } + + SECTION("extends ranges containing the target range") { + set = {5, 6}; + + set.insert_at(5); + REQUIRE_INDICES(set, 5, 6, 7); + + set.insert_at(6, 2); + REQUIRE_INDICES(set, 5, 6, 7, 8, 9); + + set.insert_at({5, 7, 11}); + REQUIRE_INDICES(set, 5, 6, 7, 8, 9, 10, 11, 12); + } + + SECTION("shifts ranges after the insertion point") { + set = {5, 6}; + + set.insert_at(3); + REQUIRE_INDICES(set, 3, 6, 7); + + set.insert_at(0, 2); + REQUIRE_INDICES(set, 0, 1, 5, 8, 9); + } + + SECTION("does not shift ranges before the insertion point") { + set = {5, 6}; + + set.insert_at(10); + REQUIRE_INDICES(set, 5, 6, 10); + + set.insert_at({15, 16}); + REQUIRE_INDICES(set, 5, 6, 10, 15, 16); + } + + SECTION("can not join ranges") { + set = {5, 7}; + set.insert_at(6); + REQUIRE_INDICES(set, 5, 6, 8); + } + + SECTION("adds later ranges after shifting for previous insertions") { + set = {5, 10}; + set.insert_at({5, 10}); + REQUIRE_INDICES(set, 5, 6, 10, 12); + } +} + +TEST_CASE("[index_set] shift_for_insert_at()") { + realm::IndexSet set; + + SECTION("does nothing given an empty set of insertion points") { + set = {5, 8}; + set.shift_for_insert_at(realm::IndexSet{}); + REQUIRE_INDICES(set, 5, 8); + } + + SECTION("does nothing when called on an empty set") { + set = {}; + set.shift_for_insert_at({5, 8}); + REQUIRE(set.empty()); + } + + SECTION("does nothing when the insertion points are all after the current indices") { + set = {10, 20}; + set.shift_for_insert_at({30, 40}); + REQUIRE_INDICES(set, 10, 20); + } + + SECTION("does shift when the insertion points are all before the current indices") { + set = {10, 20}; + set.shift_for_insert_at({2, 4}); + REQUIRE_INDICES(set, 12, 22); + } + + SECTION("shifts indices at or after the insertion points") { + set = {5}; + + set.shift_for_insert_at(4); + REQUIRE_INDICES(set, 6); + + set.shift_for_insert_at(6); + REQUIRE_INDICES(set, 7); + + set.shift_for_insert_at({3, 8}); + REQUIRE_INDICES(set, 9); + } + + SECTION("shifts indices by the count specified") { + set = {5}; + set.shift_for_insert_at(3, 10); + REQUIRE_INDICES(set, 15); + } + + SECTION("does not shift indices before the insertion points") { + set = {5}; + + set.shift_for_insert_at(6); + REQUIRE_INDICES(set, 5); + + set.shift_for_insert_at({3, 8}); + REQUIRE_INDICES(set, 6); + } + + SECTION("splits ranges containing the insertion points") { + set = {5, 6, 7, 8}; + + set.shift_for_insert_at(6); + REQUIRE_INDICES(set, 5, 7, 8, 9); + + set.shift_for_insert_at({8, 10, 12}); + REQUIRE_INDICES(set, 5, 7, 9, 11); + } +} + +TEST_CASE("[index_set] erase_at()") { + realm::IndexSet set; + + SECTION("is a no-op on an empty set") { + set.erase_at(10); + REQUIRE(set.empty()); + + set.erase_at({1, 5, 8}); + REQUIRE(set.empty()); + } + + SECTION("does nothing when given an empty set") { + set = {5}; + set.erase_at(realm::IndexSet{}); + REQUIRE_INDICES(set, 5); + } + + SECTION("removes the specified indices") { + set = {5}; + set.erase_at(5); + REQUIRE(set.empty()); + + set = {4, 7}; + set.erase_at({4, 7}); + REQUIRE(set.empty()); + } + + SECTION("does not modify indices before the removed one") { + set = {5, 8}; + set.erase_at(8); + REQUIRE_INDICES(set, 5); + + set = {5, 8, 9}; + set.erase_at({8, 9}); + REQUIRE_INDICES(set, 5); + } + + SECTION("shifts indices after the removed one") { + set = {5, 8}; + set.erase_at(5); + REQUIRE_INDICES(set, 7); + + set = {5, 10, 15, 20}; + set.erase_at({5, 10}); + REQUIRE_INDICES(set, 13, 18); + } + + SECTION("shrinks ranges when used on one of the edges of them") { + set = {5, 6, 7, 8}; + set.erase_at(8); + REQUIRE_INDICES(set, 5, 6, 7); + set.erase_at(5); + REQUIRE_INDICES(set, 5, 6); + + set = {5, 6, 7, 8}; + set.erase_at({5, 8}); + REQUIRE_INDICES(set, 5, 6); + } + + SECTION("shrinks ranges when used in the middle of them") { + set = {5, 6, 7, 8}; + set.erase_at(7); + REQUIRE_INDICES(set, 5, 6, 7); + + set = {5, 6, 7, 8}; + set.erase_at({6, 7}); + REQUIRE_INDICES(set, 5, 6); + } + + SECTION("merges ranges when the gap between them is deleted") { + set = {3, 5}; + set.erase_at(4); + REQUIRE_INDICES(set, 3, 4); + + set = {3, 5, 7}; + set.erase_at({4, 6}); + REQUIRE_INDICES(set, 3, 4, 5); + } +} + +TEST_CASE("[index_set] erase_or_unshift()") { + realm::IndexSet set; + + SECTION("removes the given index") { + set = {1, 2}; + set.erase_or_unshift(2); + REQUIRE_INDICES(set, 1); + } + + SECTION("shifts indexes after the given index") { + set = {1, 5}; + set.erase_or_unshift(2); + REQUIRE_INDICES(set, 1, 4); + } + + SECTION("returns npos for indices in the set") { + set = {1, 3, 5}; + REQUIRE(realm::IndexSet(set).erase_or_unshift(1) == realm::IndexSet::npos); + REQUIRE(realm::IndexSet(set).erase_or_unshift(3) == realm::IndexSet::npos); + REQUIRE(realm::IndexSet(set).erase_or_unshift(5) == realm::IndexSet::npos); + } + + SECTION("returns the number of indices in the set before the index for ones not in the set") { + set = {1, 3, 5, 6}; + REQUIRE(realm::IndexSet(set).erase_or_unshift(0) == 0); + REQUIRE(realm::IndexSet(set).erase_or_unshift(2) == 1); + REQUIRE(realm::IndexSet(set).erase_or_unshift(4) == 2); + REQUIRE(realm::IndexSet(set).erase_or_unshift(7) == 3); + } + +} + +TEST_CASE("[index_set] remove()") { + realm::IndexSet set; + + SECTION("is a no-op if the set is empty") { + set.remove(4); + REQUIRE(set.empty()); + + set.remove({1, 2, 3}); + REQUIRE(set.empty()); + } + + SECTION("is a no-op if the set to remove is empty") { + set = {5}; + set.remove(realm::IndexSet{}); + REQUIRE_INDICES(set, 5); + } + + SECTION("is a no-op if the index to remove is not in the set") { + set = {5}; + set.remove(4); + set.remove(6); + set.remove({4, 6}); + REQUIRE_INDICES(set, 5); + } + + SECTION("removes one-element ranges") { + set = {5}; + set.remove(5); + REQUIRE(set.empty()); + + set = {5}; + set.remove({3, 4, 5}); + REQUIRE(set.empty()); + } + + SECTION("shrinks ranges beginning with the index") { + set = {5, 6, 7}; + set.remove(5); + REQUIRE_INDICES(set, 6, 7); + + set = {5, 6, 7}; + set.remove({3, 5}); + REQUIRE_INDICES(set, 6, 7); + } + + SECTION("shrinks ranges ending with the index") { + set = {5, 6, 7}; + set.remove(7); + REQUIRE_INDICES(set, 5, 6); + + set = {5, 6, 7}; + set.remove({3, 7}); + REQUIRE_INDICES(set, 5, 6); + } + + SECTION("splits ranges containing the index") { + set = {5, 6, 7}; + set.remove(6); + REQUIRE_INDICES(set, 5, 7); + + set = {5, 6, 7}; + set.remove({3, 6}); + REQUIRE_INDICES(set, 5, 7); + } + + SECTION("does not shift other indices and uses unshifted positions") { + set = {5, 6, 7, 10, 11, 12, 13, 15}; + set.remove({6, 11, 13}); + REQUIRE_INDICES(set, 5, 7, 10, 12, 15); + } +} + +TEST_CASE("[index_set] shift()") { + realm::IndexSet set; + + SECTION("is ind + count(0, ind), but adds the count-so-far to the stop index") { + set = {1, 3, 5, 6}; + REQUIRE(set.shift(0) == 0); + REQUIRE(set.shift(1) == 2); + REQUIRE(set.shift(2) == 4); + REQUIRE(set.shift(3) == 7); + REQUIRE(set.shift(4) == 8); + } +} + +TEST_CASE("[index_set] unshift()") { + realm::IndexSet set; + + SECTION("is index - count(0, index)") { + set = {1, 3, 5, 6}; + REQUIRE(set.unshift(0) == 0); + REQUIRE(set.unshift(2) == 1); + REQUIRE(set.unshift(4) == 2); + REQUIRE(set.unshift(7) == 3); + REQUIRE(set.unshift(8) == 4); + } +} + +TEST_CASE("[index_set] clear()") { + realm::IndexSet set; + + SECTION("removes all indices from the set") { + set = {1, 2, 3}; + set.clear(); + REQUIRE(set.empty()); } } diff --git a/tests/list.cpp b/tests/list.cpp new file mode 100644 index 00000000..4495762a --- /dev/null +++ b/tests/list.cpp @@ -0,0 +1,450 @@ +#include "catch.hpp" + +#include "util/test_file.hpp" +#include "util/index_helpers.hpp" + +#include "binding_context.hpp" +#include "list.hpp" +#include "object_schema.hpp" +#include "property.hpp" +#include "results.hpp" +#include "schema.hpp" + +#include "impl/realm_coordinator.hpp" + +#include +#include +#include + +using namespace realm; + +TEST_CASE("list") { + InMemoryTestFile config; + config.automatic_change_notifications = false; + config.cache = false; + config.schema = std::make_unique(Schema{ + {"origin", "", { + {"array", PropertyType::Array, "target"} + }}, + {"target", "", { + {"value", PropertyType::Int} + }}, + {"other_origin", "", { + {"array", PropertyType::Array, "other_target"} + }}, + {"other_target", "", { + {"value", PropertyType::Int} + }}, + }); + + auto r = Realm::get_shared_realm(config); + auto& coordinator = *_impl::RealmCoordinator::get_existing_coordinator(config.path); + + auto origin = r->read_group()->get_table("class_origin"); + auto target = r->read_group()->get_table("class_target"); + + r->begin_transaction(); + + target->add_empty_row(10); + for (int i = 0; i < 10; ++i) + target->set_int(0, i, i); + + origin->add_empty_row(2); + LinkViewRef lv = origin->get_linklist(0, 0); + for (int i = 0; i < 10; ++i) + lv->add(i); + LinkViewRef lv2 = origin->get_linklist(0, 1); + for (int i = 0; i < 10; ++i) + lv2->add(i); + + r->commit_transaction(); + + SECTION("add_notification_block()") { + CollectionChangeSet change; + List lst(r, *r->config().schema->find("origin"), lv); + + auto write = [&](auto&& f) { + r->begin_transaction(); + f(); + r->commit_transaction(); + + advance_and_notify(*r); + }; + + auto require_change = [&] { + auto token = lst.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { + change = c; + }); + advance_and_notify(*r); + return token; + }; + + auto require_no_change = [&] { + bool first = true; + auto token = lst.add_notification_callback([&, first](CollectionChangeSet c, std::exception_ptr err) mutable { + REQUIRE(first); + first = false; + }); + advance_and_notify(*r); + return token; + }; + + SECTION("modifying the list sends a change notifications") { + auto token = require_change(); + write([&] { lst.remove(5); }); + REQUIRE_INDICES(change.deletions, 5); + } + + SECTION("modifying a different list doesn't send a change notification") { + auto token = require_no_change(); + write([&] { lv2->remove(5); }); + } + + SECTION("deleting the list sends a change notification") { + auto token = require_change(); + write([&] { origin->move_last_over(0); }); + REQUIRE_INDICES(change.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + + // Should not resend delete all notification after another commit + change = {}; + write([&] { target->add_empty_row(); }); + REQUIRE(change.empty()); + } + + SECTION("modifying one of the target rows sends a change notification") { + auto token = require_change(); + write([&] { lst.get(5).set_int(0, 6); }); + REQUIRE_INDICES(change.modifications, 5); + } + + SECTION("deleting a target row sends a change notification") { + auto token = require_change(); + write([&] { target->move_last_over(5); }); + REQUIRE_INDICES(change.deletions, 5); + } + + SECTION("adding a row and then modifying the target row does not mark the row as modified") { + auto token = require_change(); + write([&] { + lst.add(5); + target->set_int(0, 5, 10); + }); + REQUIRE_INDICES(change.insertions, 10); + REQUIRE_INDICES(change.modifications, 5); + } + + SECTION("modifying and then moving a row reports move/insert but not modification") { + auto token = require_change(); + write([&] { + target->set_int(0, 5, 10); + lst.move(5, 8); + }); + REQUIRE_INDICES(change.insertions, 8); + REQUIRE_INDICES(change.deletions, 5); + REQUIRE_MOVES(change, {5, 8}); + REQUIRE(change.modifications.empty()); + } + + SECTION("modifying a row which appears multiple times in a list marks them all as modified") { + r->begin_transaction(); + lst.add(5); + r->commit_transaction(); + + auto token = require_change(); + write([&] { target->set_int(0, 5, 10); }); + REQUIRE_INDICES(change.modifications, 5, 10); + } + + SECTION("deleting a row which appears multiple times in a list marks them all as modified") { + r->begin_transaction(); + lst.add(5); + r->commit_transaction(); + + auto token = require_change(); + write([&] { target->move_last_over(5); }); + REQUIRE_INDICES(change.deletions, 5, 10); + } + + SECTION("clearing the target table sends a change notification") { + auto token = require_change(); + write([&] { target->clear(); }); + REQUIRE_INDICES(change.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + SECTION("moving a target row does not send a change notification") { + // Remove a row from the LV so that we have one to delete that's not in the list + r->begin_transaction(); + lv->remove(2); + r->commit_transaction(); + + auto token = require_no_change(); + write([&] { target->move_last_over(2); }); + } + + SECTION("multiple LinkViws for the same LinkList can get notifications") { + r->begin_transaction(); + target->clear(); + target->add_empty_row(5); + r->commit_transaction(); + + auto get_list = [&] { + auto r = Realm::get_shared_realm(config); + auto lv = r->read_group()->get_table("class_origin")->get_linklist(0, 0); + return List(r, *r->config().schema->find("origin"), lv); + }; + auto change_list = [&] { + r->begin_transaction(); + if (lv->size()) { + target->set_int(0, lv->size() - 1, lv->size()); + } + lv->add(lv->size()); + r->commit_transaction(); + }; + + List lists[3]; + NotificationToken tokens[3]; + CollectionChangeSet changes[3]; + + for (int i = 0; i < 3; ++i) { + lists[i] = get_list(); + tokens[i] = lists[i].add_notification_callback([i, &changes](CollectionChangeSet c, std::exception_ptr) { + changes[i] = std::move(c); + }); + change_list(); + } + + // Each of the Lists now has a different source version and state at + // that version, so they should all see different changes despite + // being for the same LinkList + for (auto& list : lists) + advance_and_notify(*list.get_realm()); + + REQUIRE_INDICES(changes[0].insertions, 0, 1, 2); + REQUIRE(changes[0].modifications.empty()); + + REQUIRE_INDICES(changes[1].insertions, 1, 2); + REQUIRE_INDICES(changes[1].modifications, 0); + + REQUIRE_INDICES(changes[2].insertions, 2); + REQUIRE_INDICES(changes[2].modifications, 1); + + // After making another change, they should all get the same notification + change_list(); + for (auto& list : lists) + advance_and_notify(*list.get_realm()); + + for (int i = 0; i < 3; ++i) { + REQUIRE_INDICES(changes[i].insertions, 3); + REQUIRE_INDICES(changes[i].modifications, 2); + } + } + + SECTION("tables-of-interest are tracked properly for multiple source versions") { + auto other_origin = r->read_group()->get_table("class_other_origin"); + auto other_target = r->read_group()->get_table("class_other_target"); + + r->begin_transaction(); + other_target->add_empty_row(); + other_origin->add_empty_row(); + LinkViewRef lv2 = other_origin->get_linklist(0, 0); + lv2->add(0); + r->commit_transaction(); + + List lst2(r, *r->config().schema->find("other_origin"), lv2); + + // Add a callback for list1, advance the version, then add a + // callback for list2, so that the notifiers added at each source + // version have different tables watched for modifications + CollectionChangeSet changes1, changes2; + auto token1 = lst.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) { + changes1 = std::move(c); + }); + + r->begin_transaction(); r->commit_transaction(); + + auto token2 = lst2.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) { + changes2 = std::move(c); + }); + + r->begin_transaction(); + target->set_int(0, 0, 10); + r->commit_transaction(); + advance_and_notify(*r); + + REQUIRE_INDICES(changes1.modifications, 0); + REQUIRE(changes2.empty()); + } + + SECTION("modifications are reported for rows that are moved and then moved back in a second transaction") { + auto token = require_change(); + + r->begin_transaction(); + lv->get(5).set_int(0, 10); + lv->get(1).set_int(0, 10); + lv->move(5, 8); + lv->move(1, 2); + r->commit_transaction(); + + coordinator.on_change(); + + write([&]{ + lv->move(8, 5); + }); + + REQUIRE_INDICES(change.deletions, 1); + REQUIRE_INDICES(change.insertions, 2); + REQUIRE_INDICES(change.modifications, 5); + REQUIRE_MOVES(change, {1, 2}); + } + } + + SECTION("sorted add_notification_block()") { + List lst(r, *r->config().schema->find("origin"), lv); + Results results = lst.sort({{0}, {false}}); + + int notification_calls = 0; + CollectionChangeSet change; + auto token = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { + REQUIRE_FALSE(err); + change = c; + ++notification_calls; + }); + + advance_and_notify(*r); + + auto write = [&](auto&& f) { + r->begin_transaction(); + f(); + r->commit_transaction(); + + advance_and_notify(*r); + }; + + SECTION("add duplicates") { + write([&] { + lst.add(5); + lst.add(5); + lst.add(5); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.insertions, 5, 6, 7); + } + + SECTION("change order by modifying target") { + write([&] { + lst.get(5).set_int(0, 15); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 4); + REQUIRE_INDICES(change.insertions, 0); + } + + SECTION("swap") { + write([&] { + lst.swap(1, 2); + }); + REQUIRE(notification_calls == 1); + } + + SECTION("move") { + write([&] { + lst.move(5, 3); + }); + REQUIRE(notification_calls == 1); + } + } + + SECTION("filtered add_notification_block()") { + List lst(r, *r->config().schema->find("origin"), lv); + Results results = lst.filter(target->where().less(0, 9)); + + int notification_calls = 0; + CollectionChangeSet change; + auto token = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { + REQUIRE_FALSE(err); + change = c; + ++notification_calls; + }); + + advance_and_notify(*r); + + auto write = [&](auto&& f) { + r->begin_transaction(); + f(); + r->commit_transaction(); + + advance_and_notify(*r); + }; + + SECTION("add duplicates") { + write([&] { + lst.add(5); + lst.add(5); + lst.add(5); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.insertions, 9, 10, 11); + } + + SECTION("swap") { + write([&] { + lst.swap(1, 2); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 2); + REQUIRE_INDICES(change.insertions, 1); + + write([&] { + lst.swap(5, 8); + }); + REQUIRE(notification_calls == 3); + REQUIRE_INDICES(change.deletions, 5, 8); + REQUIRE_INDICES(change.insertions, 5, 8); + } + + SECTION("move") { + write([&] { + lst.move(5, 3); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 5); + REQUIRE_INDICES(change.insertions, 3); + } + + SECTION("move non-matching entry") { + write([&] { + lst.move(9, 3); + }); + REQUIRE(notification_calls == 1); + } + } + + SECTION("sort()") { + auto objectschema = &*r->config().schema->find("origin"); + List list(r, *objectschema, lv); + auto results = list.sort({{0}, {false}}); + + REQUIRE(&results.get_object_schema() == objectschema); + REQUIRE(results.get_mode() == Results::Mode::LinkView); + REQUIRE(results.size() == 10); + REQUIRE(results.sum(0) == 45); + + for (size_t i = 0; i < 10; ++i) { + REQUIRE(results.get(i).get_index() == 9 - i); + } + } + + SECTION("filter()") { + auto objectschema = &*r->config().schema->find("origin"); + List list(r, *objectschema, lv); + auto results = list.filter(target->where().greater(0, 5)); + + REQUIRE(&results.get_object_schema() == objectschema); + REQUIRE(results.get_mode() == Results::Mode::Query); + REQUIRE(results.size() == 4); + + for (size_t i = 0; i < 4; ++i) { + REQUIRE(results.get(i).get_index() == i + 6); + } + } +} diff --git a/tests/notifications-fuzzer/CMakeLists.txt b/tests/notifications-fuzzer/CMakeLists.txt new file mode 100644 index 00000000..a23d7c16 --- /dev/null +++ b/tests/notifications-fuzzer/CMakeLists.txt @@ -0,0 +1,13 @@ +macro(build_fuzzer_variant variant) + add_executable(${variant} command_file.hpp command_file.cpp ${variant}.cpp) + target_link_libraries(${variant} realm-object-store) + set_target_properties(${variant} PROPERTIES + EXCLUDE_FROM_ALL 1 + EXCLUDE_FROM_DEFAULT_BUILD 1) +endmacro() + +build_fuzzer_variant(fuzzer) +build_fuzzer_variant(fuzz-sorted-query) +build_fuzzer_variant(fuzz-unsorted-query) +build_fuzzer_variant(fuzz-sorted-linkview) +build_fuzzer_variant(fuzz-unsorted-linkview) diff --git a/tests/notifications-fuzzer/command_file.cpp b/tests/notifications-fuzzer/command_file.cpp new file mode 100644 index 00000000..7fbde719 --- /dev/null +++ b/tests/notifications-fuzzer/command_file.cpp @@ -0,0 +1,228 @@ +#include "command_file.hpp" + +#include "impl/realm_coordinator.hpp" +#include "shared_realm.hpp" + +#include +#include + +#include + +using namespace fuzzer; +using namespace realm; + +#if 0 +#define log(...) fprintf(stderr, __VA_ARGS__) +#else +#define log(...) +#endif + +template +static T read_value(std::istream& input) +{ + T ret; + input >> ret; + return ret; +} + +template +static auto make_reader(void (*fn)(RealmState&, Args...)) { + return [=](std::istream& input) { + return std::bind(fn, std::placeholders::_1, read_value(input)...); + }; +} + +static void run_add(RealmState& state, int64_t value) +{ + log("add %lld\n", value); + size_t ndx = state.table.add_empty_row(); + state.table.set_int(0, ndx, state.uid++); + state.table.set_int(1, ndx, value); +} + +static void run_modify(RealmState& state, size_t index, int64_t value) +{ + if (index < state.table.size()) { + log("modify %zu %lld\n", index, value); + state.table.set_int(1, index, value); + state.modified.push_back(state.table.get_int(0, index)); + } +} + +static void run_delete(RealmState& state, size_t index) +{ + if (index < state.table.size()) { + log("delete %zu (%lld)\n", index, state.table.get_int(1, index)); + state.table.move_last_over(index); + } +} + +static void run_commit(RealmState& state) +{ + log("commit\n"); + state.realm.commit_transaction(); + state.coordinator.on_change(); + state.realm.begin_transaction(); +} + +static void run_lv_insert(RealmState& state, size_t pos, size_t target) +{ + if (!state.lv) return; + if (target < state.table.size() && pos <= state.lv->size()) { + log("lv insert %zu %zu\n", pos, target); + state.lv->insert(pos, target); + } +} + +static void run_lv_set(RealmState& state, size_t pos, size_t target) +{ + if (!state.lv) return; + if (target < state.table.size() && pos < state.lv->size()) { + log("lv set %zu %zu\n", pos, target); + // We can't reliably detect self-assignment for verification, so don't do it + if (state.lv->get(pos).get_index() != target) + state.lv->set(pos, target); + } +} + +static void run_lv_move(RealmState& state, size_t from, size_t to) +{ + if (!state.lv) return; + if (from < state.lv->size() && to < state.lv->size()) { + log("lv move %zu %zu\n", from, to); + // FIXME: only do the move if it has an effect to avoid getting a + // notification which we weren't expecting. This is really urgh. + for (size_t i = std::min(from, to); i < std::max(from, to); ++i) { + if (state.lv->get(i).get_index() != state.lv->get(i + 1).get_index()) { + state.lv->move(from, to); + break; + } + } + } +} + +static void run_lv_swap(RealmState& state, size_t ndx1, size_t ndx2) +{ + if (!state.lv) return; + if (ndx1 < state.lv->size() && ndx2 < state.lv->size()) { + log("lv swap %zu %zu\n", ndx1, ndx2); + if (state.lv->get(ndx1).get_index() != state.lv->get(ndx2).get_index()) { + state.lv->swap(ndx1, ndx2); + // FIXME: swap() needs to produce moves so that a pair of swaps can + // be collapsed away. Currently it just marks the rows as modified. + state.modified.push_back(state.lv->get(ndx1).get_int(0)); + state.modified.push_back(state.lv->get(ndx2).get_int(0)); + } + } +} + +static void run_lv_remove(RealmState& state, size_t pos) +{ + if (!state.lv) return; + if (pos < state.lv->size()) { + log("lv remove %zu\n", pos); + state.lv->remove(pos); + } +} + +static void run_lv_remove_target(RealmState& state, size_t pos) +{ + if (!state.lv) return; + if (pos < state.lv->size()) { + log("lv target remove %zu\n", pos); + state.lv->remove_target_row(pos); + } +} + +static std::map(std::istream&)>> readers = { + // Row functions + {'a', make_reader(run_add)}, + {'c', make_reader(run_commit)}, + {'d', make_reader(run_delete)}, + {'m', make_reader(run_modify)}, + + // LinkView functions + {'i', make_reader(run_lv_insert)}, + {'s', make_reader(run_lv_set)}, + {'o', make_reader(run_lv_move)}, + {'w', make_reader(run_lv_swap)}, + {'r', make_reader(run_lv_remove)}, + {'t', make_reader(run_lv_remove_target)}, +}; + +template +static std::vector read_int_list(std::istream& input_stream) +{ + std::vector ret; + std::string line; + while (std::getline(input_stream, line) && !line.empty()) { + try { + ret.push_back(std::stoll(line)); + log("%lld\n", (long long)ret.back()); + } + catch (std::invalid_argument) { + // not an error + } + catch (std::out_of_range) { + // not an error + } + } + log("\n"); + return ret; +} + +CommandFile::CommandFile(std::istream& input) +: initial_values(read_int_list(input)) +, initial_list_indices(read_int_list(input)) +{ + if (!input.good()) + return; + + while (input.good()) { + char op = '\0'; + input >> op; + if (!input.good()) + break; + + auto it = readers.find(op); + if (it == readers.end()) + continue; + + auto fn = it->second(input); + if (!input.good()) + return; + commands.push_back(std::move(fn)); + } +} + +void CommandFile::import(RealmState& state) +{ + auto& table = state.table; + + state.realm.begin_transaction(); + + table.clear(); + size_t ndx = table.add_empty_row(initial_values.size()); + for (auto value : initial_values) { + table.set_int(0, ndx, state.uid++); + table.set_int(1, ndx++, value); + } + + state.lv->clear(); + for (auto value : initial_list_indices) { + if (value < table.size()) + state.lv->add(value); + } + + state.realm.commit_transaction(); + +} + +void CommandFile::run(RealmState& state) +{ + state.realm.begin_transaction(); + for (auto& command : commands) { + command(state); + } + state.realm.commit_transaction(); +} diff --git a/tests/notifications-fuzzer/command_file.hpp b/tests/notifications-fuzzer/command_file.hpp new file mode 100644 index 00000000..7de23d43 --- /dev/null +++ b/tests/notifications-fuzzer/command_file.hpp @@ -0,0 +1,38 @@ +#include + +#include +#include +#include +#include + +namespace realm { + class Table; + class LinkView; + class Realm; + namespace _impl { + class RealmCoordinator; + } +} + +namespace fuzzer { +struct RealmState { + realm::Realm& realm; + realm::_impl::RealmCoordinator& coordinator; + + realm::Table& table; + realm::LinkViewRef lv; + int64_t uid; + std::vector modified; +}; + +struct CommandFile { + std::vector initial_values; + std::vector initial_list_indices; + std::vector> commands; + + CommandFile(std::istream& input); + + void import(RealmState& state); + void run(RealmState& state); +}; +} \ No newline at end of file diff --git a/tests/notifications-fuzzer/fuzz-sorted-linkview.cpp b/tests/notifications-fuzzer/fuzz-sorted-linkview.cpp new file mode 100644 index 00000000..13d9bec0 --- /dev/null +++ b/tests/notifications-fuzzer/fuzz-sorted-linkview.cpp @@ -0,0 +1,3 @@ +#define FUZZ_SORTED 1 +#define FUZZ_LINKVIEW 1 +#include "fuzzer.cpp" diff --git a/tests/notifications-fuzzer/fuzz-sorted-query.cpp b/tests/notifications-fuzzer/fuzz-sorted-query.cpp new file mode 100644 index 00000000..b32e9dc3 --- /dev/null +++ b/tests/notifications-fuzzer/fuzz-sorted-query.cpp @@ -0,0 +1,3 @@ +#define FUZZ_SORTED 1 +#define FUZZ_LINKVIEW 0 +#include "fuzzer.cpp" diff --git a/tests/notifications-fuzzer/fuzz-unsorted-linkview.cpp b/tests/notifications-fuzzer/fuzz-unsorted-linkview.cpp new file mode 100644 index 00000000..24d25184 --- /dev/null +++ b/tests/notifications-fuzzer/fuzz-unsorted-linkview.cpp @@ -0,0 +1,3 @@ +#define FUZZ_SORTED 0 +#define FUZZ_LINKVIEW 1 +#include "fuzzer.cpp" diff --git a/tests/notifications-fuzzer/fuzz-unsorted-query.cpp b/tests/notifications-fuzzer/fuzz-unsorted-query.cpp new file mode 100644 index 00000000..6dec4c74 --- /dev/null +++ b/tests/notifications-fuzzer/fuzz-unsorted-query.cpp @@ -0,0 +1,3 @@ +#define FUZZ_SORTED 0 +#define FUZZ_LINKVIEW 0 +#include "fuzzer.cpp" diff --git a/tests/notifications-fuzzer/fuzzer.cpp b/tests/notifications-fuzzer/fuzzer.cpp new file mode 100644 index 00000000..6b088e85 --- /dev/null +++ b/tests/notifications-fuzzer/fuzzer.cpp @@ -0,0 +1,291 @@ +#include "command_file.hpp" + +#include "list.hpp" +#include "object_schema.hpp" +#include "property.hpp" +#include "results.hpp" +#include "schema.hpp" +#include "impl/realm_coordinator.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace realm; + +#ifndef FUZZ_SORTED +#define FUZZ_SORTED 0 +#endif + +#ifndef FUZZ_LINKVIEW +#define FUZZ_LINKVIEW 0 +#endif + +#define FUZZ_LOG 0 + +// Read from a fd until eof into a string +// Needs to use unbuffered i/o to work properly with afl +static void read_all(std::string& buffer, int fd) +{ + buffer.clear(); + size_t offset = 0; + while (true) { + buffer.resize(offset + 4096); + ssize_t bytes_read = read(fd, &buffer[offset], 4096); + if (bytes_read < 4096) { + buffer.resize(offset + bytes_read); + break; + } + offset += 4096; + } +} + +static Query query(fuzzer::RealmState& state) +{ +#if FUZZ_LINKVIEW + return state.table.where(state.lv); +#else + return state.table.where().greater(1, 100).less(1, 50000); +#endif +} + +static TableView tableview(fuzzer::RealmState& state) +{ + auto tv = query(state).find_all(); +#if FUZZ_SORTED + tv.sort({1, 0}, {true, true}); +#endif + return tv; +} + +// Apply the changes from the command file and then return whether a change +// notification should occur +static bool apply_changes(fuzzer::CommandFile& commands, fuzzer::RealmState& state) +{ + auto tv = tableview(state); +#if FUZZ_LOG + for (size_t i = 0; i < tv.size(); ++i) + fprintf(stderr, "pre: %lld\n", tv.get_int(0, i)); +#endif + + commands.run(state); + + auto tv2 = tableview(state); + if (tv.size() != tv2.size()) + return true; + + for (size_t i = 0; i < tv.size(); ++i) { +#if FUZZ_LOG + fprintf(stderr, "%lld %lld\n", tv.get_int(0, i), tv2.get_int(0, i)); +#endif + if (!tv.is_row_attached(i)) + return true; + if (tv.get_int(0, i) != tv2.get_int(0, i)) + return true; + if (find(begin(state.modified), end(state.modified), tv.get_int(0, i)) != end(state.modified)) + return true; + } + + return false; +} + +static auto verify(CollectionChangeIndices const& changes, std::vector values, fuzzer::RealmState& state) +{ + auto tv = tableview(state); + + // Apply the changes from the transaction log to our copy of the + // initial, using UITableView's batching rules (i.e. delete, then + // insert, then update) + auto it = util::make_reverse_iterator(changes.deletions.end()); + auto end = util::make_reverse_iterator(changes.deletions.begin()); + for (; it != end; ++it) { + values.erase(values.begin() + it->first, values.begin() + it->second); + } + + for (auto i : changes.insertions.as_indexes()) { + values.insert(values.begin() + i, tv.get_int(1, i)); + } + + if (values.size() != tv.size()) { + abort(); + } + + for (auto i : changes.modifications.as_indexes()) { + if (changes.insertions.contains(i)) + abort(); + values[i] = tv.get_int(1, i); + } + +#if FUZZ_SORTED + if (!std::is_sorted(values.begin(), values.end())) + abort(); +#endif + + for (size_t i = 0; i < values.size(); ++i) { + if (values[i] != tv.get_int(1, i)) { +#if FUZZ_LOG + fprintf(stderr, "%lld %lld\n", values[i], tv.get_int(1, i)); +#endif + abort(); + } + } + + return values; +} + +static void verify_no_op(CollectionChangeIndices const& changes, std::vector values, fuzzer::RealmState& state) +{ + auto new_values = verify(changes, values, state); + if (!std::equal(begin(values), end(values), begin(new_values), end(new_values))) + abort(); +} + +static void test(Realm::Config const& config, SharedRealm& r, SharedRealm& r2, std::istream& input_stream) +{ + fuzzer::RealmState state = { + *r, + *_impl::RealmCoordinator::get_existing_coordinator(r->config().path), + *r->read_group()->get_table("class_object"), + r->read_group()->get_table("class_linklist")->get_linklist(0, 0), + 0, + {} + }; + + fuzzer::CommandFile command(input_stream); + if (command.initial_values.empty()) { + return; + } + command.import(state); + + fuzzer::RealmState state2 = { + *r2, + state.coordinator, + *r2->read_group()->get_table("class_object"), +#if FUZZ_LINKVIEW + r2->read_group()->get_table("class_linklist")->get_linklist(0, 0), +#else + {}, +#endif + state.uid, + {} + }; + +#if FUZZ_LINKVIEW && !FUZZ_SORTED + auto results = List(r, ObjectSchema(), state.lv); +#else + auto results = Results(r, ObjectSchema(), query(state)) +#if FUZZ_SORTED + .sort({{1, 0}, {true, true}}) +#endif + ; +#endif // FUZZ_LINKVIEW + + std::vector initial_values; + for (size_t i = 0; i < results.size(); ++i) + initial_values.push_back(results.get(i).get_int(1)); + + CollectionChangeIndices changes; + int notification_calls = 0; + auto token = results.add_notification_callback([&](CollectionChangeIndices c, std::exception_ptr err) { + if (notification_calls > 0 && c.empty()) + abort(); + changes = c; + ++notification_calls; + }); + + state.coordinator.on_change(); r->notify(); + if (notification_calls != 1) { + abort(); + } + + bool expect_notification = apply_changes(command, state2); + state.coordinator.on_change(); r->notify(); + + if (expect_notification) { + if (notification_calls != 2) + abort(); + verify(changes, initial_values, state); + } + else { + if (notification_calls == 2) + verify_no_op(changes, initial_values, state); + } +} + +int main(int argc, char** argv) { + std::ios_base::sync_with_stdio(false); + realm::disable_sync_to_disk(); + + Realm::Config config; + config.path = "fuzzer.realm"; + config.cache = false; + config.in_memory = true; + config.automatic_change_notifications = false; + + Schema schema{ + {"object", "", { + {"id", PropertyTypeInt}, + {"value", PropertyTypeInt} + }}, + {"linklist", "", { + {"list", PropertyTypeArray, "object"} + }} + }; + + config.schema = std::make_unique(schema); + unlink(config.path.c_str()); + + auto r = Realm::get_shared_realm(config); + auto r2 = Realm::get_shared_realm(config); + auto& coordinator = *_impl::RealmCoordinator::get_existing_coordinator(config.path); + + r->begin_transaction(); + r->read_group()->get_table("class_linklist")->add_empty_row(); + r->commit_transaction(); + + auto test_on = [&](auto& buffer) { + std::istringstream ss(buffer); + test(config, r, r2, ss); + if (r->is_in_transaction()) + r->cancel_transaction(); + r2->invalidate(); + coordinator.on_change(); + }; + + if (argc > 1) { + std::string buffer; + for (int i = 1; i < argc; ++i) { + int fd = open(argv[i], O_RDONLY); + if (fd < 0) + abort(); + read_all(buffer, fd); + close(fd); + + test_on(buffer); + } + unlink(config.path.c_str()); + return 0; + } + +#ifdef __AFL_HAVE_MANUAL_CONTROL + std::string buffer; + while (__AFL_LOOP(1000)) { + read_all(buffer, 0); + test_on(buffer); + } +#else + std::string buffer; + read_all(buffer, 0); + test_on(buffer); +#endif + + unlink(config.path.c_str()); + return 0; +} diff --git a/tests/notifications-fuzzer/input-lv/0 b/tests/notifications-fuzzer/input-lv/0 new file mode 100644 index 00000000..cb6a3bd9 --- /dev/null +++ b/tests/notifications-fuzzer/input-lv/0 @@ -0,0 +1,38 @@ +3 +100 +200 +400 +1000 +2000 +50 +80 +150 +180 +6000 +5000 +60000 + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 + +a 500 +d 12 +c +m 11 10000 +a 800 +i 5 13 +s 3 8 +o 2 10 +w 1 6 +r 7 +t 11 diff --git a/tests/notifications-fuzzer/input/0 b/tests/notifications-fuzzer/input/0 new file mode 100644 index 00000000..675ab157 --- /dev/null +++ b/tests/notifications-fuzzer/input/0 @@ -0,0 +1,20 @@ +3 +100 +200 +400 +1000 +2000 +50 +80 +150 +180 +6000 +5000 +60000 + + +a 500 +d 12 +c +m 11 10000 +a 800 diff --git a/tests/notifications-fuzzer/input/1 b/tests/notifications-fuzzer/input/1 new file mode 100644 index 00000000..1cbf4855 --- /dev/null +++ b/tests/notifications-fuzzer/input/1 @@ -0,0 +1,34 @@ +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 + + +a 114 +a 115 +a 116 +a 117 +a 118 +a 119 +a 120 +a 121 +a 122 +c +m 4 200 +m 3 201 +m 2 202 +m 1 203 +m 5 203 +m 6 204 +m 7 205 +c +d 11 diff --git a/tests/results.cpp b/tests/results.cpp index 3dda335e..9113d76e 100644 --- a/tests/results.cpp +++ b/tests/results.cpp @@ -1,5 +1,6 @@ #include "catch.hpp" +#include "util/index_helpers.hpp" #include "util/test_file.hpp" #include "impl/realm_coordinator.hpp" @@ -12,6 +13,8 @@ #include #include +#include + using namespace realm; TEST_CASE("Results") { @@ -20,17 +23,17 @@ TEST_CASE("Results") { config.automatic_change_notifications = false; config.schema = std::make_unique(Schema{ {"object", "", { - {"value", PropertyTypeInt}, - {"link", PropertyTypeObject, "linked to object", false, false, true} + {"value", PropertyType::Int}, + {"link", PropertyType::Object, "linked to object", false, false, true} }}, {"other object", "", { - {"value", PropertyTypeInt} + {"value", PropertyType::Int} }}, {"linking object", "", { - {"link", PropertyTypeObject, "object", false, false, true} + {"link", PropertyType::Object, "object", false, false, true} }}, {"linked to object", "", { - {"value", PropertyTypeInt} + {"value", PropertyType::Int} }} }); @@ -41,74 +44,191 @@ TEST_CASE("Results") { r->begin_transaction(); table->add_empty_row(10); for (int i = 0; i < 10; ++i) - table->set_int(0, i, i); + table->set_int(0, i, i * 2); r->commit_transaction(); - Results results(r, *config.schema->find("object"), table->where().greater(0, 0).less(0, 5)); + Results results(r, *config.schema->find("object"), table->where().greater(0, 0).less(0, 10)); - SECTION("notifications") { + SECTION("unsorted notifications") { int notification_calls = 0; - auto token = results.async([&](std::exception_ptr err) { + CollectionChangeSet change; + auto token = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); + change = c; ++notification_calls; }); - coordinator->on_change(); - r->notify(); + advance_and_notify(*r); + + auto write = [&](auto&& f) { + r->begin_transaction(); + f(); + r->commit_transaction(); + advance_and_notify(*r); + }; SECTION("initial results are delivered") { REQUIRE(notification_calls == 1); } - SECTION("modifying the table sends a notification asynchronously") { + SECTION("notifications are sent asynchronously") { r->begin_transaction(); - table->set_int(0, 0, 0); + table->set_int(0, 0, 4); r->commit_transaction(); REQUIRE(notification_calls == 1); - coordinator->on_change(); - r->notify(); + advance_and_notify(*r); REQUIRE(notification_calls == 2); } - SECTION("modifying a linked-to table send a notification") { + SECTION("notifications are not delivered when the token is destroyed before they are calculated") { r->begin_transaction(); - r->read_group()->get_table("class_linked to object")->add_empty_row(); + table->set_int(0, 0, 4); r->commit_transaction(); REQUIRE(notification_calls == 1); - coordinator->on_change(); - r->notify(); - REQUIRE(notification_calls == 2); + token = {}; + advance_and_notify(*r); + REQUIRE(notification_calls == 1); } - SECTION("modifying a a linking table sends a notification") { + SECTION("notifications are not delivered when the token is destroyed before they are delivered") { r->begin_transaction(); - r->read_group()->get_table("class_linking object")->add_empty_row(); + table->set_int(0, 0, 4); r->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); + token = {}; r->notify(); - REQUIRE(notification_calls == 2); + REQUIRE(notification_calls == 1); } - SECTION("modifying a an unrelated table does not send a notification") { - r->begin_transaction(); - r->read_group()->get_table("class_other object")->add_empty_row(); - r->commit_transaction(); + SECTION("notifications are delivered when a new callback is added from within a callback") { + NotificationToken token2, token3; + bool called = false; + token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { + token3 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { + called = true; + }); + }); + advance_and_notify(*r); + REQUIRE(called); + } + + SECTION("notifications are not delivered when a callback is removed from within a callback") { + NotificationToken token2, token3; + token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { + token3 = {}; + }); + token3 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { + REQUIRE(false); + }); + + advance_and_notify(*r); + } + + SECTION("removing the current callback does not stop later ones from being called") { + NotificationToken token2, token3; + bool called = false; + token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { + token2 = {}; + }); + token3 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { + called = true; + }); + + advance_and_notify(*r); + + REQUIRE(called); + } + + SECTION("modifications to unrelated tables do not send notifications") { + write([&] { + r->read_group()->get_table("class_other object")->add_empty_row(); + }); REQUIRE(notification_calls == 1); - coordinator->on_change(); - r->notify(); + } + + SECTION("irrelevant modifications to linked tables do not send notifications") { + write([&] { + r->read_group()->get_table("class_linked to object")->add_empty_row(); + }); REQUIRE(notification_calls == 1); } + SECTION("irrelevant modifications to linking tables do not send notifications") { + write([&] { + r->read_group()->get_table("class_linking object")->add_empty_row(); + }); + REQUIRE(notification_calls == 1); + } + + SECTION("modifications that leave a non-matching row non-matching do not send notifications") { + write([&] { + table->set_int(0, 6, 13); + }); + REQUIRE(notification_calls == 1); + } + + SECTION("deleting non-matching rows does not send a notification") { + write([&] { + table->move_last_over(0); + table->move_last_over(6); + }); + REQUIRE(notification_calls == 1); + } + + SECTION("modifying a matching row and leaving it matching marks that row as modified") { + write([&] { + table->set_int(0, 1, 3); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.modifications, 0); + } + + SECTION("modifying a matching row to no longer match marks that row as deleted") { + write([&] { + table->set_int(0, 2, 0); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 1); + } + + SECTION("modifying a non-matching row to match marks that row as inserted, but not modified") { + write([&] { + table->set_int(0, 7, 3); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.insertions, 4); + REQUIRE(change.modifications.empty()); + } + + SECTION("deleting a matching row marks that row as deleted") { + write([&] { + table->move_last_over(3); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 2); + } + + SECTION("moving a matching row via deletion marks that row as moved") { + write([&] { + table->where().greater_equal(0, 10).find_all().clear(RemoveMode::unordered); + table->move_last_over(0); + }); + REQUIRE(notification_calls == 2); + REQUIRE_MOVES(change, {3, 0}); + } + SECTION("modifications from multiple transactions are collapsed") { r->begin_transaction(); - table->set_int(0, 0, 0); + table->set_int(0, 0, 6); r->commit_transaction(); + coordinator->on_change(); + r->begin_transaction(); table->set_int(0, 1, 0); r->commit_transaction(); @@ -119,28 +239,297 @@ TEST_CASE("Results") { REQUIRE(notification_calls == 2); } - SECTION("notifications are not delivered when the token is destroyed before they are calculated") { + SECTION("inserting a row then modifying it in a second transaction does not report it as modified") { r->begin_transaction(); - table->set_int(0, 0, 0); + size_t ndx = table->add_empty_row(); + table->set_int(0, ndx, 6); + r->commit_transaction(); + + coordinator->on_change(); + + r->begin_transaction(); + table->set_int(0, ndx, 7); + r->commit_transaction(); + + advance_and_notify(*r); + + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.insertions, 4); + REQUIRE(change.modifications.empty()); + } + + SECTION("modification indices are pre-insert/delete") { + r->begin_transaction(); + table->set_int(0, 2, 0); + table->set_int(0, 3, 6); + r->commit_transaction(); + advance_and_notify(*r); + + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 1); + REQUIRE_INDICES(change.modifications, 2); + } + + SECTION("notifications are not delivered when collapsing transactions results in no net change") { + r->begin_transaction(); + size_t ndx = table->add_empty_row(); + table->set_int(0, ndx, 5); + r->commit_transaction(); + + coordinator->on_change(); + + r->begin_transaction(); + table->move_last_over(ndx); r->commit_transaction(); REQUIRE(notification_calls == 1); - token = {}; coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 1); } - SECTION("notifications are not delivered when the token is destroyed before they are delivered") { + SECTION("the first call of a notification can include changes if it previously ran for a different callback") { + auto token2 = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) { + REQUIRE(!c.empty()); + }); + + write([&] { + table->set_int(0, table->add_empty_row(), 5); + }); + } + } + + // Sort in descending order + results = results.sort({{0}, {false}}); + + SECTION("sorted notifications") { + int notification_calls = 0; + CollectionChangeSet change; + auto token = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { + REQUIRE_FALSE(err); + change = c; + ++notification_calls; + }); + + advance_and_notify(*r); + + auto write = [&](auto&& f) { r->begin_transaction(); - table->set_int(0, 0, 0); + f(); + r->commit_transaction(); + advance_and_notify(*r); + }; + + SECTION("modifications that leave a non-matching row non-matching do not send notifications") { + write([&] { + table->set_int(0, 6, 13); + }); + REQUIRE(notification_calls == 1); + } + + SECTION("deleting non-matching rows does not send a notification") { + write([&] { + table->move_last_over(0); + table->move_last_over(6); + }); + REQUIRE(notification_calls == 1); + } + + SECTION("modifying a matching row and leaving it matching marks that row as modified") { + write([&] { + table->set_int(0, 1, 3); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.modifications, 3); + } + + SECTION("modifying a matching row to no longer match marks that row as deleted") { + write([&] { + table->set_int(0, 2, 0); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 2); + } + + SECTION("modifying a non-matching row to match marks that row as inserted") { + write([&] { + table->set_int(0, 7, 3); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.insertions, 3); + } + + SECTION("deleting a matching row marks that row as deleted") { + write([&] { + table->move_last_over(3); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 1); + } + + SECTION("moving a matching row via deletion does not send a notification") { + write([&] { + table->where().greater_equal(0, 10).find_all().clear(RemoveMode::unordered); + table->move_last_over(0); + }); + REQUIRE(notification_calls == 1); + } + + SECTION("modifying a matching row to change its position sends insert+delete") { + write([&] { + table->set_int(0, 2, 9); + }); + REQUIRE(notification_calls == 2); + REQUIRE_INDICES(change.deletions, 2); + REQUIRE_INDICES(change.insertions, 0); + } + + SECTION("modifications from multiple transactions are collapsed") { + r->begin_transaction(); + table->set_int(0, 0, 5); + r->commit_transaction(); + + r->begin_transaction(); + table->set_int(0, 1, 0); r->commit_transaction(); REQUIRE(notification_calls == 1); - coordinator->on_change(); - token = {}; - r->notify(); - REQUIRE(notification_calls == 1); + advance_and_notify(*r); + REQUIRE(notification_calls == 2); + } + + SECTION("moving a matching row by deleting all other rows") { + r->begin_transaction(); + table->clear(); + table->add_empty_row(2); + table->set_int(0, 0, 15); + table->set_int(0, 1, 5); + r->commit_transaction(); + advance_and_notify(*r); + + write([&] { + table->move_last_over(0); + table->add_empty_row(); + table->set_int(0, 1, 3); + }); + + REQUIRE(notification_calls == 3); + REQUIRE(change.deletions.empty()); + REQUIRE_INDICES(change.insertions, 1); + } + } +} + +TEST_CASE("Async Results error handling") { + InMemoryTestFile config; + config.cache = false; + config.automatic_change_notifications = false; + config.schema = std::make_unique(Schema{ + {"object", "", { + {"value", PropertyType::Int}, + }}, + }); + + auto r = Realm::get_shared_realm(config); + auto coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path); + Results results(r, *config.schema->find("object"), *r->read_group()->get_table("class_object")); + + class OpenFileLimiter { + public: + OpenFileLimiter() + { + // Set the max open files to zero so that opening new files will fail + getrlimit(RLIMIT_NOFILE, &m_old); + rlimit rl = m_old; + rl.rlim_cur = 0; + setrlimit(RLIMIT_NOFILE, &rl); + } + + ~OpenFileLimiter() + { + setrlimit(RLIMIT_NOFILE, &m_old); + } + + private: + rlimit m_old; + }; + + SECTION("error when opening the advancer SG") { + OpenFileLimiter limiter; + + SECTION("error is delivered asynchronously") { + bool called = false; + auto token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { + REQUIRE(err); + called = true; + }); + + REQUIRE(!called); + coordinator->on_change(); + REQUIRE(!called); + r->notify(); + REQUIRE(called); + } + + SECTION("adding another callback does not send the error again") { + bool called = false; + auto token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { + REQUIRE(err); + REQUIRE_FALSE(called); + called = true; + }); + + advance_and_notify(*r); + + bool called2 = false; + auto token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { + REQUIRE(err); + REQUIRE_FALSE(called2); + called2 = true; + }); + + advance_and_notify(*r); + REQUIRE(called2); + } + } + + SECTION("error when opening the executor SG") { + SECTION("error is delivered asynchronously") { + bool called = false; + auto token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { + REQUIRE(err); + called = true; + }); + OpenFileLimiter limiter; + + REQUIRE(!called); + coordinator->on_change(); + REQUIRE(!called); + r->notify(); + REQUIRE(called); + } + + SECTION("adding another callback does not send the error again") { + bool called = false; + auto token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { + REQUIRE(err); + REQUIRE_FALSE(called); + called = true; + }); + OpenFileLimiter limiter; + + advance_and_notify(*r); + + bool called2 = false; + auto token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { + REQUIRE(err); + REQUIRE_FALSE(called2); + called2 = true; + }); + + advance_and_notify(*r); + + REQUIRE(called2); } } } diff --git a/tests/transaction_log_parsing.cpp b/tests/transaction_log_parsing.cpp new file mode 100644 index 00000000..fd35503b --- /dev/null +++ b/tests/transaction_log_parsing.cpp @@ -0,0 +1,991 @@ +#include "catch.hpp" + +#include "util/index_helpers.hpp" +#include "util/test_file.hpp" + +#include "impl/collection_notifier.hpp" +#include "impl/transact_log_handler.hpp" +#include "property.hpp" +#include "object_schema.hpp" +#include "schema.hpp" + +#include +#include +#include + +using namespace realm; + +class CaptureHelper { +public: + CaptureHelper(std::string const& path, SharedRealm const& r, LinkViewRef lv) + : m_history(make_client_history(path)) + , m_sg(*m_history, SharedGroup::durability_MemOnly) + , m_realm(r) + , m_group(m_sg.begin_read()) + , m_linkview(lv) + { + m_realm->begin_transaction(); + + m_initial.reserve(lv->size()); + for (size_t i = 0; i < lv->size(); ++i) + m_initial.push_back(lv->get(i).get_int(0)); + } + + CollectionChangeSet finish(size_t ndx) { + m_realm->commit_transaction(); + + _impl::CollectionChangeBuilder c; + _impl::TransactionChangeInfo info; + info.lists.push_back({ndx, 0, 0, &c}); + info.table_modifications_needed.resize(m_group.size(), true); + info.table_moves_needed.resize(m_group.size(), true); + _impl::transaction::advance(m_sg, info); + + if (info.lists.empty()) { + REQUIRE(!m_linkview->is_attached()); + return {}; + } + + validate(c); + return c; + } + + explicit operator bool() const { return m_realm->is_in_transaction(); } + +private: + std::unique_ptr m_history; + SharedGroup m_sg; + SharedRealm m_realm; + Group const& m_group; + + LinkViewRef m_linkview; + std::vector m_initial; + + void validate(CollectionChangeSet const& info) + { + info.insertions.verify(); + info.deletions.verify(); + info.modifications.verify(); + + std::vector move_sources; + for (auto const& move : info.moves) + move_sources.push_back(m_initial[move.from]); + + // Apply the changes from the transaction log to our copy of the + // initial, using UITableView's batching rules (i.e. delete, then + // insert, then update) + auto it = util::make_reverse_iterator(info.deletions.end()); + auto end = util::make_reverse_iterator(info.deletions.begin()); + for (; it != end; ++it) { + m_initial.erase(m_initial.begin() + it->first, m_initial.begin() + it->second); + } + + for (auto const& range : info.insertions) { + for (auto i = range.first; i < range.second; ++i) + m_initial.insert(m_initial.begin() + i, m_linkview->get(i).get_int(0)); + } + + for (auto const& range : info.modifications) { + for (auto i = range.first; i < range.second; ++i) + m_initial[i] = m_linkview->get(i).get_int(0); + } + + REQUIRE(m_linkview->is_attached()); + + // and make sure we end up with the same end result + REQUIRE(m_initial.size() == m_linkview->size()); + for (size_t i = 0; i < m_initial.size(); ++i) + CHECK(m_initial[i] == m_linkview->get(i).get_int(0)); + + // Verify that everything marked as a move actually is one + for (size_t i = 0; i < move_sources.size(); ++i) { + if (!info.modifications.contains(info.moves[i].to)) { + CHECK(m_linkview->get(info.moves[i].to).get_int(0) == move_sources[i]); + } + } + } +}; + +TEST_CASE("Transaction log parsing") { + InMemoryTestFile config; + config.automatic_change_notifications = false; + + SECTION("schema change validation") { + config.schema = std::make_unique(Schema{ + {"table", "", { + {"unindexed", PropertyType::Int}, + {"indexed", PropertyType::Int, "", false, true} + }}, + }); + auto r = Realm::get_shared_realm(config); + r->read_group(); + + auto history = make_client_history(config.path); + SharedGroup sg(*history, SharedGroup::durability_MemOnly); + + SECTION("adding a table is allowed") { + WriteTransaction wt(sg); + TableRef table = wt.add_table("new table"); + table->add_column(type_String, "new col"); + wt.commit(); + + REQUIRE_NOTHROW(r->refresh()); + } + + SECTION("adding an index to an existing column is allowed") { + WriteTransaction wt(sg); + TableRef table = wt.get_table("class_table"); + table->add_search_index(0); + wt.commit(); + + REQUIRE_NOTHROW(r->refresh()); + } + + SECTION("removing an index from an existing column is allowed") { + WriteTransaction wt(sg); + TableRef table = wt.get_table("class_table"); + table->remove_search_index(1); + wt.commit(); + + REQUIRE_NOTHROW(r->refresh()); + } + + SECTION("adding a column to an existing table is not allowed (but eventually should be)") { + WriteTransaction wt(sg); + TableRef table = wt.get_table("class_table"); + table->add_column(type_String, "new col"); + wt.commit(); + + REQUIRE_THROWS(r->refresh()); + } + + SECTION("removing a column is not allowed") { + WriteTransaction wt(sg); + TableRef table = wt.get_table("class_table"); + table->remove_column(1); + wt.commit(); + + REQUIRE_THROWS(r->refresh()); + } + + SECTION("removing a table is not allowed") { + WriteTransaction wt(sg); + wt.get_group().remove_table("class_table"); + wt.commit(); + + REQUIRE_THROWS(r->refresh()); + } + } + + SECTION("table change information") { + config.schema = std::make_unique(Schema{ + {"table", "", { + {"value", PropertyType::Int} + }}, + }); + + auto r = Realm::get_shared_realm(config); + auto& table = *r->read_group()->get_table("class_table"); + + r->begin_transaction(); + table.add_empty_row(10); + for (int i = 0; i < 10; ++i) + table.set_int(0, i, i); + r->commit_transaction(); + + auto track_changes = [&](std::vector tables_needed, auto&& f) { + auto history = make_client_history(config.path); + SharedGroup sg(*history, SharedGroup::durability_MemOnly); + sg.begin_read(); + + r->begin_transaction(); + f(); + r->commit_transaction(); + + _impl::TransactionChangeInfo info; + info.table_modifications_needed = tables_needed; + info.table_moves_needed = tables_needed; + _impl::transaction::advance(sg, info); + return info; + }; + + SECTION("modifying a row marks it as modified") { + auto info = track_changes({false, false, true}, [&] { + table.set_int(0, 1, 2); + }); + REQUIRE(info.tables.size() == 3); + REQUIRE_INDICES(info.tables[2].modifications, 1); + } + + SECTION("modifications to untracked tables are ignored") { + auto info = track_changes({false, false, false}, [&] { + table.set_int(0, 1, 2); + }); + REQUIRE(info.tables.empty()); + } + + SECTION("new row additions are reported") { + auto info = track_changes({false, false, true}, [&] { + table.add_empty_row(); + table.add_empty_row(); + }); + REQUIRE(info.tables.size() == 3); + REQUIRE_INDICES(info.tables[2].insertions, 10, 11); + } + + SECTION("deleting newly added rows makes them not be reported") { + auto info = track_changes({false, false, true}, [&] { + table.add_empty_row(); + table.add_empty_row(); + table.move_last_over(11); + }); + REQUIRE(info.tables.size() == 3); + REQUIRE_INDICES(info.tables[2].insertions, 10); + REQUIRE(info.tables[2].deletions.empty()); + } + + SECTION("modifying newly added rows is reported as a modification") { + auto info = track_changes({false, false, true}, [&] { + table.add_empty_row(); + table.set_int(0, 10, 10); + }); + REQUIRE(info.tables.size() == 3); + REQUIRE_INDICES(info.tables[2].insertions, 10); + REQUIRE_INDICES(info.tables[2].modifications, 10); + } + + SECTION("move_last_over() does not shift rows other than the last one") { + auto info = track_changes({false, false, true}, [&] { + table.move_last_over(2); + table.move_last_over(3); + }); + REQUIRE(info.tables.size() == 3); + REQUIRE_INDICES(info.tables[2].deletions, 2, 3, 8, 9); + REQUIRE_INDICES(info.tables[2].insertions, 2, 3); + REQUIRE_MOVES(info.tables[2], {8, 3}, {9, 2}); + } + } + + SECTION("LinkView change information") { + config.schema = std::make_unique(Schema{ + {"origin", "", { + {"array", PropertyType::Array, "target"} + }}, + {"target", "", { + {"value", PropertyType::Int} + }}, + }); + + auto r = Realm::get_shared_realm(config); + + auto origin = r->read_group()->get_table("class_origin"); + auto target = r->read_group()->get_table("class_target"); + + r->begin_transaction(); + + target->add_empty_row(10); + for (int i = 0; i < 10; ++i) + target->set_int(0, i, i); + + origin->add_empty_row(); + LinkViewRef lv = origin->get_linklist(0, 0); + for (int i = 0; i < 10; ++i) + lv->add(i); + + r->commit_transaction(); + +#define VALIDATE_CHANGES(out) \ + for (CaptureHelper helper(config.path, r, lv); helper; out = helper.finish(origin->get_index_in_group())) + + CollectionChangeSet changes; + SECTION("single change type") { + SECTION("add single") { + VALIDATE_CHANGES(changes) { + lv->add(0); + } + REQUIRE_INDICES(changes.insertions, 10); + } + SECTION("add multiple") { + VALIDATE_CHANGES(changes) { + lv->add(0); + lv->add(0); + } + REQUIRE_INDICES(changes.insertions, 10, 11); + } + + SECTION("erase single") { + VALIDATE_CHANGES(changes) { + lv->remove(5); + } + REQUIRE_INDICES(changes.deletions, 5); + } + SECTION("erase contiguous forward") { + VALIDATE_CHANGES(changes) { + lv->remove(5); + lv->remove(5); + lv->remove(5); + } + REQUIRE_INDICES(changes.deletions, 5, 6, 7); + } + SECTION("erase contiguous reverse") { + VALIDATE_CHANGES(changes) { + lv->remove(7); + lv->remove(6); + lv->remove(5); + } + REQUIRE_INDICES(changes.deletions, 5, 6, 7); + } + SECTION("erase contiguous mixed") { + VALIDATE_CHANGES(changes) { + lv->remove(5); + lv->remove(6); + lv->remove(5); + } + REQUIRE_INDICES(changes.deletions, 5, 6, 7); + } + SECTION("erase scattered forward") { + VALIDATE_CHANGES(changes) { + lv->remove(3); + lv->remove(4); + lv->remove(5); + } + REQUIRE_INDICES(changes.deletions, 3, 5, 7); + } + SECTION("erase scattered backwards") { + VALIDATE_CHANGES(changes) { + lv->remove(7); + lv->remove(5); + lv->remove(3); + } + REQUIRE_INDICES(changes.deletions, 3, 5, 7); + } + SECTION("erase scattered mixed") { + VALIDATE_CHANGES(changes) { + lv->remove(3); + lv->remove(6); + lv->remove(4); + } + REQUIRE_INDICES(changes.deletions, 3, 5, 7); + } + + SECTION("set single") { + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + } + REQUIRE_INDICES(changes.modifications, 5); + } + SECTION("set contiguous") { + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + lv->set(6, 0); + lv->set(7, 0); + } + REQUIRE_INDICES(changes.modifications, 5, 6, 7); + } + SECTION("set scattered") { + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + lv->set(7, 0); + lv->set(9, 0); + } + REQUIRE_INDICES(changes.modifications, 5, 7, 9); + } + SECTION("set redundant") { + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + lv->set(5, 0); + lv->set(5, 0); + } + REQUIRE_INDICES(changes.modifications, 5); + } + + SECTION("clear") { + VALIDATE_CHANGES(changes) { + lv->clear(); + } + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + SECTION("move backward") { + VALIDATE_CHANGES(changes) { + lv->move(5, 3); + } + REQUIRE_MOVES(changes, {5, 3}); + } + + SECTION("move forward") { + VALIDATE_CHANGES(changes) { + lv->move(1, 3); + } + REQUIRE_MOVES(changes, {1, 3}); + } + + SECTION("chained moves") { + VALIDATE_CHANGES(changes) { + lv->move(1, 3); + lv->move(3, 5); + } + REQUIRE_MOVES(changes, {1, 5}); + } + + SECTION("backwards chained moves") { + VALIDATE_CHANGES(changes) { + lv->move(5, 3); + lv->move(3, 1); + } + REQUIRE_MOVES(changes, {5, 1}); + } + + SECTION("moves shifting other moves") { + VALIDATE_CHANGES(changes) { + lv->move(1, 5); + lv->move(2, 7); + } + REQUIRE_MOVES(changes, {1, 4}, {3, 7}); + + VALIDATE_CHANGES(changes) { + lv->move(1, 5); + lv->move(7, 0); + } + REQUIRE_MOVES(changes, {1, 6}, {7, 0}); + } + + SECTION("move to current location is a no-op") { + VALIDATE_CHANGES(changes) { + lv->move(5, 5); + } + REQUIRE(changes.insertions.empty()); + REQUIRE(changes.deletions.empty()); + REQUIRE(changes.moves.empty()); + } + + SECTION("delete a target row") { + VALIDATE_CHANGES(changes) { + target->move_last_over(5); + } + REQUIRE_INDICES(changes.deletions, 5); + } + + SECTION("delete all target rows") { + VALIDATE_CHANGES(changes) { + lv->remove_all_target_rows(); + } + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + SECTION("clear target table") { + VALIDATE_CHANGES(changes) { + target->clear(); + } + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + SECTION("swap()") { + VALIDATE_CHANGES(changes) { + lv->swap(3, 5); + } + REQUIRE_INDICES(changes.modifications, 3, 5); + } + } + + SECTION("mixed change types") { + SECTION("set -> insert") { + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + lv->insert(5, 0); + } + REQUIRE_INDICES(changes.insertions, 5); + REQUIRE_INDICES(changes.modifications, 6); + + VALIDATE_CHANGES(changes) { + lv->set(4, 0); + lv->insert(5, 0); + } + REQUIRE_INDICES(changes.insertions, 5); + REQUIRE_INDICES(changes.modifications, 4); + } + SECTION("insert -> set") { + VALIDATE_CHANGES(changes) { + lv->insert(5, 0); + lv->set(5, 1); + } + REQUIRE_INDICES(changes.insertions, 5); + REQUIRE_INDICES(changes.modifications, 5); + + VALIDATE_CHANGES(changes) { + lv->insert(5, 0); + lv->set(6, 1); + } + REQUIRE_INDICES(changes.insertions, 5); + REQUIRE_INDICES(changes.modifications, 6); + + VALIDATE_CHANGES(changes) { + lv->insert(6, 0); + lv->set(5, 1); + } + REQUIRE_INDICES(changes.insertions, 6); + REQUIRE_INDICES(changes.modifications, 5); + } + + SECTION("set -> erase") { + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + lv->remove(5); + } + REQUIRE_INDICES(changes.deletions, 5); + REQUIRE(changes.modifications.empty()); + + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + lv->remove(4); + } + REQUIRE_INDICES(changes.deletions, 4); + REQUIRE_INDICES(changes.modifications, 4); + + VALIDATE_CHANGES(changes) { + lv->set(5, 0); + lv->remove(4); + lv->remove(4); + } + REQUIRE_INDICES(changes.deletions, 4, 5); + REQUIRE(changes.modifications.empty()); + } + + SECTION("erase -> set") { + VALIDATE_CHANGES(changes) { + lv->remove(5); + lv->set(5, 0); + } + REQUIRE_INDICES(changes.deletions, 5); + REQUIRE_INDICES(changes.modifications, 5); + } + + SECTION("insert -> clear") { + VALIDATE_CHANGES(changes) { + lv->add(0); + lv->clear(); + } + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + REQUIRE(changes.insertions.empty()); + } + + SECTION("set -> clear") { + VALIDATE_CHANGES(changes) { + lv->set(0, 5); + lv->clear(); + } + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + REQUIRE(changes.modifications.empty()); + } + + SECTION("clear -> insert") { + VALIDATE_CHANGES(changes) { + lv->clear(); + lv->add(0); + } + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + REQUIRE_INDICES(changes.insertions, 0); + } + + SECTION("insert -> delete") { + VALIDATE_CHANGES(changes) { + lv->add(0); + lv->remove(10); + } + REQUIRE(changes.insertions.empty()); + REQUIRE(changes.deletions.empty()); + + VALIDATE_CHANGES(changes) { + lv->add(0); + lv->remove(9); + } + REQUIRE_INDICES(changes.deletions, 9); + REQUIRE_INDICES(changes.insertions, 9); + + VALIDATE_CHANGES(changes) { + lv->insert(1, 1); + lv->insert(3, 3); + lv->insert(5, 5); + lv->remove(6); + lv->remove(4); + lv->remove(2); + } + REQUIRE_INDICES(changes.deletions, 1, 2, 3); + REQUIRE_INDICES(changes.insertions, 1, 2, 3); + + VALIDATE_CHANGES(changes) { + lv->insert(1, 1); + lv->insert(3, 3); + lv->insert(5, 5); + lv->remove(2); + lv->remove(3); + lv->remove(4); + } + REQUIRE_INDICES(changes.deletions, 1, 2, 3); + REQUIRE_INDICES(changes.insertions, 1, 2, 3); + } + + SECTION("delete -> insert") { + VALIDATE_CHANGES(changes) { + lv->remove(9); + lv->add(0); + } + REQUIRE_INDICES(changes.deletions, 9); + REQUIRE_INDICES(changes.insertions, 9); + } + + SECTION("interleaved delete and insert") { + VALIDATE_CHANGES(changes) { + lv->remove(9); + lv->remove(7); + lv->remove(5); + lv->remove(3); + lv->remove(1); + + lv->insert(4, 9); + lv->insert(3, 7); + lv->insert(2, 5); + lv->insert(1, 3); + lv->insert(0, 1); + + lv->remove(9); + lv->remove(7); + lv->remove(5); + lv->remove(3); + lv->remove(1); + } + + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + REQUIRE_INDICES(changes.insertions, 0, 1, 2, 3, 4); + } + + SECTION("move after set is just insert+delete") { + VALIDATE_CHANGES(changes) { + lv->set(5, 6); + lv->move(5, 0); + } + + REQUIRE_INDICES(changes.deletions, 5); + REQUIRE_INDICES(changes.insertions, 0); + REQUIRE_MOVES(changes, {5, 0}); + } + + SECTION("set after move is just insert+delete") { + VALIDATE_CHANGES(changes) { + lv->move(5, 0); + lv->set(0, 6); + } + + REQUIRE_INDICES(changes.deletions, 5); + REQUIRE_INDICES(changes.insertions, 0); + REQUIRE_MOVES(changes, {5, 0}); + } + + SECTION("delete after move removes original row") { + VALIDATE_CHANGES(changes) { + lv->move(5, 0); + lv->remove(0); + } + + REQUIRE_INDICES(changes.deletions, 5); + REQUIRE(changes.moves.empty()); + } + + SECTION("moving newly inserted row just changes reported index of insert") { + VALIDATE_CHANGES(changes) { + lv->move(5, 0); + lv->remove(0); + } + + REQUIRE_INDICES(changes.deletions, 5); + REQUIRE(changes.moves.empty()); + } + + SECTION("moves shift insertions/changes like any other insertion") { + VALIDATE_CHANGES(changes) { + lv->insert(5, 5); + lv->set(6, 6); + lv->move(7, 4); + } + REQUIRE_INDICES(changes.deletions, 6); + REQUIRE_INDICES(changes.insertions, 4, 6); + REQUIRE_INDICES(changes.modifications, 7); + REQUIRE_MOVES(changes, {6, 4}); + } + + SECTION("clear after delete") { + VALIDATE_CHANGES(changes) { + lv->remove(5); + lv->clear(); + } + REQUIRE_INDICES(changes.deletions, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + SECTION("erase before previous move target") { + VALIDATE_CHANGES(changes) { + lv->move(2, 8); + lv->remove(5); + } + REQUIRE_INDICES(changes.insertions, 7); + REQUIRE_INDICES(changes.deletions, 2, 6); + REQUIRE_MOVES(changes, {2, 7}); + } + + SECTION("insert after move updates move destination") { + VALIDATE_CHANGES(changes) { + lv->move(2, 8); + lv->insert(5, 5); + } + REQUIRE_MOVES(changes, {2, 9}); + } + } + + SECTION("deleting the linkview") { + SECTION("directly") { + VALIDATE_CHANGES(changes) { + origin->move_last_over(0); + } + REQUIRE(!lv->is_attached()); + REQUIRE(changes.insertions.empty()); + REQUIRE(changes.deletions.empty()); + REQUIRE(changes.modifications.empty()); + } + + SECTION("table clear") { + VALIDATE_CHANGES(changes) { + origin->clear(); + } + REQUIRE(!lv->is_attached()); + REQUIRE(changes.insertions.empty()); + REQUIRE(changes.deletions.empty()); + REQUIRE(changes.modifications.empty()); + } + + SECTION("delete a different lv") { + r->begin_transaction(); + origin->add_empty_row(); + r->commit_transaction(); + + VALIDATE_CHANGES(changes) { + origin->move_last_over(1); + } + REQUIRE(changes.insertions.empty()); + REQUIRE(changes.deletions.empty()); + REQUIRE(changes.modifications.empty()); + } + } + + SECTION("modifying a different linkview should not produce notifications") { + r->begin_transaction(); + origin->add_empty_row(); + LinkViewRef lv2 = origin->get_linklist(0, 1); + lv2->add(5); + r->commit_transaction(); + + VALIDATE_CHANGES(changes) { + lv2->add(1); + lv2->add(2); + lv2->remove(0); + lv2->set(0, 6); + lv2->move(1, 0); + lv2->swap(0, 1); + lv2->clear(); + lv2->add(1); + } + + REQUIRE(changes.insertions.empty()); + REQUIRE(changes.deletions.empty()); + REQUIRE(changes.modifications.empty()); + } + } +} + +TEST_CASE("DeepChangeChecker") { + InMemoryTestFile config; + config.automatic_change_notifications = false; + + config.schema = std::make_unique(Schema{ + {"table", "", { + {"int", PropertyType::Int}, + {"link", PropertyType::Object, "table", false, false, true}, + {"array", PropertyType::Array, "table"} + }}, + }); + + auto r = Realm::get_shared_realm(config); + auto table = r->read_group()->get_table("class_table"); + + r->begin_transaction(); + table->add_empty_row(10); + for (int i = 0; i < 10; ++i) + table->set_int(0, i, i); + r->commit_transaction(); + + auto track_changes = [&](auto&& f) { + auto history = make_client_history(config.path); + SharedGroup sg(*history, SharedGroup::durability_MemOnly); + Group const& g = sg.begin_read(); + + r->begin_transaction(); + f(); + r->commit_transaction(); + + _impl::TransactionChangeInfo info; + info.table_modifications_needed.resize(g.size(), true); + info.table_moves_needed.resize(g.size(), true); + _impl::transaction::advance(sg, info); + return info; + }; + + std::vector<_impl::DeepChangeChecker::RelatedTable> tables; + _impl::DeepChangeChecker::find_related_tables(tables, *table); + + SECTION("direct changes are tracked") { + auto info = track_changes([&] { + table->set_int(0, 9, 10); + }); + + _impl::DeepChangeChecker checker(info, *table, tables); + REQUIRE_FALSE(checker(8)); + REQUIRE(checker(9)); + } + + SECTION("changes over links are tracked") { + r->begin_transaction(); + for (int i = 0; i < 9; ++i) + table->set_link(1, i, i + 1); + r->commit_transaction(); + + auto info = track_changes([&] { + table->set_int(0, 9, 10); + }); + + REQUIRE(_impl::DeepChangeChecker(info, *table, tables)(0)); + } + + SECTION("changes over linklists are tracked") { + r->begin_transaction(); + for (int i = 0; i < 9; ++i) + table->get_linklist(2, i)->add(i + 1); + r->commit_transaction(); + + auto info = track_changes([&] { + table->set_int(0, 9, 10); + }); + + REQUIRE(_impl::DeepChangeChecker(info, *table, tables)(0)); + } + + SECTION("cycles over links do not loop forever") { + r->begin_transaction(); + table->set_link(1, 0, 0); + r->commit_transaction(); + + auto info = track_changes([&] { + table->set_int(0, 9, 10); + }); + REQUIRE_FALSE(_impl::DeepChangeChecker(info, *table, tables)(0)); + } + + SECTION("cycles over linklists do not loop forever") { + r->begin_transaction(); + table->get_linklist(2, 0)->add(0); + r->commit_transaction(); + + auto info = track_changes([&] { + table->set_int(0, 9, 10); + }); + REQUIRE_FALSE(_impl::DeepChangeChecker(info, *table, tables)(0)); + } + + SECTION("link chains are tracked up to 16 levels deep") { + r->begin_transaction(); + table->add_empty_row(10); + for (int i = 0; i < 19; ++i) + table->set_link(1, i, i + 1); + r->commit_transaction(); + + auto info = track_changes([&] { + table->set_int(0, 19, -1); + }); + + _impl::DeepChangeChecker checker(info, *table, tables); + CHECK(checker(19)); + CHECK(checker(18)); + CHECK(checker(4)); + CHECK_FALSE(checker(3)); + CHECK_FALSE(checker(2)); + + // Check in other orders to make sure that the caching doesn't effect + // the results + _impl::DeepChangeChecker checker2(info, *table, tables); + CHECK_FALSE(checker2(2)); + CHECK_FALSE(checker2(3)); + CHECK(checker2(4)); + CHECK(checker2(18)); + CHECK(checker2(19)); + + _impl::DeepChangeChecker checker3(info, *table, tables); + CHECK(checker2(4)); + CHECK_FALSE(checker2(3)); + CHECK_FALSE(checker2(2)); + CHECK(checker2(18)); + CHECK(checker2(19)); + } + + SECTION("targets moving is not a change") { + r->begin_transaction(); + table->set_link(1, 0, 9); + table->get_linklist(2, 0)->add(9); + r->commit_transaction(); + + auto info = track_changes([&] { + table->move_last_over(5); + }); + REQUIRE_FALSE(_impl::DeepChangeChecker(info, *table, tables)(0)); + } + + SECTION("changes made before a row is moved are reported") { + r->begin_transaction(); + table->set_link(1, 0, 9); + r->commit_transaction(); + + auto info = track_changes([&] { + table->set_int(0, 9, 5); + table->move_last_over(5); + }); + REQUIRE(_impl::DeepChangeChecker(info, *table, tables)(0)); + + r->begin_transaction(); + table->get_linklist(2, 0)->add(8); + r->commit_transaction(); + + info = track_changes([&] { + table->set_int(0, 8, 5); + table->move_last_over(5); + }); + REQUIRE(_impl::DeepChangeChecker(info, *table, tables)(0)); + } + + SECTION("changes made after a row is moved are reported") { + r->begin_transaction(); + table->set_link(1, 0, 9); + r->commit_transaction(); + + auto info = track_changes([&] { + table->move_last_over(5); + table->set_int(0, 5, 5); + }); + REQUIRE(_impl::DeepChangeChecker(info, *table, tables)(0)); + + r->begin_transaction(); + table->get_linklist(2, 0)->add(8); + r->commit_transaction(); + + info = track_changes([&] { + table->move_last_over(5); + table->set_int(0, 5, 5); + }); + REQUIRE(_impl::DeepChangeChecker(info, *table, tables)(0)); + } +} diff --git a/tests/util/index_helpers.hpp b/tests/util/index_helpers.hpp new file mode 100644 index 00000000..bb5145b5 --- /dev/null +++ b/tests/util/index_helpers.hpp @@ -0,0 +1,23 @@ +#define REQUIRE_INDICES(index_set, ...) do { \ + index_set.verify(); \ + std::initializer_list expected = {__VA_ARGS__}; \ + auto actual = index_set.as_indexes(); \ + INFO("Checking " #index_set); \ + REQUIRE(expected.size() == std::distance(actual.begin(), actual.end())); \ + auto begin = actual.begin(); \ + for (auto index : expected) { \ + REQUIRE(*begin++ == index); \ + } \ +} while (0) + +#define REQUIRE_MOVES(c, ...) do { \ + auto actual = (c); \ + std::initializer_list expected = {__VA_ARGS__}; \ + REQUIRE(expected.size() == actual.moves.size()); \ + auto begin = actual.moves.begin(); \ + for (auto move : expected) { \ + CHECK(begin->from == move.from); \ + CHECK(begin->to == move.to); \ + ++begin; \ + } \ +} while (0) diff --git a/tests/util/test_file.cpp b/tests/util/test_file.cpp index 932218c5..708c0381 100644 --- a/tests/util/test_file.cpp +++ b/tests/util/test_file.cpp @@ -1,10 +1,18 @@ #include "util/test_file.hpp" +#include "impl/realm_coordinator.hpp" + #include #include #include +#if defined(__has_feature) && __has_feature(thread_sanitizer) +#include +#include +#include +#endif + TestFile::TestFile() { static std::string tmpdir = [] { @@ -29,3 +37,62 @@ InMemoryTestFile::InMemoryTestFile() { in_memory = true; } + +#if defined(__has_feature) && __has_feature(thread_sanitizer) +// A helper which synchronously runs on_change() on a fixed background thread +// so that ThreadSanitizer can potentially detect issues +// This deliberately uses an unsafe spinlock for synchronization to ensure that +// the code being tested has to supply all required safety +static class TsanNotifyWorker { +public: + TsanNotifyWorker() + { + m_thread = std::thread([&] { work(); }); + } + + void work() + { + while (true) { + auto value = m_signal.load(std::memory_order_relaxed); + if (value == 0 || value == 1) + continue; + if (value == 2) + return; + + auto c = reinterpret_cast(value); + c->on_change(); + m_signal.store(1, std::memory_order_relaxed); + } + } + + ~TsanNotifyWorker() + { + m_signal = 2; + m_thread.join(); + } + + void on_change(realm::_impl::RealmCoordinator* c) + { + m_signal.store(reinterpret_cast(c), std::memory_order_relaxed); + while (m_signal.load(std::memory_order_relaxed) != 1) ; + } + +private: + std::atomic m_signal{0}; + std::thread m_thread; +} s_worker; + +void advance_and_notify(realm::Realm& realm) +{ + s_worker.on_change(realm::_impl::RealmCoordinator::get_existing_coordinator(realm.config().path).get()); + realm.notify(); +} + +#else // __has_feature(thread_sanitizer) + +void advance_and_notify(realm::Realm& realm) +{ + realm::_impl::RealmCoordinator::get_existing_coordinator(realm.config().path)->on_change(); + realm.notify(); +} +#endif diff --git a/tests/util/test_file.hpp b/tests/util/test_file.hpp index bd20d671..2f6a5135 100644 --- a/tests/util/test_file.hpp +++ b/tests/util/test_file.hpp @@ -12,4 +12,6 @@ struct InMemoryTestFile : TestFile { InMemoryTestFile(); }; +void advance_and_notify(realm::Realm& realm); + #endif