From a2636ab1f7fd701c11824e3396c2f7b06b3e9dfc Mon Sep 17 00:00:00 2001
From: NorthernLight1 <49600465+NorthernLight1@users.noreply.github.com>
Date: Mon, 27 Apr 2026 22:43:46 -0400
Subject: [PATCH 1/7] Update dotnet.yml
---
.github/workflows/dotnet.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index e26254b..a2a216d 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -2,9 +2,9 @@ name: .NET
on:
push:
- branches: [ "master" ]
+ branches: [ "main" ]
pull_request:
- branches: [ "master" ]
+ branches: [ "main" ]
permissions:
contents: read
From 51379e8c2a0874373d20d5cb23c95ab5fe67ca77 Mon Sep 17 00:00:00 2001
From: NorthernLight1 <49600465+NorthernLight1@users.noreply.github.com>
Date: Mon, 27 Apr 2026 22:59:41 -0400
Subject: [PATCH 2/7] Fixed Incorrect Names
---
...tings => N.EntityFramework.Extensions.PostgreSql.runsettings | 0
...ttings => N.EntityFramework.Extensions.SqlServer.runsettings | 0
.../N.EntityFramework.Extensions.SqlServer.Test.csproj | 2 +-
3 files changed, 1 insertion(+), 1 deletion(-)
rename N.EntityFrameworkCore.PostgreSQL.Extensions.runsettings => N.EntityFramework.Extensions.PostgreSql.runsettings (100%)
rename N.EntityFrameworkCore.Extensions.runsettings => N.EntityFramework.Extensions.SqlServer.runsettings (100%)
diff --git a/N.EntityFrameworkCore.PostgreSQL.Extensions.runsettings b/N.EntityFramework.Extensions.PostgreSql.runsettings
similarity index 100%
rename from N.EntityFrameworkCore.PostgreSQL.Extensions.runsettings
rename to N.EntityFramework.Extensions.PostgreSql.runsettings
diff --git a/N.EntityFrameworkCore.Extensions.runsettings b/N.EntityFramework.Extensions.SqlServer.runsettings
similarity index 100%
rename from N.EntityFrameworkCore.Extensions.runsettings
rename to N.EntityFramework.Extensions.SqlServer.runsettings
diff --git a/N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.SqlServer.Test/N.EntityFramework.Extensions.SqlServer.Test.csproj b/N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.SqlServer.Test/N.EntityFramework.Extensions.SqlServer.Test.csproj
index 9ccc176..af218fe 100644
--- a/N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.SqlServer.Test/N.EntityFramework.Extensions.SqlServer.Test.csproj
+++ b/N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.SqlServer.Test/N.EntityFramework.Extensions.SqlServer.Test.csproj
@@ -3,7 +3,7 @@
net10.0
- $(MSBuildThisFileDirectory)..\..\N.EntityFrameworkCore.Extensions.runsettings
+ $(MSBuildThisFileDirectory)..\..\N.EntityFramework.Extensions.SqlServer.runsettings
From 50b4574d80f8faaa794ed71ab12ef93ea22d6f57 Mon Sep 17 00:00:00 2001
From: NorthernLight1 <49600465+NorthernLight1@users.noreply.github.com>
Date: Mon, 27 Apr 2026 23:38:58 -0400
Subject: [PATCH 3/7] Add MySQL EF Core extension and test projects
- Add N.EntityFramework.Extensions.MySql targeting net9.0 with Pomelo.EntityFrameworkCore.MySql 9.0.0
- Add N.EntityFramework.Extensions.MySql.Test with Testcontainers.MySql 4.11.0
- Add N.EntityFramework.Extensions.MySql.runsettings
- Update solution file with both new MySQL projects
MySQL-specific implementation details:
- MySqlBulkCopy for bulk insert via MySqlConnector (transitive via Pomelo)
- JOIN-based UPDATE/DELETE syntax for MERGE operations
- information_schema queries using DATABASE() for table/column checks
- CREATE TABLE ... AS SELECT ... WHERE 1=0 for CloneTable
- AUTO_INCREMENT reset via ALTER TABLE ... AUTO_INCREMENT = 1
- No schema concept (GetDefaultSchema returns null)
- Uses EF Core 9 SetPropertyCalls API for UpdateFromQuery
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...tityFramework.Extensions.MySql.runsettings | 15 +
.../Common/Constants.cs | 6 +
.../Data/BulkDeleteOptions.cs | 9 +
.../Data/BulkFetchOptions.cs | 11 +
.../Data/BulkInsertOptions.cs | 27 +
.../Data/BulkInsertResult.cs | 9 +
.../Data/BulkMergeOption.cs | 24 +
.../Data/BulkMergeOutputRow.cs | 11 +
.../Data/BulkMergeResult.cs | 12 +
.../Data/BulkOperation.cs | 232 +
.../Data/BulkOperationAsync.cs | 147 +
.../Data/BulkOptions.cs | 18 +
.../Data/BulkQueryResult.cs | 10 +
.../Data/BulkSyncOptions.cs | 10 +
.../Data/BulkSyncResult.cs | 18 +
.../Data/BulkUpdateOptions.cs | 11 +
.../Data/DatabaseFacadeExtensions.cs | 171 +
.../Data/DatabaseFacadeExtensionsAsync.cs | 83 +
.../Data/DbContextExtensions.cs | 768 +
.../Data/DbContextExtensionsAsync.cs | 678 +
.../Data/DbTransactionContext.cs | 70 +
.../Data/EfExtensionsCommand.cs | 22 +
.../Data/EfExtensionsCommandInterceptor.cs | 27 +
.../Data/EntityDataReader.cs | 248 +
.../Data/FetchOptions.cs | 11 +
.../Data/FetchResult.cs | 9 +
.../Data/QueryToFileOptions.cs | 18 +
.../Data/QueryToFileResult.cs | 8 +
.../Data/SqlMergeAction.cs | 8 +
.../Data/SqlQuery.cs | 36 +
.../Data/TableMapping.cs | 135 +
.../Enums/ConnectionBehavior.cs | 7 +
.../Enums/EfExtensionsCommandType.cs | 6 +
.../Extensions/CommonExtensions.cs | 13 +
.../Extensions/DbDataReaderExtensions.cs | 51 +
.../Extensions/IPropertyExtensions.cs | 11 +
.../Extensions/LinqExtensions.cs | 246 +
.../Extensions/ObjectExtensions.cs | 32 +
.../Extensions/SqlStatementExtensions.cs | 13 +
.../GlobalSuppressions.cs | 12 +
.../N.EntityFramework.Extensions.MySql.csproj | 40 +
.../Sql/SqlBuilder.cs | 152 +
.../Sql/SqlClause.cs | 14 +
.../Sql/SqlExpression.cs | 70 +
.../Sql/SqlExpressionType.cs | 9 +
.../Sql/SqlKeyword.cs | 29 +
.../Sql/SqlPart.cs | 14 +
.../Sql/SqlStatement.cs | 105 +
.../Util/CommonUtil.cs | 135 +
.../Util/RelationalProviderUtil.cs | 115 +
.../Util/SqlUtil.cs | 11 +
.../Common/Config.cs | 32 +
.../Common/MySqlContainerManager.cs | 69 +
.../Common/TestDatabaseInitializer.cs | 76 +
.../Data/Address.cs | 18 +
.../Data/Enums/ProductStatus.cs | 7 +
.../Data/Order.cs | 34 +
.../Data/OrderWithComplexType.cs | 18 +
.../Data/Position.cs | 8 +
.../Data/Product.cs | 32 +
.../Data/ProductCategory.cs | 8 +
.../Data/ProductWithComplexKey.cs | 25 +
.../Data/ProductWithCustomSchema.cs | 14 +
.../Data/ProductWithTrigger.cs | 22 +
.../Data/TestDbContext.cs | 64 +
.../Data/TpcCustomer.cs | 10 +
.../Data/TpcPerson.cs | 11 +
.../Data/TpcVendor.cs | 9 +
.../Data/TphCustomer.cs | 10 +
.../Data/TphPerson.cs | 11 +
.../Data/TphVendor.cs | 9 +
.../Data/TptCustomer.cs | 10 +
.../Data/TptPerson.cs | 11 +
.../Data/TptVendor.cs | 9 +
.../DatabaseExtensionsBase.cs | 70 +
.../DatabaseExtensions/SqlQueryToCsvFile.cs | 36 +
.../SqlQueryToCsvFileAsync.cs | 37 +
.../DatabaseExtensions/SqlQuery_Count.cs | 34 +
.../DatabaseExtensions/SqlQuery_CountAsync.cs | 35 +
.../DatabaseExtensions/TableExists.cs | 20 +
.../DatabaseExtensions/TruncateTable.cs | 20 +
.../DatabaseExtensions/TruncateTableAsync.cs | 21 +
.../DbContextExtensions/BulkDelete.cs | 90 +
.../DbContextExtensions/BulkDeleteAsync.cs | 91 +
.../DbContextExtensions/BulkFetch.cs | 114 +
.../DbContextExtensions/BulkFetchAsync.cs | 113 +
.../DbContextExtensions/BulkInsert.cs | 457 +
.../DbContextExtensions/BulkInsertAsync.cs | 476 +
.../DbContextExtensions/BulkMerge.cs | 406 +
.../DbContextExtensions/BulkMergeAsync.cs | 407 +
.../DbContextExtensions/BulkSaveChanges.cs | 261 +
.../BulkSaveChangesAsync.cs | 263 +
.../DbContextExtensions/BulkSync.cs | 241 +
.../DbContextExtensions/BulkSyncAsync.cs | 243 +
.../DbContextExtensions/BulkUpdate.cs | 243 +
.../DbContextExtensions/BulkUpdateAsync.cs | 243 +
.../DbContextExtensionsBase.cs | 267 +
.../DbContextExtensions/DeleteFromQuery.cs | 147 +
.../DeleteFromQueryAsync.cs | 148 +
.../DbContextExtensions/Fetch.cs | 177 +
.../DbContextExtensions/FetchAsync.cs | 193 +
.../DbContextExtensions/InsertFromQuery.cs | 86 +
.../InsertFromQueryAsync.cs | 87 +
.../DbContextExtensions/QueryToCsvFile.cs | 47 +
.../QueryToCsvFileAsync.cs | 48 +
.../DbContextExtensions/UpdateFromQuery.cs | 275 +
.../UpdateFromQueryAsync.cs | 277 +
.../DbSetExtensions/Clear.cs | 21 +
.../DbSetExtensions/ClearAsync.cs | 22 +
.../DbSetExtensions/Truncate.cs | 21 +
.../DbSetExtensions/TruncateAsync.cs | 22 +
.../LinqExtensions/ToSqlPredicateTests.cs | 102 +
...tityFramework.Extensions.MySql.Test.csproj | 33 +
.../appsettings.json | 7 +
N.EntityFrameworkCore.Extensions.sln | 29 +
PROJECT_FILES_CONTENT.txt | 23893 ++++++++++++++++
116 files changed, 34287 insertions(+)
create mode 100644 N.EntityFramework.Extensions.MySql.runsettings
create mode 100644 N.EntityFramework.Extensions.MySql/Common/Constants.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkDeleteOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkFetchOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkInsertOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkInsertResult.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkMergeOption.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkMergeOutputRow.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkMergeResult.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkOperation.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkOperationAsync.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkQueryResult.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkSyncOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkSyncResult.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/BulkUpdateOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/DatabaseFacadeExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/DatabaseFacadeExtensionsAsync.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/DbContextExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/DbContextExtensionsAsync.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/DbTransactionContext.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/EfExtensionsCommand.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/EfExtensionsCommandInterceptor.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/EntityDataReader.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/FetchOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/FetchResult.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/QueryToFileOptions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/QueryToFileResult.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/SqlMergeAction.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/SqlQuery.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Data/TableMapping.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Enums/ConnectionBehavior.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Enums/EfExtensionsCommandType.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Extensions/CommonExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Extensions/DbDataReaderExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Extensions/IPropertyExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Extensions/LinqExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Extensions/ObjectExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Extensions/SqlStatementExtensions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/GlobalSuppressions.cs
create mode 100644 N.EntityFramework.Extensions.MySql/N.EntityFramework.Extensions.MySql.csproj
create mode 100644 N.EntityFramework.Extensions.MySql/Sql/SqlBuilder.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Sql/SqlClause.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Sql/SqlExpression.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Sql/SqlExpressionType.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Sql/SqlKeyword.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Sql/SqlPart.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Sql/SqlStatement.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Util/CommonUtil.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Util/RelationalProviderUtil.cs
create mode 100644 N.EntityFramework.Extensions.MySql/Util/SqlUtil.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Common/Config.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Common/MySqlContainerManager.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Common/TestDatabaseInitializer.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/Address.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/Enums/ProductStatus.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/Order.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/OrderWithComplexType.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/Position.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/Product.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/ProductCategory.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/ProductWithComplexKey.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/ProductWithCustomSchema.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/ProductWithTrigger.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TestDbContext.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TpcCustomer.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TpcPerson.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TpcVendor.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TphCustomer.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TphPerson.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TphVendor.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TptCustomer.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TptPerson.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/Data/TptVendor.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/DatabaseExtensionsBase.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/SqlQueryToCsvFile.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/SqlQueryToCsvFileAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/SqlQuery_Count.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/SqlQuery_CountAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/TableExists.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/TruncateTable.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DatabaseExtensions/TruncateTableAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkDelete.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkDeleteAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkFetch.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkFetchAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkInsert.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkInsertAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkMerge.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkMergeAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkSaveChanges.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkSaveChangesAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkSync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkSyncAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkUpdate.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/BulkUpdateAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/DbContextExtensionsBase.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/DeleteFromQuery.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/DeleteFromQueryAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/Fetch.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/FetchAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/InsertFromQuery.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/InsertFromQueryAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/QueryToCsvFile.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/QueryToCsvFileAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/UpdateFromQuery.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbContextExtensions/UpdateFromQueryAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbSetExtensions/Clear.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbSetExtensions/ClearAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbSetExtensions/Truncate.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/DbSetExtensions/TruncateAsync.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/LinqExtensions/ToSqlPredicateTests.cs
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/N.EntityFramework.Extensions.MySql.Test.csproj
create mode 100644 N.EntityFrameworkCore.Extensions.Test/N.EntityFramework.Extensions.MySql.Test/appsettings.json
create mode 100644 PROJECT_FILES_CONTENT.txt
diff --git a/N.EntityFramework.Extensions.MySql.runsettings b/N.EntityFramework.Extensions.MySql.runsettings
new file mode 100644
index 0000000..e5061ec
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql.runsettings
@@ -0,0 +1,15 @@
+
+
+
+ 1
+
+
+
+
+
+ detailed
+
+
+
+
+
diff --git a/N.EntityFramework.Extensions.MySql/Common/Constants.cs b/N.EntityFramework.Extensions.MySql/Common/Constants.cs
new file mode 100644
index 0000000..d756e1d
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Common/Constants.cs
@@ -0,0 +1,6 @@
+namespace N.EntityFrameworkCore.Extensions.Common;
+
+public static class Constants
+{
+ public static readonly string InternalId_ColumnName = "_be_xx_id";
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkDeleteOptions.cs b/N.EntityFramework.Extensions.MySql/Data/BulkDeleteOptions.cs
new file mode 100644
index 0000000..412d3b4
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkDeleteOptions.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Linq.Expressions;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkDeleteOptions : BulkOptions
+{
+ public Expression> DeleteOnCondition { get; set; }
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkFetchOptions.cs b/N.EntityFramework.Extensions.MySql/Data/BulkFetchOptions.cs
new file mode 100644
index 0000000..790c7f2
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkFetchOptions.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Linq.Expressions;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkFetchOptions : BulkOptions
+{
+ public Expression> IgnoreColumns { get; set; }
+ public Expression> InputColumns { get; set; }
+ public Expression> JoinOnCondition { get; set; }
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkInsertOptions.cs b/N.EntityFramework.Extensions.MySql/Data/BulkInsertOptions.cs
new file mode 100644
index 0000000..9daa53c
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkInsertOptions.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkInsertOptions : BulkOptions
+{
+ public bool AutoMapOutput { get; set; }
+ public Expression> IgnoreColumns { get; set; }
+ public Expression> InputColumns { get; set; }
+ public bool InsertIfNotExists { get; set; }
+ public Expression> InsertOnCondition { get; set; }
+ public bool KeepIdentity { get; set; }
+
+ public string[] GetInputColumns() =>
+ InputColumns?.Body.Type.GetProperties().Select(o => o.Name).ToArray();
+
+ public BulkInsertOptions()
+ {
+ AutoMapOutput = true;
+ }
+ internal BulkInsertOptions(BulkOptions options)
+ {
+ EntityType = options.EntityType;
+ }
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkInsertResult.cs b/N.EntityFramework.Extensions.MySql/Data/BulkInsertResult.cs
new file mode 100644
index 0000000..29315ff
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkInsertResult.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+internal sealed class BulkInsertResult
+{
+ internal int RowsAffected { get; set; }
+ internal Dictionary EntityMap { get; set; }
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkMergeOption.cs b/N.EntityFramework.Extensions.MySql/Data/BulkMergeOption.cs
new file mode 100644
index 0000000..e388ee3
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkMergeOption.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkMergeOptions : BulkOptions
+{
+ public Expression> MergeOnCondition { get; set; }
+ public Expression> IgnoreColumnsOnInsert { get; set; }
+ public Expression> IgnoreColumnsOnUpdate { get; set; }
+ public bool AutoMapOutput { get; set; }
+ internal bool DeleteIfNotMatched { get; set; }
+
+ public BulkMergeOptions()
+ {
+ AutoMapOutput = true;
+ }
+ public List GetIgnoreColumnsOnInsert() =>
+ IgnoreColumnsOnInsert?.Body.Type.GetProperties().Select(o => o.Name).ToList() ?? [];
+ public List GetIgnoreColumnsOnUpdate() =>
+ IgnoreColumnsOnUpdate?.Body.Type.GetProperties().Select(o => o.Name).ToList() ?? [];
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkMergeOutputRow.cs b/N.EntityFramework.Extensions.MySql/Data/BulkMergeOutputRow.cs
new file mode 100644
index 0000000..a3f5703
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkMergeOutputRow.cs
@@ -0,0 +1,11 @@
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkMergeOutputRow
+{
+ public string Action { get; set; }
+
+ public BulkMergeOutputRow(string action)
+ {
+ Action = action;
+ }
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkMergeResult.cs b/N.EntityFramework.Extensions.MySql/Data/BulkMergeResult.cs
new file mode 100644
index 0000000..b28592e
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkMergeResult.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkMergeResult
+{
+ public IEnumerable> Output { get; set; }
+ public int RowsAffected { get; set; }
+ public int RowsDeleted { get; internal set; }
+ public int RowsInserted { get; internal set; }
+ public int RowsUpdated { get; internal set; }
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkOperation.cs b/N.EntityFramework.Extensions.MySql/Data/BulkOperation.cs
new file mode 100644
index 0000000..26620dd
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkOperation.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
+using Microsoft.EntityFrameworkCore.Metadata;
+using N.EntityFrameworkCore.Extensions.Common;
+using N.EntityFrameworkCore.Extensions.Sql;
+using N.EntityFrameworkCore.Extensions.Util;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+internal sealed partial class BulkOperation : IDisposable
+{
+ internal DbConnection Connection => DbTransactionContext.Connection;
+ internal DbContext Context { get; }
+ internal bool StagingTableCreated { get; set; }
+ internal string StagingTableName { get; }
+ internal string[] PrimaryKeyColumnNames { get; }
+ internal BulkOptions Options { get; }
+ internal Expression> InputColumns { get; }
+ internal Expression> IgnoreColumns { get; }
+ internal DbTransactionContext DbTransactionContext { get; }
+ internal Type EntityType => typeof(T);
+ internal DbTransaction Transaction => DbTransactionContext.CurrentTransaction;
+ internal TableMapping TableMapping { get; }
+ internal IEnumerable SchemaQualifiedTableNames => TableMapping.GetSchemaQualifiedTableNames();
+
+
+ public BulkOperation(DbContext dbContext, BulkOptions options, Expression> inputColumns = null, Expression> ignoreColumns = null)
+ {
+ Context = dbContext;
+ Options = options;
+ InputColumns = inputColumns;
+ IgnoreColumns = ignoreColumns;
+
+ DbTransactionContext = new DbTransactionContext(dbContext, options.CommandTimeout);
+ TableMapping = dbContext.GetTableMapping(typeof(T), options.EntityType);
+ StagingTableName = CommonUtil.GetStagingTableName(TableMapping, options.UsePermanentTable, Connection);
+ PrimaryKeyColumnNames = TableMapping.GetPrimaryKeyColumns().ToArray();
+ }
+ public void Dispose()
+ {
+ if (StagingTableCreated)
+ {
+ Context.Database.DropTable(StagingTableName, true);
+ }
+ }
+ internal bool ShouldKeepIdentityForMerge()
+ {
+ // MySQL does not support keeping identity values for merge in the same way as PostgreSQL
+ return false;
+ }
+ internal bool ShouldPreallocateIdentityValues(bool autoMapOutput, bool keepIdentity, IEnumerable entities)
+ {
+ // MySQL does not support preallocating identity values
+ return false;
+ }
+ internal void PreallocateIdentityValues(IEnumerable entities)
+ {
+ // No-op for MySQL
+ }
+ internal BulkInsertResult BulkInsertStagingData(IEnumerable entities, bool keepIdentity = true, bool useInternalId = false)
+ {
+ IEnumerable columnsToInsert = GetColumnNames(keepIdentity);
+ string internalIdColumn = useInternalId ? Common.Constants.InternalId_ColumnName : null;
+ Context.Database.CloneTable(SchemaQualifiedTableNames, StagingTableName, TableMapping.GetQualifiedColumnNames(columnsToInsert), internalIdColumn);
+ StagingTableCreated = true;
+ return DbContextExtensions.BulkInsert(entities, Options, TableMapping, Connection, Transaction, StagingTableName, columnsToInsert, useInternalId);
+ }
+ internal BulkMergeResult ExecuteMerge(Dictionary entityMap, Expression> mergeOnCondition,
+ bool autoMapOutput, bool keepIdentity, bool insertIfNotExists, bool update = false, bool delete = false, bool preallocatedIds = false)
+ {
+ return ExecuteMergeMySql(entityMap, mergeOnCondition, autoMapOutput, keepIdentity, insertIfNotExists, update, delete, preallocatedIds);
+ }
+
+ private IEnumerable GetMergeOutputColumns(IEnumerable autoGeneratedColumns, bool delete = false)
+ {
+ List columnsToOutput = ["$action", $"[s].[{Constants.InternalId_ColumnName}]"];
+ columnsToOutput.AddRange(autoGeneratedColumns.Select(o => $"[inserted].[{o}]"));
+ return columnsToOutput;
+ }
+ private object[] GetMergeOutputValues(IEnumerable columns, object[] values, IEnumerable properties)
+ {
+ var columnList = columns.ToList();
+ var valuesIndex = properties.Select(o => columnList.IndexOf($"[inserted].[{o.GetColumnName()}]"));
+ return valuesIndex.Select(i => values[i]).ToArray();
+ }
+ internal int ExecuteUpdate(IEnumerable entities, Expression> updateOnCondition)
+ {
+ return ExecuteUpdateMySql(updateOnCondition);
+ }
+ private BulkMergeResult ExecuteMergeMySql(Dictionary entityMap, Expression> mergeOnCondition,
+ bool autoMapOutput, bool keepIdentity, bool insertIfNotExists, bool update, bool delete, bool preallocatedIds = false)
+ {
+ Dictionary rowsInserted = [];
+ Dictionary rowsUpdated = [];
+ Dictionary rowsDeleted = [];
+ List> outputRows = [];
+
+ foreach (var entityType in TableMapping.EntityTypes)
+ {
+ var targetTableName = Context.DelimitIdentifier(entityType.GetTableName(), entityType.GetSchema() ?? Context.Database.GetDefaultSchema());
+ var columnsToInsert = GetColumnNames(entityType, keepIdentity).ToList();
+ var columnsToUpdate = update ? GetColumnNames(entityType).ToList() : [];
+ var autoGeneratedColumns = autoMapOutput ? TableMapping.GetAutoGeneratedColumns(entityType).ToList() : [];
+ var allProperties = autoMapOutput
+ ? TableMapping.GetEntityProperties(entityType, ValueGenerated.OnAdd).Concat(TableMapping.GetEntityProperties(entityType, ValueGenerated.OnAddOrUpdate)).ToList()
+ : [];
+
+ string matchJoinCondition = CommonUtil.GetJoinConditionSql(Context, mergeOnCondition, PrimaryKeyColumnNames, "s", "t");
+ string pkJoinCondition = CommonUtil.GetJoinConditionSql(Context, null, PrimaryKeyColumnNames, "s", "t");
+ string joinCondition = insertIfNotExists ? matchJoinCondition : "1=2";
+
+ HashSet matchedIds = autoMapOutput && update
+ ? GetMatchedInternalIds(targetTableName, matchJoinCondition)
+ : [];
+
+ rowsUpdated[entityType] = 0;
+ if (columnsToUpdate.Count > 0)
+ {
+ // MySQL UPDATE with JOIN syntax
+ string updateSetExpression = string.Join(",", columnsToUpdate.Select(c => $"t.{Context.DelimitIdentifier(c)}=s.{Context.DelimitIdentifier(c)}"));
+ string updateSql = $"UPDATE {StagingTableName} AS s INNER JOIN {targetTableName} AS t ON {joinCondition} SET {updateSetExpression}";
+ rowsUpdated[entityType] = Context.Database.ExecuteSqlInternal(updateSql, Options.CommandTimeout);
+ }
+
+ string insertColumnsSql = string.Join(",", columnsToInsert.Select(Context.DelimitIdentifier));
+ string sourceColumnsSql = string.Join(",", columnsToInsert.Select(c => Context.DelimitMemberAccess("s", c)));
+ string insertSql = $"INSERT INTO {targetTableName} ({insertColumnsSql}) SELECT {sourceColumnsSql} FROM {StagingTableName} AS s WHERE NOT EXISTS (SELECT 1 FROM {targetTableName} AS t WHERE {joinCondition})";
+ rowsInserted[entityType] = Context.Database.ExecuteSqlInternal(insertSql, Options.CommandTimeout);
+ if (keepIdentity && rowsInserted[entityType] > 0)
+ SyncMySqlAutoIncrement(entityType);
+
+ rowsDeleted[entityType] = 0;
+ if (TableMapping.EntityType == entityType && delete)
+ {
+ string deleteJoinCondition = (preallocatedIds && mergeOnCondition != null) ? matchJoinCondition : pkJoinCondition;
+ // MySQL multi-table DELETE syntax: DELETE alias FROM table AS alias WHERE ...
+ string deleteSql = $"DELETE t FROM {targetTableName} AS t WHERE NOT EXISTS (SELECT 1 FROM {StagingTableName} AS s WHERE {deleteJoinCondition})";
+ rowsDeleted[entityType] = Context.Database.ExecuteSqlInternal(deleteSql, Options.CommandTimeout);
+ for (int i = 0; i < rowsDeleted[entityType]; i++)
+ outputRows.Add(new BulkMergeOutputRow(SqlMergeAction.Delete));
+ }
+
+ if (autoMapOutput)
+ {
+ string outputColumnsSql = autoGeneratedColumns.Any()
+ ? "," + string.Join(",", autoGeneratedColumns.Select(c => Context.DelimitMemberAccess("t", c)))
+ : string.Empty;
+ var outputQuery = $"SELECT {Context.DelimitMemberAccess("s", Constants.InternalId_ColumnName)}{outputColumnsSql} FROM {StagingTableName} AS s JOIN {targetTableName} AS t ON {matchJoinCondition}";
+ var bulkQueryResult = Context.BulkQuery(outputQuery, Options);
+ var autoGeneratedColumnList = autoGeneratedColumns.ToList();
+ foreach (var result in bulkQueryResult.Results)
+ {
+ int entityId = Convert.ToInt32(result[0]);
+ bool wasMatched = matchedIds.Contains(entityId);
+ string action = wasMatched ? SqlMergeAction.Update : SqlMergeAction.Insert;
+ outputRows.Add(new BulkMergeOutputRow(action));
+
+ if (entityMap.TryGetValue(entityId, out var entity) && allProperties.Count > 0)
+ {
+ object[] entityValues = allProperties.Select(p => result[1 + autoGeneratedColumnList.IndexOf(p.GetColumnName())]).ToArray();
+ Context.SetStoreGeneratedValues(entity, allProperties, entityValues);
+ }
+ }
+ }
+ }
+
+ return new BulkMergeResult
+ {
+ Output = outputRows,
+ RowsAffected = rowsInserted.Values.LastOrDefault() + rowsUpdated.Values.LastOrDefault() + rowsDeleted.Values.LastOrDefault(),
+ RowsDeleted = rowsDeleted.Values.LastOrDefault(),
+ RowsInserted = rowsInserted.Values.LastOrDefault(),
+ RowsUpdated = rowsUpdated.Values.LastOrDefault()
+ };
+ }
+ private int ExecuteUpdateMySql(Expression> updateOnCondition)
+ {
+ int rowsUpdated = 0;
+ foreach (var entityType in TableMapping.EntityTypes)
+ {
+ IEnumerable columnsToUpdate = GetColumnNames(entityType);
+ string updateSetExpression = string.Join(",", columnsToUpdate.Select(c => $"t.{Context.DelimitIdentifier(c)}=s.{Context.DelimitIdentifier(c)}"));
+ string targetTableName = Context.DelimitIdentifier(entityType.GetTableName(), entityType.GetSchema() ?? Context.Database.GetDefaultSchema());
+ // MySQL UPDATE with JOIN syntax
+ string updateSql = $"UPDATE {StagingTableName} AS s INNER JOIN {targetTableName} AS t ON {CommonUtil.GetJoinConditionSql(Context, updateOnCondition, PrimaryKeyColumnNames, "s", "t")} SET {updateSetExpression}";
+ rowsUpdated = Context.Database.ExecuteSqlInternal(updateSql, Options.CommandTimeout);
+ }
+ return rowsUpdated;
+ }
+ private HashSet GetMatchedInternalIds(string targetTableName, string joinCondition)
+ {
+ var results = Context.BulkQuery(
+ $"SELECT {Context.DelimitMemberAccess("s", Constants.InternalId_ColumnName)} FROM {StagingTableName} AS s JOIN {targetTableName} AS t ON {joinCondition}",
+ Options);
+ return results.Results.Select(r => Convert.ToInt32(r[0])).ToHashSet();
+ }
+ private IProperty GetGeneratedPrimaryKeyProperty()
+ {
+ return TableMapping.EntityType.GetProperties().SingleOrDefault(o => o.IsPrimaryKey() && o.ValueGenerated != ValueGenerated.Never);
+ }
+ private void SyncMySqlAutoIncrement(IEntityType entityType)
+ {
+ var tableName = Context.DelimitIdentifier(entityType.GetTableName(), entityType.GetSchema() ?? Context.Database.GetDefaultSchema());
+ // Reset AUTO_INCREMENT to resync after bulk insert with explicit IDs
+ Context.Database.ExecuteSqlInternal($"ALTER TABLE {tableName} AUTO_INCREMENT = 1", Options.CommandTimeout);
+ }
+ internal void ValidateBulkMerge(Expression> mergeOnCondition)
+ {
+ if (PrimaryKeyColumnNames.Length == 0 && mergeOnCondition == null)
+ throw new InvalidDataException("BulkMerge requires that the entity have a primary key or that Options.MergeOnCondition be set");
+ }
+ internal void ValidateBulkUpdate(Expression> updateOnCondition)
+ {
+ if (PrimaryKeyColumnNames.Length == 0 && updateOnCondition == null)
+ throw new InvalidDataException("BulkUpdate requires that the entity have a primary key or the Options.UpdateOnCondition must be set.");
+
+ }
+ internal IEnumerable GetColumnNames(bool includePrimaryKeys = false)
+ {
+ return GetColumnNames(null, includePrimaryKeys);
+ }
+ internal IEnumerable GetColumnNames(IEntityType entityType, bool includePrimaryKeys = false)
+ {
+ return CommonUtil.FilterColumns(TableMapping.GetColumnNames(entityType, includePrimaryKeys), PrimaryKeyColumnNames, InputColumns, IgnoreColumns);
+ }
+}
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkOperationAsync.cs b/N.EntityFramework.Extensions.MySql/Data/BulkOperationAsync.cs
new file mode 100644
index 0000000..45bad43
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkOperationAsync.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+using N.EntityFrameworkCore.Extensions.Common;
+using N.EntityFrameworkCore.Extensions.Util;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+internal sealed partial class BulkOperation
+{
+ internal async Task> BulkInsertStagingDataAsync(IEnumerable entities, bool keepIdentity = true, bool useInternalId = false, CancellationToken cancellationToken = default)
+ {
+ IEnumerable columnsToInsert = GetColumnNames(keepIdentity);
+ string internalIdColumn = useInternalId ? Common.Constants.InternalId_ColumnName : null;
+ await Context.Database.CloneTableAsync(SchemaQualifiedTableNames, StagingTableName, TableMapping.GetQualifiedColumnNames(columnsToInsert), internalIdColumn, cancellationToken);
+ StagingTableCreated = true;
+ return await DbContextExtensionsAsync.BulkInsertAsync(entities, Options, TableMapping, Connection, Transaction, StagingTableName, columnsToInsert, useInternalId, cancellationToken);
+ }
+
+ internal async Task> ExecuteMergeAsync(Dictionary entityMap, Expression> mergeOnCondition,
+ bool autoMapOutput, bool keepIdentity, bool insertIfNotExists, bool update = false, bool delete = false, bool preallocatedIds = false, CancellationToken cancellationToken = default)
+ {
+ return await ExecuteMergeMySqlAsync(entityMap, mergeOnCondition, autoMapOutput, keepIdentity, insertIfNotExists, update, delete, preallocatedIds, cancellationToken);
+ }
+ internal async Task ExecuteUpdateAsync(IEnumerable entities, Expression> updateOnCondition, CancellationToken cancellationToken = default)
+ {
+ return await ExecuteUpdateMySqlAsync(updateOnCondition, cancellationToken);
+ }
+ private async Task> ExecuteMergeMySqlAsync(Dictionary entityMap, Expression> mergeOnCondition,
+ bool autoMapOutput, bool keepIdentity, bool insertIfNotExists, bool update, bool delete, bool preallocatedIds = false, CancellationToken cancellationToken = default)
+ {
+ Dictionary rowsInserted = [];
+ Dictionary rowsUpdated = [];
+ Dictionary rowsDeleted = [];
+ List> outputRows = [];
+
+ foreach (var entityType in TableMapping.EntityTypes)
+ {
+ var targetTableName = Context.DelimitIdentifier(entityType.GetTableName(), entityType.GetSchema() ?? Context.Database.GetDefaultSchema());
+ var columnsToInsert = GetColumnNames(entityType, keepIdentity).ToList();
+ var columnsToUpdate = update ? GetColumnNames(entityType).ToList() : [];
+ var autoGeneratedColumns = autoMapOutput ? TableMapping.GetAutoGeneratedColumns(entityType).ToList() : [];
+ var allProperties = autoMapOutput
+ ? TableMapping.GetEntityProperties(entityType, ValueGenerated.OnAdd).Concat(TableMapping.GetEntityProperties(entityType, ValueGenerated.OnAddOrUpdate)).ToList()
+ : [];
+
+ string matchJoinCondition = CommonUtil.GetJoinConditionSql(Context, mergeOnCondition, PrimaryKeyColumnNames, "s", "t");
+ string pkJoinCondition = CommonUtil.GetJoinConditionSql(Context, null, PrimaryKeyColumnNames, "s", "t");
+ string joinCondition = insertIfNotExists ? matchJoinCondition : "1=2";
+
+ HashSet matchedIds = autoMapOutput && update
+ ? await GetMatchedInternalIdsAsync(targetTableName, matchJoinCondition, cancellationToken)
+ : [];
+
+ rowsUpdated[entityType] = 0;
+ if (columnsToUpdate.Count > 0)
+ {
+ string updateSetExpression = string.Join(",", columnsToUpdate.Select(c => $"t.{Context.DelimitIdentifier(c)}=s.{Context.DelimitIdentifier(c)}"));
+ string updateSql = $"UPDATE {StagingTableName} AS s INNER JOIN {targetTableName} AS t ON {joinCondition} SET {updateSetExpression}";
+ rowsUpdated[entityType] = await Context.Database.ExecuteSqlAsync(updateSql, Options.CommandTimeout, cancellationToken);
+ }
+
+ string insertColumnsSql = string.Join(",", columnsToInsert.Select(Context.DelimitIdentifier));
+ string sourceColumnsSql = string.Join(",", columnsToInsert.Select(c => Context.DelimitMemberAccess("s", c)));
+ string insertSql = $"INSERT INTO {targetTableName} ({insertColumnsSql}) SELECT {sourceColumnsSql} FROM {StagingTableName} AS s WHERE NOT EXISTS (SELECT 1 FROM {targetTableName} AS t WHERE {joinCondition})";
+ rowsInserted[entityType] = await Context.Database.ExecuteSqlAsync(insertSql, Options.CommandTimeout, cancellationToken);
+ if (keepIdentity && rowsInserted[entityType] > 0)
+ await SyncMySqlAutoIncrementAsync(entityType, cancellationToken);
+
+ rowsDeleted[entityType] = 0;
+ if (TableMapping.EntityType == entityType && delete)
+ {
+ string deleteJoinCondition = (preallocatedIds && mergeOnCondition != null) ? matchJoinCondition : pkJoinCondition;
+ string deleteSql = $"DELETE t FROM {targetTableName} AS t WHERE NOT EXISTS (SELECT 1 FROM {StagingTableName} AS s WHERE {deleteJoinCondition})";
+ rowsDeleted[entityType] = await Context.Database.ExecuteSqlAsync(deleteSql, Options.CommandTimeout, cancellationToken);
+ for (int i = 0; i < rowsDeleted[entityType]; i++)
+ outputRows.Add(new BulkMergeOutputRow(SqlMergeAction.Delete));
+ }
+
+ if (autoMapOutput)
+ {
+ string outputColumnsSql = autoGeneratedColumns.Any()
+ ? "," + string.Join(",", autoGeneratedColumns.Select(c => Context.DelimitMemberAccess("t", c)))
+ : string.Empty;
+ string outputQuery = $"SELECT {Context.DelimitMemberAccess("s", Constants.InternalId_ColumnName)}{outputColumnsSql} FROM {StagingTableName} AS s JOIN {targetTableName} AS t ON {matchJoinCondition}";
+ var bulkQueryResult = await Context.BulkQueryAsync(outputQuery, Connection, Transaction, Options, cancellationToken);
+ var autoGeneratedColumnList = autoGeneratedColumns.ToList();
+ foreach (var result in bulkQueryResult.Results)
+ {
+ int entityId = Convert.ToInt32(result[0]);
+ string action = matchedIds.Contains(entityId) ? SqlMergeAction.Update : SqlMergeAction.Insert;
+ outputRows.Add(new BulkMergeOutputRow(action));
+
+ if (entityMap.TryGetValue(entityId, out var entity) && allProperties.Count > 0)
+ {
+ object[] entityValues = allProperties.Select(p => result[1 + autoGeneratedColumnList.IndexOf(p.GetColumnName())]).ToArray();
+ Context.SetStoreGeneratedValues(entity, allProperties, entityValues);
+ }
+ }
+ }
+ }
+
+ return new BulkMergeResult
+ {
+ Output = outputRows,
+ RowsAffected = rowsInserted.Values.LastOrDefault() + rowsUpdated.Values.LastOrDefault() + rowsDeleted.Values.LastOrDefault(),
+ RowsDeleted = rowsDeleted.Values.LastOrDefault(),
+ RowsInserted = rowsInserted.Values.LastOrDefault(),
+ RowsUpdated = rowsUpdated.Values.LastOrDefault()
+ };
+ }
+ private async Task ExecuteUpdateMySqlAsync(Expression> updateOnCondition, CancellationToken cancellationToken)
+ {
+ int rowsUpdated = 0;
+ foreach (var entityType in TableMapping.EntityTypes)
+ {
+ IEnumerable columnsToUpdate = GetColumnNames(entityType);
+ string updateSetExpression = string.Join(",", columnsToUpdate.Select(c => $"t.{Context.DelimitIdentifier(c)}=s.{Context.DelimitIdentifier(c)}"));
+ string targetTableName = Context.DelimitIdentifier(entityType.GetTableName(), entityType.GetSchema() ?? Context.Database.GetDefaultSchema());
+ string updateSql = $"UPDATE {StagingTableName} AS s INNER JOIN {targetTableName} AS t ON {CommonUtil.GetJoinConditionSql(Context, updateOnCondition, PrimaryKeyColumnNames, "s", "t")} SET {updateSetExpression}";
+ rowsUpdated = await Context.Database.ExecuteSqlAsync(updateSql, Options.CommandTimeout, cancellationToken);
+ }
+ return rowsUpdated;
+ }
+ internal async Task PreallocateIdentityValuesAsync(IEnumerable entities, CancellationToken cancellationToken)
+ {
+ // No-op for MySQL
+ await Task.CompletedTask;
+ }
+ private async Task> GetMatchedInternalIdsAsync(string targetTableName, string joinCondition, CancellationToken cancellationToken)
+ {
+ var results = await Context.BulkQueryAsync(
+ $"SELECT {Context.DelimitMemberAccess("s", Constants.InternalId_ColumnName)} FROM {StagingTableName} AS s JOIN {targetTableName} AS t ON {joinCondition}",
+ Connection, Transaction, Options, cancellationToken);
+ return results.Results.Select(r => Convert.ToInt32(r[0])).ToHashSet();
+ }
+ private async Task SyncMySqlAutoIncrementAsync(IEntityType entityType, CancellationToken cancellationToken)
+ {
+ var tableName = Context.DelimitIdentifier(entityType.GetTableName(), entityType.GetSchema() ?? Context.Database.GetDefaultSchema());
+ await Context.Database.ExecuteSqlAsync($"ALTER TABLE {tableName} AUTO_INCREMENT = 1", Options.CommandTimeout, cancellationToken);
+ }
+}
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkOptions.cs b/N.EntityFramework.Extensions.MySql/Data/BulkOptions.cs
new file mode 100644
index 0000000..6b9ae99
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkOptions.cs
@@ -0,0 +1,18 @@
+using Microsoft.EntityFrameworkCore.Metadata;
+using N.EntityFrameworkCore.Extensions.Enums;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkOptions
+{
+ public int BatchSize { get; set; }
+ public bool UsePermanentTable { get; set; }
+ public int? CommandTimeout { get; set; }
+ internal ConnectionBehavior ConnectionBehavior { get; set; }
+ internal IEntityType EntityType { get; set; }
+
+ public BulkOptions()
+ {
+ ConnectionBehavior = ConnectionBehavior.Default;
+ }
+}
\ No newline at end of file
diff --git a/N.EntityFramework.Extensions.MySql/Data/BulkQueryResult.cs b/N.EntityFramework.Extensions.MySql/Data/BulkQueryResult.cs
new file mode 100644
index 0000000..267af6f
--- /dev/null
+++ b/N.EntityFramework.Extensions.MySql/Data/BulkQueryResult.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace N.EntityFrameworkCore.Extensions;
+
+public class BulkQueryResult
+{
+ public IEnumerable