Skip to content

Commit c9a8eb6

Browse files
CopilotPhenXfabien.menager
authored
Fix SQL column names with spaces and reserved keywords (#86)
* Initial plan * 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> * 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> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> Co-authored-by: fabien.menager <fabien.menager@am-creations.fr>
1 parent 86c0766 commit c9a8eb6

4 files changed

Lines changed: 128 additions & 3 deletions

File tree

src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ protected IEnumerable<string> GetUpdates<T>(DbContext context, TableMetadata tab
246246
{
247247
foreach (var arg in newExpr.Arguments.Zip(newExpr.Members, (expr, member) => (expr, member)))
248248
{
249-
yield return $"{table.GetColumnName(arg.member.Name)} = {ToSqlExpression<T>(context, table, arg.expr, lambda)}";
249+
yield return $"{table.GetQuotedColumnName(arg.member.Name)} = {ToSqlExpression<T>(context, table, arg.expr, lambda)}";
250250
}
251251

252252
break;
@@ -255,13 +255,13 @@ protected IEnumerable<string> GetUpdates<T>(DbContext context, TableMetadata tab
255255
{
256256
foreach (var binding in memberInit.Bindings.OfType<MemberAssignment>())
257257
{
258-
yield return $"{table.GetColumnName(binding.Member.Name)} = {ToSqlExpression<T>(context, table, binding.Expression, lambda)}";
258+
yield return $"{table.GetQuotedColumnName(binding.Member.Name)} = {ToSqlExpression<T>(context, table, binding.Expression, lambda)}";
259259
}
260260

261261
break;
262262
}
263263
case MemberExpression memberExpr:
264-
yield return $"{table.GetColumnName(memberExpr.Member.Name)} = {ToSqlExpression<T>(context, table, memberExpr, lambda)}";
264+
yield return $"{table.GetQuotedColumnName(memberExpr.Member.Name)} = {ToSqlExpression<T>(context, table, memberExpr, lambda)}";
265265
break;
266266

267267
case ParameterExpression parameterExpr when (parameterExpr.Type == typeof(T)):

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class TestDbContext : TestDbContextBase
1414
public DbSet<TestEntityWithConverters> TestEntitiesWithConverter { get; set; } = null!;
1515
public DbSet<TestEntityWithComplexType> TestEntitiesWithComplexType { get; set; } = null!;
1616
public DbSet<TestEntityWithSmartEnum> TestEntitiesWithSmartEnum { get; set; } = null!;
17+
public DbSet<TestEntityWithSpecialColumnNames> TestEntitiesWithSpecialColumnNames { get; set; } = null!;
1718
public DbSet<Student> Students { get; set; } = null!;
1819
public DbSet<Course> Courses { get; set; } = null!;
1920

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.ComponentModel.DataAnnotations.Schema;
3+
4+
using Microsoft.EntityFrameworkCore;
5+
6+
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
7+
8+
/// <summary>
9+
/// Test entity with column names containing spaces and SQL reserved keywords.
10+
/// This is used to test that column names are properly quoted in SQL statements.
11+
/// </summary>
12+
[PrimaryKey(nameof(Id))]
13+
[Index(nameof(BusinessFunctionText), IsUnique = true)]
14+
[Table("test_entity_special_columns")]
15+
public class TestEntityWithSpecialColumnNames : TestEntityBase
16+
{
17+
public int Id { get; set; }
18+
19+
/// <summary>
20+
/// Column name with spaces and SQL reserved keyword "Function".
21+
/// </summary>
22+
[Column("Business Function Text")]
23+
[MaxLength(255)]
24+
public string BusinessFunctionText { get; set; } = string.Empty;
25+
26+
/// <summary>
27+
/// Column name with SQL reserved keyword "Order".
28+
/// </summary>
29+
[Column("Order Number")]
30+
public int OrderNumber { get; set; }
31+
32+
/// <summary>
33+
/// Regular column name for comparison.
34+
/// </summary>
35+
[Column("description")]
36+
[MaxLength(500)]
37+
public string Description { get; set; } = string.Empty;
38+
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Various/VariousTestsBase.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using FluentAssertions;
22

3+
using PhenX.EntityFrameworkCore.BulkInsert.Enums;
4+
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
5+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
36
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
47
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
58

@@ -42,4 +45,87 @@ public async Task InsertSmartEnumEntities(InsertStrategy strategy)
4245
insertedEntities.Should().BeEquivalentTo(entities,
4346
o => o.RespectingRuntimeTypes().Excluding(e => e.Id));
4447
}
48+
49+
/// <summary>
50+
/// Tests that column names with spaces and SQL reserved keywords are properly quoted.
51+
/// This addresses the issue where columns like "Business Function Text" were not being
52+
/// properly escaped, causing SQL syntax errors.
53+
/// </summary>
54+
[SkippableTheory]
55+
[CombinatorialData]
56+
public async Task InsertEntities_WithSpecialColumnNames(InsertStrategy strategy)
57+
{
58+
// Arrange
59+
var entities = new List<TestEntityWithSpecialColumnNames>
60+
{
61+
new TestEntityWithSpecialColumnNames
62+
{
63+
TestRun = _run,
64+
BusinessFunctionText = $"{_run}_BusinessFunction1",
65+
OrderNumber = 100,
66+
Description = "Test description 1"
67+
},
68+
new TestEntityWithSpecialColumnNames
69+
{
70+
TestRun = _run,
71+
BusinessFunctionText = $"{_run}_BusinessFunction2",
72+
OrderNumber = 200,
73+
Description = "Test description 2"
74+
}
75+
};
76+
77+
// Act
78+
var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities);
79+
80+
// Assert
81+
insertedEntities.Should().BeEquivalentTo(entities,
82+
o => o.RespectingRuntimeTypes().Excluding(e => e.Id));
83+
}
84+
85+
/// <summary>
86+
/// Tests that merge/upsert operations work correctly with column names containing
87+
/// spaces and SQL reserved keywords.
88+
/// </summary>
89+
[SkippableTheory]
90+
[CombinatorialData]
91+
public async Task InsertEntities_WithSpecialColumnNames_Merge(InsertStrategy strategy)
92+
{
93+
Skip.If(_context.IsProvider(ProviderType.MySql, ProviderType.Oracle));
94+
95+
// Arrange
96+
var entities = new List<TestEntityWithSpecialColumnNames>
97+
{
98+
new TestEntityWithSpecialColumnNames
99+
{
100+
TestRun = _run,
101+
BusinessFunctionText = $"{_run}_BusinessFunction1",
102+
OrderNumber = 100,
103+
Description = "Initial description"
104+
}
105+
};
106+
107+
// First insert
108+
await _context.InsertWithStrategyAsync(strategy, entities);
109+
110+
// Update entity for upsert
111+
entities[0].OrderNumber = 200;
112+
entities[0].Description = "Updated description";
113+
114+
// Act - Merge with update on conflict
115+
var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, _ => { },
116+
onConflict: new OnConflictOptions<TestEntityWithSpecialColumnNames>
117+
{
118+
Match = e => new { e.BusinessFunctionText },
119+
Update = (inserted, excluded) => new TestEntityWithSpecialColumnNames
120+
{
121+
OrderNumber = excluded.OrderNumber,
122+
Description = excluded.Description
123+
}
124+
});
125+
126+
// Assert
127+
insertedEntities.Should().HaveCount(1);
128+
insertedEntities[0].OrderNumber.Should().Be(200);
129+
insertedEntities[0].Description.Should().Be("Updated description");
130+
}
45131
}

0 commit comments

Comments
 (0)