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..748e447 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, ProviderType.Oracle)); + + // 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"); + } }