diff --git a/src/object-store/object_schema.cpp b/src/object-store/object_schema.cpp new file mode 100644 index 00000000..83edd1f1 --- /dev/null +++ b/src/object-store/object_schema.cpp @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 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 "object_schema.hpp" +#include "object_store.hpp" + +#include +#include + +using namespace realm; + +ObjectSchema::ObjectSchema(Group *group, const std::string &name) : name(name) { + TableRef tableRef = ObjectStore::table_for_object_type(group, name); + Table *table = tableRef.get(); + + size_t count = table->get_column_count(); + properties.reserve(count); + for (size_t col = 0; col < count; col++) { + Property property; + property.name = table->get_column_name(col).data(); + property.type = (PropertyType)table->get_column_type(col); + property.is_indexed = table->has_search_index(col); + property.is_primary = false; + property.is_nullable = table->is_nullable(col) || property.type == PropertyTypeObject; + property.table_column = col; + if (property.type == PropertyTypeObject || property.type == PropertyTypeArray) { + // set link type for objects and arrays + realm::TableRef linkTable = table->get_link_target(col); + property.object_type = ObjectStore::object_type_for_table_name(linkTable->get_name().data()); + } + properties.push_back(std::move(property)); + } + + primary_key = realm::ObjectStore::get_primary_key_for_object(group, name); + if (primary_key.length()) { + auto primary_key_prop = primary_key_property(); + if (!primary_key_prop) { + throw InvalidPrimaryKeyException(name, primary_key); + } + primary_key_prop->is_primary = true; + } +} + +Property *ObjectSchema::property_for_name(const std::string &name) { + for (auto& prop:properties) { + if (prop.name == name) { + return ∝ + } + } + return nullptr; +} + diff --git a/src/object-store/object_schema.hpp b/src/object-store/object_schema.hpp new file mode 100644 index 00000000..56a14d17 --- /dev/null +++ b/src/object-store/object_schema.hpp @@ -0,0 +1,49 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 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_OBJECT_SCHEMA_HPP +#define REALM_OBJECT_SCHEMA_HPP + +#include +#include + +#include "property.hpp" + +namespace realm { + class Group; + + class ObjectSchema { + public: + ObjectSchema() {} + + // create object schema from existing table + // if no table is provided it is looked up in the group + ObjectSchema(Group *group, const std::string &name); + + std::string name; + std::vector properties; + std::string primary_key; + + Property *property_for_name(const std::string &name); + Property *primary_key_property() { + return property_for_name(primary_key); + } + }; +} + +#endif /* defined(REALM_OBJECT_SCHEMA_HPP) */ diff --git a/src/object-store/object_store.cpp b/src/object-store/object_store.cpp new file mode 100644 index 00000000..cf3dd2ed --- /dev/null +++ b/src/object-store/object_store.cpp @@ -0,0 +1,552 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 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 "object_store.hpp" + +#include +#include +#include +#include +#include + +#include + +using namespace realm; + +const char * const c_metadataTableName = "metadata"; +const char * const c_versionColumnName = "version"; +const size_t c_versionColumnIndex = 0; + +const char * const c_primaryKeyTableName = "pk"; +const char * const c_primaryKeyObjectClassColumnName = "pk_table"; +const size_t c_primaryKeyObjectClassColumnIndex = 0; +const char * const c_primaryKeyPropertyNameColumnName = "pk_property"; +const size_t c_primaryKeyPropertyNameColumnIndex = 1; + +const size_t c_zeroRowIndex = 0; + +const std::string c_object_table_prefix = "class_"; +const size_t c_object_table_prefix_length = c_object_table_prefix.length(); + +const uint64_t ObjectStore::NotVersioned = std::numeric_limits::max(); + +bool ObjectStore::has_metadata_tables(Group *group) { + return group->get_table(c_primaryKeyTableName) && group->get_table(c_metadataTableName); +} + +bool ObjectStore::create_metadata_tables(Group *group) { + bool changed = false; + TableRef table = group->get_or_add_table(c_primaryKeyTableName); + if (table->get_column_count() == 0) { + table->add_column(type_String, c_primaryKeyObjectClassColumnName); + table->add_column(type_String, c_primaryKeyPropertyNameColumnName); + changed = true; + } + + table = group->get_or_add_table(c_metadataTableName); + if (table->get_column_count() == 0) { + table->add_column(type_Int, c_versionColumnName); + + // set initial version + table->add_empty_row(); + table->set_int(c_versionColumnIndex, c_zeroRowIndex, ObjectStore::NotVersioned); + changed = true; + } + + return changed; +} + +uint64_t ObjectStore::get_schema_version(Group *group) { + TableRef table = group->get_table(c_metadataTableName); + if (!table || table->get_column_count() == 0) { + return ObjectStore::NotVersioned; + } + return table->get_int(c_versionColumnIndex, c_zeroRowIndex); +} + +void ObjectStore::set_schema_version(Group *group, uint64_t version) { + TableRef table = group->get_or_add_table(c_metadataTableName); + table->set_int(c_versionColumnIndex, c_zeroRowIndex, version); +} + +StringData ObjectStore::get_primary_key_for_object(Group *group, StringData object_type) { + TableRef table = group->get_table(c_primaryKeyTableName); + if (!table) { + return ""; + } + size_t row = table->find_first_string(c_primaryKeyObjectClassColumnIndex, object_type); + if (row == not_found) { + return ""; + } + return table->get_string(c_primaryKeyPropertyNameColumnIndex, row); +} + +void ObjectStore::set_primary_key_for_object(Group *group, StringData object_type, StringData primary_key) { + TableRef table = group->get_table(c_primaryKeyTableName); + + // get row or create if new object and populate + size_t row = table->find_first_string(c_primaryKeyObjectClassColumnIndex, object_type); + if (row == not_found && primary_key.size()) { + row = table->add_empty_row(); + table->set_string(c_primaryKeyObjectClassColumnIndex, row, object_type); + } + + // set if changing, or remove if setting to nil + if (primary_key.size() == 0) { + if (row != not_found) { + table->remove(row); + } + } + else { + table->set_string(c_primaryKeyPropertyNameColumnIndex, row, primary_key); + } +} + +std::string ObjectStore::object_type_for_table_name(const std::string &table_name) { + if (table_name.size() >= c_object_table_prefix_length && table_name.compare(0, c_object_table_prefix_length, c_object_table_prefix) == 0) { + return table_name.substr(c_object_table_prefix_length, table_name.length() - c_object_table_prefix_length); + } + return std::string(); +} + +std::string ObjectStore::table_name_for_object_type(const std::string &object_type) { + return c_object_table_prefix + object_type; +} + +TableRef ObjectStore::table_for_object_type(Group *group, StringData object_type) { + return group->get_table(table_name_for_object_type(object_type)); +} + +TableRef ObjectStore::table_for_object_type_create_if_needed(Group *group, const StringData &object_type, bool &created) { + return group->get_or_add_table(table_name_for_object_type(object_type), &created); +} + +static inline bool property_has_changed(Property &p1, Property &p2) { + return p1.type != p2.type || p1.name != p2.name || p1.object_type != p2.object_type || p1.is_nullable != p2.is_nullable; +} + +void ObjectStore::verify_schema(Group *group, Schema &target_schema, bool allow_missing_tables) { + std::vector errors; + for (auto &object_schema : target_schema) { + if (!table_for_object_type(group, object_schema.first)) { + if (!allow_missing_tables) { + errors.emplace_back(ObjectSchemaValidationException(object_schema.first, + "Missing table for object type '" + object_schema.first + "'.")); + } + continue; + } + + auto more_errors = verify_object_schema(group, object_schema.second, target_schema); + errors.insert(errors.end(), more_errors.begin(), more_errors.end()); + } + if (errors.size()) { + throw SchemaValidationException(errors); + } +} + +std::vector ObjectStore::verify_object_schema(Group *group, ObjectSchema &target_schema, Schema &schema) { + std::vector exceptions; + ObjectSchema table_schema(group, target_schema.name); + + // check to see if properties are the same + Property *primary = nullptr; + for (auto& current_prop : table_schema.properties) { + auto target_prop = target_schema.property_for_name(current_prop.name); + + if (!target_prop) { + exceptions.emplace_back(MissingPropertyException(table_schema.name, current_prop)); + continue; + } + if (property_has_changed(current_prop, *target_prop)) { + exceptions.emplace_back(MismatchedPropertiesException(table_schema.name, current_prop, *target_prop)); + continue; + } + + // check object_type existence + if (current_prop.object_type.length() && schema.find(current_prop.object_type) == schema.end()) { + exceptions.emplace_back(MissingObjectTypeException(table_schema.name, current_prop)); + } + + // check nullablity + if (current_prop.type == PropertyTypeObject) { + if (!current_prop.is_nullable) { + exceptions.emplace_back(InvalidNullabilityException(table_schema.name, current_prop)); + } + } + else { + if (current_prop.is_nullable) { + exceptions.emplace_back(InvalidNullabilityException(table_schema.name, current_prop)); + } + } + + // check primary keys + if (current_prop.is_primary) { + if (primary) { + exceptions.emplace_back(DuplicatePrimaryKeysException(table_schema.name)); + } + primary = ¤t_prop; + } + + // check indexable + if (current_prop.is_indexed) { + if (current_prop.type != PropertyTypeString && current_prop.type != PropertyTypeInt) { + exceptions.emplace_back(PropertyTypeNotIndexableException(table_schema.name, current_prop)); + } + } + + // create new property with aligned column + target_prop->table_column = current_prop.table_column; + } + + // check for change to primary key + if (table_schema.primary_key != target_schema.primary_key) { + exceptions.emplace_back(ChangedPrimaryKeyException(table_schema.name, table_schema.primary_key, target_schema.primary_key)); + } + + // check for new missing properties + for (auto& target_prop : target_schema.properties) { + if (!table_schema.property_for_name(target_prop.name)) { + exceptions.emplace_back(ExtraPropertyException(table_schema.name, target_prop)); + } + } + + return exceptions; +} + +void ObjectStore::update_column_mapping(Group *group, ObjectSchema &target_schema) { + ObjectSchema table_schema(group, target_schema.name); + for (auto& target_prop : target_schema.properties) { + auto table_prop = table_schema.property_for_name(target_prop.name); + REALM_ASSERT_DEBUG(table_prop); + + target_prop.table_column = table_prop->table_column; + } +} + +// set references to tables on targetSchema and create/update any missing or out-of-date tables +// if update existing is true, updates existing tables, otherwise validates existing tables +// NOTE: must be called from within write transaction +bool ObjectStore::create_tables(Group *group, Schema &target_schema, bool update_existing) { + bool changed = false; + + // first pass to create missing tables + std::vector to_update; + for (auto& object_schema : target_schema) { + bool created = false; + ObjectStore::table_for_object_type_create_if_needed(group, object_schema.first, created); + + // we will modify tables for any new objectSchema (table was created) or for all if update_existing is true + if (update_existing || created) { + to_update.push_back(&object_schema.second); + changed = true; + } + } + + // second pass adds/removes columns for out of date tables + for (auto& target_object_schema : to_update) { + TableRef table = table_for_object_type(group, target_object_schema->name); + ObjectSchema current_schema(group, target_object_schema->name); + std::vector &target_props = target_object_schema->properties; + + // add missing columns + for (auto& target_prop : target_props) { + auto current_prop = current_schema.property_for_name(target_prop.name); + + // add any new properties (new name or different type) + if (!current_prop || property_has_changed(*current_prop, target_prop)) { + switch (target_prop.type) { + // for objects and arrays, we have to specify target table + case PropertyTypeObject: + case PropertyTypeArray: { + TableRef link_table = ObjectStore::table_for_object_type(group, target_prop.object_type); + target_prop.table_column = table->add_column_link(DataType(target_prop.type), target_prop.name, *link_table); + break; + } + default: + target_prop.table_column = table->add_column(DataType(target_prop.type), target_prop.name); + break; + } + + changed = true; + } + } + + // remove extra columns + sort(begin(current_schema.properties), end(current_schema.properties), [](Property &i, Property &j) { + return j.table_column < i.table_column; + }); + for (auto& current_prop : current_schema.properties) { + auto target_prop = target_object_schema->property_for_name(current_prop.name); + if (!target_prop || property_has_changed(current_prop, *target_prop)) { + table->remove_column(current_prop.table_column); + changed = true; + } + } + + // update table metadata + if (target_object_schema->primary_key.length()) { + // if there is a primary key set, check if it is the same as the old key + if (current_schema.primary_key != target_object_schema->primary_key) { + set_primary_key_for_object(group, target_object_schema->name, target_object_schema->primary_key); + changed = true; + } + } + else if (current_schema.primary_key.length()) { + // there is no primary key, so if there was one nil out + set_primary_key_for_object(group, target_object_schema->name, ""); + changed = true; + } + } + return changed; +} + +bool ObjectStore::is_schema_at_version(Group *group, uint64_t version) { + uint64_t old_version = get_schema_version(group); + if (old_version > version && old_version != NotVersioned) { + throw InvalidSchemaVersionException(old_version, version); + } + return old_version == version; +} + +bool ObjectStore::realm_requires_update(Group *group, uint64_t version, Schema &schema) { + if (!is_schema_at_version(group, version)) { + return true; + } + for (auto& target_schema : schema) { + TableRef table = table_for_object_type(group, target_schema.first); + if (!table) { + return true; + } + } + if (!indexes_are_up_to_date(group, schema)) { + return true; + } + return false; +} + +bool ObjectStore::update_realm_with_schema(Group *group, + uint64_t version, + Schema &schema, + MigrationFunction migration) { + // Recheck the schema version after beginning the write transaction as + // another process may have done the migration after we opened the read + // transaction + bool migrating = !is_schema_at_version(group, version); + + // create tables + bool changed = create_metadata_tables(group); + changed = create_tables(group, schema, migrating) || changed; + + verify_schema(group, schema); + + changed = update_indexes(group, schema) || changed; + + if (!migrating) { + return changed; + } + + // apply the migration block if provided and there's any old data + if (get_schema_version(group) != ObjectStore::NotVersioned) { + migration(group, schema); + } + + validate_primary_column_uniqueness(group, schema); + + set_schema_version(group, version); + return true; +} + +Schema ObjectStore::schema_from_group(Group *group) { + Schema schema; + for (size_t i = 0; i < group->size(); i++) { + std::string object_type = object_type_for_table_name(group->get_table_name(i)); + if (object_type.length()) { + schema.emplace(object_type, std::move(ObjectSchema(group, object_type))); + } + } + return schema; +} + +bool ObjectStore::indexes_are_up_to_date(Group *group, Schema &schema) { + for (auto &object_schema : schema) { + TableRef table = table_for_object_type(group, object_schema.first); + if (!table) { + continue; + } + + update_column_mapping(group, object_schema.second); + for (auto& property : object_schema.second.properties) { + if (property.requires_index() != table->has_search_index(property.table_column)) { + return false; + } + } + } + return true; +} + +bool ObjectStore::update_indexes(Group *group, Schema &schema) { + bool changed = false; + for (auto& object_schema : schema) { + TableRef table = table_for_object_type(group, object_schema.first); + if (!table) { + continue; + } + + for (auto& property : object_schema.second.properties) { + if (property.requires_index() == table->has_search_index(property.table_column)) { + continue; + } + + changed = true; + if (property.requires_index()) { + try { + table->add_search_index(property.table_column); + } + catch (LogicError const&) { + throw PropertyTypeNotIndexableException(object_schema.first, property); + } + } + else { + table->remove_search_index(property.table_column); + } + } + } + return changed; +} + +void ObjectStore::validate_primary_column_uniqueness(Group *group, Schema &schema) { + for (auto& object_schema : schema) { + auto primary_prop = object_schema.second.primary_key_property(); + if (!primary_prop) { + continue; + } + + TableRef table = table_for_object_type(group, object_schema.first); + if (table->get_distinct_view(primary_prop->table_column).size() != table->size()) { + throw DuplicatePrimaryKeyValueException(object_schema.first, *primary_prop); + } + } +} + +void ObjectStore::delete_data_for_object(Group *group, const StringData &object_type) { + TableRef table = table_for_object_type(group, object_type); + if (table) { + group->remove_table(table->get_index_in_group()); + set_primary_key_for_object(group, object_type, ""); + } +} + + + +InvalidSchemaVersionException::InvalidSchemaVersionException(uint64_t old_version, uint64_t new_version) : + m_old_version(old_version), m_new_version(new_version) +{ + m_what = "Provided schema version " + std::to_string(old_version) + " is less than last set version " + std::to_string(new_version) + "."; +} + +DuplicatePrimaryKeyValueException::DuplicatePrimaryKeyValueException(std::string object_type, Property &property) : + m_object_type(object_type), m_property(property) +{ + m_what = "Primary key property '" + property.name + "' has duplicate values after migration."; +}; + + +SchemaValidationException::SchemaValidationException(std::vector errors) : + m_validation_errors(errors) +{ + m_what ="Migration is required due to the following errors: "; + for (auto error : errors) { + m_what += std::string("\n- ") + error.what(); + } +} + +PropertyTypeNotIndexableException::PropertyTypeNotIndexableException(std::string object_type, Property &property) : + ObjectSchemaPropertyException(object_type, property) +{ + m_what = "Can't index property " + object_type + "." + property.name + ": indexing a property of type '" + string_for_property_type(property.type) + "' is currently not supported"; +} + +ExtraPropertyException::ExtraPropertyException(std::string object_type, Property &property) : + ObjectSchemaPropertyException(object_type, property) +{ + m_what = "Property '" + property.name + "' has been added to latest object model."; +} + +MissingPropertyException::MissingPropertyException(std::string object_type, Property &property) : + ObjectSchemaPropertyException(object_type, property) +{ + m_what = "Property '" + property.name + "' is missing from latest object model."; +} + +InvalidNullabilityException::InvalidNullabilityException(std::string object_type, Property &property) : + ObjectSchemaPropertyException(object_type, property) +{ + if (property.type == PropertyTypeObject) { + if (!property.is_nullable) { + m_what = "'Object' property '" + property.name + "' must be nullable."; + } + } + else { + if (property.is_nullable) { + m_what = "Only 'Object' property types are nullable"; + } + } +} + +MissingObjectTypeException::MissingObjectTypeException(std::string object_type, Property &property) : + ObjectSchemaPropertyException(object_type, property) +{ + m_what = "Target type '" + property.object_type + "' doesn't exist for property '" + property.name + "'."; +} + +MismatchedPropertiesException::MismatchedPropertiesException(std::string object_type, Property &old_property, Property &new_property) : + ObjectSchemaValidationException(object_type), m_old_property(old_property), m_new_property(new_property) +{ + if (new_property.type != old_property.type) { + m_what = "Property types for '" + old_property.name + "' property do not match. Old type '" + string_for_property_type(old_property.type) + + "', new type '" + string_for_property_type(new_property.type) + "'"; + } + else if (new_property.object_type != old_property.object_type) { + m_what = "Target object type for property '" + old_property.name + "' do not match. Old type '" + old_property.object_type + "', new type '" + new_property.object_type + "'"; + } + else if (new_property.is_nullable != old_property.is_nullable) { + m_what = "Nullability for property '" + old_property.name + "' has changed from '" + std::to_string(old_property.is_nullable) + "' to '" + std::to_string(new_property.is_nullable) + "'."; + } +} + +ChangedPrimaryKeyException::ChangedPrimaryKeyException(std::string object_type, std::string old_primary, std::string new_primary) : ObjectSchemaValidationException(object_type), m_old_primary(old_primary), m_new_primary(new_primary) +{ + if (old_primary.size()) { + m_what = "Property '" + old_primary + "' is no longer a primary key."; + } + else { + m_what = "Property '" + new_primary + "' has been made a primary key."; + } +} + +InvalidPrimaryKeyException::InvalidPrimaryKeyException(std::string object_type, std::string primary) : + ObjectSchemaValidationException(object_type), m_primary_key(primary) +{ + m_what = "Specified primary key property '" + primary + "' does not exist."; +} + +DuplicatePrimaryKeysException::DuplicatePrimaryKeysException(std::string object_type) : ObjectSchemaValidationException(object_type) +{ + m_what = "Duplicate primary keys for object '" + object_type + "'."; +} + diff --git a/src/object-store/object_store.hpp b/src/object-store/object_store.hpp new file mode 100644 index 00000000..cacdcd8b --- /dev/null +++ b/src/object-store/object_store.hpp @@ -0,0 +1,238 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 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_OBJECT_STORE_HPP +#define REALM_OBJECT_STORE_HPP + +#include +#include +#include +#include +#include + +#include "object_schema.hpp" + +namespace realm { + class ObjectSchemaValidationException; + class Schema : public std::map { + }; + + class ObjectStore { + public: + // Schema version used for uninitialized Realms + static const uint64_t NotVersioned; + + // get the last set schema version + static uint64_t get_schema_version(Group *group); + + // checks if the schema in the group is at the given version + static bool is_schema_at_version(realm::Group *group, uint64_t version); + + // verify a target schema against tables in the given group + // updates the column mapping on all ObjectSchema properties + // throws if the schema is invalid or does not match tables in the given group + static void verify_schema(Group *group, Schema &target_schema, bool allow_missing_tables = false); + + // updates the target_column member for all properties based on the column indexes in the passed in group + static void update_column_mapping(Group *group, ObjectSchema &target_schema); + + // determines if you must call update_realm_with_schema for a given realm. + // returns true if there is a schema version mismatch, if there tables which still need to be created, + // or if file format or other changes/updates need to be made + static bool realm_requires_update(Group *group, uint64_t version, Schema &schema); + + // updates a Realm to a given target schema/version creating tables and updating indexes as necessary + // returns if any changes were made + // passed in schema ar updated with the correct column mapping + // optionally runs migration function/lambda if schema is out of date + // NOTE: must be performed within a write transaction + typedef std::function MigrationFunction; + static bool update_realm_with_schema(Group *group, uint64_t version, Schema &schema, MigrationFunction migration); + + // get a table for an object type + static realm::TableRef table_for_object_type(Group *group, StringData object_type); + + // get existing Schema from a group + static Schema schema_from_group(Group *group); + + // deletes the table for the given type + static void delete_data_for_object(Group *group, const StringData &object_type); + + private: + // set a new schema version + static void set_schema_version(Group *group, uint64_t version); + + // check if the realm already has all metadata tables + static bool has_metadata_tables(Group *group); + + // create any metadata tables that don't already exist + // must be in write transaction to set + // returns true if it actually did anything + static bool create_metadata_tables(Group *group); + + // set references to tables on targetSchema and create/update any missing or out-of-date tables + // if update existing is true, updates existing tables, otherwise only adds and initializes new tables + static bool create_tables(realm::Group *group, Schema &target_schema, bool update_existing); + + // verify a target schema against its table, setting the table_column property on each schema object + // updates the column mapping on the target_schema + // returns array of validation errors + static std::vector verify_object_schema(Group *group, ObjectSchema &target_schema, Schema &schema); + + // get primary key property name for object type + static StringData get_primary_key_for_object(Group *group, StringData object_type); + + // sets primary key property for object type + // must be in write transaction to set + static void set_primary_key_for_object(Group *group, StringData object_type, StringData primary_key); + + static TableRef table_for_object_type_create_if_needed(Group *group, const StringData &object_type, bool &created); + static std::string table_name_for_object_type(const std::string &class_name); + static std::string object_type_for_table_name(const std::string &table_name); + + // check if indexes are up to date - if false you need to call update_realm_with_schema + static bool indexes_are_up_to_date(Group *group, Schema &schema); + + // returns if any indexes were changed + static bool update_indexes(Group *group, Schema &schema); + + // validates that all primary key properties have unique values + static void validate_primary_column_uniqueness(Group *group, Schema &schema); + + friend ObjectSchema; + }; + + // Base exception + class ObjectStoreException : public std::exception { + public: + ObjectStoreException() = default; + ObjectStoreException(const std::string &what) : m_what(what) {} + virtual const char* what() const noexcept { return m_what.c_str(); } + protected: + std::string m_what; + }; + + // Migration exceptions + class MigrationException : public ObjectStoreException {}; + + class InvalidSchemaVersionException : public MigrationException { + public: + InvalidSchemaVersionException(uint64_t old_version, uint64_t new_version); + uint64_t old_version() { return m_old_version; } + uint64_t new_version() { return m_new_version; } + private: + uint64_t m_old_version, m_new_version; + }; + + class DuplicatePrimaryKeyValueException : public MigrationException { + public: + DuplicatePrimaryKeyValueException(std::string object_type, Property &property); + std::string object_type() { return m_object_type; } + Property &property() { return m_property; } + private: + std::string m_object_type; + Property m_property; + }; + + // Schema validation exceptions + class SchemaValidationException : public ObjectStoreException { + public: + SchemaValidationException(std::vector errors); + std::vector &validation_errors() { return m_validation_errors; } + private: + std::vector m_validation_errors; + }; + + class ObjectSchemaValidationException : public ObjectStoreException { + public: + ObjectSchemaValidationException(std::string object_type) : m_object_type(object_type) {} + ObjectSchemaValidationException(std::string object_type, std::string message) : + m_object_type(object_type) { m_what = message; } + std::string object_type() { return m_object_type; } + protected: + std::string m_object_type; + }; + + class ObjectSchemaPropertyException : public ObjectSchemaValidationException { + public: + ObjectSchemaPropertyException(std::string object_type, Property &property) : + ObjectSchemaValidationException(object_type), m_property(property) {} + Property &property() { return m_property; } + private: + Property m_property; + }; + + class PropertyTypeNotIndexableException : public ObjectSchemaPropertyException { + public: + PropertyTypeNotIndexableException(std::string object_type, Property &property); + }; + + class ExtraPropertyException : public ObjectSchemaPropertyException { + public: + ExtraPropertyException(std::string object_type, Property &property); + }; + + class MissingPropertyException : public ObjectSchemaPropertyException { + public: + MissingPropertyException(std::string object_type, Property &property); + }; + + class InvalidNullabilityException : public ObjectSchemaPropertyException { + public: + InvalidNullabilityException(std::string object_type, Property &property); + }; + + class MissingObjectTypeException : public ObjectSchemaPropertyException { + public: + MissingObjectTypeException(std::string object_type, Property &property); + }; + + class DuplicatePrimaryKeysException : public ObjectSchemaValidationException { + public: + DuplicatePrimaryKeysException(std::string object_type); + }; + + class MismatchedPropertiesException : public ObjectSchemaValidationException { + public: + MismatchedPropertiesException(std::string object_type, Property &old_property, Property &new_property); + Property &old_property() { return m_old_property; } + Property &new_property() { return m_new_property; } + private: + Property m_old_property, m_new_property; + }; + + class ChangedPrimaryKeyException : public ObjectSchemaValidationException { + public: + ChangedPrimaryKeyException(std::string object_type, std::string old_primary, std::string new_primary); + std::string old_primary() { return m_old_primary; } + std::string new_primary() { return m_new_primary; } + private: + std::string m_old_primary, m_new_primary; + }; + + class InvalidPrimaryKeyException : public ObjectSchemaValidationException { + public: + InvalidPrimaryKeyException(std::string object_type, std::string primary_key); + std::string primary_key() { return m_primary_key; } + private: + std::string m_primary_key; + }; +} + +#endif /* defined(REALM_OBJECT_STORE_HPP) */ + diff --git a/src/object-store/property.hpp b/src/object-store/property.hpp new file mode 100644 index 00000000..22e6f90c --- /dev/null +++ b/src/object-store/property.hpp @@ -0,0 +1,89 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 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_PROPERTY_HPP +#define REALM_PROPERTY_HPP + +#include + +namespace realm { + enum PropertyType { + /** Integer type: NSInteger, int, long, Int (Swift) */ + PropertyTypeInt = 0, + /** Boolean type: BOOL, bool, Bool (Swift) */ + PropertyTypeBool = 1, + /** Float type: CGFloat (32bit), float, Float (Swift) */ + PropertyTypeFloat = 9, + /** Double type: CGFloat (64bit), double, Double (Swift) */ + PropertyTypeDouble = 10, + /** String type: NSString, String (Swift) */ + PropertyTypeString = 2, + /** Data type: NSData */ + PropertyTypeData = 4, + /** Any type: id, **not supported in Swift** */ + PropertyTypeAny = 6, + /** Date type: NSDate */ + PropertyTypeDate = 7, + /** Object type. See [Realm Models](http://realm.io/docs/cocoa/latest/#models) */ + PropertyTypeObject = 12, + /** Array type. See [Realm Models](http://realm.io/docs/cocoa/latest/#models) */ + PropertyTypeArray = 13, + }; + + struct Property { + public: + Property() : object_type(""), is_primary(false), is_indexed(false), is_nullable(false) {} + + std::string name; + PropertyType type; + std::string object_type; + bool is_primary; + bool is_indexed; + bool is_nullable; + + size_t table_column; + bool requires_index() { return is_primary || is_indexed; } + }; + + static inline const char *string_for_property_type(PropertyType type) { + switch (type) { + case PropertyTypeString: + return "string"; + case PropertyTypeInt: + return "int"; + case PropertyTypeBool: + return "bool"; + case PropertyTypeDate: + return "date"; + case PropertyTypeData: + return "data"; + case PropertyTypeDouble: + return "double"; + case PropertyTypeFloat: + return "float"; + case PropertyTypeAny: + return "any"; + case PropertyTypeObject: + return "object"; + case PropertyTypeArray: + return "array"; + } + } +} + +#endif /* REALM_PROPERTY_HPP */ diff --git a/src/object-store/shared_realm.cpp b/src/object-store/shared_realm.cpp new file mode 100644 index 00000000..9704b58e --- /dev/null +++ b/src/object-store/shared_realm.cpp @@ -0,0 +1,399 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 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 "shared_realm.hpp" +#include +#include + +using namespace realm; + +RealmCache Realm::s_global_cache; +std::mutex Realm::s_init_mutex; + +Realm::Config::Config(const Config& c) : path(c.path), read_only(c.read_only), in_memory(c.in_memory), schema_version(c.schema_version), encryption_key(c.encryption_key), migration_function(c.migration_function) +{ + if (c.schema) { + schema = std::make_unique(*c.schema); + } +} + +Realm::Realm(Config &config) : m_config(config), m_thread_id(std::this_thread::get_id()), m_auto_refresh(true), m_in_transaction(false) +{ + try { + if (config.read_only) { + m_read_only_group = std::make_unique(config.path, config.encryption_key.data(), Group::mode_ReadOnly); + m_group = m_read_only_group.get(); + } + else { + m_history = realm::make_client_history(config.path, config.encryption_key.data()); + SharedGroup::DurabilityLevel durability = config.in_memory ? SharedGroup::durability_MemOnly : + SharedGroup::durability_Full; + m_shared_group = std::make_unique(*m_history, durability, config.encryption_key.data()); + m_group = nullptr; + } + } + catch (util::File::PermissionDenied const& ex) { + throw RealmFileException(RealmFileException::Kind::PermissionDenied, "Unable to open a realm at path '" + config.path + + "'. Please use a path where your app has " + (config.read_only ? "read" : "read-write") + " permissions."); + } + catch (util::File::Exists const& ex) { + throw RealmFileException(RealmFileException::Kind::Exists, "Unable to open a realm at path '" + config.path + "'"); + } + catch (util::File::AccessError const& ex) { + throw RealmFileException(RealmFileException::Kind::AccessError, "Unable to open a realm at path '" + config.path + "'"); + } + catch (IncompatibleLockFile const&) { + throw RealmFileException(RealmFileException::Kind::IncompatibleLockFile, "Realm file is currently open in another process " + "which cannot share access with this process. All processes sharing a single file must be the same architecture."); + } +} + +Realm::~Realm() +{ + s_global_cache.remove(m_config.path, m_thread_id); +} + +Group *Realm::read_group() +{ + if (!m_group) { + m_group = &const_cast(m_shared_group->begin_read()); + } + return m_group; +} + +SharedRealm Realm::get_shared_realm(Config &config) +{ + SharedRealm realm = s_global_cache.get_realm(config.path); + if (realm) { + if (realm->config().read_only != config.read_only) { + throw MismatchedConfigException("Realm at path already opened with different read permissions."); + } + if (realm->config().in_memory != config.in_memory) { + throw MismatchedConfigException("Realm at path already opened with different inMemory settings."); + } + if (realm->config().encryption_key != config.encryption_key) { + throw MismatchedConfigException("Realm at path already opened with a different encryption key."); + } + if (realm->config().schema_version != config.schema_version && config.schema_version != ObjectStore::NotVersioned) { + throw MismatchedConfigException("Realm at path already opened with different schema version."); + } + // FIXME - enable schma comparison + /*if (realm->config().schema != config.schema) { + throw MismatchedConfigException("Realm at path already opened with different schema"); + }*/ + + realm->m_config.migration_function = config.migration_function; + + return realm; + } + + realm = SharedRealm(new Realm(config)); + + // we want to ensure we are only initializing a single realm at a time + std::lock_guard lock(s_init_mutex); + + uint64_t old_version = ObjectStore::get_schema_version(realm->read_group()); + if (!realm->m_config.schema) { + // get schema from group and skip validation + realm->m_config.schema_version = old_version; + realm->m_config.schema = std::make_unique(ObjectStore::schema_from_group(realm->read_group())); + } + else if (realm->m_config.read_only) { + if (old_version == ObjectStore::NotVersioned) { + throw UnitializedRealmException("Can't open an un-initizliazed Realm without a Schema"); + } + ObjectStore::verify_schema(realm->read_group(), *realm->m_config.schema, true); + } + else if(auto existing = s_global_cache.get_any_realm(realm->config().path)) { + // if there is an existing realm at the current path steal its schema/column mapping + // FIXME - need to validate that schemas match + realm->m_config.schema = std::make_unique(*existing->m_config.schema); + } + else { + // its a non-cached realm so update/migrate if needed + realm->update_schema(*realm->m_config.schema, realm->m_config.schema_version); + } + + s_global_cache.cache_realm(realm, realm->m_thread_id); + return realm; +} + +bool Realm::update_schema(Schema &schema, uint64_t version) +{ + bool changed = false; + Config old_config(m_config); + + // set new version/schema + if (m_config.schema.get() != &schema) { + m_config.schema = std::make_unique(schema); + } + m_config.schema_version = version; + + try { + if (!m_config.read_only && ObjectStore::realm_requires_update(read_group(), version, schema)) { + // keep old copy to pass to migration function + old_config.read_only = true; + SharedRealm old_realm = SharedRealm(new Realm(old_config)), updated_realm = shared_from_this(); + + // update and migrate + begin_transaction(); + changed = ObjectStore::update_realm_with_schema(read_group(), version, *m_config.schema, [=](__unused Group *group, __unused Schema &target_schema) { + m_config.migration_function(old_realm, updated_realm); + }); + commit_transaction(); + } + else { + ObjectStore::verify_schema(read_group(), *m_config.schema, m_config.read_only); + } + } + catch (...) { + if (is_in_transaction()) { + cancel_transaction(); + } + m_config.schema_version = old_config.schema_version; + m_config.schema = std::move(old_config.schema); + throw; + } + return changed; +} + +static void check_read_write(Realm *realm) +{ + if (realm->config().read_only) { + throw InvalidTransactionException("Can't perform transactions on read-only Realms."); + } +} + +void Realm::verify_thread() +{ + if (m_thread_id != std::this_thread::get_id()) { + throw IncorrectThreadException("Realm accessed from incorrect thread."); + } +} + +void Realm::begin_transaction() +{ + check_read_write(this); + verify_thread(); + + if (m_in_transaction) { + throw InvalidTransactionException("The Realm is already in a write transaction"); + } + + // if the upgrade to write will move the transaction forward, announce the change after promoting + bool announce = m_shared_group->has_changed(); + + // make sure we have a read transaction + read_group(); + + LangBindHelper::promote_to_write(*m_shared_group, *m_history); + m_in_transaction = true; + + if (announce) { + send_local_notifications(DidChangeNotification); + } +} + +void Realm::commit_transaction() +{ + check_read_write(this); + verify_thread(); + + if (!m_in_transaction) { + throw InvalidTransactionException("Can't commit a non-existing write transaction"); + } + + LangBindHelper::commit_and_continue_as_read(*m_shared_group); + m_in_transaction = false; + + send_external_notifications(); + send_local_notifications(DidChangeNotification); +} + +void Realm::cancel_transaction() +{ + check_read_write(this); + verify_thread(); + + if (!m_in_transaction) { + throw InvalidTransactionException("Can't cancel a non-existing write transaction"); + } + + LangBindHelper::rollback_and_continue_as_read(*m_shared_group, *m_history); + m_in_transaction = false; +} + + +void Realm::invalidate() +{ + verify_thread(); + check_read_write(this); + + if (m_in_transaction) { + cancel_transaction(); + } + if (!m_group) { + return; + } + + m_shared_group->end_read(); + m_group = nullptr; +} + +bool Realm::compact() +{ + verify_thread(); + + bool success = false; + if (m_in_transaction) { + throw InvalidTransactionException("Can't compact a Realm within a write transaction"); + } + + for (auto &object_schema : *m_config.schema) { + ObjectStore::table_for_object_type(read_group(), object_schema.first)->optimize(); + } + + m_shared_group->end_read(); + success = m_shared_group->compact(); + m_shared_group->begin_read(); + + return success; +} + +void Realm::notify() +{ + verify_thread(); + + if (m_shared_group->has_changed()) { // Throws + if (m_auto_refresh) { + if (m_group) { + LangBindHelper::advance_read(*m_shared_group, *m_history); + } + send_local_notifications(DidChangeNotification); + } + else { + send_local_notifications(RefreshRequiredNotification); + } + } +} + + +void Realm::send_local_notifications(const std::string &type) +{ + verify_thread(); + for (NotificationFunction notification : m_notifications) { + (*notification)(type); + } +} + + +bool Realm::refresh() +{ + verify_thread(); + check_read_write(this); + + // can't be any new changes if we're in a write transaction + if (m_in_transaction) { + return false; + } + + // advance transaction if database has changed + if (!m_shared_group->has_changed()) { // Throws + return false; + } + + if (m_group) { + LangBindHelper::advance_read(*m_shared_group, *m_history); + } + else { + // Create the read transaction + read_group(); + } + + send_local_notifications(DidChangeNotification); + return true; +} + +SharedRealm RealmCache::get_realm(const std::string &path, std::thread::id thread_id) +{ + std::lock_guard lock(m_mutex); + + auto path_iter = m_cache.find(path); + if (path_iter == m_cache.end()) { + return SharedRealm(); + } + + auto thread_iter = path_iter->second.find(thread_id); + if (thread_iter == path_iter->second.end()) { + return SharedRealm(); + } + + return thread_iter->second.lock(); +} + +SharedRealm RealmCache::get_any_realm(const std::string &path) +{ + std::lock_guard lock(m_mutex); + + auto path_iter = m_cache.find(path); + if (path_iter == m_cache.end()) { + return SharedRealm(); + } + + for (auto thread_iter = path_iter->second.begin(); thread_iter != path_iter->second.end(); thread_iter++) { + if (auto realm = thread_iter->second.lock()) { + return realm; + } + path_iter->second.erase(thread_iter); + } + + return SharedRealm(); +} + +void RealmCache::remove(const std::string &path, std::thread::id thread_id) +{ + std::lock_guard lock(m_mutex); + + auto path_iter = m_cache.find(path); + if (path_iter == m_cache.end()) { + return; + } + + auto thread_iter = path_iter->second.find(thread_id); + if (thread_iter != path_iter->second.end()) { + path_iter->second.erase(thread_iter); + } + + if (path_iter->second.size() == 0) { + m_cache.erase(path_iter); + } +} + +void RealmCache::cache_realm(SharedRealm &realm, std::thread::id thread_id) +{ + std::lock_guard lock(m_mutex); + + auto path_iter = m_cache.find(realm->config().path); + if (path_iter == m_cache.end()) { + m_cache.emplace(realm->config().path, std::map{{thread_id, realm}}); + } + else { + path_iter->second.emplace(thread_id, realm); + } +} + diff --git a/src/object-store/shared_realm.hpp b/src/object-store/shared_realm.hpp new file mode 100644 index 00000000..2807549d --- /dev/null +++ b/src/object-store/shared_realm.hpp @@ -0,0 +1,194 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2015 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_REALM_HPP +#define REALM_REALM_HPP + +#include +#include +#include +#include +#include +#include + +#include "object_store.hpp" + +namespace realm { + class RealmCache; + class Realm; + typedef std::shared_ptr SharedRealm; + typedef std::weak_ptr WeakRealm; + class ClientHistory; + + class Realm : public std::enable_shared_from_this + { + public: + typedef std::function MigrationFunction; + + struct Config + { + std::string path; + bool read_only; + bool in_memory; + StringData encryption_key; + + std::unique_ptr schema; + uint64_t schema_version; + + MigrationFunction migration_function; + + Config() : read_only(false), in_memory(false), schema_version(ObjectStore::NotVersioned) {}; + Config(const Config& c); + }; + + // Get a cached Realm or create a new one if no cached copies exists + // Caching is done by path - mismatches for inMemory and readOnly Config properties + // will raise an exception + // If schema/schema_version is specified, update_schema is called automatically on the realm + // and a migration is performed. If not specified, the schema version and schema are dynamically + // read from the the existing Realm. + static SharedRealm get_shared_realm(Config &config); + + // Updates a Realm to a given target schema/version creating tables and updating indexes as necessary + // Uses the existing migration function on the Config, and the resulting Schema and version with updated + // column mappings are set on the realms config upon success. + // returns if any changes were made + bool update_schema(Schema &schema, uint64_t version); + + const Config &config() const { return m_config; } + + void begin_transaction(); + void commit_transaction(); + void cancel_transaction(); + bool is_in_transaction() { return m_in_transaction; } + + bool refresh(); + void set_auto_refresh(bool auto_refresh) { m_auto_refresh = auto_refresh; } + bool auto_refresh() { return m_auto_refresh; } + void notify(); + + typedef std::shared_ptr> NotificationFunction; + void add_notification(NotificationFunction ¬ification) { m_notifications.insert(notification); } + void remove_notification(NotificationFunction notification) { m_notifications.erase(notification); } + void remove_all_notifications() { m_notifications.clear(); } + + void invalidate(); + bool compact(); + + std::thread::id thread_id() const { return m_thread_id; } + void verify_thread(); + + const std::string RefreshRequiredNotification = "RefreshRequiredNotification"; + const std::string DidChangeNotification = "DidChangeNotification"; + + private: + Realm(Config &config); + + Config m_config; + std::thread::id m_thread_id; + bool m_in_transaction; + bool m_auto_refresh; + + std::set m_notifications; + void send_local_notifications(const std::string ¬ification); + + typedef std::unique_ptr> ExternalNotificationFunction; + void send_external_notifications() { if (m_external_notifier) (*m_external_notifier)(); } + + std::unique_ptr m_history; + std::unique_ptr m_shared_group; + std::unique_ptr m_read_only_group; + + Group *m_group; + + static std::mutex s_init_mutex; + static RealmCache s_global_cache; + + public: + ~Realm(); + ExternalNotificationFunction m_external_notifier; + + // FIXME private + Group *read_group(); + }; + + class RealmCache + { + public: + SharedRealm get_realm(const std::string &path, std::thread::id thread_id = std::this_thread::get_id()); + SharedRealm get_any_realm(const std::string &path); + void remove(const std::string &path, std::thread::id thread_id); + void cache_realm(SharedRealm &realm, std::thread::id thread_id = std::this_thread::get_id()); + + private: + std::map> m_cache; + std::mutex m_mutex; + }; + + class RealmFileException : public std::runtime_error + { + public: + enum class Kind + { + /** Thrown for any I/O related exception scenarios when a realm is opened. */ + AccessError, + /** Thrown if the user does not have permission to open or create + the specified file in the specified access mode when the realm is opened. */ + PermissionDenied, + /** Thrown if no_create was specified and the file did already exist when the realm is opened. */ + Exists, + /** Thrown if no_create was specified and the file was not found when the realm is opened. */ + NotFound, + /** Thrown if the database file is currently open in another + process which cannot share with the current process due to an + architecture mismatch. */ + IncompatibleLockFile, + }; + RealmFileException(Kind kind, std::string message) : std::runtime_error(message), m_kind(kind) {} + Kind kind() const { return m_kind; } + + private: + Kind m_kind; + }; + + class MismatchedConfigException : public std::runtime_error + { + public: + MismatchedConfigException(std::string message) : std::runtime_error(message) {} + }; + + class InvalidTransactionException : public std::runtime_error + { + public: + InvalidTransactionException(std::string message) : std::runtime_error(message) {} + }; + + class IncorrectThreadException : public std::runtime_error + { + public: + IncorrectThreadException(std::string message) : std::runtime_error(message) {} + }; + + class UnitializedRealmException : public std::runtime_error + { + public: + UnitializedRealmException(std::string message) : std::runtime_error(message) {} + }; +} + +#endif /* defined(REALM_REALM_HPP) */