Skip to content

Commit

Permalink
Added Support For Foreign Relationships To DeleteFromQuery(), DeleteF…
Browse files Browse the repository at this point in the history
…romQueryAsync()

Fixed bug in InternalBulkMerge() so that is works with triggers when AutoMapOutput is set to false.
  • Loading branch information
NorthernLight1 committed May 3, 2024
1 parent cc9c341 commit ef6f897
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 99 deletions.
3 changes: 3 additions & 0 deletions N.EntityFrameworkCore.Extensions.Test/Data/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ public class Product
[Column("Status")]
[StringLength(25)]
public string StatusString { get; set; }
public int? ProductCategoryId { get; set; }
public ProductStatus? StatusEnum { get; set; }
public DateTime? UpdatedDateTime { get; set; }

public virtual ProductCategory ProductCategory { get; set; }
public Product()
{

Expand Down
17 changes: 17 additions & 0 deletions N.EntityFrameworkCore.Extensions.Test/Data/ProductCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using N.EntityFrameworkCore.Extensions.Test.Data.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace N.EntityFrameworkCore.Extensions.Test.Data
{
public class ProductCategory
{
public int Id { get; set; }
public string Name { get; set; }
public bool Active { get; internal set; }
}
}
80 changes: 0 additions & 80 deletions N.EntityFrameworkCore.Extensions.Test/Data/SqlExpression.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class TestDbContext : DbContext
public virtual DbSet<ProductWithCustomSchema> ProductsWithCustomSchema { get; set; }
public virtual DbSet<ProductWithComplexKey> ProductsWithComplexKey { get; set; }
public virtual DbSet<Order> Orders { get; set; }
public virtual DbSet<ProductCategory> ProductCategories { get; set; }
public virtual DbSet<TpcPerson> TpcPeople { get; set; }
public virtual DbSet<TphPerson> TphPeople { get; set; }
public virtual DbSet<TphCustomer> TphCustomers { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace N.EntityFrameworkCore.Extensions.Test.DbContextExtensions
{
Expand All @@ -29,6 +30,7 @@ protected TestDbContext SetupDbContext(bool populateData, PopulateDataMode mode
TestDbContext dbContext = new TestDbContext();
dbContext.Orders.Truncate();
dbContext.Products.Truncate();
dbContext.ProductCategories.Clear();
dbContext.ProductsWithCustomSchema.Truncate();
dbContext.Database.ClearTable("TpcCustomer");
dbContext.Database.ClearTable("TpcVendor");
Expand Down Expand Up @@ -81,11 +83,20 @@ protected TestDbContext SetupDbContext(bool populateData, PopulateDataMode mode

Debug.WriteLine("Last Id for Order is {0}", id);
dbContext.BulkInsert(orders, new BulkInsertOptions<Order>() { KeepIdentity = true });

var productCategories = new List<ProductCategory>()
{
new ProductCategory { Id=1, Name="Category-1", Active=true},
new ProductCategory { Id=2, Name="Category-2", Active=true},
new ProductCategory { Id=3, Name="Category-3", Active=true},
new ProductCategory { Id=4, Name="Category-4", Active=false},
};
dbContext.BulkInsert(productCategories, o => { o.KeepIdentity = true; o.UsePermanentTable = true; });
var products = new List<Product>();
id = 1;
for (int i = 0; i < 2050; i++)
{
products.Add(new Product { Id = i.ToString(), Price = 1.25M, OutOfStock = false });
products.Add(new Product { Id = i.ToString(), Price = 1.25M, OutOfStock = false, ProductCategoryId = 4 });
id++;
}
for (int i = 2050; i < 7000; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ public void With_Boolean_Value()
Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were updated");
}
[TestMethod]
public void With_Child_Relationship()
{
var dbContext = SetupDbContext(true);
var products = dbContext.Products.Where(p => !p.ProductCategory.Active);
int oldTotal = products.Count();
int rowsDeleted = products.DeleteFromQuery();
int newTotal = products.Count();

Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition (ProductCategory.Active == false)");
Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows update must match the count of rows that match the condition (ProductCategory.Active == false)");
Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted");
}
[TestMethod]
public void With_Decimal_Using_IQuerable()
{
var dbContext = SetupDbContext(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace N.EntityFrameworkCore.Extensions.Test.DbContextExtensions
Expand All @@ -22,6 +23,19 @@ public async Task With_Boolean_Value()
Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were updated");
}
[TestMethod]
public async Task With_Child_Relationship()
{
var dbContext = SetupDbContext(true);
var products = dbContext.Products.Where(p => !p.ProductCategory.Active);
int oldTotal = products.Count();
int rowsDeleted = await products.DeleteFromQueryAsync();
int newTotal = products.Count();

Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition (ProductCategory.Active == false)");
Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows update must match the count of rows that match the condition (ProductCategory.Active == false)");
Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted");
}
[TestMethod]
public async Task With_Decimal_Using_IQuerable()
{
var dbContext = SetupDbContext(true);
Expand Down Expand Up @@ -90,11 +104,11 @@ public async Task With_Different_Values()
Assert.IsTrue(oldTotal - newTotal == rowsDeleted, "The rows deleted must match the new count minues the old count");
}
[TestMethod]
public void With_Empty_List()
public async Task With_Empty_List()
{
var dbContext = SetupDbContext(false);
int oldTotal = dbContext.Orders.Count();
int rowsDeleted = dbContext.Orders.DeleteFromQuery();
int rowsDeleted = await dbContext.Orders.DeleteFromQueryAsync();
int newTotal = dbContext.Orders.Count();

Assert.IsTrue(oldTotal == 0, "There must be no orders in database that match this condition");
Expand Down
11 changes: 8 additions & 3 deletions N.EntityFrameworkCore.Extensions/Data/BulkOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal BulkInsertResult<T> BulkInsertStagingData(IEnumerable<T> entities, bool
return DbContextExtensions.BulkInsert(entities, Options, TableMapping, Connection, Transaction, StagingTableName, columnsToInsert, SqlBulkCopyOptions.KeepIdentity, useInternalId);
}
internal BulkMergeResult<T> ExecuteMerge(Dictionary<long, T> entityMap, Expression<Func<T, T, bool>> mergeOnCondition,
bool autoMapOutput, bool insertIfNotExists, bool update = false, bool delete = false)
bool autoMapOutput, bool keepIdentity, bool insertIfNotExists, bool update = false, bool delete = false)
{
var rowsInserted = new Dictionary<IEntityType, int>();
var rowsUpdated = new Dictionary<IEntityType, int>();
Expand All @@ -76,14 +76,19 @@ internal BulkMergeResult<T> ExecuteMerge(Dictionary<long, T> entityMap, Expressi
rowsAffected[entityType] = 0;

var columnsToInsert = TableMapping.GetColumnNames(entityType).Intersect(GetColumnNames(entityType));
if(keepIdentity)
{
columnsToInsert = columnsToInsert.Union(TableMapping.GetPrimaryKeyColumns());
}
var columnsToUpdate = update ? TableMapping.GetColumnNames(entityType).Intersect(GetColumnNames(entityType)) : new string[] { };
var autoGeneratedColumns = autoMapOutput ? TableMapping.GetAutoGeneratedColumns(entityType) : new string[] { };
var columnsToOutput = GetMergeOutputColumns(autoGeneratedColumns, delete);
var columnsToOutput = autoMapOutput ? GetMergeOutputColumns(autoGeneratedColumns, delete) : new string[] { };
var deleteEntityType = TableMapping.EntityType == entityType & delete ? delete : false;

string mergeOnConditionSql = insertIfNotExists ? CommonUtil<T>.GetJoinConditionSql(mergeOnCondition, PrimaryKeyColumnNames, "t", "s") : "1=2";
bool toggleIdentity = keepIdentity && TableMapping.HasIdentityColumn;
var mergeStatement = SqlStatement.CreateMerge(StagingTableName, entityType.GetSchemaQualifiedTableName(),
mergeOnConditionSql, columnsToInsert, columnsToUpdate, columnsToOutput, deleteEntityType);
mergeOnConditionSql, columnsToInsert, columnsToUpdate, columnsToOutput, deleteEntityType, toggleIdentity);

if (autoMapOutput)
{
Expand Down
4 changes: 2 additions & 2 deletions N.EntityFrameworkCore.Extensions/Data/DbContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ public static int BulkInsert<T>(this DbContext context, IEnumerable<T> entities,
{
var bulkInsertResult = bulkOperation.BulkInsertStagingData(entities, options.KeepIdentity, true);
var bulkMergeResult = bulkOperation.ExecuteMerge(bulkInsertResult.EntityMap, options.InsertOnCondition,
options.AutoMapOutput, options.InsertIfNotExists);
options.AutoMapOutput, options.KeepIdentity, options.InsertIfNotExists);
rowsAffected = bulkMergeResult.RowsAffected;
bulkOperation.DbTransactionContext.Commit();
}
Expand Down Expand Up @@ -414,7 +414,7 @@ private static BulkMergeResult<T> InternalBulkMerge<T>(this DbContext context, I
bulkOperation.ValidateBulkMerge(options.MergeOnCondition);
var bulkInsertResult = bulkOperation.BulkInsertStagingData(entities, true, true);
bulkMergeResult = bulkOperation.ExecuteMerge(bulkInsertResult.EntityMap, options.MergeOnCondition, options.AutoMapOutput,
true, true, options.DeleteIfNotMatched);
false, true, true, options.DeleteIfNotMatched);
bulkOperation.DbTransactionContext.Commit();
}
catch (Exception)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>8.0.0.5</Version>
<Version>8.0.0.6</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageProjectUrl>https://github.com/NorthernLight1/N.EntityFrameworkCore.Extensions/</PackageProjectUrl>
<Authors>Northern25</Authors>
Expand Down
4 changes: 3 additions & 1 deletion N.EntityFrameworkCore.Extensions/Sql/SqlBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ public void ChangeToDelete()
if(sqlClause != null)
{
sqlClause.Name = "DELETE";
sqlClause.InputText = sqlFromClause.InputText.Substring(sqlFromClause.InputText.LastIndexOf("AS ") + 3);
int aliasStartIndex = sqlFromClause.InputText.IndexOf("AS ") + 3;
int aliasLength = sqlFromClause.InputText.IndexOf("]", aliasStartIndex) - aliasStartIndex + 1;
sqlClause.InputText = sqlFromClause.InputText.Substring(aliasStartIndex, aliasLength);
}
}
public void ChangeToUpdate(string updateExpression, string setExpression)
Expand Down
5 changes: 4 additions & 1 deletion N.EntityFrameworkCore.Extensions/Sql/SqlKeyword.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public enum SqlKeyword
As,
By,
Source,
Target
Target,
Off,
Identity_Insert,
Semicolon,
}
}
48 changes: 40 additions & 8 deletions N.EntityFrameworkCore.Extensions/Sql/SqlStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,28 @@ internal void CreatePart(SqlKeyword keyword, SqlExpression expression = null)
{
SqlParts.Add(new SqlPart(keyword, expression));
}
internal void SetIdentityInsert(string tableName, bool enable)
{
this.CreatePart(SqlKeyword.Set);
this.CreatePart(SqlKeyword.Identity_Insert, SqlExpression.Table(tableName));
if (enable)
this.CreatePart(SqlKeyword.On);
else
this.CreatePart(SqlKeyword.Off);
this.CreatePart(SqlKeyword.Semicolon);
}
//internal static SqlStatement CreateMergeInsert(string sourceTableName, string targetTableName, string mergeOnCondition,
// IEnumerable<string> insertColumns, IEnumerable<string> outputColumns, bool deleteIfNotMatched = false)
//{

//}
internal static SqlStatement CreateMerge(string sourceTableName, string targetTableName, string joinOnCondition,
IEnumerable<string> insertColumns, IEnumerable<string> updateColumns, IEnumerable<string> outputColumns, bool deleteIfNotMatched=false)
IEnumerable<string> insertColumns, IEnumerable<string> updateColumns, IEnumerable<string> outputColumns,
bool deleteIfNotMatched=false, bool hasIdentityColumn=false)
{
var statement = new SqlStatement();
if (hasIdentityColumn)
statement.SetIdentityInsert(targetTableName, true);
statement.CreatePart(SqlKeyword.Merge, SqlExpression.Table(targetTableName, "t"));
statement.CreatePart(SqlKeyword.Using, SqlExpression.Table(sourceTableName, "s"));
statement.CreatePart(SqlKeyword.On, SqlExpression.String(joinOnCondition));
Expand Down Expand Up @@ -71,7 +84,12 @@ internal static SqlStatement CreateMerge(string sourceTableName, string targetTa
statement.CreatePart(SqlKeyword.Then);
statement.CreatePart(SqlKeyword.Delete);
}
statement.CreatePart(SqlKeyword.Output, SqlExpression.Columns(outputColumns));
if(outputColumns.Any())
statement.CreatePart(SqlKeyword.Output, SqlExpression.Columns(outputColumns));
statement.CreatePart(SqlKeyword.Semicolon);

if (hasIdentityColumn)
statement.SetIdentityInsert(targetTableName, false);
return statement;
}

Expand All @@ -80,9 +98,23 @@ private string ToSql()
StringBuilder sbSql = new StringBuilder();
foreach(var part in SqlParts)
{
if (!part.IgnoreOutput)
if (part.Keyword == SqlKeyword.Semicolon)
{
sbSql.Append(part.Keyword.ToString().ToUpper() + " ");
int lastIndex = sbSql.Length - 1;
if (lastIndex > -1 && sbSql[lastIndex] == ' ')
{
sbSql[lastIndex] = ';';
sbSql.Append("\n");
}
else
{
sbSql.Append(";\n");
}
}
else if (!part.IgnoreOutput)
{
sbSql.Append(part.Keyword.ToString().ToUpper());
sbSql.Append(" ");
bool useParenthese = part.Keyword == SqlKeyword.Insert || part.Keyword == SqlKeyword.Values;
string format = useParenthese ? "({0})" : "{0}";

Expand All @@ -94,10 +126,10 @@ private string ToSql()
}
}
//Output a semicolon for certain SQL Statments
if(SqlParts.First().Keyword == SqlKeyword.Merge)
{
sbSql.Append(";");
}
//if(SqlParts.First().Keyword == SqlKeyword.Merge)
//{
// sbSql.Append(";");
//}
return sbSql.ToString();
}
}
Expand Down

0 comments on commit ef6f897

Please sign in to comment.