diff --git a/src/EFCore.Relational/Storage/IRelationalCommandBuilder.cs b/src/EFCore.Relational/Storage/IRelationalCommandBuilder.cs
index 5885ed35511..7a3da9c6924 100644
--- a/src/EFCore.Relational/Storage/IRelationalCommandBuilder.cs
+++ b/src/EFCore.Relational/Storage/IRelationalCommandBuilder.cs
@@ -30,6 +30,13 @@ public interface IRelationalCommandBuilder
/// The same builder instance so that multiple calls can be chained.
IRelationalCommandBuilder AddParameter(IRelationalParameter parameter);
+ ///
+ /// Removes the parameter with the given index from this command.
+ ///
+ /// The index of the parameter to be removed.
+ /// The same builder instance so that multiple calls can be chained.
+ IRelationalCommandBuilder RemoveParameterAt(int index);
+
///
/// The source for s to use.
///
diff --git a/src/EFCore.Relational/Storage/RelationalCommandBuilder.cs b/src/EFCore.Relational/Storage/RelationalCommandBuilder.cs
index 7f0f3084359..486184be4e5 100644
--- a/src/EFCore.Relational/Storage/RelationalCommandBuilder.cs
+++ b/src/EFCore.Relational/Storage/RelationalCommandBuilder.cs
@@ -3,19 +3,7 @@
namespace Microsoft.EntityFrameworkCore.Storage;
-///
-///
-/// Builds a command to be executed against a relational database.
-///
-///
-/// This type is typically used by database providers (and other extensions). It is generally
-/// not used in application code.
-///
-///
-///
-/// See Implementation of database providers and extensions
-/// for more information and examples.
-///
+///
public class RelationalCommandBuilder : IRelationalCommandBuilder
{
private readonly List _parameters = new();
@@ -42,17 +30,12 @@ public RelationalCommandBuilder(
///
protected virtual RelationalCommandBuilderDependencies Dependencies { get; }
- ///
- /// The source for s to use.
- ///
+ ///
[Obsolete("Code trying to add parameter should add type mapped parameter using TypeMappingSource directly.")]
public virtual IRelationalTypeMappingSource TypeMappingSource
=> Dependencies.TypeMappingSource;
- ///
- /// Creates the command.
- ///
- /// The newly created command.
+ ///
public virtual IRelationalCommand Build()
=> new RelationalCommand(Dependencies, _commandTextBuilder.ToString(), Parameters);
@@ -62,17 +45,11 @@ public virtual IRelationalCommand Build()
public override string ToString()
=> _commandTextBuilder.ToString();
- ///
- /// The collection of parameters.
- ///
+ ///
public virtual IReadOnlyList Parameters
=> _parameters;
- ///
- /// Adds the given parameter to this command.
- ///
- /// The parameter.
- /// The same builder instance so that multiple calls can be chained.
+ ///
public virtual IRelationalCommandBuilder AddParameter(IRelationalParameter parameter)
{
_parameters.Add(parameter);
@@ -80,11 +57,15 @@ public virtual IRelationalCommandBuilder AddParameter(IRelationalParameter param
return this;
}
- ///
- /// Appends an object to the command text.
- ///
- /// The object to be written.
- /// The same builder instance so that multiple calls can be chained.
+ ///
+ public virtual IRelationalCommandBuilder RemoveParameterAt(int index)
+ {
+ _parameters.RemoveAt(index);
+
+ return this;
+ }
+
+ ///
public virtual IRelationalCommandBuilder Append(string value)
{
_commandTextBuilder.Append(value);
@@ -92,10 +73,7 @@ public virtual IRelationalCommandBuilder Append(string value)
return this;
}
- ///
- /// Appends a blank line to the command text.
- ///
- /// The same builder instance so that multiple calls can be chained.
+ ///
public virtual IRelationalCommandBuilder AppendLine()
{
_commandTextBuilder.AppendLine();
@@ -103,10 +81,7 @@ public virtual IRelationalCommandBuilder AppendLine()
return this;
}
- ///
- /// Increments the indent of subsequent lines.
- ///
- /// The same builder instance so that multiple calls can be chained.
+ ///
public virtual IRelationalCommandBuilder IncrementIndent()
{
_commandTextBuilder.IncrementIndent();
@@ -114,10 +89,7 @@ public virtual IRelationalCommandBuilder IncrementIndent()
return this;
}
- ///
- /// Decrements the indent of subsequent lines.
- ///
- /// The same builder instance so that multiple calls can be chained.
+ ///
public virtual IRelationalCommandBuilder DecrementIndent()
{
_commandTextBuilder.DecrementIndent();
@@ -125,9 +97,7 @@ public virtual IRelationalCommandBuilder DecrementIndent()
return this;
}
- ///
- /// Gets the length of the command text.
- ///
+ ///
public virtual int CommandTextLength
=> _commandTextBuilder.Length;
}
diff --git a/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs b/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs
index aaed02217c6..88b0101dfb3 100644
--- a/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs
@@ -180,7 +180,7 @@ protected virtual int ConsumeResultSetWithPropagation(int startResultSetIndex, R
}
var tableModification = ModificationCommands[
- ResultsPositionalMappingEnabled?[resultSetIndex] == true
+ ResultsPositionalMappingEnabled?.Length > resultSetIndex && ResultsPositionalMappingEnabled[resultSetIndex]
? startResultSetIndex + reader.DbDataReader.GetInt32(reader.DbDataReader.FieldCount - 1)
: resultSetIndex];
@@ -231,7 +231,7 @@ protected virtual async Task ConsumeResultSetWithPropagationAsync(
}
var tableModification = ModificationCommands[
- ResultsPositionalMappingEnabled?[resultSetIndex] == true
+ ResultsPositionalMappingEnabled?.Length > resultSetIndex && ResultsPositionalMappingEnabled[resultSetIndex]
? startResultSetIndex + reader.DbDataReader.GetInt32(reader.DbDataReader.FieldCount - 1)
: resultSetIndex];
diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
index 7c5457b00fd..28e73094e27 100644
--- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
+++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
@@ -74,7 +74,7 @@ public CommandBatchPreparer(CommandBatchPreparerDependencies dependencies)
continue;
}
- if (!batch.AddCommand(modificationCommand))
+ if (!batch.TryAddCommand(modificationCommand))
{
if (batch.ModificationCommands.Count == 1
|| batch.ModificationCommands.Count >= _minBatchSize)
@@ -144,7 +144,7 @@ private ModificationCommandBatch StartNewBatch(
{
parameterNameGenerator.Reset();
var batch = Dependencies.ModificationCommandBatchFactory.Create();
- batch.AddCommand(modificationCommand);
+ batch.TryAddCommand(modificationCommand);
return batch;
}
diff --git a/src/EFCore.Relational/Update/ModificationCommandBatch.cs b/src/EFCore.Relational/Update/ModificationCommandBatch.cs
index 152e665be45..ba7e10efb11 100644
--- a/src/EFCore.Relational/Update/ModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/ModificationCommandBatch.cs
@@ -31,7 +31,7 @@ public abstract class ModificationCommandBatch
/// if the command was successfully added; if there was no
/// room in the current batch to add the command and it must instead be added to a new batch.
///
- public abstract bool AddCommand(IReadOnlyModificationCommand modificationCommand);
+ public abstract bool TryAddCommand(IReadOnlyModificationCommand modificationCommand);
///
/// Indicates that no more commands will be added to this batch, and prepares it for execution.
diff --git a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
index 7ada76329fe..3345156c344 100644
--- a/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
@@ -22,8 +22,10 @@ namespace Microsoft.EntityFrameworkCore.Update;
public abstract class ReaderModificationCommandBatch : ModificationCommandBatch
{
private readonly List _modificationCommands = new();
- private string? _finalCommandText;
+ private readonly int _batchHeaderLength;
+ private readonly List _pendingParameterNames = new();
private bool _requiresTransaction = true;
+ private int _sqlBuilderPosition, _commandResultSetCount, _resultsPositionalMappingEnabledLength;
///
/// Creates a new instance.
@@ -32,7 +34,12 @@ public abstract class ReaderModificationCommandBatch : ModificationCommandBatch
protected ReaderModificationCommandBatch(ModificationCommandBatchFactoryDependencies dependencies)
{
Dependencies = dependencies;
- CachedCommandText = new StringBuilder();
+
+ RelationalCommandBuilder = dependencies.CommandBuilderFactory.Create();
+
+ UpdateSqlGenerator = dependencies.UpdateSqlGenerator;
+ UpdateSqlGenerator.AppendBatchHeader(SqlBuilder);
+ _batchHeaderLength = SqlBuilder.Length;
}
///
@@ -43,18 +50,22 @@ protected ReaderModificationCommandBatch(ModificationCommandBatchFactoryDependen
///
/// The update SQL generator.
///
- protected virtual IUpdateSqlGenerator UpdateSqlGenerator
- => Dependencies.UpdateSqlGenerator;
+ protected virtual IUpdateSqlGenerator UpdateSqlGenerator { get; }
+
+ ///
+ /// Gets or sets the relational command builder for the commands in the batch.
+ ///
+ protected virtual IRelationalCommandBuilder RelationalCommandBuilder { get; }
///
- /// Gets or sets the cached command text for the commands in the batch.
+ /// Gets the command text builder for the commands in the batch.
///
- protected virtual StringBuilder CachedCommandText { get; set; }
+ protected virtual StringBuilder SqlBuilder { get; } = new();
///
- /// The ordinal of the last command for which command text was built.
+ /// Gets the parameter values for the commands in the batch.
///
- protected virtual int LastCachedCommandIndex { get; set; }
+ protected virtual Dictionary ParameterValues { get; } = new();
///
/// The list of conceptual insert/update/delete s in the batch.
@@ -75,66 +86,82 @@ public override IReadOnlyList ModificationCommands
protected virtual BitArray? ResultsPositionalMappingEnabled { get; set; }
///
- /// Adds the given insert/update/delete to the batch.
+ /// The store command generated from this batch when is called.
+ ///
+ protected virtual RawSqlCommand? StoreCommand { get; set; }
+
+ ///
+ /// Attempts to adds the given insert/update/delete to the batch.
///
/// The command to add.
///
/// if the command was successfully added; if there was no
/// room in the current batch to add the command and it must instead be added to a new batch.
///
- public override bool AddCommand(IReadOnlyModificationCommand modificationCommand)
+ public override bool TryAddCommand(IReadOnlyModificationCommand modificationCommand)
{
- if (_finalCommandText is not null)
+ if (StoreCommand is not null)
{
throw new InvalidOperationException(RelationalStrings.ModificationCommandBatchAlreadyComplete);
}
- if (ModificationCommands.Count == 0)
- {
- ResetCommandText();
- }
-
if (!CanAddCommand(modificationCommand))
{
return false;
}
- _modificationCommands.Add(modificationCommand);
- CommandResultSet.Add(ResultSetMapping.LastInResultSet);
+ _sqlBuilderPosition = SqlBuilder.Length;
+ _commandResultSetCount = CommandResultSet.Count;
+ _pendingParameterNames.Clear();
+ _resultsPositionalMappingEnabledLength = ResultsPositionalMappingEnabled?.Length ?? 0;
+
+ AddCommand(modificationCommand);
- if (!IsCommandTextValid())
+ // Check if the batch is still valid after having added the command (e.g. have we bypassed a maximum CommandText size?)
+ // A batch with only one command is always considered valid (otherwise we'd get an endless loop); allow the batch to fail
+ // server-side.
+ if (IsBatchValid() || _modificationCommands.Count == 0)
{
- ResetCommandText();
- _modificationCommands.RemoveAt(_modificationCommands.Count - 1);
- CommandResultSet.RemoveAt(CommandResultSet.Count - 1);
- return false;
+ _modificationCommands.Add(modificationCommand);
+
+ return true;
}
- return true;
+ RollbackLastCommand();
+
+ return false;
}
///
- /// Resets the builder to start building a new batch.
+ /// Rolls back the last command added. Used when adding a command caused the batch to become invalid (e.g. CommandText too long).
///
- protected virtual void ResetCommandText()
+ protected virtual void RollbackLastCommand()
{
- CachedCommandText.Clear();
+ SqlBuilder.Length = _sqlBuilderPosition;
- UpdateSqlGenerator.AppendBatchHeader(CachedCommandText);
- _batchHeaderLength = CachedCommandText.Length;
+ while (CommandResultSet.Count > _commandResultSetCount)
+ {
+ CommandResultSet.RemoveAt(CommandResultSet.Count - 1);
+ }
- SetRequiresTransaction(true);
+ if (ResultsPositionalMappingEnabled is not null)
+ {
+ ResultsPositionalMappingEnabled.Length = _resultsPositionalMappingEnabledLength;
+ }
- LastCachedCommandIndex = -1;
- }
+ foreach (var pendingParameterName in _pendingParameterNames)
+ {
+ ParameterValues.Remove(pendingParameterName);
- private int _batchHeaderLength;
+ RelationalCommandBuilder.RemoveParameterAt(RelationalCommandBuilder.Parameters.Count - 1);
+ }
+ }
///
/// Whether any SQL has already been added to the batch command text.
///
- protected virtual bool IsCachedCommandTextEmpty
- => CachedCommandText.Length == _batchHeaderLength;
+ protected virtual bool IsCommandTextEmpty
+ => SqlBuilder.Length == _batchHeaderLength;
///
public override bool RequiresTransaction
@@ -158,153 +185,132 @@ protected virtual void SetRequiresTransaction(bool requiresTransaction)
/// Checks whether the command text is valid.
///
/// if the command text is valid; otherwise.
- protected abstract bool IsCommandTextValid();
-
- ///
- /// Processes all unprocessed commands in the batch, making sure their corresponding SQL is populated in
- /// .
- ///
- protected virtual void UpdateCachedCommandText()
- {
- for (var i = LastCachedCommandIndex + 1; i < ModificationCommands.Count; i++)
- {
- UpdateCachedCommandText(i);
- }
- }
+ protected abstract bool IsBatchValid();
///
- /// Updates the command text for the command at the given position in the
- /// list.
+ /// Adds Updates the command text for the command at the given position in the list.
///
- /// The position of the command to generate command text for.
- protected virtual void UpdateCachedCommandText(int commandPosition)
+ /// The command to add.
+ protected virtual void AddCommand(IReadOnlyModificationCommand modificationCommand)
{
- var newModificationCommand = ModificationCommands[commandPosition];
-
bool requiresTransaction;
- switch (newModificationCommand.EntityState)
+ var commandPosition = CommandResultSet.Count;
+
+ switch (modificationCommand.EntityState)
{
case EntityState.Added:
- CommandResultSet[commandPosition] =
+ CommandResultSet.Add(
UpdateSqlGenerator.AppendInsertOperation(
- CachedCommandText, newModificationCommand, commandPosition, out requiresTransaction);
+ SqlBuilder, modificationCommand, commandPosition, out requiresTransaction));
break;
case EntityState.Modified:
- CommandResultSet[commandPosition] =
+ CommandResultSet.Add(
UpdateSqlGenerator.AppendUpdateOperation(
- CachedCommandText, newModificationCommand, commandPosition, out requiresTransaction);
+ SqlBuilder, modificationCommand, commandPosition, out requiresTransaction));
break;
case EntityState.Deleted:
- CommandResultSet[commandPosition] =
+ CommandResultSet.Add(
UpdateSqlGenerator.AppendDeleteOperation(
- CachedCommandText, newModificationCommand, commandPosition, out requiresTransaction);
+ SqlBuilder, modificationCommand, commandPosition, out requiresTransaction));
break;
default:
throw new InvalidOperationException(
RelationalStrings.ModificationCommandInvalidEntityState(
- newModificationCommand.Entries[0].EntityType,
- newModificationCommand.EntityState));
+ modificationCommand.Entries[0].EntityType,
+ modificationCommand.EntityState));
}
- _requiresTransaction = commandPosition > 0 || requiresTransaction;
+ AddParameters(modificationCommand);
- LastCachedCommandIndex = commandPosition;
+ _requiresTransaction = commandPosition > 0 || requiresTransaction;
}
- ///
- /// Gets the total number of parameters needed for the batch.
- ///
- /// The total parameter count.
- protected virtual int GetParameterCount()
- => ModificationCommands.Sum(c => c.ColumnModifications.Count);
-
///
public override void Complete()
{
- UpdateCachedCommandText();
+ if (StoreCommand is not null)
+ {
+ throw new InvalidOperationException(RelationalStrings.ModificationCommandBatchAlreadyComplete);
+ }
// Some database have a mode where autocommit is off, and so executing a command outside of an explicit transaction implicitly
// creates a new transaction (which needs to be explicitly committed).
// The below is a hook for allowing providers to turn autocommit on, in case it's off.
if (!RequiresTransaction)
{
- UpdateSqlGenerator.PrependEnsureAutocommit(CachedCommandText);
+ UpdateSqlGenerator.PrependEnsureAutocommit(SqlBuilder);
}
- _finalCommandText = CachedCommandText.ToString();
+ RelationalCommandBuilder.Append(SqlBuilder.ToString());
+
+ StoreCommand = new RawSqlCommand(RelationalCommandBuilder.Build(), ParameterValues);
}
///
- /// Generates a for the batch.
+ /// Adds parameters for all column modifications in the given to the relational command
+ /// being built for this batch.
///
- /// The command.
- protected virtual RawSqlCommand CreateStoreCommand()
+ /// The modification command for which to add parameters.
+ protected virtual void AddParameters(IReadOnlyModificationCommand modificationCommand)
{
- Check.DebugAssert(_finalCommandText is not null, "_finalCommandText is not null, checked in Execute");
+ foreach (var columnModification in modificationCommand.ColumnModifications)
+ {
+ AddParameter(columnModification);
+ }
+ }
+
+ ///
+ /// Adds a parameter for the given to the relational command being built for this batch.
+ ///
+ /// The column modification for which to add parameters.
+ protected virtual void AddParameter(IColumnModification columnModification)
+ {
+ if (columnModification.UseCurrentValueParameter)
+ {
+ RelationalCommandBuilder.AddParameter(
+ columnModification.ParameterName,
+ Dependencies.SqlGenerationHelper.GenerateParameterName(columnModification.ParameterName),
+ columnModification.TypeMapping!,
+ columnModification.IsNullable);
- var commandBuilder = Dependencies.CommandBuilderFactory
- .Create()
- .Append(_finalCommandText);
+ ParameterValues.Add(columnModification.ParameterName, columnModification.Value);
- var parameterValues = new Dictionary(GetParameterCount());
+ _pendingParameterNames.Add(columnModification.ParameterName);
+ }
- // ReSharper disable once ForCanBeConvertedToForeach
- for (var commandIndex = 0; commandIndex < ModificationCommands.Count; commandIndex++)
+ if (columnModification.UseOriginalValueParameter)
{
- var command = ModificationCommands[commandIndex];
- // ReSharper disable once ForCanBeConvertedToForeach
- for (var columnIndex = 0; columnIndex < command.ColumnModifications.Count; columnIndex++)
- {
- var columnModification = command.ColumnModifications[columnIndex];
- if (columnModification.UseCurrentValueParameter)
- {
- commandBuilder.AddParameter(
- columnModification.ParameterName,
- Dependencies.SqlGenerationHelper.GenerateParameterName(columnModification.ParameterName),
- columnModification.TypeMapping!,
- columnModification.IsNullable);
-
- parameterValues.Add(columnModification.ParameterName, columnModification.Value);
- }
-
- if (columnModification.UseOriginalValueParameter)
- {
- commandBuilder.AddParameter(
- columnModification.OriginalParameterName,
- Dependencies.SqlGenerationHelper.GenerateParameterName(columnModification.OriginalParameterName),
- columnModification.TypeMapping!,
- columnModification.IsNullable);
-
- parameterValues.Add(columnModification.OriginalParameterName, columnModification.OriginalValue);
- }
- }
- }
+ RelationalCommandBuilder.AddParameter(
+ columnModification.OriginalParameterName,
+ Dependencies.SqlGenerationHelper.GenerateParameterName(columnModification.OriginalParameterName),
+ columnModification.TypeMapping!,
+ columnModification.IsNullable);
+
+ ParameterValues.Add(columnModification.OriginalParameterName, columnModification.OriginalValue);
- return new RawSqlCommand(commandBuilder.Build(), parameterValues);
+ _pendingParameterNames.Add(columnModification.OriginalParameterName);
+ }
}
///
- /// Executes the command generated by against a
- /// database using the given connection.
+ /// Executes the command generated by this batch against a database using the given connection.
///
/// The connection to the database to update.
public override void Execute(IRelationalConnection connection)
{
- if (_finalCommandText is null)
+ if (StoreCommand is null)
{
throw new InvalidOperationException(RelationalStrings.ModificationCommandBatchNotComplete);
}
- var storeCommand = CreateStoreCommand();
-
try
{
- using var dataReader = storeCommand.RelationalCommand.ExecuteReader(
+ using var dataReader = StoreCommand.RelationalCommand.ExecuteReader(
new RelationalCommandParameterObject(
connection,
- storeCommand.ParameterValues,
+ StoreCommand.ParameterValues,
null,
Dependencies.CurrentContext.Context,
Dependencies.Logger, CommandSource.SaveChanges));
@@ -320,8 +326,7 @@ public override void Execute(IRelationalConnection connection)
}
///
- /// Executes the command generated by against a
- /// database using the given connection.
+ /// Executes the command generated by this batch against a database using the given connection.
///
/// The connection to the database to update.
/// A to observe while waiting for the task to complete.
@@ -331,19 +336,17 @@ public override async Task ExecuteAsync(
IRelationalConnection connection,
CancellationToken cancellationToken = default)
{
- if (_finalCommandText is null)
+ if (StoreCommand is null)
{
throw new InvalidOperationException(RelationalStrings.ModificationCommandBatchNotComplete);
}
- var storeCommand = CreateStoreCommand();
-
try
{
- var dataReader = await storeCommand.RelationalCommand.ExecuteReaderAsync(
+ var dataReader = await StoreCommand.RelationalCommand.ExecuteReaderAsync(
new RelationalCommandParameterObject(
connection,
- storeCommand.ParameterValues,
+ StoreCommand.ParameterValues,
null,
Dependencies.CurrentContext.Context,
Dependencies.Logger, CommandSource.SaveChanges),
diff --git a/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs b/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs
index 9af3e5e3704..96ef63d97a9 100644
--- a/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs
+++ b/src/EFCore.Relational/Update/SingularModificationCommandBatch.cs
@@ -41,6 +41,6 @@ protected override bool CanAddCommand(IReadOnlyModificationCommand modificationC
///
///
///
- protected override bool IsCommandTextValid()
+ protected override bool IsBatchValid()
=> true;
}
diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
index 2d2429f88c6..2e7143cd909 100644
--- a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
+++ b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
-using System.Text;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
@@ -20,10 +19,8 @@ public class SqlServerModificationCommandBatch : AffectedCountModificationComman
private const int MaxScriptLength = 65536 * DefaultNetworkPacketSizeBytes / 2;
private const int MaxParameterCount = 2100;
private const int MaxRowCount = 1000;
- private int _parameterCount = 1; // Implicit parameter for the command text
private readonly int _maxBatchSize;
- private readonly List _bulkInsertCommands = new();
- private int _commandsLeftToLengthCheck = 50;
+ private readonly List _pendingBulkInsertCommands = new();
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -36,8 +33,7 @@ public SqlServerModificationCommandBatch(
int? maxBatchSize)
: base(dependencies)
{
- if (maxBatchSize.HasValue
- && maxBatchSize.Value <= 0)
+ if (maxBatchSize is <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxBatchSize), RelationalStrings.InvalidMaxBatchSize(maxBatchSize.Value));
}
@@ -61,22 +57,7 @@ public SqlServerModificationCommandBatch(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override bool CanAddCommand(IReadOnlyModificationCommand modificationCommand)
- {
- if (ModificationCommands.Count >= _maxBatchSize)
- {
- return false;
- }
-
- var additionalParameterCount = CountParameters(modificationCommand);
-
- if (_parameterCount + additionalParameterCount >= MaxParameterCount)
- {
- return false;
- }
-
- _parameterCount += additionalParameterCount;
- return true;
- }
+ => ModificationCommands.Count < _maxBatchSize;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -84,53 +65,15 @@ protected override bool CanAddCommand(IReadOnlyModificationCommand modificationC
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- protected override bool IsCommandTextValid()
+ protected override void RollbackLastCommand()
{
- if (--_commandsLeftToLengthCheck < 0)
+ if (_pendingBulkInsertCommands.Count > 0)
{
- UpdateCachedCommandText();
- var commandTextLength = CachedCommandText.Length;
- if (commandTextLength >= MaxScriptLength)
- {
- return false;
- }
-
- var averageCommandLength = commandTextLength / ModificationCommands.Count;
- var expectedAdditionalCommandCapacity = (MaxScriptLength - commandTextLength) / averageCommandLength;
- _commandsLeftToLengthCheck = Math.Max(1, expectedAdditionalCommandCapacity / 4);
- }
-
- return true;
- }
-
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- protected override int GetParameterCount()
- => _parameterCount;
-
- private static int CountParameters(IReadOnlyModificationCommand modificationCommand)
- {
- var parameterCount = 0;
- // ReSharper disable once ForCanBeConvertedToForeach
- for (var columnIndex = 0; columnIndex < modificationCommand.ColumnModifications.Count; columnIndex++)
- {
- var columnModification = modificationCommand.ColumnModifications[columnIndex];
- if (columnModification.UseCurrentValueParameter)
- {
- parameterCount++;
- }
-
- if (columnModification.UseOriginalValueParameter)
- {
- parameterCount++;
- }
+ _pendingBulkInsertCommands.RemoveAt(_pendingBulkInsertCommands.Count - 1);
+ return;
}
- return parameterCount;
+ base.RollbackLastCommand();
}
///
@@ -139,24 +82,24 @@ private static int CountParameters(IReadOnlyModificationCommand modificationComm
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- protected override void ResetCommandText()
- {
- base.ResetCommandText();
-
- _bulkInsertCommands.Clear();
- }
+ protected override bool IsBatchValid()
+ => SqlBuilder.Length < MaxScriptLength
+ // A single implicit parameter for the command text itself
+ && ParameterValues.Count + 1 < MaxParameterCount;
- private void AppendBulkInsertCommandText(int lastIndex)
+ private void ApplyPendingBulkInsertCommands()
{
- if (_bulkInsertCommands.Count == 0)
+ if (_pendingBulkInsertCommands.Count == 0)
{
return;
}
- var wasCachedCommandTextEmpty = IsCachedCommandTextEmpty;
+ var commandPosition = CommandResultSet.Count;
+
+ var wasCachedCommandTextEmpty = IsCommandTextEmpty;
var resultSetMapping = UpdateSqlGenerator.AppendBulkInsertOperation(
- CachedCommandText, _bulkInsertCommands, lastIndex - _bulkInsertCommands.Count, out var resultsContainPositionMapping,
+ SqlBuilder, _pendingBulkInsertCommands, commandPosition, out var resultsContainPositionMapping,
out var requiresTransaction);
SetRequiresTransaction(!wasCachedCommandTextEmpty || requiresTransaction);
@@ -165,27 +108,29 @@ private void AppendBulkInsertCommandText(int lastIndex)
{
if (ResultsPositionalMappingEnabled is null)
{
- ResultsPositionalMappingEnabled = new BitArray(CommandResultSet.Count);
+ ResultsPositionalMappingEnabled = new BitArray(CommandResultSet.Count + _pendingBulkInsertCommands.Count);
}
else
{
- ResultsPositionalMappingEnabled.Length = CommandResultSet.Count;
+ ResultsPositionalMappingEnabled.Length = CommandResultSet.Count + _pendingBulkInsertCommands.Count;
}
- for (var i = lastIndex - _bulkInsertCommands.Count; i < lastIndex; i++)
+ for (var i = commandPosition; i < commandPosition + _pendingBulkInsertCommands.Count; i++)
{
ResultsPositionalMappingEnabled![i] = true;
}
}
- for (var i = lastIndex - _bulkInsertCommands.Count; i < lastIndex; i++)
+ foreach (var pendingCommand in _pendingBulkInsertCommands)
{
- CommandResultSet[i] = resultSetMapping;
+ AddParameters(pendingCommand);
+
+ CommandResultSet.Add(resultSetMapping);
}
if (resultSetMapping != ResultSetMapping.NoResultSet)
{
- CommandResultSet[lastIndex - 1] = ResultSetMapping.LastInResultSet;
+ CommandResultSet[^1] = ResultSetMapping.LastInResultSet;
}
}
@@ -195,50 +140,33 @@ private void AppendBulkInsertCommandText(int lastIndex)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- protected override void UpdateCachedCommandText()
+ protected override void AddCommand(IReadOnlyModificationCommand modificationCommand)
{
- base.UpdateCachedCommandText();
-
- AppendBulkInsertCommandText(ModificationCommands.Count);
- }
-
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- protected override void UpdateCachedCommandText(int commandPosition)
- {
- var newModificationCommand = ModificationCommands[commandPosition];
-
- if (newModificationCommand.EntityState == EntityState.Added)
+ if (modificationCommand.EntityState == EntityState.Added)
{
- if (_bulkInsertCommands.Count > 0
- && !CanBeInsertedInSameStatement(_bulkInsertCommands[0], newModificationCommand))
+ if (_pendingBulkInsertCommands.Count > 0
+ && !CanBeInsertedInSameStatement(_pendingBulkInsertCommands[0], modificationCommand))
{
// The new Add command cannot be added to the pending bulk insert commands (e.g. different table).
// Write out the pending commands before starting a new pending chain.
- AppendBulkInsertCommandText(commandPosition);
- _bulkInsertCommands.Clear();
+ ApplyPendingBulkInsertCommands();
+ _pendingBulkInsertCommands.Clear();
}
- _bulkInsertCommands.Add(newModificationCommand);
-
- LastCachedCommandIndex = commandPosition;
+ _pendingBulkInsertCommands.Add(modificationCommand);
}
else
{
// If we have any pending bulk insert commands, write them out before the next non-Add command
- if (_bulkInsertCommands.Count > 0)
+ if (_pendingBulkInsertCommands.Count > 0)
{
// Note that we don't care about the transactionality of the bulk insert SQL, since there's the additional non-Add
// command coming right afterwards, and so a transaction is required in any case.
- AppendBulkInsertCommandText(commandPosition);
- _bulkInsertCommands.Clear();
+ ApplyPendingBulkInsertCommands();
+ _pendingBulkInsertCommands.Clear();
}
- base.UpdateCachedCommandText(commandPosition);
+ base.AddCommand(modificationCommand);
}
}
@@ -252,6 +180,19 @@ private static bool CanBeInsertedInSameStatement(
&& firstCommand.ColumnModifications.Where(o => o.IsRead).Select(o => o.ColumnName).SequenceEqual(
secondCommand.ColumnModifications.Where(o => o.IsRead).Select(o => o.ColumnName));
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override void Complete()
+ {
+ ApplyPendingBulkInsertCommands();
+
+ base.Complete();
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs
index f4848969fe0..4cee040d567 100644
--- a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs
+++ b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs
@@ -20,16 +20,15 @@ public void AddCommand_adds_command_if_possible()
var command = CreateModificationCommand("T1", null, true, columnModifications: null);
var batch = new ModificationCommandBatchFake();
- batch.AddCommand(command);
+ batch.TryAddCommand(command);
batch.ShouldAddCommand = true;
- batch.ShouldValidateSql = true;
- batch.AddCommand(command);
+ batch.TryAddCommand(command);
batch.Complete();
Assert.Equal(2, batch.ModificationCommands.Count);
Assert.Same(command, batch.ModificationCommands[0]);
- Assert.Equal("..", batch.CommandText);
+ Assert.Equal(2, batch.FakeSqlGenerator.AppendUpdateOperationCalls);
}
[ConditionalFact]
@@ -38,15 +37,14 @@ public void AddCommand_does_not_add_command_if_not_possible()
var command = CreateModificationCommand("T1", null, true, columnModifications: null);
var batch = new ModificationCommandBatchFake();
- batch.AddCommand(command);
+ batch.TryAddCommand(command);
batch.ShouldAddCommand = false;
- batch.ShouldValidateSql = true;
- batch.AddCommand(command);
+ batch.TryAddCommand(command);
batch.Complete();
Assert.Equal(1, batch.ModificationCommands.Count);
- Assert.Equal(".", batch.CommandText);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendUpdateOperationCalls);
}
[ConditionalFact]
@@ -55,15 +53,20 @@ public void AddCommand_does_not_add_command_if_resulting_sql_is_invalid()
var command = CreateModificationCommand("T1", null, true, columnModifications: null);
var batch = new ModificationCommandBatchFake();
- batch.AddCommand(command);
+ batch.TryAddCommand(command);
batch.ShouldAddCommand = true;
- batch.ShouldValidateSql = false;
+ batch.MaxValidSql = batch.CommandText.Length;
- batch.AddCommand(command);
+ batch.TryAddCommand(command);
batch.Complete();
Assert.Equal(1, batch.ModificationCommands.Count);
- Assert.Equal(".", batch.CommandText);
+ Assert.Equal(@"UPDATE ""T1"" SET ;
+SELECT provider_specific_rowcount();
+
+",
+ batch.CommandText,
+ ignoreLineEndingDifferences: true);
}
[ConditionalFact]
@@ -74,16 +77,12 @@ public void UpdateCommandText_compiles_inserts()
var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null);
command.AddEntry(entry, true);
- var fakeSqlGenerator = new FakeSqlGenerator(
- RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService());
- var batch = new ModificationCommandBatchFake(fakeSqlGenerator);
- batch.AddCommand(command);
+ var batch = new ModificationCommandBatchFake();
+ batch.TryAddCommand(command);
batch.Complete();
- batch.UpdateCachedCommandTextBase(0);
-
- Assert.Equal(1, fakeSqlGenerator.AppendBatchHeaderCalls);
- Assert.Equal(1, fakeSqlGenerator.AppendInsertOperationCalls);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendBatchHeaderCalls);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendInsertOperationCalls);
}
[ConditionalFact]
@@ -94,16 +93,12 @@ public void UpdateCommandText_compiles_updates()
var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null);
command.AddEntry(entry, true);
- var fakeSqlGenerator = new FakeSqlGenerator(
- RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService());
- var batch = new ModificationCommandBatchFake(fakeSqlGenerator);
- batch.AddCommand(command);
-
- batch.UpdateCachedCommandTextBase(0);
+ var batch = new ModificationCommandBatchFake();
+ batch.TryAddCommand(command);
batch.Complete();
- Assert.Equal(1, fakeSqlGenerator.AppendBatchHeaderCalls);
- Assert.Equal(1, fakeSqlGenerator.AppendUpdateOperationCalls);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendBatchHeaderCalls);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendUpdateOperationCalls);
}
[ConditionalFact]
@@ -114,16 +109,12 @@ public void UpdateCommandText_compiles_deletes()
var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null);
command.AddEntry(entry, true);
- var fakeSqlGenerator = new FakeSqlGenerator(
- RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService());
- var batch = new ModificationCommandBatchFake(fakeSqlGenerator);
- batch.AddCommand(command);
-
- batch.UpdateCachedCommandTextBase(0);
+ var batch = new ModificationCommandBatchFake();
+ batch.TryAddCommand(command);
batch.Complete();
- Assert.Equal(1, fakeSqlGenerator.AppendBatchHeaderCalls);
- Assert.Equal(1, fakeSqlGenerator.AppendDeleteOperationCalls);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendBatchHeaderCalls);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendDeleteOperationCalls);
}
[ConditionalFact]
@@ -131,25 +122,25 @@ public void UpdateCommandText_compiles_multiple_commands()
{
var entry = CreateEntry(EntityState.Added);
- var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null);
- command.AddEntry(entry, true);
+ var parameterNameGenerator = new ParameterNameGenerator();
+ var command1 = CreateModificationCommand("T1", null, parameterNameGenerator.GenerateNext, true, null);
+ command1.AddEntry(entry, true);
+ var command2 = CreateModificationCommand("T1", null, parameterNameGenerator.GenerateNext, true, null);
+ command2.AddEntry(entry, true);
- var fakeSqlGenerator = new FakeSqlGenerator(
- RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService());
- var batch = new ModificationCommandBatchFake(fakeSqlGenerator);
- batch.AddCommand(command);
- batch.AddCommand(command);
+ var batch = new ModificationCommandBatchFake();
+ batch.TryAddCommand(command1);
+ batch.TryAddCommand(command2);
batch.Complete();
- Assert.Equal("..", batch.CommandText);
-
- Assert.Equal(1, fakeSqlGenerator.AppendBatchHeaderCalls);
+ Assert.Equal(1, batch.FakeSqlGenerator.AppendBatchHeaderCalls);
+ Assert.Equal(2, batch.FakeSqlGenerator.AppendInsertOperationCalls);
}
[ConditionalFact]
public async Task ExecuteAsync_executes_batch_commands_and_consumes_reader()
{
- var entry = CreateEntry(EntityState.Added);
+ var entry = CreateEntry(EntityState.Added, generateKeyValues: true);
var command = CreateModificationCommand("T1", null, new ParameterNameGenerator().GenerateNext, true, null);
command.AddEntry(entry, true);
@@ -159,7 +150,7 @@ public async Task ExecuteAsync_executes_batch_commands_and_consumes_reader()
var connection = CreateConnection(dbDataReader);
var batch = new ModificationCommandBatchFake();
- batch.AddCommand(command);
+ batch.TryAddCommand(command);
batch.Complete();
await batch.ExecuteAsync(connection);
@@ -182,7 +173,7 @@ public async Task ExecuteAsync_saves_store_generated_values()
new[] { "Col1" }, new List