diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 58dfacb9ae..bf6371476f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_3_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; @@ -297,5 +298,8 @@ public class UmbracoPlan : MigrationPlan // To 11.0.0/10.4.0 To("{56833770-3B7E-4FD5-A3B6-3416A26A7A3F}"); + + // To 12.0.0 + To("{888A0D5D-51E4-4C7E-AA0A-01306523C7FB}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs new file mode 100644 index 0000000000..4c3a0c889a --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs @@ -0,0 +1,94 @@ +using System.Linq.Expressions; +using System.Text; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_0_0; + +public class UseNvarcharInsteadOfNText : MigrationBase +{ + public UseNvarcharInsteadOfNText(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // We don't need to run this migration for SQLite, since ntext is not a thing there, text is just text. + if (DatabaseType == DatabaseType.SQLite) + { + return; + } + + MigrateNtextColumn("data", Constants.DatabaseSchema.Tables.NodeData, x => x.Data); + MigrateNtextColumn("jsonInstruction", Constants.DatabaseSchema.Tables.CacheInstruction, x => x.Instructions, false); + MigrateNtextColumn("config", Constants.DatabaseSchema.Tables.DataType, x => x.Configuration); + MigrateNtextColumn("userData", Constants.DatabaseSchema.Tables.ExternalLogin, x => x.UserData); + MigrateNtextColumn("tourData", Constants.DatabaseSchema.Tables.User, x => x.TourData); + MigrateNtextColumn("textValue", Constants.DatabaseSchema.Tables.PropertyData, x => x.TextValue); + } + + private void MigrateNtextColumn(string columnName, string tableName, Expression> fieldSelector, bool nullable = true) + { + var columnType = ColumnType(tableName, columnName); + if (columnType is null || columnType.Equals("ntext", StringComparison.InvariantCultureIgnoreCase) is false) + { + return; + } + + var oldColumnName = $"Old{columnName}"; + + // Rename the column so we can create the new one and copy over the data. + Rename + .Column(columnName) + .OnTable(tableName) + .To(oldColumnName) + .Do(); + + // Create new column with the correct type + // This is pretty ugly, but we have to do ti this way because the CacheInstruction.Instruction column doesn't support nullable. + // So we have to populate with some temporary placeholder value before we copy over the actual data. + ICreateColumnOptionBuilder builder = Create + .Column(columnName) + .OnTable(tableName) + .AsCustom("nvarchar(max)"); + + if (nullable is false) + { + builder + .NotNullable() + .WithDefaultValue("Placeholder"); + } + else + { + builder.Nullable(); + } + + builder.Do(); + + // Copy over data NPOCO doesn't support this for some reason, so we'll have to do it like so + // While we're add it we'll also set all the old values to be NULL since it's recommended here: + // https://learn.microsoft.com/en-us/sql/t-sql/data-types/ntext-text-and-image-transact-sql?view=sql-server-ver16#remarks + StringBuilder queryBuilder = new StringBuilder() + .AppendLine($"UPDATE {tableName}") + .AppendLine("SET") + .Append($"\t{SqlSyntax.GetFieldNameForUpdate(fieldSelector)} = {SqlSyntax.GetQuotedTableName(tableName)}.{SqlSyntax.GetQuotedColumnName(oldColumnName)}"); + + if (nullable) + { + queryBuilder.AppendLine($"\n,\t{SqlSyntax.GetQuotedColumnName(oldColumnName)} = NULL"); + } + + Sql copyDataQuery = Database.SqlContext.Sql(queryBuilder.ToString()); + Database.Execute(copyDataQuery); + + // Delete old column + Delete + .Column(oldColumnName) + .FromTable(tableName) + .Do(); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs index b5e57a3f3f..85d91a7412 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs @@ -20,6 +20,7 @@ public struct SpecialDbType : IEquatable public SpecialDbType(SpecialDbTypes specialDbTypes) => _dbType = specialDbTypes.ToString(); + [Obsolete("Use NVARCHARMAX instead")] public static SpecialDbType NTEXT { get; } = new(SpecialDbTypes.NTEXT); public static SpecialDbType NCHAR { get; } = new(SpecialDbTypes.NCHAR); diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs index 98b0558a2e..4cad1f8e2b 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs @@ -5,6 +5,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; /// public enum SpecialDbTypes { + [Obsolete("Use NVARCHARMAX instead")] NTEXT, NCHAR, NVARCHARMAX, diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs index 0e73112f76..bc4babef95 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs @@ -19,7 +19,7 @@ public class CacheInstructionDto public DateTime UtcStamp { get; set; } [Column("jsonInstruction")] - [SpecialDbType(SpecialDbTypes.NTEXT)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] [NullSetting(NullSetting = NullSettings.NotNull)] public string Instructions { get; set; } = null!; diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs index 7f64054d14..6fa45d9cce 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs @@ -25,7 +25,7 @@ public class ContentNuDto /// Pretty much anything that would require a 1:M lookup is serialized here /// [Column("data")] - [SpecialDbType(SpecialDbTypes.NTEXT)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] [NullSetting(NullSetting = NullSettings.Null)] public string? Data { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs index f3d376b078..5b10e70fc1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs @@ -22,7 +22,7 @@ public class DataTypeDto public string DbType { get; set; } = null!; [Column("config")] - [SpecialDbType(SpecialDbTypes.NTEXT)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] [NullSetting(NullSetting = NullSettings.Null)] public string? Configuration { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index 017ab3c6e4..05c94ed3db 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -51,6 +51,6 @@ internal class ExternalLoginDto /// [Column("userData")] [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] public string? UserData { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs index f0c57e0d18..2158f6c586 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs @@ -66,7 +66,7 @@ internal class PropertyDataDto [Column("textValue")] [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] public string? TextValue { get; set; } [ResultColumn] diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index 16db4a10ad..21c6afde38 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -111,7 +111,7 @@ public class UserDto /// [Column("tourData")] [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] public string? TourData { get; set; } [ResultColumn]