diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 8f3b8991e4..0d967cb054 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Runtime { @@ -313,21 +314,19 @@ namespace Umbraco.Core.Runtime // else // look for a matching migration entry - bypassing services entirely - they are not 'up' yet // fixme - in a LB scenario, ensure that the DB gets upgraded only once! - bool exists; + bool noUpgrade; try { - exists = EnsureUmbracoUpgradeState(databaseFactory, logger); + noUpgrade = EnsureUmbracoUpgradeState(databaseFactory, logger); } catch (Exception e) { - // can connect to the database but cannot access the migration table... need to install + // can connect to the database but cannot check the upgrade state... oops logger.Warn(e, "Could not check the upgrade state."); - logger.Debug("Could not check the upgrade state, need to install Umbraco."); - _state.Level = RuntimeLevel.Install; - return; + throw new BootFailedException("Could not check the upgrade state.", e); } - if (exists) + if (noUpgrade) { // the database version matches the code & files version, all clear, can run _state.Level = RuntimeLevel.Run; @@ -345,27 +344,19 @@ namespace Umbraco.Core.Runtime protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger) { - // no scope, no key value service - just directly accessing the database - var umbracoPlan = new UmbracoPlan(); var stateValueKey = Upgrader.GetStateValueKey(umbracoPlan); - string state; + // no scope, no service - just directly accessing the database using (var database = databaseFactory.CreateDatabase()) { - var sql = databaseFactory.SqlContext.Sql() - .Select() - .From() - .Where(x => x.Key == stateValueKey); - state = database.FirstOrDefault(sql)?.Value; + _state.CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); + _state.FinalMigrationState = umbracoPlan.FinalState; } - _state.CurrentMigrationState = state; - _state.FinalMigrationState = umbracoPlan.FinalState; + logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", _state.FinalMigrationState, _state.CurrentMigrationState ?? ""); - logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", _state.FinalMigrationState, state ?? ""); - - return state == _state.FinalMigrationState; + return _state.CurrentMigrationState == _state.FinalMigrationState; } #region Locals diff --git a/src/Umbraco.Core/Services/Implement/KeyValueService.cs b/src/Umbraco.Core/Services/Implement/KeyValueService.cs index cb1c423535..e0d78b0258 100644 --- a/src/Umbraco.Core/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Core/Services/Implement/KeyValueService.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Migrations; -using Umbraco.Core.Migrations.Expressions.Create; using Umbraco.Core.Scoping; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -35,23 +34,23 @@ namespace Umbraco.Core.Services.Implement private void Initialize() { - // all this cannot be achieved via an UmbracoPlan migration since it needs to - // run before any migration, in order to figure out the current plan's state. - // (does not prevent us from using a migration to do it, though) + // the key/value service is entirely self-managed, because it is used by the + // upgrader and anything we might change need to happen before everything else + + // if already running 8, either following an upgrade or an install, + // then everything should be ok (the table should exist, etc) + + if (UmbracoVersion.Local.Major >= 8) + return; + + // else we are upgrading from 7, we can assume that the locks table + // exists, but we need to create everything for key/value using (var scope = _scopeProvider.CreateScope()) { - // assume that if the lock object for key/value exists, then everything is ok - if (scope.Database.Exists(Constants.Locks.KeyValues)) - { - scope.Complete(); - return; - } - var context = new MigrationContext(scope.Database, _logger); var initMigration = new InitializeMigration(context); initMigration.Migrate(); - scope.Complete(); } } @@ -59,7 +58,7 @@ namespace Umbraco.Core.Services.Implement /// /// A custom migration that executes standalone during the Initialize phase of this service. /// - private class InitializeMigration : MigrationBase + internal class InitializeMigration : MigrationBase { public InitializeMigration(IMigrationContext context) : base(context) @@ -67,26 +66,47 @@ namespace Umbraco.Core.Services.Implement public override void Migrate() { + // as long as we are still running 7 this migration will be invoked, + // but due to multiple restarts during upgrades, maybe the table + // exists already + if (TableExists(Constants.DatabaseSchema.Tables.KeyValue)) + return; + + Logger.Info("Creating KeyValue structure."); + + // the locks table was initially created with an identity (auto-increment) primary key, + // but we don't want this, especially as we are about to insert a new row into the table, + // so here we drop that identity + DropLockTableIdentity(); + + // insert the lock object for key/value + Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new {id = Constants.Locks.KeyValues, name = "KeyValues", value = 1}).Do(); + + // create the key-value table + Create.Table().Do(); + } + + private void DropLockTableIdentity() + { + // one cannot simply drop an identity, that requires a bit of work + // create a temp. id column and copy values Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do(); Execute.Sql("update umbracoLock set nid = id").Do(); + // drop the id column entirely (cannot just drop identity) Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + // recreate the id column without identity and copy values Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do(); Execute.Sql("update umbracoLock set id = nid").Do(); + // drop the temp. id column Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + // complete the primary key Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do(); - - // insert the key-value lock - Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new {id = Constants.Locks.KeyValues, name = "KeyValues", value = 1}).Do(); - - // create the key-value table if it's not there - if (TableExists(Constants.DatabaseSchema.Tables.KeyValue) == false) - Create.Table().Do(); } } @@ -169,5 +189,22 @@ namespace Umbraco.Core.Services.Implement return true; } + + /// + /// Gets a value directly from the database, no scope, nothing. + /// + /// Used by to determine the runtime state. + internal static string GetValue(IUmbracoDatabase database, string key) + { + // not 8 yet = no key/value table, no value + if (UmbracoVersion.Local.Major < 8) + return null; + + var sql = database.SqlContext.Sql() + .Select() + .From() + .Where(x => x.Key == key); + return database.FirstOrDefault(sql)?.Value; + } } }