diff --git a/src/Hrimsoft.SqlBulk.PostgreSql/Extensions/PropertyProfileExtensions.cs b/src/Hrimsoft.SqlBulk.PostgreSql/Extensions/PropertyProfileExtensions.cs
index 303900b..7d1f0e8 100644
--- a/src/Hrimsoft.SqlBulk.PostgreSql/Extensions/PropertyProfileExtensions.cs
+++ b/src/Hrimsoft.SqlBulk.PostgreSql/Extensions/PropertyProfileExtensions.cs
@@ -11,6 +11,26 @@ namespace Hrimsoft.SqlBulk.PostgreSql
///
public static class PropertyProfileExtensions
{
+ ///
+ /// Will the property value getter execute dynamic invoke or not.
+ ///
+ /// Returns True if it will use dynamic invoke, otherwise returns False.
+ public static bool IsDynamicallyInvoked(this PropertyProfile profile)
+ {
+ switch (profile.DbColumnType)
+ {
+ case NpgsqlDbType.Bigint:
+ case NpgsqlDbType.Integer:
+ case NpgsqlDbType.Real:
+ case NpgsqlDbType.Double:
+ case NpgsqlDbType.Numeric:
+ case NpgsqlDbType.Boolean:
+ case NpgsqlDbType.Smallint:
+ return false;
+ }
+ return true;
+ }
+
/// Calculates property values of item
/// item with values
/// information about which property it is needed to get value
diff --git a/src/Hrimsoft.SqlBulk.PostgreSql/IoC/BulkServiceExtensions.cs b/src/Hrimsoft.SqlBulk.PostgreSql/IoC/BulkServiceExtensions.cs
index 6dc3857..9acb364 100644
--- a/src/Hrimsoft.SqlBulk.PostgreSql/IoC/BulkServiceExtensions.cs
+++ b/src/Hrimsoft.SqlBulk.PostgreSql/IoC/BulkServiceExtensions.cs
@@ -27,7 +27,9 @@ public static IServiceCollection AddPostgreSqlBulkService(this IServiceCollectio
services.AddSingleton(options);
services.AddTransient();
services.AddTransient();
- services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient(
diff --git a/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/DeleteSqlCommandMediator.cs b/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/DeleteSqlCommandMediator.cs
new file mode 100644
index 0000000..317fa2b
--- /dev/null
+++ b/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/DeleteSqlCommandMediator.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+
+namespace Hrimsoft.SqlBulk.PostgreSql
+{
+ ///
+ /// Mediator selects the best sql-command builder for the given entity type
+ ///
+ public class DeleteSqlCommandMediator : IDeleteSqlCommandBuilder
+ {
+ private readonly SimpleDeleteSqlCommandBuilder _simpleBuilder;
+ private readonly WhereInDeleteSqlCommandBuilder _whereInBuilder;
+ private readonly ILogger _logger;
+
+ public DeleteSqlCommandMediator(SimpleDeleteSqlCommandBuilder simpleBuilder,
+ WhereInDeleteSqlCommandBuilder whereInBuilder,
+ ILogger logger)
+ {
+ _simpleBuilder = simpleBuilder;
+ _whereInBuilder = whereInBuilder;
+ _logger = logger;
+ }
+
+ ///
+ public IList Generate(ICollection elements, EntityProfile entityProfile, CancellationToken cancellationToken) where TEntity : class
+ {
+ if (elements == null)
+ throw new ArgumentNullException(nameof(elements));
+ if (entityProfile == null)
+ throw new ArgumentNullException(nameof(entityProfile));
+
+ if (elements.Count == 0)
+ throw new ArgumentException("There is no elements in the collection. At least one element must be.", nameof(elements));
+
+ var privateKeys = entityProfile.Properties
+ .Values
+ .Where(x => x.IsPrivateKey)
+ .ToList();
+ if (privateKeys.Count == 0)
+ throw new ArgumentException($"Entity {entityProfile.EntityType.FullName} must have at least one private key.",
+ nameof(entityProfile));
+ if (privateKeys.Count > 1)
+ {
+ _logger.LogDebug($"Simple sql-delete command builder has been selected as there are more than one private keys in entity {entityProfile.EntityType.FullName}");
+ return _simpleBuilder.Generate(elements, entityProfile, cancellationToken);
+ }
+ _logger.LogDebug($"WhereIn sql-delete command builder has been selected as there is only one private key in entity {entityProfile.EntityType.FullName}");
+ return _whereInBuilder.Generate(elements, entityProfile, cancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/SimpleDeleteSqlCommandBuilder.cs b/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/SimpleDeleteSqlCommandBuilder.cs
index 28e559e..5f400d4 100644
--- a/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/SimpleDeleteSqlCommandBuilder.cs
+++ b/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/SimpleDeleteSqlCommandBuilder.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Text;
using System.Threading;
-using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Npgsql;
@@ -15,6 +14,8 @@ public class SimpleDeleteSqlCommandBuilder : IDeleteSqlCommandBuilder
{
private readonly ILogger _logger;
+ private const int MAX_PARAMS_PER_CMD = 65_535;
+
///
public SimpleDeleteSqlCommandBuilder(ILoggerFactory loggerFactory)
{
@@ -42,8 +43,9 @@ public IList Generate(ICollection ele
_logger.LogTrace($"Generating delete sql for {elements.Count} elements.");
- var result = "";
- var allItemsParameters = new List();
+ var result = new List();
+ var resultCommand = "";
+ var parameters = new List();
if (_logger.IsEnabled(LogLevel.Debug))
{
@@ -66,17 +68,13 @@ public IList Generate(ICollection ele
if (thereIsMoreElements)
{
allElementsAreNull = false;
- var (commandForOneItem, itemParameters)
- = GenerateForItem(entityProfile, elementsEnumerator.Current, null, elementIndex);
- allItemsParameters.AddRange(itemParameters);
-
- var entireCommandLength = commandForOneItem.Length * elements.Count;
- if (_logger.IsEnabled(LogLevel.Debug))
- _logger.LogDebug($"approximate entire command length: {entireCommandLength}");
+ var (commandForOneItem, itemParameters) = GenerateForItem(entityProfile, elementsEnumerator.Current, null, elementIndex);
+ parameters.AddRange(itemParameters);
- var resultBuilder = new StringBuilder(entireCommandLength);
- resultBuilder.AppendLine(commandForOneItem);
+ var paramsPerElement = itemParameters.Count;
+ var commandBuilder = new StringBuilder(commandForOneItem.Length * elements.Count - elementIndex);
+ commandBuilder.AppendLine(commandForOneItem);
while (elementsEnumerator.MoveNext())
{
@@ -84,30 +82,35 @@ public IList Generate(ICollection ele
// ignore all null items
if (elementsEnumerator.Current == null)
continue;
- (commandForOneItem, itemParameters)
- = GenerateForItem(entityProfile, elementsEnumerator.Current, resultBuilder, elementIndex);
-
- allItemsParameters.AddRange(itemParameters);
- resultBuilder.AppendLine(commandForOneItem);
+ if (parameters.Count + paramsPerElement > MAX_PARAMS_PER_CMD)
+ {
+ result.Add(new SqlCommandBuilderResult
+ {
+ Command = commandBuilder.ToString(),
+ Parameters = parameters,
+ IsThereReturningClause = false
+ });
+ commandBuilder.Clear();
+ parameters.Clear();
+ }
+ (commandForOneItem, itemParameters) = GenerateForItem(entityProfile, elementsEnumerator.Current, commandBuilder, elementIndex);
+ parameters.AddRange(itemParameters);
+ commandBuilder.AppendLine(commandForOneItem);
}
- result = resultBuilder.ToString();
+ resultCommand = commandBuilder.ToString();
if (_logger.IsEnabled(LogLevel.Debug))
- _logger.LogDebug($"result command: {result}");
+ _logger.LogDebug($"result command: {resultCommand}");
}
}
-
if (allElementsAreNull)
throw new ArgumentException($"There is no elements in the collection. At least one element must be.", nameof(elements));
-
- return new List
- {
- new SqlCommandBuilderResult
+ result.Add(new SqlCommandBuilderResult
{
- Command = result,
- Parameters = allItemsParameters,
+ Command = resultCommand,
+ Parameters = parameters,
IsThereReturningClause = false
- }
- };
+ });
+ return result;
}
///
@@ -118,8 +121,11 @@ public IList Generate(ICollection ele
/// Builder to which the generated for an item command will be appended
/// As this method is called for each item, this value will be added to the sql parameter name
/// Returns named tuple with generated command and list of db parameters.
- public (string Command, ICollection Parameters) GenerateForItem(EntityProfile entityProfile,
- TEntity item, StringBuilder externalBuilder, int elementIndex)
+ public (string Command, ICollection Parameters)
+ GenerateForItem(EntityProfile entityProfile,
+ TEntity item,
+ StringBuilder externalBuilder,
+ int elementIndex)
where TEntity : class
{
if (item == null)
diff --git a/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/WhereInDeleteSqlCommandBuilder.cs b/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/WhereInDeleteSqlCommandBuilder.cs
new file mode 100644
index 0000000..5a55209
--- /dev/null
+++ b/src/Hrimsoft.SqlBulk.PostgreSql/SqlCommandBuilders/Delete/WhereInDeleteSqlCommandBuilder.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Npgsql;
+
+namespace Hrimsoft.SqlBulk.PostgreSql
+{
+ ///
+ /// Generates bulk delete sql command with where clause using in operand
+ ///
+ public class WhereInDeleteSqlCommandBuilder : IDeleteSqlCommandBuilder
+ {
+ private readonly ILogger _logger;
+
+ private const int MAX_IN_CLAUSE_IDS = 1000;
+ private const int MAX_PARAMS_PER_CMD = 65_535;
+
+ ///
+ public WhereInDeleteSqlCommandBuilder(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ /// Generates bulk delete sql command with where clause using in operand
+ ///
+ /// elements that have to be deleted
+ /// elements type profile (contains mapping and other options)
+ ///
+ /// Returns a text of an sql delete command and collection of database parameters
+ public IList Generate(ICollection elements, EntityProfile entityProfile,
+ CancellationToken cancellationToken)
+ where TEntity : class
+ {
+ if (elements == null)
+ throw new ArgumentNullException(nameof(elements));
+ if (entityProfile == null)
+ throw new ArgumentNullException(nameof(entityProfile));
+
+ if (elements.Count == 0)
+ throw new ArgumentException($"There is no elements in the collection. At least one element must be.", nameof(elements));
+
+ var privateKeys = entityProfile.Properties
+ .Values
+ .Where(x => x.IsPrivateKey)
+ .ToList();
+ if (privateKeys.Count == 0)
+ throw new ArgumentException($"Entity {entityProfile.EntityType.FullName} must have at least one private key.",
+ nameof(entityProfile));
+ if (privateKeys.Count > 1)
+ throw new ArgumentException(
+ $"Cannot generate delete sql command with collapsed where clause as there are more than one private keys in the entity {entityProfile.EntityType.FullName}",
+ nameof(entityProfile));
+
+ _logger.LogTrace($"Generating delete sql for {elements.Count} elements.");
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.LogDebug($"{nameof(TEntity)}: {typeof(TEntity).FullName}");
+ _logger.LogDebug($"{nameof(elements)}.Count: {elements.Count}");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var commandHeader = $"delete from {entityProfile.TableName} where \"{privateKeys[0].DbColumnName}\" in (";
+
+ var result = new List();
+ var cmdBuilder = new StringBuilder();
+ var parameters = privateKeys[0].IsDynamicallyInvoked()
+ ? new List(Math.Min(elements.Count, MAX_PARAMS_PER_CMD))
+ : null;
+ cmdBuilder.Append(commandHeader);
+
+ var elementAbsIndex = -1;
+ var elementIndex = -1;
+ using (var elementsEnumerator = elements.GetEnumerator())
+ {
+ while (elementsEnumerator.MoveNext())
+ {
+ elementAbsIndex++;
+ if (elementsEnumerator.Current == null)
+ continue;
+ try
+ {
+ elementIndex++;
+ var whereDelimiter = elementIndex == 0 ? "" : ",";
+ if (parameters != null && parameters.Count + 1 > MAX_PARAMS_PER_CMD)
+ {
+ cmdBuilder.AppendLine(");");
+ result.Add(new SqlCommandBuilderResult
+ {
+ Command = cmdBuilder.ToString(),
+ Parameters = parameters,
+ IsThereReturningClause = false
+ });
+ cmdBuilder.Clear();
+ cmdBuilder.Append(commandHeader);
+ parameters.Clear();
+ whereDelimiter = "";
+ }
+ else if (elementIndex == MAX_IN_CLAUSE_IDS + 1)
+ {
+ cmdBuilder.AppendLine(");");
+ cmdBuilder.Append(commandHeader);
+ whereDelimiter = "";
+ }
+ var propValue = privateKeys[0].GetPropertyValueAsString(elementsEnumerator.Current);
+ if (parameters != null)
+ {
+ var paramName = $"@param_{privateKeys[0].DbColumnName}_{elementIndex}";
+ cmdBuilder.Append($"{whereDelimiter}{paramName}");
+ var value = privateKeys[0].GetPropertyValue(elementsEnumerator.Current);
+ if (value == null)
+ throw new ArgumentException($"Private key must not be null. property: {privateKeys[0].DbColumnName}, item index: {elementAbsIndex}",
+ nameof(elements));
+ parameters.Add(new NpgsqlParameter(paramName, privateKeys[0].DbColumnType)
+ {
+ Value = value
+ });
+ }
+ else
+ {
+ cmdBuilder.Append($"{whereDelimiter}{propValue}");
+ }
+ }
+ catch (Exception ex)
+ {
+ var message = $"an error occurred while calculating {privateKeys[0].DbColumnName} of item at index {elementAbsIndex}";
+ throw new SqlGenerationException(SqlOperation.Delete, message, ex);
+ }
+ }
+ }
+ if (elementIndex == -1)
+ throw new ArgumentException($"There is no elements in the collection. At least one element must be.", nameof(elements));
+
+ cmdBuilder.AppendLine(");");
+ result.Add(new SqlCommandBuilderResult
+ {
+ Command = cmdBuilder.ToString(),
+ Parameters = parameters ?? new List(),
+ IsThereReturningClause = false
+ });
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/tests/Hrimsoft.SqlBulk.PostgreSql.IntegrationTests/BulkDelete/SimpleBulkDeleteIntegrationTests.cs b/src/tests/Hrimsoft.SqlBulk.PostgreSql.IntegrationTests/BulkDelete/SimpleBulkDeleteIntegrationTests.cs
index e75c339..cea3d34 100644
--- a/src/tests/Hrimsoft.SqlBulk.PostgreSql.IntegrationTests/BulkDelete/SimpleBulkDeleteIntegrationTests.cs
+++ b/src/tests/Hrimsoft.SqlBulk.PostgreSql.IntegrationTests/BulkDelete/SimpleBulkDeleteIntegrationTests.cs
@@ -61,11 +61,9 @@ public async Task SimpleDelete_should_update_autogenerated_fields()
new TestEntity {RecordId = "rec-01", SensorId = "sens-01", Value = 127},
new TestEntity {RecordId = "rec-02", SensorId = "sens-01", Value = 128}
};
- using (var connection = new NpgsqlConnection(_configuration.ConnectionString))
- {
- await _testService.InsertAsync(connection, elements, CancellationToken.None);
- await _testService.DeleteAsync(connection, elements, CancellationToken.None);
- }
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile, new[] {elements[0].Id, elements[1].Id});
Assert.AreEqual(0, countOfRows);
@@ -81,11 +79,9 @@ public async Task SimpleDelete_should_split_correctly_even_number_of_elements()
new TestEntity {RecordId = "rec-01", SensorId = "sens-02", Value = 227},
new TestEntity {RecordId = "rec-02", SensorId = "sens-02", Value = 228}
};
- using (var connection = new NpgsqlConnection(_configuration.ConnectionString))
- {
- await _testService.InsertAsync(connection, elements, CancellationToken.None);
- await _testService.DeleteAsync(connection, elements, CancellationToken.None);
- }
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile, new[] {elements[0].Id, elements[1].Id, elements[2].Id, elements[3].Id});
Assert.AreEqual(0, countOfRows);
@@ -100,11 +96,9 @@ public async Task SimpleDelete_should_split_correctly_odd_number_of_elements()
new TestEntity {RecordId = "rec-02", SensorId = "sens-01", Value = 128},
new TestEntity {RecordId = "rec-01", SensorId = "sens-02", Value = 227},
};
- using (var connection = new NpgsqlConnection(_configuration.ConnectionString))
- {
- await _testService.InsertAsync(connection, elements, CancellationToken.None);
- await _testService.DeleteAsync(connection, elements, CancellationToken.None);
- }
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile, new[] {elements[0].Id, elements[1].Id, elements[2].Id});
Assert.AreEqual(0, countOfRows);
@@ -150,5 +144,23 @@ public async Task SimpleDelete_should_delete_elements_with_multiple_pk()
var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile, new[] {elements[0].Id, elements[1].Id, elements[2].Id});
Assert.AreEqual(0, countOfRows);
}
+
+ [Test]
+ public async Task SimpleDelete_should_delete_more_than_65K_elements()
+ {
+ const int MAX_ELEMENTS_COUNT = 70_000;
+
+ var elements = new List();
+ for (var i = 0; i < MAX_ELEMENTS_COUNT; i++)
+ {
+ elements.Add(new TestEntity {RecordId = $"rec-{i}", SensorId = $"sens-{i}", Value = 127+i});
+ }
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
+
+ var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile,elements.Select(x => x.Id).ToArray());
+ Assert.AreEqual(0, countOfRows);
+ }
}
}
\ No newline at end of file
diff --git a/src/tests/Hrimsoft.SqlBulk.PostgreSql.IntegrationTests/BulkDelete/WhereInDeleteIntegrationTests.cs b/src/tests/Hrimsoft.SqlBulk.PostgreSql.IntegrationTests/BulkDelete/WhereInDeleteIntegrationTests.cs
new file mode 100644
index 0000000..38b807e
--- /dev/null
+++ b/src/tests/Hrimsoft.SqlBulk.PostgreSql.IntegrationTests/BulkDelete/WhereInDeleteIntegrationTests.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Hrimsoft.SqlBulk.PostgreSql.IntegrationTests.TestModels;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Npgsql;
+using NUnit.Framework;
+
+namespace Hrimsoft.SqlBulk.PostgreSql.IntegrationTests.BulkDelete
+{
+ public class WhereInDeleteIntegrationTests
+ {
+ private readonly TestConfiguration _configuration;
+ private readonly TestUtils _testUtils;
+
+ private SimpleEntityProfile _entityProfile;
+ private NpgsqlCommandsBulkService _testService;
+
+ public WhereInDeleteIntegrationTests()
+ {
+ _configuration = new TestConfiguration();
+ _testUtils = new TestUtils(_configuration);
+ }
+
+ [SetUp]
+ public async Task SetUp()
+ {
+ var truncateTableCmd = "truncate \"unit_tests\".\"simple_test_entity\";";
+ var resetIdSequenceCmd = "ALTER SEQUENCE \"unit_tests\".\"simple_test_entity_id_seq\" RESTART WITH 1;";
+
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await using var command = new NpgsqlCommand($"{truncateTableCmd}{resetIdSequenceCmd}", connection);
+ await connection.OpenAsync();
+ await command.ExecuteNonQueryAsync();
+
+ var bulkServiceOptions = new BulkServiceOptions();
+ _entityProfile = new SimpleEntityProfile();
+ bulkServiceOptions.AddEntityProfile(_entityProfile);
+
+ var insertCommandBuilder = new InsertSqlCommandBuilder(NullLoggerFactory.Instance);
+ var updateCommandBuilder = new Mock().Object;
+ var deleteCommandBuilder = new WhereInDeleteSqlCommandBuilder(NullLogger.Instance);
+ var upsertCommandBuilder = new Mock().Object;
+
+ _testService = new NpgsqlCommandsBulkService(
+ bulkServiceOptions,
+ NullLoggerFactory.Instance,
+ insertCommandBuilder,
+ updateCommandBuilder,
+ deleteCommandBuilder,
+ upsertCommandBuilder);
+ }
+
+ [Test]
+ public async Task Should_update_autogenerated_fields()
+ {
+ var elements = new List
+ {
+ new TestEntity {RecordId = "rec-01", SensorId = "sens-01", Value = 127},
+ new TestEntity {RecordId = "rec-02", SensorId = "sens-01", Value = 128}
+ };
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
+
+ var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile, new[] {elements[0].Id, elements[1].Id});
+ Assert.AreEqual(0, countOfRows);
+ }
+
+ [Test]
+ public async Task Should_split_correctly_even_number_of_elements()
+ {
+ var elements = new List
+ {
+ new TestEntity {RecordId = "rec-01", SensorId = "sens-01", Value = 127},
+ new TestEntity {RecordId = "rec-02", SensorId = "sens-01", Value = 128},
+ new TestEntity {RecordId = "rec-01", SensorId = "sens-02", Value = 227},
+ new TestEntity {RecordId = "rec-02", SensorId = "sens-02", Value = 228}
+ };
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
+
+ var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile, new[] {elements[0].Id, elements[1].Id, elements[2].Id, elements[3].Id});
+ Assert.AreEqual(0, countOfRows);
+ }
+
+ [Test]
+ public async Task Should_split_correctly_odd_number_of_elements()
+ {
+ var elements = new List
+ {
+ new TestEntity {RecordId = "rec-01", SensorId = "sens-01", Value = 127},
+ new TestEntity {RecordId = "rec-02", SensorId = "sens-01", Value = 128},
+ new TestEntity {RecordId = "rec-01", SensorId = "sens-02", Value = 227},
+ };
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
+
+ var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile, new[] {elements[0].Id, elements[1].Id, elements[2].Id});
+ Assert.AreEqual(0, countOfRows);
+ }
+
+ [Test]
+ public async Task Should_delete_elements_with_multiple_pk()
+ {
+ var bulkServiceOptions = new BulkServiceOptions();
+ var profile = new EntityProfile(typeof(TestEntity));
+ profile.HasProperty(x => x.Id)
+ .ThatIsPrivateKey()
+ .ThatIsAutoGenerated();
+ profile.HasProperty(x => x.RecordId)
+ .ThatIsPrivateKey();
+ profile.HasProperty(x => x.Value);
+ profile.ToTable("simple_test_entity", "unit_tests");
+ bulkServiceOptions.AddEntityProfile(profile);
+
+ var insertCommandBuilder = new InsertSqlCommandBuilder(NullLoggerFactory.Instance);
+ var updateCommandBuilder = new Mock().Object;
+ var deleteCommandBuilder = new WhereInDeleteSqlCommandBuilder(NullLogger.Instance);
+ var upsertCommandBuilder = new Mock().Object;
+
+ _testService = new NpgsqlCommandsBulkService(
+ bulkServiceOptions,
+ NullLoggerFactory.Instance,
+ insertCommandBuilder,
+ updateCommandBuilder,
+ deleteCommandBuilder,
+ upsertCommandBuilder);
+
+ var elements = new List
+ {
+ new TestEntity {RecordId = "rec-01", SensorId = "sens-01", Value = 127},
+ new TestEntity {RecordId = "rec-02", SensorId = "sens-01", Value = 128},
+ new TestEntity {RecordId = "rec-01", SensorId = "sens-02", Value = 227},
+ };
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ var ex = Assert.ThrowsAsync>(() => _testService.DeleteAsync(connection, elements, CancellationToken.None));
+ Assert.IsTrue(ex.InnerException is ArgumentException);
+ }
+
+ [Test]
+ public async Task Should_delete_more_than_65K_elements()
+ {
+ const int MAX_ELEMENTS_COUNT = 70_000;
+
+ var elements = new List();
+ for (var i = 0; i < MAX_ELEMENTS_COUNT; i++)
+ {
+ elements.Add(new TestEntity {RecordId = $"rec-{i}", SensorId = $"sens-{i}", Value = 127+i});
+ }
+ await using var connection = new NpgsqlConnection(_configuration.ConnectionString);
+ await _testService.InsertAsync(connection, elements, CancellationToken.None);
+ await _testService.DeleteAsync(connection, elements, CancellationToken.None);
+
+ var countOfRows = await _testUtils.HowManyRowsWithIdsAsync(_entityProfile,elements.Select(x => x.Id).ToArray());
+ Assert.AreEqual(0, countOfRows);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/tests/Hrimsoft.SqlBulk.PostgreSql.Tests/DeleteSqlCommandBuilderTests/WhereInDeleteSqlCommandBuilderTests.cs b/src/tests/Hrimsoft.SqlBulk.PostgreSql.Tests/DeleteSqlCommandBuilderTests/WhereInDeleteSqlCommandBuilderTests.cs
new file mode 100644
index 0000000..46b3a02
--- /dev/null
+++ b/src/tests/Hrimsoft.SqlBulk.PostgreSql.Tests/DeleteSqlCommandBuilderTests/WhereInDeleteSqlCommandBuilderTests.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using Hrimsoft.SqlBulk.PostgreSql.Tests.TestModels;
+using Microsoft.Extensions.Logging.Abstractions;
+using NUnit.Framework;
+
+namespace Hrimsoft.SqlBulk.PostgreSql.Tests
+{
+ public class WhereInDeleteSqlCommandBuilderTests
+ {
+ private WhereInDeleteSqlCommandBuilder _testService;
+ private const string DELETE_PURE_PATTERN =
+ @"(delete\s+from\s+(""\w+"".)?""\w+""\s+where\s+""\w+""\s*in\s*\(\d+\s*(,\s*\d+)*\s*\);)+";
+ private const string DELETE_PARAM_PATTERN =
+ @"(delete\s+from\s+(""\w+"".)?""\w+""\s+where\s+""\w+""\s*in\s*\(@param_\w+_\d+\s*(,\s*@param_\w+_\d+)*\s*\);)+";
+
+ [SetUp]
+ public void SetUp()
+ {
+ _testService = new WhereInDeleteSqlCommandBuilder(NullLogger.Instance);
+ }
+
+ [Test]
+ public void Should_match_delete_cmd_without_returning_clause()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.Id)
+ .ThatIsPrivateKey();
+ entityProfile.HasProperty(x => x.RecordId);
+ entityProfile.HasProperty(x => x.SensorId);
+ entityProfile.ToTable("test_entity", "custom");
+
+ var elements = new List
+ {
+ new TestEntity{ Id=12, RecordId = "rec-01", SensorId = "sens-01", IntValue = 127 },
+ };
+ var commands = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(commands);
+ Assert.AreEqual(1, commands.Count);
+ var commandResult = commands.First();
+ Assert.NotNull(commandResult.Command);
+
+ Assert.IsTrue(Regex.IsMatch(commandResult.Command, DELETE_PURE_PATTERN, RegexOptions.IgnoreCase));
+ }
+
+ [Test]
+ public void Should_not_allow_generate_cmd_without_private_key()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.Id);
+ entityProfile.HasProperty(x => x.RecordId);
+ entityProfile.HasProperty(x => x.SensorId);
+ entityProfile.ToTable("test_entity", "custom");
+
+ var elements = new List
+ {
+ new TestEntity{ Id=12, RecordId = "rec-01", SensorId = "sens-01", IntValue = 127 },
+ };
+ Assert.Throws(() => _testService.Generate(elements, entityProfile, CancellationToken.None));
+ }
+
+ [Test]
+ public void Should_match_delete_cmd_for_entities_with_returning_clause()
+ {
+ var entityProfile = new ReturningEntityProfile();
+ var elements = new List
+ {
+ new TestEntity{ Id=12, RecordId = "rec-01", SensorId = "sens-01", IntValue = 127 },
+ };
+ var commands = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(commands);
+ Assert.AreEqual(1, commands.Count);
+ var commandResult = commands.First();
+
+ Assert.NotNull(commandResult.Command);
+
+ Assert.IsTrue(Regex.IsMatch(commandResult.Command, DELETE_PURE_PATTERN, RegexOptions.IgnoreCase));
+ }
+
+ [Test]
+ public void Should_match_delete_cmd_of_one_element()
+ {
+ var entityProfile = new ReturningEntityProfile();
+ var elements = new List
+ {
+ new TestEntity{ RecordId = "rec-01", SensorId = "sens-01", IntValue = 127 },
+ };
+ var commands = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(commands);
+ Assert.AreEqual(1, commands.Count);
+ var commandResult = commands.First();
+ Assert.NotNull(commandResult.Command);
+
+ Assert.IsTrue(Regex.IsMatch(commandResult.Command, DELETE_PURE_PATTERN, RegexOptions.IgnoreCase));
+ }
+
+ [Test]
+ public void Should_match_delete_cmd_of_many_elements()
+ {
+ var entityProfile = new ReturningEntityProfile();
+ var elements = new List
+ {
+ new TestEntity{ Id=1,RecordId = "rec-01", SensorId = "sens-01", IntValue = 127 },
+ new TestEntity{ Id=2,RecordId = "rec-02", SensorId = "sens-01", IntValue = 128 },
+ new TestEntity{ Id=3,RecordId = "rec-01", SensorId = "sens-02", IntValue = 227 },
+ new TestEntity{ Id=4,RecordId = "rec-02", SensorId = "sens-02", IntValue = 228 }
+ };
+ var commands = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(commands);
+ Assert.AreEqual(1, commands.Count);
+ var commandResult = commands.First();
+ Assert.NotNull(commandResult.Command);
+
+ Assert.IsTrue(Regex.IsMatch(commandResult.Command, DELETE_PURE_PATTERN, RegexOptions.IgnoreCase));
+ }
+
+ [Test]
+ public void Should_not_generate_cmd_for_entities_with_multiple_pk()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.Id)
+ .ThatIsPrivateKey();
+ entityProfile.HasProperty(x => x.RecordId)
+ .ThatIsPrivateKey();
+ var elements = new List
+ {
+ new TestEntity {Id = 12, RecordId = "rec-01", SensorId = "sen-0"}
+ };
+ Assert.Throws(()=> _testService.Generate(elements,
+ entityProfile,
+ CancellationToken.None));
+ }
+
+ [Test]
+ public void Should_build_correct_id_property_name()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.IntValue);
+ entityProfile.HasProperty(x => x.RecordId)
+ .ThatIsPrivateKey();
+ var elements = new List
+ {
+ new TestEntity {IntValue = 12, RecordId = "rec-01", SensorId = "sen-0"},
+ new TestEntity {IntValue = 13, RecordId = "rec-02", SensorId = "sen-1"}
+ };
+ var result = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(result);
+ var commandResult = result.FirstOrDefault();
+ Assert.NotNull(commandResult);
+
+ Assert.IsTrue(commandResult.Command.Contains("where \"record_id\" in"));
+ }
+
+ [Test]
+ public void Should_build_params_for_string_id()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.IntValue);
+ entityProfile.HasProperty(x => x.RecordId)
+ .ThatIsPrivateKey();
+ var elements = new List
+ {
+ new TestEntity {IntValue = 12, RecordId = "rec-01", SensorId = "sen-0"},
+ new TestEntity {IntValue = 13, RecordId = "rec-02", SensorId = "sen-1"}
+ };
+ var result = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(result);
+ var commandResult = result.FirstOrDefault();
+ Assert.NotNull(commandResult);
+ Assert.NotNull(commandResult.Parameters);
+ Assert.AreEqual(2, commandResult.Parameters.Count);
+
+ Assert.IsTrue(Regex.IsMatch(commandResult.Command, DELETE_PARAM_PATTERN, RegexOptions.IgnoreCase));
+ }
+
+ [Test]
+ public void Should_not_build_params_for_int_id()
+ {
+ var entityProfile = new SimpleEntityProfile();
+ var elements = new List
+ {
+ new TestEntity {Id=10, RecordId = "rec-01", SensorId = "sens-02", IntValue = 12}
+ };
+ var commands = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(commands);
+ Assert.AreEqual(1, commands.Count);
+ var commandResult = commands.First();
+ Assert.NotNull(commandResult.Command);
+ Assert.NotNull(commandResult.Parameters);
+ Assert.IsEmpty(commandResult.Parameters);
+ }
+
+ [Test]
+ public void Should_include_scheme_and_table_name()
+ {
+ var entityProfile = new SimpleEntityProfile();
+ var elements = new List
+ {
+ new TestEntity {RecordId = "rec-01"}
+ };
+ var commands = _testService.Generate(elements, entityProfile, CancellationToken.None);
+ Assert.NotNull(commands);
+ Assert.AreEqual(1, commands.Count);
+ var commandResult = commands.First();
+
+ Assert.NotNull(commandResult.Command);
+ var pattern = @"delete\s+from\s+""unit_tests"".""simple_test_entity""\s+";
+ Assert.IsTrue(Regex.IsMatch(commandResult.Command, pattern, RegexOptions.IgnoreCase));
+ }
+
+ [Test]
+ public void Should_throw_exception_for_empty_elements()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.Id).ThatIsPrivateKey();
+ entityProfile.ToTable("test_entity", "custom");
+ var elements = new List();
+ Assert.Throws(() => _testService.Generate(elements, entityProfile, CancellationToken.None));
+ }
+
+ [Test]
+ public void Should_throw_exception_when_elements_collection_items_are_all_null()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.Id).ThatIsPrivateKey();
+ entityProfile.ToTable("test_entity", "custom");
+ var elements = new List { null, null };
+ Assert.Throws(() => _testService.Generate(elements, entityProfile, CancellationToken.None));
+ }
+
+ [Test]
+ public void Should_not_throw_exception_when_at_least_one_item_is_not_null()
+ {
+ var entityProfile = new EntityProfile(typeof(TestEntity));
+ entityProfile.HasProperty(x => x.Id)
+ .ThatIsPrivateKey();
+ entityProfile.ToTable("test_entity", "custom");
+ var elements = new List
+ {
+ null,
+ new TestEntity {RecordId = "rec-01"},
+ null
+ };
+ Assert.DoesNotThrow(() => _testService.Generate(elements, entityProfile, CancellationToken.None));
+ }
+ }
+}
\ No newline at end of file