diff --git a/src/realm/object-store/impl/object_accessor_impl.hpp b/src/realm/object-store/impl/object_accessor_impl.hpp index c737158933..0093a8e511 100644 --- a/src/realm/object-store/impl/object_accessor_impl.hpp +++ b/src/realm/object-store/impl/object_accessor_impl.hpp @@ -70,15 +70,15 @@ class CppContext { // value present. The property is identified both by the name of the // property and its index within the ObjectScehma's persisted_properties // array. - util::Optional value_for_property(std::any& dict, const Property& prop, + util::Optional value_for_property(std::any& dict, const std::string& name, size_t /* property_index */) const { #if REALM_ENABLE_GEOSPATIAL if (auto geo = std::any_cast(&dict)) { - if (prop.name == Geospatial::c_geo_point_type_col_name) { + if (name == Geospatial::c_geo_point_type_col_name) { return geo->get_type_string(); } - else if (prop.name == Geospatial::c_geo_point_coords_col_name) { + else if (name == Geospatial::c_geo_point_coords_col_name) { std::vector coords; auto&& point = geo->get(); // throws coords.push_back(point.longitude); @@ -88,11 +88,11 @@ class CppContext { } return coords; } - REALM_ASSERT_EX(false, prop.name); // unexpected property type + REALM_ASSERT_EX(false, name); // unexpected property type } #endif auto const& v = util::any_cast(dict); - auto it = v.find(prop.name); + auto it = v.find(name); return it == v.end() ? util::none : util::make_optional(it->second); } @@ -118,6 +118,20 @@ class CppContext { template void enumerate_dictionary(std::any& value, Func&& fn) { +#if REALM_ENABLE_GEOSPATIAL + if (auto geo = std::any_cast(&value)) { + fn(Geospatial::c_geo_point_type_col_name, std::any(geo->get_type_string())); + std::vector coords; + auto&& point = geo->get(); // throws + coords.push_back(point.longitude); + coords.push_back(point.latitude); + if (point.has_altitude()) { + coords.push_back(*point.get_altitude()); + } + fn(Geospatial::c_geo_point_coords_col_name, std::any(coords)); + return; + } +#endif for (auto&& v : util::any_cast(value)) fn(v.first, v.second); } diff --git a/src/realm/object-store/object_accessor.hpp b/src/realm/object-store/object_accessor.hpp index 868325c55c..6022db7a0e 100644 --- a/src/realm/object-store/object_accessor.hpp +++ b/src/realm/object-store/object_accessor.hpp @@ -364,7 +364,7 @@ Object Object::create(ContextType& ctx, std::shared_ptr const& realm, Obj // or throw an exception if updating is disabled. if (auto primary_prop = object_schema.primary_key_property()) { auto primary_value = - ctx.value_for_property(value, *primary_prop, primary_prop - &object_schema.persisted_properties[0]); + ctx.value_for_property(value, primary_prop->name, primary_prop - &object_schema.persisted_properties[0]); if (!primary_value) primary_value = ctx.default_value_for_property(object_schema, *primary_prop); if (!primary_value && !is_nullable(primary_prop->type)) @@ -417,30 +417,44 @@ Object Object::create(ContextType& ctx, std::shared_ptr const& realm, Obj // that. if (out_row && object_schema.table_type != ObjectSchema::ObjectType::TopLevelAsymmetric) *out_row = obj; - for (size_t i = 0; i < object_schema.persisted_properties.size(); ++i) { - auto& prop = object_schema.persisted_properties[i]; - // If table has primary key, it must have been set during object creation - if (prop.is_primary && skip_primary) - continue; - - auto v = ctx.value_for_property(value, prop, i); - if (!created && !v) - continue; - - bool is_default = false; - if (!v) { - v = ctx.default_value_for_property(object_schema, prop); - is_default = true; + + std::unordered_set props_supplied; + ctx.enumerate_dictionary(value, [&](StringData name, auto&& value) { + if (auto prop = object_schema.property_for_name(name)) { + if (!prop->is_primary || !skip_primary) + object.set_property_value_impl(ctx, *prop, value, policy, false); + props_supplied.insert(name); + } + else { + object.set_additional_property_value_impl(ctx, name, value, policy); } - // We consider null or a missing value to be equivalent to an empty - // array/set for historical reasons; the original implementation did this - // accidentally and it's not worth changing. - if ((!v || ctx.is_null(*v)) && !is_nullable(prop.type) && !is_collection(prop.type)) { - if (prop.is_primary || !ctx.allow_missing(value)) - throw MissingPropertyValueException(object_schema.name, prop.name); + }); + + if (created) { + // assign default values + for (size_t i = 0; i < object_schema.persisted_properties.size(); ++i) { + auto& prop = object_schema.persisted_properties[i]; + // If table has primary key, it must have been set during object creation + if (prop.is_primary && skip_primary) + continue; + + bool already_set = props_supplied.count(prop.name); + if (already_set) + continue; + + bool is_default = true; + auto v = ctx.default_value_for_property(object_schema, prop); + + // We consider null or a missing value to be equivalent to an empty + // array/set for historical reasons; the original implementation did this + // accidentally and it's not worth changing. + if ((!v || ctx.is_null(*v)) && !is_nullable(prop.type) && !is_collection(prop.type)) { + if (prop.is_primary || !ctx.allow_missing(value)) + throw MissingPropertyValueException(object_schema.name, prop.name); + } + if (v) + object.set_property_value_impl(ctx, prop, *v, policy, is_default); } - if (v) - object.set_property_value_impl(ctx, prop, *v, policy, is_default); } if (object_schema.table_type == ObjectSchema::ObjectType::TopLevelAsymmetric) { return Object{}; diff --git a/test/object-store/object.cpp b/test/object-store/object.cpp index 5006453cfb..6b105f0ecd 100644 --- a/test/object-store/object.cpp +++ b/test/object-store/object.cpp @@ -155,6 +155,100 @@ class CreatePolicyRecordingContext { mutable CreatePolicy last_create_policy; }; +TEST_CASE("object with flexible schema") { + using namespace std::string_literals; + _impl::RealmCoordinator::assert_no_open_realms(); + + InMemoryTestFile config; + config.automatic_change_notifications = false; + config.schema_mode = SchemaMode::AdditiveExplicit; + config.flexible_schema = true; + config.schema = Schema{{ + "table", + { + {"_id", PropertyType::Int, Property::IsPrimary{true}}, + }, + }}; + + config.schema_version = 0; + auto r = Realm::get_shared_realm(config); + + TestContext d(r); + auto create = [&](std::any&& value, CreatePolicy policy = CreatePolicy::ForceCreate) { + r->begin_transaction(); + auto obj = Object::create(d, r, *r->schema().find("table"), value, policy); + r->commit_transaction(); + return obj; + }; + + SECTION("create object") { + auto object = create(AnyDict{ + {"_id", INT64_C(1)}, + {"bool", true}, + {"int", INT64_C(5)}, + {"float", 2.2f}, + {"double", 3.3}, + {"string", "hello"s}, + {"date", Timestamp(10, 20)}, + {"object id", ObjectId("000000000000000000000001")}, + {"decimal", Decimal128("1.23e45")}, + {"uuid", UUID("3b241101-abba-baba-caca-4136c566a962")}, + {"mixed", "mixed"s}, + + {"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}}, + {"date array", AnyVec{Timestamp(10, 20), Timestamp(30, 40)}}, + {"object id array", AnyVec{ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"), ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")}}, + {"decimal array", AnyVec{Decimal128("1.23e45"), Decimal128("6.78e9")}}, + {"uuid array", AnyVec{UUID(), UUID("3b241101-e2bb-4255-8caf-4136c566a962")}}, + {"mixed array", + AnyVec{25, "b"s, 1.45, util::none, Timestamp(30, 40), Decimal128("1.23e45"), + ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"), UUID("3b241101-e2bb-4255-8caf-4136c566a962")}}, + {"dictionary", AnyDict{{"key", "value"s}}}, + }); + + Obj obj = object.get_obj(); + REQUIRE(obj.get("_id") == 1); + REQUIRE(obj.get("bool") == true); + REQUIRE(obj.get("int") == 5); + REQUIRE(obj.get("float") == 2.2f); + REQUIRE(obj.get("double") == 3.3); + REQUIRE(obj.get("string") == "hello"); + REQUIRE(obj.get("date") == Timestamp(10, 20)); + REQUIRE(obj.get("object id") == ObjectId("000000000000000000000001")); + REQUIRE(obj.get("decimal") == Decimal128("1.23e45")); + REQUIRE(obj.get("uuid") == UUID("3b241101-abba-baba-caca-4136c566a962")); + REQUIRE(obj.get("mixed") == Mixed("mixed")); + + auto check_array = [&](StringData prop, auto... values) { + auto vec = get_vector({values...}); + using U = typename decltype(vec)::value_type; + auto list = obj.get_list_ptr(prop); + size_t i = 0; + for (auto value : vec) { + CAPTURE(i); + REQUIRE(i < list->size()); + REQUIRE(value == list->get(i).get()); + ++i; + } + }; + check_array("bool array", true, false); + check_array("int array", INT64_C(5), INT64_C(6)); + check_array("float array", 1.1f, 2.2f); + check_array("double array", 3.3, 4.4); + check_array("string array", StringData("a"), StringData("b"), StringData("c")); + check_array("date array", Timestamp(10, 20), Timestamp(30, 40)); + check_array("object id array", ObjectId("AAAAAAAAAAAAAAAAAAAAAAAA"), ObjectId("BBBBBBBBBBBBBBBBBBBBBBBB")); + check_array("decimal array", Decimal128("1.23e45"), Decimal128("6.78e9")); + check_array("uuid array", UUID(), UUID("3b241101-e2bb-4255-8caf-4136c566a962")); + + REQUIRE(obj.get_dictionary_ptr("dictionary")->get("key") == Mixed("value")); + } +} + TEST_CASE("object") { using namespace std::string_literals; _impl::RealmCoordinator::assert_no_open_realms();