From 1324333ffd1cc5094894a8161e44c5235f93c3ba Mon Sep 17 00:00:00 2001 From: Matthew Dickinson Date: Fri, 16 Sep 2016 16:09:19 -0400 Subject: [PATCH] Add rough handling for automatic foreign keys This handles basic creating/dropping of FKs based on model relations --- lib/migration.js | 95 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/lib/migration.js b/lib/migration.js index b8e9cdc..6211583 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -42,11 +42,13 @@ function mixinMigration(MySQL, mysql) { var table = self.tableEscaped(model); self.execute('SHOW FIELDS FROM ' + table, function(err, fields) { 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); - } + self.discoverForeignKeys(model, {}, function(discoverErr, foreignKeys) { + if (!err && fields && fields.length) { + self.alterTable(model, fields, indexes, foreignKeys, done); + } else { + self.createTable(model, done); + } + }); }); }); }, cb); @@ -93,14 +95,16 @@ function mixinMigration(MySQL, mysql) { var table = self.tableEscaped(model); self.execute('SHOW FIELDS FROM ' + table, function(err, fields) { 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); + self.discoverForeignKeys(model, {}, function(discoverErr, foreignKeys) { + self.alterTable(model, fields, indexes, foreignKeys, function(err, needAlter) { + if (err) { + return done(err); + } else { + ok = ok || needAlter; + done(err); + } + }, true); + }); }); }); }, function(err) { @@ -111,7 +115,7 @@ function mixinMigration(MySQL, mysql) { }); }; - MySQL.prototype.alterTable = function(model, actualFields, actualIndexes, done, checkOnly) { + MySQL.prototype.alterTable = function(model, actualFields, actualIndexes, actualFks, done, checkOnly) { var self = this; var m = this.getModelDefinition(model); var propNames = Object.keys(m.properties).filter(function(name) { @@ -121,6 +125,10 @@ function mixinMigration(MySQL, mysql) { var indexNames = Object.keys(indexes).filter(function(name) { return !!m.settings.indexes[name]; }); + var newFks = m.settings.foreignKeys || {}; + var newFkNames = Object.keys(newFks).filter(function(name) { + return !!m.settings.foreignKeys[name]; + }); var sql = []; var ai = {}; @@ -159,6 +167,35 @@ function mixinMigration(MySQL, mysql) { } }); + //drop foreign keys for removed fields + if (actualFks) { + var removedFks = []; + actualFks.forEach(function(fk) { + var needsToDrop = false; + var newFk = newFks[fk.fkName]; + if (newFk) { + var fkCol = expectedColName(newFk.foreignKey); + var fkRefKey = expectedColNameForModel(newFk.entityKey, newFk.entity); + 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 +214,8 @@ function mixinMigration(MySQL, mysql) { aiNames.forEach(function(indexName) { if (indexName === 'PRIMARY' || (m.properties[indexName] && self.id(model, indexName))) return; + + if (newFkNames.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,6 +332,16 @@ function mixinMigration(MySQL, mysql) { } }); + //add new foreign keys + if (newFkNames.length) { + //TODO validate that these are in the same DB, etc. + var oldKeyNames = actualFks.map(function(oldKey) { return oldKey.fkName; }); + newFkNames.filter(function(key) { return !~oldKeyNames.indexOf(key); }) + .forEach(function(key) { + sql.push(self.buildForeignKeyDefinition(m, key)); + }); + } + if (sql.length) { var query = 'ALTER TABLE ' + self.tableEscaped(model) + ' ' + sql.join(',\n'); @@ -336,7 +385,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 +401,18 @@ function mixinMigration(MySQL, mysql) { } }; + MySQL.prototype.buildForeignKeyDefinition = function(model, keyName) { + var fk = model.settings.foreignKeys[keyName]; + if (fk) { + //TODO verify that the other model in the same DB + return ' ADD CONSTRAINT ' + this.client.escapeId(fk.name) + + ' FOREIGN KEY (' + fk.foreignKey + ')' + + ' REFERENCES ' + this.tableEscaped(fk.entity.name) + + '(' + this.client.escapeId(fk.entityKey) + ')'; + } + return ''; + }; + MySQL.prototype.buildColumnDefinitions = MySQL.prototype.propertiesSQL = function(model) { var self = this;