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.
995 lines
39 KiB
995 lines
39 KiB
////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Copyright 2017 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/test_file.hpp"
|
|
|
|
#include "feature_checks.hpp"
|
|
#include "collection_notifications.hpp"
|
|
#include "object_accessor.hpp"
|
|
#include "property.hpp"
|
|
#include "schema.hpp"
|
|
|
|
#include "impl/realm_coordinator.hpp"
|
|
#include "impl/object_accessor_impl.hpp"
|
|
|
|
#include <realm/group_shared.hpp>
|
|
#include <realm/util/any.hpp>
|
|
|
|
#include <cstdint>
|
|
|
|
using namespace realm;
|
|
|
|
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)
|
|
{ }
|
|
|
|
util::Optional<util::Any>
|
|
default_value_for_property(ObjectSchema const& object, Property const& prop)
|
|
{
|
|
auto obj_it = defaults.find(object.name);
|
|
if (obj_it == defaults.end())
|
|
return util::none;
|
|
auto prop_it = obj_it->second.find(prop.name);
|
|
if (prop_it == obj_it->second.end())
|
|
return util::none;
|
|
return prop_it->second;
|
|
}
|
|
|
|
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("object") {
|
|
using namespace std::string_literals;
|
|
_impl::RealmCoordinator::assert_no_open_realms();
|
|
|
|
InMemoryTestFile config;
|
|
config.automatic_change_notifications = false;
|
|
config.cache = false;
|
|
config.schema = Schema{
|
|
{"table", {
|
|
{"value 1", PropertyType::Int},
|
|
{"value 2", PropertyType::Int},
|
|
}},
|
|
{"all types", {
|
|
{"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, "link target"},
|
|
|
|
{"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, "array target"},
|
|
}},
|
|
{"all optional types", {
|
|
{"pk", PropertyType::Int|PropertyType::Nullable, Property::IsPrimary{true}},
|
|
{"bool", PropertyType::Bool|PropertyType::Nullable},
|
|
{"int", PropertyType::Int|PropertyType::Nullable},
|
|
{"float", PropertyType::Float|PropertyType::Nullable},
|
|
{"double", PropertyType::Double|PropertyType::Nullable},
|
|
{"string", PropertyType::String|PropertyType::Nullable},
|
|
{"data", PropertyType::Data|PropertyType::Nullable},
|
|
{"date", PropertyType::Date|PropertyType::Nullable},
|
|
|
|
{"bool array", PropertyType::Array|PropertyType::Bool|PropertyType::Nullable},
|
|
{"int array", PropertyType::Array|PropertyType::Int|PropertyType::Nullable},
|
|
{"float array", PropertyType::Array|PropertyType::Float|PropertyType::Nullable},
|
|
{"double array", PropertyType::Array|PropertyType::Double|PropertyType::Nullable},
|
|
{"string array", PropertyType::Array|PropertyType::String|PropertyType::Nullable},
|
|
{"data array", PropertyType::Array|PropertyType::Data|PropertyType::Nullable},
|
|
{"date array", PropertyType::Array|PropertyType::Date|PropertyType::Nullable},
|
|
}},
|
|
{"link target", {
|
|
{"value", PropertyType::Int},
|
|
}, {
|
|
{"origin", PropertyType::LinkingObjects|PropertyType::Array, "all types", "object"},
|
|
}},
|
|
{"array target", {
|
|
{"value", PropertyType::Int},
|
|
}},
|
|
{"pk after list", {
|
|
{"array 1", PropertyType::Array|PropertyType::Object, "array target"},
|
|
{"int 1", PropertyType::Int},
|
|
{"pk", PropertyType::Int, Property::IsPrimary{true}},
|
|
{"int 2", PropertyType::Int},
|
|
{"array 2", PropertyType::Array|PropertyType::Object, "array target"},
|
|
}},
|
|
{"nullable int pk", {
|
|
{"pk", PropertyType::Int|PropertyType::Nullable, Property::IsPrimary{true}},
|
|
}},
|
|
{"nullable string pk", {
|
|
{"pk", PropertyType::String|PropertyType::Nullable, Property::IsPrimary{true}},
|
|
}},
|
|
{"person", {
|
|
{"name", PropertyType::String, Property::IsPrimary{true}},
|
|
{"age", PropertyType::Int},
|
|
{"scores", PropertyType::Array|PropertyType::Int},
|
|
{"assistant", PropertyType::Object|PropertyType::Nullable, "person"},
|
|
{"team", PropertyType::Array|PropertyType::Object, "person"},
|
|
}},
|
|
};
|
|
config.schema_version = 0;
|
|
auto r = Realm::get_shared_realm(config);
|
|
auto& coordinator = *_impl::RealmCoordinator::get_existing_coordinator(config.path);
|
|
|
|
SECTION("add_notification_callback()") {
|
|
auto table = r->read_group().get_table("class_table");
|
|
r->begin_transaction();
|
|
|
|
table->add_empty_row(10);
|
|
for (int i = 0; i < 10; ++i)
|
|
table->set_int(0, i, i);
|
|
r->commit_transaction();
|
|
|
|
auto r2 = coordinator.get_realm();
|
|
|
|
CollectionChangeSet change;
|
|
Row row = table->get(0);
|
|
Object object(r, *r->schema().find("table"), row);
|
|
|
|
auto write = [&](auto&& f) {
|
|
r->begin_transaction();
|
|
f();
|
|
r->commit_transaction();
|
|
|
|
advance_and_notify(*r);
|
|
};
|
|
|
|
auto require_change = [&] {
|
|
auto token = object.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) {
|
|
change = c;
|
|
});
|
|
advance_and_notify(*r);
|
|
return token;
|
|
};
|
|
|
|
auto require_no_change = [&] {
|
|
bool first = true;
|
|
auto token = object.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) {
|
|
REQUIRE(first);
|
|
first = false;
|
|
});
|
|
advance_and_notify(*r);
|
|
return token;
|
|
};
|
|
|
|
SECTION("deleting the object sends a change notification") {
|
|
auto token = require_change();
|
|
write([&] { row.move_last_over(); });
|
|
REQUIRE_INDICES(change.deletions, 0);
|
|
}
|
|
|
|
SECTION("modifying the object sends a change notification") {
|
|
auto token = require_change();
|
|
|
|
write([&] { row.set_int(0, 10); });
|
|
REQUIRE_INDICES(change.modifications, 0);
|
|
REQUIRE(change.columns.size() == 1);
|
|
REQUIRE_INDICES(change.columns[0], 0);
|
|
|
|
write([&] { row.set_int(1, 10); });
|
|
REQUIRE_INDICES(change.modifications, 0);
|
|
REQUIRE(change.columns.size() == 2);
|
|
REQUIRE(change.columns[0].empty());
|
|
REQUIRE_INDICES(change.columns[1], 0);
|
|
}
|
|
|
|
SECTION("modifying a different object") {
|
|
auto token = require_no_change();
|
|
write([&] { table->get(1).set_int(0, 10); });
|
|
}
|
|
|
|
SECTION("moving the object") {
|
|
auto token = require_no_change();
|
|
write([&] { table->swap_rows(0, 5); });
|
|
}
|
|
|
|
SECTION("subsuming the object") {
|
|
auto token = require_change();
|
|
write([&] {
|
|
table->insert_empty_row(0);
|
|
table->merge_rows(row.get_index(), 0);
|
|
row.set_int(0, 10);
|
|
});
|
|
REQUIRE(change.columns.size() == 1);
|
|
REQUIRE_INDICES(change.columns[0], 0);
|
|
}
|
|
|
|
SECTION("multiple write transactions") {
|
|
auto token = require_change();
|
|
|
|
auto r2row = r2->read_group().get_table("class_table")->get(0);
|
|
r2->begin_transaction();
|
|
r2row.set_int(0, 1);
|
|
r2->commit_transaction();
|
|
r2->begin_transaction();
|
|
r2row.set_int(1, 2);
|
|
r2->commit_transaction();
|
|
|
|
advance_and_notify(*r);
|
|
REQUIRE(change.columns.size() == 2);
|
|
REQUIRE_INDICES(change.columns[0], 0);
|
|
REQUIRE_INDICES(change.columns[1], 0);
|
|
}
|
|
|
|
SECTION("skipping a notification") {
|
|
auto token = require_no_change();
|
|
write([&] {
|
|
row.set_int(0, 1);
|
|
token.suppress_next();
|
|
});
|
|
}
|
|
|
|
SECTION("skipping only effects the current transaction even if no notification would occur anyway") {
|
|
auto token = require_change();
|
|
|
|
// would not produce a notification even if it wasn't skipped because no changes were made
|
|
write([&] {
|
|
token.suppress_next();
|
|
});
|
|
REQUIRE(change.empty());
|
|
|
|
// should now produce a notification
|
|
write([&] {
|
|
row.set_int(0, 1);
|
|
});
|
|
REQUIRE_INDICES(change.modifications, 0);
|
|
}
|
|
|
|
SECTION("add notification callback, remove it, then add another notification callback") {
|
|
{
|
|
auto token = object.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) {
|
|
FAIL("This should never happen");
|
|
});
|
|
}
|
|
auto token = require_change();
|
|
write([&] { row.move_last_over(); });
|
|
REQUIRE_INDICES(change.deletions, 0);
|
|
}
|
|
|
|
SECTION("observing deleted object throws") {
|
|
write([&] {
|
|
row.move_last_over();
|
|
});
|
|
REQUIRE_THROWS(require_change());
|
|
}
|
|
}
|
|
|
|
TestContext d(r);
|
|
auto create = [&](util::Any&& value, bool update, bool update_only_diff = false) {
|
|
r->begin_transaction();
|
|
auto obj = Object::create(d, r, *r->schema().find("all types"), value, update, update_only_diff);
|
|
r->commit_transaction();
|
|
return obj;
|
|
};
|
|
auto create_sub = [&](util::Any&& value, bool update, bool update_only_diff = false) {
|
|
r->begin_transaction();
|
|
auto obj = Object::create(d, r, *r->schema().find("link target"), value, update, update_only_diff);
|
|
r->commit_transaction();
|
|
return obj;
|
|
};
|
|
auto create_company = [&](util::Any&& value, bool update, bool update_only_diff = false) {
|
|
r->begin_transaction();
|
|
auto obj = Object::create(d, r, *r->schema().find("person"), value, update, update_only_diff);
|
|
r->commit_transaction();
|
|
return obj;
|
|
};
|
|
|
|
SECTION("create object") {
|
|
auto obj = create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(10)}}},
|
|
|
|
{"bool array", AnyVec{true, false}},
|
|
{"int array", AnyVec{INT64_C(5), INT64_C(6)}},
|
|
{"float array", AnyVec{1.1f, 2.2f}},
|
|
{"double array", AnyVec{3.3, 4.4}},
|
|
{"string array", AnyVec{"a"s, "b"s, "c"s}},
|
|
{"data array", AnyVec{"d"s, "e"s, "f"s}},
|
|
{"date array", AnyVec{}},
|
|
{"object array", AnyVec{AnyDict{{"value", INT64_C(20)}}}},
|
|
}, false);
|
|
|
|
auto row = obj.row();
|
|
REQUIRE(row.get_int(0) == 1);
|
|
REQUIRE(row.get_bool(1) == true);
|
|
REQUIRE(row.get_int(2) == 5);
|
|
REQUIRE(row.get_float(3) == 2.2f);
|
|
REQUIRE(row.get_double(4) == 3.3);
|
|
REQUIRE(row.get_string(5) == "hello");
|
|
REQUIRE(row.get_binary(6) == BinaryData("olleh", 5));
|
|
REQUIRE(row.get_timestamp(7) == Timestamp(10, 20));
|
|
REQUIRE(row.get_link(8) == 0);
|
|
|
|
auto link_target = r->read_group().get_table("class_link target")->get(0);
|
|
REQUIRE(link_target.get_int(0) == 10);
|
|
|
|
auto check_array = [&](size_t col, auto... values) {
|
|
auto table = row.get_subtable(col);
|
|
size_t i = 0;
|
|
for (auto& value : {values...}) {
|
|
CAPTURE(i);
|
|
REQUIRE(i < row.get_subtable_size(col));
|
|
REQUIRE(value == table->get<typename std::decay<decltype(value)>::type>(0, i));
|
|
++i;
|
|
}
|
|
};
|
|
check_array(9, true, false);
|
|
check_array(10, INT64_C(5), INT64_C(6));
|
|
check_array(11, 1.1f, 2.2f);
|
|
check_array(12, 3.3, 4.4);
|
|
check_array(13, StringData("a"), StringData("b"), StringData("c"));
|
|
check_array(14, BinaryData("d", 1), BinaryData("e", 1), BinaryData("f", 1));
|
|
|
|
auto list = row.get_linklist(16);
|
|
REQUIRE(list->size() == 1);
|
|
REQUIRE(list->get(0).get_int(0) == 20);
|
|
}
|
|
|
|
SECTION("create uses defaults for missing values") {
|
|
d.defaults["all types"] = {
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(10)}}},
|
|
|
|
{"bool array", AnyVec{true, false}},
|
|
{"int array", AnyVec{INT64_C(5), INT64_C(6)}},
|
|
{"float array", AnyVec{1.1f, 2.2f}},
|
|
{"double array", AnyVec{3.3, 4.4}},
|
|
{"string array", AnyVec{"a"s, "b"s, "c"s}},
|
|
{"data array", AnyVec{"d"s, "e"s, "f"s}},
|
|
{"date array", AnyVec{}},
|
|
{"object array", AnyVec{AnyDict{{"value", INT64_C(20)}}}},
|
|
};
|
|
|
|
auto obj = create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"float", 6.6f},
|
|
}, false);
|
|
|
|
auto row = obj.row();
|
|
REQUIRE(row.get_int(0) == 1);
|
|
REQUIRE(row.get_bool(1) == true);
|
|
REQUIRE(row.get_int(2) == 5);
|
|
REQUIRE(row.get_float(3) == 6.6f);
|
|
REQUIRE(row.get_double(4) == 3.3);
|
|
REQUIRE(row.get_string(5) == "hello");
|
|
REQUIRE(row.get_binary(6) == BinaryData("olleh", 5));
|
|
REQUIRE(row.get_timestamp(7) == Timestamp(10, 20));
|
|
|
|
REQUIRE(row.get_subtable(9)->size() == 2);
|
|
REQUIRE(row.get_subtable(10)->size() == 2);
|
|
REQUIRE(row.get_subtable(11)->size() == 2);
|
|
REQUIRE(row.get_subtable(12)->size() == 2);
|
|
REQUIRE(row.get_subtable(13)->size() == 3);
|
|
REQUIRE(row.get_subtable(14)->size() == 3);
|
|
REQUIRE(row.get_subtable(15)->size() == 0);
|
|
REQUIRE(row.get_linklist(16)->size() == 1);
|
|
}
|
|
|
|
SECTION("create can use defaults for primary key") {
|
|
d.defaults["all types"] = {
|
|
{"pk", INT64_C(10)},
|
|
};
|
|
auto obj = create(AnyDict{
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(10)}}},
|
|
{"array", AnyVector{AnyDict{{"value", INT64_C(20)}}}},
|
|
}, false);
|
|
|
|
auto row = obj.row();
|
|
REQUIRE(row.get_int(0) == 10);
|
|
}
|
|
|
|
SECTION("create does not complain about missing values for nullable fields") {
|
|
r->begin_transaction();
|
|
realm::Object obj;
|
|
REQUIRE_NOTHROW(obj = Object::create(d, r, *r->schema().find("all optional types"), util::Any(AnyDict{}), false));
|
|
r->commit_transaction();
|
|
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "pk").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "bool").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "int").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "float").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "double").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "string").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "data").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "date").has_value());
|
|
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "bool array")).size() == 0);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "int array")).size() == 0);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "float array")).size() == 0);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "double array")).size() == 0);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "string array")).size() == 0);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "data array")).size() == 0);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "date array")).size() == 0);
|
|
}
|
|
|
|
SECTION("create throws for missing values if there is no default") {
|
|
REQUIRE_THROWS(create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"float", 6.6f},
|
|
}, false));
|
|
}
|
|
|
|
SECTION("create always sets the PK first") {
|
|
AnyDict value{
|
|
{"array 1", AnyVector{AnyDict{{"value", INT64_C(1)}}}},
|
|
{"array 2", AnyVector{AnyDict{{"value", INT64_C(2)}}}},
|
|
{"int 1", INT64_C(0)},
|
|
{"int 2", INT64_C(0)},
|
|
{"pk", INT64_C(7)},
|
|
};
|
|
// Core will throw if the list is populated before the PK is set
|
|
r->begin_transaction();
|
|
REQUIRE_NOTHROW(Object::create(d, r, *r->schema().find("pk after list"), util::Any(value), false));
|
|
}
|
|
|
|
SECTION("create with update") {
|
|
CollectionChangeSet change;
|
|
bool callback_called;
|
|
Object obj = create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(10)}}},
|
|
|
|
{"bool array", AnyVec{true, false}},
|
|
{"int array", AnyVec{INT64_C(5), INT64_C(6)}},
|
|
{"float array", AnyVec{1.1f, 2.2f}},
|
|
{"double array", AnyVec{3.3, 4.4}},
|
|
{"string array", AnyVec{"a"s, "b"s, "c"s}},
|
|
{"data array", AnyVec{"d"s, "e"s, "f"s}},
|
|
{"date array", AnyVec{}},
|
|
{"object array", AnyVec{AnyDict{{"value", INT64_C(20)}}}},
|
|
}, false);
|
|
|
|
auto token = obj.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) {
|
|
change = c;
|
|
callback_called = true;
|
|
});
|
|
advance_and_notify(*r);
|
|
|
|
create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"int", INT64_C(6)},
|
|
{"string", "a"s},
|
|
}, true);
|
|
|
|
callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(callback_called);
|
|
REQUIRE_INDICES(change.modifications, 0);
|
|
|
|
auto row = obj.row();
|
|
REQUIRE(row.get_int(0) == 1);
|
|
REQUIRE(row.get_bool(1) == true);
|
|
REQUIRE(row.get_int(2) == 6);
|
|
REQUIRE(row.get_float(3) == 2.2f);
|
|
REQUIRE(row.get_double(4) == 3.3);
|
|
REQUIRE(row.get_string(5) == "a");
|
|
REQUIRE(row.get_binary(6) == BinaryData("olleh", 5));
|
|
REQUIRE(row.get_timestamp(7) == Timestamp(10, 20));
|
|
}
|
|
|
|
SECTION("create with update - only with diffs") {
|
|
CollectionChangeSet change;
|
|
bool callback_called;
|
|
AnyDict adam {
|
|
{"name", "Adam"s},
|
|
{"age", INT64_C(32)},
|
|
{"scores", AnyVec{INT64_C(1), INT64_C(2)}},
|
|
};
|
|
AnyDict brian {
|
|
{"name", "Brian"s},
|
|
{"age", INT64_C(33)},
|
|
};
|
|
AnyDict charley {
|
|
{"name", "Charley"s},
|
|
{"age", INT64_C(34)},
|
|
{"team", AnyVec{adam, brian}}
|
|
};
|
|
AnyDict donald {
|
|
{"name", "Donald"s},
|
|
{"age", INT64_C(35)},
|
|
};
|
|
AnyDict eddie {
|
|
{"name", "Eddie"s},
|
|
{"age", INT64_C(36)},
|
|
{"assistant", donald},
|
|
{"team", AnyVec{donald, charley}}
|
|
};
|
|
Object obj = create_company(eddie, true);
|
|
|
|
auto table = r->read_group().get_table("class_person");
|
|
REQUIRE(table->size() == 5);
|
|
Results result(r, *table);
|
|
auto token = result.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) {
|
|
change = c;
|
|
callback_called = true;
|
|
});
|
|
advance_and_notify(*r);
|
|
|
|
// First update unconditionally
|
|
create_company(eddie, true, false);
|
|
|
|
callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(callback_called);
|
|
REQUIRE_INDICES(change.modifications, 0, 1, 2, 3, 4);
|
|
|
|
// Now, only update where differences (there should not be any diffs - so no update)
|
|
create_company(eddie, true, true);
|
|
|
|
REQUIRE(table->size() == 5);
|
|
callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(!callback_called);
|
|
|
|
// Now, only update sub-object)
|
|
donald["scores"] = AnyVec{INT64_C(3), INT64_C(4), INT64_C(5)};
|
|
// Insert the new donald
|
|
eddie["assistant"] = donald;
|
|
create_company(eddie, true, true);
|
|
|
|
REQUIRE(table->size() == 5);
|
|
callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(callback_called);
|
|
REQUIRE_INDICES(change.modifications, 1);
|
|
|
|
// Shorten list
|
|
donald["scores"] = AnyVec{INT64_C(3), INT64_C(4)};
|
|
eddie["assistant"] = donald;
|
|
create_company(eddie, true, true);
|
|
|
|
REQUIRE(table->size() == 5);
|
|
callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(callback_called);
|
|
REQUIRE_INDICES(change.modifications, 1);
|
|
}
|
|
|
|
SECTION("create with update - identical sub-object") {
|
|
bool callback_called;
|
|
bool sub_callback_called;
|
|
Object sub_obj = create_sub(AnyDict{{"value", INT64_C(10)}}, false);
|
|
Object obj = create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", sub_obj},
|
|
}, false);
|
|
|
|
auto token1 = obj.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) {
|
|
callback_called = true;
|
|
});
|
|
auto token2 = sub_obj.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) {
|
|
sub_callback_called = true;
|
|
});
|
|
advance_and_notify(*r);
|
|
|
|
auto table = r->read_group().get_table("class_link target");
|
|
REQUIRE(table->size() == 1);
|
|
|
|
create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(10)}}},
|
|
}, true, true);
|
|
|
|
REQUIRE(table->size() == 1);
|
|
callback_called = false;
|
|
sub_callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(!callback_called);
|
|
REQUIRE(!sub_callback_called);
|
|
|
|
// Now change sub object
|
|
create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(11)}}},
|
|
}, true, true);
|
|
|
|
callback_called = false;
|
|
sub_callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(!callback_called);
|
|
REQUIRE(sub_callback_called);
|
|
}
|
|
|
|
SECTION("create with update - identical array of sub-objects") {
|
|
bool callback_called;
|
|
auto dict = AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object array", AnyVec{ AnyDict{{"value", INT64_C(20)}}, AnyDict{{"value", INT64_C(21)}} } },
|
|
};
|
|
Object obj = create(dict, false);
|
|
|
|
auto token1 = obj.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) {
|
|
callback_called = true;
|
|
});
|
|
advance_and_notify(*r);
|
|
|
|
create(dict, true, true);
|
|
|
|
callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(!callback_called);
|
|
|
|
// Now change list
|
|
dict["object array"] = AnyVec{AnyDict{{"value", INT64_C(23)}}};
|
|
create(dict, true, true);
|
|
|
|
callback_called = false;
|
|
advance_and_notify(*r);
|
|
REQUIRE(callback_called);
|
|
}
|
|
|
|
for (auto diffed_update : {false, true}) {
|
|
SECTION("set existing fields to null with update "s + (diffed_update ? "(diffed)" : "(all)")) {
|
|
AnyDict initial_values{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
|
|
{"bool array", AnyVec{true, false}},
|
|
{"int array", AnyVec{INT64_C(5), INT64_C(6)}},
|
|
{"float array", AnyVec{1.1f, 2.2f}},
|
|
{"double array", AnyVec{3.3, 4.4}},
|
|
{"string array", AnyVec{"a"s, "b"s, "c"s}},
|
|
{"data array", AnyVec{"d"s, "e"s, "f"s}},
|
|
{"date array", AnyVec{}},
|
|
{"object array", AnyVec{AnyDict{{"value", INT64_C(20)}}}},
|
|
};
|
|
r->begin_transaction();
|
|
auto obj = Object::create(d, r, *r->schema().find("all optional types"), util::Any(initial_values));
|
|
|
|
// Missing fields in dictionary do not update anything
|
|
Object::create(d, r, *r->schema().find("all optional types"),
|
|
util::Any(AnyDict{{"pk", INT64_C(1)}}), true, diffed_update);
|
|
|
|
REQUIRE(any_cast<bool>(obj.get_property_value<util::Any>(d, "bool")) == true);
|
|
REQUIRE(any_cast<int64_t>(obj.get_property_value<util::Any>(d, "int")) == 5);
|
|
REQUIRE(any_cast<float>(obj.get_property_value<util::Any>(d, "float")) == 2.2f);
|
|
REQUIRE(any_cast<double>(obj.get_property_value<util::Any>(d, "double")) == 3.3);
|
|
REQUIRE(any_cast<std::string>(obj.get_property_value<util::Any>(d, "string")) == "hello");
|
|
REQUIRE(any_cast<Timestamp>(obj.get_property_value<util::Any>(d, "date")) == Timestamp(10, 20));
|
|
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "bool array")).get<util::Optional<bool>>(0) == true);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "int array")).get<util::Optional<int64_t>>(0) == 5);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "float array")).get<util::Optional<float>>(0) == 1.1f);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "double array")).get<util::Optional<double>>(0) == 3.3);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "string array")).get<StringData>(0) == "a");
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "date array")).size() == 0);
|
|
|
|
// Set all properties to null
|
|
AnyDict null_values{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", util::Any()},
|
|
{"int", util::Any()},
|
|
{"float", util::Any()},
|
|
{"double", util::Any()},
|
|
{"string", util::Any()},
|
|
{"data", util::Any()},
|
|
{"date", util::Any()},
|
|
|
|
{"bool array", AnyVec{util::Any()}},
|
|
{"int array", AnyVec{util::Any()}},
|
|
{"float array", AnyVec{util::Any()}},
|
|
{"double array", AnyVec{util::Any()}},
|
|
{"string array", AnyVec{util::Any()}},
|
|
{"data array", AnyVec{util::Any()}},
|
|
{"date array", AnyVec{Timestamp()}},
|
|
};
|
|
Object::create(d, r, *r->schema().find("all optional types"), util::Any(null_values), true, diffed_update);
|
|
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "bool").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "int").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "float").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "double").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "string").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "data").has_value());
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "date").has_value());
|
|
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "bool array")).get<util::Optional<bool>>(0) == util::none);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "int array")).get<util::Optional<int64_t>>(0) == util::none);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "float array")).get<util::Optional<float>>(0) == util::none);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "double array")).get<util::Optional<double>>(0) == util::none);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "string array")).get<StringData>(0) == StringData());
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "data array")).get<BinaryData>(0) == BinaryData());
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "date array")).get<Timestamp>(0) == Timestamp());
|
|
|
|
// Set all properties back to non-null
|
|
Object::create(d, r, *r->schema().find("all optional types"), util::Any(initial_values), true, diffed_update);
|
|
REQUIRE(any_cast<bool>(obj.get_property_value<util::Any>(d, "bool")) == true);
|
|
REQUIRE(any_cast<int64_t>(obj.get_property_value<util::Any>(d, "int")) == 5);
|
|
REQUIRE(any_cast<float>(obj.get_property_value<util::Any>(d, "float")) == 2.2f);
|
|
REQUIRE(any_cast<double>(obj.get_property_value<util::Any>(d, "double")) == 3.3);
|
|
REQUIRE(any_cast<std::string>(obj.get_property_value<util::Any>(d, "string")) == "hello");
|
|
REQUIRE(any_cast<Timestamp>(obj.get_property_value<util::Any>(d, "date")) == Timestamp(10, 20));
|
|
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "bool array")).get<util::Optional<bool>>(0) == true);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "int array")).get<util::Optional<int64_t>>(0) == 5);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "float array")).get<util::Optional<float>>(0) == 1.1f);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "double array")).get<util::Optional<double>>(0) == 3.3);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "string array")).get<StringData>(0) == "a");
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "date array")).size() == 0);
|
|
}
|
|
}
|
|
|
|
SECTION("create throws for duplicate pk if update is not specified") {
|
|
create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(10)}}},
|
|
{"array", AnyVector{AnyDict{{"value", INT64_C(20)}}}},
|
|
}, false);
|
|
REQUIRE_THROWS(create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
{"object", AnyDict{{"value", INT64_C(10)}}},
|
|
{"array", AnyVector{AnyDict{{"value", INT64_C(20)}}}},
|
|
}, false));
|
|
}
|
|
|
|
SECTION("create with explicit null pk does not fall back to default") {
|
|
d.defaults["nullable int pk"] = {
|
|
{"pk", INT64_C(10)},
|
|
};
|
|
d.defaults["nullable string pk"] = {
|
|
{"pk", "value"s},
|
|
};
|
|
auto create = [&](util::Any&& value, StringData type) {
|
|
r->begin_transaction();
|
|
auto obj = Object::create(d, r, *r->schema().find(type), value, false);
|
|
r->commit_transaction();
|
|
return obj;
|
|
};
|
|
|
|
auto obj = create(AnyDict{{"pk", d.null_value()}}, "nullable int pk");
|
|
REQUIRE(obj.row().is_null(0));
|
|
obj = create(AnyDict{{"pk", d.null_value()}}, "nullable string pk");
|
|
REQUIRE(obj.row().is_null(0));
|
|
|
|
obj = create(AnyDict{{}}, "nullable int pk");
|
|
REQUIRE(obj.row().get_int(0) == 10);
|
|
obj = create(AnyDict{{}}, "nullable string pk");
|
|
REQUIRE(obj.row().get_string(0) == "value");
|
|
}
|
|
|
|
SECTION("getters and setters") {
|
|
r->begin_transaction();
|
|
|
|
auto& table = *r->read_group().get_table("class_all types");
|
|
table.add_empty_row();
|
|
Object obj(r, *r->schema().find("all types"), table[0]);
|
|
|
|
auto& link_table = *r->read_group().get_table("class_link target");
|
|
link_table.add_empty_row();
|
|
Object linkobj(r, *r->schema().find("link target"), link_table[0]);
|
|
|
|
obj.set_property_value(d, "bool", util::Any(true), false);
|
|
REQUIRE(any_cast<bool>(obj.get_property_value<util::Any>(d, "bool")) == true);
|
|
|
|
obj.set_property_value(d, "int", util::Any(INT64_C(5)), false);
|
|
REQUIRE(any_cast<int64_t>(obj.get_property_value<util::Any>(d, "int")) == 5);
|
|
|
|
obj.set_property_value(d, "float", util::Any(1.23f), false);
|
|
REQUIRE(any_cast<float>(obj.get_property_value<util::Any>(d, "float")) == 1.23f);
|
|
|
|
obj.set_property_value(d, "double", util::Any(1.23), false);
|
|
REQUIRE(any_cast<double>(obj.get_property_value<util::Any>(d, "double")) == 1.23);
|
|
|
|
obj.set_property_value(d, "string", util::Any("abc"s), false);
|
|
REQUIRE(any_cast<std::string>(obj.get_property_value<util::Any>(d, "string")) == "abc");
|
|
|
|
obj.set_property_value(d, "data", util::Any("abc"s), false);
|
|
REQUIRE(any_cast<std::string>(obj.get_property_value<util::Any>(d, "data")) == "abc");
|
|
|
|
obj.set_property_value(d, "date", util::Any(Timestamp(1, 2)), false);
|
|
REQUIRE(any_cast<Timestamp>(obj.get_property_value<util::Any>(d, "date")) == Timestamp(1, 2));
|
|
|
|
REQUIRE_FALSE(obj.get_property_value<util::Any>(d, "object").has_value());
|
|
obj.set_property_value(d, "object", util::Any(linkobj), false);
|
|
REQUIRE(any_cast<Object>(obj.get_property_value<util::Any>(d, "object")).row().get_index() == linkobj.row().get_index());
|
|
|
|
auto linking = any_cast<Results>(linkobj.get_property_value<util::Any>(d, "origin"));
|
|
REQUIRE(linking.size() == 1);
|
|
|
|
REQUIRE_THROWS(obj.set_property_value(d, "pk", util::Any(INT64_C(5)), false));
|
|
REQUIRE_THROWS(obj.set_property_value(d, "not a property", util::Any(INT64_C(5)), false));
|
|
|
|
r->commit_transaction();
|
|
|
|
REQUIRE_THROWS(obj.get_property_value<util::Any>(d, "not a property"));
|
|
REQUIRE_THROWS(obj.set_property_value(d, "int", util::Any(INT64_C(5)), false));
|
|
}
|
|
|
|
SECTION("list property self-assign is a no-op") {
|
|
auto obj = create(AnyDict{
|
|
{"pk", INT64_C(1)},
|
|
{"bool", true},
|
|
{"int", INT64_C(5)},
|
|
{"float", 2.2f},
|
|
{"double", 3.3},
|
|
{"string", "hello"s},
|
|
{"data", "olleh"s},
|
|
{"date", Timestamp(10, 20)},
|
|
|
|
{"bool array", AnyVec{true, false}},
|
|
{"object array", AnyVec{AnyDict{{"value", INT64_C(20)}}}},
|
|
}, false);
|
|
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "bool array")).size() == 2);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "object array")).size() == 1);
|
|
|
|
r->begin_transaction();
|
|
obj.set_property_value(d, "bool array", obj.get_property_value<util::Any>(d, "bool array"), false);
|
|
obj.set_property_value(d, "object array", obj.get_property_value<util::Any>(d, "object array"), false);
|
|
r->commit_transaction();
|
|
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "bool array")).size() == 2);
|
|
REQUIRE(any_cast<List&&>(obj.get_property_value<util::Any>(d, "object array")).size() == 1);
|
|
}
|
|
|
|
#if REALM_ENABLE_SYNC
|
|
if (!util::EventLoop::has_implementation())
|
|
return;
|
|
|
|
SyncServer server(false);
|
|
SyncTestFile config1(server, "shared");
|
|
config1.schema = config.schema;
|
|
SyncTestFile config2(server, "shared");
|
|
config2.schema = config.schema;
|
|
|
|
SECTION("defaults do not override values explicitly passed to create()") {
|
|
AnyDict v1{
|
|
{"pk", INT64_C(7)},
|
|
{"array 1", AnyVector{AnyDict{{"value", INT64_C(1)}}}},
|
|
{"array 2", AnyVector{AnyDict{{"value", INT64_C(2)}}}},
|
|
};
|
|
auto v2 = v1;
|
|
v1["int 1"] = INT64_C(1);
|
|
v2["int 2"] = INT64_C(2);
|
|
|
|
auto r1 = Realm::get_shared_realm(config1);
|
|
auto r2 = Realm::get_shared_realm(config2);
|
|
|
|
TestContext c1(r1);
|
|
TestContext c2(r2);
|
|
|
|
c1.defaults["pk after list"] = {
|
|
{"int 1", INT64_C(10)},
|
|
{"int 2", INT64_C(10)},
|
|
};
|
|
c2.defaults = c1.defaults;
|
|
|
|
r1->begin_transaction();
|
|
r2->begin_transaction();
|
|
auto obj = Object::create(c1, r1, *r1->schema().find("pk after list"), util::Any(v1), false);
|
|
Object::create(c2, r2, *r2->schema().find("pk after list"), util::Any(v2), false);
|
|
r2->commit_transaction();
|
|
r1->commit_transaction();
|
|
|
|
server.start();
|
|
util::EventLoop::main().run_until([&] {
|
|
return r1->read_group().get_table("class_array target")->size() == 4;
|
|
});
|
|
|
|
// With stable IDs, sync creates the primary key column at index 0.
|
|
REQUIRE(obj.row().get_int(0) == 7); // pk
|
|
REQUIRE(obj.row().get_linklist(1)->size() == 2);
|
|
REQUIRE(obj.row().get_int(2) == 1); // non-default from r1
|
|
REQUIRE(obj.row().get_int(3) == 2); // non-default from r2
|
|
REQUIRE(obj.row().get_linklist(4)->size() == 2);
|
|
|
|
}
|
|
#endif
|
|
}
|
|
|