Merge pull request #383 from realm/al-migration

Basic data migrations
This commit is contained in:
Ari Lazier 2016-04-28 15:40:27 -07:00
commit a2ed550dde
10 changed files with 360 additions and 23 deletions

View File

@ -147,6 +147,7 @@ class Realm {
// properties
static void get_path(ContextType, ObjectType, ReturnValue &);
static void get_schema_version(ContextType, ObjectType, ReturnValue &);
static void get_schema(ContextType, ObjectType, ReturnValue &);
// static methods
static void constructor(ContextType, ObjectType, size_t, const ValueType[]);
@ -222,6 +223,7 @@ struct RealmClass : ClassDefinition<T, SharedRealm> {
PropertyMap<T> const properties = {
{"path", {wrap<Realm::get_path>, nullptr}},
{"schemaVersion", {wrap<Realm::get_schema_version>, nullptr}},
{"schema", {wrap<Realm::get_schema>, nullptr}},
};
};
@ -248,6 +250,7 @@ void Realm<T>::constructor(ContextType ctx, ObjectType this_object, size_t argc,
static const String schema_string = "schema";
static const String schema_version_string = "schemaVersion";
static const String encryption_key_string = "encryptionKey";
static const String migration_string = "migration";
realm::Realm::Config config;
typename Schema<T>::ObjectDefaultsMap defaults;
@ -286,6 +289,18 @@ void Realm<T>::constructor(ContextType ctx, ObjectType this_object, size_t argc,
config.schema_version = 0;
}
ValueType migration_value = Object::get_property(ctx, object, migration_string);
if (!Value::is_undefined(ctx, migration_value)) {
FunctionType migration_function = Value::validated_to_function(ctx, migration_value, "migration");
config.migration_function = [=](SharedRealm old_realm, SharedRealm realm) {
ValueType arguments[2] = {
create_object<T, RealmClass<T>>(ctx, new SharedRealm(old_realm)),
create_object<T, RealmClass<T>>(ctx, new SharedRealm(realm))
};
Function<T>::call(ctx, migration_function, 2, arguments);
};
}
ValueType encryption_key_value = Object::get_property(ctx, object, encryption_key_string);
if (!Value::is_undefined(ctx, encryption_key_value)) {
std::string encryption_key = NativeAccessor::to_binary(ctx, encryption_key_value);
@ -362,6 +377,12 @@ void Realm<T>::get_schema_version(ContextType ctx, ObjectType object, ReturnValu
return_value.set(version);
}
template<typename T>
void Realm<T>::get_schema(ContextType ctx, ObjectType object, ReturnValue &return_value) {
auto schema = get_internal<T, RealmClass<T>>(object)->get()->config().schema.get();
return_value.set(Schema<T>::object_for_schema(ctx, *schema));
}
template<typename T>
void Realm<T>::objects(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
validate_argument_count(argc, 1);

View File

@ -80,7 +80,7 @@ typename T::Object RealmObject<T>::create_instance(ContextType ctx, realm::Objec
auto name = realm_object.get_object_schema().name;
auto object = create_object<T, RealmObjectClass<T>>(ctx, new realm::Object(realm_object));
if (!delegate->m_constructors.count(name)) {
if (!delegate || !delegate->m_constructors.count(name)) {
return object;
}

View File

@ -44,6 +44,10 @@ struct Schema {
static Property parse_property(ContextType, ValueType, std::string, ObjectDefaults &);
static ObjectSchema parse_object_schema(ContextType, ObjectType, ObjectDefaultsMap &, ConstructorMap &);
static realm::Schema parse_schema(ContextType, ObjectType, ObjectDefaultsMap &, ConstructorMap &);
static ObjectType object_for_schema(ContextType, const realm::Schema &);
static ObjectType object_for_object_schema(ContextType, const ObjectSchema &);
static ObjectType object_for_property(ContextType, const Property &);
};
template<typename T>
@ -218,5 +222,67 @@ realm::Schema Schema<T>::parse_schema(ContextType ctx, ObjectType schema_object,
return realm::Schema(schema);
}
template<typename T>
typename T::Object Schema<T>::object_for_schema(ContextType ctx, const realm::Schema &schema) {
ObjectType object = Object::create_array(ctx);
uint32_t count = 0;
for (auto& object_schema : schema) {
Object::set_property(ctx, object, count++, object_for_object_schema(ctx, object_schema));
}
return object;
}
template<typename T>
typename T::Object Schema<T>::object_for_object_schema(ContextType ctx, const ObjectSchema &object_schema) {
ObjectType object = Object::create_empty(ctx);
static const String name_string = "name";
Object::set_property(ctx, object, name_string, Value::from_string(ctx, object_schema.name));
ObjectType properties = Object::create_empty(ctx);
for (auto& property : object_schema.properties) {
Object::set_property(ctx, properties, property.name, object_for_property(ctx, property));
}
static const String properties_string = "properties";
Object::set_property(ctx, object, properties_string, properties);
static const String primary_key_string = "primaryKey";
if (object_schema.primary_key.size()) {
Object::set_property(ctx, object, primary_key_string, Value::from_string(ctx, object_schema.primary_key));
}
return object;
}
template<typename T>
typename T::Object Schema<T>::object_for_property(ContextType ctx, const Property &property) {
ObjectType object = Object::create_empty(ctx);
static const String name_string = "name";
Object::set_property(ctx, object, name_string, Value::from_string(ctx, property.name));
static const String type_string = "type";
const std::string type = property.type != PropertyTypeArray ? string_for_property_type(property.type) : "list";
Object::set_property(ctx, object, type_string, Value::from_string(ctx, type));
static const String object_type_string = "objectType";
if (property.object_type.size()) {
Object::set_property(ctx, object, object_type_string, Value::from_string(ctx, property.object_type));
}
static const String indexed_string = "indexed";
if (property.is_indexed) {
Object::set_property(ctx, object, indexed_string, Value::from_boolean(ctx, true));
}
static const String optional_string = "optional";
if (property.is_nullable) {
Object::set_property(ctx, object, optional_string, Value::from_boolean(ctx, true));
}
return object;
}
} // js
} // realm

View File

@ -129,6 +129,9 @@ struct Function {
using ValueType = typename T::Value;
static ValueType call(ContextType, const FunctionType &, const ObjectType &, size_t, const ValueType[]);
static ValueType call(ContextType ctx, const FunctionType &function, size_t argument_count, const ValueType arguments[]) {
return call(ctx, function, {}, argument_count, arguments);
}
static ValueType call(ContextType ctx, const FunctionType &function, const ObjectType &this_object, const std::vector<ValueType> &arguments) {
return call(ctx, function, this_object, arguments.size(), arguments.data());
}

View File

@ -26,7 +26,9 @@ namespace js {
template<>
inline v8::Local<v8::Value> node::Function::call(v8::Isolate* isolate, const v8::Local<v8::Function> &function, const v8::Local<v8::Object> &this_object, size_t argc, const v8::Local<v8::Value> arguments[]) {
Nan::TryCatch trycatch;
auto result = Nan::Call(function, this_object, (int)argc, const_cast<v8::Local<v8::Value>*>(arguments));
auto recv = this_object.IsEmpty() ? isolate->GetCurrentContext()->Global() : this_object;
auto result = Nan::Call(function, recv, (int)argc, const_cast<v8::Local<v8::Value>*>(arguments));
if (trycatch.HasCaught()) {
throw node::Exception(isolate, trycatch.Exception());

View File

@ -87,6 +87,23 @@ module.exports = {
}
},
assertThrowsException: function(func, expectedException) {
var caught = false;
try {
func();
}
catch (e) {
caught = true;
if (e != expectedException) {
throw new TestFailureError('Expected exception "' + expectedException + '" not thrown - instead caught: "' + e + '"');
}
}
if (!caught) {
throw new TestFailureError('Expected exception not thrown');
}
},
assertTrue: function(condition, errorMessage) {
if (!condition) {
throw new TestFailureError(errorMessage || 'Condition expected to be true');

View File

@ -25,6 +25,7 @@ var TESTS = {
ResultsTests: require('./results-tests'),
QueryTests: require('./query-tests'),
EncryptionTests: require('./encryption-tests'),
MigrationTests: require('./migration-tests'),
};
var SPECIAL_METHODS = {

162
tests/js/migration-tests.js Normal file
View File

@ -0,0 +1,162 @@
////////////////////////////////////////////////////////////////////////////
//
// 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.
//
////////////////////////////////////////////////////////////////////////////
'use strict';
var Realm = require('realm');
var BaseTest = require('./base-test');
var TestCase = require('./asserts');
var Schemas = require('./schemas');
module.exports = BaseTest.extend({
testMigrationFunction: function() {
var count = 0;
function migrationFunction(oldRealm, newRealm) {
TestCase.assertEqual(oldRealm.schemaVersion, 0);
TestCase.assertEqual(newRealm.schemaVersion, 1);
count++;
}
// no migration should be run
var realm = new Realm({schema: [], migration: migrationFunction});
TestCase.assertEqual(0, count);
realm.close();
// migration should be run
realm = new Realm({schema: [Schemas.TestObject], migration: migrationFunction, schemaVersion: 1});
TestCase.assertEqual(1, count);
realm.close();
// invalid migration function
TestCase.assertThrows(function() {
new Realm({schema: [], schemaVersion: 2, migration: 'invalid'});
});
// migration function exceptions should propogate
var exception = 'expected exception';
realm = undefined;
TestCase.assertThrowsException(function() {
realm = new Realm({schema: [], schemaVersion: 3, migration: function() {
throw exception;
}});
}, exception);
TestCase.assertEqual(realm, undefined);
TestCase.assertEqual(Realm.schemaVersion(Realm.defaultPath), 1);
// migration function shouldn't run if nothing changes
realm = new Realm({schema: [Schemas.TestObject], migration: migrationFunction, schemaVersion: 1});
TestCase.assertEqual(1, count);
realm.close();
// migration function should run if only schemaVersion changes
realm = new Realm({schema: [Schemas.TestObject], migration: function() { count++; }, schemaVersion: 2});
TestCase.assertEqual(2, count);
realm.close();
},
testDataMigration: function() {
var realm = new Realm({schema: [{
name: 'TestObject',
properties: {
prop0: 'string',
prop1: 'int',
}
}]});
realm.write(function() {
realm.create('TestObject', ['stringValue', 1]);
});
realm.close();
var realm = new Realm({
schema: [{
name: 'TestObject',
properties: {
renamed: 'string',
prop1: 'int',
}
}],
schemaVersion: 1,
migration: function(oldRealm, newRealm) {
var oldObjects = oldRealm.objects('TestObject');
var newObjects = newRealm.objects('TestObject');
TestCase.assertEqual(oldObjects.length, 1);
TestCase.assertEqual(newObjects.length, 1);
TestCase.assertEqual(oldObjects[0].prop0, 'stringValue');
TestCase.assertEqual(oldObjects[0].prop1, 1);
TestCase.assertEqual(oldObjects[0].renamed, undefined);
TestCase.assertEqual(newObjects[0].prop0, undefined);
TestCase.assertEqual(newObjects[0].renamed, '');
TestCase.assertEqual(newObjects[0].prop1, 1);
newObjects[0].renamed = oldObjects[0].prop0;
TestCase.assertThrows(function() {
oldObjects[0].prop0 = 'throws';
});
}
});
var objects = realm.objects('TestObject');
TestCase.assertEqual(objects.length, 1);
TestCase.assertEqual(objects[0].renamed, 'stringValue');
TestCase.assertEqual(objects[0].prop1, 1);
TestCase.assertEqual(objects[0].prop0, undefined);
},
testMigrationSchema: function() {
var realm = new Realm({schema: [{
name: 'TestObject',
properties: {
prop0: 'string',
prop1: 'int',
}
}]});
realm.close();
var realm = new Realm({
schema: [{
name: 'TestObject',
properties: {
renamed: 'string',
prop1: 'int',
}
}],
schemaVersion: 1,
migration: function(oldRealm, newRealm) {
var oldSchema = oldRealm.schema;
var newSchema = newRealm.schema;
TestCase.assertEqual(oldSchema.length, 1);
TestCase.assertEqual(newSchema.length, 1);
TestCase.assertEqual(oldSchema[0].name, 'TestObject');
TestCase.assertEqual(newSchema[0].name, 'TestObject');
TestCase.assertEqual(oldSchema[0].properties.prop0.type, 'string');
TestCase.assertEqual(newSchema[0].properties.prop0, undefined);
TestCase.assertEqual(oldSchema[0].properties.prop1.type, 'int');
TestCase.assertEqual(newSchema[0].properties.prop1.type, 'int');
TestCase.assertEqual(oldSchema[0].properties.renamed, undefined);
TestCase.assertEqual(newSchema[0].properties.renamed.type, 'string');
}
});
},
});

View File

@ -84,6 +84,19 @@ module.exports = BaseTest.extend({
TestCase.assertEqual(realm.objects('TestObject')[0].doubleCol, 1)
},
testRealmConstructorDynamicSchema: function() {
var realm = new Realm({schema: [schemas.TestObject]});
realm.write(function() {
realm.create('TestObject', [1])
});
realm.close();
realm = new Realm();
var objects = realm.objects('TestObject');
TestCase.assertEqual(objects.length, 1);
TestCase.assertEqual(objects[0].doubleCol, 1.0);
},
testRealmConstructorSchemaValidation: function() {
TestCase.assertThrows(function() {
new Realm({schema: schemas.AllTypes});
@ -323,17 +336,7 @@ module.exports = BaseTest.extend({
},
testRealmWithIndexedProperties: function() {
var IndexedTypes = {
name: 'IndexedTypesObject',
properties: {
boolCol: {type: 'bool', indexed: true},
intCol: {type: 'int', indexed: true},
stringCol: {type: 'string', indexed: true},
dateCol: {type: 'date', indexed: true},
}
};
var realm = new Realm({schema: [IndexedTypes]});
var realm = new Realm({schema: [schemas.IndexedTypes]});
realm.write(function() {
realm.create('IndexedTypesObject', {boolCol: true, intCol: 1, stringCol: '1', dateCol: new Date(1)});
});
@ -347,27 +350,30 @@ module.exports = BaseTest.extend({
new Realm({schema: [NotIndexed], path: '1'});
var IndexedSchema = {
name: 'IndexedSchema',
};
TestCase.assertThrows(function() {
IndexedTypes.properties = { floatCol: {type: 'float', indexed: true} }
new Realm({schema: [IndexedTypes], path: '2'});
IndexedSchema.properties = { floatCol: {type: 'float', indexed: true} };
new Realm({schema: [IndexedSchema], path: '2'});
});
TestCase.assertThrows(function() {
IndexedTypes.properties = { doubleCol: {type: 'double', indexed: true} }
new Realm({schema: [IndexedTypes], path: '3'});
IndexedSchema.properties = { doubleCol: {type: 'double', indexed: true} }
new Realm({schema: [IndexedSchema], path: '3'});
});
TestCase.assertThrows(function() {
IndexedTypes.properties = { dataCol: {type: 'data', indexed: true} }
new Realm({schema: [IndexedTypes], path: '4'});
IndexedSchema.properties = { dataCol: {type: 'data', indexed: true} }
new Realm({schema: [IndexedSchema], path: '4'});
});
// primary key
IndexedTypes.primaryKey = 'boolCol';
IndexedTypes.properties = { boolCol: {type: 'bool', indexed: true} }
IndexedSchema.properties = { boolCol: {type: 'bool', indexed: true} };
IndexedSchema.primaryKey = 'boolCol';
// Test this doesn't throw
new Realm({schema: [IndexedTypes], path: '5'});
// Test this doesn't throw
new Realm({schema: [IndexedSchema], path: '5'});
},
testRealmCreateWithDefaults: function() {
@ -600,4 +606,52 @@ module.exports = BaseTest.extend({
realm.write(function() {});
});
},
testSchema: function() {
var originalSchema = [schemas.TestObject, schemas.BasicTypes, schemas.NullableBasicTypes, schemas.IndexedTypes, schemas.IntPrimary,
schemas.PersonObject, schemas.LinkTypes];
var schemaMap = {};
originalSchema.forEach(function(objectSchema) { schemaMap[objectSchema.name] = objectSchema; });
var realm = new Realm({schema: originalSchema});
var schema = realm.schema;
TestCase.assertEqual(schema.length, originalSchema.length);
function isString(val) {
return typeof val === 'string' || val instanceof String;
}
function verifyObjectSchema(returned) {
var original = schemaMap[returned.name];
if (original.schema) {
original = original.schema;
}
TestCase.assertEqual(returned.primaryKey, original.primaryKey);
for (var propName in returned.properties) {
var prop1 = returned.properties[propName];
var prop2 = original.properties[propName];
if (prop1.type == 'object') {
TestCase.assertEqual(prop1.objectType, isString(prop2) ? prop2 : prop2.objectType);
TestCase.assertEqual(prop1.optional, true);
}
else if (prop1.type == 'list') {
TestCase.assertEqual(prop1.objectType, prop2.objectType);
TestCase.assertEqual(prop1.optional, undefined);
}
else {
TestCase.assertEqual(prop1.type, isString(prop2) ? prop2 : prop2.type);
TestCase.assertEqual(prop1.optional, prop2.optional || undefined);
}
TestCase.assertEqual(prop1.indexed, prop2.indexed || undefined);
}
}
for (var i = 0; i < originalSchema.length; i++) {
verifyObjectSchema(schema[i]);
}
},
});

View File

@ -77,6 +77,17 @@ exports.NullableBasicTypes = {
}
};
exports.IndexedTypes = {
name: 'IndexedTypesObject',
properties: {
boolCol: {type: 'bool', indexed: true},
intCol: {type: 'int', indexed: true},
stringCol: {type: 'string', indexed: true},
dateCol: {type: 'date', indexed: true},
}
};
exports.LinkTypes = {
name: 'LinkTypesObject',
properties: {