Skip to content

Commit

Permalink
Added sql-delete command builder with where pk in clause
Browse files Browse the repository at this point in the history
  • Loading branch information
Basim108 committed Mar 5, 2021
1 parent cf7fc86 commit 694ad9b
Show file tree
Hide file tree
Showing 8 changed files with 702 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ namespace Hrimsoft.SqlBulk.PostgreSql
/// </summary>
public static class PropertyProfileExtensions
{
/// <summary>
/// Will the property value getter execute dynamic invoke or not.
/// </summary>
/// <returns>Returns True if it will use dynamic invoke, otherwise returns False.</returns>
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;
}

/// <summary> Calculates property values of item </summary>
/// <param name="item">item with values</param>
/// <param name="profile">information about which property it is needed to get value</param>
Expand Down
4 changes: 3 additions & 1 deletion src/Hrimsoft.SqlBulk.PostgreSql/IoC/BulkServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public static IServiceCollection AddPostgreSqlBulkService(this IServiceCollectio
services.AddSingleton<IBulkServiceOptions>(options);
services.AddTransient<IInsertSqlCommandBuilder, InsertSqlCommandBuilder>();
services.AddTransient<IUpdateSqlCommandBuilder, UpdateSqlCommandBuilder>();
services.AddTransient<IDeleteSqlCommandBuilder, SimpleDeleteSqlCommandBuilder>();
services.AddTransient<IDeleteSqlCommandBuilder, DeleteSqlCommandMediator>();
services.AddTransient<SimpleDeleteSqlCommandBuilder>();
services.AddTransient<WhereInDeleteSqlCommandBuilder>();
services.AddTransient<IUpsertSqlCommandBuilder, UpsertSqlCommandBuilder>();

services.AddTransient<IPostgreSqlBulkService>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;

namespace Hrimsoft.SqlBulk.PostgreSql
{
/// <summary>
/// Mediator selects the best sql-command builder for the given entity type
/// </summary>
public class DeleteSqlCommandMediator : IDeleteSqlCommandBuilder
{
private readonly SimpleDeleteSqlCommandBuilder _simpleBuilder;
private readonly WhereInDeleteSqlCommandBuilder _whereInBuilder;
private readonly ILogger<DeleteSqlCommandMediator> _logger;

public DeleteSqlCommandMediator(SimpleDeleteSqlCommandBuilder simpleBuilder,
WhereInDeleteSqlCommandBuilder whereInBuilder,
ILogger<DeleteSqlCommandMediator> logger)
{
_simpleBuilder = simpleBuilder;
_whereInBuilder = whereInBuilder;
_logger = logger;
}

/// <inheritdoc />
public IList<SqlCommandBuilderResult> Generate<TEntity>(ICollection<TEntity> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Text;
using System.Threading;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Npgsql;

Expand All @@ -15,6 +14,8 @@ public class SimpleDeleteSqlCommandBuilder : IDeleteSqlCommandBuilder
{
private readonly ILogger<SimpleDeleteSqlCommandBuilder> _logger;

private const int MAX_PARAMS_PER_CMD = 65_535;

/// <summary> </summary>
public SimpleDeleteSqlCommandBuilder(ILoggerFactory loggerFactory)
{
Expand Down Expand Up @@ -42,8 +43,9 @@ public IList<SqlCommandBuilderResult> Generate<TEntity>(ICollection<TEntity> ele

_logger.LogTrace($"Generating delete sql for {elements.Count} elements.");

var result = "";
var allItemsParameters = new List<NpgsqlParameter>();
var result = new List<SqlCommandBuilderResult>();
var resultCommand = "";
var parameters = new List<NpgsqlParameter>();

if (_logger.IsEnabled(LogLevel.Debug))
{
Expand All @@ -66,48 +68,49 @@ public IList<SqlCommandBuilderResult> Generate<TEntity>(ICollection<TEntity> 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())
{
elementIndex++;
// 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<SqlCommandBuilderResult>
{
new SqlCommandBuilderResult
result.Add(new SqlCommandBuilderResult
{
Command = result,
Parameters = allItemsParameters,
Command = resultCommand,
Parameters = parameters,
IsThereReturningClause = false
}
};
});
return result;
}

/// <summary>
Expand All @@ -118,8 +121,11 @@ public IList<SqlCommandBuilderResult> Generate<TEntity>(ICollection<TEntity> ele
/// <param name="externalBuilder">Builder to which the generated for an item command will be appended</param>
/// <param name="elementIndex">As this method is called for each item, this value will be added to the sql parameter name</param>
/// <returns> Returns named tuple with generated command and list of db parameters. </returns>
public (string Command, ICollection<NpgsqlParameter> Parameters) GenerateForItem<TEntity>(EntityProfile entityProfile,
TEntity item, StringBuilder externalBuilder, int elementIndex)
public (string Command, ICollection<NpgsqlParameter> Parameters)
GenerateForItem<TEntity>(EntityProfile entityProfile,
TEntity item,
StringBuilder externalBuilder,
int elementIndex)
where TEntity : class
{
if (item == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Generates bulk delete sql command with where clause using in operand
/// </summary>
public class WhereInDeleteSqlCommandBuilder : IDeleteSqlCommandBuilder
{
private readonly ILogger<WhereInDeleteSqlCommandBuilder> _logger;

private const int MAX_IN_CLAUSE_IDS = 1000;
private const int MAX_PARAMS_PER_CMD = 65_535;

/// <summary> </summary>
public WhereInDeleteSqlCommandBuilder(ILogger<WhereInDeleteSqlCommandBuilder> logger)
{
_logger = logger;
}

/// <summary> Generates bulk delete sql command with where clause using in operand
/// </summary>
/// <param name="elements">elements that have to be deleted</param>
/// <param name="entityProfile">elements type profile (contains mapping and other options)</param>
/// <param name="cancellationToken"></param>
/// <returns>Returns a text of an sql delete command and collection of database parameters</returns>
public IList<SqlCommandBuilderResult> Generate<TEntity>(ICollection<TEntity> 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<SqlCommandBuilderResult>();
var cmdBuilder = new StringBuilder();
var parameters = privateKeys[0].IsDynamicallyInvoked()
? new List<NpgsqlParameter>(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<NpgsqlParameter>(),
IsThereReturningClause = false
});
return result;
}
}
}
Loading

0 comments on commit 694ad9b

Please sign in to comment.