diff --git a/src/EntityFramework.Core/Metadata/Builders/EntityTypeBuilder`.cs b/src/EntityFramework.Core/Metadata/Builders/EntityTypeBuilder`.cs index 6eae635d244..4c28825bbc3 100644 --- a/src/EntityFramework.Core/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EntityFramework.Core/Metadata/Builders/EntityTypeBuilder`.cs @@ -65,7 +65,7 @@ protected override EntityTypeBuilder New(InternalEntityTypeBuilder builder) /// /// The name of the base type. /// The same builder instance so that multiple configuration calls can be chained. - public new virtual EntityTypeBuilder HasBaseType([NotNull] string name) + public new virtual EntityTypeBuilder HasBaseType([CanBeNull] string name) => (EntityTypeBuilder)base.HasBaseType(name); /// @@ -73,7 +73,7 @@ protected override EntityTypeBuilder New(InternalEntityTypeBuilder builder) /// /// The base type. /// The same builder instance so that multiple configuration calls can be chained. - public new virtual EntityTypeBuilder HasBaseType([NotNull] Type entityType) + public new virtual EntityTypeBuilder HasBaseType([CanBeNull] Type entityType) => (EntityTypeBuilder)base.HasBaseType(entityType); /// diff --git a/src/EntityFramework.MicrosoftSqlServer/EntityFramework.MicrosoftSqlServer.csproj b/src/EntityFramework.MicrosoftSqlServer/EntityFramework.MicrosoftSqlServer.csproj index e62329b0c96..d70102fa226 100644 --- a/src/EntityFramework.MicrosoftSqlServer/EntityFramework.MicrosoftSqlServer.csproj +++ b/src/EntityFramework.MicrosoftSqlServer/EntityFramework.MicrosoftSqlServer.csproj @@ -78,6 +78,7 @@ + diff --git a/src/EntityFramework.MicrosoftSqlServer/Storage/Internal/SqlServerSqlGenerator.cs b/src/EntityFramework.MicrosoftSqlServer/Storage/Internal/SqlServerSqlGenerator.cs index b218ca8c770..4dfdc4b0971 100644 --- a/src/EntityFramework.MicrosoftSqlServer/Storage/Internal/SqlServerSqlGenerator.cs +++ b/src/EntityFramework.MicrosoftSqlServer/Storage/Internal/SqlServerSqlGenerator.cs @@ -1,10 +1,9 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Globalization; using System.Text; -using JetBrains.Annotations; using Microsoft.Data.Entity.Utilities; namespace Microsoft.Data.Entity.Storage.Internal @@ -13,13 +12,13 @@ public class SqlServerSqlGenerator : RelationalSqlGenerator { public override string BatchSeparator => "GO" + Environment.NewLine + Environment.NewLine; - public override string EscapeIdentifier([NotNull] string identifier) + public override string EscapeIdentifier(string identifier) => Check.NotEmpty(identifier, nameof(identifier)).Replace("]", "]]"); - public override string DelimitIdentifier([NotNull] string identifier) + public override string DelimitIdentifier(string identifier) => $"[{EscapeIdentifier(Check.NotEmpty(identifier, nameof(identifier)))}]"; - protected override string GenerateLiteralValue([NotNull] byte[] value) + protected override string GenerateLiteralValue(byte[] value) { Check.NotNull(value, nameof(value)); diff --git a/src/EntityFramework.MicrosoftSqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs index bd2b002a73b..c2adbdefa99 100644 --- a/src/EntityFramework.MicrosoftSqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs +++ b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/ISqlServerUpdateSqlGenerator.cs @@ -9,8 +9,9 @@ namespace Microsoft.Data.Entity.Update.Internal { public interface ISqlServerUpdateSqlGenerator : IUpdateSqlGenerator { - SqlServerUpdateSqlGenerator.ResultsGrouping AppendBulkInsertOperation( + ResultsGrouping AppendBulkInsertOperation( [NotNull] StringBuilder commandStringBuilder, - [NotNull] IReadOnlyList modificationCommands); + [NotNull] IReadOnlyList modificationCommands, + int commandPosition); } } diff --git a/src/EntityFramework.MicrosoftSqlServer/Update/Internal/ResultsGrouping.cs b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/ResultsGrouping.cs new file mode 100644 index 00000000000..b4010832e21 --- /dev/null +++ b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/ResultsGrouping.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Data.Entity.Update.Internal +{ + public enum ResultsGrouping + { + OneResultSet, + OneCommandPerResultSet + } +} diff --git a/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerModificationCommandBatch.cs b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerModificationCommandBatch.cs index 8b2a7de66d0..30a82cee109 100644 --- a/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerModificationCommandBatch.cs +++ b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerModificationCommandBatch.cs @@ -25,6 +25,7 @@ public class SqlServerModificationCommandBatch : AffectedCountModificationComman public SqlServerModificationCommandBatch( [NotNull] IRelationalCommandBuilderFactory commandBuilderFactory, [NotNull] ISqlGenerator sqlGenerator, + // ReSharper disable once SuggestBaseTypeForParameter [NotNull] ISqlServerUpdateSqlGenerator updateSqlGenerator, [NotNull] IRelationalValueBufferFactoryFactory valueBufferFactoryFactory, [CanBeNull] int? maxBatchSize) @@ -43,6 +44,8 @@ public SqlServerModificationCommandBatch( _maxBatchSize = Math.Min(maxBatchSize ?? int.MaxValue, MaxRowCount); } + protected new virtual ISqlServerUpdateSqlGenerator UpdateSqlGenerator => (ISqlServerUpdateSqlGenerator)base.UpdateSqlGenerator; + protected override bool CanAddCommand(ModificationCommand modificationCommand) { if (_maxBatchSize <= ModificationCommands.Count) @@ -115,10 +118,10 @@ private string GetBulkInsertCommandText(int lastIndex) } var stringBuilder = new StringBuilder(); - var grouping = ((ISqlServerUpdateSqlGenerator)UpdateSqlGenerator).AppendBulkInsertOperation(stringBuilder, _bulkInsertCommands); + var grouping = UpdateSqlGenerator.AppendBulkInsertOperation(stringBuilder, _bulkInsertCommands, lastIndex); for (var i = lastIndex - _bulkInsertCommands.Count; i < lastIndex; i++) { - ResultSetEnds[i] = grouping == SqlServerUpdateSqlGenerator.ResultsGrouping.OneCommandPerResultSet; + ResultSetEnds[i] = grouping == ResultsGrouping.OneCommandPerResultSet; } ResultSetEnds[lastIndex - 1] = true; diff --git a/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs index 1aee3374632..4f0844bc8c1 100644 --- a/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs +++ b/src/EntityFramework.MicrosoftSqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs @@ -1,10 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Linq; using System.Text; using JetBrains.Annotations; +using Microsoft.Data.Entity.Metadata; using Microsoft.Data.Entity.Storage; using Microsoft.Data.Entity.Utilities; @@ -12,23 +14,26 @@ namespace Microsoft.Data.Entity.Update.Internal { public class SqlServerUpdateSqlGenerator : UpdateSqlGenerator, ISqlServerUpdateSqlGenerator { - public SqlServerUpdateSqlGenerator([NotNull] ISqlGenerator sqlGenerator) + private readonly IRelationalTypeMapper _typeMapper; + + public SqlServerUpdateSqlGenerator([NotNull] ISqlGenerator sqlGenerator, + [NotNull] IRelationalTypeMapper typeMapper) : base(sqlGenerator) { + _typeMapper = typeMapper; } - public override void AppendInsertOperation( - StringBuilder commandStringBuilder, - ModificationCommand command) + public override void AppendInsertOperation(StringBuilder commandStringBuilder, ModificationCommand command, int commandPosition) { Check.NotNull(command, nameof(command)); - AppendBulkInsertOperation(commandStringBuilder, new[] { command }); + AppendBulkInsertOperation(commandStringBuilder, new[] { command }, commandPosition); } public virtual ResultsGrouping AppendBulkInsertOperation( StringBuilder commandStringBuilder, - IReadOnlyList modificationCommands) + IReadOnlyList modificationCommands, + int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(modificationCommands, nameof(modificationCommands)); @@ -51,10 +56,15 @@ public virtual ResultsGrouping AppendBulkInsertOperation( var writeOperations = operations.Where(o => o.IsWrite).ToArray(); var readOperations = operations.Where(o => o.IsRead).ToArray(); + if (readOperations.Length > 0) + { + AppendDeclareGeneratedTable(commandStringBuilder, readOperations, commandPosition); + } + AppendInsertCommandHeader(commandStringBuilder, name, schema, writeOperations); if (readOperations.Length > 0) { - AppendOutputClause(commandStringBuilder, readOperations); + AppendOutputClause(commandStringBuilder, readOperations, commandPosition); } AppendValuesHeader(commandStringBuilder, writeOperations); AppendValues(commandStringBuilder, writeOperations); @@ -65,9 +75,13 @@ public virtual ResultsGrouping AppendBulkInsertOperation( } commandStringBuilder.Append(SqlGenerator.BatchCommandSeparator).AppendLine(); - if (readOperations.Length == 0) + if (readOperations.Length > 0) + { + AppendSelectGeneratedCommand(commandStringBuilder, readOperations, commandPosition); + } + else { - AppendSelectAffectedCountCommand(commandStringBuilder, name, schema); + AppendSelectAffectedCountCommand(commandStringBuilder, name, schema, commandPosition); } } @@ -76,9 +90,7 @@ public virtual ResultsGrouping AppendBulkInsertOperation( : ResultsGrouping.OneResultSet; } - public override void AppendUpdateOperation( - StringBuilder commandStringBuilder, - ModificationCommand command) + public override void AppendUpdateOperation(StringBuilder commandStringBuilder, ModificationCommand command, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(command, nameof(command)); @@ -91,30 +103,80 @@ public override void AppendUpdateOperation( var conditionOperations = operations.Where(o => o.IsCondition).ToArray(); var readOperations = operations.Where(o => o.IsRead).ToArray(); + if (readOperations.Length > 0) + { + AppendDeclareGeneratedTable(commandStringBuilder, readOperations, commandPosition); + } AppendUpdateCommandHeader(commandStringBuilder, name, schema, writeOperations); if (readOperations.Length > 0) { - AppendOutputClause(commandStringBuilder, readOperations); + AppendOutputClause(commandStringBuilder, readOperations, commandPosition); } AppendWhereClause(commandStringBuilder, conditionOperations); commandStringBuilder.Append(SqlGenerator.BatchCommandSeparator).AppendLine(); - if (readOperations.Length == 0) + if (readOperations.Length > 0) { - AppendSelectAffectedCountCommand(commandStringBuilder, name, schema); + AppendSelectGeneratedCommand(commandStringBuilder, readOperations, commandPosition); + } + else + { + AppendSelectAffectedCountCommand(commandStringBuilder, name, schema, commandPosition); } } + private void AppendDeclareGeneratedTable(StringBuilder commandStringBuilder, ColumnModification[] readOperations, int commandPosition) + { + commandStringBuilder + .Append($"DECLARE @generated{commandPosition} TABLE (") + .AppendJoin(readOperations.Select(c => + SqlGenerator.DelimitIdentifier(c.ColumnName) + " " + GetTypeNameForCopy(c.Property))) + .Append(")") + .Append(SqlGenerator.BatchCommandSeparator) + .AppendLine(); + } + + private string GetTypeNameForCopy(IProperty property) + { + var mapping = _typeMapper.GetMapping(property); + var typeName = mapping.DefaultTypeName; + if (property.IsConcurrencyToken + && (typeName.Equals("rowversion", StringComparison.OrdinalIgnoreCase) + || typeName.Equals("timestamp", StringComparison.OrdinalIgnoreCase))) + { + return property.IsNullable ? "varbinary(8)" : "binary(8)"; + } + + return typeName; + } + // ReSharper disable once ParameterTypeCanBeEnumerable.Local private void AppendOutputClause( StringBuilder commandStringBuilder, - IReadOnlyList operations) - => commandStringBuilder + IReadOnlyList operations, + int commandPosition) + { + commandStringBuilder .AppendLine() .Append("OUTPUT ") .AppendJoin(operations.Select(c => "INSERTED." + SqlGenerator.DelimitIdentifier(c.ColumnName))); - protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema) + commandStringBuilder + .AppendLine() + .Append($"INTO @generated{commandPosition}"); + } + + private void AppendSelectGeneratedCommand(StringBuilder commandStringBuilder, ColumnModification[] readOperations, int commandPosition) + { + commandStringBuilder + .Append("SELECT ") + .AppendJoin(readOperations.Select(c => SqlGenerator.DelimitIdentifier(c.ColumnName))) + .Append($" FROM @generated{commandPosition}") + .Append(SqlGenerator.BatchCommandSeparator) + .AppendLine(); + } + + protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema, int commandPosition) => Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)) .Append("SELECT @@ROWCOUNT") .Append(SqlGenerator.BatchCommandSeparator).AppendLine(); @@ -133,11 +195,5 @@ protected override void AppendIdentityWhereCondition(StringBuilder commandString protected override void AppendRowsAffectedWhereCondition(StringBuilder commandStringBuilder, int expectedRowsAffected) => Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)) .Append("@@ROWCOUNT = " + expectedRowsAffected); - - public enum ResultsGrouping - { - OneResultSet, - OneCommandPerResultSet - } } } diff --git a/src/EntityFramework.Relational/Update/IUpdateSqlGenerator.cs b/src/EntityFramework.Relational/Update/IUpdateSqlGenerator.cs index 2aab172490a..eb42de9427e 100644 --- a/src/EntityFramework.Relational/Update/IUpdateSqlGenerator.cs +++ b/src/EntityFramework.Relational/Update/IUpdateSqlGenerator.cs @@ -10,8 +10,14 @@ public interface IUpdateSqlGenerator { string GenerateNextSequenceValueOperation([NotNull] string name, [CanBeNull] string schema); void AppendBatchHeader([NotNull] StringBuilder commandStringBuilder); - void AppendDeleteOperation([NotNull] StringBuilder commandStringBuilder, [NotNull] ModificationCommand command); - void AppendInsertOperation([NotNull] StringBuilder commandStringBuilder, [NotNull] ModificationCommand command); - void AppendUpdateOperation([NotNull] StringBuilder commandStringBuilder, [NotNull] ModificationCommand command); + + void AppendDeleteOperation( + [NotNull] StringBuilder commandStringBuilder, [NotNull] ModificationCommand command, int commandPosition); + + void AppendInsertOperation( + [NotNull] StringBuilder commandStringBuilder, [NotNull] ModificationCommand command, int commandPosition); + + void AppendUpdateOperation( + [NotNull] StringBuilder commandStringBuilder, [NotNull] ModificationCommand command, int commandPosition); } } diff --git a/src/EntityFramework.Relational/Update/ReaderModificationCommandBatch.cs b/src/EntityFramework.Relational/Update/ReaderModificationCommandBatch.cs index abdb0bf8072..94976c09e1c 100644 --- a/src/EntityFramework.Relational/Update/ReaderModificationCommandBatch.cs +++ b/src/EntityFramework.Relational/Update/ReaderModificationCommandBatch.cs @@ -105,13 +105,13 @@ protected virtual void UpdateCachedCommandText(int commandPosition) switch (newModificationCommand.EntityState) { case EntityState.Added: - UpdateSqlGenerator.AppendInsertOperation(CachedCommandText, newModificationCommand); + UpdateSqlGenerator.AppendInsertOperation(CachedCommandText, newModificationCommand, commandPosition); break; case EntityState.Modified: - UpdateSqlGenerator.AppendUpdateOperation(CachedCommandText, newModificationCommand); + UpdateSqlGenerator.AppendUpdateOperation(CachedCommandText, newModificationCommand, commandPosition); break; case EntityState.Deleted: - UpdateSqlGenerator.AppendDeleteOperation(CachedCommandText, newModificationCommand); + UpdateSqlGenerator.AppendDeleteOperation(CachedCommandText, newModificationCommand, commandPosition); break; } diff --git a/src/EntityFramework.Relational/Update/UpdateSqlGenerator.cs b/src/EntityFramework.Relational/Update/UpdateSqlGenerator.cs index e8666899f41..18d89c2fc78 100644 --- a/src/EntityFramework.Relational/Update/UpdateSqlGenerator.cs +++ b/src/EntityFramework.Relational/Update/UpdateSqlGenerator.cs @@ -21,7 +21,7 @@ protected UpdateSqlGenerator([NotNull] ISqlGenerator sqlGenerator) protected virtual ISqlGenerator SqlGenerator { get; } - public virtual void AppendInsertOperation(StringBuilder commandStringBuilder, ModificationCommand command) + public virtual void AppendInsertOperation(StringBuilder commandStringBuilder, ModificationCommand command, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(command, nameof(command)); @@ -39,15 +39,15 @@ public virtual void AppendInsertOperation(StringBuilder commandStringBuilder, Mo { var keyOperations = operations.Where(o => o.IsKey).ToArray(); - AppendSelectAffectedCommand(commandStringBuilder, name, schema, readOperations, keyOperations); + AppendSelectAffectedCommand(commandStringBuilder, name, schema, readOperations, keyOperations, commandPosition); } else { - AppendSelectAffectedCountCommand(commandStringBuilder, name, schema); + AppendSelectAffectedCountCommand(commandStringBuilder, name, schema, commandPosition); } } - public virtual void AppendUpdateOperation(StringBuilder commandStringBuilder, ModificationCommand command) + public virtual void AppendUpdateOperation(StringBuilder commandStringBuilder, ModificationCommand command, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(command, nameof(command)); @@ -66,15 +66,15 @@ public virtual void AppendUpdateOperation(StringBuilder commandStringBuilder, Mo { var keyOperations = operations.Where(o => o.IsKey).ToArray(); - AppendSelectAffectedCommand(commandStringBuilder, name, schema, readOperations, keyOperations); + AppendSelectAffectedCommand(commandStringBuilder, name, schema, readOperations, keyOperations, commandPosition); } else { - AppendSelectAffectedCountCommand(commandStringBuilder, name, schema); + AppendSelectAffectedCountCommand(commandStringBuilder, name, schema, commandPosition); } } - public virtual void AppendDeleteOperation(StringBuilder commandStringBuilder, ModificationCommand command) + public virtual void AppendDeleteOperation(StringBuilder commandStringBuilder, ModificationCommand command, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotNull(command, nameof(command)); @@ -85,7 +85,7 @@ public virtual void AppendDeleteOperation(StringBuilder commandStringBuilder, Mo AppendDeleteCommand(commandStringBuilder, name, schema, conditionOperations); - AppendSelectAffectedCountCommand(commandStringBuilder, name, schema); + AppendSelectAffectedCountCommand(commandStringBuilder, name, schema, commandPosition); } protected virtual void AppendInsertCommand( @@ -139,7 +139,8 @@ protected virtual void AppendDeleteCommand( protected virtual void AppendSelectAffectedCountCommand( [NotNull] StringBuilder commandStringBuilder, [NotNull] string name, - [CanBeNull] string schema) + [CanBeNull] string schema, + int commandPosition) { } @@ -148,7 +149,8 @@ protected virtual void AppendSelectAffectedCommand( [NotNull] string name, [CanBeNull] string schema, [NotNull] IReadOnlyList readOperations, - [NotNull] IReadOnlyList conditionOperations) + [NotNull] IReadOnlyList conditionOperations, + int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); diff --git a/src/EntityFramework.Sqlite/Update/Internal/SqliteUpdateSqlGenerator.cs b/src/EntityFramework.Sqlite/Update/Internal/SqliteUpdateSqlGenerator.cs index d0fda00164b..ffb4a49029f 100644 --- a/src/EntityFramework.Sqlite/Update/Internal/SqliteUpdateSqlGenerator.cs +++ b/src/EntityFramework.Sqlite/Update/Internal/SqliteUpdateSqlGenerator.cs @@ -28,7 +28,7 @@ protected override void AppendIdentityWhereCondition(StringBuilder commandString .Append("last_insert_rowid()"); } - protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema) + protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema, int commandPosition) { Check.NotNull(commandStringBuilder, nameof(commandStringBuilder)); Check.NotEmpty(name, nameof(name)); diff --git a/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/BatchingTest.cs b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/BatchingTest.cs index 38ad2405ede..38ce20b999b 100644 --- a/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/BatchingTest.cs +++ b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/BatchingTest.cs @@ -5,7 +5,6 @@ using System.Linq; using Microsoft.Data.Entity.FunctionalTests; using Microsoft.Data.Entity.Infrastructure; -using Microsoft.Data.Entity.Metadata; using Microsoft.Extensions.DependencyInjection; using Xunit; diff --git a/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/DataAnnotationSqlServerTest.cs b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/DataAnnotationSqlServerTest.cs index b48697af7ff..d5cd2093e22 100644 --- a/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/DataAnnotationSqlServerTest.cs +++ b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/DataAnnotationSqlServerTest.cs @@ -56,9 +56,12 @@ public override void DatabaseGeneratedAttribute_autogenerates_values_when_set_to @p2: 00000000-0000-0000-0000-000000000003 SET NOCOUNT OFF; +DECLARE @generated1 TABLE ([UniqueNo] int); INSERT INTO [Sample] ([MaxLengthProperty], [Name], [RowVersion]) OUTPUT INSERTED.[UniqueNo] -VALUES (@p0, @p1, @p2);", +INTO @generated1 +VALUES (@p0, @p1, @p2); +SELECT [UniqueNo] FROM @generated1;", Sql); } @@ -71,18 +74,24 @@ public override void MaxLengthAttribute_throws_while_inserting_value_longer_than @p2: 00000000-0000-0000-0000-000000000001 SET NOCOUNT OFF; +DECLARE @generated1 TABLE ([UniqueNo] int); INSERT INTO [Sample] ([MaxLengthProperty], [Name], [RowVersion]) OUTPUT INSERTED.[UniqueNo] +INTO @generated1 VALUES (@p0, @p1, @p2); +SELECT [UniqueNo] FROM @generated1; @p0: VeryVeryVeryVeryVeryVeryLongString @p1: ValidString @p2: 00000000-0000-0000-0000-000000000002 SET NOCOUNT OFF; +DECLARE @generated1 TABLE ([UniqueNo] int); INSERT INTO [Sample] ([MaxLengthProperty], [Name], [RowVersion]) OUTPUT INSERTED.[UniqueNo] -VALUES (@p0, @p1, @p2);", +INTO @generated1 +VALUES (@p0, @p1, @p2); +SELECT [UniqueNo] FROM @generated1;", Sql); } @@ -117,18 +126,24 @@ public override void RequiredAttribute_for_property_throws_while_inserting_null_ @p2: 00000000-0000-0000-0000-000000000001 SET NOCOUNT OFF; +DECLARE @generated1 TABLE ([UniqueNo] int); INSERT INTO [Sample] ([MaxLengthProperty], [Name], [RowVersion]) OUTPUT INSERTED.[UniqueNo] +INTO @generated1 VALUES (@p0, @p1, @p2); +SELECT [UniqueNo] FROM @generated1; @p0: @p1: @p2: 00000000-0000-0000-0000-000000000002 SET NOCOUNT OFF; +DECLARE @generated1 TABLE ([UniqueNo] int); INSERT INTO [Sample] ([MaxLengthProperty], [Name], [RowVersion]) OUTPUT INSERTED.[UniqueNo] -VALUES (@p0, @p1, @p2);", +INTO @generated1 +VALUES (@p0, @p1, @p2); +SELECT [UniqueNo] FROM @generated1;", Sql); } @@ -139,16 +154,22 @@ public override void StringLengthAttribute_throws_while_inserting_value_longer_t Assert.Equal(@"@p0: ValidString SET NOCOUNT OFF; +DECLARE @generated1 TABLE ([Id] int, [Timestamp] varbinary(8)); INSERT INTO [Two] ([Data]) OUTPUT INSERTED.[Id], INSERTED.[Timestamp] +INTO @generated1 VALUES (@p0); +SELECT [Id], [Timestamp] FROM @generated1; @p0: ValidButLongString SET NOCOUNT OFF; +DECLARE @generated1 TABLE ([Id] int, [Timestamp] varbinary(8)); INSERT INTO [Two] ([Data]) OUTPUT INSERTED.[Id], INSERTED.[Timestamp] -VALUES (@p0);", +INTO @generated1 +VALUES (@p0); +SELECT [Id], [Timestamp] FROM @generated1;", Sql); } @@ -169,18 +190,24 @@ FROM [Two] AS [r] @p2: System.Byte[] SET NOCOUNT OFF; +DECLARE @generated0 TABLE ([Timestamp] varbinary(8)); UPDATE [Two] SET [Data] = @p1 OUTPUT INSERTED.[Timestamp] +INTO @generated0 WHERE [Id] = @p0 AND [Timestamp] = @p2; +SELECT [Timestamp] FROM @generated0; @p0: 1 @p1: ChangedData @p2: System.Byte[] SET NOCOUNT OFF; +DECLARE @generated0 TABLE ([Timestamp] varbinary(8)); UPDATE [Two] SET [Data] = @p1 OUTPUT INSERTED.[Timestamp] -WHERE [Id] = @p0 AND [Timestamp] = @p2;", +INTO @generated0 +WHERE [Id] = @p0 AND [Timestamp] = @p2; +SELECT [Timestamp] FROM @generated0;", Sql); } diff --git a/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/EntityFramework.MicrosoftSqlServer.FunctionalTests.csproj b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/EntityFramework.MicrosoftSqlServer.FunctionalTests.csproj index 3f114fce456..5be1e303d4e 100644 --- a/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/EntityFramework.MicrosoftSqlServer.FunctionalTests.csproj +++ b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/EntityFramework.MicrosoftSqlServer.FunctionalTests.csproj @@ -96,6 +96,7 @@ + diff --git a/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/SqlServerTriggersTest.cs b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/SqlServerTriggersTest.cs new file mode 100644 index 00000000000..88dadc49b49 --- /dev/null +++ b/test/EntityFramework.MicrosoftSqlServer.FunctionalTests/SqlServerTriggersTest.cs @@ -0,0 +1,213 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.Data.Entity.FunctionalTests; +using Microsoft.Data.Entity.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.Data.Entity.SqlServer.FunctionalTests +{ + public class SqlServerTriggersTest : IClassFixture, IDisposable + { + [Fact] + public void Triggers_run_on_insert_update_and_delete() + { + using (var context = CreateContext()) + { + var product = new Product { Name = "blah" }; + context.Products.Add(product); + context.SaveChanges(); + + var firstVersion = product.Version; + var productBackup = context.ProductBackups.AsNoTracking().Single(); + Assert.Equal(product.Id, productBackup.Id); + Assert.Equal(product.Name, productBackup.Name); + Assert.Equal(product.Version, productBackup.Version); + + product.Name = "fooh"; + context.SaveChanges(); + + Assert.NotEqual(firstVersion, product.Version); + productBackup = context.ProductBackups.AsNoTracking().Single(); + Assert.Equal(product.Id, productBackup.Id); + Assert.Equal(product.Name, productBackup.Name); + Assert.Equal(product.Version, productBackup.Version); + + context.Products.Remove(product); + context.SaveChanges(); + + Assert.Empty(context.Products); + Assert.Empty(context.ProductBackups); + } + } + + [Fact] + public void Triggers_work_with_batch_operations() + { + using (var context = CreateContext()) + { + var productToBeUpdated1 = new Product { Name = "u1" }; + var productToBeUpdated2 = new Product { Name = "u2" }; + context.Products.Add(productToBeUpdated1); + context.Products.Add(productToBeUpdated2); + + var productToBeDeleted1 = new Product { Name = "d1" }; + var productToBeDeleted2 = new Product { Name = "d2" }; + context.Products.Add(productToBeDeleted1); + context.Products.Add(productToBeDeleted2); + + context.SaveChanges(); + + var productToBeAdded1 = new Product { Name = "a1" }; + var productToBeAdded2 = new Product { Name = "a2" }; + context.Products.Add(productToBeAdded1); + context.Products.Add(productToBeAdded2); + + productToBeUpdated1.Name = "n1"; + productToBeUpdated2.Name = "n2"; + + context.Products.Remove(productToBeDeleted1); + context.Products.Remove(productToBeDeleted2); + + context.SaveChanges(); + + var productBackups = context.ProductBackups.ToList(); + + Assert.Equal(4, productBackups.Count); + Assert.True(productBackups.Any(p => p.Name == "a1")); + Assert.True(productBackups.Any(p => p.Name == "a2")); + Assert.True(productBackups.Any(p => p.Name == "n1")); + Assert.True(productBackups.Any(p => p.Name == "n2")); + } + } + + private readonly SqlServerTriggersFixture _fixture; + private readonly SqlServerTestStore _testStore; + + public SqlServerTriggersTest(SqlServerTriggersFixture fixture) + { + _fixture = fixture; + _testStore = _fixture.GetTestStore(); + } + + private TriggersContext CreateContext() => _fixture.CreateContext(_testStore); + + public void Dispose() => _testStore.Dispose(); + + public class SqlServerTriggersFixture + { + private readonly IServiceProvider _serviceProvider; + + public SqlServerTriggersFixture() + { + _serviceProvider + = new ServiceCollection() + .AddEntityFramework() + .AddSqlServer() + .ServiceCollection() + .AddSingleton(TestSqlServerModelSource.GetFactory(OnModelCreating)) + .AddSingleton(new TestSqlLoggerFactory()) + .BuildServiceProvider(); + } + + public virtual SqlServerTestStore GetTestStore() + { + var testStore = SqlServerTestStore.CreateScratch(); + + using (var context = CreateContext(testStore)) + { + context.Database.EnsureCreated(); + + testStore.ExecuteNonQuery(@" +CREATE TRIGGER TRG_InsertProduct +ON Product +AFTER INSERT AS +BEGIN + if @@ROWCOUNT = 0 + return + set nocount on; + + INSERT INTO ProductBackup + SELECT * FROM INSERTED; +END"); + + testStore.ExecuteNonQuery(@" +CREATE TRIGGER TRG_UpdateProduct +ON Product +AFTER UPDATE AS +BEGIN + if @@ROWCOUNT = 0 + return + set nocount on; + + DELETE FROM ProductBackup + WHERE Id IN(SELECT DELETED.Id FROM DELETED); + + INSERT INTO ProductBackup + SELECT * FROM INSERTED; +END"); + + testStore.ExecuteNonQuery(@" +CREATE TRIGGER TRG_DeleteProduct +ON Product +AFTER DELETE AS +BEGIN + if @@ROWCOUNT = 0 + return + set nocount on; + + DELETE FROM ProductBackup + WHERE Id IN(SELECT DELETED.Id FROM DELETED); +END"); + } + + return testStore; + } + + public void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(e => e.Version) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + modelBuilder.Entity().HasBaseType((Type)null) + .Property(e => e.Id).ValueGeneratedNever(); + } + + public TriggersContext CreateContext(SqlServerTestStore testStore) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder + .EnableSensitiveDataLogging() + .UseSqlServer(testStore.Connection); + + return new TriggersContext(_serviceProvider, optionsBuilder.Options); + } + } + + public class TriggersContext : DbContext + { + public TriggersContext(IServiceProvider serviceProvider, DbContextOptions options) + : base(serviceProvider, options) + { + } + + public virtual DbSet Products { get; set; } + public virtual DbSet ProductBackups { get; set; } + } + + public class Product + { + public virtual int Id { get; set; } + public virtual byte[] Version { get; set; } + public virtual string Name { get; set; } + } + + public class ProductBackup : Product + { + } + } +} diff --git a/test/EntityFramework.MicrosoftSqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs b/test/EntityFramework.MicrosoftSqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs index bf23984530d..0699ae0b1ff 100644 --- a/test/EntityFramework.MicrosoftSqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs +++ b/test/EntityFramework.MicrosoftSqlServer.Tests/SqlServerSequenceValueGeneratorTest.cs @@ -58,7 +58,7 @@ public void Generates_sequential_values() var generator = new SqlServerSequenceHiLoValueGenerator( new FakeSqlCommandBuilder(blockSize), - new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator()), + new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator(), new SqlServerTypeMapper()), state, CreateConnection()); @@ -104,7 +104,7 @@ private IEnumerable> GenerateValuesInMultipleThreads(int threadCount, }); var executor = new FakeSqlCommandBuilder(blockSize); - var sqlGenerator = new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator()); + var sqlGenerator = new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator(), new SqlServerTypeMapper()); var tests = new Action[threadCount]; var generatedValues = new List[threadCount]; @@ -141,7 +141,7 @@ public void Does_not_generate_temp_values() var generator = new SqlServerSequenceHiLoValueGenerator( new FakeSqlCommandBuilder(4), - new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator()), + new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator(), new SqlServerTypeMapper()), state, CreateConnection()); diff --git a/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs b/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs index cdce021ed1f..b0c2181b665 100644 --- a/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs +++ b/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchFactoryTest.cs @@ -25,7 +25,7 @@ public void Uses_MaxBatchSize_specified_in_SqlServerOptionsExtension() new DiagnosticListener("Fake"), new SqlServerTypeMapper()), new SqlServerSqlGenerator(), - new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator()), + new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator(), new SqlServerTypeMapper()), new UntypedRelationalValueBufferFactoryFactory(), optionsBuilder.Options); @@ -47,7 +47,7 @@ public void MaxBatchSize_is_optional() new DiagnosticListener("Fake"), new SqlServerTypeMapper()), new SqlServerSqlGenerator(), - new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator()), + new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator(), new SqlServerTypeMapper()), new UntypedRelationalValueBufferFactoryFactory(), optionsBuilder.Options); diff --git a/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs b/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs index 357ef3d6a27..e95c1a0dcd1 100644 --- a/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs +++ b/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerModificationCommandBatchTest.cs @@ -22,7 +22,7 @@ public void AddCommand_returns_false_when_max_batch_size_is_reached() new DiagnosticListener("Fake"), new SqlServerTypeMapper()), new SqlServerSqlGenerator(), - new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator()), + new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator(), new SqlServerTypeMapper()), new UntypedRelationalValueBufferFactoryFactory(), 1); diff --git a/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerUpdateSqlGeneratorTest.cs b/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerUpdateSqlGeneratorTest.cs index 2e8ede363e8..8d3d4dc286a 100644 --- a/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerUpdateSqlGeneratorTest.cs +++ b/test/EntityFramework.MicrosoftSqlServer.Tests/Update/SqlServerUpdateSqlGeneratorTest.cs @@ -14,7 +14,7 @@ namespace Microsoft.Data.Entity.SqlServer.Tests public class SqlServerUpdateSqlGeneratorTest : UpdateSqlGeneratorTestBase { protected override IUpdateSqlGenerator CreateSqlGenerator() - => new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator()); + => new SqlServerUpdateSqlGenerator(new SqlServerSqlGenerator(), new SqlServerTypeMapper()); [Fact] public void AppendBatchHeader_should_append_SET_NOCOUNT_OFF() @@ -29,63 +29,84 @@ public void AppendBatchHeader_should_append_SET_NOCOUNT_OFF() protected override void AppendInsertOperation_appends_insert_and_select_store_generated_columns_but_no_identity_verification(StringBuilder stringBuilder) { Assert.Equal( + "DECLARE @generated0 TABLE ([Computed] uniqueidentifier);" + Environment.NewLine + "INSERT INTO [dbo].[Ducks] ([Id], [Name], [Quacks], [ConcurrencyToken])" + Environment.NewLine + "OUTPUT INSERTED.[Computed]" + Environment.NewLine + - "VALUES (@p0, @p1, @p2, @p3);" + Environment.NewLine, + "INTO @generated0" + Environment.NewLine + + "VALUES (@p0, @p1, @p2, @p3);" + Environment.NewLine + + "SELECT [Computed] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); } protected override void AppendInsertOperation_appends_insert_and_select_and_where_if_store_generated_columns_exist_verification(StringBuilder stringBuilder) { Assert.Equal( + "DECLARE @generated0 TABLE ([Id] int, [Computed] uniqueidentifier);" + Environment.NewLine + "INSERT INTO [dbo].[Ducks] ([Name], [Quacks], [ConcurrencyToken])" + Environment.NewLine + "OUTPUT INSERTED.[Id], INSERTED.[Computed]" + Environment.NewLine + - "VALUES (@p0, @p1, @p2);" + Environment.NewLine, + "INTO @generated0" + Environment.NewLine + + "VALUES (@p0, @p1, @p2);" + Environment.NewLine + + "SELECT [Id], [Computed] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); } protected override void AppendInsertOperation_appends_insert_and_select_for_only_single_identity_columns_verification(StringBuilder stringBuilder) { Assert.Equal( + "DECLARE @generated0 TABLE ([Id] int);" + Environment.NewLine + "INSERT INTO [dbo].[Ducks]" + Environment.NewLine + "OUTPUT INSERTED.[Id]" + Environment.NewLine + - "DEFAULT VALUES;" + Environment.NewLine, + "INTO @generated0" + Environment.NewLine + + "DEFAULT VALUES;" + Environment.NewLine + + "SELECT [Id] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); } protected override void AppendInsertOperation_appends_insert_and_select_for_only_identity_verification(StringBuilder stringBuilder) { Assert.Equal( + "DECLARE @generated0 TABLE ([Id] int);" + Environment.NewLine + "INSERT INTO [dbo].[Ducks] ([Name], [Quacks], [ConcurrencyToken])" + Environment.NewLine + "OUTPUT INSERTED.[Id]" + Environment.NewLine + - "VALUES (@p0, @p1, @p2);" + Environment.NewLine, + "INTO @generated0" + Environment.NewLine + + "VALUES (@p0, @p1, @p2);" + Environment.NewLine + + "SELECT [Id] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); } protected override void AppendInsertOperation_appends_insert_and_select_for_all_store_generated_columns_verification(StringBuilder stringBuilder) { Assert.Equal( + "DECLARE @generated0 TABLE ([Id] int, [Computed] uniqueidentifier);" + Environment.NewLine + "INSERT INTO [dbo].[Ducks]" + Environment.NewLine + "OUTPUT INSERTED.[Id], INSERTED.[Computed]" + Environment.NewLine + - "DEFAULT VALUES;" + Environment.NewLine, + "INTO @generated0" + Environment.NewLine + + "DEFAULT VALUES;" + Environment.NewLine + + "SELECT [Id], [Computed] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); } protected override void AppendUpdateOperation_appends_update_and_select_if_store_generated_columns_exist_verification(StringBuilder stringBuilder) { Assert.Equal( + "DECLARE @generated0 TABLE ([Computed] uniqueidentifier);" + Environment.NewLine + "UPDATE [dbo].[Ducks] SET [Name] = @p0, [Quacks] = @p1, [ConcurrencyToken] = @p2" + Environment.NewLine + "OUTPUT INSERTED.[Computed]" + Environment.NewLine + - "WHERE [Id] = @p3 AND [ConcurrencyToken] = @p4;" + Environment.NewLine, + "INTO @generated0" + Environment.NewLine + + "WHERE [Id] = @p3 AND [ConcurrencyToken] = @p4;" + Environment.NewLine + + "SELECT [Computed] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); } - + protected override void AppendUpdateOperation_appends_select_for_computed_property_verification(StringBuilder stringBuilder) { Assert.Equal( + "DECLARE @generated0 TABLE ([Computed] uniqueidentifier);" + Environment.NewLine + "UPDATE [dbo].[Ducks] SET [Name] = @p0, [Quacks] = @p1, [ConcurrencyToken] = @p2" + Environment.NewLine + "OUTPUT INSERTED.[Computed]" + Environment.NewLine + - "WHERE [Id] = @p3;" + Environment.NewLine, + "INTO @generated0" + Environment.NewLine + + "WHERE [Id] = @p3;" + Environment.NewLine + + "SELECT [Computed] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); } @@ -96,15 +117,18 @@ public void AppendBulkInsertOperation_appends_insert_if_store_generated_columns_ var command = CreateInsertCommand(identityKey: true, isComputed: true); var sqlGenerator = (ISqlServerUpdateSqlGenerator)CreateSqlGenerator(); - var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }); + var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }, 0); Assert.Equal( + "DECLARE @generated0 TABLE ([Id] int, [Computed] uniqueidentifier);" + Environment.NewLine + "INSERT INTO [dbo].[Ducks] ([Name], [Quacks], [ConcurrencyToken])" + Environment.NewLine + "OUTPUT INSERTED.[Id], INSERTED.[Computed]" + Environment.NewLine + + "INTO @generated0" + Environment.NewLine + "VALUES (@p0, @p1, @p2)," + Environment.NewLine + - "(@p0, @p1, @p2);" + Environment.NewLine, + "(@p0, @p1, @p2);" + Environment.NewLine + + "SELECT [Id], [Computed] FROM @generated0;" + Environment.NewLine, stringBuilder.ToString()); - Assert.Equal(SqlServerUpdateSqlGenerator.ResultsGrouping.OneResultSet, grouping); + Assert.Equal(ResultsGrouping.OneResultSet, grouping); } [Fact] @@ -114,7 +138,7 @@ public void AppendBulkInsertOperation_appends_insert_if_no_store_generated_colum var command = CreateInsertCommand(identityKey: false, isComputed: false); var sqlGenerator = (ISqlServerUpdateSqlGenerator)CreateSqlGenerator(); - var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }); + var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }, 0); Assert.Equal( "INSERT INTO [dbo].[Ducks] ([Id], [Name], [Quacks], [ConcurrencyToken])" + Environment.NewLine + @@ -122,7 +146,7 @@ public void AppendBulkInsertOperation_appends_insert_if_no_store_generated_colum "(@p0, @p1, @p2, @p3);" + Environment.NewLine + "SELECT @@ROWCOUNT;" + Environment.NewLine, stringBuilder.ToString()); - Assert.Equal(SqlServerUpdateSqlGenerator.ResultsGrouping.OneResultSet, grouping); + Assert.Equal(ResultsGrouping.OneResultSet, grouping); } [Fact] @@ -132,14 +156,18 @@ public void AppendBulkInsertOperation_appends_insert_if_store_generated_columns_ var command = CreateInsertCommand(identityKey: true, isComputed: true, defaultsOnly: true); var sqlGenerator = (ISqlServerUpdateSqlGenerator)CreateSqlGenerator(); - var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }); + var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }, 0); - var expectedText = "INSERT INTO [dbo].[Ducks]" + Environment.NewLine + - "OUTPUT INSERTED.[Id], INSERTED.[Computed]" + Environment.NewLine + - "DEFAULT VALUES;" + Environment.NewLine; + var expectedText = + "DECLARE @generated0 TABLE ([Id] int, [Computed] uniqueidentifier);" + Environment.NewLine + + "INSERT INTO [dbo].[Ducks]" + Environment.NewLine + + "OUTPUT INSERTED.[Id], INSERTED.[Computed]" + Environment.NewLine + + "INTO @generated0" + Environment.NewLine + + "DEFAULT VALUES;" + Environment.NewLine + + "SELECT [Id], [Computed] FROM @generated0;" + Environment.NewLine; Assert.Equal(expectedText + expectedText, stringBuilder.ToString()); - Assert.Equal(SqlServerUpdateSqlGenerator.ResultsGrouping.OneCommandPerResultSet, grouping); + Assert.Equal(ResultsGrouping.OneCommandPerResultSet, grouping); } [Fact] @@ -149,34 +177,25 @@ public void AppendBulkInsertOperation_appends_insert_if_no_store_generated_colum var command = CreateInsertCommand(identityKey: false, isComputed: false, defaultsOnly: true); var sqlGenerator = (ISqlServerUpdateSqlGenerator)CreateSqlGenerator(); - var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }); + var grouping = sqlGenerator.AppendBulkInsertOperation(stringBuilder, new[] { command, command }, 0); var expectedText = "INSERT INTO [dbo].[Ducks]" + Environment.NewLine + "DEFAULT VALUES;" + Environment.NewLine + "SELECT @@ROWCOUNT;" + Environment.NewLine; Assert.Equal(expectedText + expectedText, stringBuilder.ToString()); - Assert.Equal(SqlServerUpdateSqlGenerator.ResultsGrouping.OneCommandPerResultSet, grouping); + Assert.Equal(ResultsGrouping.OneCommandPerResultSet, grouping); } - protected override string RowsAffected - { - get { return "@@ROWCOUNT"; } - } + protected override string RowsAffected => "@@ROWCOUNT"; protected override string Identity { get { throw new NotImplementedException(); } } - protected override string OpenDelimeter - { - get { return "["; } - } + protected override string OpenDelimeter => "["; - protected override string CloseDelimeter - { - get { return "]"; } - } + protected override string CloseDelimeter => "]"; } } diff --git a/test/EntityFramework.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs b/test/EntityFramework.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs index c4e46b153bc..d19cceeb8be 100644 --- a/test/EntityFramework.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs +++ b/test/EntityFramework.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs @@ -87,7 +87,7 @@ public void UpdateCommandText_compiles_inserts() batch.UpdateCachedCommandTextBase(0); sqlGeneratorMock.Verify(g => g.AppendBatchHeader(It.IsAny())); - sqlGeneratorMock.Verify(g => g.AppendInsertOperation(It.IsAny(), command)); + sqlGeneratorMock.Verify(g => g.AppendInsertOperation(It.IsAny(), command, 0)); } [Fact] @@ -105,7 +105,7 @@ public void UpdateCommandText_compiles_updates() batch.UpdateCachedCommandTextBase(0); sqlGeneratorMock.Verify(g => g.AppendBatchHeader(It.IsAny())); - sqlGeneratorMock.Verify(g => g.AppendUpdateOperation(It.IsAny(), command)); + sqlGeneratorMock.Verify(g => g.AppendUpdateOperation(It.IsAny(), command, 0)); } [Fact] @@ -123,7 +123,7 @@ public void UpdateCommandText_compiles_deletes() batch.UpdateCachedCommandTextBase(0); sqlGeneratorMock.Verify(g => g.AppendBatchHeader(It.IsAny())); - sqlGeneratorMock.Verify(g => g.AppendDeleteOperation(It.IsAny(), command)); + sqlGeneratorMock.Verify(g => g.AppendDeleteOperation(It.IsAny(), command, 0)); } [Fact] @@ -151,7 +151,7 @@ public FakeSqlGenerator() { } - public override void AppendInsertOperation(StringBuilder commandStringBuilder, ModificationCommand command) + public override void AppendInsertOperation(StringBuilder commandStringBuilder, ModificationCommand command, int commandPosition) { if (!string.IsNullOrEmpty(command.Schema)) { @@ -168,7 +168,7 @@ public override void AppendBatchHeader(StringBuilder commandStringBuilder) base.AppendBatchHeader(commandStringBuilder); } - protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema) + protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema, int commandPosition) { } diff --git a/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTest.cs b/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTest.cs index bf312235e11..33ca1bd615c 100644 --- a/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTest.cs +++ b/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTest.cs @@ -10,25 +10,16 @@ namespace Microsoft.Data.Entity.Tests { public class UpdateSqlGeneratorTest : UpdateSqlGeneratorTestBase { - protected override IUpdateSqlGenerator CreateSqlGenerator() - { - return new ConcreteSqlGenerator(); - } + protected override IUpdateSqlGenerator CreateSqlGenerator() => new ConcreteSqlGenerator(); - protected override string RowsAffected - { - get { return "provider_specific_rowcount()"; } - } + protected override string RowsAffected => "provider_specific_rowcount()"; - protected override string Identity - { - get { return "provider_specific_identity()"; } - } + protected override string Identity => "provider_specific_identity()"; private class ConcreteSqlGenerator : UpdateSqlGenerator { public ConcreteSqlGenerator() - :base(new RelationalSqlGenerator()) + : base(new RelationalSqlGenerator()) { } @@ -40,7 +31,7 @@ protected override void AppendIdentityWhereCondition(StringBuilder commandString .Append("provider_specific_identity()"); } - protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema) + protected override void AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema, int commandPosition) { commandStringBuilder .Append("SELECT provider_specific_rowcount();" + Environment.NewLine); diff --git a/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTestBase.cs b/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTestBase.cs index 1b930386456..ff234db0a25 100644 --- a/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTestBase.cs +++ b/test/EntityFramework.Relational.Tests/Update/UpdateSqlGeneratorTestBase.cs @@ -16,14 +16,13 @@ namespace Microsoft.Data.Entity.Tests { public abstract class UpdateSqlGeneratorTestBase { - [Fact] public void AppendDeleteOperation_creates_full_delete_command_text() { var stringBuilder = new StringBuilder(); var command = CreateDeleteCommand(false); - CreateSqlGenerator().AppendDeleteOperation(stringBuilder, command); + CreateSqlGenerator().AppendDeleteOperation(stringBuilder, command, 0); Assert.Equal( "DELETE FROM " + SchemaPrefix + OpenDelimeter + "Ducks" + CloseDelimeter + "" + Environment.NewLine + @@ -38,7 +37,7 @@ public virtual void AppendDeleteOperation_creates_full_delete_command_text_with_ var stringBuilder = new StringBuilder(); var command = CreateDeleteCommand(concurrencyToken: true); - CreateSqlGenerator().AppendDeleteOperation(stringBuilder, command); + CreateSqlGenerator().AppendDeleteOperation(stringBuilder, command, 0); Assert.Equal( "DELETE FROM " + SchemaPrefix + OpenDelimeter + "Ducks" + CloseDelimeter + "" + Environment.NewLine + @@ -53,7 +52,7 @@ public void AppendInsertOperation_appends_insert_and_select_and_where_if_store_g var stringBuilder = new StringBuilder(); var command = CreateInsertCommand(identityKey: true, isComputed: true); - CreateSqlGenerator().AppendInsertOperation(stringBuilder, command); + CreateSqlGenerator().AppendInsertOperation(stringBuilder, command, 0); AppendInsertOperation_appends_insert_and_select_and_where_if_store_generated_columns_exist_verification(stringBuilder); } @@ -75,7 +74,7 @@ public void AppendInsertOperation_appends_insert_and_select_rowcount_if_no_store var stringBuilder = new StringBuilder(); var command = CreateInsertCommand(false, false); - CreateSqlGenerator().AppendInsertOperation(stringBuilder, command); + CreateSqlGenerator().AppendInsertOperation(stringBuilder, command, 0); Assert.Equal( "INSERT INTO " + SchemaPrefix + OpenDelimeter + "Ducks" + CloseDelimeter + " (" + @@ -92,7 +91,7 @@ public void AppendInsertOperation_appends_insert_and_select_store_generated_colu var stringBuilder = new StringBuilder(); var command = CreateInsertCommand(false, isComputed: true); - CreateSqlGenerator().AppendInsertOperation(stringBuilder, command); + CreateSqlGenerator().AppendInsertOperation(stringBuilder, command, 0); AppendInsertOperation_appends_insert_and_select_store_generated_columns_but_no_identity_verification(stringBuilder); } @@ -116,7 +115,7 @@ public void AppendInsertOperation_appends_insert_and_select_for_only_identity() var stringBuilder = new StringBuilder(); var command = CreateInsertCommand(true, false); - CreateSqlGenerator().AppendInsertOperation(stringBuilder, command); + CreateSqlGenerator().AppendInsertOperation(stringBuilder, command, 0); AppendInsertOperation_appends_insert_and_select_for_only_identity_verification(stringBuilder); } @@ -139,7 +138,7 @@ public void AppendInsertOperation_appends_insert_and_select_for_all_store_genera var stringBuilder = new StringBuilder(); var command = CreateInsertCommand(true, true, true); - CreateSqlGenerator().AppendInsertOperation(stringBuilder, command); + CreateSqlGenerator().AppendInsertOperation(stringBuilder, command, 0); AppendInsertOperation_appends_insert_and_select_for_all_store_generated_columns_verification(stringBuilder); } @@ -161,7 +160,7 @@ public void AppendInsertOperation_appends_insert_and_select_for_only_single_iden var stringBuilder = new StringBuilder(); var command = CreateInsertCommand(true, false, true); - CreateSqlGenerator().AppendInsertOperation(stringBuilder, command); + CreateSqlGenerator().AppendInsertOperation(stringBuilder, command, 0); AppendInsertOperation_appends_insert_and_select_for_only_single_identity_columns_verification(stringBuilder); } @@ -183,7 +182,7 @@ public void AppendUpdateOperation_appends_update_and_select_if_store_generated_c var stringBuilder = new StringBuilder(); var command = CreateUpdateCommand(isComputed: true, concurrencyToken: true); - CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command); + CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command, 0); AppendUpdateOperation_appends_update_and_select_if_store_generated_columns_exist_verification(stringBuilder); } @@ -206,7 +205,7 @@ public void AppendUpdateOperation_appends_update_and_select_rowcount_if_store_ge var stringBuilder = new StringBuilder(); var command = CreateUpdateCommand(false, false); - CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command); + CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command, 0); Assert.Equal( "UPDATE " + SchemaPrefix + OpenDelimeter + "Ducks" + CloseDelimeter + " SET " + @@ -223,7 +222,7 @@ public void AppendUpdateOperation_appends_where_for_concurrency_token() var stringBuilder = new StringBuilder(); var command = CreateUpdateCommand(false, concurrencyToken: true); - CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command); + CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command, 0); Assert.Equal( "UPDATE " + SchemaPrefix + OpenDelimeter + "Ducks" + CloseDelimeter + " SET " + @@ -240,7 +239,7 @@ public void AppendUpdateOperation_appends_select_for_computed_property() var stringBuilder = new StringBuilder(); var command = CreateUpdateCommand(true, false); - CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command); + CreateSqlGenerator().AppendUpdateOperation(stringBuilder, command, 0); AppendUpdateOperation_appends_select_for_computed_property_verification(stringBuilder); } @@ -284,10 +283,11 @@ public virtual void GenerateNextSequenceValueOperation_correctly_handles_schemas protected abstract string Identity { get; } - protected IProperty CreateMockProperty(string name) + protected IProperty CreateMockProperty(string name, Type type) { var propertyMock = new Mock(); propertyMock.Setup(m => m.Name).Returns(name); + propertyMock.Setup(m => m.ClrType).Returns(type); return propertyMock.Object; } @@ -304,27 +304,28 @@ protected IProperty CreateMockProperty(string name) protected ModificationCommand CreateInsertCommand(bool identityKey = true, bool isComputed = true, bool defaultsOnly = false) { - var entry = CreateInternalEntryMock().Object; + var duck = GetDuckType(); + var entry = CreateInternalEntryMock(duck).Object; var generator = new ParameterNameGenerator(); - var idProperty = CreateMockProperty("Id"); - var nameProperty = CreateMockProperty("Name"); - var quacksProperty = CreateMockProperty("Quacks"); - var computedProperty = CreateMockProperty("Computed"); - var concurrencyProperty = CreateMockProperty("ConcurrencyToken"); + var idProperty = duck.FindProperty(nameof(Duck.Id)); + var nameProperty = duck.FindProperty(nameof(Duck.Name)); + var quacksProperty = duck.FindProperty(nameof(Duck.Quacks)); + var computedProperty = duck.FindProperty(nameof(Duck.Computed)); + var concurrencyProperty = duck.FindProperty(nameof(Duck.ConcurrencyToken)); var columnModifications = new[] - { - new ColumnModification( - entry, idProperty, idProperty.TestProvider(), generator, identityKey, !identityKey, true, false), - new ColumnModification( - entry, nameProperty, nameProperty.TestProvider(), generator, false, true, false, false), - new ColumnModification( - entry, quacksProperty, quacksProperty.TestProvider(), generator, false, true, false, false), - new ColumnModification( - entry, computedProperty, computedProperty.TestProvider(), generator, isComputed, false, false, false), - new ColumnModification( - entry, concurrencyProperty, concurrencyProperty.TestProvider(), generator, false, true, false, false) - }; + { + new ColumnModification( + entry, idProperty, idProperty.TestProvider(), generator, identityKey, !identityKey, true, false), + new ColumnModification( + entry, nameProperty, nameProperty.TestProvider(), generator, false, true, false, false), + new ColumnModification( + entry, quacksProperty, quacksProperty.TestProvider(), generator, false, true, false, false), + new ColumnModification( + entry, computedProperty, computedProperty.TestProvider(), generator, isComputed, false, false, false), + new ColumnModification( + entry, concurrencyProperty, concurrencyProperty.TestProvider(), generator, false, true, false, false) + }; if (defaultsOnly) { @@ -340,27 +341,28 @@ protected ModificationCommand CreateInsertCommand(bool identityKey = true, bool protected ModificationCommand CreateUpdateCommand(bool isComputed = true, bool concurrencyToken = true) { - var entry = CreateInternalEntryMock().Object; + var duck = GetDuckType(); + var entry = CreateInternalEntryMock(duck).Object; var generator = new ParameterNameGenerator(); - var idProperty = CreateMockProperty("Id"); - var nameProperty = CreateMockProperty("Name"); - var quacksProperty = CreateMockProperty("Quacks"); - var computedProperty = CreateMockProperty("Computed"); - var concurrencyProperty = CreateMockProperty("ConcurrencyToken"); + var idProperty = duck.FindProperty(nameof(Duck.Id)); + var nameProperty = duck.FindProperty(nameof(Duck.Name)); + var quacksProperty = duck.FindProperty(nameof(Duck.Quacks)); + var computedProperty = duck.FindProperty(nameof(Duck.Computed)); + var concurrencyProperty = duck.FindProperty(nameof(Duck.ConcurrencyToken)); var columnModifications = new[] - { - new ColumnModification( - entry, idProperty, idProperty.TestProvider(), generator, false, false, true, true), - new ColumnModification( - entry, nameProperty, nameProperty.TestProvider(), generator, false, true, false, false), - new ColumnModification( - entry, quacksProperty, quacksProperty.TestProvider(), generator, false, true, false, false), - new ColumnModification( - entry, computedProperty, computedProperty.TestProvider(), generator, isComputed, false, false, false), - new ColumnModification( - entry, concurrencyProperty, concurrencyProperty.TestProvider(), generator, false, true, false, concurrencyToken) - }; + { + new ColumnModification( + entry, idProperty, idProperty.TestProvider(), generator, false, false, true, true), + new ColumnModification( + entry, nameProperty, nameProperty.TestProvider(), generator, false, true, false, false), + new ColumnModification( + entry, quacksProperty, quacksProperty.TestProvider(), generator, false, true, false, false), + new ColumnModification( + entry, computedProperty, computedProperty.TestProvider(), generator, isComputed, false, false, false), + new ColumnModification( + entry, concurrencyProperty, concurrencyProperty.TestProvider(), generator, false, true, false, concurrencyToken) + }; Func func = p => p.TestProvider(); var commandMock = new Mock("Ducks", Schema, new ParameterNameGenerator(), func) { CallBase = true }; @@ -371,18 +373,19 @@ protected ModificationCommand CreateUpdateCommand(bool isComputed = true, bool c protected ModificationCommand CreateDeleteCommand(bool concurrencyToken = true) { - var entry = CreateInternalEntryMock().Object; + var duck = GetDuckType(); + var entry = CreateInternalEntryMock(duck).Object; var generator = new ParameterNameGenerator(); - var idProperty = CreateMockProperty("Id"); - var concurrencyProperty = CreateMockProperty("ConcurrencyToken"); + var idProperty = duck.FindProperty(nameof(Duck.Id)); + var concurrencyProperty = duck.FindProperty(nameof(Duck.ConcurrencyToken)); var columnModifications = new[] - { - new ColumnModification( - entry, idProperty, idProperty.TestProvider(), generator, false, false, true, true), - new ColumnModification( - entry, concurrencyProperty, concurrencyProperty.TestProvider(), generator, false, false, false, concurrencyToken) - }; + { + new ColumnModification( + entry, idProperty, idProperty.TestProvider(), generator, false, false, true, true), + new ColumnModification( + entry, concurrencyProperty, concurrencyProperty.TestProvider(), generator, false, false, false, concurrencyToken) + }; Func func = p => p.TestProvider(); var commandMock = new Mock("Ducks", Schema, new ParameterNameGenerator(), func) { CallBase = true }; @@ -391,16 +394,28 @@ protected ModificationCommand CreateDeleteCommand(bool concurrencyToken = true) return commandMock.Object; } - private static Mock CreateInternalEntryMock() - { - var entityTypeMock = new Mock(); - entityTypeMock.Setup(e => e.GetProperties()).Returns(new IProperty[0]); + private static Mock CreateInternalEntryMock(EntityType entityType) + => new Mock(Mock.Of(), entityType); - entityTypeMock.As().Setup(e => e.Counts).Returns(new PropertyCounts(0, 0, 0, 0, 0, 0)); + private EntityType GetDuckType() + { + var entityType = new Entity.Metadata.Internal.Model().AddEntityType(typeof(Duck)); + var id = entityType.AddProperty(typeof(Duck).GetProperty(nameof(Duck.Id))); + entityType.AddProperty(typeof(Duck).GetProperty(nameof(Duck.Name))); + entityType.AddProperty(typeof(Duck).GetProperty(nameof(Duck.Quacks))); + entityType.AddProperty(typeof(Duck).GetProperty(nameof(Duck.Computed))); + entityType.AddProperty(typeof(Duck).GetProperty(nameof(Duck.ConcurrencyToken))); + entityType.SetPrimaryKey(id); + return entityType; + } - var internalEntryMock = new Mock( - Mock.Of(), entityTypeMock.Object); - return internalEntryMock; + protected class Duck + { + public int Id { get; set; } + public string Name { get; set; } + public int Quacks { get; set; } + public Guid Computed { get; set; } + public byte[] ConcurrencyToken { get; set; } } } }