diff --git a/lib/migration.js b/lib/migration.js index adf6476..c526fca 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -71,41 +71,31 @@ function mixinMigration(MySQL, mysql) { 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); + self.alterTable(model, fields, indexes, foreignKeys, function(err, result) { + if (!err) { + self.addForeignKeys(model, function(err, result) { + done(err); + }); + } else { + done(err); } - done(err); }); } else { //if there is not yet a definition, create this table - var res = self.createTable(model, function(err) { + 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)); - } + self.addForeignKeys(model, function(err, result) { + done(err); + }); + } else { + done(err); } - done(err); }); } }); }); }, 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); - }); + return cb(err); }); }; @@ -166,50 +156,114 @@ function mixinMigration(MySQL, mysql) { }); }; - MySQL.prototype.getForeignKeySQL = function(model, actualFks) { + MySQL.prototype.getAddModifyColumns = function(model, actualFields) { + var sql = []; 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; + sql = sql.concat(self.getColumnsToAdd(model, actualFields)); + return sql; }; - 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; - } - + MySQL.prototype.getColumnsToAdd = function(model, actualFields) { var self = this; var m = this.getModelDefinition(model); var propNames = Object.keys(m.properties).filter(function(name) { return !!m.properties[name]; }); - var indexes = m.settings.indexes || {}; - var indexNames = Object.keys(indexes).filter(function(name) { - return !!m.settings.indexes[name]; + var sql = []; + + propNames.forEach(function(propName) { + if (m.properties[propName] && self.id(model, propName)) return; + var found; + var colName = expectedColNameForModel(propName, m); + if (actualFields) { + actualFields.forEach(function(f) { + if (f.Field === colName) { + found = f; + } + }); + } + if (found) { + actualize(colName, found); + } else { + sql.push('ADD COLUMN ' + self.client.escapeId(colName) + ' ' + + self.buildColumnDefinition(model, propName)); + } }); - //add new foreign keys - var correctFks = m.settings.foreignKeys || {}; + function actualize(propName, oldSettings) { + var newSettings = m.properties[propName]; + if (newSettings && changed(newSettings, oldSettings)) { + var pName = self.client.escapeId(propName); + sql.push('CHANGE COLUMN ' + pName + ' ' + pName + ' ' + + self.buildColumnDefinition(model, propName)); + } + } + + function changed(newSettings, oldSettings) { + if (oldSettings.Null === 'YES') { + // Used to allow null and does not now. + if (!self.isNullable(newSettings)) { + return true; + } + } + if (oldSettings.Null === 'NO') { + // Did not allow null and now does. + if (self.isNullable(newSettings)) { + return true; + } + } + + if (oldSettings.Type.toUpperCase() !== + self.buildColumnType(newSettings).toUpperCase()) { + return true; + } + return false; + } + return sql; + }; + + MySQL.prototype.getDropColumns = function(model, actualFields) { + var sql = []; + var self = this; + sql = sql.concat(self.getColumnsToDrop(model, actualFields)); + return sql; + }; + + MySQL.prototype.getColumnsToDrop = function(model, actualFields) { + var self = this; + var fields = actualFields; + var sql = []; + var m = this.getModelDefinition(model); + var propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + // drop columns + if (fields) { + fields.forEach(function(f) { + var colNames = propNames.map(function expectedColName(propName) { + return expectedColNameForModel(propName, m); + }); + var index = colNames.indexOf(f.Field); + var propName = index >= 0 ? propNames[index] : f.Field; + var notFound = !~index; + if (m.properties[propName] && self.id(model, propName)) return; + if (notFound || !m.properties[propName]) { + sql.push('DROP COLUMN ' + self.client.escapeId(f.Field)); + } + }); + } + return sql; + }; + + MySQL.prototype.addIndexes = function(model, actualIndexes) { + var self = this; + var m = this.getModelDefinition(model); + var propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + var indexNames = m.settings.indexes && Object.keys(m.settings.indexes).filter(function(name) { + return !!m.settings.indexes[name]; + }) || []; var sql = []; var ai = {}; @@ -227,71 +281,6 @@ function mixinMigration(MySQL, mysql) { } var aiNames = Object.keys(ai); - // change/add new fields - propNames.forEach(function(propName) { - if (m.properties[propName] && self.id(model, propName)) return; - var found; - var colName = expectedColName(propName); - if (actualFields) { - actualFields.forEach(function(f) { - if (f.Field === colName) { - found = f; - } - }); - } - - if (found) { - actualize(colName, found); - } else { - sql.push('ADD COLUMN ' + self.client.escapeId(colName) + ' ' + - self.buildColumnDefinition(model, propName)); - } - }); - - //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) { - var colNames = propNames.map(expectedColName); - var index = colNames.indexOf(f.Field); - var propName = index >= 0 ? propNames[index] : f.Field; - var notFound = !~index; - if (m.properties[propName] && self.id(model, propName)) return; - if (notFound || !m.properties[propName]) { - sql.push('DROP COLUMN ' + self.client.escapeId(f.Field)); - } - }); - } - // remove indexes aiNames.forEach(function(indexName) { if (indexName === 'PRIMARY' || @@ -347,7 +336,7 @@ function mixinMigration(MySQL, mysql) { } var found = ai[propName] && ai[propName].info; if (!found) { - var colName = expectedColName(propName); + var colName = expectedColNameForModel(propName, m); var pName = self.client.escapeId(colName); var type = ''; var kind = ''; @@ -412,86 +401,88 @@ function mixinMigration(MySQL, mysql) { } } }); + return sql; + }; - //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); + MySQL.prototype.getForeignKeySQL = function(model, actualFks) { + var self = this; + var m = this.getModelDefinition(model); + var addFksSql = []; - //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; }); + if (actualFks) { + var keys = Object.keys(actualFks); + for (var i = 0; i < keys.length; i++) { + var constraint = self.buildForeignKeyDefinition(model, keys[i]); - //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 { - //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(); - } - - function actualize(propName, oldSettings) { - var newSettings = m.properties[propName]; - if (newSettings && changed(newSettings, oldSettings)) { - var pName = self.client.escapeId(propName); - sql.push('CHANGE COLUMN ' + pName + ' ' + pName + ' ' + - self.buildColumnDefinition(model, propName)); - } - } - - function changed(newSettings, oldSettings) { - if (oldSettings.Null === 'YES') { - // Used to allow null and does not now. - if (!self.isNullable(newSettings)) { - return true; + if (constraint) { + addFksSql.push('ADD ' + constraint); } } - if (oldSettings.Null === 'NO') { - // Did not allow null and now does. - if (self.isNullable(newSettings)) { - return true; + } + return addFksSql; + }; + + MySQL.prototype.addForeignKeys = function(model, fkSQL, cb) { + var self = this; + var m = this.getModelDefinition(model); + + if ((!cb) && ('function' === typeof fkSQL)) { + cb = fkSQL; + fkSQL = undefined; + } + + if (!fkSQL) { + var newFks = m.settings.foreignKeys; + if (newFks) + fkSQL = self.getForeignKeySQL(model, newFks); + } + if (fkSQL && fkSQL.length) { + self.applySqlChanges(model, fkSQL, function(err, result) { + if (err) cb(err); + else + cb(null, result); + }); + } else cb(null, {}); + }; + + MySQL.prototype.dropForeignKeys = function(model, actualFks) { + var self = this; + var m = this.getModelDefinition(model); + + var fks = actualFks; + var sql = []; + var correctFks = m.settings.foreignKeys || {}; + + //drop foreign keys for removed fields + if (fks && fks.length) { + var removedFks = []; + fks.forEach(function(fk) { + var needsToDrop = false; + var newFk = correctFks[fk.fkName]; + if (newFk) { + 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 + needsToDrop = fkCol != fk.fkColumnName || + fkRefKey != fk.pkColumnName || + fkRefTable != fk.pkTableName; + } else { + needsToDrop = true; } - } - if (oldSettings.Type.toUpperCase() !== - self.buildColumnType(newSettings).toUpperCase()) { - return true; - } - return false; - } + if (needsToDrop) { + sql.push('DROP FOREIGN KEY ' + fk.fkName); + removedFks.push(fk); //keep track that we removed these + } + }); - function expectedColName(propName) { - return expectedColNameForModel(propName, m); - } - - function expectedColNameForModel(propName, modelToCheck) { - var mysql = modelToCheck.properties[propName].mysql; - if (typeof mysql === 'undefined') { - return propName; - } - var colName = mysql.columnName; - if (typeof colName === 'undefined') { - return propName; - } - return colName; + //update out list of existing keys by removing dropped keys + fks = actualFks.filter(function(k) { + return removedFks.indexOf(k) == -1; + }); } + return sql; }; MySQL.prototype.getAlterStatement = function(model, statements) { @@ -500,6 +491,65 @@ function mixinMigration(MySQL, mysql) { ''; }; + 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 statements = []; + + async.series([ + function(cb) { + statements = self.getAddModifyColumns(model, actualFields); + cb(); + }, + function(cb) { + statements = statements.concat(self.getDropColumns(model, actualFields)); + cb(); + }, + function(cb) { + statements = statements.concat(self.addIndexes(model, actualIndexes)); + cb(); + }, + function(cb) { + statements = statements.concat(self.dropForeignKeys(model, actualFks)); + cb(); + }, + ], function(err, result) { + if (err) done(err); + + //determine if there are column, index, or foreign keys changes (all require update) + if (statements.length) { + //get the required alter statements + var alterStmt = self.getAlterStatement(model, statements); + var stmtList = [alterStmt]; + + //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(';'), + }; + + //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) { + done(null, true, retValues); + } else { + //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(); + } + }); + }; + MySQL.prototype.buildForeignKeyDefinition = function(model, keyName) { var definition = this.getModelDefinition(model); @@ -850,4 +900,15 @@ function mixinMigration(MySQL, mysql) { } return columnType; } + function expectedColNameForModel(propName, modelToCheck) { + var mysql = modelToCheck.properties[propName].mysql; + if (typeof mysql === 'undefined') { + return propName; + } + var colName = mysql.columnName; + if (typeof colName === 'undefined') { + return propName; + } + return colName; + } }