diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 1645b116e79..27cec1b08ff 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -530,12 +530,16 @@ protected override void Generate( throw new ArgumentException(SqlServerStrings.CannotProduceUnterminatedSQLWithComments(nameof(CreateTableOperation))); } + var needsExec = false; + + var tableCreationOptions = new List(); + if (operation[SqlServerAnnotationNames.IsTemporal] as bool? == true) { var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? model?.GetDefaultSchema(); - var needsExec = historyTableSchema == null; + needsExec = historyTableSchema == null; var subBuilder = needsExec ? new MigrationCommandListBuilder(Dependencies) : builder; @@ -557,6 +561,8 @@ protected override void Generate( subBuilder.AppendLine($"PERIOD FOR SYSTEM_TIME({start}, {end})"); } + subBuilder.Append(")"); + if (needsExec) { subBuilder @@ -575,12 +581,12 @@ protected override void Generate( if (needsExec) { historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!); - builder.Append($") WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].{historyTable}))')"); + tableCreationOptions.Add($"SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].{historyTable})"); } else { historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!, historyTableSchema); - builder.Append($") WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable}))"); + tableCreationOptions.Add($"SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable})"); } } else @@ -591,17 +597,46 @@ protected override void Generate( var memoryOptimized = IsMemoryOptimized(operation); if (memoryOptimized) { - builder.AppendLine(); - using (builder.Indent()) + tableCreationOptions.Add("MEMORY_OPTIMIZED = ON"); + } + + if (tableCreationOptions.Count > 0) + { + builder.Append(" WITH ("); + if (tableCreationOptions.Count == 1) { - builder.AppendLine("WITH"); + builder + .Append(tableCreationOptions[0]) + .Append(")"); + } + else + { + builder.AppendLine(); + using (builder.Indent()) { - builder.Append("(MEMORY_OPTIMIZED = ON)"); + for (var i = 0; i < tableCreationOptions.Count; i++) + { + builder.Append(tableCreationOptions[i]); + + if (i < tableCreationOptions.Count - 1) + { + builder.Append(","); + } + + builder.AppendLine(); + } } + + builder.Append(")"); } } + if (needsExec) + { + builder.Append("')"); + } + if (hasComments) { Check.DebugAssert(terminate, "terminate is false but there are comments"); @@ -2232,8 +2267,8 @@ private IReadOnlyList RewriteOperations( { var operations = new List(); - var versioningMap = new Dictionary<(string?, string?), (string, string?)>(); - var periodMap = new Dictionary<(string?, string?), (string, string)>(); + var versioningMap = new Dictionary<(string?, string?), (string, string?, bool)>(); + var periodMap = new Dictionary<(string?, string?), (string, string, bool)>(); var availableSchemas = new List(); foreach (var operation in migrationOperations) @@ -2255,6 +2290,8 @@ private IReadOnlyList RewriteOperations( schema = tableMigrationOperation.Schema; } + var suppressTransaction = table is not null && IsMemoryOptimized(operation, model, schema, table); + schema ??= model?.GetDefaultSchema(); var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string @@ -2277,7 +2314,7 @@ private IReadOnlyList RewriteOperations( break; case DropTableOperation: - DisableVersioning(table!, schema, historyTableName!, historyTableSchema); + DisableVersioning(table!, schema, historyTableName!, historyTableSchema, suppressTransaction); operations.Add(operation); versioningMap.Remove((table, schema)); @@ -2285,14 +2322,14 @@ private IReadOnlyList RewriteOperations( break; case RenameTableOperation renameTableOperation: - DisableVersioning(table!, schema, historyTableName!, historyTableSchema); + DisableVersioning(table!, schema, historyTableName!, historyTableSchema, suppressTransaction); operations.Add(operation); // since table was renamed, remove old entry and add new entry // marked as versioning disabled, so we enable it in the end for the new table versioningMap.Remove((table, schema)); versioningMap[(renameTableOperation.NewName, renameTableOperation.NewSchema)] = - (historyTableName!, historyTableSchema); + (historyTableName!, historyTableSchema, suppressTransaction); // same thing for disabled system period - remove one associated with old table and add one for the new table if (periodMap.TryGetValue((table, schema), out var result)) @@ -2308,9 +2345,9 @@ private IReadOnlyList RewriteOperations( if (!oldIsTemporal) { periodMap[(alterTableOperation.Name, alterTableOperation.Schema)] = - (periodStartColumnName!, periodEndColumnName!); + (periodStartColumnName!, periodEndColumnName!, suppressTransaction); versioningMap[(alterTableOperation.Name, alterTableOperation.Schema)] = - (historyTableName!, historyTableSchema); + (historyTableName!, historyTableSchema, suppressTransaction); } else { @@ -2343,7 +2380,7 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema if (versioningMap.ContainsKey((alterTableOperation.Name, alterTableOperation.Schema))) { versioningMap[(alterTableOperation.Name, alterTableOperation.Schema)] = - (historyTableName!, historyTableSchema); + (historyTableName!, historyTableSchema, suppressTransaction); } } } @@ -2376,19 +2413,19 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema case DropPrimaryKeyOperation: case AddPrimaryKeyOperation: - DisableVersioning(table!, schema, historyTableName!, historyTableSchema); + DisableVersioning(table!, schema, historyTableName!, historyTableSchema, suppressTransaction); operations.Add(operation); break; case DropColumnOperation dropColumnOperation: - DisableVersioning(table!, schema, historyTableName!, historyTableSchema); + DisableVersioning(table!, schema, historyTableName!, historyTableSchema, suppressTransaction); if (dropColumnOperation.Name == periodStartColumnName || dropColumnOperation.Name == periodEndColumnName) { // period columns can be null here - it doesn't really matter since we are never enabling the period back - // if we remove the period columns, it means we will be dropping the table also or at least convert it back to regular - // which will clear the entry in the periodMap for this table - DisablePeriod(table!, schema, periodStartColumnName!, periodEndColumnName!); + // if we remove the period columns, it means we will be dropping the table also or at least convert it back to + // regular which will clear the entry in the periodMap for this table + DisablePeriod(table!, schema, periodStartColumnName!, periodEndColumnName!, suppressTransaction); } operations.Add(operation); @@ -2397,16 +2434,17 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema case AddColumnOperation addColumnOperation: // when adding a period column, we need to add it as a normal column first, and only later enable period - // removing the period information now, so that when we generate SQL that adds the column we won't be making them auto generated as period - // it won't work, unless period is enabled - // but we can't enable period without adding the columns first - chicken and egg + // removing the period information now, so that when we generate SQL that adds the column we won't be making them + // auto generated as period it won't work, unless period is enabled but we can't enable period without adding the + // columns first - chicken and egg if (addColumnOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true) { addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); - // model differ adds default value, but for period end we need to replace it with the correct one - DateTime.MaxValue + // model differ adds default value, but for period end we need to replace it with the correct one - + // DateTime.MaxValue if (addColumnOperation.Name == periodEndColumnName) { addColumnOperation.DefaultValue = DateTime.MaxValue; @@ -2437,9 +2475,13 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; var periodEndColumnName = alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + var suppressTransaction = IsMemoryOptimized(operation, model, alterTableOperation.Schema, alterTableOperation.Name); - DisableVersioning(alterTableOperation.Name, alterTableOperation.Schema, historyTableName!, historyTableSchema); - DisablePeriod(alterTableOperation.Name, alterTableOperation.Schema, periodStartColumnName!, periodEndColumnName!); + DisableVersioning( + alterTableOperation.Name, alterTableOperation.Schema, historyTableName!, historyTableSchema, suppressTransaction); + DisablePeriod( + alterTableOperation.Name, alterTableOperation.Schema, periodStartColumnName!, periodEndColumnName!, + suppressTransaction); if (historyTableName != null) { @@ -2470,25 +2512,23 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema } } - foreach (var (key, value) in periodMap) + foreach (var ((table, schema), (periodStartColumnName, periodEndColumnName, suppressTransaction)) in periodMap) { - EnablePeriod(key.Item1!, key.Item2, value.Item1, value.Item2); + EnablePeriod(table!, schema, periodStartColumnName, periodEndColumnName, suppressTransaction); } - foreach (var (key, value) in versioningMap) + foreach (var ((table, schema), (historyTableName, historyTableSchema, suppressTransaction)) in versioningMap) { - EnableVersioning( - key.Item1!, key.Item2, value.Item1, - value.Item2); + EnableVersioning(table!, schema, historyTableName, historyTableSchema, suppressTransaction); } return operations; - void DisableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema) + void DisableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema, bool suppressTransaction) { if (!versioningMap.TryGetValue((table, schema), out _)) { - versioningMap[(table, schema)] = (historyTableName, historyTableSchema); + versioningMap[(table, schema)] = (historyTableName, historyTableSchema, suppressTransaction); operations.Add( new SqlOperation @@ -2497,12 +2537,13 @@ void DisableVersioning(string table, string? schema, string historyTableName, st .Append("ALTER TABLE ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) .AppendLine(" SET (SYSTEM_VERSIONING = OFF)") - .ToString() + .ToString(), + SuppressTransaction = suppressTransaction }); } } - void EnableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema) + void EnableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema, bool suppressTransaction) { var stringBuilder = new StringBuilder(); @@ -2532,14 +2573,14 @@ void EnableVersioning(string table, string? schema, string historyTableName, str } operations.Add( - new SqlOperation { Sql = stringBuilder.ToString() }); + new SqlOperation { Sql = stringBuilder.ToString(), SuppressTransaction = suppressTransaction }); } - void DisablePeriod(string table, string? schema, string periodStartColumnName, string periodEndColumnName) + void DisablePeriod(string table, string? schema, string periodStartColumnName, string periodEndColumnName, bool suppressTransaction) { if (!periodMap.TryGetValue((table, schema), out _)) { - periodMap[(table, schema)] = (periodStartColumnName, periodEndColumnName); + periodMap[(table, schema)] = (periodStartColumnName, periodEndColumnName, suppressTransaction); operations.Add( new SqlOperation @@ -2548,12 +2589,13 @@ void DisablePeriod(string table, string? schema, string periodStartColumnName, s .Append("ALTER TABLE ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) .AppendLine(" DROP PERIOD FOR SYSTEM_TIME") - .ToString() + .ToString(), + SuppressTransaction = suppressTransaction }); } } - void EnablePeriod(string table, string? schema, string periodStartColumnName, string periodEndColumnName) + void EnablePeriod(string table, string? schema, string periodStartColumnName, string periodEndColumnName, bool suppressTransaction) { var addPeriodSql = new StringBuilder() .Append("ALTER TABLE ") @@ -2575,7 +2617,7 @@ void EnablePeriod(string table, string? schema, string periodStartColumnName, st } operations.Add( - new SqlOperation { Sql = addPeriodSql }); + new SqlOperation { Sql = addPeriodSql, SuppressTransaction = suppressTransaction }); operations.Add( new SqlOperation @@ -2586,7 +2628,8 @@ void EnablePeriod(string table, string? schema, string periodStartColumnName, st .Append(" ALTER COLUMN ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName)) .Append(" ADD HIDDEN") - .ToString() + .ToString(), + SuppressTransaction = suppressTransaction }); operations.Add( @@ -2598,7 +2641,8 @@ void EnablePeriod(string table, string? schema, string periodStartColumnName, st .Append(" ALTER COLUMN ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName)) .Append(" ADD HIDDEN") - .ToString() + .ToString(), + SuppressTransaction = suppressTransaction }); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 36394b3efb6..c083f54412c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -6,6 +6,10 @@ using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal; +// ReSharper disable StringLiteralTypo +// ReSharper disable UnusedParameter.Local +// ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local + #nullable enable namespace Microsoft.EntityFrameworkCore.Migrations; @@ -162,6 +166,136 @@ [IdentityColumn] smallint NOT NULL IDENTITY );"); } + [ConditionalFact] + [SqlServerCondition(SqlServerCondition.SupportsMemoryOptimized)] + public virtual async Task Create_memory_optimized_table() + { + await Test( + _ => { }, + builder => builder.UseIdentityColumns().Entity("People", b => + { + b.IsMemoryOptimized(); + b.Property("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.True((bool)table[SqlServerAnnotationNames.MemoryOptimized]!); + }); + + AssertSql( + @"IF SERVERPROPERTY('IsXTPSupported') = 1 AND SERVERPROPERTY('EngineEdition') <> 5 + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM [sys].[filegroups] [FG] JOIN [sys].[database_files] [F] ON [FG].[data_space_id] = [F].[data_space_id] WHERE [FG].[type] = N'FX' AND [F].[type] = 2) + BEGIN + ALTER DATABASE CURRENT SET AUTO_CLOSE OFF; + DECLARE @db_name nvarchar(max) = DB_NAME(); + DECLARE @fg_name nvarchar(max); + SELECT TOP(1) @fg_name = [name] FROM [sys].[filegroups] WHERE [type] = N'FX'; + + IF @fg_name IS NULL + BEGIN + SET @fg_name = @db_name + N'_MODFG'; + EXEC(N'ALTER DATABASE CURRENT ADD FILEGROUP [' + @fg_name + '] CONTAINS MEMORY_OPTIMIZED_DATA;'); + END + + DECLARE @path nvarchar(max); + SELECT TOP(1) @path = [physical_name] FROM [sys].[database_files] WHERE charindex('\', [physical_name]) > 0 ORDER BY [file_id]; + IF (@path IS NULL) + SET @path = '\' + @db_name; + + DECLARE @filename nvarchar(max) = right(@path, charindex('\', reverse(@path)) - 1); + SET @filename = REPLACE(left(@filename, len(@filename) - charindex('.', reverse(@filename))), '''', '''''') + N'_MOD'; + DECLARE @new_path nvarchar(max) = REPLACE(CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(max)), '''', '''''') + @filename; + + EXEC(N' + ALTER DATABASE CURRENT + ADD FILE (NAME=''' + @filename + ''', filename=''' + @new_path + ''') + TO FILEGROUP [' + @fg_name + '];') + END + END + +IF SERVERPROPERTY('IsXTPSupported') = 1 +EXEC(N' + ALTER DATABASE CURRENT + SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT ON;')", + // + @"CREATE TABLE [People] ( + [Id] int NOT NULL IDENTITY, + CONSTRAINT [PK_People] PRIMARY KEY NONCLUSTERED ([Id]) +) WITH (MEMORY_OPTIMIZED = ON);"); + } + + [ConditionalFact] + [SqlServerCondition(SqlServerCondition.SupportsMemoryOptimized)] + public virtual async Task Create_memory_optimized_temporal_table() + { + await Test( + _ => { }, + builder => builder.UseIdentityColumns().Entity("People", b => + { + b.ToTable("Customers", tb => tb.IsTemporal()); + b.IsMemoryOptimized(); + b.Property("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.True((bool)table[SqlServerAnnotationNames.MemoryOptimized]!); + }); + + AssertSql( + @"IF SERVERPROPERTY('IsXTPSupported') = 1 AND SERVERPROPERTY('EngineEdition') <> 5 + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM [sys].[filegroups] [FG] JOIN [sys].[database_files] [F] ON [FG].[data_space_id] = [F].[data_space_id] WHERE [FG].[type] = N'FX' AND [F].[type] = 2) + BEGIN + ALTER DATABASE CURRENT SET AUTO_CLOSE OFF; + DECLARE @db_name nvarchar(max) = DB_NAME(); + DECLARE @fg_name nvarchar(max); + SELECT TOP(1) @fg_name = [name] FROM [sys].[filegroups] WHERE [type] = N'FX'; + + IF @fg_name IS NULL + BEGIN + SET @fg_name = @db_name + N'_MODFG'; + EXEC(N'ALTER DATABASE CURRENT ADD FILEGROUP [' + @fg_name + '] CONTAINS MEMORY_OPTIMIZED_DATA;'); + END + + DECLARE @path nvarchar(max); + SELECT TOP(1) @path = [physical_name] FROM [sys].[database_files] WHERE charindex('\', [physical_name]) > 0 ORDER BY [file_id]; + IF (@path IS NULL) + SET @path = '\' + @db_name; + + DECLARE @filename nvarchar(max) = right(@path, charindex('\', reverse(@path)) - 1); + SET @filename = REPLACE(left(@filename, len(@filename) - charindex('.', reverse(@filename))), '''', '''''') + N'_MOD'; + DECLARE @new_path nvarchar(max) = REPLACE(CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(max)), '''', '''''') + @filename; + + EXEC(N' + ALTER DATABASE CURRENT + ADD FILE (NAME=''' + @filename + ''', filename=''' + @new_path + ''') + TO FILEGROUP [' + @fg_name + '];') + END + END + +IF SERVERPROPERTY('IsXTPSupported') = 1 +EXEC(N' + ALTER DATABASE CURRENT + SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT ON;')", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY NONCLUSTERED ([Id]), + PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd]) +) WITH ( + SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomersHistory]), + MEMORY_OPTIMIZED = ON +)');"); + } + public override async Task Drop_table() { await base.Drop_table();