From a54d2216f92f89a7641ef6024f7ba25794092141 Mon Sep 17 00:00:00 2001 From: Ari Lazier Date: Wed, 3 Jun 2015 17:27:21 -0700 Subject: [PATCH] refact cocoa to use c++ table creation apis --- object_schema.cpp | 82 +++++++++++++++++++ object_schema.hpp | 49 +++++++++++ object_store.cpp | 204 +++++++++++++++++++++++++++++++++++++++++++++- object_store.hpp | 62 ++++++++++++-- property.cpp | 21 +++++ property.hpp | 85 +++++++++++++++++++ 6 files changed, 496 insertions(+), 7 deletions(-) create mode 100644 object_schema.cpp create mode 100644 object_schema.hpp create mode 100644 property.cpp create mode 100644 property.hpp diff --git a/object_schema.cpp b/object_schema.cpp new file mode 100644 index 00000000..12d61209 --- /dev/null +++ b/object_schema.cpp @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2014 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" + +using namespace realm; +using namespace std; + +ObjectSchema::ObjectSchema(realm::Group *group, std::string name) : name(name) { + TableRef table = ObjectStore::table_for_object_type(group, name); + size_t count = table->get_column_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.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::class_for_table_name(linkTable->get_name().data()); + } + else { + property.object_type = ""; + } + properties.push_back(property); + } + + primary_key = realm::ObjectStore::get_primary_key_for_object(group, name); + if (primary_key.length()) { + auto primary_key_iter = primary_key_property(); + if (primary_key_iter == properties.end()) { + std::vector errors; + errors.push_back("No property matching primary key '" + primary_key + "'"); + throw ObjectStoreValidationException(errors, name); + } + primary_key_iter->is_primary = true; + } +} + +std::vector::iterator ObjectSchema::property_for_name(std::string name) { + auto iter = properties.begin(); + while (iter < properties.end()) { + if (iter->name == name) { + return iter; + } + iter++; + } + return iter; +} + +std::vector ObjectSchema::object_schema_from_group(realm::Group *group) { + // generate object schema and class mapping for all tables in the realm + unsigned long numTables = group->size(); + vector object_schema; + + for (unsigned long i = 0; i < numTables; i++) { + std::string name = ObjectStore::class_for_table_name(group->get_table_name(i).data()); + if (name.length()) { + object_schema.push_back(ObjectSchema(group, name)); + } + } + + return object_schema; +} diff --git a/object_schema.hpp b/object_schema.hpp new file mode 100644 index 00000000..7d962018 --- /dev/null +++ b/object_schema.hpp @@ -0,0 +1,49 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2014 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__ +#define __realm__object_schema__ + +#include +#include + +#include "property.hpp" +#include + +namespace realm { + class ObjectSchema { + public: + ObjectSchema() {} + ObjectSchema(Group *group, std::string name); + + static std::vector object_schema_from_group(Group *group); + + std::string name; + std::vector properties; + std::string primary_key; + + std::vector::iterator property_for_name(std::string name); + std::vector::iterator primary_key_property() { + return property_for_name(primary_key); + } + }; + + typedef std::shared_ptr ObjectSchemaRef; +} + +#endif /* defined(__realm__object_schema__) */ diff --git a/object_store.cpp b/object_store.cpp index f4ea0f30..7fc969d1 100644 --- a/object_store.cpp +++ b/object_store.cpp @@ -19,6 +19,7 @@ #include "object_store.hpp" using namespace realm; +using namespace std; const char * const c_metadataTableName = "metadata"; const char * const c_versionColumnName = "version"; @@ -30,7 +31,9 @@ const size_t c_primaryKeyObjectClassColumnIndex = 0; const char * const c_primaryKeyPropertyNameColumnName = "pk_property"; const size_t c_primaryKeyPropertyNameColumnIndex = 1; -const uint64_t ObjectStore::NotVersioned = std::numeric_limits::max(); +const string c_object_table_name_prefix = "class_"; + +const uint64_t ObjectStore::NotVersioned = numeric_limits::max(); bool ObjectStore::has_metadata_tables(realm::Group *group) { return group->get_table(c_primaryKeyTableName) && group->get_table(c_metadataTableName); @@ -102,3 +105,202 @@ void ObjectStore::set_primary_key_for_object(realm::Group *group, StringData obj } } +string ObjectStore::class_for_table_name(string table_name) { + if (table_name.compare(0, 6, c_object_table_name_prefix) == 0) { + return table_name.substr(6, table_name.length()-6); + } + return string(); +} + +string ObjectStore::table_name_for_class(string class_name) { + return c_object_table_name_prefix + class_name; +} + +realm::TableRef ObjectStore::table_for_object_type(realm::Group *group, StringData object_type) { + return group->get_table(table_name_for_class(object_type)); +} + +realm::TableRef ObjectStore::table_for_object_type_create_if_needed(realm::Group *group, StringData object_type, bool &created) { + return group->get_or_add_table(table_name_for_class(object_type), &created); +} + +std::vector ObjectStore::validate_and_update_column_mapping(realm::Group *group, ObjectSchema &target_schema) { + vector validation_errors; + ObjectSchema table_schema(group, target_schema.name); + + // check to see if properties are the same + for (auto current_prop = table_schema.properties.begin(); current_prop != table_schema.properties.end(); current_prop++) { + auto target_prop = target_schema.property_for_name(current_prop->name); + + if (target_prop == target_schema.properties.end()) { + validation_errors.push_back("Property '" + current_prop->name + "' is missing from latest object model."); + continue; + } + + if (current_prop->type != target_prop->type) { + validation_errors.push_back("Property types for '" + target_prop->name + "' property do not match. " + + "Old type '" + string_for_property_type(current_prop->type) + + "', new type '" + string_for_property_type(target_prop->type) + "'"); + continue; + } + if (current_prop->type == PropertyTypeObject || target_prop->type == PropertyTypeArray) { + if (current_prop->object_type != target_prop->object_type) { + validation_errors.push_back("Target object type for property '" + current_prop->name + "' does not match. " + + "Old type '" + current_prop->object_type + + "', new type '" + target_prop->object_type + "'."); + } + } + if (current_prop->is_primary != target_prop->is_primary) { + if (current_prop->is_primary) { + validation_errors.push_back("Property '" + current_prop->name + "' is no longer a primary key."); + } + else { + validation_errors.push_back("Property '" + current_prop->name + "' has been made a primary key."); + } + } + + // create new property with aligned column + target_prop->table_column = current_prop->table_column; + } + + // check for new missing properties + for (auto target_iter = target_schema.properties.begin(); target_iter != target_schema.properties.end(); target_iter++) { + if (table_schema.property_for_name(target_iter->name) == table_schema.properties.end()) { + validation_errors.push_back("Property '" + target_iter->name + "' has been added to latest object model."); + } + } + + return validation_errors; +} + +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; +} + +// 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 +static inline bool create_tables(realm::Group *group, ObjectStore::Schema target_schema, bool update_existing) { + // create metadata tables if neded + bool changed = realm::ObjectStore::create_metadata_tables(group); + + // first pass to create missing tables + vector to_update; + for (size_t i = 0; i < target_schema.size(); i++) { + ObjectSchema *object_schema = target_schema[i].get(); + bool created = false; + ObjectStore::table_for_object_type_create_if_needed(group, object_schema->name, 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); + changed = true; + } + } + + // second pass adds/removes columns for out of date tables + for (size_t i = 0; i < to_update.size(); i++) { + ObjectSchema *target_schema = to_update[i]; + TableRef table = ObjectStore::table_for_object_type(group, target_schema->name); + + ObjectSchema current_schema(group, target_schema->name); + vector &target_props = target_schema->properties; + + // add missing columns + for (auto target_prop = target_props.begin(); target_prop < target_props.end(); target_prop++) { + auto current_prop = current_schema.property_for_name(target_prop->name); + + // add any new properties (new name or different type) + if (current_prop == current_schema.properties.end() || 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: { + realm::TableRef link_table = ObjectStore::table_for_object_type(group, target_prop->object_type); + target_prop->table_column = table->add_column_link(realm::DataType(target_prop->type), target_prop->name, *link_table); + break; + } + default: + target_prop->table_column = table->add_column(realm::DataType(target_prop->type), target_prop->name); + break; + } + changed = true; + } + } + + // remove extra columns + vector reverse_props = current_schema.properties; + std::sort(reverse_props.begin(), reverse_props.end(), [](Property &i, Property &j){ return (j.table_column < i.table_column); }); + for (auto iter = reverse_props.begin(); iter != reverse_props.end(); iter++) { + auto target_prop_iter = target_schema->property_for_name(iter->name); + if (target_prop_iter == target_props.end() || property_has_changed(*iter, *target_prop_iter)) { + table->remove_column(iter->table_column); + changed = true; + } + } + + // update table metadata + if (target_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.length() || current_schema.primary_key != target_schema->primary_key) { + realm::ObjectStore::set_primary_key_for_object(group, target_schema->name, target_schema->primary_key); + changed = true; + } + } + else if (current_schema.primary_key.length()) { + // there is no primary key, so if there was one nil out + realm::ObjectStore::set_primary_key_for_object(group, target_schema->name, ""); + changed = true; + } + } + + return changed; +} + +bool ObjectStore::is_migration_required(realm::Group *group, uint64_t new_version) { + uint64_t old_version = get_schema_version(group); + if (old_version > new_version && old_version != realm::ObjectStore::NotVersioned) { + throw ObjectStoreException(ObjectStoreException::RealmVersionGreaterThanSchemaVersion); + } + return old_version != new_version; +} + + +bool ObjectStore::update_realm_with_schema(realm::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_migration_required(group, version); + + // create tables + bool changed = create_tables(group, schema, migrating); + for (size_t i = 0; i < schema.size(); i++) { + ObjectSchema *target_schema = schema[i].get(); + TableRef table = table_for_object_type(group, target_schema->name); + + // read-only realms may be missing tables entirely + if (table) { + auto errors = validate_and_update_column_mapping(group, *target_schema); + if (errors.size()) { + throw ObjectStoreValidationException(errors, target_schema->name); + } + } + } + + if (!migrating) { + return changed; + } + + // apply the migration block if provided and there's any old data + // to be migrated + if (get_schema_version(group) != realm::ObjectStore::NotVersioned) { + migration(); + } + + set_schema_version(group, version); + return true; +} + diff --git a/object_store.hpp b/object_store.hpp index 0f71dc27..19fbe772 100644 --- a/object_store.hpp +++ b/object_store.hpp @@ -20,6 +20,7 @@ #define __realm__object_store__ #include +#include "object_schema.hpp" namespace realm { class ObjectStore { @@ -28,25 +29,74 @@ namespace realm { static const uint64_t NotVersioned; // check if the realm already has all metadata tables - static bool has_metadata_tables(realm::Group *group); + 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(realm::Group *group); + static bool create_metadata_tables(Group *group); // get the last set schema version - static uint64_t get_schema_version(realm::Group *group); + static uint64_t get_schema_version(Group *group); // set a new schema version - static void set_schema_version(realm::Group *group, uint64_t version); + static void set_schema_version(Group *group, uint64_t version); // get primary key property name for object type - static StringData get_primary_key_for_object(realm::Group *group, StringData 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(realm::Group *group, StringData object_type, StringData primary_key); + static void set_primary_key_for_object(Group *group, StringData object_type, StringData primary_key); + + // verify a target schema against its table, setting the table_column property on each schema object + // returns array of validation errors + static std::vector validate_and_update_column_mapping(Group *group, ObjectSchema &target_schema); + + // get the table used to store object of objectClass + static std::string table_name_for_class(std::string class_name); + static std::string class_for_table_name(std::string table_name); + static realm::TableRef table_for_object_type(Group *group, StringData object_type); + static realm::TableRef table_for_object_type_create_if_needed(Group *group, StringData object_type, bool &created); + + static bool is_migration_required(realm::Group *group, uint64_t new_version); + + // updates a Realm to a given target schema/version creating tables 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; + typedef std::vector Schema; + static bool update_realm_with_schema(Group *group, + uint64_t version, + Schema schema, + MigrationFunction migration); + }; + + class ObjectStoreException : public std::exception { + public: + enum Kind { + // thrown when calling update_realm_to_schema and the realm version is greater than the given version + RealmVersionGreaterThanSchemaVersion, + }; + ObjectStoreException(Kind kind) : m_kind(kind) {} + ObjectStoreException::Kind kind() { return m_kind; } + + private: + Kind m_kind; + }; + + class ObjectStoreValidationException : public std::exception { + public: + ObjectStoreValidationException(std::vector validation_errors, std::string object_type) : + m_validation_errors(validation_errors), m_object_type(object_type) {} + std::vector validation_errors() { return m_validation_errors; } + std::string object_type() { return m_object_type; } + + private: + std::vector m_validation_errors; + std::string m_object_type; }; } diff --git a/property.cpp b/property.cpp new file mode 100644 index 00000000..44dfa8fa --- /dev/null +++ b/property.cpp @@ -0,0 +1,21 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2014 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 "property.hpp" + +using namespace realm; diff --git a/property.hpp b/property.hpp new file mode 100644 index 00000000..a9a1d145 --- /dev/null +++ b/property.hpp @@ -0,0 +1,85 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2014 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__ +#define __realm__property__ + +#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, + }; + + class Property { + public: + std::string name; + PropertyType type; + std::string object_type; + bool is_primary; + bool is_indexed; + + size_t table_column; + }; + + 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 /* defined__realm__property__ */