Skip to content

Commit 5cf9062

Browse files
CopilotPhenX
andcommitted
Fix complex property update in ON CONFLICT clause and add tests
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
1 parent e6ae8ac commit 5cf9062

2 files changed

Lines changed: 141 additions & 3 deletions

File tree

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,20 @@ private string ToSqlExpression<TEntity>(DbContext context, TableMetadata table,
297297
case MemberExpression memberExpr:
298298
var columnName = table.GetColumnName(memberExpr.Member.Name);
299299

300+
// Traverse up the expression chain to find the root parameter
301+
// This handles both simple properties (e.g., excluded.Name) and
302+
// complex properties (e.g., excluded.ComplexObject.Property)
303+
var rootParam = GetRootParameter(memberExpr);
304+
300305
// If the member expression is a property of the current lambda
301-
if (lambda is { Parameters.Count: > 1 } && memberExpr.Expression is ParameterExpression paramExpr)
306+
if (lambda is { Parameters.Count: > 1 } && rootParam != null)
302307
{
303-
if (paramExpr.Name == lambda.Parameters[0].Name)
308+
if (rootParam.Name == lambda.Parameters[0].Name)
304309
{
305310
return GetInsertedColumnName(columnName);
306311
}
307312

308-
if (paramExpr.Name == lambda.Parameters[1].Name)
313+
if (rootParam.Name == lambda.Parameters[1].Name)
309314
{
310315
return GetExcludedColumnName(columnName);
311316
}
@@ -405,4 +410,31 @@ private string ToSqlExpression<TEntity>(DbContext context, TableMetadata table,
405410
throw new NotSupportedException($"Expression not supported: {expr.NodeType}");
406411
}
407412
}
413+
414+
/// <summary>
415+
/// Traverses up a member expression chain to find the root parameter expression.
416+
/// This handles both simple properties (e.g., excluded.Name) and complex properties (e.g., excluded.ComplexObject.Property).
417+
/// </summary>
418+
private static ParameterExpression? GetRootParameter(MemberExpression memberExpr)
419+
{
420+
Expression? current = memberExpr.Expression;
421+
while (current != null)
422+
{
423+
if (current is ParameterExpression param)
424+
{
425+
return param;
426+
}
427+
428+
if (current is MemberExpression nested)
429+
{
430+
current = nested.Expression;
431+
}
432+
else
433+
{
434+
break;
435+
}
436+
}
437+
438+
return null;
439+
}
408440
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Merge/MergeTestsBase.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,110 @@ public async Task InsertEntities_WithConflict_MultipleColumns(InsertStrategy str
348348
Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2");
349349
Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1 - Conflict" && e.Price == 0);
350350
}
351+
352+
[SkippableTheory]
353+
[InlineData(InsertStrategy.InsertReturn)]
354+
[InlineData(InsertStrategy.InsertReturnAsync)]
355+
public async Task InsertEntities_WithComplexType_UpdateAll(InsertStrategy strategy)
356+
{
357+
Skip.If(_context.IsProvider(ProviderType.MySql));
358+
// Oracle MERGE does not support returning entities
359+
Skip.If(_context.IsProvider(ProviderType.Oracle));
360+
361+
// Arrange
362+
var entities = new List<TestEntityWithComplexType>
363+
{
364+
new TestEntityWithComplexType
365+
{
366+
TestRun = _run,
367+
OwnedComplexType = new OwnedObject { Code = 1, Name = "Name1" }
368+
},
369+
new TestEntityWithComplexType
370+
{
371+
TestRun = _run,
372+
OwnedComplexType = new OwnedObject { Code = 2, Name = "Name2" }
373+
}
374+
};
375+
376+
// Act - First insert (without CopyGeneratedColumns - returns generated IDs via RETURNING)
377+
var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities);
378+
379+
// Update the complex properties
380+
foreach (var entity in insertedEntities)
381+
{
382+
entity.OwnedComplexType = new OwnedObject
383+
{
384+
Code = entity.OwnedComplexType.Code + 100,
385+
Name = $"Updated_{entity.OwnedComplexType.Name}"
386+
};
387+
}
388+
389+
// Act - Second insert with update on conflict
390+
// Using 'inserted' parameter to update all columns (both behave the same for ParameterExpression case)
391+
var updatedEntities = await _context.InsertWithStrategyAsync(strategy, insertedEntities, o => o.CopyGeneratedColumns = true,
392+
onConflict: new OnConflictOptions<TestEntityWithComplexType>
393+
{
394+
Update = (inserted, excluded) => inserted,
395+
});
396+
397+
// Assert - complex properties should be updated
398+
Assert.Equal(2, updatedEntities.Count);
399+
Assert.All(updatedEntities, e =>
400+
{
401+
Assert.StartsWith("Updated_", e.OwnedComplexType.Name);
402+
Assert.True(e.OwnedComplexType.Code > 100);
403+
});
404+
}
405+
406+
[SkippableTheory]
407+
[InlineData(InsertStrategy.InsertReturn)]
408+
[InlineData(InsertStrategy.InsertReturnAsync)]
409+
public async Task InsertEntities_WithComplexType_UpdateWithWhere(InsertStrategy strategy)
410+
{
411+
Skip.If(_context.IsProvider(ProviderType.MySql));
412+
// Oracle MERGE does not support returning entities
413+
Skip.If(_context.IsProvider(ProviderType.Oracle));
414+
415+
// Arrange
416+
var entities = new List<TestEntityWithComplexType>
417+
{
418+
new TestEntityWithComplexType
419+
{
420+
TestRun = _run,
421+
OwnedComplexType = new OwnedObject { Code = 10, Name = "Original1" }
422+
},
423+
new TestEntityWithComplexType
424+
{
425+
TestRun = _run,
426+
OwnedComplexType = new OwnedObject { Code = 20, Name = "Original2" }
427+
}
428+
};
429+
430+
// Act - First insert (without CopyGeneratedColumns - returns generated IDs via RETURNING)
431+
var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities);
432+
433+
// Update the complex property name
434+
foreach (var entity in insertedEntities)
435+
{
436+
entity.OwnedComplexType.Name = $"Changed_{entity.OwnedComplexType.Name}";
437+
entity.OwnedComplexType.Code = entity.OwnedComplexType.Code + 100;
438+
}
439+
440+
// Act - Second insert updating complex properties with a WHERE condition
441+
// This tests that complex property access works correctly in the Where clause
442+
var updatedEntities = await _context.InsertWithStrategyAsync(strategy, insertedEntities, o => o.CopyGeneratedColumns = true,
443+
onConflict: new OnConflictOptions<TestEntityWithComplexType>
444+
{
445+
Update = (inserted, excluded) => inserted,
446+
Where = (inserted, excluded) => excluded.OwnedComplexType.Code > inserted.OwnedComplexType.Code
447+
});
448+
449+
// Assert - entities should be updated because excluded.Code (110, 120) > inserted.Code (10, 20)
450+
Assert.Equal(2, updatedEntities.Count);
451+
Assert.All(updatedEntities, e =>
452+
{
453+
Assert.StartsWith("Changed_", e.OwnedComplexType.Name);
454+
Assert.True(e.OwnedComplexType.Code > 100);
455+
});
456+
}
351457
}

0 commit comments

Comments
 (0)