fix compare of foreign keys for autoupdate (#272)

* fix compare of foreign keys for autoupdate

use mysql table name in case it is not equal model name

* fix column name lookup for fk definition

* fix ADD CONSTRAINT for multiple fk

if more the one foreign key added ADD CONSTRAINT
must be separated with comma but not with space as applySqlChanges do.

* fix duplicate foreign key creation on autoupdate

* fix entity name selection

* change join to toString as requested

* fix removing dropped keys

* fix linter issues

* add test case with columnName in foreign key

and isActual check after autoupdate

* uncommit accitenally commited test case
This commit is contained in:
Sergey Nosenko 2017-05-06 02:13:23 +03:00 committed by Sakib Hasan
parent 204242663e
commit 72abcb8e9f
2 changed files with 342 additions and 326 deletions

View File

@ -73,7 +73,12 @@ function mixinMigration(MySQL, mysql) {
//if we already have a definition, update this table
self.alterTable(model, fields, indexes, foreignKeys, function(err, result) {
if (!err) {
self.addForeignKeys(model, function(err, result) {
//foreignKeys is a list of EXISTING fkeys here, so you don't need to recreate them again
//prepare fkSQL for new foreign keys
var fkSQL = self.getForeignKeySQL(model,
self.getModelDefinition(model).settings.foreignKeys,
foreignKeys);
self.addForeignKeys(model, fkSQL, function(err, result) {
done(err);
});
} else {
@ -390,14 +395,20 @@ function mixinMigration(MySQL, mysql) {
return sql;
};
MySQL.prototype.getForeignKeySQL = function(model, actualFks) {
MySQL.prototype.getForeignKeySQL = function(model, actualFks, existingFks) {
var self = this;
var m = this.getModelDefinition(model);
var addFksSql = [];
existingFks = existingFks || [];
if (actualFks) {
var keys = Object.keys(actualFks);
for (var i = 0; i < keys.length; i++) {
//all existing fks are already checked in MySQL.prototype.dropForeignKeys
//so we need check only names - skip if found
if (existingFks.filter(function(fk) {
return fk.fkName === keys[i];
}).length > 0) continue;
var constraint = self.buildForeignKeyDefinition(model, keys[i]);
if (constraint) {
@ -423,7 +434,7 @@ function mixinMigration(MySQL, mysql) {
fkSQL = self.getForeignKeySQL(model, newFks);
}
if (fkSQL && fkSQL.length) {
self.applySqlChanges(model, fkSQL, function(err, result) {
self.applySqlChanges(model, [fkSQL.toString()], function(err, result) {
if (err) cb(err);
else
cb(null, result);
@ -449,7 +460,8 @@ function mixinMigration(MySQL, mysql) {
var fkCol = expectedColNameForModel(newFk.foreignKey, m);
var fkEntity = self.getModelDefinition(newFk.entity);
var fkRefKey = expectedColNameForModel(newFk.entityKey, fkEntity);
var fkRefTable = newFk.entity.name; //TODO check for mysql name
var fkEntityName = (typeof newFk.entity === 'object') ? newFk.entity.name : newFk.entity;
var fkRefTable = self.table(fkEntityName);
needsToDrop = fkCol != fk.fkColumnName ||
fkRefKey != fk.pkColumnName ||
fkRefTable != fk.pkTableName;
@ -464,8 +476,9 @@ function mixinMigration(MySQL, mysql) {
});
//update out list of existing keys by removing dropped keys
fks = actualFks.filter(function(k) {
return removedFks.indexOf(k) == -1;
removedFks.forEach(function(k) {
var index = actualFks.indexOf(k);
if (index !== -1) actualFks.splice(index, 1);
});
}
return sql;
@ -547,7 +560,7 @@ function mixinMigration(MySQL, mysql) {
//verify that the other model in the same DB
if (this._models[fkEntityName]) {
return ' CONSTRAINT ' + this.client.escapeId(fk.name) +
' FOREIGN KEY (' + fk.foreignKey + ')' +
' FOREIGN KEY (`' + expectedColNameForModel(fk.foreignKey, definition) + '`)' +
' REFERENCES ' + this.tableEscaped(fkEntityName) +
'(' + this.client.escapeId(fk.entityKey) + ')';
}

View File

@ -18,152 +18,150 @@ describe('MySQL connector', function() {
});
it('should auto migrate/update tables', function(done) {
var schema_v1 =
{
'name': 'CustomerTest',
'options': {
'idInjection': false,
var schema_v1 = {
'name': 'CustomerTest',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test',
},
'indexes': {
'name_index': {
'keys': {
'name': 1,
},
'options': {
'unique': true,
},
},
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'name': {
'type': 'String',
'required': false,
'length': 40,
},
'email': {
'type': 'String',
'required': true,
'length': 40,
},
'age': {
'type': 'Number',
'required': false,
},
'discount': {
'type': 'Number',
'required': false,
'dataType': 'decimal',
'precision': 10,
'scale': 2,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test',
},
'indexes': {
'name_index': {
'keys': {
'name': 1,
},
'options': {
'unique': true,
},
},
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'name': {
'type': 'String',
'required': false,
'length': 40,
},
'email': {
'type': 'String',
'required': true,
'length': 40,
},
'age': {
'type': 'Number',
'required': false,
},
'discount': {
'type': 'Number',
'required': false,
'columnName': 'customer_discount',
'dataType': 'decimal',
'precision': 10,
'scale': 2,
'mysql': {
'columnName': 'customer_discount',
'dataType': 'decimal',
'dataPrecision': 10,
'dataScale': 2,
},
'dataPrecision': 10,
'dataScale': 2,
},
},
};
},
};
var schema_v2 =
{
'name': 'CustomerTest',
'options': {
'idInjection': false,
var schema_v2 = {
'name': 'CustomerTest',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test',
},
'indexes': {
'updated_name_index': {
'keys': {
'firstName': 1,
'lastName': -1,
},
'options': {
'unique': true,
},
},
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'email': {
'type': 'String',
'required': false,
'length': 60,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test',
},
'indexes': {
'updated_name_index': {
'keys': {
'firstName': 1,
'lastName': -1,
},
'options': {
'unique': true,
},
},
'columnName': 'email',
'dataType': 'varchar',
'dataLength': 60,
'nullable': 'YES',
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'email': {
'type': 'String',
'required': false,
'length': 60,
'mysql': {
'columnName': 'email',
'dataType': 'varchar',
'dataLength': 60,
'nullable': 'YES',
},
},
'firstName': {
'type': 'String',
'required': false,
'length': 40,
},
'lastName': {
'type': 'String',
'required': false,
'length': 40,
},
// remove age
// change data type details with column name
'discount': {
'type': 'Number',
'required': false,
'firstName': {
'type': 'String',
'required': false,
'length': 40,
},
'lastName': {
'type': 'String',
'required': false,
'length': 40,
},
// remove age
// change data type details with column name
'discount': {
'type': 'Number',
'required': false,
'dataType': 'decimal',
'precision': 12,
'scale': 5,
'mysql': {
'columnName': 'customer_discount',
'dataType': 'decimal',
'precision': 12,
'scale': 5,
'mysql': {
'columnName': 'customer_discount',
'dataType': 'decimal',
'dataPrecision': 12,
'dataScale': 5,
},
},
// add new column with column name
'address': {
'type': 'String',
'required': false,
'length': 10,
'mysql': {
'columnName': 'customer_address',
'dataType': 'varchar',
'length': 10,
},
},
// add new column with index & column name
'code': {
'type': 'String',
'required': true,
'length': 12,
'index': {
unique: true,
},
'mysql': {
'columnName': 'customer_code',
'dataType': 'varchar',
'length': 12,
},
'dataPrecision': 12,
'dataScale': 5,
},
},
};
// add new column with column name
'address': {
'type': 'String',
'required': false,
'length': 10,
'mysql': {
'columnName': 'customer_address',
'dataType': 'varchar',
'length': 10,
},
},
// add new column with index & column name
'code': {
'type': 'String',
'required': true,
'length': 12,
'index': {
unique: true,
},
'mysql': {
'columnName': 'customer_code',
'dataType': 'varchar',
'length': 12,
},
},
},
};
ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options);
@ -184,16 +182,16 @@ describe('MySQL connector', function() {
assert.equal(names[4], 'customer_discount');
ds.connector.execute('SHOW INDEXES FROM customer_test', function(err, indexes) {
if (err) return done (err);
if (err) return done(err);
assert(indexes);
assert(indexes.length.should.be.above(1));
assert.equal(indexes[1].Key_name, 'name_index');
assert.equal(indexes[1].Non_unique, 0);
ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options);
ds.autoupdate(function(err, result) {
if (err) return done (err);
if (err) return done(err);
ds.discoverModelProperties('customer_test', function(err, props) {
if (err) return done (err);
if (err) return done(err);
assert.equal(props.length, 7);
var names = props.map(function(p) {
return p.columnName;
@ -206,7 +204,7 @@ describe('MySQL connector', function() {
assert.equal(names[5], 'customer_address');
assert.equal(names[6], 'customer_code');
ds.connector.execute('SHOW INDEXES FROM customer_test', function(err, updatedindexes) {
if (err) return done (err);
if (err) return done(err);
assert(updatedindexes);
assert(updatedindexes.length.should.be.above(3));
assert.equal(updatedindexes[1].Key_name, 'customer_code');
@ -229,173 +227,171 @@ describe('MySQL connector', function() {
});
it('should auto migrate/update foreign keys in tables', function(done) {
var customer2_schema =
{
'name': 'CustomerTest2',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test2',
},
var customer2_schema = {
'name': 'CustomerTest2',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test2',
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'name': {
'type': 'String',
'required': false,
'length': 40,
},
'email': {
'type': 'String',
'required': true,
'length': 40,
},
'age': {
'type': 'Number',
'required': false,
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
};
var customer3_schema =
{
'name': 'CustomerTest3',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test3',
},
'name': {
'type': 'String',
'required': false,
'length': 40,
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'name': {
'type': 'String',
'required': false,
'length': 40,
},
'email': {
'type': 'String',
'required': true,
'length': 40,
},
'age': {
'type': 'Number',
'required': false,
},
'email': {
'type': 'String',
'required': true,
'length': 40,
},
};
'age': {
'type': 'Number',
'required': false,
},
},
};
var customer3_schema = {
'name': 'CustomerTest3',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'customer_test3',
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'name': {
'type': 'String',
'required': false,
'length': 40,
},
'email': {
'type': 'String',
'required': true,
'length': 40,
},
'age': {
'type': 'Number',
'required': false,
},
},
};
var schema_v1 =
{
'name': 'OrderTest',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'order_test',
},
'foreignKeys': {
'fk_ordertest_customerId': {
'name': 'fk_ordertest_customerId',
'entity': 'CustomerTest3',
'entityKey': 'id',
'foreignKey': 'customerId',
},
var schema_v1 = {
'name': 'OrderTest',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'order_test',
},
'foreignKeys': {
'fk_ordertest_customerId': {
'name': 'fk_ordertest_customerId',
'entity': 'CustomerTest3',
'entityKey': 'id',
'foreignKey': 'customerId',
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'customerId': {
'type': 'String',
'length': 20,
'id': 1,
},
'description': {
'type': 'String',
'required': false,
'length': 40,
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
};
'customerId': {
'type': 'String',
'length': 20,
},
'description': {
'type': 'String',
'required': false,
'length': 40,
},
},
};
var schema_v2 =
{
'name': 'OrderTest',
'options': {
'idInjection': false,
var schema_v2 = {
'name': 'OrderTest',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'order_test',
},
'foreignKeys': {
'fk_ordertest_customerId': {
'name': 'fk_ordertest_customerId',
'entity': 'CustomerTest2',
'entityKey': 'id',
'foreignKey': 'customerId',
},
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'customerId': {
'type': 'String',
'length': 20,
'mysql': {
'schema': 'myapp_test',
'table': 'order_test',
},
'foreignKeys': {
'fk_ordertest_customerId': {
'name': 'fk_ordertest_customerId',
'entity': 'CustomerTest2',
'entityKey': 'id',
'foreignKey': 'customerId',
},
'columnName': 'customer_id',
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'customerId': {
'type': 'String',
'length': 20,
'id': 1,
},
'description': {
'type': 'String',
'required': false,
'length': 40,
},
'description': {
'type': 'String',
'required': false,
'length': 40,
},
};
},
};
var schema_v3 =
{
'name': 'OrderTest',
'options': {
'idInjection': false,
var schema_v3 = {
'name': 'OrderTest',
'options': {
'idInjection': false,
'mysql': {
'schema': 'myapp_test',
'table': 'order_test',
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'customerId': {
'type': 'String',
'length': 20,
'mysql': {
'schema': 'myapp_test',
'table': 'order_test',
'columnName': 'customer_id',
},
},
'properties': {
'id': {
'type': 'String',
'length': 20,
'id': 1,
},
'customerId': {
'type': 'String',
'length': 20,
'id': 1,
},
'description': {
'type': 'String',
'required': false,
'length': 40,
},
'description': {
'type': 'String',
'required': false,
'length': 40,
},
};
},
};
var foreignKeySelect =
'SELECT COLUMN_NAME,CONSTRAINT_NAME,REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME ' +
@ -408,14 +404,15 @@ describe('MySQL connector', function() {
ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options);
//do initial update/creation of table
ds.autoupdate(function() {
ds.autoupdate(function(err) {
assert(!err, err);
ds.discoverModelProperties('order_test', function(err, props) {
//validate that we have the correct number of properties
assert.equal(props.length, 3);
//get the foreign keys for this table
ds.connector.execute(foreignKeySelect, function(err, foreignKeys) {
if (err) return done (err);
if (err) return done(err);
//validate that the foreign key exists and points to the right column
assert(foreignKeys);
assert(foreignKeys.length.should.be.equal(1));
@ -427,42 +424,48 @@ describe('MySQL connector', function() {
//update our model (move foreign key) and run autoupdate to migrate
ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options);
ds.autoupdate(function(err, result) {
if (err) return done (err);
if (err) return done(err);
//get and validate the properties on this model
ds.discoverModelProperties('order_test', function(err, props) {
if (err) return done (err);
//should be actual after autoupdate
ds.isActual(function(err, isEqual) {
if (err) return done(err);
assert(!isEqual);
assert.equal(props.length, 3);
//get and validate the properties on this model
ds.discoverModelProperties('order_test', function(err, props) {
if (err) return done(err);
//get the foreign keys that exist after the migration
ds.connector.execute(foreignKeySelect, function(err, updatedForeignKeys) {
if (err) return done (err);
//validate that the foreign keys was moved to the new column
assert(updatedForeignKeys);
assert(updatedForeignKeys.length.should.be.equal(1));
assert.equal(updatedForeignKeys[0].REFERENCED_TABLE_NAME, 'customer_test2');
assert.equal(updatedForeignKeys[0].COLUMN_NAME, 'customerId');
assert.equal(updatedForeignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId');
assert.equal(updatedForeignKeys[0].REFERENCED_COLUMN_NAME, 'id');
assert.equal(props.length, 3);
//update model (to drop foreign key) and autoupdate
ds.createModel(schema_v3.name, schema_v3.properties, schema_v3.options);
ds.autoupdate(function(err, result) {
if (err) return done (err);
//validate the properties
ds.discoverModelProperties('order_test', function(err, props) {
if (err) return done (err);
//get the foreign keys that exist after the migration
ds.connector.execute(foreignKeySelect, function(err, updatedForeignKeys) {
if (err) return done(err);
//validate that the foreign keys was moved to the new column
assert(updatedForeignKeys);
assert(updatedForeignKeys.length.should.be.equal(1));
assert.equal(updatedForeignKeys[0].REFERENCED_TABLE_NAME, 'customer_test2');
assert.equal(updatedForeignKeys[0].COLUMN_NAME, 'customer_id');
assert.equal(updatedForeignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId');
assert.equal(updatedForeignKeys[0].REFERENCED_COLUMN_NAME, 'id');
assert.equal(props.length, 3);
//update model (to drop foreign key) and autoupdate
ds.createModel(schema_v3.name, schema_v3.properties, schema_v3.options);
ds.autoupdate(function(err, result) {
if (err) return done(err);
//validate the properties
ds.discoverModelProperties('order_test', function(err, props) {
if (err) return done(err);
//get the foreign keys and validate the foreign key has been dropped
ds.connector.execute(foreignKeySelect, function(err, thirdForeignKeys) {
if (err) return done (err);
assert(thirdForeignKeys);
assert(thirdForeignKeys.length.should.be.equal(0));
assert.equal(props.length, 3);
done(err, result);
//get the foreign keys and validate the foreign key has been dropped
ds.connector.execute(foreignKeySelect, function(err, thirdForeignKeys) {
if (err) return done(err);
assert(thirdForeignKeys);
assert(thirdForeignKeys.length.should.be.equal(0));
done(err, result);
});
});
});
});