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; }
}
}
}