Skip to content

Commit 35e9e5a

Browse files
authored
Fix bug where a non-sproc command comes before a sproc command (#29680)
Fixes #29643
1 parent 99a642b commit 35e9e5a

File tree

3 files changed

+111
-6
lines changed

3 files changed

+111
-6
lines changed

src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs

+39-6
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,14 @@ protected override void Consume(RelationalDataReader reader)
9191
var parameterCounter = 0;
9292
IReadOnlyModificationCommand command;
9393

94-
for (commandIndex = 0;
95-
commandIndex < ResultSetMappings.Count;
96-
commandIndex++, parameterCounter += command.StoreStoredProcedure!.Parameters.Count)
94+
for (commandIndex = 0; commandIndex < ResultSetMappings.Count; commandIndex++, parameterCounter += ParameterCount(command))
9795
{
9896
command = ModificationCommands[commandIndex];
9997

98+
Check.DebugAssert(
99+
command.ColumnModifications.All(c => c.UseParameter),
100+
"This code assumes all column modifications involve a DbParameter (see counting above)");
101+
100102
if (!ResultSetMappings[commandIndex].HasFlag(ResultSetMapping.HasOutputParameters))
101103
{
102104
continue;
@@ -210,12 +212,14 @@ protected override async Task ConsumeAsync(
210212
var parameterCounter = 0;
211213
IReadOnlyModificationCommand command;
212214

213-
for (commandIndex = 0;
214-
commandIndex < ResultSetMappings.Count;
215-
commandIndex++, parameterCounter += command.StoreStoredProcedure!.Parameters.Count)
215+
for (commandIndex = 0; commandIndex < ResultSetMappings.Count; commandIndex++, parameterCounter += ParameterCount(command))
216216
{
217217
command = ModificationCommands[commandIndex];
218218

219+
Check.DebugAssert(
220+
command.ColumnModifications.All(c => c.UseParameter),
221+
"This code assumes all column modifications involve a DbParameter (see counting above)");
222+
219223
if (!ResultSetMappings[commandIndex].HasFlag(ResultSetMapping.HasOutputParameters))
220224
{
221225
continue;
@@ -477,6 +481,35 @@ await ThrowAggregateUpdateConcurrencyExceptionAsync(
477481
return commandIndex - 1;
478482
}
479483

484+
private static int ParameterCount(IReadOnlyModificationCommand command)
485+
{
486+
// As a shortcut, if the command uses a stored procedure, return the number of parameters directly from it.
487+
if (command.StoreStoredProcedure is { } storedProcedure)
488+
{
489+
return storedProcedure.Parameters.Count;
490+
}
491+
492+
// Otherwise we need to count the total parameters used by all column modifications
493+
var parameterCount = 0;
494+
495+
for (var i = 0; i < command.ColumnModifications.Count; i++)
496+
{
497+
var columnModification = command.ColumnModifications[i];
498+
499+
if (columnModification.UseCurrentValueParameter)
500+
{
501+
parameterCount++;
502+
}
503+
504+
if (columnModification.UseOriginalValueParameter)
505+
{
506+
parameterCount++;
507+
}
508+
}
509+
510+
return parameterCount;
511+
}
512+
480513
private IReadOnlyList<IUpdateEntry> AggregateEntries(int endIndex, int commandCount)
481514
{
482515
var entries = new List<IUpdateEntry>();

test/EFCore.Relational.Specification.Tests/Update/StoredProcedureUpdateTestBase.cs

+42
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,48 @@ protected async Task Tpc(bool async, string createSprocSql)
10281028
}
10291029
}
10301030

1031+
[ConditionalTheory]
1032+
[MemberData(nameof(IsAsyncData))]
1033+
public abstract Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async);
1034+
1035+
protected async Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async, string createSprocSql)
1036+
{
1037+
var contextFactory = await InitializeAsync<DbContext>(
1038+
modelBuilder => modelBuilder.Entity<EntityWithAdditionalProperty>()
1039+
.InsertUsingStoredProcedure(
1040+
nameof(EntityWithAdditionalProperty) + "_Insert",
1041+
spb => spb
1042+
.HasParameter(w => w.Name)
1043+
.HasParameter(w => w.Id, pb => pb.IsOutput())
1044+
.HasParameter(w => w.AdditionalProperty))
1045+
.Property(e => e.AdditionalProperty).IsConcurrencyToken(),
1046+
seed: ctx => CreateStoredProcedures(ctx, createSprocSql));
1047+
1048+
await using var context = contextFactory.CreateContext();
1049+
1050+
// Prepare by adding an entity
1051+
var entity1 = new EntityWithAdditionalProperty { Name = "Entity1", AdditionalProperty = 1 };
1052+
context.Set<EntityWithAdditionalProperty>().Add(entity1);
1053+
1054+
using (TestSqlLoggerFactory.SuspendRecordingEvents())
1055+
{
1056+
await SaveChanges(context, async);
1057+
}
1058+
1059+
// Now add a second entity and update the first one. The update gets ordered first, and doesn't use a sproc, and then the insertion
1060+
// does.
1061+
var entity2 = new EntityWithAdditionalProperty { Name = "Entity2" };
1062+
context.Set<EntityWithAdditionalProperty>().Add(entity2);
1063+
entity1.Name = "Entity1_Modified";
1064+
entity1.AdditionalProperty = 2;
1065+
await SaveChanges(context, async);
1066+
1067+
using (TestSqlLoggerFactory.SuspendRecordingEvents())
1068+
{
1069+
Assert.Equal("Entity2", context.Set<EntityWithAdditionalProperty>().Single(b => b.Id == entity2.Id).Name);
1070+
}
1071+
}
1072+
10311073
/// <summary>
10321074
/// A method to be implement by the provider, to set up a store-generated concurrency token shadow property with the given name.
10331075
/// </summary>

test/EFCore.SqlServer.FunctionalTests/Update/StoredProcedureUpdateSqlServerTest.cs

+30
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,36 @@ AS BEGIN
665665
""");
666666
}
667667

668+
public override async Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async)
669+
{
670+
await base.Non_sproc_followed_by_sproc_commands_in_the_same_batch(
671+
async,
672+
"""
673+
CREATE PROCEDURE EntityWithAdditionalProperty_Insert(@Name varchar(max), @Id int OUT, @AdditionalProperty int)
674+
AS BEGIN
675+
INSERT INTO [EntityWithAdditionalProperty] ([Name], [AdditionalProperty]) VALUES (@Name, @AdditionalProperty);
676+
SET @Id = SCOPE_IDENTITY();
677+
END
678+
""");
679+
680+
AssertSql(
681+
"""
682+
@p2='1'
683+
@p0='2'
684+
@p3='1'
685+
@p1='Entity1_Modified' (Size = 4000)
686+
@p4='Entity2' (Size = 4000)
687+
@p5=NULL (Nullable = false) (Direction = Output) (DbType = Int32)
688+
@p6='0'
689+
690+
SET NOCOUNT ON;
691+
UPDATE [EntityWithAdditionalProperty] SET [AdditionalProperty] = @p0, [Name] = @p1
692+
OUTPUT 1
693+
WHERE [Id] = @p2 AND [AdditionalProperty] = @p3;
694+
EXEC [EntityWithAdditionalProperty_Insert] @p4, @p5 OUTPUT, @p6;
695+
""");
696+
}
697+
668698
protected override void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName)
669699
=> entityTypeBuilder.Property<byte[]>(propertyName).IsRowVersion();
670700

0 commit comments

Comments
 (0)