The database you're trying to use does not support case insensitive queries.
We currently do not support these types of databases.
" +
- "You can fix this by changing the following setting in your my.ini file in your MySQL installation directory:
" +
- "Note: Make sure to check with your hosting provider if they support case insensitive queries as well.
" +
- "("Database upgrade started");
- //var database = scope.Database;
- //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database);
-
- var message = GetResultMessageForMySql();
-
- // fixme - remove this code
- //var schemaResult = ValidateDatabaseSchema();
- //
- //var installedSchemaVersion = new SemVersion(schemaResult.DetermineInstalledVersion());
- //var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService);
- //var targetVersion = UmbracoVersion.Current;
- //
- ////In some cases - like upgrading from 7.2.6 -> 7.3, there will be no migration information in the database and therefore it will
- //// return a version of 0.0.0 and we don't necessarily want to run all migrations from 0 -> 7.3, so we'll just ensure that the
- //// migrations are run for the target version
- //if (installedMigrationVersion == new SemVersion(new Version(0, 0, 0)) && installedSchemaVersion > new SemVersion(new Version(0, 0, 0)))
- //{
- // //set the installedMigrationVersion to be one less than the target so the latest migrations are guaranteed to execute
- // installedMigrationVersion = new SemVersion(targetVersion.SubtractRevision());
- //}
- //
- ////Figure out what our current installed version is. If the web.config doesn't have a version listed, then we'll use the minimum
- //// version detected between the schema installed and the migrations listed in the migration table.
- //// If there is a version in the web.config, we'll take the minimum between the listed migration in the db and what
- //// is declared in the web.config.
- //
- //var currentInstalledVersion = string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus)
- // //Take the minimum version between the detected schema version and the installed migration version
- // ? new[] { installedSchemaVersion, installedMigrationVersion }.Min()
- // //Take the minimum version between the installed migration version and the version specified in the config
- // : new[] { SemVersion.Parse(GlobalSettings.ConfigurationStatus), installedMigrationVersion }.Min();
- //
- ////Ok, another edge case here. If the current version is a pre-release,
- //// then we want to ensure all migrations for the current release are executed.
- //if (currentInstalledVersion.Prerelease.IsNullOrWhiteSpace() == false)
- //{
- // currentInstalledVersion = new SemVersion(currentInstalledVersion.GetVersion().SubtractRevision());
- //}
+ var message = _scopeProvider.SqlContext.DatabaseType.IsMySql() ? ResultMessageForMySql : "";
// upgrade
- var upgrader = new UmbracoUpgrader(_scopeProvider, _migrationBuilder, _keyValueService, _postMigrations, _logger);
- upgrader.Execute();
-
- // fixme remove this code
- //var runner = new MigrationRunner(_scopeProvider, builder, migrationEntryService, _logger, currentInstalledVersion, UmbracoVersion.SemanticVersion, Constants.System.UmbracoMigrationName);
- //var upgraded = runner.Execute(/*upgrade:true*/);
- //if (upgraded == false)
- //{
- // throw new ApplicationException("Upgrading failed, either an error occurred during the upgrade process or an event canceled the upgrade process, see log for full details");
- //}
+ var upgrader = new UmbracoUpgrader();
+ upgrader.Execute(_scopeProvider, _migrationBuilder, _keyValueService, _logger, _postMigrations);
message = message + "Upgrade completed!
";
@@ -553,47 +515,14 @@ namespace Umbraco.Core.Migrations.Install
}
}
- private string GetResultMessageForMySql()
- {
- if (_databaseFactory.GetType() == typeof(MySqlSyntaxProvider))
- {
- return "
Congratulations, the database step ran successfully!
" +
- "Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.
" +
- "However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries
" +
- "Make sure to check with your hosting provider if they support case insensitive queries as well.
" +
- "They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:
" +
- "lower_case_table_names=1
" +
- "For more technical information on case sensitivity in MySQL, have a look at " +
- "the documentation on the subject
";
- }
- return string.Empty;
- }
-
- /*
- private string GetResultMessageForMySql(bool? supportsCaseInsensitiveQueries)
- {
- if (supportsCaseInsensitiveQueries == null)
- {
- return "
Warning! Could not check if your database type supports case insensitive queries.
We currently do not support these databases that do not support case insensitive queries.
" +
- "You can check this by looking for the following setting in your my.ini file in your MySQL installation directory:
" +
- "lower_case_table_names=1
" +
- "Note: Make sure to check with your hosting provider if they support case insensitive queries as well.
" +
- "For more technical information on case sensitivity in MySQL, have a look at " +
- "the documentation on the subject
";
- }
- if (SqlSyntax.GetType() == typeof(MySqlSyntaxProvider))
- {
- return "
Congratulations, the database step ran successfully!
" +
- "Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.
" +
- "However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries
" +
- "Make sure to check with your hosting provider if they support case insensitive queries as well.
" +
- "They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:
" +
- "lower_case_table_names=1
" +
- "For more technical information on case sensitivity in MySQL, have a look at " +
- "the documentation on the subject
";
- }
- return string.Empty;
- }*/
+ private const string ResultMessageForMySql = "
Congratulations, the database step ran successfully!
" +
+ "Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.
" +
+ "However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries
" +
+ "Make sure to check with your hosting provider if they support case insensitive queries as well.
" +
+ "They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:
" +
+ "lower_case_table_names=1
" +
+ "For more technical information on case sensitivity in MySQL, have a look at " +
+ "the documentation on the subject
";
private Attempt CheckReadyForInstall()
{
@@ -629,11 +558,29 @@ namespace Umbraco.Core.Migrations.Install
};
}
- internal class Result
+ ///
+ /// Represents the result of a database creation or upgrade.
+ ///
+ public class Result
{
+ ///
+ /// Gets or sets a value indicating whether an upgrade is required.
+ ///
public bool RequiresUpgrade { get; set; }
+
+ ///
+ /// Gets or sets the message returned by the operation.
+ ///
public string Message { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the operation succeeded.
+ ///
public bool Success { get; set; }
+
+ ///
+ /// Gets or sets an install progress pseudo-percentage.
+ ///
public string Percentage { get; set; }
}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
index eb7cafcb01..f32ea1cb6f 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
@@ -321,9 +321,9 @@ namespace Umbraco.Core.Migrations.Install
{
// on install, initialize the umbraco migration plan with the final state
- var plan = new UmbracoPlan();
- var stateValueKey = Upgrader.GetStateValueKey(plan);
- var finalState = plan.FinalState;
+ var upgrader = new UmbracoUpgrader();
+ var stateValueKey = upgrader.StateValueKey;
+ var finalState = upgrader.Plan.FinalState;
_database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, Updated = DateTime.Now });
}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
index 042c6117aa..5525cc4a50 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
@@ -142,9 +142,8 @@ namespace Umbraco.Core.Migrations.Install
{
var result = new DatabaseSchemaResult(SqlSyntax);
- //get the db index defs
- result.DbIndexDefinitions = SqlSyntax.GetDefinedIndexes(_database)
- .Select(x => new DbIndexDefinition(x)).ToArray();
+ result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database)
+ .Select(x => new DbIndexDefinition(x)));
result.TableDefinitions.AddRange(OrderedTables
.Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax)));
@@ -283,7 +282,7 @@ namespace Umbraco.Core.Migrations.Install
{
//These are just column indexes NOT constraints or Keys
//var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList();
- var colIndexesInDatabase = result.DbIndexDefinitions.Select(x => x.IndexName).ToList();
+ var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList();
var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList();
//Add valid and invalid index differences to the result object
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
index 0ec27cf0b1..4c68addebc 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
@@ -2,153 +2,55 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
-using Umbraco.Core.Configuration;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Migrations.Install
{
- public class DatabaseSchemaResult
+ ///
+ /// Represents ...
+ ///
+ internal class DatabaseSchemaResult
{
- private readonly ISqlSyntaxProvider _sqlSyntax;
+ private readonly bool _isMySql;
public DatabaseSchemaResult(ISqlSyntaxProvider sqlSyntax)
{
- _sqlSyntax = sqlSyntax;
+ _isMySql = sqlSyntax is MySqlSyntaxProvider;
+
Errors = new List>();
TableDefinitions = new List();
ValidTables = new List();
ValidColumns = new List();
ValidConstraints = new List();
ValidIndexes = new List();
+ IndexDefinitions = new List();
}
- public List> Errors { get; set; }
+ public List> Errors { get; }
- public List TableDefinitions { get; set; }
+ public List TableDefinitions { get; }
- public List ValidTables { get; set; }
+ // fixme TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB?
+ internal List IndexDefinitions { get; }
- public List ValidColumns { get; set; }
+ public List ValidTables { get; }
- public List ValidConstraints { get; set; }
+ public List ValidColumns { get; }
- public List ValidIndexes { get; set; }
+ public List ValidConstraints { get; }
- internal IEnumerable DbIndexDefinitions { get; set; }
+ public List ValidIndexes { get; }
///
- /// Determines the version of the currently installed database by detecting the current database structure
+ /// Determines whether the database contains an installed version.
///
- ///
- /// A with Major and Minor values for
- /// non-empty database, otherwise "0.0.0" for empty databases.
- ///
- public Version DetermineInstalledVersion()
+ ///
+ /// A database contains an installed version when it contains at least one valid table.
+ ///
+ public bool DetermineHasInstalledVersion()
{
- // v8 = kill versions older than 7
-
- //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);
-
- // FIXME - but the whole detection is borked really
- return new Version(8, 0, 0);
-
- //If Errors is empty or if TableDefinitions tables + columns correspond to valid tables + columns then we're at current version
- if (Errors.Any() == false ||
- (TableDefinitions.All(x => ValidTables.Contains(x.Name))
- && TableDefinitions.SelectMany(definition => definition.Columns).All(x => ValidColumns.Contains(x.Name))))
- 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.InvariantEquals("umbracoApp") || x.Item2.InvariantEquals("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.InvariantContains("umbracoUser2app_umbracoApp")
- || x.Item2.InvariantContains("umbracoAppTree_umbracoApp"))))
- {
- return new Version(4, 7, 0);
- }
-
- return new Version(4, 8, 0);
- }
-
- //if the error is for umbracoServer
- if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoServer"))))
- {
- return new Version(6, 0, 0);
- }
-
- //if the error indicates a problem with the column cmsMacroProperty.macroPropertyType then it is not version 7
- // since these columns get removed in v7
- if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,macroPropertyType"))))
- {
- //if the error is for this IX_umbracoNodeTrashed which is added in 6.2 AND in 7.1 but we do not have the above columns
- // then it must mean that we aren't on 6.2 so must be 6.1
- if (Errors.Any(x => x.Item1.Equals("Index") && (x.Item2.InvariantEquals("IX_umbracoNodeTrashed"))))
- {
- return new Version(6, 1, 0);
- }
- else
- {
- //if there are no errors for that index, then the person must have 6.2 installed
- return new Version(6, 2, 0);
- }
- }
-
- //if the error indicates a problem with the constraint FK_cms-OBSOLETE-Content_cmsContentType_nodeId then it is not version 7.2
- // since this gets added in 7.2.0 so it must be the previous version
- if (Errors.Any(x => x.Item1.Equals("Constraint") && (x.Item2.InvariantEquals("FK_cms-OBSOLETE-Content_cmsContentType_nodeId"))))
- {
- return new Version(7, 0, 0);
- }
-
- //if the error is for umbracoAccess it must be the previous version to 7.3 since that is when it is added
- if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoAccess"))))
- {
- return new Version(7, 2, 0);
- }
-
- //if the error is for cms-OBSOLETE-PropertyData.dataDecimal it must be the previous version to 7.4 since that is when it is added
- if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cms-OBSOLETE-PropertyData,dataDecimal"))))
- {
- return new Version(7, 3, 0);
- }
-
- //if the error is for umbracoRedirectUrl it must be the previous version to 7.5 since that is when it is added
- if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoRedirectUrl"))))
- {
- return new Version(7, 4, 0);
- }
-
- //if the error indicates a problem with the column cmsMacroProperty.uniquePropertyId then it is not version 7.6 since that is when it is added
- if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,uniquePropertyId"))))
- {
- return new Version(7, 5, 0);
- }
-
- //if the error is for umbracoUserGroup it must be the previous version to 7.7 since that is when it is added
- if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoUserStartNode"))))
- {
- return new Version(7, 6, 0);
- }
-
- //if the error is for cmsMedia it must be the previous version to 7.8 since that is when it is added
- if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoMedia"))))
- {
- return new Version(7, 7, 0);
- }
-
- //if the error is for isSensitive column it must be the previous version to 7.9 since that is when it is added
- if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMemberType,isSensitive"))))
- {
- return new Version(7, 8, 0);
- }
-
- return UmbracoVersion.Current;
+ return ValidTables.Count > 0;
}
///
@@ -200,9 +102,9 @@ namespace Umbraco.Core.Migrations.Install
sb.AppendLine(" ");
}
- if (_sqlSyntax is MySqlSyntaxProvider)
+ if (_isMySql)
{
- sb.AppendLine("Please note that the constraints could not be validated because the current dataprovider is MySql.");
+ sb.AppendLine("Please note that the constraints could not be validated because the current data provider is MySql.");
}
return sb.ToString();
diff --git a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs
index 6ac92a07aa..8b5d9cc78c 100644
--- a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs
+++ b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Text;
using NPoco;
using Umbraco.Core.Logging;
@@ -88,6 +87,31 @@ namespace Umbraco.Core.Migrations
expression.Execute();
}
+ protected void Execute(Sql sql)
+ {
+ if (_executed)
+ throw new InvalidOperationException("This expression has already been executed.");
+ _executed = true;
+
+ if (sql == null)
+ {
+ Logger.Info(GetType(), $"SQL [{Context.Index}]: ");
+ }
+ else
+ {
+ Logger.Info(GetType(), $"SQL [{Context.Index}]: {sql.ToText()}");
+ Database.Execute(sql);
+ }
+
+ Context.Index++;
+
+ if (_expressions == null)
+ return;
+
+ foreach (var expression in _expressions)
+ expression.Execute();
+ }
+
private void ExecuteStatement(StringBuilder stmtBuilder)
{
var stmt = stmtBuilder.ToString();
diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs
index 5c999ad6ef..2e2bc8b661 100644
--- a/src/Umbraco.Core/Migrations/MigrationPlan.cs
+++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
+using Type = System.Type;
namespace Umbraco.Core.Migrations
{
@@ -12,8 +13,6 @@ namespace Umbraco.Core.Migrations
///
public class MigrationPlan
{
- private readonly IMigrationBuilder _migrationBuilder;
- private readonly ILogger _logger;
private readonly Dictionary _transitions = new Dictionary();
private string _prevState;
@@ -23,64 +22,24 @@ namespace Umbraco.Core.Migrations
/// Initializes a new instance of the class.
///
/// The name of the plan.
- /// The plan cannot be executed. Use this constructor e.g. when only validating the plan,
- /// or trying to get its final state, without actually needing to execute it.
public MigrationPlan(string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
Name = name;
-
- // ReSharper disable once VirtualMemberCallInConstructor
- // (accepted)
- DefinePlan();
}
///
- /// Initializes a new instance of the class.
+ /// Gets the transitions.
///
- /// The name of the plan.
- /// A migration builder.
- /// A logger.
- /// The plan can be executed.
- public MigrationPlan(string name, IMigrationBuilder migrationBuilder, ILogger logger)
- {
- if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
- Name = name;
- _migrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-
- // ReSharper disable once VirtualMemberCallInConstructor
- // (accepted)
- DefinePlan();
- }
-
- ///
- /// Defines the plan.
- ///
- protected virtual void DefinePlan() { }
+ public IReadOnlyDictionary Transitions => _transitions;
///
/// Gets the name of the plan.
///
public string Name { get; }
- ///
- /// Adds an empty migration from source to target state.
- ///
- public MigrationPlan Add(string sourceState, string targetState)
- => Add(sourceState, targetState);
-
- ///
- /// Adds a migration from source to target state.
- ///
- public MigrationPlan Add(string sourceState, string targetState)
- where TMigration : IMigration
- => Add(sourceState, targetState, typeof(TMigration));
-
- ///
- /// Adds a migration from source to target state.
- ///
- public MigrationPlan Add(string sourceState, string targetState, Type migration)
+ // adds a transition
+ private MigrationPlan Add(string sourceState, string targetState, Type migration)
{
if (sourceState == null) throw new ArgumentNullException(nameof(sourceState));
if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState));
@@ -113,26 +72,26 @@ namespace Umbraco.Core.Migrations
}
///
- /// Chains an empty migration from chain to target state.
- ///
- public MigrationPlan Chain(string targetState)
- => Chain(targetState);
+ /// Adds a transition to a target state through an empty migration.
+ ///
+ public MigrationPlan To(string targetState)
+ => To(targetState);
///
- /// Chains a migration from chain to target state.
+ /// Adds a transition to a target state through a migration.
///
- public MigrationPlan Chain(string targetState)
+ public MigrationPlan To(string targetState)
where TMigration : IMigration
- => Chain(targetState, typeof(TMigration));
+ => To(targetState, typeof(TMigration));
///
- /// Chains a migration from chain to target state.
+ /// Adds a transition to a target state through a migration.
///
- public MigrationPlan Chain(string targetState, Type migration)
+ public MigrationPlan To(string targetState, Type migration)
=> Add(_prevState, targetState, migration);
///
- /// Sets the chain state.
+ /// Sets the starting state.
///
public MigrationPlan From(string sourceState)
{
@@ -141,19 +100,16 @@ namespace Umbraco.Core.Migrations
}
///
- /// Copies a chain.
+ /// Adds transitions to a target state by copying transitions from a start state to an end state.
///
- /// Copies the chain going from startState to endState, with new states going from sourceState to targetState.
- public MigrationPlan CopyChain(string sourceState, string startState, string endState, string targetState)
+ public MigrationPlan To(string targetState, string startState, string endState)
{
- if (sourceState == null) throw new ArgumentNullException(nameof(sourceState));
if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState));
if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState));
if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState));
- if (sourceState == targetState) throw new ArgumentException("Source and target states cannot be identical.");
+
if (startState == endState) throw new ArgumentException("Start and end states cannot be identical.");
- sourceState = sourceState.Trim();
startState = startState.Trim();
endState = endState.Trim();
targetState = targetState.Trim();
@@ -168,26 +124,18 @@ namespace Umbraco.Core.Migrations
visited.Add(state);
if (!_transitions.TryGetValue(state, out var transition))
- throw new InvalidOperationException($"There is no transition from state \"{sourceState}\".");
+ throw new InvalidOperationException($"There is no transition from state \"{state}\".");
var newTargetState = transition.TargetState == endState
? targetState
: Guid.NewGuid().ToString("B").ToUpper();
- Add(sourceState, newTargetState, transition.MigrationType);
- sourceState = newTargetState;
+ To(newTargetState, transition.MigrationType);
state = transition.TargetState;
}
return this;
}
- ///
- /// Copies a chain.
- ///
- /// Copies the chain going from startState to endState, with new states going from chain to targetState.
- public MigrationPlan CopyChain(string startState, string endState, string targetState)
- => CopyChain(_prevState, startState, endState, targetState);
-
///
/// Gets the initial state.
///
@@ -260,50 +208,92 @@ namespace Umbraco.Core.Migrations
///
/// A scope.
/// The state to start execution at.
+ /// A migration builder.
+ /// A logger.
/// The final state.
/// The plan executes within the scope, which must then be completed.
- public string Execute(IScope scope, string fromState)
+ public string Execute(IScope scope, string fromState, IMigrationBuilder migrationBuilder, ILogger logger)
{
Validate();
- if (_migrationBuilder == null || _logger == null)
- throw new InvalidOperationException("Cannot execute a non-executing plan.");
+ if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder));
+ if (logger == null) throw new ArgumentNullException(nameof(logger));
- _logger.Info("Starting '{MigrationName}'...", Name);
+ logger.Info("Starting '{MigrationName}'...", Name);
var origState = fromState ?? string.Empty;
- _logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState);
+ logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState);
if (!_transitions.TryGetValue(origState, out var transition))
throw new Exception($"Unknown state \"{origState}\".");
- var context = new MigrationContext(scope.Database, _logger);
+ var context = new MigrationContext(scope.Database, logger);
while (transition != null)
{
- var migration = _migrationBuilder.Build(transition.MigrationType, context);
+ var migration = migrationBuilder.Build(transition.MigrationType, context);
migration.Migrate();
var nextState = transition.TargetState;
origState = nextState;
- _logger.Info("At {OrigState}", origState);
+ logger.Info("At {OrigState}", origState);
if (!_transitions.TryGetValue(origState, out transition))
throw new Exception($"Unknown state \"{origState}\".");
}
- _logger.Info("Done (pending scope completion).");
+ logger.Info("Done (pending scope completion).");
+
+ // safety check
+ if (origState != _finalState)
+ throw new Exception($"Internal error, reached state {origState} which is not final state {_finalState}");
+
+ return origState;
+ }
+
+ ///
+ /// Follows a path (for tests and debugging).
+ ///
+ /// Does the same thing Execute does, but does not actually execute migrations.
+ internal string FollowPath(string fromState, string toState = null)
+ {
+ toState = toState.NullOrWhiteSpaceAsNull();
+
+ Validate();
+
+ var origState = fromState ?? string.Empty;
+
+ if (!_transitions.TryGetValue(origState, out var transition))
+ throw new Exception($"Unknown state \"{origState}\".");
+
+ while (transition != null)
+ {
+ var nextState = transition.TargetState;
+ origState = nextState;
+
+ if (nextState == toState)
+ {
+ transition = null;
+ continue;
+ }
+
+ if (!_transitions.TryGetValue(origState, out transition))
+ throw new Exception($"Unknown state \"{origState}\".");
+ }
+
+ // safety check
+ if (origState != (toState ?? _finalState))
+ throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}");
- // fixme - what about post-migrations?
return origState;
}
///
/// Represents a plan transition.
///
- private class Transition
+ public class Transition
{
///
/// Initializes a new instance of the class.
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index 53a86f3524..ca9ff3fb72 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -2,7 +2,6 @@
using System.Configuration;
using Semver;
using Umbraco.Core.Configuration;
-using Umbraco.Core.Logging;
using Umbraco.Core.Migrations.Upgrade.V_7_12_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_0;
@@ -18,14 +17,9 @@ namespace Umbraco.Core.Migrations.Upgrade
///
public UmbracoPlan()
: base(Constants.System.UmbracoUpgradePlanName)
- { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- public UmbracoPlan(IMigrationBuilder migrationBuilder, ILogger logger)
- : base(Constants.System.UmbracoUpgradePlanName, migrationBuilder, logger)
- { }
+ {
+ DefinePlan();
+ }
///
///
@@ -61,8 +55,8 @@ namespace Umbraco.Core.Migrations.Upgrade
}
}
- ///
- protected override void DefinePlan()
+ // define the plan
+ protected void DefinePlan()
{
// MODIFYING THE PLAN
//
@@ -85,66 +79,75 @@ namespace Umbraco.Core.Migrations.Upgrade
// upgrades from 7 to 8, and then takes care of all eventual upgrades
//
From("{init-7.10.0}");
- Chain("{7C447271-CA3F-4A6A-A913-5D77015655CB}");
- Chain("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}");
- Chain("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}");
- Chain("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}");
- Chain("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}");
- Chain("{8640C9E4-A1C0-4C59-99BB-609B4E604981}");
- Chain("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}");
- Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}");
- Chain("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}");
- Chain("{7F59355A-0EC9-4438-8157-EB517E6D2727}");
+ To("{7C447271-CA3F-4A6A-A913-5D77015655CB}");
+ To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}");
+ To("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}");
+ To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}");
+ To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}");
+ To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}");
+ To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}");
+ To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}");
+ To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}");
+ To("{7F59355A-0EC9-4438-8157-EB517E6D2727}");
+ //To("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); // AddVariationTables1 has been superseded by AddVariationTables2
+ To("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
- // AddVariationTables1 has been superceeded by AddVariationTables2
- //Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}");
- Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
- // however, provide a path out of the old state
- Add("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
+ // way out of the commented state
+ From("{941B2ABA-2D06-4E04-81F5-74224F1DB037}");
+ To("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
// resume at {76DF5CD7-A884-41A5-8DC6-7860D95B1DF5} ...
- Chain("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}");
+ To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}");
+ To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
+ //To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove
+ To("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state
- Chain("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
- //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove,
- Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state,
- Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // and provide a path out of the conflict state
+ // way out of the commented state
+ From("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}");
+ To("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}");
// resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ...
- Chain("{1350617A-4930-4D61-852F-E3AA9E692173}");
- Chain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
- //Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
- Chain("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
- Chain("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
- Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
- Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
- //Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
+ To("{1350617A-4930-4D61-852F-E3AA9E692173}");
+ To("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
+ //To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
+ To("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
+ To("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
+ To("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
+ To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
+ //To("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
- Chain("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state
- From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's
- .CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next
+ To("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state
+
+ // way out of andy's
+ From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}");
+ To("{8B14CEBD-EE47-4AAD-A841-93551D917F11}", "{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}");
// resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ...
- Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state
- From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}") // and provide a path out of stephan's
- .Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
+ To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state
+
+ // way out of the commented state
+ From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}");
+ To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
// resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ...
- //Chain("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
- Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
- From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}")
- .Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
+ //To("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
+ To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
+
+ // way out of the commented state
+ From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
+ To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
// resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}...
- Chain("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}");
- Chain("{77874C77-93E5-4488-A404-A630907CEEF0}");
- Chain("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
- Chain("{23275462-446E-44C7-8C2C-3B8C1127B07D}");
- Chain("{6B251841-3069-4AD5-8AE9-861F9523E8DA}");
- Chain("{EE429F1B-9B26-43CA-89F8-A86017C809A3}");
- Chain("{08919C4B-B431-449C-90EC-2B8445B5C6B1}");
- Chain("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}");
- Chain("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version
+ To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}");
+ To("{77874C77-93E5-4488-A404-A630907CEEF0}");
+ To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
+ To("{23275462-446E-44C7-8C2C-3B8C1127B07D}");
+ To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}");
+ To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}");
+ To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}");
+ To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}");
+ To("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version
+ To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}");
//FINAL
@@ -153,20 +156,20 @@ namespace Umbraco.Core.Migrations.Upgrade
// and then, need to support upgrading from more recent 7.x
//
- From("{init-7.10.1}").Chain("{init-7.10.0}"); // same as 7.10.0
- From("{init-7.10.2}").Chain("{init-7.10.0}"); // same as 7.10.0
- From("{init-7.10.3}").Chain("{init-7.10.0}"); // same as 7.10.0
- From("{init-7.10.4}").Chain("{init-7.10.0}"); // same as 7.10.0
- From("{init-7.11.0}").Chain("{init-7.10.0}"); // same as 7.10.0
- From("{init-7.11.1}").Chain("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.10.1}").To("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.10.2}").To("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.10.3}").To("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.10.4}").To("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.11.0}").To("{init-7.10.0}"); // same as 7.10.0
+ From("{init-7.11.1}").To("{init-7.10.0}"); // same as 7.10.0
// 7.12.0 has migrations, define a custom chain which copies the chain
// going from {init-7.10.0} to former final (1350617A) , and then goes straight to
// main chain, skipping the migrations
//
From("{init-7.12.0}");
- // copy from copy to (former final) main chain
- CopyChain("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}");
+ // target copy from copy to (former final)
+ To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}");
}
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs
index b24ad2a20e..fa29e80a6b 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs
@@ -8,24 +8,41 @@ using Umbraco.Core.Services;
namespace Umbraco.Core.Migrations.Upgrade
{
+ ///
+ /// Represents the Umbraco upgrader.
+ ///
public class UmbracoUpgrader : Upgrader
{
- public UmbracoUpgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger)
- : base(scopeProvider, migrationBuilder, keyValueService, postMigrations, logger)
+ private PostMigrationCollection _postMigrations;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UmbracoUpgrader()
+ : base(new UmbracoPlan())
{ }
- protected override MigrationPlan GetPlan()
+ ///
+ /// Executes.
+ ///
+ public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger, PostMigrationCollection postMigrations)
{
- return new UmbracoPlan(MigrationBuilder, Logger);
+ _postMigrations = postMigrations;
+ Execute(scopeProvider, migrationBuilder, keyValueService, logger);
}
- protected override (SemVersion, SemVersion) GetVersions()
+ ///
+ public override void AfterMigrations(IScope scope, ILogger logger)
{
- // assume we have something in web.config that makes some sense
- if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var currentVersion))
+ // assume we have something in web.config that makes some sense = the origin version
+ if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var originVersion))
throw new InvalidOperationException("Could not get current version from web.config umbracoConfigurationStatus appSetting.");
- return (currentVersion, UmbracoVersion.SemanticVersion);
+ // target version is the code version
+ var targetVersion = UmbracoVersion.SemanticVersion;
+
+ foreach (var postMigration in _postMigrations)
+ postMigration.Execute(Name, scope, originVersion, targetVersion, logger);
}
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs
index 974ed7b4f8..f6df52bc1e 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs
@@ -1,49 +1,60 @@
using System;
-using Semver;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
namespace Umbraco.Core.Migrations.Upgrade
{
- public abstract class Upgrader
+ ///
+ /// Represents an upgrader.
+ ///
+ public class Upgrader
{
- private readonly IKeyValueService _keyValueService;
- private readonly PostMigrationCollection _postMigrations;
- private MigrationPlan _plan;
-
- protected Upgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Upgrader(MigrationPlan plan)
{
- ScopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
- MigrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder));
- _keyValueService = keyValueService ?? throw new ArgumentNullException(nameof(keyValueService));
- _postMigrations = postMigrations ?? throw new ArgumentNullException(nameof(postMigrations));
- Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ Plan = plan;
}
+ ///
+ /// Gets the name of the migration plan.
+ ///
public string Name => Plan.Name;
- public string StateValueKey => GetStateValueKey(Plan);
+ ///
+ /// Gets the migration plan.
+ ///
+ public MigrationPlan Plan { get; }
- protected IScopeProvider ScopeProvider { get; }
+ ///
+ /// Gets the key for the state value.
+ ///
+ public virtual string StateValueKey => "Umbraco.Core.Upgrader.State+" + Name;
- protected IMigrationBuilder MigrationBuilder { get; }
-
- protected ILogger Logger { get; }
-
- protected MigrationPlan Plan => _plan ?? (_plan = GetPlan());
-
- protected abstract MigrationPlan GetPlan();
- protected abstract (SemVersion, SemVersion) GetVersions();
-
- public void Execute()
+ ///
+ /// Executes.
+ ///
+ /// A scope provider.
+ /// A migration builder.
+ /// A key-value service.
+ /// A logger.
+ public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger)
{
+ if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider));
+ if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder));
+ if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService));
+ if (logger == null) throw new ArgumentNullException(nameof(logger));
+
var plan = Plan;
- using (var scope = ScopeProvider.CreateScope())
+ using (var scope = scopeProvider.CreateScope())
{
+ BeforeMigrations(scope, logger);
+
// read current state
- var currentState = _keyValueService.GetValue(StateValueKey);
+ var currentState = keyValueService.GetValue(StateValueKey);
var forceState = false;
if (currentState == null)
@@ -53,25 +64,33 @@ namespace Umbraco.Core.Migrations.Upgrade
}
// execute plan
- var state = plan.Execute(scope, currentState);
+ var state = plan.Execute(scope, currentState, migrationBuilder, logger);
if (string.IsNullOrWhiteSpace(state))
throw new Exception("Plan execution returned an invalid null or empty state.");
// save new state
if (forceState)
- _keyValueService.SetValue(StateValueKey, state);
+ keyValueService.SetValue(StateValueKey, state);
else if (currentState != state)
- _keyValueService.SetValue(StateValueKey, currentState, state);
+ keyValueService.SetValue(StateValueKey, currentState, state);
- // run post-migrations
- (var originVersion, var targetVersion) = GetVersions();
- foreach (var postMigration in _postMigrations)
- postMigration.Execute(Name, scope, originVersion, targetVersion, Logger);
+ AfterMigrations(scope, logger);
scope.Complete();
}
}
- public static string GetStateValueKey(MigrationPlan plan) => "Umbraco.Core.Upgrader.State+" + plan.Name;
+ ///
+ /// Executes as part of the upgrade scope and before all migrations have executed.
+ ///
+ public virtual void BeforeMigrations(IScope scope, ILogger logger)
+ { }
+
+ ///
+ /// Executes as part of the upgrade scope and after all migrations have executed.
+ ///
+ public virtual void AfterMigrations(IScope scope, ILogger logger)
+ { }
+
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs
new file mode 100644
index 0000000000..9ccd6d5e76
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs
@@ -0,0 +1,27 @@
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
+{
+ public class MakeTagsVariant : MigrationBase
+ {
+ public MakeTagsVariant(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ AddColumn("languageId");
+
+ Delete.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag).Do();
+ Create.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag)
+ .OnColumn("group")
+ .Ascending()
+ .OnColumn("tag")
+ .Ascending()
+ .OnColumn("languageId")
+ .Ascending()
+ .WithOptions().Unique()
+ .Do();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentTagsExtensions.cs b/src/Umbraco.Core/Models/ContentTagsExtensions.cs
index 8aceaac762..dd7a716520 100644
--- a/src/Umbraco.Core/Models/ContentTagsExtensions.cs
+++ b/src/Umbraco.Core/Models/ContentTagsExtensions.cs
@@ -15,10 +15,10 @@ namespace Umbraco.Core.Models
/// The property alias.
/// The tags.
/// A value indicating whether to merge the tags with existing tags instead of replacing them.
- /// Tags do not support variants.
- public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false)
+ /// A culture, for multi-lingual properties.
+ public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false, string culture = null)
{
- content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge);
+ content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge, culture);
}
///
@@ -27,10 +27,10 @@ namespace Umbraco.Core.Models
/// The content item.
/// The property alias.
/// The tags.
- /// Tags do not support variants.
- public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags)
+ /// A culture, for multi-lingual properties.
+ public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, string culture = null)
{
- content.GetTagProperty(propertyTypeAlias).RemoveTags(tags);
+ content.GetTagProperty(propertyTypeAlias).RemoveTags(tags, culture);
}
// gets and validates the property
diff --git a/src/Umbraco.Core/Models/ITag.cs b/src/Umbraco.Core/Models/ITag.cs
index 6f492a2d78..f2c30b2644 100644
--- a/src/Umbraco.Core/Models/ITag.cs
+++ b/src/Umbraco.Core/Models/ITag.cs
@@ -20,6 +20,12 @@ namespace Umbraco.Core.Models
[DataMember]
string Text { get; set; }
+ ///
+ /// Gets or sets the tag language.
+ ///
+ [DataMember]
+ int? LanguageId { get; set; }
+
///
/// Gets the number of nodes tagged with this tag.
///
diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs
index 26779161a1..39172fff34 100644
--- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs
+++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs
@@ -38,13 +38,13 @@ namespace Umbraco.Core.Models
}
///
- /// Assign default tags.
+ /// Assign tags.
///
/// The property.
/// The tags.
/// A value indicating whether to merge the tags with existing tags instead of replacing them.
- /// Tags do not support variants.
- public static void AssignTags(this Property property, IEnumerable tags, bool merge = false)
+ /// A culture, for multi-lingual properties.
+ public static void AssignTags(this Property property, IEnumerable tags, bool merge = false, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
@@ -52,11 +52,11 @@ namespace Umbraco.Core.Models
if (configuration == null)
throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags.");
- property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter);
+ property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter, culture);
}
// assumes that parameters are consistent with the datatype configuration
- internal static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter)
+ private static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter, string culture)
{
// set the property value
var trimmedTags = tags.Select(x => x.Trim()).ToArray();
@@ -68,11 +68,11 @@ namespace Umbraco.Core.Models
switch (storageType)
{
case TagsStorageType.Csv:
- property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags))); // csv string
+ property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string
break;
case TagsStorageType.Json:
- property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray())); // json array
+ property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray()), culture); // json array
break;
}
}
@@ -81,23 +81,23 @@ namespace Umbraco.Core.Models
switch (storageType)
{
case TagsStorageType.Csv:
- property.SetValue(string.Join(delimiter.ToString(), trimmedTags)); // csv string
+ property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string
break;
case TagsStorageType.Json:
- property.SetValue(JsonConvert.SerializeObject(trimmedTags)); // json array
+ property.SetValue(JsonConvert.SerializeObject(trimmedTags), culture); // json array
break;
}
}
}
///
- /// Removes default tags.
+ /// Removes tags.
///
/// The property.
/// The tags.
- /// Tags do not support variants.
- public static void RemoveTags(this Property property, IEnumerable tags)
+ /// A culture, for multi-lingual properties.
+ public static void RemoveTags(this Property property, IEnumerable tags, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
@@ -105,33 +105,33 @@ namespace Umbraco.Core.Models
if (configuration == null)
throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags.");
- property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter);
+ property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter, culture);
}
// assumes that parameters are consistent with the datatype configuration
- private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter)
+ private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter, string culture)
{
// already empty = nothing to do
- //fixme doesn't take into account variants
- var value = property.GetValue()?.ToString();
+ var value = property.GetValue(culture)?.ToString();
if (string.IsNullOrWhiteSpace(value)) return;
// set the property value
var trimmedTags = tags.Select(x => x.Trim()).ToArray();
- var currentTags = property.GetTagsValue(storageType, delimiter);
+ var currentTags = property.GetTagsValue(storageType, delimiter, culture);
switch (storageType)
{
case TagsStorageType.Csv:
- property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags))); // csv string
+ property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string
break;
case TagsStorageType.Json:
- property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray())); // json array
+ property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray()), culture); // json array
break;
}
}
- internal static IEnumerable GetTagsValue(this Property property)
+ // used by ContentRepositoryBase
+ internal static IEnumerable GetTagsValue(this Property property, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
@@ -139,15 +139,14 @@ namespace Umbraco.Core.Models
if (configuration == null)
throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags.");
- return property.GetTagsValue(configuration.StorageType, configuration.Delimiter);
+ return property.GetTagsValue(configuration.StorageType, configuration.Delimiter, culture);
}
- internal static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter)
+ private static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter, string culture = null)
{
if (property == null) throw new ArgumentNullException(nameof(property));
- //fixme doesn't take into account variants
- var value = property.GetValue()?.ToString();
+ var value = property.GetValue(culture)?.ToString();
if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty();
switch (storageType)
@@ -158,7 +157,6 @@ namespace Umbraco.Core.Models
case TagsStorageType.Json:
try
{
- //fixme doesn't take into account variants
return JsonConvert.DeserializeObject(value).Select(x => x.ToString().Trim());
}
catch (JsonException)
@@ -178,34 +176,33 @@ namespace Umbraco.Core.Models
/// The property.
/// The property value.
/// The datatype configuration.
- ///
+ /// A culture, for multi-lingual properties.
+ ///
/// The value is either a string (delimited string) or an enumeration of strings (tag list).
/// This is used both by the content repositories to initialize a property with some tag values, and by the
/// content controllers to update a property with values received from the property editor.
///
- internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration)
+ internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration, string culture)
{
if (property == null) throw new ArgumentNullException(nameof(property));
if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration));
- var merge = false; // fixme always!
var storageType = tagConfiguration.StorageType;
var delimiter = tagConfiguration.Delimiter;
- SetTagsValue(property, value, merge, storageType, delimiter);
+ SetTagsValue(property, value, storageType, delimiter, culture);
}
// assumes that parameters are consistent with the datatype configuration
// value can be an enumeration of string, or a serialized value using storageType format
- // fixme merge always false here?!
- private static void SetTagsValue(Property property, object value, bool merge, TagsStorageType storageType, char delimiter)
+ private static void SetTagsValue(Property property, object value, TagsStorageType storageType, char delimiter, string culture)
{
if (value == null) value = Enumerable.Empty();
// if value is already an enumeration of strings, just use it
if (value is IEnumerable tags1)
{
- property.AssignTags(tags1, merge, storageType, delimiter);
+ property.AssignTags(tags1, false, storageType, delimiter, culture);
return;
}
@@ -214,14 +211,14 @@ namespace Umbraco.Core.Models
{
case TagsStorageType.Csv:
var tags2 = value.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries);
- property.AssignTags(tags2, merge, storageType, delimiter);
+ property.AssignTags(tags2, false, storageType, delimiter, culture);
break;
case TagsStorageType.Json:
try
{
var tags3 = JsonConvert.DeserializeObject>(value.ToString());
- property.AssignTags(tags3 ?? Enumerable.Empty(), merge, storageType, delimiter);
+ property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, delimiter, culture);
}
catch (Exception ex)
{
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs
index 5b604eff3f..0c049e81bf 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs
@@ -151,7 +151,7 @@ namespace Umbraco.Core.Models.PublishedContent
/// is the edited version) or false (document is published, and has not been edited, and
/// what is returned is the published version).
///
- bool IsDraft { get; }
+ bool IsDraft(string culture = null);
// fixme - consider having an IsPublished flag too
// so that when IsDraft is true, we can check whether there is a published version?
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs
index 5bdeb3685d..6a69d0b9e1 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs
@@ -109,7 +109,7 @@ namespace Umbraco.Core.Models.PublishedContent
public virtual PublishedItemType ItemType => _content.ItemType;
///
- public virtual bool IsDraft => _content.IsDraft;
+ public virtual bool IsDraft(string culture = null) => _content.IsDraft(culture);
#endregion
diff --git a/src/Umbraco.Core/Models/Tag.cs b/src/Umbraco.Core/Models/Tag.cs
index 867d43c257..e9707e587d 100644
--- a/src/Umbraco.Core/Models/Tag.cs
+++ b/src/Umbraco.Core/Models/Tag.cs
@@ -16,6 +16,7 @@ namespace Umbraco.Core.Models
private string _group;
private string _text;
+ private int? _languageId;
///
/// Initializes a new instance of the class.
@@ -26,11 +27,12 @@ namespace Umbraco.Core.Models
///
/// Initializes a new instance of the class.
///
- public Tag(int id, string group, string text)
+ public Tag(int id, string group, string text, int? languageId = null)
{
Id = id;
Text = text;
Group = group;
+ LanguageId = languageId;
}
private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors());
@@ -39,6 +41,7 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo Group = ExpressionHelper.GetPropertyInfo(x => x.Group);
public readonly PropertyInfo Text = ExpressionHelper.GetPropertyInfo(x => x.Text);
+ public readonly PropertyInfo LanguageId = ExpressionHelper.GetPropertyInfo(x => x.LanguageId);
}
///
@@ -55,6 +58,13 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _text, Selectors.Text);
}
+ ///
+ public int? LanguageId
+ {
+ get => _languageId;
+ set => SetPropertyValueAndDetectChanges(value, ref _languageId, Selectors.LanguageId);
+ }
+
///
public int NodeCount { get; internal set; }
}
diff --git a/src/Umbraco.Core/Models/TaggedEntity.cs b/src/Umbraco.Core/Models/TaggedEntity.cs
index ac194c15cd..8c4695555d 100644
--- a/src/Umbraco.Core/Models/TaggedEntity.cs
+++ b/src/Umbraco.Core/Models/TaggedEntity.cs
@@ -5,10 +5,13 @@ namespace Umbraco.Core.Models
///
/// Represents a tagged entity.
///
- /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that is tagged,
- /// which is why this class is composed of a list of tagged properties and an Id reference to the actual entity.
+ /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged,
+ /// which is why this class is composed of a list of tagged properties and the identifier the actual entity.
public class TaggedEntity
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public TaggedEntity(int entityId, IEnumerable taggedProperties)
{
EntityId = entityId;
@@ -16,13 +19,13 @@ namespace Umbraco.Core.Models
}
///
- /// Id of the entity, which is tagged
+ /// Gets the identifier of the entity.
///
- public int EntityId { get; private set; }
+ public int EntityId { get; }
///
- /// An enumerable list of tagged properties
+ /// Gets the tagged properties.
///
- public IEnumerable TaggedProperties { get; private set; }
+ public IEnumerable TaggedProperties { get; }
}
}
diff --git a/src/Umbraco.Core/Models/TaggedProperty.cs b/src/Umbraco.Core/Models/TaggedProperty.cs
index 2b9650b432..2d9fda9a4f 100644
--- a/src/Umbraco.Core/Models/TaggedProperty.cs
+++ b/src/Umbraco.Core/Models/TaggedProperty.cs
@@ -7,6 +7,9 @@ namespace Umbraco.Core.Models
///
public class TaggedProperty
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public TaggedProperty(int propertyTypeId, string propertyTypeAlias, IEnumerable tags)
{
PropertyTypeId = propertyTypeId;
@@ -15,18 +18,18 @@ namespace Umbraco.Core.Models
}
///
- /// Id of the PropertyType, which this tagged property is based on
+ /// Gets the identifier of the property type.
///
- public int PropertyTypeId { get; private set; }
+ public int PropertyTypeId { get; }
///
- /// Alias of the PropertyType, which this tagged property is based on
+ /// Gets the alias of the property type.
///
- public string PropertyTypeAlias { get; private set; }
+ public string PropertyTypeAlias { get; }
///
- /// An enumerable list of Tags for the property
+ /// Gets the tags.
///
- public IEnumerable Tags { get; private set; }
+ public IEnumerable Tags { get; }
}
}
diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs
index 318a826b25..1bc2fb48a5 100644
--- a/src/Umbraco.Core/ObjectExtensions.cs
+++ b/src/Umbraco.Core/ObjectExtensions.cs
@@ -595,7 +595,6 @@ namespace Umbraco.Core
return null;
}
-
///
/// Attempts to serialize the value to an XmlString using ToXmlString
///
@@ -788,5 +787,7 @@ namespace Umbraco.Core
return BoolConvertCache[type] = false;
}
+
+
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs
index 15c309d9e5..f6296e4bd0 100644
--- a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs
@@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.Dtos
{
- [TableName(Constants.DatabaseSchema.Tables.Tag)]
+ [TableName(TableName)]
[PrimaryKey("id")]
[ExplicitColumns]
internal class TagDto
{
+ public const string TableName = Constants.DatabaseSchema.Tables.Tag;
+
[Column("id")]
[PrimaryKeyColumn]
public int Id { get; set; }
@@ -16,9 +18,15 @@ namespace Umbraco.Core.Persistence.Dtos
[Length(100)]
public string Group { get; set; }
+ [Column("languageId")]
+ [ForeignKey(typeof(LanguageDto))]
+ [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public int? LanguageId { get;set; }
+
[Column("tag")]
[Length(200)]
- [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag", Name = "IX_cmsTags")]
+ [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")]
public string Text { get; set; }
//[Column("key")]
diff --git a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs
index 4a07b16a07..cbe4cf0cd4 100644
--- a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs
@@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.Dtos
{
- [TableName(Constants.DatabaseSchema.Tables.TagRelationship)]
+ [TableName(TableName)]
[PrimaryKey("nodeId", AutoIncrement = false)]
[ExplicitColumns]
internal class TagRelationshipDto
{
+ public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship;
+
[Column("nodeId")]
[PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")]
[ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")]
diff --git a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs
index 867e6b0ae3..10441707ec 100644
--- a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs
@@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Factories
{
public static ITag BuildEntity(TagDto dto)
{
- var entity = new Tag(dto.Id, dto.Group, dto.Text) { NodeCount = dto.NodeCount };
+ var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount };
// reset dirty initial properties (U4-1946)
entity.ResetDirtyProperties(false);
return entity;
@@ -20,6 +20,7 @@ namespace Umbraco.Core.Persistence.Factories
Id = entity.Id,
Group = entity.Group,
Text = entity.Text,
+ LanguageId = entity.LanguageId
//Key = entity.Group + "/" + entity.Text // de-normalize
};
}
diff --git a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs
index 8cd2ab27d7..63f73d060a 100644
--- a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs
+++ b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs
@@ -23,6 +23,7 @@ namespace Umbraco.Core.Persistence.Mappers
CacheMap(src => src.Id, dto => dto.Id);
CacheMap(src => src.Text, dto => dto.Text);
CacheMap(src => src.Group, dto => dto.Group);
+ CacheMap(src => src.LanguageId, dto => dto.LanguageId);
}
}
}
diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs
index a5ab62d25f..7aa8b707be 100644
--- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs
+++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs
@@ -76,7 +76,7 @@ namespace Umbraco.Core.Persistence
var (s, a) = sql.SqlContext.VisitDto(predicate, alias);
return sql.Where(s, a);
}
-
+
///
/// Appends a WHERE clause to the Sql statement.
///
@@ -589,11 +589,14 @@ namespace Umbraco.Core.Persistence
/// Creates a SELECT COUNT(*) Sql statement.
///
/// The origin sql.
+ /// An optional alias.
/// The Sql statement.
- public static Sql SelectCount(this Sql sql)
+ public static Sql SelectCount(this Sql sql, string alias = null)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
- return sql.Select("COUNT(*)");
+ var text = "COUNT(*)";
+ if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
+ return sql.Select(text);
}
///
@@ -607,13 +610,29 @@ namespace Umbraco.Core.Persistence
/// If is empty, all columns are counted.
///
public static Sql SelectCount(this Sql sql, params Expression>[] fields)
+ => sql.SelectCount(null, fields);
+
+ ///
+ /// Creates a SELECT COUNT Sql statement.
+ ///
+ /// The type of the DTO to count.
+ /// The origin sql.
+ /// An alias.
+ /// Expressions indicating the columns to count.
+ /// The Sql statement.
+ ///
+ /// If is empty, all columns are counted.
+ ///
+ public static Sql SelectCount(this Sql sql, string alias, params Expression>[] fields)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
: fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
- return sql.Select("COUNT (" + string.Join(", ", columns) + ")");
+ var text = "COUNT (" + string.Join(", ", columns) + ")";
+ if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
+ return sql.Select(text);
}
///
@@ -643,6 +662,26 @@ namespace Umbraco.Core.Persistence
return sql.Select(sql.GetColumns(columnExpressions: fields));
}
+ ///
+ /// Creates a SELECT DISTINCT Sql statement.
+ ///
+ /// The type of the DTO to select.
+ /// The origin sql.
+ /// Expressions indicating the columns to select.
+ /// The Sql statement.
+ ///
+ /// If is empty, all columns are selected.
+ ///
+ public static Sql SelectDistinct(this Sql sql, params Expression>[] fields)
+ {
+ if (sql == null) throw new ArgumentNullException(nameof(sql));
+ var columns = sql.GetColumns(columnExpressions: fields);
+ sql.Append("SELECT DISTINCT " + string.Join(", ", columns));
+ return sql;
+ }
+
+ //this.Append("SELECT " + string.Join(", ", columns), new object[0]);
+
///
/// Creates a SELECT Sql statement.
///
@@ -705,6 +744,56 @@ namespace Umbraco.Core.Persistence
return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)));
}
+ ///
+ /// Adds a COUNT(*) to a SELECT Sql statement.
+ ///
+ /// The origin sql.
+ /// An optional alias.
+ /// The Sql statement.
+ public static Sql AndSelectCount(this Sql sql, string alias = null)
+ {
+ if (sql == null) throw new ArgumentNullException(nameof(sql));
+ var text = ", COUNT(*)";
+ if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
+ return sql.Append(text);
+ }
+
+ ///
+ /// Adds a COUNT to a SELECT Sql statement.
+ ///
+ /// The type of the DTO to count.
+ /// The origin sql.
+ /// Expressions indicating the columns to count.
+ /// The Sql statement.
+ ///
+ /// If is empty, all columns are counted.
+ ///
+ public static Sql AndSelectCount(this Sql sql, params Expression>[] fields)
+ => sql.AndSelectCount(null, fields);
+
+ ///
+ /// Adds a COUNT to a SELECT Sql statement.
+ ///
+ /// The type of the DTO to count.
+ /// The origin sql.
+ /// An alias.
+ /// Expressions indicating the columns to count.
+ /// The Sql statement.
+ ///
+ /// If is empty, all columns are counted.
+ ///
+ public static Sql AndSelectCount(this Sql sql, string alias = null, params Expression>[] fields)
+ {
+ if (sql == null) throw new ArgumentNullException(nameof(sql));
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
+ var columns = fields.Length == 0
+ ? sql.GetColumns(withAlias: false)
+ : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
+ var text = ", COUNT (" + string.Join(", ", columns) + ")";
+ if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias);
+ return sql.Append(text);
+ }
+
///
/// Creates a SELECT Sql statement with a referenced Dto.
///
@@ -1115,12 +1204,37 @@ namespace Umbraco.Core.Persistence
return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name;
}
- internal static void WriteToConsole(this Sql sql)
+ internal static string ToText(this Sql sql)
{
- Console.WriteLine(sql.SQL);
+ var text = new StringBuilder();
+ sql.ToText(text);
+ return text.ToString();
+ }
+
+ internal static void ToText(this Sql sql, StringBuilder text)
+ {
+ ToText(sql.SQL, sql.Arguments, text);
+ }
+
+ internal static void ToText(string sql, object[] arguments, StringBuilder text)
+ {
+ text.AppendLine(sql);
+
+ if (arguments == null || arguments.Length == 0)
+ return;
+
+ text.Append(" --");
+
var i = 0;
- foreach (var arg in sql.Arguments)
- Console.WriteLine($" @{i++}: {arg}");
+ foreach (var arg in arguments)
+ {
+ text.Append(" @");
+ text.Append(i++);
+ text.Append(":");
+ text.Append(arg);
+ }
+
+ text.AppendLine();
}
#endregion
diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
index d313d27bbc..16bfc9b164 100644
--- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
+++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
@@ -653,6 +653,23 @@ namespace Umbraco.Core.Persistence.Querying
else
throw new NotSupportedException("Expression is not a proper lambda.");
+ // c# 'x == null' becomes sql 'x IS NULL' which is fine
+ // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types,
+ // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required,
+ // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback
+ // value which will be used when values are null - turning the comparison into
+ // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside
+ // of x and y range - and if that is not possible, then a manual comparison need
+ // to be written
+ //TODO support SqlNullableEquals with 0 parameters, using the full syntax below
+ case "SqlNullableEquals":
+ var compareTo = Visit(m.Arguments[1]);
+ var fallback = Visit(m.Arguments[2]);
+ // that would work without a fallback value but is more cumbersome
+ //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))";
+ // use a fallback value
+ return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))";
+
default:
throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name);
diff --git a/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs
index 0f9eb47d77..710997472c 100644
--- a/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs
+++ b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs
@@ -9,6 +9,21 @@ namespace Umbraco.Core.Persistence.Querying
///
internal static class SqlExpressionExtensions
{
+ ///
+ /// Indicates whether two nullable values are equal, substituting a fallback value for nulls.
+ ///
+ /// The nullable type.
+ /// The value to compare.
+ /// The value to compare to.
+ /// The value to use when any value is null.
+ /// Do not use outside of Sql expressions.
+ // see usage in ExpressionVisitorBase
+ public static bool SqlNullableEquals(this T? value, T? other, T fallbackValue)
+ where T : struct
+ {
+ return (value ?? fallbackValue).Equals(other ?? fallbackValue);
+ }
+
public static bool SqlIn(this IEnumerable collection, T item)
{
return collection.Contains(item);
diff --git a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs
index 782f3f1b89..c3e6dc028b 100644
--- a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs
@@ -19,7 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories
/// When is false, the tags specified in are added to those already assigned.
/// When is empty and is true, all assigned tags are removed.
///
- void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags);
+ // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it
+ void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true);
///
/// Removes assigned tags from a content property.
@@ -46,54 +47,48 @@ namespace Umbraco.Core.Persistence.Repositories
#region Queries
+ ///
+ /// Gets a tagged entity.
+ ///
TaggedEntity GetTaggedEntityByKey(Guid key);
+
+ ///
+ /// Gets a tagged entity.
+ ///
TaggedEntity GetTaggedEntityById(int id);
- IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup);
-
- IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null);
+ /// Gets all entities of a type, tagged with any tag in the specified group.
+ IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null);
///
- /// Returns all tags for an entity type (content/media/member)
+ /// Gets all entities of a type, tagged with the specified tag.
///
- /// Entity type
- /// Optional group
- ///
- IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null);
+ IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null);
///
- /// Returns all tags that exist on the content item - Content/Media/Member
+ /// Gets all tags for an entity type.
///
- /// The content item id to get tags for
- /// Optional group
- ///
- IEnumerable GetTagsForEntity(int contentId, string group = null);
+ IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null);
///
- /// Returns all tags that exist on the content item - Content/Media/Member
+ /// Gets all tags attached to an entity.
///
- /// The content item id to get tags for
- /// Optional group
- ///
- IEnumerable GetTagsForEntity(Guid contentId, string group = null);
+ IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null);
///
- /// Returns all tags that exist on the content item for the property specified - Content/Media/Member
+ /// Gets all tags attached to an entity.
///
- /// The content item id to get tags for
- /// The property alias to get tags for
- /// Optional group
- ///
- IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null);
+ IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null);
///
- /// Returns all tags that exist on the content item for the property specified - Content/Media/Member
+ /// Gets all tags attached to an entity via a property.
///
- /// The content item id to get tags for
- /// The property alias to get tags for
- /// Optional group
- ///
- IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null);
+ IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null);
+
+ ///
+ /// Gets all tags attached to an entity via a property.
+ ///
+ IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null);
#endregion
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index 58f58c3d84..bd7943ff1d 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -217,8 +217,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
foreach (var property in entity.Properties)
{
var tagConfiguration = property.GetTagConfiguration();
- if (tagConfiguration == null) continue;
- tagRepo.Assign(entity.Id, property.PropertyTypeId, property.GetTagsValue().Select(x => new Tag { Group = tagConfiguration.Group, Text = x }), true);
+ if (tagConfiguration == null) continue; // not a tags property
+
+ if (property.PropertyType.VariesByCulture())
+ {
+ var tags = new List();
+ foreach (var pvalue in property.Values)
+ {
+ var tagsValue = property.GetTagsValue(pvalue.Culture);
+ var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture);
+ var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId });
+ tags.AddRange(cultureTags);
+ }
+ tagRepo.Assign(entity.Id, property.PropertyTypeId, tags);
+ }
+ else
+ {
+ var tagsValue = property.GetTagsValue(); // strings
+ var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x });
+ tagRepo.Assign(entity.Id, property.PropertyTypeId, tags);
+ }
}
}
@@ -541,16 +559,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
propertyDataDtos.AddRange(propertyDataDtos2);
var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList();
- // deal with tags
- foreach (var property in properties)
- {
- if (!tagConfigurations.TryGetValue(property.PropertyType.PropertyEditorAlias, out var tagConfiguration))
- continue;
-
- //fixme doesn't take into account variants
- property.SetTagsValue(property.GetValue(), tagConfiguration);
- }
-
if (result.ContainsKey(temp.VersionId))
{
if (ContentRepositoryBase.ThrowOnWarning)
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index 44215b7f7e..662254d1ee 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -663,9 +663,11 @@ AND umbracoNode.id <> @id",
{
case ContentVariation.Culture:
CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL);
+ CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL);
break;
case ContentVariation.Nothing:
CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL);
+ CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL);
break;
case ContentVariation.CultureAndSegment:
case ContentVariation.Segment:
@@ -757,6 +759,139 @@ AND umbracoNode.id <> @id",
}
}
+ ///
+ private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null)
+ {
+ // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers
+
+ // fixme - should we batch then?
+ var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0);
+ if (whereInArgsCount > 2000)
+ throw new NotSupportedException("Too many property/content types.");
+
+ // delete existing relations (for target language)
+ // do *not* delete existing tags
+
+ var sqlSelectTagsToDelete = Sql()
+ .Select(x => x.Id)
+ .From()
+ .InnerJoin().On((tag, rel) => tag.Id == rel.TagId);
+
+ if (contentTypeIds != null)
+ sqlSelectTagsToDelete
+ .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId)
+ .WhereIn(x => x.ContentTypeId, contentTypeIds);
+
+ sqlSelectTagsToDelete
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds)
+ .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
+
+ var sqlDeleteRelations = Sql()
+ .Delete()
+ .WhereIn(x => x.TagId, sqlSelectTagsToDelete);
+
+ Database.Execute(sqlDeleteRelations);
+
+ // do *not* delete the tags - they could be used by other content types / property types
+ /*
+ var sqlDeleteTag = Sql()
+ .Delete()
+ .WhereIn(x => x.Id, sqlTagToDelete);
+ Database.Execute(sqlDeleteTag);
+ */
+
+ // copy tags from source language to target language
+ // target tags may exist already, so we have to check for existence here
+ //
+ // select tags to insert: tags pointed to by a relation ship, for proper property/content types,
+ // and of source language, and where we cannot left join to an existing tag with same text,
+ // group and languageId
+
+ var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL";
+ var sqlSelectTagsToInsert = Sql()
+ .SelectDistinct(x => x.Text, x => x.Group)
+ .Append(", " + targetLanguageIdS)
+ .From();
+
+ sqlSelectTagsToInsert
+ .InnerJoin().On((tag, rel) => tag.Id == rel.TagId)
+ .LeftJoin("xtags").On((tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags");
+
+ if (contentTypeIds != null)
+ sqlSelectTagsToInsert
+ .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId)
+ .WhereIn(x => x.ContentTypeId, contentTypeIds);
+
+ sqlSelectTagsToInsert
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds)
+ .WhereNull(x => x.Id, "xtags") // ie, not exists
+ .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1));
+
+ var cols = Sql().Columns(x => x.Text, x => x.Group, x => x.LanguageId);
+ var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert);
+
+ Database.Execute(sqlInsertTags);
+
+ // create relations to new tags
+ // any existing relations have been deleted above, no need to check for existence here
+ //
+ // select node id and property type id from existing relations to tags of source language,
+ // for proper property/content types, and select new tag id from tags, with matching text,
+ // and group, but for the target language
+
+ var sqlSelectRelationsToInsert = Sql()
+ .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId)
+ .AndSelect("otag", x => x.Id)
+ .From()
+ .InnerJoin().On((rel, tag) => rel.TagId == tag.Id)
+ .InnerJoin("otag").On((tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag");
+
+ if (contentTypeIds != null)
+ sqlSelectRelationsToInsert
+ .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId)
+ .WhereIn(x => x.ContentTypeId, contentTypeIds);
+
+ sqlSelectRelationsToInsert
+ .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1))
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds);
+
+ var relationColumnsToInsert = Sql().Columns(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId);
+ var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert);
+
+ Database.Execute(sqlInsertRelations);
+
+ // delete original relations - *not* the tags - all of them
+ // cannot really "go back" with relations, would have to do it with property values
+
+ sqlSelectTagsToDelete = Sql()
+ .Select(x => x.Id)
+ .From()
+ .InnerJoin().On((tag, rel) => tag.Id == rel.TagId);
+
+ if (contentTypeIds != null)
+ sqlSelectTagsToDelete
+ .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId)
+ .WhereIn(x => x.ContentTypeId, contentTypeIds);
+
+ sqlSelectTagsToDelete
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds)
+ .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
+
+ sqlDeleteRelations = Sql()
+ .Delete()
+ .WhereIn(x => x.TagId, sqlSelectTagsToDelete);
+
+ Database.Execute(sqlDeleteRelations);
+
+ // no
+ /*
+ var sqlDeleteTag = Sql()
+ .Delete()
+ .WhereIn(x => x.Id, sqlTagToDelete);
+ Database.Execute(sqlDeleteTag);
+ */
+ }
+
///
/// Copies property data from one language to another.
///
@@ -766,6 +901,8 @@ AND umbracoNode.id <> @id",
/// The content type identifiers.
private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null)
{
+ // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers
+ //
// fixme - should we batch then?
var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0);
if (whereInArgsCount > 2000)
@@ -793,11 +930,7 @@ AND umbracoNode.id <> @id",
sqlDelete.WhereIn(x => x.VersionId, inSql);
}
- // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
- if (targetLanguageId == null)
- sqlDelete.Where(x => x.LanguageId == null);
- else
- sqlDelete.Where(x => x.LanguageId == targetLanguageId);
+ sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1));
sqlDelete
.WhereIn(x => x.PropertyTypeId, propertyTypeIds);
@@ -821,11 +954,7 @@ AND umbracoNode.id <> @id",
.InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id)
.InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId);
- // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
- if (sourceLanguageId == null)
- sqlSelectData.Where(x => x.LanguageId == null);
- else
- sqlSelectData.Where(x => x.LanguageId == sourceLanguageId);
+ sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1));
sqlSelectData
.WhereIn(x => x.PropertyTypeId, propertyTypeIds);
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs
index 09fb664ffe..e236670e74 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs
@@ -111,7 +111,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
"DELETE FROM umbracoPropertyData WHERE languageId = @id",
"DELETE FROM umbracoContentVersionCultureVariation WHERE languageId = @id",
"DELETE FROM umbracoDocumentCultureVariation WHERE languageId = @id",
- "DELETE FROM umbracoLanguage WHERE id = @id"
+ "DELETE FROM umbracoLanguage WHERE id = @id",
+ "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id"
};
return list;
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs
index 418e3d8ac3..77e474be08 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs
@@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
+using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -109,74 +110,65 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
#region Assign and Remove Tags
///
- public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags)
+ // only invoked from ContentRepositoryBase with all cultures + replaceTags being true
+ public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true)
{
// to no-duplicates array
var tagsA = tags.Distinct(new TagComparer()).ToArray();
- // no tags?
- if (tagsA.Length == 0)
+ // replacing = clear all
+ if (replaceTags)
{
- // replacing = clear all
- if (replaceTags)
- {
- var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId);
- Database.Execute(sql0);
- }
-
- // nothing else to do
- return;
+ var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId);
+ Database.Execute(sql0);
}
+ // no tags? nothing else to do
+ if (tagsA.Length == 0)
+ return;
+
// tags
// using some clever logic (?) to insert tags that don't exist in 1 query
+ // must coalesce languageId because equality of NULLs does not exist
var tagSetSql = GetTagSet(tagsA);
var group = SqlSyntax.GetQuotedColumnName("group");
// insert tags
- var sql1 = $@"INSERT INTO cmsTags (tag, {group})
-SELECT tagSet.tag, tagSet.{group}
+ var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId)
+SELECT tagSet.tag, tagSet.{group}, tagSet.languageId
FROM {tagSetSql}
-LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group})
+LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1))
WHERE cmsTags.id IS NULL";
Database.Execute(sql1);
- // if replacing, remove everything first
- if (replaceTags)
- {
- var sql2 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId);
- Database.Execute(sql2);
- }
-
// insert relations
- var sql3 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId)
+ var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId)
SELECT {contentId}, {propertyTypeId}, tagSet2.Id
FROM (
SELECT t.Id
FROM {tagSetSql}
- INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group})
+ INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1))
) AS tagSet2
LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId})
WHERE r.tagId IS NULL";
- Database.Execute(sql3);
+ Database.Execute(sql2);
}
///
+ // only invoked from tests
public void Remove(int contentId, int propertyTypeId, IEnumerable tags)
{
var tagSetSql = GetTagSet(tags);
+ var group = SqlSyntax.GetQuotedColumnName("group");
- var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ",
- contentId,
- " AND propertyTypeId = ",
- propertyTypeId,
- " AND tagId IN ",
- "(SELECT id FROM cmsTags INNER JOIN ",
- tagSetSql,
- " ON (TagSet.Tag = cmsTags.Tag and TagSet." + SqlSyntax.GetQuotedColumnName("group") + @" = cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @"))");
+ var deleteSql = $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN (
+ SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON (
+ tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)
+ )
+ )";
Database.Execute(deleteSql);
}
@@ -207,13 +199,6 @@ WHERE r.tagId IS NULL";
//
private string GetTagSet(IEnumerable tags)
{
- string EscapeSqlString(string s)
- {
- // why were we escaping @ symbols?
- //return NPocoDatabaseExtensions.EscapeAtSymbols(s.Replace("'", "''"));
- return s.Replace("'", "''");
- }
-
var sql = new StringBuilder();
var group = SqlSyntax.GetQuotedColumnName("group");
var first = true;
@@ -226,11 +211,17 @@ WHERE r.tagId IS NULL";
else sql.Append(" UNION ");
sql.Append("SELECT N'");
- sql.Append(EscapeSqlString(tag.Text));
+ sql.Append(SqlSyntax.EscapeString(tag.Text));
sql.Append("' AS tag, '");
- sql.Append(EscapeSqlString(tag.Group));
+ sql.Append(SqlSyntax.EscapeString(tag.Group));
sql.Append("' AS ");
sql.Append(group);
+ sql.Append(" , ");
+ if (tag.LanguageId.HasValue)
+ sql.Append(tag.LanguageId);
+ else
+ sql.Append("NULL");
+ sql.Append(" AS languageId");
}
sql.Append(") AS tagSet");
@@ -244,14 +235,17 @@ WHERE r.tagId IS NULL";
public bool Equals(ITag x, ITag y)
{
return ReferenceEquals(x, y) // takes care of both being null
- || x != null && y != null && x.Text == y.Text && x.Group == y.Group;
+ || x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId;
}
public int GetHashCode(ITag obj)
{
unchecked
{
- return (obj.Text.GetHashCode() * 397) ^ obj.Group.GetHashCode();
+ var h = obj.Text.GetHashCode();
+ h = h * 397 ^ obj.Group.GetHashCode();
+ h = h * 397 ^ (obj.LanguageId?.GetHashCode() ?? 0);
+ return h;
}
}
}
@@ -264,118 +258,126 @@ WHERE r.tagId IS NULL";
// consider caching implications
// add lookups for parentId or path (ie get content in tag group, that are descendants of x)
+ // ReSharper disable once ClassNeverInstantiated.Local
+ // ReSharper disable UnusedAutoPropertyAccessor.Local
+ private class TaggedEntityDto
+ {
+ public int NodeId { get; set; }
+ public string PropertyTypeAlias { get; set; }
+ public int PropertyTypeId { get; set; }
+ public int TagId { get; set; }
+ public string TagText { get; set; }
+ public string TagGroup { get; set; }
+ public int? TagLanguage { get; set; }
+ }
+ // ReSharper restore UnusedAutoPropertyAccessor.Local
+
+ ///
public TaggedEntity GetTaggedEntityByKey(Guid key)
{
- var sql = Sql()
- .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
- .From()
- .InnerJoin()
- .On(left => left.TagId, right => right.Id)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
- .InnerJoin()
- .On(left => left.Id, right => right.PropertyTypeId)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
+ var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*");
+
+ sql = sql
.Where(dto => dto.UniqueId == key);
- return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault();
+ return Map(Database.Fetch(sql)).FirstOrDefault();
}
+ ///
public TaggedEntity GetTaggedEntityById(int id)
{
- var sql = Sql()
- .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
- .From()
- .InnerJoin()
- .On(left => left.TagId, right => right.Id)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
- .InnerJoin()
- .On(left => left.Id, right => right.PropertyTypeId)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
+ var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*");
+
+ sql = sql
.Where(dto => dto.NodeId == id);
- return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault();
+ return Map(Database.Fetch(sql)).FirstOrDefault();
}
- public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup)
+ ///
+ public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null)
{
- var sql = Sql()
- .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
- .From()
- .InnerJoin()
- .On(left => left.TagId, right => right.Id)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
- .InnerJoin()
- .On(left => left.Id, right => right.PropertyTypeId)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
- .Where(dto => dto.Group == tagGroup);
+ var sql = GetTaggedEntitiesSql(objectType, culture);
- if (objectType != TaggableObjectTypes.All)
- {
- var nodeObjectType = GetNodeObjectType(objectType);
- sql = sql
- .Where(dto => dto.NodeObjectType == nodeObjectType);
- }
+ sql = sql
+ .Where(x => x.Group == group);
- return CreateTaggedEntityCollection(
- Database.Fetch(sql));
+ return Map(Database.Fetch(sql));
}
- public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null)
+ ///
+ public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null)
{
- var sql = Sql()
- .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group"))
- .From()
- .InnerJoin()
- .On(left => left.TagId, right => right.Id)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
- .InnerJoin()
- .On(left => left.Id, right => right.PropertyTypeId)
- .InnerJoin()
- .On(left => left.NodeId, right => right.NodeId)
+ var sql = GetTaggedEntitiesSql(objectType, culture);
+
+ sql = sql
.Where(dto => dto.Text == tag);
+ if (group.IsNullOrWhiteSpace() == false)
+ sql = sql
+ .Where(dto => dto.Group == group);
+
+ return Map(Database.Fetch(sql));
+ }
+
+ private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture)
+ {
+ var sql = Sql()
+ .Select(x => Alias(x.NodeId, "NodeId"))
+ .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId"))
+ .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage"))
+ .From()
+ .InnerJoin().On((tag, rel) => tag.Id == rel.TagId)
+ .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId)
+ .InnerJoin().On((rel, prop) => rel.PropertyTypeId == prop.Id)
+ .InnerJoin().On((content, node) => content.NodeId == node.NodeId);
+
+ if (culture == null)
+ {
+ sql = sql
+ .Where(dto => dto.LanguageId == null);
+ }
+ else if (culture != "*")
+ {
+ sql = sql
+ .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id)
+ .Where(x => x.IsoCode == culture);
+ }
+
if (objectType != TaggableObjectTypes.All)
{
var nodeObjectType = GetNodeObjectType(objectType);
- sql = sql
- .Where(dto => dto.NodeObjectType == nodeObjectType);
+ sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType);
}
- if (tagGroup.IsNullOrWhiteSpace() == false)
- {
- sql = sql.Where(dto => dto.Group == tagGroup);
- }
-
- return CreateTaggedEntityCollection(
- Database.Fetch(sql));
+ return sql;
}
- private IEnumerable CreateTaggedEntityCollection(IEnumerable dbResult)
+ private static IEnumerable Map(IEnumerable dtos)
{
- foreach (var node in dbResult.GroupBy(x => (int)x.nodeId))
+ return dtos.GroupBy(x => x.NodeId).Select(dtosForNode =>
{
- var properties = new List();
- foreach (var propertyType in node.GroupBy(x => new { id = (int)x.propertyTypeId, alias = (string)x.Alias }))
+ var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty =>
{
- var tags = propertyType.Select(x => new Tag((int)x.tagId, (string)x.group, (string)x.tag));
- properties.Add(new TaggedProperty(propertyType.Key.id, propertyType.Key.alias, tags));
- }
- yield return new TaggedEntity(node.Key, properties);
- }
+ string propertyTypeAlias = null;
+ var tags = dtosForProperty.Select(dto =>
+ {
+ propertyTypeAlias = dto.PropertyTypeAlias;
+ return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage);
+ }).ToList();
+ return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags);
+ }).ToList();
+
+ return new TaggedEntity(dtosForNode.Key, taggedProperties);
+ }).ToList();
}
- public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null)
+ ///
+ public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null)
{
- var sql = GetTagsQuerySelect(true);
+ var sql = GetTagsSql(culture, true);
- sql = ApplyRelationshipJoinToTagsQuery(sql);
+ AddTagsSqlWhere(sql, culture);
if (objectType != TaggableObjectTypes.All)
{
@@ -384,116 +386,126 @@ WHERE r.tagId IS NULL";
.Where(dto => dto.NodeObjectType == nodeObjectType);
}
- sql = ApplyGroupFilterToTagsQuery(sql, group);
+ if (group.IsNullOrWhiteSpace() == false)
+ sql = sql
+ .Where(dto => dto.Group == group);
- sql = ApplyGroupByToTagsQuery(sql);
+ sql = sql
+ .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId);
return ExecuteTagsQuery(sql);
}
- public IEnumerable GetTagsForEntity(int contentId, string group = null)
+ ///
+ public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null)
{
- var sql = GetTagsQuerySelect();
+ var sql = GetTagsSql(culture);
- sql = ApplyRelationshipJoinToTagsQuery(sql);
+ AddTagsSqlWhere(sql, culture);
sql = sql
.Where(dto => dto.NodeId == contentId);
- sql = ApplyGroupFilterToTagsQuery(sql, group);
+ if (group.IsNullOrWhiteSpace() == false)
+ sql = sql
+ .Where(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
- public IEnumerable GetTagsForEntity(Guid contentId, string group = null)
+ ///
+ public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null)
{
- var sql = GetTagsQuerySelect();
+ var sql = GetTagsSql(culture);
- sql = ApplyRelationshipJoinToTagsQuery(sql);
+ AddTagsSqlWhere(sql, culture);
sql = sql
.Where(dto => dto.UniqueId == contentId);
- sql = ApplyGroupFilterToTagsQuery(sql, group);
+ if (group.IsNullOrWhiteSpace() == false)
+ sql = sql
+ .Where(dto => dto.Group == group);
return ExecuteTagsQuery(sql);
}
- public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null)
+ ///
+ public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null)
{
- var sql = GetTagsQuerySelect();
-
- sql = ApplyRelationshipJoinToTagsQuery(sql);
+ var sql = GetTagsSql(culture);
sql = sql
- .InnerJoin()
- .On(left => left.Id, right => right.PropertyTypeId)
- .Where(dto => dto.NodeId == contentId)
- .Where(dto => dto.Alias == propertyTypeAlias);
+ .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId)
+ .Where