diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index a79792d84b..fda9f55f00 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Migrations; +using Umbraco.Core.Persistence.Migrations.Initial; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core @@ -242,20 +243,37 @@ namespace Umbraco.Core } - if (providerName.StartsWith("MySql")) + Initialize(providerName); + } + else if (ConfigurationManager.AppSettings.ContainsKey(GlobalSettings.UmbracoConnectionName)) + { + //A valid connectionstring does not exist, but the legacy appSettings key was found, so we'll reconfigure the conn.string. + var legacyConnString = ConfigurationManager.AppSettings[GlobalSettings.UmbracoConnectionName]; + if (legacyConnString.ToLowerInvariant().Contains("sqlce4umbraco")) { - SyntaxConfig.SqlSyntaxProvider = MySqlSyntax.Provider; + ConfigureDatabaseConnection(); } - else if (providerName.Contains("SqlServerCe")) + else if (legacyConnString.ToLowerInvariant().Contains("database.windows.net") && + legacyConnString.ToLowerInvariant().Contains("tcp:")) { - SyntaxConfig.SqlSyntaxProvider = SqlCeSyntax.Provider; + //Must be sql azure + SaveConnectionString(legacyConnString, "System.Data.SqlClient"); + } + else if (legacyConnString.ToLowerInvariant().Contains("Uid") && + legacyConnString.ToLowerInvariant().Contains("Pwd") && + legacyConnString.ToLowerInvariant().Contains("Server")) + { + //Must be MySql + SaveConnectionString(legacyConnString, "MySql.Data.MySqlClient"); } else { - SyntaxConfig.SqlSyntaxProvider = SqlServerSyntax.Provider; + //Must be sql + SaveConnectionString(legacyConnString, "System.Data.SqlClient"); } - _configured = true; + //Remove the legacy connection string, so we don't end up in a loop if something goes wrong. + GlobalSettings.RemoveSetting(GlobalSettings.UmbracoConnectionName); } else { @@ -277,28 +295,51 @@ namespace Umbraco.Core { SyntaxConfig.SqlSyntaxProvider = SqlServerSyntax.Provider; } - + + _providerName = providerName; _configured = true; } + internal DatabaseSchemaResult ValidateDatabaseSchema() + { + if (_configured == false || (string.IsNullOrEmpty(_connectionString) || string.IsNullOrEmpty(ProviderName))) + return new DatabaseSchemaResult(); + + var database = new UmbracoDatabase(_connectionString, ProviderName); + var dbSchema = new DatabaseSchemaCreation(database); + var result = dbSchema.ValidateSchema(); + return result; + } + internal Result CreateDatabaseSchemaAndDataOrUpgrade() { if (_configured == false || (string.IsNullOrEmpty(_connectionString) || string.IsNullOrEmpty(ProviderName))) { - return new Result{Message = "Database configuration is invalid", Success = false, Percentage = "10"}; + return new Result + { + Message = + "Database configuration is invalid. Please check that the entered database exists and that the provided username and password has write access to the database.", + Success = false, + Percentage = "10" + }; } try { var database = new UmbracoDatabase(_connectionString, ProviderName); - //If Configuration Status is empty its a new install - otherwise upgrade the existing - if (string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus)) + var schemaResult = ValidateDatabaseSchema(); + var installedVersion = schemaResult.DetermineInstalledVersion(); + + //If Configuration Status is empty and the determined version is "empty" its a new install - otherwise upgrade the existing + if (string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) && installedVersion.Equals(new Version(0, 0, 0))) { database.CreateDatabaseSchema(); } else { - var configuredVersion = new Version(GlobalSettings.ConfigurationStatus); + var configuredVersion = string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) + ? installedVersion + : new Version(GlobalSettings.ConfigurationStatus); var targetVersion = UmbracoVersion.Current; var runner = new MigrationRunner(configuredVersion, targetVersion, GlobalSettings.UmbracoMigrationName); var upgraded = runner.Execute(database, true); diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index ce66c70713..f987e69b3d 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -84,10 +84,14 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions var primaryKeyColumnAttribute = propertyInfo.FirstAttribute(); if (primaryKeyColumnAttribute != null) { + string primaryKeyName = string.IsNullOrEmpty(primaryKeyColumnAttribute.Name) + ? string.Format("PK_{0}", tableName) + : primaryKeyColumnAttribute.Name; + definition.IsPrimaryKey = true; definition.IsIdentity = primaryKeyColumnAttribute.AutoIncrement; definition.IsIndexed = primaryKeyColumnAttribute.Clustered; - definition.PrimaryKeyName = primaryKeyColumnAttribute.Name ?? string.Empty; + definition.PrimaryKeyName = primaryKeyName; definition.PrimaryKeyColumns = primaryKeyColumnAttribute.OnColumns ?? string.Empty; definition.Seeding = primaryKeyColumnAttribute.IdentitySeed; } @@ -120,9 +124,13 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions ? referencedPrimaryKey.Value : attribute.Column; + string foreignKeyName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("FK_{0}_{1}_{2}", tableName, referencedTable.Value, referencedColumn) + : attribute.Name; + var definition = new ForeignKeyDefinition { - Name = attribute.Name, + Name = foreignKeyName, ForeignTable = tableName, PrimaryTable = referencedTable.Value }; @@ -134,9 +142,13 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions public static IndexDefinition GetIndexDefinition(Type modelType, PropertyInfo propertyInfo, IndexAttribute attribute, string columnName, string tableName) { + string indexName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("IX_{0}_{1}", tableName, columnName) + : attribute.Name; + var definition = new IndexDefinition { - Name = attribute.Name, + Name = indexName, IndexType = attribute.IndexType, ColumnName = columnName, TableName = tableName, diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 9392c047bf..2c3e245dff 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Initial { @@ -95,19 +97,97 @@ namespace Umbraco.Core.Persistence.Migrations.Initial foreach (var item in OrderedTables.OrderBy(x => x.Key)) { - var tableNameAttribute = item.Value.FirstAttribute(); - if (tableNameAttribute != null) + var tableDefinition = DefinitionFactory.GetTableDefinition(item.Value); + result.TableDefinitions.Add(tableDefinition); + } + + //Check tables in configured database against tables in schema + var tablesInDatabase = SyntaxConfig.SqlSyntaxProvider.GetTablesInSchema(_database).ToList(); + var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); + //Add valid and invalid table differences to the result object + var validTableDifferences = tablesInDatabase.Intersect(tablesInSchema); + foreach (var tableName in validTableDifferences) + { + result.ValidTables.Add(tableName); + } + var invalidTableDifferences = tablesInDatabase.Except(tablesInSchema); + foreach (var tableName in invalidTableDifferences) + { + result.Errors.Add(new Tuple("Table", tableName)); + } + + //Check columns in configured database against columns in schema + var columnsInDatabase = SyntaxConfig.SqlSyntaxProvider.GetColumnsInSchema(_database); + var columnsPerTableInDatabase = columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); + var columnsPerTableInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); + //Add valid and invalid column differences to the result object + var validColumnDifferences = columnsPerTableInDatabase.Intersect(columnsPerTableInSchema); + foreach (var column in validColumnDifferences) + { + result.ValidColumns.Add(column); + } + var invalidColumnDifferences = columnsPerTableInDatabase.Except(columnsPerTableInSchema); + foreach (var column in invalidColumnDifferences) + { + result.Errors.Add(new Tuple("Column", column)); + } + + //Check constraints in configured database against constraints in schema + var constraintsInDatabase = SyntaxConfig.SqlSyntaxProvider.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); + var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.StartsWith("FK_")).Select(x => x.Item3).ToList(); + var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.StartsWith("PK_")).Select(x => x.Item3).ToList(); + var indexesInDatabase = constraintsInDatabase.Where(x => x.Item3.StartsWith("IX_")).Select(x => x.Item3).ToList(); + var unknownConstraintsInDatabase = + constraintsInDatabase.Where( + x => + x.Item3.StartsWith("FK_") == false && x.Item3.StartsWith("PK_") == false && + x.Item3.StartsWith("IX_") == false).Select(x => x.Item3).ToList(); + var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList(); + var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + //Add valid and invalid foreign key differences to the result object + foreach (var unknown in unknownConstraintsInDatabase) + { + if (foreignKeysInSchema.Contains(unknown) || primaryKeysInSchema.Contains(unknown) || indexesInSchema.Contains(unknown)) { - var tableExist = _database.TableExist(tableNameAttribute.Value); - if (tableExist) - { - result.Successes.Add(tableNameAttribute.Value, "Table exists"); - } - else - { - result.Errors.Add(tableNameAttribute.Value, "Table does not exist"); - } + result.ValidConstraints.Add(unknown); } + else + { + result.Errors.Add(new Tuple("Unknown", unknown)); + } + } + var validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema); + foreach (var foreignKey in validForeignKeyDifferences) + { + result.ValidConstraints.Add(foreignKey); + } + var invalidForeignKeyDifferences = foreignKeysInDatabase.Except(foreignKeysInSchema); + foreach (var foreignKey in invalidForeignKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", foreignKey)); + } + //Add valid and invalid primary key differences to the result object + var validPrimaryKeyDifferences = primaryKeysInDatabase.Intersect(primaryKeysInSchema); + foreach (var primaryKey in validPrimaryKeyDifferences) + { + result.ValidConstraints.Add(primaryKey); + } + var invalidPrimaryKeyDifferences = primaryKeysInDatabase.Except(primaryKeysInSchema); + foreach (var primaryKey in invalidPrimaryKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", primaryKey)); + } + //Add valid and invalid index differences to the result object + var validIndexDifferences = indexesInDatabase.Intersect(indexesInSchema); + foreach (var index in validIndexDifferences) + { + result.ValidConstraints.Add(index); + } + var invalidIndexDifferences = indexesInDatabase.Except(indexesInSchema); + foreach (var index in invalidIndexDifferences) + { + result.Errors.Add(new Tuple("Constraint", index)); } return result; diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index 71997028ce..9b0ae69270 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Persistence.Migrations.Initial { @@ -6,12 +10,68 @@ namespace Umbraco.Core.Persistence.Migrations.Initial { public DatabaseSchemaResult() { - Errors = new Dictionary(); - Successes = new Dictionary(); + Errors = new List>(); + TableDefinitions = new List(); + ValidTables = new List(); + ValidColumns = new List(); + ValidConstraints = new List(); } - public IDictionary Errors { get; set; } + public List> Errors { get; set; } - public IDictionary Successes { get; set; } + public List TableDefinitions { get; set; } + + public List ValidTables { get; set; } + + public List ValidColumns { get; set; } + + public List ValidConstraints { get; set; } + + /// + /// Determines the version of the currently installed database. + /// + /// + /// A with Major and Minor values for + /// non-empty database, otherwise "0.0.0" for empty databases. + /// + public Version DetermineInstalledVersion() + { + //If (ValidTables.Count == 0) database is empty and we return -> new Version(0, 0, 0); + if(ValidTables.Count == 0) + return new Version(0, 0, 0); + + //If Errors is empty then we're at current version + if (Errors.Any() == false) + return UmbracoVersion.Current; + + //If Errors contains umbracoApp or umbracoAppTree its pre-6.0.0 -> new Version(4, 10, 0); + if ( + Errors.Any( + x => x.Item1.Equals("Table") && (x.Item2.Equals("umbracoApp") || x.Item2.Equals("umbracoAppTree")))) + { + //If Errors contains umbracoUser2app or umbracoAppTree foreignkey to umbracoApp exists its pre-4.8.0 -> new Version(4, 7, 0); + if ( + Errors.Any( + x => + x.Item1.Equals("Constraint") && + (x.Item2.Contains("umbracoUser2app_umbracoApp") || x.Item2.Contains("umbracoAppTree_umbracoApp")))) + { + return new Version(4, 7, 0); + } + + return new Version(4, 10, 0); + } + + return new Version(0, 0, 0); + } + + /// + /// Gets a summary of the schema validation result + /// + /// A string containing a human readable string with a summary message + public string GetSummary() + { + return string.Empty; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ColumnInfo.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ColumnInfo.cs new file mode 100644 index 0000000000..71b6d58d8b --- /dev/null +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ColumnInfo.cs @@ -0,0 +1,31 @@ +namespace Umbraco.Core.Persistence.SqlSyntax +{ + public class ColumnInfo + { + public ColumnInfo(string tableName, string columnName, int ordinal, string columnDefault, string isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + ColumnDefault = columnDefault; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; + } + + public ColumnInfo(string tableName, string columnName, int ordinal, string isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; + } + + public string TableName { get; set; } + public string ColumnName { get; set; } + public int Ordinal { get; set; } + public string ColumnDefault { get; set; } + public bool IsNullable { get; set; } + public string DataType { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 02426fba52..5aa5c3591d 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -47,5 +48,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax string FormatTableRename(string oldName, string newName); bool SupportsClustered(); bool SupportsIdentityInsert(); + IEnumerable GetTablesInSchema(Database db); + IEnumerable GetColumnsInSchema(Database db); + IEnumerable> GetConstraintsPerTable(Database db); + IEnumerable> GetConstraintsPerColumn(Database db); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 1d124b3687..92f0d617f3 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -39,6 +40,27 @@ namespace Umbraco.Core.Persistence.SqlSyntax DefaultValueFormat = "DEFAULT '{0}'"; } + public override IEnumerable GetTablesInSchema(Database db) + { + var items = db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"); + return items.Select(x => x.TABLE_NAME).Cast().ToList(); + } + + public override IEnumerable GetColumnsInSchema(Database db) + { + return new List(); + } + + public override IEnumerable> GetConstraintsPerTable(Database db) + { + return new List>(); + } + + public override IEnumerable> GetConstraintsPerColumn(Database db) + { + return new List>(); + } + public override bool DoesTableExist(Database db, string tableName) { long result; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 28383a666e..06d37c9df0 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -112,6 +113,50 @@ namespace Umbraco.Core.Persistence.SqlSyntax columns); } + public override IEnumerable GetTablesInSchema(Database db) + { + var items = db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"); + return items.Select(x => x.TABLE_NAME).Cast().ToList(); + } + + public override IEnumerable GetColumnsInSchema(Database db) + { + var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS"); + return + items.Select( + item => + new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, item.ORDINAL_POSITION, item.COLUMN_DEFAULT, + item.IS_NULLABLE, item.DATA_TYPE)).ToList(); + } + + public override IEnumerable> GetConstraintsPerTable(Database db) + { + var items = db.Fetch("SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS"); + var indexItems = db.Fetch("SELECT TABLE_NAME, INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES"); + return + items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)) + .Union( + indexItems.Select( + indexItem => new Tuple(indexItem.TABLE_NAME, indexItem.INDEX_NAME))) + .ToList(); + } + + public override IEnumerable> GetConstraintsPerColumn(Database db) + { + var items = + db.Fetch( + "SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE"); + var indexItems = db.Fetch("SELECT INDEX_NAME, TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.INDEXES"); + return + items.Select( + item => new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)) + .Union( + indexItems.Select( + indexItem => + new Tuple(indexItem.TABLE_NAME, indexItem.COLUMN_NAME, + indexItem.INDEX_NAME))).ToList(); + } + public override bool DoesTableExist(Database db, string tableName) { var result = diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index b97033c17c..eda04199bd 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -1,4 +1,7 @@ -using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Persistence.SqlSyntax { @@ -49,6 +52,38 @@ namespace Umbraco.Core.Persistence.SqlSyntax return string.Format("[{0}]", name); } + public override IEnumerable GetTablesInSchema(Database db) + { + var items = db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"); + return items.Select(x => x.TABLE_NAME).Cast().ToList(); + } + + public override IEnumerable GetColumnsInSchema(Database db) + { + var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS"); + return + items.Select( + item => + new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, item.ORDINAL_POSITION, item.COLUMN_DEFAULT, + item.IS_NULLABLE, item.DATA_TYPE)).ToList(); + } + + public override IEnumerable> GetConstraintsPerTable(Database db) + { + var items = + db.Fetch( + "SELECT TABLE_NAME, CONSTRAINT_NAME FROM SELECT * FROM INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE"); + return items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList(); + } + + public override IEnumerable> GetConstraintsPerColumn(Database db) + { + var items = + db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE"); + return items.Select(item => new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)).ToList(); + } + public override bool DoesTableExist(Database db, string tableName) { var result = diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 0f6f832c66..c453b46dc4 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -151,6 +151,26 @@ namespace Umbraco.Core.Persistence.SqlSyntax return "NVARCHAR"; } + public virtual IEnumerable GetTablesInSchema(Database db) + { + return new List(); + } + + public virtual IEnumerable GetColumnsInSchema(Database db) + { + return new List(); + } + + public virtual IEnumerable> GetConstraintsPerTable(Database db) + { + return new List>(); + } + + public virtual IEnumerable> GetConstraintsPerColumn(Database db) + { + return new List>(); + } + public virtual bool DoesTableExist(Database db, string tableName) { return false; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 0aeab69175..96e43ad6a7 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -439,6 +439,7 @@ + diff --git a/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs b/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs index e229655699..326aa304c2 100644 --- a/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs +++ b/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs @@ -10,7 +10,6 @@ using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Strategies.Migrations; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; namespace Umbraco.Tests.Migrations.Upgrades diff --git a/src/Umbraco.Tests/Migrations/Upgrades/ValidateOlderSchemaTest.cs b/src/Umbraco.Tests/Migrations/Upgrades/ValidateOlderSchemaTest.cs new file mode 100644 index 0000000000..9515970f3c --- /dev/null +++ b/src/Umbraco.Tests/Migrations/Upgrades/ValidateOlderSchemaTest.cs @@ -0,0 +1,113 @@ +using System; +using System.Configuration; +using System.Data.SqlServerCe; +using System.IO; +using System.Text.RegularExpressions; +using NUnit.Framework; +using SQLCE4Umbraco; +using Umbraco.Core.Configuration; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Migrations.Initial; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Migrations.Upgrades +{ + [TestFixture] + public class ValidateOlderSchemaTest + { + /// Regular expression that finds multiline block comments. + private static readonly Regex FindComments = new Regex(@"\/\*.*?\*\/", RegexOptions.Singleline | RegexOptions.Compiled); + + [Test] + public virtual void DatabaseSchemaCreation_Returns_DatabaseSchemaResult_Where_DetermineInstalledVersion_Is_4_7_0() + { + // Arrange + var db = GetConfiguredDatabase(); + var schema = new DatabaseSchemaCreation(db); + + //Create db schema and data from old Total.sql file for Sql Ce + string statements = GetDatabaseSpecificSqlScript(); + // replace block comments by whitespace + statements = FindComments.Replace(statements, " "); + // execute all non-empty statements + foreach (string statement in statements.Split(";".ToCharArray())) + { + string rawStatement = statement.Replace("GO", "").Trim(); + if (rawStatement.Length > 0) + db.Execute(new Sql(rawStatement)); + } + + // Act + var result = schema.ValidateSchema(); + + // Assert + var expected = new Version(4, 7, 0); + Assert.AreEqual(expected, result.DetermineInstalledVersion()); + } + + [SetUp] + public virtual void Initialize() + { + TestHelper.SetupLog4NetForTests(); + TestHelper.InitializeContentDirectories(); + + Path = TestHelper.CurrentAssemblyDirectory; + AppDomain.CurrentDomain.SetData("DataDirectory", Path); + + UmbracoSettings.UseLegacyXmlSchema = false; + + Resolution.Freeze(); + + //Delete database file before continueing + string filePath = string.Concat(Path, "\\UmbracoPetaPocoTests.sdf"); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + //Get the connectionstring settings from config + var settings = ConfigurationManager.ConnectionStrings[Core.Configuration.GlobalSettings.UmbracoConnectionName]; + + //Create the Sql CE database + var engine = new SqlCeEngine(settings.ConnectionString); + engine.CreateDatabase(); + + SyntaxConfig.SqlSyntaxProvider = SqlCeSyntax.Provider; + } + + [TearDown] + public virtual void TearDown() + { + SyntaxConfig.SqlSyntaxProvider = null; + Resolution.IsFrozen = false; + + TestHelper.CleanContentDirectories(); + + Path = TestHelper.CurrentAssemblyDirectory; + AppDomain.CurrentDomain.SetData("DataDirectory", null); + + //legacy API database connection close + SqlCeContextGuardian.CloseBackgroundConnection(); + + string filePath = string.Concat(Path, "\\UmbracoPetaPocoTests.sdf"); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + } + + public string Path { get; set; } + + public UmbracoDatabase GetConfiguredDatabase() + { + return new UmbracoDatabase("Datasource=|DataDirectory|UmbracoPetaPocoTests.sdf", "System.Data.SqlServerCe.4.0"); + } + + public string GetDatabaseSpecificSqlScript() + { + return SqlScripts.SqlResources.SqlCeTotal_480; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs b/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs new file mode 100644 index 0000000000..f4aaa7c064 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/SchemaValidationTest.cs @@ -0,0 +1,38 @@ +using NUnit.Framework; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence.Migrations.Initial; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Persistence +{ + [TestFixture] + public class SchemaValidationTest : BaseDatabaseFactoryTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + [Test] + public void DatabaseSchemaCreation_Produces_DatabaseSchemaResult_With_Zero_Errors() + { + // Arrange + var db = DatabaseContext.Database; + var schema = new DatabaseSchemaCreation(db); + + // Act + var result = schema.ValidateSchema(); + + // Assert + Assert.That(result.Errors.Count, Is.EqualTo(0)); + Assert.AreEqual(result.DetermineInstalledVersion(), UmbracoVersion.Current); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 4bff2016c1..66cd2854c4 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -175,6 +175,7 @@ + @@ -183,6 +184,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/install/steps/Definitions/Database.cs b/src/Umbraco.Web/umbraco.presentation/install/steps/Definitions/Database.cs index 315e80b805..0c2b61108f 100644 --- a/src/Umbraco.Web/umbraco.presentation/install/steps/Definitions/Database.cs +++ b/src/Umbraco.Web/umbraco.presentation/install/steps/Definitions/Database.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Core; using Umbraco.Core.Configuration; using umbraco.cms.businesslogic.installer; using umbraco.IO; @@ -35,8 +36,16 @@ namespace umbraco.presentation.install.steps.Definitions public override bool Completed() { // Fresh installs don't have a version number so this step cannot be complete yet - if (string.IsNullOrEmpty(Umbraco.Core.Configuration.GlobalSettings.ConfigurationStatus)) - return false; + if (string.IsNullOrEmpty(Umbraco.Core.Configuration.GlobalSettings.ConfigurationStatus)) + { + //Even though the ConfigurationStatus is blank we try to determine the version if we can connect to the database + var result = ApplicationContext.Current.DatabaseContext.ValidateDatabaseSchema(); + var determinedVersion = result.DetermineInstalledVersion(); + if(determinedVersion.Equals(new Version(0, 0, 0))) + return false; + + return UmbracoVersion.Current < determinedVersion; + } var configuredVersion = new Version(Umbraco.Core.Configuration.GlobalSettings.ConfigurationStatus); var targetVersion = UmbracoVersion.Current;