You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3117 lines
106 KiB
3117 lines
106 KiB
////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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 "catch.hpp"
|
|
|
|
#include "util/event_loop.hpp"
|
|
#include "util/index_helpers.hpp"
|
|
#include "util/templated_test_case.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 <realm/group_shared.hpp>
|
|
#include <realm/link_view.hpp>
|
|
#include <realm/query_engine.hpp>
|
|
#include <realm/query_expression.hpp>
|
|
|
|
#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<std::string, util::Any>;
|
|
using AnyVec = std::vector<util::Any>;
|
|
}
|
|
|
|
struct TestContext : CppContext {
|
|
std::map<std::string, AnyDict> 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<ObserverState> const&, std::vector<void*> 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<void(void)> on_before = []{};
|
|
std::function<void(void)> 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<Results>(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<Int>(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<Int>(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<Int>(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<Int>(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<size_t, size_t>;
|
|
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<cols_0_3> 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<size_t> 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<Realm> r, Table* table) {
|
|
return Results(std::move(r), *table);
|
|
}
|
|
};
|
|
struct ResultsFromQuery {
|
|
static Results call(std::shared_ptr<Realm> r, Table* table) {
|
|
return Results(std::move(r), table->where());
|
|
}
|
|
};
|
|
struct ResultsFromTableView {
|
|
static Results call(std::shared_ptr<Realm> r, Table* table) {
|
|
return Results(std::move(r), table->where().find_all());
|
|
}
|
|
};
|
|
struct ResultsFromLinkView {
|
|
static Results call(std::shared_ptr<Realm> 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", 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<util::Any>(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>(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<typename std::decay<decltype(value)>::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);
|
|
}
|
|
}
|
|
|