private void FindOneToOnes(IMap map, IAnswerProvider answerProvider) { foreach (var column in map.Columns.Where(c => c.Value.Relationship == RelationshipType.ManyToOne)) { var otherMapCandidates = column.Value.ParentMap.Columns.Where( c => c.Value.Type == column.Value.Map.Type && (column.Value.ParentMap != map || c.Key != column.Key)).ToArray(); if (otherMapCandidates.Length == 0) { continue; } else if (otherMapCandidates.Length == 1) { column.Value.Relationship = RelationshipType.OneToOne; // one relationship coming back, assume one to one column.Value.OppositeColumn = otherMapCandidates.First().Value; } else { // we've got more than 1 foreign key coming back - let's ask the user var choices = otherMapCandidates.Select(c => new MultipleChoice<IColumn> { DisplayString = c.Key, Choice = c.Value }).ToList(); const string oneToOneText = "No matching column but one-to-one"; const string manyToOneText = "No matching column but many-to-one"; choices.Add(new MultipleChoice<IColumn> { DisplayString = oneToOneText, Choice = new Column<string> { Name = "One to One" } }); choices.Add(new MultipleChoice<IColumn> { DisplayString = manyToOneText, Choice = new Column<string> { Name = "Many to One" } }); var oppositeColumn = answerProvider.GetMultipleChoiceAnswer( "The column " + column.Key + " on " + column.Value.Map.Table + " has multiple incoming relationships. Which column on the related table is the other side of the one-to-one relationship?", choices); if (oppositeColumn.DisplayString == manyToOneText) { continue; // many to one } column.Value.Relationship = RelationshipType.OneToOne; if (oppositeColumn.DisplayString != oneToOneText) { column.Value.OppositeColumn = oppositeColumn.Choice; } } } }
public IEnumerable<IMap> ReverseEngineer( DatabaseSchema schema, ISqlDialect sqlDialect, IEnumerable<string> tablesToIgnore, IAnswerProvider answerProvider, bool fixOneToOnes) { if (tablesToIgnore == null) { tablesToIgnore = new string[0]; } var maps = new List<IMap>(); this.configuration = new Configuration(sqlDialect); foreach (var table in schema.Tables.Where(t => !tablesToIgnore.Contains(t.Name))) { maps.Add(this.MapTable(table)); } // go back through and add indexes and foreign keys foreach (var map in maps) { GetIndexesAndForeignKeys(schema.Tables.First(t => t.Name == map.Table), map); } // go back through and try to spot one-to-one columns if (fixOneToOnes) { foreach (var map in maps) { FindOneToOnes(map, answerProvider); } } return maps; }
public AnswerController( ApplicationDbContext dbContext, IAnswerProvider DataProcessor, RoleManager <IdentityRole> roleManager, UserManager <ApplicationUser> userManager, IConfiguration configuration) : base(dbContext, roleManager, userManager, configuration) { dataProcessor = DataProcessor; }
private void FindOneToOnes(IMap map, IAnswerProvider answerProvider) { foreach (var column in map.Columns.Where(c => c.Value.Relationship == RelationshipType.ManyToOne)) { var otherMapCandidates = column.Value.ParentMap.Columns.Where( c => c.Value.Type == column.Value.Map.Type && (column.Value.ParentMap != map || c.Key != column.Key)).ToArray(); if (otherMapCandidates.Length == 0) { continue; } else if (otherMapCandidates.Length == 1) { column.Value.Relationship = RelationshipType.OneToOne; // one relationship coming back, assume one to one column.Value.OppositeColumn = otherMapCandidates.First().Value; } else { // we've got more than 1 foreign key coming back - let's ask the user var choices = otherMapCandidates.Select(c => new MultipleChoice <IColumn> { DisplayString = c.Key, Choice = c.Value }).ToList(); const string oneToOneText = "No matching column but one-to-one"; const string manyToOneText = "No matching column but many-to-one"; choices.Add(new MultipleChoice <IColumn> { DisplayString = oneToOneText, Choice = new Column <string> { Name = "One to One" } }); choices.Add(new MultipleChoice <IColumn> { DisplayString = manyToOneText, Choice = new Column <string> { Name = "Many to One" } }); var oppositeColumn = answerProvider.GetMultipleChoiceAnswer( "The column " + column.Key + " on " + column.Value.Map.Table + " has multiple incoming relationships. Which column on the related table is the other side of the one-to-one relationship?", choices); if (oppositeColumn.DisplayString == manyToOneText) { continue; // many to one } column.Value.Relationship = RelationshipType.OneToOne; if (oppositeColumn.DisplayString != oneToOneText) { column.Value.OppositeColumn = oppositeColumn.Choice; } } } }
public IDictionary<string, string> GenerateFiles( IEnumerable<IMap> maps, DatabaseSchema schema, string domainNamespace, IAnswerProvider answerProvider) { // note that we're just doing string building here // simple POCOs and CodeDom does not support auto-properties // and frankly I wouldn't want these things to have backing fields in the source code var result = new Dictionary<string, string>(); // iterate over the configuration generating classes foreach (var map in maps) { this.GenerateClass(result, map, schema, domainNamespace); } return result; }
public IDictionary <string, string> GenerateFiles( IEnumerable <IMap> maps, Database schema, string domainNamespace, IAnswerProvider answerProvider) { // note that we're just doing string building here // simple POCOs and CodeDom does not support auto-properties // and frankly I wouldn't want these things to have backing fields in the source code var result = new Dictionary <string, string>(); // iterate over the configuration generating classes foreach (var map in maps) { this.GenerateClass(result, map, schema, domainNamespace); } return(result); }
public IEnumerable <IMap> ReverseEngineer( DatabaseSchema schema, ISqlDialect sqlDialect, IEnumerable <string> tablesToIgnore, IAnswerProvider answerProvider, bool fixOneToOnes) { if (tablesToIgnore == null) { tablesToIgnore = new string[0]; } var maps = new List <IMap>(); this.configuration = new Configuration(sqlDialect); foreach (var table in schema.Tables.Where(t => !tablesToIgnore.Contains(t.Name))) { maps.Add(this.MapTable(table)); } // go back through and add indexes and foreign keys foreach (var map in maps) { GetIndexesAndForeignKeys(schema.Tables.First(t => t.Name == map.Table), map); } // go back through and try to spot one-to-one columns if (fixOneToOnes) { foreach (var map in maps) { FindOneToOnes(map, answerProvider); } } return(maps); }
public string GenerateSqlDiff( IEnumerable <IMap> fromMaps, IEnumerable <IMap> toMaps, IAnswerProvider answerProvider, ILogger logger, IEnumerable <string> indexesToIgnore, out IEnumerable <string> warnings, out IEnumerable <string> errors) { // fetch data for current database IDictionary <string, Statistics> currentData = new Dictionary <string, Statistics>(); if (fromMaps.Any()) { currentData = this.statisticsProvider.GetStatistics(fromMaps); } var sql = new StringBuilder(); var warningList = new List <string>(); var errorList = new List <string>(); var renamePrimaryKeyModifications = new Dictionary <Tuple <string, string>, bool>(); var from = fromMaps.ToArray(); var to = toMaps.ToArray(); // get additions and removals var mapComparer = new TableNameEqualityComparer(); var additions = to.Except(from, mapComparer).ToList(); var removals = from.Except(to, mapComparer).ToList(); var matches = from.Join(to, f => f.Table, t => t.Table, MigrationPair.Of).ToList(); // trace output logger.Trace("Additions:"); logger.Trace(""); logger.Trace(additions.Select(a => new { a.Table, a.Type.Name }), new[] { "Table", "Map Name" }); logger.Trace("Removals:"); logger.Trace(""); logger.Trace(removals.Select(r => new { r.Table, r.Type.Name }), new[] { "Table", "Map Name" }); logger.Trace("Matches:"); logger.Trace(""); logger.Trace( matches.Select(m => new { FromTable = m.From.Table, FromMap = m.From.Type.Name, ToTable = m.To.Table, ToMap = m.To.Type.Name }), new[] { "From Table", "From Map", "To Table", "To Map" }); // look for possible entity name changes if (additions.Any() && removals.Any()) { // TODO do something a bit more sensible with regards to likelihood of rename foreach (var removed in removals.Select(r => r).ToArray()) { // copy the array as we'll update var answer = answerProvider.GetMultipleChoiceAnswer( string.Format("The entity {0} has been removed. If it has been renamed please specify what to:", removed.Type.Name), new[] { new MultipleChoice <string> { DisplayString = "Not renamed - please delete", Choice = NoRename } }.Union( additions.Select(a => new MultipleChoice <string> { Choice = a.Type.Name, DisplayString = a.Type.Name }))); if (answer.Choice != NoRename) { // rename the table var renameFrom = removed; var renameTo = additions.First(a => a.Type.Name == answer.Choice); sql.AppendSql(this.alterTableWriter.RenameTable(renameFrom, renameTo)); // add to the matches matches.Add(MigrationPair.Of(renameFrom, renameTo)); // modify additions and removals removals.Remove(renameFrom); additions.Remove(renameTo); // sort out the primary key var fromPrimaryKey = renameFrom.PrimaryKey; var toPrimaryKey = renameTo.PrimaryKey; if (!AreColumnDefinitionsEqual(fromPrimaryKey, toPrimaryKey)) { if (fromPrimaryKey.DbName != toPrimaryKey.DbName && fromPrimaryKey.DbType == toPrimaryKey.DbType && (!fromPrimaryKey.DbType.TypeTakesLength() || (fromPrimaryKey.MaxLength && toPrimaryKey.MaxLength) || (fromPrimaryKey.Length == toPrimaryKey.Length)) && (!fromPrimaryKey.DbType.TypeTakesPrecisionAndScale() || (fromPrimaryKey.Precision == toPrimaryKey.Precision && fromPrimaryKey.Scale == toPrimaryKey.Scale))) { // just a change in name sql.AppendSql(this.alterTableWriter.ChangeColumnName(fromPrimaryKey, toPrimaryKey)); } else { // ask the question // TODO may things more sensible based on the type var attemptChange = answerProvider.GetBooleanAnswer( "The primary key change required for this table rename involves a change " + (fromPrimaryKey.DbType != toPrimaryKey.DbType ? "of data type" : "of specification") + ". Would you like to attempt the change? (Selecting No will drop and re-create the column)"); if (attemptChange) { if (fromPrimaryKey.DbName != toPrimaryKey.DbName) { sql.AppendSql(this.alterTableWriter.ChangeColumnName(fromPrimaryKey, toPrimaryKey)); } sql.AppendSql(this.alterTableWriter.ModifyColumn(fromPrimaryKey, toPrimaryKey)); renamePrimaryKeyModifications.Add(Tuple.Create(fromPrimaryKey.Map.Type.Name, toPrimaryKey.Map.Type.Name), true); } else { // drop and re-create sql.AppendSql(this.alterTableWriter.DropColumn(fromPrimaryKey)); sql.AppendSql(this.alterTableWriter.AddColumn(toPrimaryKey)); renamePrimaryKeyModifications.Add(Tuple.Create(fromPrimaryKey.Map.Type.Name, toPrimaryKey.Map.Type.Name), false); } } } } } } // do removal of foreign keys and indexes that we don't need // only do this on remaining tables as drops should be deleted automatically foreach (var matchPair in matches) { var fkRemovals = matchPair.From.ForeignKeys.Except(matchPair.To.ForeignKeys); foreach (var foreignKey in fkRemovals) { sql.AppendSql(this.alterTableWriter.DropForeignKey(foreignKey)); } var indexRemovals = matchPair.From.Indexes.Except(matchPair.To.Indexes); foreach (var index in indexRemovals.Where(i => !indexesToIgnore.Contains(i.Name))) { sql.AppendSql(this.alterTableWriter.DropIndex(index)); } } // do renames of columns var columnKeyValuePairEqualityComparer = new ColumnKeyValuePairEqualityComparer(); var addedProperties = new List <IColumn>(); foreach (var pair in matches) { var fromCols = pair.From.OwnedColumns(true).ToDictionary(c => c.DbName, c => c); var toCols = pair.To.OwnedColumns(true).ToDictionary(c => c.DbName, c => c); var removedColumns = fromCols.Except(toCols, columnKeyValuePairEqualityComparer); var addedColumns = toCols.Except(fromCols, columnKeyValuePairEqualityComparer).ToList(); if (removedColumns.Any()) { // handle drops and renames foreach (var removal in removedColumns) { if (pair.From.Type.Name != pair.To.Type.Name && removal.Value.IsPrimaryKey) { // ignore this one and get the new pk and remove it as handled above var newPrimaryKey = toCols.Single(c => c.Value.IsPrimaryKey); addedColumns.Remove(newPrimaryKey); continue; } if (addedColumns.Any()) { var answer = answerProvider.GetMultipleChoiceAnswer( string.Format( "The property {0} has been removed. If it has been renamed please specify what to:", removal.Value.Name), new[] { new MultipleChoice <string> { DisplayString = "Not renamed - please delete", Choice = NoRename } }.Union( addedColumns.Select(a => new MultipleChoice <string> { Choice = a.Value.Name, DisplayString = a.Value.Name }))); if (answer.Choice == NoRename) { // drop the column if (pair.From.Type.Name != pair.To.Type.Name) { removal.Value.Map = pair.To; // want to delete from the correctly named table in the event of a rename } sql.AppendSql(this.alterTableWriter.DropColumn(removal.Value)); } else { // rename the column var toColumn = addedColumns.First(c => c.Value.Name == answer.Choice); sql.AppendSql(this.alterTableWriter.ChangeColumnName(removal.Value, toColumn.Value)); // if need be perform a modify statement if (this.RequiresColumnSpecificationChange(removal.Value, toColumn.Value)) { sql.AppendSql(this.alterTableWriter.ModifyColumn(removal.Value, toColumn.Value)); } // remove the column from the additions addedColumns.Remove(toColumn); } } else { // drop the column if (pair.From.Type.Name != pair.To.Type.Name) { removal.Value.Map = pair.To; // want to delete from the correctly named table in the event of a rename } sql.AppendSql(this.alterTableWriter.DropColumn(removal.Value)); } } } // go through existing columns and handle modifications foreach (var fromProp in pair.From.Columns) { logger.Trace("Examining {1}.{0}", fromProp.Value.Name, pair.From.Table); var matchingToProp = pair.To.Columns.Select(p => p.Value).FirstOrDefault(p => p.Name == fromProp.Key); if (matchingToProp != null) { if (this.RequiresColumnSpecificationChange(fromProp.Value, matchingToProp)) { // check for potential errors if (fromProp.Value.DbType != matchingToProp.DbType) { bool skipQuestion = false; bool wasPrimaryKeyDroppedAndRecreated = false; var renamePrimaryKeyModificationsKey = Tuple.Create( fromProp.Value.Relationship == RelationshipType.ManyToOne ? fromProp.Value.ParentMap.Type.Name : fromProp.Value.OppositeColumn.ParentMap.Type.Name, matchingToProp.Relationship == RelationshipType.ManyToOne ? matchingToProp.ParentMap.Type.Name : matchingToProp.OppositeColumn.Map.Type.Name); if ((fromProp.Value.Relationship == RelationshipType.OneToOne || fromProp.Value.Relationship == RelationshipType.ManyToOne) && (matchingToProp.Relationship == RelationshipType.ManyToOne || matchingToProp.Relationship == RelationshipType.OneToOne) && renamePrimaryKeyModifications.ContainsKey(renamePrimaryKeyModificationsKey)) { // skip the question as we've already attempted the modify for the pk so may as well here as well! skipQuestion = true; wasPrimaryKeyDroppedAndRecreated = !renamePrimaryKeyModifications[renamePrimaryKeyModificationsKey]; } bool dropAndRecreate = wasPrimaryKeyDroppedAndRecreated; if (!skipQuestion) { dropAndRecreate = answerProvider.GetBooleanAnswer( string.Format( "Attempting to change DbType for property {0} on {1} from {2} to {3}. Would you like to attempt the change? (selecting \"No\" will drop and re-create the column)", matchingToProp.Name, matchingToProp.Map.Type.Name, fromProp.Value.DbType, matchingToProp.DbType)); } if (dropAndRecreate) { sql.AppendSql(this.alterTableWriter.DropColumn(matchingToProp)); sql.AppendSql(this.alterTableWriter.AddColumn(matchingToProp)); continue; } warningList.Add( string.Format( "Changing DB Type is not guaranteed to work: {0} on {1}", fromProp.Value.Name, fromProp.Value.Map.Type.Name)); } if ((this.RequiresLengthChange(fromProp.Value, matchingToProp) && (fromProp.Value.MaxLength || fromProp.Value.Length < matchingToProp.Length)) || (this.RequiresPrecisionOrScaleChange(fromProp.Value, matchingToProp) && (fromProp.Value.Precision > matchingToProp.Precision || fromProp.Value.Scale > matchingToProp.Scale))) { warningList.Add( string.Format( "{0} on {1} is having its precision, scale or length reduced. This may result in loss of data", fromProp.Value.Name, fromProp.Value.Map.Type.Name)); } sql.AppendSql(this.alterTableWriter.ModifyColumn(fromProp.Value, matchingToProp)); } else { if ((matchingToProp.Relationship == RelationshipType.ManyToOne || matchingToProp.Relationship == RelationshipType.OneToOne) && (fromProp.Value.Relationship == RelationshipType.ManyToOne || fromProp.Value.Relationship == RelationshipType.OneToOne) && fromProp.Value.Type.Name != matchingToProp.Type.Name && currentData != null && currentData[matchingToProp.Map.Type.Name].HasRows) { warningList.Add( string.Format( "Property {0} on {1} has changed type but the column was not dropped. There is data in that table, please empty that column if necessary", matchingToProp.Name, matchingToProp.Map.Type.Name)); } } } } // add the added columns to the addedProperties list addedProperties.AddRange(addedColumns.Select(c => c.Value)); } // do deletes of entities foreach (var removal in removals) { sql.AppendSql(this.dropTableWriter.DropTable(removal)); } // do additions of entities foreach (var addition in additions) { sql.AppendSql(this.createTableWriter.CreateTable(addition)); } // do additions of properties foreach (var newProp in addedProperties) { // check for relationships where the related table is not empty and the prop is not null if ((newProp.Relationship == RelationshipType.ManyToOne || newProp.Relationship == RelationshipType.OneToOne) && !newProp.IsNullable && string.IsNullOrWhiteSpace(newProp.Default) && currentData.ContainsKey(newProp.Map.Type.Name) && currentData[newProp.Map.Type.Name].HasRows) { var foreignKeyPrimaryKeyType = newProp.Relationship == RelationshipType.ManyToOne ? newProp.ParentMap.PrimaryKey.Type : newProp.OppositeColumn.Map.PrimaryKey.Type; var answer = answerProvider.GetType() .GetMethod("GetAnswer") .MakeGenericMethod(foreignKeyPrimaryKeyType) .Invoke( answerProvider, new object[] { string.Format( "You are adding a property {0} on {1} that has a foreign key to {2} (which is non-empty) with primary key type {3}. Please specify a default value for the column:", newProp.Name, newProp.Map.Type.Name, newProp.Type.Name, foreignKeyPrimaryKeyType) }); newProp.Default = answer.ToString(); } sql.AppendSql(this.alterTableWriter.AddColumn(newProp)); } // add in new foreign keys for additions foreach (var map in additions) { var statements = this.createTableWriter.CreateForeignKeys(map); foreach (var statement in statements) { sql.AppendSql(statement); } } // add in new indexes for additions foreach (var map in additions) { var statements = this.createTableWriter.CreateIndexes(map); foreach (var statement in statements) { sql.AppendSql(statement); } } // add in missing foreign keys and indexes foreach (var matchPair in matches) { var fkAdditions = matchPair.To.ForeignKeys.Except(matchPair.From.ForeignKeys); var fkStatements = this.createTableWriter.CreateForeignKeys(fkAdditions); foreach (var statement in fkStatements) { sql.AppendSql(statement); } var indexAdditions = matchPair.To.Indexes.Except(matchPair.From.Indexes); var indexStatements = this.createTableWriter.CreateIndexes(indexAdditions); foreach (var statement in indexStatements) { sql.AppendSql(statement); } } warnings = warningList; errors = errorList; return(sql.ToString()); }
private static void InnerMain(string[] args) { var options = new CommandLineOptions(); if (!Parser.Default.ParseArguments(args, options)) { ShowHelpText(options); return; } isVerbose = options.Verbose; // weaving if (options.Weave) { if (string.IsNullOrWhiteSpace(options.WeaveDir)) { throw new CatchyException("You must specify the directory to weave"); } if (!options.IgnorePeVerify) { TryFindIgnoreConfigSetting(options); } var task = new ExtendDomainTask { LaunchDebugger = options.LaunchDebugger, WeaveDir = options.WeaveDir, Logger = new ConsoleLogger(options.Verbose), IgnorePEVerify = options.IgnorePeVerify }; if (!task.Execute()) { throw new CatchyException("Weaving failed"); } return; } // prevalidation if (string.IsNullOrWhiteSpace(options.ConfigPath)) { throw new CatchyException("You must specify a configuration path or a project name"); } if (!File.Exists(options.ConfigPath)) { throw new CatchyException("Could not locate configuration file {0}", options.ConfigPath); } // dependency init consoleAnswerProvider = new ConsoleAnswerProvider("~" + Path.GetFileNameWithoutExtension(options.ConfigPath) + ".answers"); // parse all of the configuration stuffs ConnectionStringSettings connectionStringSettings; DashingSettings dashingSettings; ReverseEngineerSettings reverseEngineerSettings; ParseIni(options, out connectionStringSettings, out dashingSettings, out reverseEngineerSettings); // postvalidation if (!File.Exists(dashingSettings.PathToDll)) { throw new CatchyException("Could not locate {0}", dashingSettings.PathToDll); } // load the configuration NOW and try to inherit its version of Dashing, Dapper, etc var configAssembly = Assembly.LoadFrom(dashingSettings.PathToDll); GC.KeepAlive(configAssembly); configObject = LoadConfiguration(configAssembly, dashingSettings, connectionStringSettings); // now decide what to do if (options.Script) { DoScript(options.Location, options.Naive, connectionStringSettings, dashingSettings, reverseEngineerSettings); } else if (options.Seed) { DoSeed(connectionStringSettings); } else if (options.Migration) { DoMigrate(options.Naive, connectionStringSettings, dashingSettings, reverseEngineerSettings); } else if (options.ReverseEngineer) { DoReverseEngineer(options, dashingSettings, reverseEngineerSettings, connectionStringSettings); } else { ShowHelpText(options); } }
public AnswerController(IAnswerProvider answerProvider) { _answerProvider = answerProvider; }
public void Execute(IConfiguration configuration, string connectionString, string providerName, IEnumerable <string> tablesToIgnore, IEnumerable <string> indexesToIgnore, IEnumerable <KeyValuePair <string, string> > extraPluralizationWords, IAnswerProvider answerProvider) { var script = this.scriptGenerator.Generate(configuration, connectionString, providerName, tablesToIgnore, indexesToIgnore, extraPluralizationWords, answerProvider); if (string.IsNullOrWhiteSpace(script)) { using (new ColorContext(ConsoleColor.Green)) { Log.Logger.Information("No migration script to run"); return; } } // set up the database connection var dialectFactory = new DialectFactory(); var dialect = dialectFactory.Create(providerName, connectionString); var dbProviderFactoryFactory = new DbProviderFactoryFactory(); var dbProviderFactory = dbProviderFactoryFactory.Create(providerName, connectionString); dbProviderFactory.CreateDatabaseIfNotExists(connectionString, providerName, dialect); // run the script using (var connection = dbProviderFactory.CreateConnection()) { connection.ConnectionString = connectionString; connection.Open(); connection.Execute(script); } }
private string GenerateScript(IEnumerable <IMap> fromMaps, IEnumerable <IMap> configurationMaps, ISqlDialect dialect, IStatisticsProvider statisticsProvider, IAnswerProvider answerProvider, IEnumerable <string> tablesToIgnore, IEnumerable <string> indexesToIgnore) { var migrator = new Migrator( dialect, new CreateTableWriter(dialect), new AlterTableWriter(dialect), new DropTableWriter(dialect), statisticsProvider); IEnumerable <string> warnings; IEnumerable <string> errors; return(migrator.GenerateSqlDiff( fromMaps, configurationMaps, answerProvider, indexesToIgnore, tablesToIgnore, out warnings, out errors)); }
public string Generate(IConfiguration configuration, string connectionString, string providerName, IEnumerable <string> tablesToIgnore, IEnumerable <string> indexesToIgnore, IEnumerable <KeyValuePair <string, string> > extraPluralizationWords, IAnswerProvider answerProvider) { // set up the database connection var dialectFactory = new DialectFactory(); var dialect = dialectFactory.Create(providerName, connectionString); var dbProviderFactoryFactory = new DbProviderFactoryFactory(); var dbProviderFactory = dbProviderFactoryFactory.Create(providerName, connectionString); IEnumerable <IMap> fromMaps; if (!dbProviderFactory.DatabaseExists(connectionString, providerName, dialect)) { fromMaps = Enumerable.Empty <IMap>(); return(this.GenerateScript(fromMaps, configuration.Maps, dialect, new NullStatisticsProvider(), answerProvider, tablesToIgnore, indexesToIgnore)); } else { // get the schema from the existing database var schemaReaderFactory = new SchemaReaderFactory(); var schemaReader = schemaReaderFactory.GetSchemaReader(providerName); var connectionStringManipulator = new ConnectionStringManipulator(dbProviderFactory, connectionString); using (var connection = dbProviderFactory.CreateConnection()) { connection.ConnectionString = connectionString; connection.Open(); var schema = schemaReader.Read(connection, connectionStringManipulator.GetDatabaseName()); // reverse engineer the maps var engineer = new Engineer(extraPluralizationWords.Union(configuration.Maps.Select(m => new KeyValuePair <string, string>(m.Type.Name, m.Table)))); // we use our configuration to inform us as to the correct naming of tables fromMaps = engineer.ReverseEngineer(schema, dialect, tablesToIgnore, answerProvider, false); return(this.GenerateScript(fromMaps, configuration.Maps, dialect, new StatisticsProvider(connection, dialect), answerProvider, tablesToIgnore, indexesToIgnore)); } } }
public string GenerateSqlDiff( IEnumerable<IMap> fromMaps, IEnumerable<IMap> toMaps, IAnswerProvider answerProvider, ILogger logger, IEnumerable<string> indexesToIgnore, IEnumerable<string> tablesToIgnore, out IEnumerable<string> warnings, out IEnumerable<string> errors) { // fetch data for current database IDictionary<string, Statistics> currentData = new Dictionary<string, Statistics>(); if (fromMaps.Any()) { currentData = this.statisticsProvider.GetStatistics(fromMaps); } var sql = new StringBuilder(); var warningList = new List<string>(); var errorList = new List<string>(); var renamePrimaryKeyModifications = new Dictionary<Tuple<string, string>, bool>(); var from = fromMaps.ToArray(); var to = toMaps.ToArray(); // get additions and removals var mapComparer = new TableNameEqualityComparer(); var additions = to.Except(from, mapComparer).Where(m => !tablesToIgnore.Contains(m.Table)).ToList(); var removals = from.Except(to, mapComparer).ToList(); var matches = from.Join(to, f => f.Table.ToLowerInvariant(), t => t.Table.ToLowerInvariant(), MigrationPair.Of).ToList(); // trace output logger.Trace("Additions:"); logger.Trace(""); logger.Trace(additions.Select(a => new { a.Table, a.Type.Name }), new[] { "Table", "Map Name" }); logger.Trace("Removals:"); logger.Trace(""); logger.Trace(removals.Select(r => new { r.Table, r.Type.Name }), new[] { "Table", "Map Name" }); logger.Trace("Matches:"); logger.Trace(""); logger.Trace( matches.Select(m => new { FromTable = m.From.Table, FromMap = m.From.Type.Name, ToTable = m.To.Table, ToMap = m.To.Type.Name }), new[] { "From Table", "From Map", "To Table", "To Map" }); // look for possible entity name changes if (additions.Any() && removals.Any()) { // TODO do something a bit more sensible with regards to likelihood of rename foreach (var removed in removals.Select(r => r).ToArray()) { // copy the array as we'll update var answer = answerProvider.GetMultipleChoiceAnswer( string.Format("The entity {0} has been removed. If it has been renamed please specify what to:", removed.Type.Name), new[] { new MultipleChoice<string> { DisplayString = "Not renamed - please delete", Choice = NoRename } }.Union( additions.Select(a => new MultipleChoice<string> { Choice = a.Type.Name, DisplayString = a.Type.Name }))); if (answer.Choice != NoRename) { // rename the table var renameFrom = removed; var renameTo = additions.First(a => a.Type.Name == answer.Choice); sql.AppendSql(this.alterTableWriter.RenameTable(renameFrom, renameTo)); // add to the matches matches.Add(MigrationPair.Of(renameFrom, renameTo)); // modify additions and removals removals.Remove(renameFrom); additions.Remove(renameTo); // sort out the primary key var fromPrimaryKey = renameFrom.PrimaryKey; var toPrimaryKey = renameTo.PrimaryKey; if (!AreColumnDefinitionsEqual(fromPrimaryKey, toPrimaryKey)) { if (fromPrimaryKey.DbName != toPrimaryKey.DbName && fromPrimaryKey.DbType == toPrimaryKey.DbType && (!fromPrimaryKey.DbType.TypeTakesLength() || (fromPrimaryKey.MaxLength && toPrimaryKey.MaxLength) || (fromPrimaryKey.Length == toPrimaryKey.Length)) && (!fromPrimaryKey.DbType.TypeTakesPrecisionAndScale() || (fromPrimaryKey.Precision == toPrimaryKey.Precision && fromPrimaryKey.Scale == toPrimaryKey.Scale))) { // just a change in name sql.AppendSql(this.alterTableWriter.ChangeColumnName(fromPrimaryKey, toPrimaryKey)); } else { // ask the question // TODO may things more sensible based on the type var attemptChange = answerProvider.GetBooleanAnswer( "The primary key change required for this table rename involves a change " + (fromPrimaryKey.DbType != toPrimaryKey.DbType ? "of data type" : "of specification") + ". Would you like to attempt the change? (Selecting No will drop and re-create the column)"); if (attemptChange) { if (fromPrimaryKey.DbName != toPrimaryKey.DbName) { sql.AppendSql(this.alterTableWriter.ChangeColumnName(fromPrimaryKey, toPrimaryKey)); } sql.AppendSql(this.alterTableWriter.ModifyColumn(fromPrimaryKey, toPrimaryKey)); renamePrimaryKeyModifications.Add(Tuple.Create(fromPrimaryKey.Map.Type.Name, toPrimaryKey.Map.Type.Name), true); } else { // drop and re-create sql.AppendSql(this.alterTableWriter.DropColumn(fromPrimaryKey)); sql.AppendSql(this.alterTableWriter.AddColumn(toPrimaryKey)); renamePrimaryKeyModifications.Add(Tuple.Create(fromPrimaryKey.Map.Type.Name, toPrimaryKey.Map.Type.Name), false); } } } } } } // do removal of foreign keys and indexes that we don't need // only do this on remaining tables as drops should be deleted automatically foreach (var matchPair in matches) { var fkRemovals = matchPair.From.ForeignKeys.Except(matchPair.To.ForeignKeys); foreach (var foreignKey in fkRemovals) { sql.AppendSql(this.alterTableWriter.DropForeignKey(foreignKey)); } var indexRemovals = matchPair.From.Indexes.Except(matchPair.To.Indexes); foreach (var index in indexRemovals.Where(i => !indexesToIgnore.Contains(i.Name))) { sql.AppendSql(this.alterTableWriter.DropIndex(index)); } } // do renames of columns var columnKeyValuePairEqualityComparer = new ColumnKeyValuePairEqualityComparer(); var addedProperties = new List<IColumn>(); foreach (var pair in matches) { var fromCols = pair.From.OwnedColumns(true).ToDictionary(c => c.DbName, c => c); var toCols = pair.To.OwnedColumns(true).ToDictionary(c => c.DbName, c => c); var removedColumns = fromCols.Except(toCols, columnKeyValuePairEqualityComparer); var addedColumns = toCols.Except(fromCols, columnKeyValuePairEqualityComparer).ToList(); if (removedColumns.Any()) { // handle drops and renames foreach (var removal in removedColumns) { if (pair.From.Type.Name != pair.To.Type.Name && removal.Value.IsPrimaryKey) { // ignore this one and get the new pk and remove it as handled above var newPrimaryKey = toCols.Single(c => c.Value.IsPrimaryKey); addedColumns.Remove(newPrimaryKey); continue; } if (addedColumns.Any()) { var answer = answerProvider.GetMultipleChoiceAnswer( string.Format( "The property {0} has been removed. If it has been renamed please specify what to:", removal.Value.Name), new[] { new MultipleChoice<string> { DisplayString = "Not renamed - please delete", Choice = NoRename } }.Union( addedColumns.Select(a => new MultipleChoice<string> { Choice = a.Value.Name, DisplayString = a.Value.Name }))); if (answer.Choice == NoRename) { // drop the column if (pair.From.Type.Name != pair.To.Type.Name) { removal.Value.Map = pair.To; // want to delete from the correctly named table in the event of a rename } sql.AppendSql(this.alterTableWriter.DropColumn(removal.Value)); } else { // rename the column var toColumn = addedColumns.First(c => c.Value.Name == answer.Choice); sql.AppendSql(this.alterTableWriter.ChangeColumnName(removal.Value, toColumn.Value)); // if need be perform a modify statement if (this.RequiresColumnSpecificationChange(removal.Value, toColumn.Value)) { sql.AppendSql(this.alterTableWriter.ModifyColumn(removal.Value, toColumn.Value)); } // remove the column from the additions addedColumns.Remove(toColumn); } } else { // drop the column if (pair.From.Type.Name != pair.To.Type.Name) { removal.Value.Map = pair.To; // want to delete from the correctly named table in the event of a rename } sql.AppendSql(this.alterTableWriter.DropColumn(removal.Value)); } } } // go through existing columns and handle modifications foreach (var fromProp in pair.From.Columns) { logger.Trace("Examining {1}.{0}", fromProp.Value.Name, pair.From.Table); var matchingToProp = pair.To.Columns.Select(p => p.Value).FirstOrDefault(p => p.Name == fromProp.Key); if (matchingToProp != null) { if (this.RequiresColumnSpecificationChange(fromProp.Value, matchingToProp)) { // check for potential errors if (fromProp.Value.DbType != matchingToProp.DbType) { bool skipQuestion = false; bool wasPrimaryKeyDroppedAndRecreated = false; var renamePrimaryKeyModificationsKey = Tuple.Create( fromProp.Value.Relationship == RelationshipType.ManyToOne ? fromProp.Value.ParentMap.Type.Name : fromProp.Value.OppositeColumn.ParentMap.Type.Name, matchingToProp.Relationship == RelationshipType.ManyToOne ? matchingToProp.ParentMap.Type.Name : matchingToProp.OppositeColumn.Map.Type.Name); if ((fromProp.Value.Relationship == RelationshipType.OneToOne || fromProp.Value.Relationship == RelationshipType.ManyToOne) && (matchingToProp.Relationship == RelationshipType.ManyToOne || matchingToProp.Relationship == RelationshipType.OneToOne) && renamePrimaryKeyModifications.ContainsKey(renamePrimaryKeyModificationsKey)) { // skip the question as we've already attempted the modify for the pk so may as well here as well! skipQuestion = true; wasPrimaryKeyDroppedAndRecreated = !renamePrimaryKeyModifications[renamePrimaryKeyModificationsKey]; } bool dropAndRecreate = wasPrimaryKeyDroppedAndRecreated; if (!skipQuestion) { dropAndRecreate = answerProvider.GetBooleanAnswer( string.Format( "Attempting to change DbType for property {0} on {1} from {2} to {3}. Would you like to attempt the change? (selecting \"No\" will drop and re-create the column)", matchingToProp.Name, matchingToProp.Map.Type.Name, fromProp.Value.DbType, matchingToProp.DbType)); } if (dropAndRecreate) { sql.AppendSql(this.alterTableWriter.DropColumn(matchingToProp)); sql.AppendSql(this.alterTableWriter.AddColumn(matchingToProp)); continue; } warningList.Add( string.Format( "Changing DB Type is not guaranteed to work: {0} on {1}", fromProp.Value.Name, fromProp.Value.Map.Type.Name)); } if ((this.RequiresLengthChange(fromProp.Value, matchingToProp) && (fromProp.Value.MaxLength || fromProp.Value.Length < matchingToProp.Length)) || (this.RequiresPrecisionOrScaleChange(fromProp.Value, matchingToProp) && (fromProp.Value.Precision > matchingToProp.Precision || fromProp.Value.Scale > matchingToProp.Scale))) { warningList.Add( string.Format( "{0} on {1} is having its precision, scale or length reduced. This may result in loss of data", fromProp.Value.Name, fromProp.Value.Map.Type.Name)); } sql.AppendSql(this.alterTableWriter.ModifyColumn(fromProp.Value, matchingToProp)); } else { if ((matchingToProp.Relationship == RelationshipType.ManyToOne || matchingToProp.Relationship == RelationshipType.OneToOne) && (fromProp.Value.Relationship == RelationshipType.ManyToOne || fromProp.Value.Relationship == RelationshipType.OneToOne) && fromProp.Value.Type.Name != matchingToProp.Type.Name && currentData != null && currentData[matchingToProp.Map.Type.Name].HasRows) { warningList.Add( string.Format( "Property {0} on {1} has changed type but the column was not dropped. There is data in that table, please empty that column if necessary", matchingToProp.Name, matchingToProp.Map.Type.Name)); } } } } // add the added columns to the addedProperties list addedProperties.AddRange(addedColumns.Select(c => c.Value)); } // do deletes of entities foreach (var removal in removals) { sql.AppendSql(this.dropTableWriter.DropTable(removal)); } // do additions of entities foreach (var addition in additions) { sql.AppendSql(this.createTableWriter.CreateTable(addition)); } // do additions of properties foreach (var newProp in addedProperties) { // check for relationships where the related table is not empty and the prop is not null if ((newProp.Relationship == RelationshipType.ManyToOne || newProp.Relationship == RelationshipType.OneToOne) && !newProp.IsNullable && string.IsNullOrWhiteSpace(newProp.Default) && currentData.ContainsKey(newProp.Map.Type.Name) && currentData[newProp.Map.Type.Name].HasRows) { var foreignKeyPrimaryKeyType = newProp.Relationship == RelationshipType.ManyToOne ? newProp.ParentMap.PrimaryKey.Type : newProp.OppositeColumn.Map.PrimaryKey.Type; var answer = answerProvider.GetType() .GetMethod("GetAnswer") .MakeGenericMethod(foreignKeyPrimaryKeyType) .Invoke( answerProvider, new object[] { string.Format( "You are adding a property {0} on {1} that has a foreign key to {2} (which is non-empty) with primary key type {3}. Please specify a default value for the column:", newProp.Name, newProp.Map.Type.Name, newProp.Type.Name, foreignKeyPrimaryKeyType) }); newProp.Default = answer.ToString(); } sql.AppendSql(this.alterTableWriter.AddColumn(newProp)); } // add in new foreign keys for additions foreach (var map in additions) { var statements = this.createTableWriter.CreateForeignKeys(map); foreach (var statement in statements) { sql.AppendSql(statement); } } // add in new indexes for additions foreach (var map in additions) { var statements = this.createTableWriter.CreateIndexes(map); foreach (var statement in statements) { sql.AppendSql(statement); } } // add in missing foreign keys and indexes foreach (var matchPair in matches) { var fkAdditions = matchPair.To.ForeignKeys.Except(matchPair.From.ForeignKeys); var fkStatements = this.createTableWriter.CreateForeignKeys(fkAdditions); foreach (var statement in fkStatements) { sql.AppendSql(statement); } var indexAdditions = matchPair.To.Indexes.Except(matchPair.From.Indexes); var indexStatements = this.createTableWriter.CreateIndexes(indexAdditions); foreach (var statement in indexStatements) { sql.AppendSql(statement); } } warnings = warningList; errors = errorList; return sql.ToString(); }