Merge pull request #193 from matthewdickinson/foreign_key_migration
Foreign key migration
This commit is contained in:
commit
692c07bd51
216
lib/migration.js
216
lib/migration.js
|
@ -21,6 +21,7 @@ function mixinMigration(MySQL, mysql) {
|
|||
*/
|
||||
MySQL.prototype.autoupdate = function(models, cb) {
|
||||
var self = this;
|
||||
var foreignKeyStatements = [];
|
||||
|
||||
if ((!cb) && ('function' === typeof models)) {
|
||||
cb = models;
|
||||
|
@ -41,15 +42,55 @@ function mixinMigration(MySQL, mysql) {
|
|||
}
|
||||
var table = self.tableEscaped(model);
|
||||
self.execute('SHOW FIELDS FROM ' + table, function(err, fields) {
|
||||
if (err) console.log('Failed to discover "' + table + '" fields', err);
|
||||
|
||||
self.execute('SHOW INDEXES FROM ' + table, function(err, indexes) {
|
||||
if (!err && fields && fields.length) {
|
||||
self.alterTable(model, fields, indexes, done);
|
||||
} else {
|
||||
self.createTable(model, done);
|
||||
}
|
||||
if (err) console.log('Failed to discover "' + table + '" indexes', err);
|
||||
|
||||
self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) {
|
||||
if (err) console.log('Failed to discover "' + table + '" foreign keys', err);
|
||||
|
||||
if (!err && fields && fields.length) {
|
||||
//if we already have a definition, update this table
|
||||
self.alterTable(model, fields, indexes, foreignKeys, function(err, changed, res) {
|
||||
//check to see if there were any new foreign keys for this table.
|
||||
//If so, we'll create them once all tables have been updated
|
||||
if (!err && res && res.newFks && res.newFks.length) {
|
||||
foreignKeyStatements.push(res.newFks);
|
||||
}
|
||||
|
||||
done(err);
|
||||
});
|
||||
} else {
|
||||
//if there is not yet a definition, create this table
|
||||
var res = self.createTable(model, function(err) {
|
||||
if (!err) {
|
||||
//get a list of the alter statements needed to add the defined foreign keys
|
||||
var newFks = self.getForeignKeySQL(model, foreignKeys);
|
||||
|
||||
//check to see if there were any new foreign keys for this table.
|
||||
//If so, we'll create them once all tables have been updated
|
||||
if (newFks && newFks.length) {
|
||||
foreignKeyStatements.push(self.getAlterStatement(model, newFks));
|
||||
}
|
||||
}
|
||||
|
||||
done(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, cb);
|
||||
}, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
//add any new foreign keys
|
||||
async.each(foreignKeyStatements, function(addFkStmt, execDone) {
|
||||
self.execute(addFkStmt, execDone);
|
||||
}, function(err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -92,26 +133,62 @@ function mixinMigration(MySQL, mysql) {
|
|||
async.each(models, function(model, done) {
|
||||
var table = self.tableEscaped(model);
|
||||
self.execute('SHOW FIELDS FROM ' + table, function(err, fields) {
|
||||
if (err) console.log('Failed to discover "' + table + '" fields', err);
|
||||
|
||||
self.execute('SHOW INDEXES FROM ' + table, function(err, indexes) {
|
||||
self.alterTable(model, fields, indexes, function(err, needAlter) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
} else {
|
||||
ok = ok || needAlter;
|
||||
done(err);
|
||||
}
|
||||
}, true);
|
||||
if (err) console.log('Failed to discover "' + table + '" indexes', err);
|
||||
|
||||
self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) {
|
||||
if (err) console.log('Failed to discover "' + table + '" foreign keys', err);
|
||||
|
||||
self.alterTable(model, fields, indexes, foreignKeys, function(err, needAlter) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
} else {
|
||||
ok = ok || needAlter;
|
||||
done(err);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
cb(null, !ok);
|
||||
cb(err, !ok);
|
||||
});
|
||||
};
|
||||
|
||||
MySQL.prototype.alterTable = function(model, actualFields, actualIndexes, done, checkOnly) {
|
||||
MySQL.prototype.getForeignKeySQL = function(model, actualFks) {
|
||||
var self = this;
|
||||
var m = this.getModelDefinition(model);
|
||||
var addFksSql = [];
|
||||
var newFks = m.settings.foreignKeys || {};
|
||||
var newFkNames = Object.keys(newFks).filter(function(name) {
|
||||
return !!m.settings.foreignKeys[name];
|
||||
});
|
||||
|
||||
//add new foreign keys
|
||||
if (newFkNames.length) {
|
||||
//narrow down our key names to only those that don't already exist
|
||||
var oldKeyNames = actualFks.map(function(oldKey) { return oldKey.fkName; });
|
||||
newFkNames.filter(function(key) { return !~oldKeyNames.indexOf(key); })
|
||||
.forEach(function(key) {
|
||||
var constraint = self.buildForeignKeyDefinition(model, key);
|
||||
if (constraint) {
|
||||
addFksSql.push('ADD ' + constraint);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return addFksSql;
|
||||
};
|
||||
|
||||
MySQL.prototype.alterTable = function(model, actualFields, actualIndexes, actualFks, done, checkOnly) {
|
||||
//if this is using an old signature, then grab the correct callback and check boolean
|
||||
if ('function' == typeof actualFks && typeof done !== 'function') {
|
||||
checkOnly = done || false;
|
||||
done = actualFks;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var m = this.getModelDefinition(model);
|
||||
var propNames = Object.keys(m.properties).filter(function(name) {
|
||||
|
@ -121,6 +198,9 @@ function mixinMigration(MySQL, mysql) {
|
|||
var indexNames = Object.keys(indexes).filter(function(name) {
|
||||
return !!m.settings.indexes[name];
|
||||
});
|
||||
|
||||
//add new foreign keys
|
||||
var correctFks = m.settings.foreignKeys || {};
|
||||
var sql = [];
|
||||
var ai = {};
|
||||
|
||||
|
@ -159,6 +239,36 @@ function mixinMigration(MySQL, mysql) {
|
|||
}
|
||||
});
|
||||
|
||||
//drop foreign keys for removed fields
|
||||
if (actualFks) {
|
||||
var removedFks = [];
|
||||
actualFks.forEach(function(fk) {
|
||||
var needsToDrop = false;
|
||||
var newFk = correctFks[fk.fkName];
|
||||
if (newFk) {
|
||||
var fkCol = expectedColName(newFk.foreignKey);
|
||||
var fkEntity = self.getModelDefinition(newFk.entity);
|
||||
var fkRefKey = expectedColNameForModel(newFk.entityKey, fkEntity);
|
||||
var fkRefTable = newFk.entity.name; //TODO check for mysql name
|
||||
needsToDrop = fkCol != fk.fkColumnName ||
|
||||
fkRefKey != fk.pkColumnName ||
|
||||
fkRefTable != fk.pkTableName;
|
||||
} else {
|
||||
needsToDrop = true;
|
||||
}
|
||||
|
||||
if (needsToDrop) {
|
||||
sql.push('DROP FOREIGN KEY ' + fk.fkName);
|
||||
removedFks.push(fk); //keep track that we removed these
|
||||
}
|
||||
});
|
||||
|
||||
//update out list of existing keys by removing dropped keys
|
||||
actualFks = actualFks.filter(function(k) {
|
||||
return removedFks.indexOf(k) == -1;
|
||||
});
|
||||
}
|
||||
|
||||
// drop columns
|
||||
if (actualFields) {
|
||||
actualFields.forEach(function(f) {
|
||||
|
@ -177,6 +287,8 @@ function mixinMigration(MySQL, mysql) {
|
|||
aiNames.forEach(function(indexName) {
|
||||
if (indexName === 'PRIMARY' ||
|
||||
(m.properties[indexName] && self.id(model, indexName))) return;
|
||||
|
||||
if (Object.keys(actualFks).indexOf(indexName) > -1) return; //this index is from an FK
|
||||
if (indexNames.indexOf(indexName) === -1 && !m.properties[indexName] ||
|
||||
m.properties[indexName] && !m.properties[indexName].index) {
|
||||
sql.push('DROP INDEX ' + self.client.escapeId(indexName));
|
||||
|
@ -293,13 +405,35 @@ function mixinMigration(MySQL, mysql) {
|
|||
}
|
||||
});
|
||||
|
||||
if (sql.length) {
|
||||
var query = 'ALTER TABLE ' + self.tableEscaped(model) + ' ' +
|
||||
sql.join(',\n');
|
||||
if (checkOnly) {
|
||||
done(null, true, {statements: sql, query: query});
|
||||
//since we're passing the actualFks list to this,
|
||||
//this code has be called after foreign keys have been dropped
|
||||
//(in case we're replacing FKs)
|
||||
var addFksSql = this.getForeignKeySQL(model, actualFks);
|
||||
|
||||
//determine if there are column, index, or foreign keys changes (all require update)
|
||||
if (sql.length || addFksSql.length) {
|
||||
//get the required alter statements
|
||||
var alterStmt = self.getAlterStatement(model, sql);
|
||||
var newFksStatement = self.getAlterStatement(model, addFksSql);
|
||||
var stmtList = [alterStmt, newFksStatement].filter(function(s) { return s.length; });
|
||||
|
||||
//set up an object to pass back all changes, changes that have been run,
|
||||
//and foreign key statements that haven't been run
|
||||
var retValues = {
|
||||
statements: stmtList,
|
||||
query: stmtList.join(';'),
|
||||
newFks: newFksStatement,
|
||||
};
|
||||
|
||||
//if we're running in read only mode OR if the only changes are foreign keys additions,
|
||||
//then just return the object directly
|
||||
if (checkOnly || !alterStmt.length) {
|
||||
done(null, true, retValues);
|
||||
} else {
|
||||
this.execute(query, done);
|
||||
//if there are changes in the alter statement, then execute them and return the object
|
||||
self.execute(alterStmt, function(err) {
|
||||
done(err, true, retValues);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
done();
|
||||
|
@ -336,7 +470,11 @@ function mixinMigration(MySQL, mysql) {
|
|||
}
|
||||
|
||||
function expectedColName(propName) {
|
||||
var mysql = m.properties[propName].mysql;
|
||||
return expectedColNameForModel(propName, m);
|
||||
}
|
||||
|
||||
function expectedColNameForModel(propName, modelToCheck) {
|
||||
var mysql = modelToCheck.properties[propName].mysql;
|
||||
if (typeof mysql === 'undefined') {
|
||||
return propName;
|
||||
}
|
||||
|
@ -348,6 +486,31 @@ function mixinMigration(MySQL, mysql) {
|
|||
}
|
||||
};
|
||||
|
||||
MySQL.prototype.getAlterStatement = function(model, statements) {
|
||||
return statements.length ?
|
||||
'ALTER TABLE ' + this.tableEscaped(model) + ' ' + statements.join(',\n') :
|
||||
'';
|
||||
};
|
||||
|
||||
MySQL.prototype.buildForeignKeyDefinition = function(model, keyName) {
|
||||
var definition = this.getModelDefinition(model);
|
||||
|
||||
var fk = definition.settings.foreignKeys[keyName];
|
||||
if (fk) {
|
||||
//get the definition of the referenced object
|
||||
var fkEntityName = (typeof fk.entity === 'object') ? fk.entity.name : fk.entity;
|
||||
|
||||
//verify that the other model in the same DB
|
||||
if (this._models[fkEntityName]) {
|
||||
return ' CONSTRAINT ' + this.client.escapeId(fk.name) +
|
||||
' FOREIGN KEY (' + fk.foreignKey + ')' +
|
||||
' REFERENCES ' + this.tableEscaped(fkEntityName) +
|
||||
'(' + this.client.escapeId(fk.entityKey) + ')';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
MySQL.prototype.buildColumnDefinitions =
|
||||
MySQL.prototype.propertiesSQL = function(model) {
|
||||
var self = this;
|
||||
|
@ -385,6 +548,7 @@ function mixinMigration(MySQL, mysql) {
|
|||
indexes.forEach(function(i) {
|
||||
sql.push(i);
|
||||
});
|
||||
|
||||
return sql.join(',\n ');
|
||||
};
|
||||
|
||||
|
|
|
@ -228,6 +228,252 @@ 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',
|
||||
},
|
||||
},
|
||||
'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 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'String',
|
||||
'length': 20,
|
||||
'id': 1,
|
||||
},
|
||||
'customerId': {
|
||||
'type': 'String',
|
||||
'length': 20,
|
||||
'id': 1,
|
||||
},
|
||||
'description': {
|
||||
'type': 'String',
|
||||
'required': false,
|
||||
'length': 40,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
'id': 1,
|
||||
},
|
||||
'description': {
|
||||
'type': 'String',
|
||||
'required': false,
|
||||
'length': 40,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
'id': 1,
|
||||
},
|
||||
'description': {
|
||||
'type': 'String',
|
||||
'required': false,
|
||||
'length': 40,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var foreignKeySelect =
|
||||
'SELECT COLUMN_NAME,CONSTRAINT_NAME,REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME ' +
|
||||
'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE ' +
|
||||
'WHERE REFERENCED_TABLE_SCHEMA = "myapp_test" ' +
|
||||
'AND TABLE_NAME = "order_test"';
|
||||
|
||||
ds.createModel(customer2_schema.name, customer2_schema.properties, customer2_schema.options);
|
||||
ds.createModel(customer3_schema.name, customer3_schema.properties, customer3_schema.options);
|
||||
ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options);
|
||||
|
||||
//do initial update/creation of table
|
||||
ds.autoupdate(function() {
|
||||
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);
|
||||
//validate that the foreign key exists and points to the right column
|
||||
assert(foreignKeys);
|
||||
assert(foreignKeys.length.should.be.equal(1));
|
||||
assert.equal(foreignKeys[0].REFERENCED_TABLE_NAME, 'customer_test3');
|
||||
assert.equal(foreignKeys[0].COLUMN_NAME, 'customerId');
|
||||
assert.equal(foreignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId');
|
||||
assert.equal(foreignKeys[0].REFERENCED_COLUMN_NAME, 'id');
|
||||
|
||||
//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);
|
||||
|
||||
//get and validate the properties on this model
|
||||
ds.discoverModelProperties('order_test', function(err, props) {
|
||||
if (err) return done (err);
|
||||
|
||||
assert.equal(props.length, 3);
|
||||
|
||||
//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');
|
||||
|
||||
//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);
|
||||
|
||||
assert.equal(props.length, 3);
|
||||
|
||||
//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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupAltColNameData() {
|
||||
var schema = {
|
||||
name: 'ColRenameTest',
|
||||
|
|
Loading…
Reference in New Issue