Merge pull request #251 from strongloop/refactor-alter-table
Refactor alter table
This commit is contained in:
commit
e81de01aa6
451
lib/migration.js
451
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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue