diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index e6862f587b..38d97febd5 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -174,15 +174,17 @@ public static class ContentRepositoryExtensions foreach (IProperty property in content.Properties) { // each property type may or may not support the variation - if (!property.PropertyType?.SupportsVariation(culture, "*", true) ?? false) + if ((!property.PropertyType?.SupportsVariation(culture, "*", true) ?? false) && + !(property.PropertyType?.Variations == ContentVariation.Nothing)) { continue; } foreach (IPropertyValue pvalue in property.Values) { - if ((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && - (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) + if (((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && + (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) || + property.PropertyType?.Variations == ContentVariation.Nothing) { property.SetValue(null, pvalue.Culture, pvalue.Segment); } @@ -193,7 +195,8 @@ public static class ContentRepositoryExtensions IPropertyCollection otherProperties = other.Properties; foreach (IProperty otherProperty in otherProperties) { - if (!otherProperty?.PropertyType?.SupportsVariation(culture, "*", true) ?? true) + if ((!otherProperty?.PropertyType?.SupportsVariation(culture, "*", true) ?? true) && + !(otherProperty?.PropertyType?.Variations == ContentVariation.Nothing)) { continue; } @@ -203,8 +206,9 @@ public static class ContentRepositoryExtensions { foreach (IPropertyValue pvalue in otherProperty.Values) { - if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, true) && - (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) + if (((otherProperty?.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && + (culture == "*" ||(pvalue.Culture?.InvariantEquals(culture) ?? false))) || + otherProperty?.PropertyType?.Variations == ContentVariation.Nothing) { var value = published ? pvalue.PublishedValue : pvalue.EditedValue; content.SetValue(alias, value, pvalue.Culture, pvalue.Segment); diff --git a/src/Umbraco.Core/Sync/RefreshMethodType.cs b/src/Umbraco.Core/Sync/RefreshMethodType.cs index f249a4701e..11de3a627f 100644 --- a/src/Umbraco.Core/Sync/RefreshMethodType.cs +++ b/src/Umbraco.Core/Sync/RefreshMethodType.cs @@ -10,12 +10,12 @@ public enum RefreshMethodType // that enum should get merged somehow with MessageType and renamed somehow // but at the moment it is exposed in CacheRefresher webservice through RefreshInstruction // so for the time being we keep it as-is for backward compatibility reasons - RefreshAll, - RefreshByGuid, - RefreshById, - RefreshByIds, - RefreshByJson, - RemoveById, + RefreshAll = 0, + RefreshByGuid = 1, + RefreshById = 2, + RefreshByIds = 3, + RefreshByJson = 4, + RemoveById = 5, // would adding values break backward compatibility? // RemoveByIds diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 1dd09d4171..f6abdc9ba7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -62,6 +62,7 @@ public class UmbracoPlan : MigrationPlan To("{F74CDA0C-7AAA-48C8-94C6-C6EC3C06F599}"); To("{21C42760-5109-4C03-AB4F-7EA53577D1F5}"); To("{6158F3A3-4902-4201-835E-1ED7F810B2D8}"); + To("{985AF2BA-69D3-4DBA-95E0-AD3FA7459FA7}"); // To 14.0.0 To("{419827A0-4FCE-464B-A8F3-247C6092AF55}"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs new file mode 100644 index 0000000000..84171e8717 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_3_0/AlignUpgradedDatabase.cs @@ -0,0 +1,181 @@ +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_3_0; + +/// +/// We see some differences between an updated database and a fresh one, +/// the purpose of this migration is to align the two. +/// +public class AlignUpgradedDatabase : MigrationBase +{ + public AlignUpgradedDatabase(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // We ignore SQLite since it's considered a development DB + if (DatabaseType == DatabaseType.SQLite) + { + return; + } + + ColumnInfo[] columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + Tuple[] indexes = SqlSyntax.GetDefinedIndexes(Database).ToArray(); + + DropCacheInstructionDefaultConstraint(columns); + AlignContentVersionTable(columns); + UpdateExternalLoginIndexes(indexes); + AlignNodeTable(columns); + MakeRelationTypeIndexUnique(indexes); + RemoveUserGroupDefault(columns); + MakeWebhookUrlNotNullable(columns); + MakeWebhookLogUrlNotNullable(columns); + } + + private void MakeIndexUnique(string tableName, string indexName, IEnumerable> indexes) + { + // Let's only mess with the indexes if we have to. + // Indexes are in format TableName, IndexName, ColumnName, IsUnique + Tuple? loginProviderIndex = indexes.FirstOrDefault(x => + x.Item1 == tableName && x.Item2 == indexName); + + // Item4 == IsUnique + if (loginProviderIndex?.Item4 is false) + { + // The recommended way to change an index from non-unique to unique is to drop and recreate it. + DeleteIndex(indexName); + CreateIndex(indexName); + } + } + + private void RemoveDefaultConstraint(string tableName, string columnName, IEnumerable columns) + { + ColumnInfo? targetColumn = columns + .FirstOrDefault(x => x.TableName == tableName && x.ColumnName == columnName); + + if (targetColumn is null) + { + throw new InvalidOperationException($"Could not find {columnName} column on {tableName} table."); + } + + if (targetColumn.ColumnDefault is null) + { + return; + } + + Delete.DefaultConstraint() + .OnTable(tableName) + .OnColumn(columnName) + .Do(); + } + + private void RenameColumn(string tableName, string oldColumnName, string newColumnName, IEnumerable columns) + { + ColumnInfo? targetColumn = columns + .FirstOrDefault(x => x.TableName == tableName && x.ColumnName == oldColumnName); + + if (targetColumn is null) + { + // The column was not found I.E. the column is correctly named + return; + } + + Rename.Column(oldColumnName) + .OnTable(tableName) + .To(newColumnName) + .Do(); + } + + private void MakeNvarCharColumnNotNullable(string tableName, string columnName, IEnumerable columns) + { + ColumnInfo? targetColumn = columns.FirstOrDefault(x => x.TableName == tableName && x.ColumnName == columnName); + + if (targetColumn is null) + { + throw new InvalidOperationException($"Could not find {columnName} column in {tableName} table."); + } + + if (targetColumn.IsNullable is false) + { + return; + } + + Alter.Table(tableName) + .AlterColumn(columnName) + .AsCustom("nvarchar(max)") + .NotNullable() + .Do(); + } + + private void DropCacheInstructionDefaultConstraint(IEnumerable columns) + => RemoveDefaultConstraint("umbracoCacheInstruction", "jsonInstruction", columns); + + private void AlignContentVersionTable(ColumnInfo[] columns) + { + // We need to do this to ensure we don't try to rename the constraint if it doesn't exist. + const string tableName = "umbracoContentVersion"; + const string columnName = "VersionDate"; + ColumnInfo? versionDateColumn = columns + .FirstOrDefault(x => x is { TableName: tableName, ColumnName: columnName }); + + if (versionDateColumn is null) + { + // The column was not found I.E. the column is correctly named + return; + } + + RenameColumn(tableName, columnName, "versionDate", columns); + + // Renames the default constraint for the column, + // apparently the content version table used to be prefixed with cms and not umbraco + // We don't have a fluid way to rename the default constraint so we have to use raw SQL + // This should be okay though since we are only running this migration on SQL Server + Sql renameConstraintQuery = Database.SqlContext.Sql( + "EXEC sp_rename N'DF_cmsContentVersion_VersionDate', N'DF_umbracoContentVersion_versionDate', N'OBJECT'"); + Database.Execute(renameConstraintQuery); + } + + private void UpdateExternalLoginIndexes(IEnumerable> indexes) + { + const string userMemberOrKeyIndexName = "IX_umbracoExternalLogin_userOrMemberKey"; + + MakeIndexUnique("umbracoExternalLogin", "IX_umbracoExternalLogin_LoginProvider", indexes); + + if (IndexExists(userMemberOrKeyIndexName)) + { + return; + } + + CreateIndex(userMemberOrKeyIndexName); + } + + private void AlignNodeTable(ColumnInfo[] columns) + { + const string tableName = "umbracoNode"; + RenameColumn(tableName, "parentID", "parentId", columns); + RenameColumn(tableName, "uniqueID", "uniqueId", columns); + + const string extraIndexName = "IX_umbracoNode_ParentId"; + if (IndexExists(extraIndexName)) + { + DeleteIndex(extraIndexName); + } + } + + private void MakeRelationTypeIndexUnique(Tuple[] indexes) + => MakeIndexUnique("umbracoRelationType", "IX_umbracoRelationType_alias", indexes); + + private void RemoveUserGroupDefault(ColumnInfo[] columns) + => RemoveDefaultConstraint("umbracoUserGroup", "hasAccessToAllLanguages", columns); + + private void MakeWebhookUrlNotNullable(ColumnInfo[] columns) + => MakeNvarCharColumnNotNullable("umbracoWebhook", "url", columns); + + private void MakeWebhookLogUrlNotNullable(ColumnInfo[] columns) + => MakeNvarCharColumnNotNullable("umbracoWebhookLog", "url", columns); +}