From 10477ea578ae1f730474c4625d0a216cf4e10082 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:34:19 +0000 Subject: [PATCH 1/3] Initial plan From 3e2174f847b03472a7d200a395c77f8764910c1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:42:34 +0000 Subject: [PATCH 2/3] Fix SQL column names with spaces and reserved keywords Changed GetColumnName to GetQuotedColumnName in SqlDialectBuilder.GetUpdates() to properly quote column names in UPDATE SET clauses. Added test entity TestEntityWithSpecialColumnNames with columns like "Business Function Text" and "Order Number" to verify the fix. Added test cases for both basic insert and merge operations with special column names. Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../Dialect/SqlDialectBuilder.cs | 6 +- .../DbContext/TestDbContext.cs | 1 + .../TestEntityWithSpecialColumnNames.cs | 38 ++++++++ .../Tests/Various/VariousTestsBase.cs | 86 +++++++++++++++++++ 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithSpecialColumnNames.cs diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs index 4508f69..4dd19cb 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs @@ -246,7 +246,7 @@ protected IEnumerable GetUpdates(DbContext context, TableMetadata tab { foreach (var arg in newExpr.Arguments.Zip(newExpr.Members, (expr, member) => (expr, member))) { - yield return $"{table.GetColumnName(arg.member.Name)} = {ToSqlExpression(context, table, arg.expr, lambda)}"; + yield return $"{table.GetQuotedColumnName(arg.member.Name)} = {ToSqlExpression(context, table, arg.expr, lambda)}"; } break; @@ -255,13 +255,13 @@ protected IEnumerable GetUpdates(DbContext context, TableMetadata tab { foreach (var binding in memberInit.Bindings.OfType()) { - yield return $"{table.GetColumnName(binding.Member.Name)} = {ToSqlExpression(context, table, binding.Expression, lambda)}"; + yield return $"{table.GetQuotedColumnName(binding.Member.Name)} = {ToSqlExpression(context, table, binding.Expression, lambda)}"; } break; } case MemberExpression memberExpr: - yield return $"{table.GetColumnName(memberExpr.Member.Name)} = {ToSqlExpression(context, table, memberExpr, lambda)}"; + yield return $"{table.GetQuotedColumnName(memberExpr.Member.Name)} = {ToSqlExpression(context, table, memberExpr, lambda)}"; break; case ParameterExpression parameterExpr when (parameterExpr.Type == typeof(T)): diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs index 98b4ae0..8cb9d64 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs @@ -14,6 +14,7 @@ public class TestDbContext : TestDbContextBase public DbSet TestEntitiesWithConverter { get; set; } = null!; public DbSet TestEntitiesWithComplexType { get; set; } = null!; public DbSet TestEntitiesWithSmartEnum { get; set; } = null!; + public DbSet TestEntitiesWithSpecialColumnNames { get; set; } = null!; public DbSet Students { get; set; } = null!; public DbSet Courses { get; set; } = null!; diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithSpecialColumnNames.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithSpecialColumnNames.cs new file mode 100644 index 0000000..a5ab8f6 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithSpecialColumnNames.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using Microsoft.EntityFrameworkCore; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +/// +/// Test entity with column names containing spaces and SQL reserved keywords. +/// This is used to test that column names are properly quoted in SQL statements. +/// +[PrimaryKey(nameof(Id))] +[Index(nameof(BusinessFunctionText), IsUnique = true)] +[Table("test_entity_special_columns")] +public class TestEntityWithSpecialColumnNames : TestEntityBase +{ + public int Id { get; set; } + + /// + /// Column name with spaces and SQL reserved keyword "Function". + /// + [Column("Business Function Text")] + [MaxLength(255)] + public string BusinessFunctionText { get; set; } = string.Empty; + + /// + /// Column name with SQL reserved keyword "Order". + /// + [Column("Order Number")] + public int OrderNumber { get; set; } + + /// + /// Regular column name for comparison. + /// + [Column("description")] + [MaxLength(500)] + public string Description { get; set; } = string.Empty; +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs index e3769f5..d5d9983 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs @@ -1,5 +1,8 @@ using FluentAssertions; +using PhenX.EntityFrameworkCore.BulkInsert.Enums; +using PhenX.EntityFrameworkCore.BulkInsert.Extensions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; @@ -42,4 +45,87 @@ public async Task InsertSmartEnumEntities(InsertStrategy strategy) insertedEntities.Should().BeEquivalentTo(entities, o => o.RespectingRuntimeTypes().Excluding(e => e.Id)); } + + /// + /// Tests that column names with spaces and SQL reserved keywords are properly quoted. + /// This addresses the issue where columns like "Business Function Text" were not being + /// properly escaped, causing SQL syntax errors. + /// + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithSpecialColumnNames(InsertStrategy strategy) + { + // Arrange + var entities = new List + { + new TestEntityWithSpecialColumnNames + { + TestRun = _run, + BusinessFunctionText = $"{_run}_BusinessFunction1", + OrderNumber = 100, + Description = "Test description 1" + }, + new TestEntityWithSpecialColumnNames + { + TestRun = _run, + BusinessFunctionText = $"{_run}_BusinessFunction2", + OrderNumber = 200, + Description = "Test description 2" + } + }; + + // Act + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); + + // Assert + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding(e => e.Id)); + } + + /// + /// Tests that merge/upsert operations work correctly with column names containing + /// spaces and SQL reserved keywords. + /// + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithSpecialColumnNames_Merge(InsertStrategy strategy) + { + Skip.If(_context.IsProvider(ProviderType.MySql)); + + // Arrange + var entities = new List + { + new TestEntityWithSpecialColumnNames + { + TestRun = _run, + BusinessFunctionText = $"{_run}_BusinessFunction1", + OrderNumber = 100, + Description = "Initial description" + } + }; + + // First insert + await _context.InsertWithStrategyAsync(strategy, entities); + + // Update entity for upsert + entities[0].OrderNumber = 200; + entities[0].Description = "Updated description"; + + // Act - Merge with update on conflict + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, _ => { }, + onConflict: new OnConflictOptions + { + Match = e => new { e.BusinessFunctionText }, + Update = (inserted, excluded) => new TestEntityWithSpecialColumnNames + { + OrderNumber = excluded.OrderNumber, + Description = excluded.Description + } + }); + + // Assert + insertedEntities.Should().HaveCount(1); + insertedEntities[0].OrderNumber.Should().Be(200); + insertedEntities[0].Description.Should().Be("Updated description"); + } } From 1045b600d731aaff5cc418a19e960ecaba1b04a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 11:01:13 +0000 Subject: [PATCH 3/3] Skip InsertEntities_WithSpecialColumnNames_Merge test for Oracle Oracle does not support insert+merge+return operations. Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../Tests/Various/VariousTestsBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs index d5d9983..748e447 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs @@ -90,7 +90,7 @@ public async Task InsertEntities_WithSpecialColumnNames(InsertStrategy strategy) [CombinatorialData] public async Task InsertEntities_WithSpecialColumnNames_Merge(InsertStrategy strategy) { - Skip.If(_context.IsProvider(ProviderType.MySql)); + Skip.If(_context.IsProvider(ProviderType.MySql, ProviderType.Oracle)); // Arrange var entities = new List