//////////////////////////////////////////////////////////////////////////// // // 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 "catch2/catch.hpp" #include "util/event_loop.hpp" #include "util/index_helpers.hpp" #include "util/test_file.hpp" #include "impl/object_accessor_impl.hpp" #include "impl/realm_coordinator.hpp" #include "binding_context.hpp" #include "object_schema.hpp" #include "property.hpp" #include "results.hpp" #include "schema.hpp" #include #include #include #include #if REALM_ENABLE_SYNC #include "sync/sync_manager.hpp" #include "sync/sync_session.hpp" #endif namespace realm { class TestHelper { public: static SharedGroup& get_shared_group(SharedRealm const& shared_realm) { return *Realm::Internal::get_shared_group(*shared_realm); } }; } using namespace realm; using namespace std::string_literals; namespace { using AnyDict = std::map; using AnyVec = std::vector; } struct TestContext : CppContext { std::map defaults; using CppContext::CppContext; TestContext(TestContext& parent, realm::Property const& prop) : CppContext(parent, prop) , defaults(parent.defaults) { } void will_change(Object const&, Property const&) {} void did_change() {} std::string print(util::Any) { return "not implemented"; } bool allow_missing(util::Any) { return false; } }; TEST_CASE("notifications: async delivery") { _impl::RealmCoordinator::assert_no_open_realms(); InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({ {"object", { {"value", PropertyType::Int} }}, }); auto coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path); auto table = r->read_group().get_table("class_object"); r->begin_transaction(); table->add_empty_row(10); for (int i = 0; i < 10; ++i) table->set_int(0, i, i * 2); r->commit_transaction(); Results results(r, table->where().greater(0, 0).less(0, 10)); int notification_calls = 0; auto token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); ++notification_calls; }); auto make_local_change = [&] { r->begin_transaction(); table->set_int(0, 0, 4); r->commit_transaction(); }; auto make_remote_change = [&] { auto r2 = coordinator->get_realm(); r2->begin_transaction(); r2->read_group().get_table("class_object")->set_int(0, 0, 5); r2->commit_transaction(); }; SECTION("initial notification") { SECTION("is delivered on notify()") { REQUIRE(notification_calls == 0); advance_and_notify(*r); REQUIRE(notification_calls == 1); } SECTION("is delivered on refresh()") { coordinator->on_change(); REQUIRE(notification_calls == 0); r->refresh(); REQUIRE(notification_calls == 1); } SECTION("is delivered on begin_transaction()") { coordinator->on_change(); REQUIRE(notification_calls == 0); r->begin_transaction(); REQUIRE(notification_calls == 1); r->cancel_transaction(); } SECTION("is delivered on notify() even with autorefresh disabled") { r->set_auto_refresh(false); REQUIRE(notification_calls == 0); advance_and_notify(*r); REQUIRE(notification_calls == 1); } SECTION("refresh() blocks due to initial results not being ready") { REQUIRE(notification_calls == 0); JoiningThread thread([&] { std::this_thread::sleep_for(std::chrono::microseconds(5000)); coordinator->on_change(); }); r->refresh(); REQUIRE(notification_calls == 1); } SECTION("begin_transaction() blocks due to initial results not being ready") { REQUIRE(notification_calls == 0); JoiningThread thread([&] { std::this_thread::sleep_for(std::chrono::microseconds(5000)); coordinator->on_change(); }); r->begin_transaction(); REQUIRE(notification_calls == 1); r->cancel_transaction(); } SECTION("notify() does not block due to initial results not being ready") { REQUIRE(notification_calls == 0); r->notify(); REQUIRE(notification_calls == 0); } SECTION("is delivered after invalidate()") { r->invalidate(); SECTION("notify()") { coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->notify(); REQUIRE(notification_calls == 1); } SECTION("notify() without autorefresh") { r->set_auto_refresh(false); coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->notify(); REQUIRE(notification_calls == 1); } SECTION("refresh()") { coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->refresh(); REQUIRE(notification_calls == 1); } SECTION("begin_transaction()") { coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->begin_transaction(); REQUIRE(notification_calls == 1); r->cancel_transaction(); } } SECTION("is delivered by notify() even if there are later versions") { REQUIRE(notification_calls == 0); coordinator->on_change(); make_remote_change(); r->notify(); REQUIRE(notification_calls == 1); } } advance_and_notify(*r); SECTION("notifications for local changes") { make_local_change(); coordinator->on_change(); REQUIRE(notification_calls == 1); SECTION("notify()") { r->notify(); REQUIRE(notification_calls == 2); } SECTION("notify() without autorefresh") { r->set_auto_refresh(false); r->notify(); REQUIRE(notification_calls == 2); } SECTION("refresh()") { r->refresh(); REQUIRE(notification_calls == 2); } SECTION("begin_transaction()") { r->begin_transaction(); REQUIRE(notification_calls == 2); r->cancel_transaction(); } } SECTION("notifications for remote changes") { make_remote_change(); coordinator->on_change(); REQUIRE(notification_calls == 1); SECTION("notify()") { r->notify(); REQUIRE(notification_calls == 2); } SECTION("notify() without autorefresh") { r->set_auto_refresh(false); r->notify(); REQUIRE(notification_calls == 1); r->refresh(); REQUIRE(notification_calls == 2); } SECTION("refresh()") { r->refresh(); REQUIRE(notification_calls == 2); } SECTION("begin_transaction()") { r->begin_transaction(); REQUIRE(notification_calls == 2); r->cancel_transaction(); } } SECTION("notifications are not delivered when the token is destroyed before they are calculated") { make_remote_change(); REQUIRE(notification_calls == 1); token = {}; advance_and_notify(*r); REQUIRE(notification_calls == 1); } SECTION("notifications are not delivered when the token is destroyed before they are delivered") { make_remote_change(); REQUIRE(notification_calls == 1); coordinator->on_change(); token = {}; r->notify(); REQUIRE(notification_calls == 1); } SECTION("notifications are delivered on the next cycle 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) { token2 = {}; token3 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { called = true; }); }); advance_and_notify(*r); REQUIRE_FALSE(called); advance_and_notify(*r); REQUIRE(called); } SECTION("notifications are delivered on the next cycle when a new callback is added from within a callback") { auto results2 = results; auto results3 = results; NotificationToken token2, token3, token4; bool called = false; auto check = [&](Results& outer, Results& inner) { token2 = outer.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token2 = {}; token3 = inner.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { called = true; }); }); advance_and_notify(*r); REQUIRE_FALSE(called); advance_and_notify(*r); REQUIRE(called); }; SECTION("same Results") { check(results, results); } SECTION("Results which has never had a notifier") { check(results, results2); } SECTION("Results which used to have callbacks but no longer does") { SECTION("notifier before active") { token3 = results2.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token3 = {}; }); check(results3, results2); } SECTION("notifier after active") { token3 = results2.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token3 = {}; }); check(results, results2); } } SECTION("Results which already has callbacks") { SECTION("notifier before active") { token4 = results2.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { }); check(results3, results2); } SECTION("notifier after active") { token4 = results2.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { }); check(results, results2); } } } SECTION("remote changes made before adding a callback from within a callback are not reported") { NotificationToken token2, token3; bool called = false; token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token2 = {}; make_remote_change(); coordinator->on_change(); token3 = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) { called = true; REQUIRE(c.empty()); REQUIRE(table->get_int(0, 0) == 5); }); }); advance_and_notify(*r); REQUIRE_FALSE(called); 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("the first call of a notification can include changes if it previously ran for a different callback") { r->begin_transaction(); auto token2 = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) { REQUIRE(!c.empty()); }); table->set_int(0, table->add_empty_row(), 5); r->commit_transaction(); advance_and_notify(*r); } SECTION("handling of results not ready") { make_remote_change(); SECTION("notify() does nothing") { r->notify(); REQUIRE(notification_calls == 1); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 2); } SECTION("refresh() blocks") { REQUIRE(notification_calls == 1); JoiningThread thread([&] { std::this_thread::sleep_for(std::chrono::microseconds(5000)); coordinator->on_change(); }); r->refresh(); REQUIRE(notification_calls == 2); } SECTION("refresh() advances to the first version with notifiers ready that is at least a recent as the newest at the time it is called") { JoiningThread thread([&] { std::this_thread::sleep_for(std::chrono::microseconds(5000)); make_remote_change(); coordinator->on_change(); make_remote_change(); }); // advances to the version after the one it was waiting for, but still // not the latest r->refresh(); REQUIRE(notification_calls == 2); thread.join(); REQUIRE(notification_calls == 2); // now advances to the latest coordinator->on_change(); r->refresh(); REQUIRE(notification_calls == 3); } SECTION("begin_transaction() blocks") { REQUIRE(notification_calls == 1); JoiningThread thread([&] { std::this_thread::sleep_for(std::chrono::microseconds(5000)); coordinator->on_change(); }); r->begin_transaction(); REQUIRE(notification_calls == 2); r->cancel_transaction(); } SECTION("refresh() does not block for results without callbacks") { token = {}; // this would deadlock if it waits for the notifier to be ready r->refresh(); } SECTION("begin_transaction() does not block for results without callbacks") { token = {}; // this would deadlock if it waits for the notifier to be ready r->begin_transaction(); r->cancel_transaction(); } SECTION("begin_transaction() does not block for Results for different Realms") { // this would deadlock if beginning the write on the secondary Realm // waited for the primary Realm to be ready make_remote_change(); // sanity check that the notifications never did run r->notify(); REQUIRE(notification_calls == 1); } } SECTION("handling of stale results") { make_remote_change(); coordinator->on_change(); make_remote_change(); SECTION("notify() uses the older version") { r->notify(); REQUIRE(notification_calls == 2); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 3); r->notify(); REQUIRE(notification_calls == 3); } SECTION("refresh() blocks") { REQUIRE(notification_calls == 1); JoiningThread thread([&] { std::this_thread::sleep_for(std::chrono::microseconds(5000)); coordinator->on_change(); }); r->refresh(); REQUIRE(notification_calls == 2); } SECTION("begin_transaction() blocks") { REQUIRE(notification_calls == 1); JoiningThread thread([&] { std::this_thread::sleep_for(std::chrono::microseconds(5000)); coordinator->on_change(); }); r->begin_transaction(); REQUIRE(notification_calls == 2); r->cancel_transaction(); } } SECTION("updates are delivered after invalidate()") { r->invalidate(); make_remote_change(); SECTION("notify()") { coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->notify(); REQUIRE(notification_calls == 2); } SECTION("notify() without autorefresh") { r->set_auto_refresh(false); coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->notify(); REQUIRE(notification_calls == 1); r->refresh(); REQUIRE(notification_calls == 2); } SECTION("refresh()") { coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->refresh(); REQUIRE(notification_calls == 2); } SECTION("begin_transaction()") { coordinator->on_change(); REQUIRE_FALSE(r->is_in_read_transaction()); r->begin_transaction(); REQUIRE(notification_calls == 2); r->cancel_transaction(); } } SECTION("refresh() from within changes_available() do not interfere with notification delivery") { struct Context : BindingContext { Realm& realm; Context(Realm& realm) : realm(realm) { } void changes_available() override { REQUIRE(realm.refresh()); } }; make_remote_change(); coordinator->on_change(); r->set_auto_refresh(false); REQUIRE(notification_calls == 1); r->notify(); REQUIRE(notification_calls == 1); r->m_binding_context.reset(new Context(*r)); r->notify(); REQUIRE(notification_calls == 2); } SECTION("refresh() from within a notification is a no-op") { token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); REQUIRE_FALSE(r->refresh()); // would deadlock if it actually tried to refresh }); advance_and_notify(*r); make_remote_change(); // 1 coordinator->on_change(); make_remote_change(); // 2 r->notify(); // advances to version from 1 coordinator->on_change(); REQUIRE(r->refresh()); // advances to version from 2 REQUIRE_FALSE(r->refresh()); // does not advance since it's now up-to-date } SECTION("begin_transaction() from within a notification does not send notifications immediately") { bool first = true; auto token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); if (first) first = false; else { // would deadlock if it tried to send notifications as they aren't ready yet r->begin_transaction(); r->cancel_transaction(); } }); advance_and_notify(*r); make_remote_change(); // 1 coordinator->on_change(); make_remote_change(); // 2 r->notify(); // advances to version from 1 REQUIRE(notification_calls == 2); coordinator->on_change(); REQUIRE_FALSE(r->refresh()); // we made the commit locally, so no advancing here REQUIRE(notification_calls == 3); } SECTION("begin_transaction() from within a notification does not break delivering additional notifications") { size_t calls = 0; token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); if (++calls == 1) return; // force the read version to advance by beginning a transaction r->begin_transaction(); r->cancel_transaction(); }); auto results2 = results; size_t calls2 = 0; auto token2 = results2.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); if (++calls2 == 1) return; REQUIRE_INDICES(c.insertions, 0); }); advance_and_notify(*r); REQUIRE(calls == 1); REQUIRE(calls2 == 1); make_remote_change(); // 1 coordinator->on_change(); make_remote_change(); // 2 r->notify(); // advances to version from 1 REQUIRE(calls == 2); REQUIRE(calls2 == 2); } SECTION("begin_transaction() from within did_change() does not break delivering collection notification") { struct Context : BindingContext { Realm& realm; Context(Realm& realm) : realm(realm) { } void did_change(std::vector const&, std::vector const&, bool) override { if (!realm.is_in_transaction()) { // advances to version from 2 (and recursively calls this, hence the check above) realm.begin_transaction(); realm.cancel_transaction(); } } }; r->m_binding_context.reset(new Context(*r)); make_remote_change(); // 1 coordinator->on_change(); make_remote_change(); // 2 r->notify(); // advances to version from 1 } SECTION("is_in_transaction() is reported correctly within a notification from begin_transaction() and changes can be made") { bool first = true; token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); if (first) { REQUIRE_FALSE(r->is_in_transaction()); first = false; } else { REQUIRE(r->is_in_transaction()); table->set_int(0, 0, 100); } }); advance_and_notify(*r); make_remote_change(); coordinator->on_change(); r->begin_transaction(); REQUIRE(table->get_int(0, 0) == 100); r->cancel_transaction(); REQUIRE(table->get_int(0, 0) != 100); } SECTION("invalidate() from within notification is a no-op") { token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); r->invalidate(); REQUIRE(r->is_in_read_transaction()); }); advance_and_notify(*r); REQUIRE(r->is_in_read_transaction()); make_remote_change(); coordinator->on_change(); r->begin_transaction(); REQUIRE(r->is_in_transaction()); r->cancel_transaction(); } SECTION("cancel_transaction() from within notification ends the write transaction started by begin_transaction()") { token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); if (r->is_in_transaction()) r->cancel_transaction(); }); advance_and_notify(*r); make_remote_change(); coordinator->on_change(); r->begin_transaction(); REQUIRE_FALSE(r->is_in_transaction()); } } TEST_CASE("notifications: skip") { _impl::RealmCoordinator::assert_no_open_realms(); InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({ {"object", { {"value", PropertyType::Int} }}, }); auto coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path); auto table = r->read_group().get_table("class_object"); r->begin_transaction(); table->add_empty_row(10); for (int i = 0; i < 10; ++i) table->set_int(0, i, i * 2); r->commit_transaction(); Results results(r, table->where()); auto add_callback = [](Results& results, int& calls, CollectionChangeSet& changes) { return results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); ++calls; changes = std::move(c); }); }; auto make_local_change = [&](auto& token) { r->begin_transaction(); table->add_empty_row(); token.suppress_next(); r->commit_transaction(); }; auto make_remote_change = [&] { auto r2 = coordinator->get_realm(); r2->begin_transaction(); r2->read_group().get_table("class_object")->add_empty_row(); r2->commit_transaction(); }; int calls1 = 0; CollectionChangeSet changes1; auto token1 = add_callback(results, calls1, changes1); SECTION("no notification is sent when only callback is skipped") { advance_and_notify(*r); REQUIRE(calls1 == 1); make_local_change(token1); advance_and_notify(*r); REQUIRE(calls1 == 1); REQUIRE(changes1.empty()); } SECTION("unskipped tokens for the same Results are still delivered") { int calls2 = 0; CollectionChangeSet changes2; auto token2 = add_callback(results, calls2, changes2); advance_and_notify(*r); REQUIRE(calls1 == 1); REQUIRE(calls2 == 1); make_local_change(token1); advance_and_notify(*r); REQUIRE(calls1 == 1); REQUIRE(changes1.empty()); REQUIRE(calls2 == 2); REQUIRE_INDICES(changes2.insertions, 10); } SECTION("unskipped tokens for different Results are still delivered") { Results results2(r, table->where()); int calls2 = 0; CollectionChangeSet changes2; auto token2 = add_callback(results2, calls2, changes2); advance_and_notify(*r); REQUIRE(calls1 == 1); REQUIRE(calls2 == 1); make_local_change(token1); advance_and_notify(*r); REQUIRE(calls1 == 1); REQUIRE(changes1.empty()); REQUIRE(calls2 == 2); REQUIRE_INDICES(changes2.insertions, 10); } SECTION("additional commits which occur before calculation are merged in") { int calls2 = 0; CollectionChangeSet changes2; auto token2 = add_callback(results, calls2, changes2); advance_and_notify(*r); REQUIRE(calls1 == 1); REQUIRE(calls2 == 1); make_local_change(token1); make_remote_change(); advance_and_notify(*r); REQUIRE(calls1 == 2); REQUIRE_INDICES(changes1.insertions, 11); REQUIRE(calls2 == 2); REQUIRE_INDICES(changes2.insertions, 10, 11); } SECTION("additional commits which occur before delivery are merged in") { int calls2 = 0; CollectionChangeSet changes2; auto token2 = add_callback(results, calls2, changes2); advance_and_notify(*r); REQUIRE(calls1 == 1); REQUIRE(calls2 == 1); make_local_change(token1); coordinator->on_change(); make_remote_change(); advance_and_notify(*r); REQUIRE(calls1 == 2); REQUIRE_INDICES(changes1.insertions, 11); REQUIRE(calls2 == 2); REQUIRE_INDICES(changes2.insertions, 10, 11); } SECTION("skipping must be done from within a write transaction") { REQUIRE_THROWS(token1.suppress_next()); } SECTION("skipping must be done from the Realm's thread") { advance_and_notify(*r); r->begin_transaction(); std::thread([&] { REQUIRE_THROWS(token1.suppress_next()); }).join(); r->cancel_transaction(); } SECTION("new notifiers do not interfere with skipping") { advance_and_notify(*r); REQUIRE(calls1 == 1); CollectionChangeSet changes; // new notifier at a version before the skipped one auto r2 = coordinator->get_realm(); Results results2(r2, r2->read_group().get_table("class_object")->where()); int calls2 = 0; auto token2 = add_callback(results2, calls2, changes); make_local_change(token1); // new notifier at the skipped version auto r3 = coordinator->get_realm(); Results results3(r3, r3->read_group().get_table("class_object")->where()); int calls3 = 0; auto token3 = add_callback(results3, calls3, changes); make_remote_change(); // new notifier at version after the skipped one auto r4 = coordinator->get_realm(); Results results4(r4, r4->read_group().get_table("class_object")->where()); int calls4 = 0; auto token4 = add_callback(results4, calls4, changes); coordinator->on_change(); r->notify(); r2->notify(); r3->notify(); r4->notify(); REQUIRE(calls1 == 2); REQUIRE(calls2 == 1); REQUIRE(calls3 == 1); REQUIRE(calls4 == 1); } SECTION("skipping only effects the current transaction even if no notification would occur anyway") { advance_and_notify(*r); REQUIRE(calls1 == 1); // would not produce a notification even if it wasn't skipped because no changes were made r->begin_transaction(); token1.suppress_next(); r->commit_transaction(); advance_and_notify(*r); REQUIRE(calls1 == 1); // should now produce a notification r->begin_transaction(); table->add_empty_row(); r->commit_transaction(); advance_and_notify(*r); REQUIRE(calls1 == 2); } SECTION("removing skipped notifier before it gets the chance to run") { advance_and_notify(*r); REQUIRE(calls1 == 1); // Set the skip version make_local_change(token1); // Advance the file to a version after the skip version make_remote_change(); REQUIRE(calls1 == 1); // Remove the skipped notifier and add an entirely new notifier, so that // notifications need to run but the skip logic shouldn't be used token1 = {}; results = {}; Results results2(r, table->where()); auto token2 = add_callback(results2, calls1, changes1); advance_and_notify(*r); REQUIRE(calls1 == 2); } } #if REALM_PLATFORM_APPLE TEST_CASE("notifications: async error handling") { _impl::RealmCoordinator::assert_no_open_realms(); InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({ {"object", { {"value", PropertyType::Int}, }}, }); auto coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path); Results results(r, *r->read_group().get_table("class_object")); auto r2 = Realm::get_shared_realm(config); 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; bool called = false; auto token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); REQUIRE(!called); SECTION("error is delivered on notify() without changes") { coordinator->on_change(); REQUIRE(!called); r->notify(); REQUIRE(called); } SECTION("error is delivered on notify() with changes") { r2->begin_transaction(); r2->commit_transaction(); REQUIRE(!called); coordinator->on_change(); REQUIRE(!called); r->notify(); REQUIRE(called); } SECTION("error is delivered on refresh() without changes") { coordinator->on_change(); REQUIRE(!called); r->refresh(); REQUIRE(called); } SECTION("error is delivered on refresh() with changes") { r2->begin_transaction(); r2->commit_transaction(); REQUIRE(!called); coordinator->on_change(); REQUIRE(!called); r->refresh(); REQUIRE(called); } SECTION("error is delivered on begin_transaction() without changes") { coordinator->on_change(); REQUIRE(!called); r->begin_transaction(); REQUIRE(called); r->cancel_transaction(); } SECTION("error is delivered on begin_transaction() with changes") { r2->begin_transaction(); r2->commit_transaction(); REQUIRE(!called); coordinator->on_change(); REQUIRE(!called); r->begin_transaction(); REQUIRE(called); r->cancel_transaction(); } SECTION("adding another callback sends the error to only the newly added one") { advance_and_notify(*r); REQUIRE(called); 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("destroying a token from before the error does not remove newly added callbacks") { advance_and_notify(*r); bool called = false; auto token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); token = {}; advance_and_notify(*r); REQUIRE(called); } SECTION("adding another callback from within an error callback defers delivery") { NotificationToken token2; token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); }); advance_and_notify(*r); REQUIRE(!called); advance_and_notify(*r); REQUIRE(called); } SECTION("adding a callback to a different collection from within the error callback defers delivery") { auto results2 = results; NotificationToken token2; token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token2 = results2.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); }); advance_and_notify(*r); REQUIRE(!called); advance_and_notify(*r); REQUIRE(called); } } 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 only sends the error to the new one") { 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); } } } #endif #if REALM_ENABLE_SYNC TEST_CASE("notifications: sync") { _impl::RealmCoordinator::assert_no_open_realms(); SyncServer server(false); SyncTestFile config(server); config.cache = false; config.schema = Schema{ {"object", { {"value", PropertyType::Int}, }}, }; SECTION("sync progress commits do not distrupt notifications") { auto r = Realm::get_shared_realm(config); auto wait_realm = Realm::get_shared_realm(config); Results results(r, *r->read_group().get_table("class_object")); Results wait_results(wait_realm, *wait_realm->read_group().get_table("class_object")); auto token1 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { }); auto token2 = wait_results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { }); // Add an object to the Realm so that notifications are needed { auto write_realm = Realm::get_shared_realm(config); write_realm->begin_transaction(); sync::create_object(write_realm->read_group(), *write_realm->read_group().get_table("class_object")); write_realm->commit_transaction(); } // Wait for the notifications to become ready for the new version wait_realm->refresh(); // Start the server and wait for the Realm to be uploaded so that sync // makes some writes to the Realm and bumps the version server.start(); wait_for_upload(*r); // Make sure that the notifications still get delivered rather than // waiting forever due to that we don't get a commit notification from // the commits sync makes to store the upload progress r->refresh(); } } #endif TEST_CASE("notifications: results") { _impl::RealmCoordinator::assert_no_open_realms(); InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({ {"object", { {"value", PropertyType::Int}, {"link", PropertyType::Object|PropertyType::Nullable, "linked to object"} }}, {"other object", { {"value", PropertyType::Int} }}, {"linking object", { {"link", PropertyType::Object|PropertyType::Nullable, "object"} }}, {"linked to object", { {"value", PropertyType::Int} }} }); auto coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path); auto table = r->read_group().get_table("class_object"); r->begin_transaction(); r->read_group().get_table("class_linked to object")->add_empty_row(10); table->add_empty_row(10); for (int i = 0; i < 10; ++i) { table->set_int(0, i, i * 2); table->set_link(1, i, i); } r->commit_transaction(); auto r2 = coordinator->get_realm(); auto r2_table = r2->read_group().get_table("class_object"); Results results(r, table->where().greater(0, 0).less(0, 10)); SECTION("unsorted 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(); f(); r->commit_transaction(); advance_and_notify(*r); }; 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); } 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("swapping adjacent matching and non-matching rows does not send notifications") { write([&] { table->swap_rows(0, 1); }); REQUIRE(notification_calls == 1); } SECTION("swapping non-adjacent matching and non-matching rows send a single insert/delete pair") { write([&] { table->swap_rows(0, 2); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 1); REQUIRE_INDICES(change.insertions, 0); } SECTION("swapping matching rows sends insert/delete pairs") { write([&] { table->swap_rows(1, 4); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 0, 3); REQUIRE_INDICES(change.insertions, 0, 3); write([&] { table->swap_rows(1, 2); table->swap_rows(2, 3); table->swap_rows(3, 4); }); REQUIRE(notification_calls == 3); REQUIRE_INDICES(change.deletions, 1, 2, 3); REQUIRE_INDICES(change.insertions, 0, 1, 2); } SECTION("swap does not inhibit move collapsing after removals") { write([&] { table->swap_rows(2, 3); table->set_int(0, 3, 100); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 1); REQUIRE(change.insertions.empty()); } 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); REQUIRE_INDICES(change.modifications_new, 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()); REQUIRE(change.modifications_new.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("moving a matching row via subsumption marks that row as modified") { 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") { r2->begin_transaction(); r2_table->set_int(0, 0, 6); r2->commit_transaction(); coordinator->on_change(); r2->begin_transaction(); r2_table->set_int(0, 1, 0); r2->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 2); } SECTION("inserting a row then modifying it in a second transaction does not report it as modified") { r2->begin_transaction(); size_t ndx = r2_table->add_empty_row(); r2_table->set_int(0, ndx, 6); r2->commit_transaction(); coordinator->on_change(); r2->begin_transaction(); r2_table->set_int(0, ndx, 7); r2->commit_transaction(); advance_and_notify(*r); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.insertions, 4); REQUIRE(change.modifications.empty()); REQUIRE(change.modifications_new.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); REQUIRE_INDICES(change.modifications_new, 1); } SECTION("notifications are not delivered when collapsing transactions results in no net change") { r2->begin_transaction(); size_t ndx = r2_table->add_empty_row(); r2_table->set_int(0, ndx, 5); r2->commit_transaction(); coordinator->on_change(); r2->begin_transaction(); r2_table->move_last_over(ndx); r2->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 1); } SECTION("inserting a non-matching row at the beginning does not produce a notification") { write([&] { table->insert_empty_row(1); }); REQUIRE(notification_calls == 1); } SECTION("inserting a matching row at the beginning marks just it as inserted") { write([&] { table->insert_empty_row(0); table->set_int(0, 0, 5); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.insertions, 0); } } SECTION("before/after change callback") { struct Callback { size_t before_calls = 0; size_t after_calls = 0; CollectionChangeSet before_change; CollectionChangeSet after_change; std::function on_before = []{}; std::function on_after = []{}; void before(CollectionChangeSet c) { before_change = c; ++before_calls; on_before(); } void after(CollectionChangeSet c) { after_change = c; ++after_calls; on_after(); } void error(std::exception_ptr) { FAIL("error() should not be called"); } } callback; auto token = results.add_notification_callback(&callback); advance_and_notify(*r); SECTION("only after() is called for initial results") { REQUIRE(callback.before_calls == 0); REQUIRE(callback.after_calls == 1); REQUIRE(callback.after_change.empty()); } auto write = [&](auto&& func) { r2->begin_transaction(); func(*r2_table); r2->commit_transaction(); advance_and_notify(*r); }; SECTION("both are called after a write") { write([&](auto&& t) { t.set_int(0, t.add_empty_row(), 5); }); REQUIRE(callback.before_calls == 1); REQUIRE(callback.after_calls == 2); REQUIRE_INDICES(callback.before_change.insertions, 4); REQUIRE_INDICES(callback.after_change.insertions, 4); } SECTION("deleted objects are usable in before()") { callback.on_before = [&] { REQUIRE(results.size() == 4); REQUIRE_INDICES(callback.before_change.deletions, 0); REQUIRE(results.get(0).is_attached()); REQUIRE(results.get(0).get_int(0) == 2); }; write([&](auto&& t) { t.move_last_over(results.get(0).get_index()); }); REQUIRE(callback.before_calls == 1); REQUIRE(callback.after_calls == 2); } SECTION("inserted objects are usable in after()") { callback.on_after = [&] { REQUIRE(results.size() == 5); REQUIRE_INDICES(callback.after_change.insertions, 4); REQUIRE(results.last()->get_int(0) == 5); }; write([&](auto&& t) { t.set_int(0, t.add_empty_row(), 5); }); REQUIRE(callback.before_calls == 1); REQUIRE(callback.after_calls == 2); } } SECTION("sorted notifications") { // Sort in descending order results = results.sort({*table, {{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("swapping rows does not send notifications") { write([&] { table->swap_rows(2, 3); }); 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, 3); REQUIRE_INDICES(change.modifications_new, 3); } SECTION("swapping leaves modified rows marked as modified") { write([&] { table->set_int(0, 1, 3); table->swap_rows(1, 2); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.modifications, 3); REQUIRE_INDICES(change.modifications_new, 3); write([&] { table->swap_rows(3, 1); table->set_int(0, 1, 7); }); REQUIRE(notification_calls == 3); REQUIRE_INDICES(change.modifications, 1); REQUIRE_INDICES(change.modifications_new, 1); } 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") { r2->begin_transaction(); r2_table->set_int(0, 0, 5); r2->commit_transaction(); r2->begin_transaction(); r2_table->set_int(0, 1, 0); r2->commit_transaction(); 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); } } SECTION("distinct notifications") { results = results.distinct(SortDescriptor(*table, {{0}})); 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("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); REQUIRE_INDICES(change.modifications_new, 0); } SECTION("modifying a non-matching row which is after the distinct results in the table to be a same value \ in the distinct results doesn't send notification.") { write([&] { table->set_int(0, 6, 2); }); REQUIRE(notification_calls == 1); } SECTION("modifying a non-matching row which is before the distinct results in the table to be a same value \ in the distinct results send insert + delete.") { write([&] { table->set_int(0, 0, 2); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 0); REQUIRE_INDICES(change.insertions, 0); } SECTION("modifying a matching row to duplicated value in distinct results marks that row as deleted") { write([&] { table->set_int(0, 2, 2); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 1); } SECTION("modifying a non-matching row to match and different value marks that row as inserted") { write([&] { table->set_int(0, 0, 1); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.insertions, 0); } } SECTION("schema changes") { CollectionChangeSet change; auto token = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); change = c; }); advance_and_notify(*r); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); advance_and_notify(*r); }; SECTION("insert table before observed table") { write([&] { size_t row = table->add_empty_row(); table->set_int(0, row, 5); r->read_group().insert_table(0, "new table"); table->insert_empty_row(0); table->set_int(0, 0, 5); }); REQUIRE_INDICES(change.insertions, 0, 5); } auto linked_table = table->get_link_target(1); SECTION("insert new column before link column") { write([&] { linked_table->set_int(0, 1, 5); table->insert_column(0, type_Int, "new col"); linked_table->set_int(0, 2, 5); }); REQUIRE_INDICES(change.modifications, 0, 1); } SECTION("insert table before link target") { write([&] { linked_table->set_int(0, 1, 5); r->read_group().insert_table(0, "new table"); linked_table->set_int(0, 2, 5); }); REQUIRE_INDICES(change.modifications, 0, 1); } } } TEST_CASE("results: notifications after move") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({ {"object", { {"value", PropertyType::Int}, }}, }); auto table = r->read_group().get_table("class_object"); auto results = std::make_unique(r, *table); int notification_calls = 0; auto token = results->add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); ++notification_calls; }); advance_and_notify(*r); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); advance_and_notify(*r); }; SECTION("notifications continue to work after Results is moved (move-constructor)") { Results r(std::move(*results)); results.reset(); write([&] { table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(notification_calls == 2); } SECTION("notifications continue to work after Results is moved (move-assignment)") { Results r; r = std::move(*results); results.reset(); write([&] { table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(notification_calls == 2); } } TEST_CASE("results: notifier with no callbacks") { _impl::RealmCoordinator::assert_no_open_realms(); InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path); auto r = coordinator->get_realm(std::move(config)); r->update_schema({ {"object", { {"value", PropertyType::Int}, }}, }); auto table = r->read_group().get_table("class_object"); Results results(r, table->where()); results.last(); // force evaluation and creation of TableView SECTION("refresh() does not block due to implicit notifier") { // Create and then immediately remove a callback because // `automatic_change_notifications = false` makes Results not implicitly // create a notifier results.add_notification_callback([](CollectionChangeSet const&, std::exception_ptr) {}); auto r2 = coordinator->get_realm(); r2->begin_transaction(); r2->read_group().get_table("class_object")->add_empty_row(); r2->commit_transaction(); r->refresh(); // would deadlock if there was a callback } SECTION("refresh() does not attempt to deliver stale results") { results.add_notification_callback([](CollectionChangeSet const&, std::exception_ptr) {}); // Create version 1 r->begin_transaction(); table->add_empty_row(); r->commit_transaction(); r->begin_transaction(); // Run async query for version 1 coordinator->on_change(); // Create version 2 without ever letting 1 be delivered table->add_empty_row(); r->commit_transaction(); // Give it a chance to deliver the async query results (and fail, becuse // they're for version 1 and the realm is at 2) r->refresh(); } SECTION("should not pin the source version even after the Realm has been closed") { auto r2 = coordinator->get_realm(); REQUIRE(r != r2); r->close(); auto& shared_group = TestHelper::get_shared_group(r2); // There's always at least 2 live versions because the previous version // isn't clean up until the *next* commit REQUIRE(shared_group.get_number_of_versions() == 2); auto table = r2->read_group().get_table("class_object"); r2->begin_transaction(); table->add_empty_row(); r2->commit_transaction(); r2->begin_transaction(); table->add_empty_row(); r2->commit_transaction(); // Would now be 3 if the closed Realm is still pinning the version it was at REQUIRE(shared_group.get_number_of_versions() == 2); } } TEST_CASE("results: error messages") { InMemoryTestFile config; config.schema = Schema{ {"object", { {"value", PropertyType::String}, }}, }; auto r = Realm::get_shared_realm(config); auto table = r->read_group().get_table("class_object"); Results results(r, *table); r->begin_transaction(); table->add_empty_row(); r->commit_transaction(); SECTION("out of bounds access") { REQUIRE_THROWS_WITH(results.get(5), "Requested index 5 greater than max 0"); } SECTION("unsupported aggregate operation") { REQUIRE_THROWS_WITH(results.sum(0), "Cannot sum property 'value': operation not supported for 'string' properties"); } } TEST_CASE("results: snapshots") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; config.schema = Schema{ {"object", { {"value", PropertyType::Int}, {"array", PropertyType::Array|PropertyType::Object, "linked to object"} }}, {"linked to object", { {"value", PropertyType::Int} }} }; auto r = Realm::get_shared_realm(config); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); advance_and_notify(*r); }; SECTION("snapshot of empty Results") { Results results; auto snapshot = results.snapshot(); REQUIRE(snapshot.size() == 0); } SECTION("snapshot of Results based on Table") { auto table = r->read_group().get_table("class_object"); Results results(r, *table); { // A newly-added row should not appear in the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 0); write([=]{ table->add_empty_row(); }); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 0); } { // Removing a row present in the snapshot should not affect the size of the snapshot, // but will result in the snapshot returning a detached row accessor. auto snapshot = results.snapshot(); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 1); write([=]{ table->move_last_over(0); }); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); // Adding a row at the same index that was formerly present in the snapshot shouldn't // affect the state of the snapshot. write([=]{ table->add_empty_row(); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); } } SECTION("snapshot of Results based on LinkView") { auto object = r->read_group().get_table("class_object"); auto linked_to = r->read_group().get_table("class_linked to object"); write([=]{ object->add_empty_row(); }); LinkViewRef lv = object->get_linklist(1, 0); Results results(r, lv); { // A newly-added row should not appear in the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 0); write([=]{ lv->add(linked_to->add_empty_row()); }); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 0); } { // Removing a row from the link list should not affect the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 1); write([=]{ lv->remove(0); }); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 1); REQUIRE(snapshot.get(0).is_attached()); // Removing a row present in the snapshot from its table should result in the snapshot // returning a detached row accessor. write([=]{ linked_to->remove(0); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); // Adding a new row to the link list shouldn't affect the state of the snapshot. write([=]{ lv->add(linked_to->add_empty_row()); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); } } SECTION("snapshot of Results based on Query") { auto table = r->read_group().get_table("class_object"); Query q = table->column(0) > 0; Results results(r, std::move(q)); { // A newly-added row should not appear in the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 0); write([=]{ table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 0); } { // Updating a row to no longer match the query criteria should not affect the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 1); write([=]{ table->set_int(0, 0, 0); }); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 1); REQUIRE(snapshot.get(0).is_attached()); // Removing a row present in the snapshot from its table should result in the snapshot // returning a detached row accessor. write([=]{ table->remove(0); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); // Adding a new row that matches the query criteria shouldn't affect the state of the snapshot. write([=]{ table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); } } SECTION("snapshot of Results based on TableView from query") { auto table = r->read_group().get_table("class_object"); Query q = table->column(0) > 0; Results results(r, q.find_all()); { // A newly-added row should not appear in the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 0); write([=]{ table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 0); } { // Updating a row to no longer match the query criteria should not affect the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 1); write([=]{ table->set_int(0, 0, 0); }); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 1); REQUIRE(snapshot.get(0).is_attached()); // Removing a row present in the snapshot from its table should result in the snapshot // returning a detached row accessor. write([=]{ table->remove(0); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); // Adding a new row that matches the query criteria shouldn't affect the state of the snapshot. write([=]{ table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); } } SECTION("snapshot of Results based on TableView from backlinks") { auto object = r->read_group().get_table("class_object"); auto linked_to = r->read_group().get_table("class_linked to object"); write([=]{ linked_to->add_empty_row(); }); TableView backlinks = linked_to->get_backlink_view(0, object.get(), 1); Results results(r, std::move(backlinks)); auto lv = object->get_linklist(1, object->add_empty_row()); { // A newly-added row should not appear in the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 0); write([=]{ lv->add(0); }); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 0); } { // Removing the link should not affect the snapshot. auto snapshot = results.snapshot(); REQUIRE(results.size() == 1); REQUIRE(snapshot.size() == 1); write([=]{ lv->remove(0); }); REQUIRE(results.size() == 0); REQUIRE(snapshot.size() == 1); REQUIRE(snapshot.get(0).is_attached()); // Removing a row present in the snapshot from its table should result in the snapshot // returning a detached row accessor. write([=]{ object->remove(0); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); // Adding a new link shouldn't affect the state of the snapshot. write([=]{ object->add_empty_row(); auto lv = object->get_linklist(1, object->add_empty_row()); lv->add(0); }); REQUIRE(snapshot.size() == 1); REQUIRE(!snapshot.get(0).is_attached()); } } SECTION("snapshot of Results with notification callback registered") { auto table = r->read_group().get_table("class_object"); Query q = table->column(0) > 0; Results results(r, q.find_all()); auto token = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE_FALSE(err); }); advance_and_notify(*r); SECTION("snapshot of lvalue") { auto snapshot = results.snapshot(); write([=] { table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(snapshot.size() == 0); } SECTION("snapshot of rvalue") { auto snapshot = std::move(results).snapshot(); write([=] { table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(snapshot.size() == 0); } } SECTION("adding notification callback to snapshot throws") { auto table = r->read_group().get_table("class_object"); Query q = table->column(0) > 0; Results results(r, q.find_all()); auto snapshot = results.snapshot(); CHECK_THROWS(snapshot.add_notification_callback([](CollectionChangeSet, std::exception_ptr) {})); } SECTION("accessors should return none for detached row") { auto table = r->read_group().get_table("class_object"); write([=] { table->add_empty_row(); }); Results results(r, *table); auto snapshot = results.snapshot(); write([=] {; table->clear(); }); REQUIRE_FALSE(snapshot.get(0).is_attached()); REQUIRE_FALSE(snapshot.first()->is_attached()); REQUIRE_FALSE(snapshot.last()->is_attached()); } } TEST_CASE("results: distinct") { const int N = 10; InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({ {"object", { {"num1", PropertyType::Int}, {"string", PropertyType::String}, {"num2", PropertyType::Int}, {"num3", PropertyType::Int} }}, }); auto table = r->read_group().get_table("class_object"); r->begin_transaction(); table->add_empty_row(N); for (int i = 0; i < N; ++i) { table->set_int(0, i, i % 3); table->set_string(1, i, util::format("Foo_%1", i % 3).c_str()); table->set_int(2, i, N - i); table->set_int(3, i, i % 2); } // table: // 0, Foo_0, 10, 0 // 1, Foo_1, 9, 1 // 2, Foo_2, 8, 0 // 0, Foo_0, 7, 1 // 1, Foo_1, 6, 0 // 2, Foo_2, 5, 1 // 0, Foo_0, 4, 0 // 1, Foo_1, 3, 1 // 2, Foo_2, 2, 0 // 0, Foo_0, 1, 1 r->commit_transaction(); Results results(r, table->where()); SECTION("Single integer property") { Results unique = results.distinct(SortDescriptor(results.get_tableview().get_parent(), {{0}})); // unique: // 0, Foo_0, 10 // 1, Foo_1, 9 // 2, Foo_2, 8 REQUIRE(unique.size() == 3); REQUIRE(unique.get(0).get_int(2) == 10); REQUIRE(unique.get(1).get_int(2) == 9); REQUIRE(unique.get(2).get_int(2) == 8); } SECTION("Single integer via apply_ordering") { DescriptorOrdering ordering; ordering.append_sort(SortDescriptor(results.get_tableview().get_parent(), {{0}})); ordering.append_distinct(DistinctDescriptor(results.get_tableview().get_parent(), {{0}})); Results unique = results.apply_ordering(std::move(ordering)); // unique: // 0, Foo_0, 10 // 1, Foo_1, 9 // 2, Foo_2, 8 REQUIRE(unique.size() == 3); REQUIRE(unique.get(0).get_int(2) == 10); REQUIRE(unique.get(1).get_int(2) == 9); REQUIRE(unique.get(2).get_int(2) == 8); } SECTION("Single string property") { Results unique = results.distinct(SortDescriptor(results.get_tableview().get_parent(), {{1}})); // unique: // 0, Foo_0, 10 // 1, Foo_1, 9 // 2, Foo_2, 8 REQUIRE(unique.size() == 3); REQUIRE(unique.get(0).get_int(2) == 10); REQUIRE(unique.get(1).get_int(2) == 9); REQUIRE(unique.get(2).get_int(2) == 8); } SECTION("Two integer properties combined") { Results unique = results.distinct(SortDescriptor(results.get_tableview().get_parent(), {{0}, {2}})); // unique is the same as the table REQUIRE(unique.size() == N); for (int i = 0; i < N; ++i) { REQUIRE(unique.get(i).get_string(1) == StringData(util::format("Foo_%1", i % 3).c_str())); } } SECTION("String and integer combined") { Results unique = results.distinct(SortDescriptor(results.get_tableview().get_parent(), {{2}, {1}})); // unique is the same as the table REQUIRE(unique.size() == N); for (int i = 0; i < N; ++i) { REQUIRE(unique.get(i).get_string(1) == StringData(util::format("Foo_%1", i % 3).c_str())); } } // This section and next section demonstrate that sort().distinct() != distinct().sort() SECTION("Order after sort and distinct") { Results reverse = results.sort(SortDescriptor(results.get_tableview().get_parent(), {{2}}, {true})); // reverse: // 0, Foo_0, 1 // ... // 0, Foo_0, 10 REQUIRE(reverse.first()->get_int(2) == 1); REQUIRE(reverse.last()->get_int(2) == 10); // distinct() will be applied to the table, after sorting Results unique = reverse.distinct(SortDescriptor(reverse.get_tableview().get_parent(), {{0}})); // unique: // 0, Foo_0, 1 // 2, Foo_2, 2 // 1, Foo_1, 3 REQUIRE(unique.size() == 3); REQUIRE(unique.get(0).get_int(2) == 1); REQUIRE(unique.get(1).get_int(2) == 2); REQUIRE(unique.get(2).get_int(2) == 3); } SECTION("Order after distinct and sort") { Results unique = results.distinct(SortDescriptor(results.get_tableview().get_parent(), {{0}})); // unique: // 0, Foo_0, 10 // 1, Foo_1, 9 // 2, Foo_2, 8 REQUIRE(unique.size() == 3); REQUIRE(unique.first()->get_int(2) == 10); REQUIRE(unique.last()->get_int(2) == 8); // sort() is only applied to unique Results reverse = unique.sort(SortDescriptor(unique.get_tableview().get_parent(), {{2}}, {true})); // reversed: // 2, Foo_2, 8 // 1, Foo_1, 9 // 0, Foo_0, 10 REQUIRE(reverse.size() == 3); REQUIRE(reverse.get(0).get_int(2) == 8); REQUIRE(reverse.get(1).get_int(2) == 9); REQUIRE(reverse.get(2).get_int(2) == 10); } SECTION("Chaining distinct") { Results first = results.distinct(SortDescriptor(results.get_tableview().get_parent(), {{0}})); REQUIRE(first.size() == 3); // distinct() will not discard the previous applied distinct() calls Results second = first.distinct(SortDescriptor(first.get_tableview().get_parent(), {{3}})); REQUIRE(second.size() == 2); } SECTION("Chaining sort") { using cols_0_3 = std::pair; Results first = results.sort(SortDescriptor(results.get_tableview().get_parent(), {{0}})); Results second = first.sort(SortDescriptor(first.get_tableview().get_parent(), {{3}})); REQUIRE(second.size() == 10); // results are ordered first by the last sorted column // if any duplicates exist in that column, they are resolved by sorting the // previously sorted column. Eg. sort(a).sort(b) == sort(b, a) std::vector results = {{0, 0}, {0, 0}, {1, 0}, {2, 0}, {2, 0}, {0, 1}, {0, 1}, {1, 1}, {1, 1}, {2, 1}}; for (size_t i = 0; i < results.size(); ++i) { REQUIRE(second.get(i).get_int(0) == results[i].first); REQUIRE(second.get(i).get_int(3) == results[i].second); } } SECTION("Distinct is carried over to new queries") { Results unique = results.distinct(SortDescriptor(results.get_tableview().get_parent(), {{0}})); // unique: // 0, Foo_0, 10 // 1, Foo_1, 9 // 2, Foo_2, 8 REQUIRE(unique.size() == 3); Results filtered = unique.filter(Query(table->where().less(0, 2))); // filtered: // 0, Foo_0, 10 // 1, Foo_1, 9 REQUIRE(filtered.size() == 2); REQUIRE(filtered.get(0).get_int(2) == 10); REQUIRE(filtered.get(1).get_int(2) == 9); } SECTION("Distinct will not forget previous query") { Results filtered = results.filter(Query(table->where().greater(2, 5))); // filtered: // 0, Foo_0, 10 // 1, Foo_1, 9 // 2, Foo_2, 8 // 0, Foo_0, 7 // 1, Foo_1, 6 REQUIRE(filtered.size() == 5); Results unique = filtered.distinct(SortDescriptor(filtered.get_tableview().get_parent(), {{0}})); // unique: // 0, Foo_0, 10 // 1, Foo_1, 9 // 2, Foo_2, 8 REQUIRE(unique.size() == 3); REQUIRE(unique.get(0).get_int(2) == 10); REQUIRE(unique.get(1).get_int(2) == 9); REQUIRE(unique.get(2).get_int(2) == 8); Results further_filtered = unique.filter(Query(table->where().equal(2, 9))); // further_filtered: // 1, Foo_1, 9 REQUIRE(further_filtered.size() == 1); REQUIRE(further_filtered.get(0).get_int(2) == 9); } } TEST_CASE("results: sort") { InMemoryTestFile config; config.cache = false; config.schema = Schema{ {"object", { {"value", PropertyType::Int}, {"bool", PropertyType::Bool}, {"data prop", PropertyType::Data}, {"link", PropertyType::Object|PropertyType::Nullable, "object 2"}, {"array", PropertyType::Object|PropertyType::Array, "object 2"}, }}, {"object 2", { {"value", PropertyType::Int}, {"link", PropertyType::Object|PropertyType::Nullable, "object"}, }}, }; auto realm = Realm::get_shared_realm(config); auto table = realm->read_group().get_table("class_object"); auto table2 = realm->read_group().get_table("class_object 2"); Results r(realm, *table); SECTION("invalid keypaths") { SECTION("empty property name") { REQUIRE_THROWS_WITH(r.sort({{"", true}}), "Cannot sort on key path '': missing property name."); REQUIRE_THROWS_WITH(r.sort({{".", true}}), "Cannot sort on key path '.': missing property name."); REQUIRE_THROWS_WITH(r.sort({{"link.", true}}), "Cannot sort on key path 'link.': missing property name."); REQUIRE_THROWS_WITH(r.sort({{".value", true}}), "Cannot sort on key path '.value': missing property name."); REQUIRE_THROWS_WITH(r.sort({{"link..value", true}}), "Cannot sort on key path 'link..value': missing property name."); } SECTION("bad property name") { REQUIRE_THROWS_WITH(r.sort({{"not a property", true}}), "Cannot sort on key path 'not a property': property 'object.not a property' does not exist."); REQUIRE_THROWS_WITH(r.sort({{"link.not a property", true}}), "Cannot sort on key path 'link.not a property': property 'object 2.not a property' does not exist."); } SECTION("subscript primitive") { REQUIRE_THROWS_WITH(r.sort({{"value.link", true}}), "Cannot sort on key path 'value.link': property 'object.value' of type 'int' may only be the final property in the key path."); } SECTION("end in link") { REQUIRE_THROWS_WITH(r.sort({{"link", true}}), "Cannot sort on key path 'link': property 'object.link' of type 'object' cannot be the final property in the key path."); REQUIRE_THROWS_WITH(r.sort({{"link.link", true}}), "Cannot sort on key path 'link.link': property 'object 2.link' of type 'object' cannot be the final property in the key path."); } SECTION("sort involving bad property types") { REQUIRE_THROWS_WITH(r.sort({{"array", true}}), "Cannot sort on key path 'array': property 'object.array' is of unsupported type 'array'."); REQUIRE_THROWS_WITH(r.sort({{"array.value", true}}), "Cannot sort on key path 'array.value': property 'object.array' is of unsupported type 'array'."); REQUIRE_THROWS_WITH(r.sort({{"link.link.array.value", true}}), "Cannot sort on key path 'link.link.array.value': property 'object.array' is of unsupported type 'array'."); REQUIRE_THROWS_WITH(r.sort({{"data prop", true}}), "Cannot sort on key path 'data prop': property 'object.data prop' is of unsupported type 'data'."); } } realm->begin_transaction(); table->add_empty_row(4); table2->add_empty_row(4); for (int i = 0; i < 4; ++i) { table->set_int(0, i, (i + 2) % 4); table->set_bool(1, i, i % 2); table->set_link(3, i, 3 - i); table2->set_int(0, i, (i + 1) % 4); table2->set_link(1, i, i); } realm->commit_transaction(); /* | index | value | bool | link.value | link.link.value | |-------|-------|------|------------|-----------------| | 0 | 2 | 0 | 0 | 1 | | 1 | 3 | 1 | 3 | 0 | | 2 | 0 | 0 | 2 | 3 | | 3 | 1 | 1 | 1 | 2 | */ #define REQUIRE_ORDER(sort, ...) do { \ std::vector expected = {__VA_ARGS__}; \ auto results = sort; \ REQUIRE(results.size() == expected.size()); \ for (size_t i = 0; i < expected.size(); ++i) \ REQUIRE(results.get(i).get_index() == expected[i]); \ } while (0) SECTION("sort on single property") { REQUIRE_ORDER((r.sort({{"value", true}})), 2, 3, 0, 1); REQUIRE_ORDER((r.sort({{"value", false}})), 1, 0, 3, 2); } SECTION("sort on two properties") { REQUIRE_ORDER((r.sort({{"bool", true}, {"value", true}})), 2, 0, 3, 1); REQUIRE_ORDER((r.sort({{"bool", false}, {"value", true}})), 3, 1, 2, 0); REQUIRE_ORDER((r.sort({{"bool", true}, {"value", false}})), 0, 2, 1, 3); REQUIRE_ORDER((r.sort({{"bool", false}, {"value", false}})), 1, 3, 0, 2); } SECTION("sort over link") { REQUIRE_ORDER((r.sort({{"link.value", true}})), 0, 3, 2, 1); REQUIRE_ORDER((r.sort({{"link.value", false}})), 1, 2, 3, 0); } SECTION("sort over two links") { REQUIRE_ORDER((r.sort({{"link.link.value", true}})), 1, 0, 3, 2); REQUIRE_ORDER((r.sort({{"link.link.value", false}})), 2, 3, 0, 1); } } struct ResultsFromTable { static Results call(std::shared_ptr r, Table* table) { return Results(std::move(r), *table); } }; struct ResultsFromQuery { static Results call(std::shared_ptr r, Table* table) { return Results(std::move(r), table->where()); } }; struct ResultsFromTableView { static Results call(std::shared_ptr r, Table* table) { return Results(std::move(r), table->where().find_all()); } }; struct ResultsFromLinkView { static Results call(std::shared_ptr r, Table* table) { r->begin_transaction(); auto link_table = r->read_group().get_table("class_linking_object"); link_table->add_empty_row(1); auto link_view = link_table->get_linklist(0, 0); for (size_t i = 0; i < table->size(); ++i) link_view->add(i); r->commit_transaction(); return Results(r, link_view); } }; TEMPLATE_TEST_CASE("results: aggregate", "[query][aggregate]", ResultsFromTable, ResultsFromQuery, ResultsFromTableView, ResultsFromLinkView) { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); r->update_schema({ {"object", { {"int", PropertyType::Int|PropertyType::Nullable}, {"float", PropertyType::Float|PropertyType::Nullable}, {"double", PropertyType::Double|PropertyType::Nullable}, {"date", PropertyType::Date|PropertyType::Nullable}, }}, {"linking_object", { {"link", PropertyType::Array|PropertyType::Object, "object"} }}, }); auto table = r->read_group().get_table("class_object"); SECTION("one row with null values") { r->begin_transaction(); table->add_empty_row(3); table->set_int(0, 1, 0); table->set_float(1, 1, 0.f); table->set_double(2, 1, 0.0); table->set_timestamp(3, 1, Timestamp(0, 0)); table->set_int(0, 2, 2); table->set_float(1, 2, 2.f); table->set_double(2, 2, 2.0); table->set_timestamp(3, 2, Timestamp(2, 0)); // table: // null, null, null, null, // 0, 0, 0, (0, 0) // 2, 2, 2, (2, 0) r->commit_transaction(); Results results = TestType::call(r, table.get()); SECTION("max") { REQUIRE(results.max(0)->get_int() == 2); REQUIRE(results.max(1)->get_float() == 2.f); REQUIRE(results.max(2)->get_double() == 2.0); REQUIRE(results.max(3)->get_timestamp() == Timestamp(2, 0)); } SECTION("min") { REQUIRE(results.min(0)->get_int() == 0); REQUIRE(results.min(1)->get_float() == 0.f); REQUIRE(results.min(2)->get_double() == 0.0); REQUIRE(results.min(3)->get_timestamp() == Timestamp(0, 0)); } SECTION("average") { REQUIRE(results.average(0) == 1.0); REQUIRE(results.average(1) == 1.0); REQUIRE(results.average(2) == 1.0); REQUIRE_THROWS_AS(results.average(3), Results::UnsupportedColumnTypeException); } SECTION("sum") { REQUIRE(results.sum(0)->get_int() == 2); REQUIRE(results.sum(1)->get_double() == 2.0); REQUIRE(results.sum(2)->get_double() == 2.0); REQUIRE_THROWS_AS(results.sum(3), Results::UnsupportedColumnTypeException); } } SECTION("rows with all null values") { r->begin_transaction(); table->add_empty_row(3); // table: // null, null, null, null, null // null, null, null, null, null // null, null, null, null, null r->commit_transaction(); Results results = TestType::call(r, table.get()); SECTION("max") { REQUIRE(!results.max(0)); REQUIRE(!results.max(1)); REQUIRE(!results.max(2)); REQUIRE(!results.max(3)); } SECTION("min") { REQUIRE(!results.min(0)); REQUIRE(!results.min(1)); REQUIRE(!results.min(2)); REQUIRE(!results.min(3)); } SECTION("average") { REQUIRE(!results.average(0)); REQUIRE(!results.average(1)); REQUIRE(!results.average(2)); REQUIRE_THROWS_AS(results.average(3), Results::UnsupportedColumnTypeException); } SECTION("sum") { REQUIRE(results.sum(0)->get_int() == 0); REQUIRE(results.sum(1)->get_double() == 0.0); REQUIRE(results.sum(2)->get_double() == 0.0); REQUIRE_THROWS_AS(results.sum(3), Results::UnsupportedColumnTypeException); } } SECTION("empty") { Results results = TestType::call(r, table.get()); SECTION("max") { REQUIRE(!results.max(0)); REQUIRE(!results.max(1)); REQUIRE(!results.max(2)); REQUIRE(!results.max(3)); } SECTION("min") { REQUIRE(!results.min(0)); REQUIRE(!results.min(1)); REQUIRE(!results.min(2)); REQUIRE(!results.min(3)); } SECTION("average") { REQUIRE(!results.average(0)); REQUIRE(!results.average(1)); REQUIRE(!results.average(2)); REQUIRE_THROWS_AS(results.average(3), Results::UnsupportedColumnTypeException); } SECTION("sum") { REQUIRE(results.sum(0)->get_int() == 0); REQUIRE(results.sum(1)->get_double() == 0.0); REQUIRE(results.sum(2)->get_double() == 0.0); REQUIRE_THROWS_AS(results.sum(3), Results::UnsupportedColumnTypeException); } } } TEST_CASE("results: set property value on all objects", "[batch_updates]") { InMemoryTestFile config; config.automatic_change_notifications = false; config.cache = false; config.schema = Schema{ {"AllTypes", { {"pk", PropertyType::Int, Property::IsPrimary{true}}, {"bool", PropertyType::Bool}, {"int", PropertyType::Int}, {"float", PropertyType::Float}, {"double", PropertyType::Double}, {"string", PropertyType::String}, {"data", PropertyType::Data}, {"date", PropertyType::Date}, {"object", PropertyType::Object|PropertyType::Nullable, "AllTypes"}, {"list", PropertyType::Array|PropertyType::Object, "AllTypes"}, {"bool array", PropertyType::Array|PropertyType::Bool}, {"int array", PropertyType::Array|PropertyType::Int}, {"float array", PropertyType::Array|PropertyType::Float}, {"double array", PropertyType::Array|PropertyType::Double}, {"string array", PropertyType::Array|PropertyType::String}, {"data array", PropertyType::Array|PropertyType::Data}, {"date array", PropertyType::Array|PropertyType::Date}, {"object array", PropertyType::Array|PropertyType::Object, "AllTypes"}, }, { {"parents", PropertyType::LinkingObjects|PropertyType::Array, "AllTypes", "object"}, }} }; config.schema_version = 0; auto realm = Realm::get_shared_realm(config); auto table = realm->read_group().get_table("class_AllTypes"); realm->begin_transaction(); table->add_empty_row(2); realm->commit_transaction(); Results r(realm, *table); TestContext ctx(realm); SECTION("non-existing property name") { realm->begin_transaction(); REQUIRE_THROWS_AS(r.set_property_value(ctx, "i dont exist", util::Any(false)), Results::InvalidPropertyException); realm->cancel_transaction(); } SECTION("readonly property") { realm->begin_transaction(); REQUIRE_THROWS_AS(r.set_property_value(ctx, "parents", util::Any(false)), ReadOnlyPropertyException); realm->cancel_transaction(); } SECTION("primarykey property") { realm->begin_transaction(); REQUIRE_THROWS_AS(r.set_property_value(ctx, "pk", util::Any(1)), std::logic_error); realm->cancel_transaction(); } SECTION("set property values removes object from Results") { realm->begin_transaction(); Results results(realm, table->where().equal(2,0)); CHECK(results.size() == 2); r.set_property_value(ctx, "int", util::Any(INT64_C(42))); CHECK(results.size() == 0); realm->cancel_transaction(); } SECTION("set property value") { realm->begin_transaction(); r.set_property_value(ctx, "bool", util::Any(true)); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_bool(1) == true); } r.set_property_value(ctx, "int", util::Any(INT64_C(42))); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_int(2) == 42); } r.set_property_value(ctx, "float", util::Any(1.23f)); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_float(3) == 1.23f); } r.set_property_value(ctx, "double", util::Any(1.234)); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_double(4) == 1.234); } r.set_property_value(ctx, "string", util::Any(std::string("abc"))); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_string(5) == "abc"); } r.set_property_value(ctx, "data", util::Any(std::string("abc"))); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_binary(6) == BinaryData("abc", 3)); } util::Any timestamp = Timestamp(1, 2); r.set_property_value(ctx, "date", timestamp); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_timestamp(7) == any_cast(timestamp)); } size_t object_ndx = table->add_empty_row(); Object linked_obj(realm, "AllTypes", object_ndx); r.set_property_value(ctx, "object", util::Any(linked_obj)); for (size_t i = 0; i < r.size(); i++) { CHECK(r.get(i).get_link(8) == object_ndx); } size_t list_object_ndx = table->add_empty_row(); Object list_object(realm, "AllTypes", list_object_ndx); r.set_property_value(ctx, "list", util::Any(AnyVector{list_object, list_object})); for (size_t i = 0; i < r.size(); i++) { auto list = r.get(i).get_linklist(9); CHECK(list->size() == 2); CHECK(list->get(0).get_index() == list_object_ndx); CHECK(list->get(1).get_index() == list_object_ndx); } auto check_array = [&](size_t col, auto... values) { size_t rows = r.size(); for (size_t i = 0; i < rows; ++i) { RowExpr row = r.get(i); auto table = row.get_subtable(col); size_t j = 0; for (auto& value : {values...}) { CAPTURE(j); REQUIRE(j < row.get_subtable_size(col)); REQUIRE(value == table->get::type>(0, j)); ++j; } } }; r.set_property_value(ctx, "bool array", util::Any(AnyVec{true, false})); check_array(10, true, false); r.set_property_value(ctx, "int array", util::Any(AnyVec{INT64_C(5), INT64_C(6)})); check_array(11, INT64_C(5), INT64_C(6)); r.set_property_value(ctx, "float array", util::Any(AnyVec{1.1f, 2.2f})); check_array(12, 1.1f, 2.2f); r.set_property_value(ctx, "double array", util::Any(AnyVec{3.3, 4.4})); check_array(13, 3.3, 4.4); r.set_property_value(ctx, "string array", util::Any(AnyVec{"a"s, "b"s, "c"s})); check_array(14, StringData("a"), StringData("b"), StringData("c")); r.set_property_value(ctx, "data array", util::Any(AnyVec{"d"s, "e"s, "f"s})); check_array(15, BinaryData("d",1), BinaryData("e",1), BinaryData("f",1)); r.set_property_value(ctx, "date array", util::Any(AnyVec{Timestamp(10,20), Timestamp(20,30), Timestamp(30,40)})); check_array(16, Timestamp(10,20), Timestamp(20,30), Timestamp(30,40)); } } TEST_CASE("results: limit", "[limit]") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; config.schema = Schema{ {"object", { {"value", PropertyType::Int}, }}, }; auto realm = Realm::get_shared_realm(config); auto table = realm->read_group().get_table("class_object"); realm->begin_transaction(); table->add_empty_row(8); for (int i = 0; i < 8; ++i) { table->set_int(0, i, (i + 2) % 4); } realm->commit_transaction(); Results r(realm, *table); SECTION("unsorted") { REQUIRE(r.limit(0).size() == 0); REQUIRE_ORDER(r.limit(1), 0); REQUIRE_ORDER(r.limit(2), 0, 1); REQUIRE_ORDER(r.limit(8), 0, 1, 2, 3, 4, 5, 6, 7); REQUIRE_ORDER(r.limit(100), 0, 1, 2, 3, 4, 5, 6, 7); } SECTION("sorted") { auto sorted = r.sort({{"value", true}}); REQUIRE(sorted.limit(0).size() == 0); REQUIRE_ORDER(sorted.limit(1), 2); REQUIRE_ORDER(sorted.limit(2), 2, 6); REQUIRE_ORDER(sorted.limit(8), 2, 6, 3, 7, 0, 4, 1, 5); REQUIRE_ORDER(sorted.limit(100), 2, 6, 3, 7, 0, 4, 1, 5); } SECTION("sort after limit") { REQUIRE(r.limit(0).sort({{"value", true}}).size() == 0); REQUIRE_ORDER(r.limit(1).sort({{"value", true}}), 0); REQUIRE_ORDER(r.limit(3).sort({{"value", true}}), 2, 0, 1); REQUIRE_ORDER(r.limit(8).sort({{"value", true}}), 2, 6, 3, 7, 0, 4, 1, 5); REQUIRE_ORDER(r.limit(100).sort({{"value", true}}), 2, 6, 3, 7, 0, 4, 1, 5); } SECTION("distinct") { auto sorted = r.distinct({"value"}); REQUIRE(sorted.limit(0).size() == 0); REQUIRE_ORDER(sorted.limit(1), 0); REQUIRE_ORDER(sorted.limit(2), 0, 1); REQUIRE_ORDER(sorted.limit(8), 0, 1, 2, 3); sorted = r.sort({{"value", true}}).distinct({"value"}); REQUIRE(sorted.limit(0).size() == 0); REQUIRE_ORDER(sorted.limit(1), 2); REQUIRE_ORDER(sorted.limit(2), 2, 3); REQUIRE_ORDER(sorted.limit(8), 2, 3, 0, 1); } SECTION("notifications on results using all descriptor types") { r = r.distinct({"value"}).sort({{"value", false}}).limit(2); int notification_calls = 0; auto token = r.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); if (notification_calls == 0) { REQUIRE(c.empty()); REQUIRE(r.size() == 2); REQUIRE(r.get(0).get_int(0) == 3); REQUIRE(r.get(1).get_int(0) == 2); } else if (notification_calls == 1) { REQUIRE(!c.empty()); REQUIRE_INDICES(c.insertions, 0); REQUIRE_INDICES(c.deletions, 1); REQUIRE(c.moves.size() == 0); REQUIRE(c.modifications.count() == 0); REQUIRE(r.size() == 2); REQUIRE(r.get(0).get_int(0) == 5); REQUIRE(r.get(1).get_int(0) == 3); } ++notification_calls; }); advance_and_notify(*realm); REQUIRE(notification_calls == 1); realm->begin_transaction(); table->add_empty_row(1); table->set_int(0, 8, 5); realm->commit_transaction(); advance_and_notify(*realm); REQUIRE(notification_calls == 2); } SECTION("notifications on only limited results") { r = r.limit(2); int notification_calls = 0; auto token = r.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); if (notification_calls == 0) { REQUIRE(c.empty()); REQUIRE(r.size() == 2); } else if (notification_calls == 1) { REQUIRE(!c.empty()); REQUIRE(c.insertions.count() == 0); REQUIRE(c.deletions.count() == 0); REQUIRE(c.modifications.count() == 1); REQUIRE_INDICES(c.modifications, 1); REQUIRE(r.size() == 2); } ++notification_calls; }); advance_and_notify(*realm); REQUIRE(notification_calls == 1); realm->begin_transaction(); table->set_int(0, 1, 5); realm->commit_transaction(); advance_and_notify(*realm); REQUIRE(notification_calls == 2); } SECTION("does not support further filtering") { auto limited = r.limit(0); REQUIRE_THROWS_AS(limited.filter(table->where()), Results::UnimplementedOperationException); } }