From 7aabf459ea0e5e51c7468801136c49022501e9d1 Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 25 Jan 2013 15:05:42 -0100 Subject: [PATCH] Adding schema validation to the DatabaseSchemaCreation class. Helps determine if valid database exists and which version it corresponds to. On startup the legacy connectionstring is used if one exists, so its not ignore but rather reconfigured. Relates to U4-1520. --- src/Umbraco.Core/DatabaseContext.cs | 63 ++++++++-- .../DefinitionFactory.cs | 18 ++- .../Initial/DatabaseSchemaCreation.cs | 102 ++++++++++++++-- .../Initial/DatabaseSchemaResult.cs | 70 ++++++++++- .../Persistence/SqlSyntax/ColumnInfo.cs | 31 +++++ .../SqlSyntax/ISqlSyntaxProvider.cs | 5 + .../SqlSyntax/MySqlSyntaxProvider.cs | 22 ++++ .../SqlSyntax/SqlCeSyntaxProvider.cs | 45 +++++++ .../SqlSyntax/SqlServerSyntaxProvider.cs | 37 +++++- .../SqlSyntax/SqlSyntaxProviderBase.cs | 20 ++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Migrations/Upgrades/BaseUpgradeTest.cs | 1 - .../Upgrades/ValidateOlderSchemaTest.cs | 113 ++++++++++++++++++ .../Persistence/SchemaValidationTest.cs | 38 ++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../install/steps/Definitions/Database.cs | 13 +- 16 files changed, 547 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/SqlSyntax/ColumnInfo.cs create mode 100644 src/Umbraco.Tests/Migrations/Upgrades/ValidateOlderSchemaTest.cs create mode 100644 src/Umbraco.Tests/Persistence/SchemaValidationTest.cs 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;