Newer
Older
bremer-ios-app / Pods / Realm / core / realm-monorepo.xcframework / xros-arm64_x86_64-simulator / Headers / realm / object-store / object_accessor.hpp
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

#ifndef REALM_OS_OBJECT_ACCESSOR_HPP
#define REALM_OS_OBJECT_ACCESSOR_HPP

#include <realm/object-store/object.hpp>

#include <realm/object-store/feature_checks.hpp>
#include <realm/object-store/list.hpp>
#include <realm/object-store/dictionary.hpp>
#include <realm/object-store/set.hpp>
#include <realm/object-store/dictionary.hpp>
#include <realm/object-store/object_schema.hpp>
#include <realm/object-store/object_store.hpp>
#include <realm/object-store/results.hpp>
#include <realm/object-store/schema.hpp>
#include <realm/object-store/shared_realm.hpp>

#include <realm/util/assert.hpp>
#include <realm/util/optional.hpp>
#include <realm/table_view.hpp>

#include <string>

namespace realm {
template <typename ValueType, typename ContextType>
void Object::set_property_value(ContextType& ctx, StringData prop_name, ValueType value, CreatePolicy policy)
{
    auto& property = property_for_name(prop_name);
    validate_property_for_setter(property);
    set_property_value_impl(ctx, property, value, policy, false);
}

template <typename ValueType, typename ContextType>
void Object::set_property_value(ContextType& ctx, const Property& property, ValueType value, CreatePolicy policy)
{
    set_property_value_impl(ctx, property, value, policy, false);
}

template <typename ValueType, typename ContextType>
ValueType Object::get_property_value(ContextType& ctx, const Property& property) const
{
    return get_property_value_impl<ValueType>(ctx, property);
}

template <typename ValueType, typename ContextType>
ValueType Object::get_property_value(ContextType& ctx, StringData prop_name) const
{
    return get_property_value_impl<ValueType>(ctx, property_for_name(prop_name));
}

namespace {
template <typename ValueType, typename ContextType>
struct ValueUpdater {
    ContextType& ctx;
    Property const& property;
    ValueType& value;
    Obj& obj;
    ColKey col;
    CreatePolicy policy;
    bool is_default;

    void operator()(Obj*)
    {
        ContextType child_ctx(ctx, obj, property);
        auto policy2 = policy;
        policy2.create = false;
        auto link = child_ctx.template unbox<Obj>(value, policy2);
        if (!policy.copy && link && link.get_table()->is_embedded())
            throw InvalidArgument("Cannot set a link to an existing managed embedded object");

        ObjKey curr_link;
        if (policy.diff)
            curr_link = obj.get<ObjKey>(col);
        if (!link || link.get_table()->is_embedded())
            link = child_ctx.template unbox<Obj>(value, policy, curr_link);
        if (!policy.diff || curr_link != link.get_key()) {
            if (!link || !link.get_table()->is_embedded())
                obj.set(col, link.get_key());
        }
    }

    template <typename T>
    void operator()(T*)
    {
        bool attr_changed = !policy.diff;
        auto new_val = ctx.template unbox<T>(value, policy);

        if (!attr_changed) {
            auto old_val = obj.get<T>(col);

            if constexpr (std::is_same<T, realm::Mixed>::value) {
                attr_changed = !new_val.is_same_type(old_val);
            }

            attr_changed = attr_changed || new_val != old_val;
        }

        if (attr_changed)
            obj.set(col, new_val, is_default);
    }
};
} // namespace

template <typename ValueType, typename ContextType>
void Object::set_property_value_impl(ContextType& ctx, const Property& property, ValueType value, CreatePolicy policy,
                                     bool is_default)
{
    ctx.will_change(*this, property);

    ColKey col{property.column_key};
    if (!is_collection(property.type) && is_nullable(property.type) && ctx.is_null(value)) {
        if (!policy.diff || !m_obj.is_null(col)) {
            if (property.type == PropertyType::Object) {
                if (!is_default)
                    m_obj.set_null(col);
            }
            else {
                m_obj.set_null(col, is_default);
            }
        }

        ctx.did_change();
        return;
    }

    if (is_array(property.type)) {
        if (property.type == PropertyType::LinkingObjects)
            throw ReadOnlyPropertyException(m_object_schema->name, property.name);

        ContextType child_ctx(ctx, m_obj, property);
        List list(m_realm, m_obj, col);
        list.assign(child_ctx, value, policy);
        ctx.did_change();
        return;
    }

    if (is_dictionary(property.type)) {
        ContextType child_ctx(ctx, m_obj, property);
        object_store::Dictionary dict(m_realm, m_obj, col);
        dict.assign(child_ctx, value, policy);
        ctx.did_change();
        return;
    }

    if (is_set(property.type)) {
        if (property.type == PropertyType::LinkingObjects)
            throw ReadOnlyPropertyException(m_object_schema->name, property.name);

        ContextType child_ctx(ctx, m_obj, property);
        object_store::Set set(m_realm, m_obj, col);
        set.assign(child_ctx, value, policy);
        ctx.did_change();
        return;
    }

    ValueUpdater<ValueType, ContextType> updater{ctx, property, value, m_obj, col, policy, is_default};
    switch_on_type(property.type, updater);
    ctx.did_change();
}

template <typename ValueType, typename ContextType>
ValueType Object::get_property_value_impl(ContextType& ctx, const Property& property) const
{
    verify_attached();

    ColKey column{property.column_key};
    if (is_nullable(property.type) && m_obj.is_null(column))
        return ctx.null_value();
    if (is_array(property.type) && property.type != PropertyType::LinkingObjects)
        return ctx.box(List(m_realm, m_obj, column));
    if (is_set(property.type) && property.type != PropertyType::LinkingObjects)
        return ctx.box(object_store::Set(m_realm, m_obj, column));
    if (is_dictionary(property.type))
        return ctx.box(object_store::Dictionary(m_realm, m_obj, column));

    switch (property.type & ~PropertyType::Flags) {
        case PropertyType::Bool:
            return ctx.box(m_obj.get<bool>(column));
        case PropertyType::Int:
            return is_nullable(property.type) ? ctx.box(*m_obj.get<util::Optional<int64_t>>(column))
                                              : ctx.box(m_obj.get<int64_t>(column));
        case PropertyType::Float:
            return ctx.box(m_obj.get<float>(column));
        case PropertyType::Double:
            return ctx.box(m_obj.get<double>(column));
        case PropertyType::String:
            return ctx.box(m_obj.get<StringData>(column));
        case PropertyType::Data:
            return ctx.box(m_obj.get<BinaryData>(column));
        case PropertyType::Date:
            return ctx.box(m_obj.get<Timestamp>(column));
        case PropertyType::ObjectId:
            return is_nullable(property.type) ? ctx.box(m_obj.get<util::Optional<ObjectId>>(column))
                                              : ctx.box(m_obj.get<ObjectId>(column));
        case PropertyType::Decimal:
            return ctx.box(m_obj.get<Decimal>(column));
        case PropertyType::UUID:
            return is_nullable(property.type) ? ctx.box(m_obj.get<util::Optional<UUID>>(column))
                                              : ctx.box(m_obj.get<UUID>(column));
        case PropertyType::Mixed:
            return ctx.box(m_obj.get<Mixed>(column));
        case PropertyType::Object: {
            auto linkObjectSchema = m_realm->schema().find(property.object_type);
            auto linked = const_cast<Obj&>(m_obj).get_linked_object(column);
            return ctx.box(Object(m_realm, *linkObjectSchema, linked, m_obj, column));
        }
        case PropertyType::LinkingObjects: {
            auto target_object_schema = m_realm->schema().find(property.object_type);
            auto link_property = target_object_schema->property_for_name(property.link_origin_property_name);
            auto table = m_realm->read_group().get_table(target_object_schema->table_key);
            auto tv = const_cast<Obj&>(m_obj).get_backlink_view(table, ColKey(link_property->column_key));
            return ctx.box(Results(m_realm, std::move(tv)));
        }
        default:
            REALM_UNREACHABLE();
    }
}

template <typename ValueType, typename ContextType>
Object Object::create(ContextType& ctx, std::shared_ptr<Realm> const& realm, StringData object_type, ValueType value,
                      CreatePolicy policy, ObjKey current_obj, Obj* out_row)
{
    auto object_schema = realm->schema().find(object_type);
    REALM_ASSERT(object_schema != realm->schema().end());
    return create(ctx, realm, *object_schema, value, policy, current_obj, out_row);
}

template <typename ValueType, typename ContextType>
Mixed as_mixed(ContextType& ctx, ValueType& value, PropertyType type)
{
    if (!value)
        return {};
    return switch_on_type(type, [&](auto* t) {
        return Mixed(ctx.template unbox<NonObjTypeT<decltype(*t)>>(*value));
    });
}

template <typename ValueType, typename ContextType>
Object Object::create(ContextType& ctx, std::shared_ptr<Realm> const& realm, ObjectSchema const& object_schema,
                      ValueType value, CreatePolicy policy, ObjKey current_obj, Obj* out_row)
{
    realm->verify_in_write();

    // When setting each property, we normally want to skip over the primary key
    // as that's set as part of object creation. However, during migrations the
    // property marked as the primary key in the schema may not currently be
    // considered a primary key by core, and so will need to be set.
    bool skip_primary = true;
    // If the input value is missing values for any of the properties we want to
    // set the property to the default value for new objects, but leave it
    // untouched for existing objects.
    bool created = false;

    Obj obj;
    auto table = realm->read_group().get_table(object_schema.table_key);

    // Asymmetric objects cannot be updated through Object::create.
    if (object_schema.table_type == ObjectSchema::ObjectType::TopLevelAsymmetric) {
        REALM_ASSERT(!policy.update);
        REALM_ASSERT(!current_obj);
        REALM_ASSERT(object_schema.primary_key_property());
    }

    // If there's a primary key, we need to first check if an object with the
    // same primary key already exists. If it does, we either update that object
    // 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]);
        if (!primary_value)
            primary_value = ctx.default_value_for_property(object_schema, *primary_prop);
        if (!primary_value && !is_nullable(primary_prop->type))
            throw MissingPropertyValueException(object_schema.name, primary_prop->name);

        // When changing the primary key of a table, we remove the existing pk (if any), call
        // the migration function, then add the new pk (if any). This means that we can't call
        // create_object_with_primary_key(), and creating duplicate primary keys is allowed as
        // long as they're unique by the end of the migration.
        if (table->get_primary_key_column() == ColKey{}) {
            REALM_ASSERT(realm->is_in_migration());
            if (policy.update) {
                if (auto key = get_for_primary_key_in_migration(ctx, *table, *primary_prop, *primary_value))
                    obj = table->get_object(key);
            }
            if (!obj)
                skip_primary = false;
        }
        else {
            obj = table->create_object_with_primary_key(as_mixed(ctx, primary_value, primary_prop->type), &created);
            if (!created && !policy.update) {
                if (!realm->is_in_migration()) {
                    auto pk_val = primary_value ? ctx.print(*primary_value) : "null";
                    throw ObjectAlreadyExists(object_schema.name, pk_val);
                }
                table->set_primary_key_column(ColKey{});
                skip_primary = false;
                obj = {};
            }
        }
    }

    // No primary key (possibly temporarily due to migrations). If we're
    // currently performing a recursive update on an existing object tree then
    // an object key was passed in that we need to look up, and otherwise we
    // need to create the new object.
    if (!obj) {
        if (current_obj)
            obj = table->get_object(current_obj);
        else if (object_schema.table_type == ObjectSchema::ObjectType::Embedded)
            obj = ctx.create_embedded_object();
        else
            obj = table->create_object();
        created = !policy.diff || !current_obj;
    }

    Object object(realm, object_schema, obj);
    // KVO in Cocoa requires that the obj ivar on the wrapper object be set
    // *before* we start setting the properties, so it passes in a pointer to
    // 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;
        }
        // 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 (object_schema.table_type == ObjectSchema::ObjectType::TopLevelAsymmetric) {
        return Object{};
    }
    return object;
}

template <typename ValueType, typename ContextType>
Object Object::get_for_primary_key(ContextType& ctx, std::shared_ptr<Realm> const& realm, StringData object_type,
                                   ValueType primary_value)
{
    auto object_schema = realm->schema().find(object_type);
    REALM_ASSERT(object_schema != realm->schema().end());
    return get_for_primary_key(ctx, realm, *object_schema, primary_value);
}

template <typename ValueType, typename ContextType>
Object Object::get_for_primary_key(ContextType& ctx, std::shared_ptr<Realm> const& realm,
                                   const ObjectSchema& object_schema, ValueType primary_value)
{
    auto primary_prop = object_schema.primary_key_property();
    if (!primary_prop) {
        throw MissingPrimaryKeyException(object_schema.name);
    }

    TableRef table;
    if (object_schema.table_key)
        table = realm->read_group().get_table(object_schema.table_key);
    if (!table)
        return Object(realm, object_schema, Obj());
    if (ctx.is_null(primary_value) && !is_nullable(primary_prop->type))
        throw NotNullable(util::format("Invalid null value for non-nullable primary key '%1.%2'.", object_schema.name,
                                       primary_prop->name));

    auto primary_key_value = switch_on_type(primary_prop->type, [&](auto* t) {
        return Mixed(ctx.template unbox<NonObjTypeT<decltype(*t)>>(primary_value));
    });
    auto key = table->find_primary_key(primary_key_value);
    return Object(realm, object_schema, key ? table->get_object(key) : Obj{});
}

template <typename ValueType, typename ContextType>
ObjKey Object::get_for_primary_key_in_migration(ContextType& ctx, Table const& table, const Property& primary_prop,
                                                ValueType&& primary_value)
{
    bool is_null = ctx.is_null(primary_value);
    if (is_null && !is_nullable(primary_prop.type))
        throw NotNullable(util::format("Invalid null value for non-nullable primary key '%1.%2'.",
                                       table.get_class_name(), primary_prop.name));
    if (primary_prop.type == PropertyType::String) {
        return table.find_first(primary_prop.column_key, ctx.template unbox<StringData>(primary_value));
    }
    if (primary_prop.type == PropertyType::ObjectId) {
        if (is_nullable(primary_prop.type)) {
            return table.find_first(primary_prop.column_key,
                                    ctx.template unbox<util::Optional<ObjectId>>(primary_value));
        }
        return table.find_first(primary_prop.column_key, ctx.template unbox<ObjectId>(primary_value));
    }
    else if (primary_prop.type == PropertyType::UUID) {
        if (is_nullable(primary_prop.type)) {
            return table.find_primary_key(ctx.template unbox<util::Optional<UUID>>(primary_value));
        }
        return table.find_primary_key(ctx.template unbox<UUID>(primary_value));
    }
    if (is_nullable(primary_prop.type))
        return table.find_first(primary_prop.column_key, ctx.template unbox<util::Optional<int64_t>>(primary_value));
    return table.find_first(primary_prop.column_key, ctx.template unbox<int64_t>(primary_value));
}

} // namespace realm

#endif // REALM_OS_OBJECT_ACCESSOR_HPP