Skip to content

Commit 622ee0c

Browse files
CopilotPhenX
andcommitted
Fix Oracle MERGE syntax - remove AS keyword, wrap ON clause in parentheses
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
1 parent 11e3838 commit 622ee0c

3 files changed

Lines changed: 55 additions & 18 deletions

File tree

src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleDialectBuilder.cs

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ public override string BuildMoveDataSql<T>(
3535
// Merge handling
3636
if (onConflict is OnConflictOptions<T> onConflictTyped)
3737
{
38+
// Oracle MERGE doesn't support returning entities
39+
if (returnedColumns.Count != 0)
40+
{
41+
throw new NotSupportedException("Oracle MERGE does not support returning entities. Use ExecuteBulkInsertAsync without returning results when using conflict resolution.");
42+
}
43+
3844
IEnumerable<string> matchColumns;
3945
if (onConflictTyped.Match != null)
4046
{
@@ -49,27 +55,21 @@ public override string BuildMoveDataSql<T>(
4955
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
5056
}
5157

52-
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS {PseudoTableInserted}");
58+
// Oracle MERGE syntax does NOT use AS for table aliases
59+
q.AppendLine($"MERGE INTO {target.QuotedTableName} {PseudoTableInserted}");
5360

5461
q.Append("USING (SELECT ");
5562
q.AppendColumns(insertedColumns);
56-
q.Append($" FROM {source}) AS {PseudoTableExcluded} (");
57-
q.AppendColumns(insertedColumns);
58-
q.AppendLine(")");
59-
60-
q.Append("ON ");
61-
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
63+
// Oracle MERGE syntax does NOT use AS for subquery aliases
64+
q.Append($" FROM {source}) {PseudoTableExcluded}");
6265
q.AppendLine();
6366

64-
if (onConflictTyped.Update != null)
65-
{
66-
var columns = target.GetColumns(false);
67-
68-
q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
69-
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
70-
q.AppendLine();
71-
}
67+
// Oracle requires ON clause conditions to be wrapped in parentheses
68+
q.Append("ON (");
69+
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
70+
q.AppendLine(")");
7271

72+
// Oracle MERGE puts WHEN NOT MATCHED before WHEN MATCHED for insert-first logic
7373
q.Append("WHEN NOT MATCHED THEN INSERT (");
7474
q.AppendColumns(insertedColumns);
7575
q.AppendLine(")");
@@ -78,10 +78,25 @@ public override string BuildMoveDataSql<T>(
7878
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"{PseudoTableExcluded}.{col.QuotedColumName}"));
7979
q.AppendLine(")");
8080

81-
if (returnedColumns.Count != 0)
81+
if (onConflictTyped.Update != null)
8282
{
83-
q.Append("OUTPUT ");
84-
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
83+
var columns = target.GetColumns(false);
84+
85+
q.Append("WHEN MATCHED ");
86+
87+
if (onConflictTyped.RawWhere != null || onConflictTyped.Where != null)
88+
{
89+
if (onConflictTyped is { RawWhere: not null, Where: not null })
90+
{
91+
throw new ArgumentException("Cannot specify both RawWhere and Where in OnConflictOptions.");
92+
}
93+
94+
q.Append("AND ");
95+
AppendConflictCondition(q, target, context, onConflictTyped);
96+
}
97+
98+
q.AppendLine("THEN UPDATE SET ");
99+
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
85100
q.AppendLine();
86101
}
87102
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ public async Task InsertEntities_MultipleTimes_With_Conflict_On_Id(InsertStrateg
126126
public async Task InsertEntities_WithConflict_SingleColumn(InsertStrategy strategy)
127127
{
128128
Skip.If(_context.IsProvider(ProviderType.MySql));
129+
// Oracle MERGE does not support returning entities
130+
Skip.If(_context.IsProvider(ProviderType.Oracle));
129131

130132
// Arrange
131133
_context.TestEntities.Add(new TestEntity { TestRun = _run,Name = $"{_run}_Entity1" });
@@ -194,6 +196,8 @@ public async Task InsertEntities_WithConflict_DoNothing(InsertStrategy strategy)
194196
public async Task InsertEntities_WithConflict_RawCondition(InsertStrategy strategy)
195197
{
196198
Skip.If(_context.IsProvider(ProviderType.MySql));
199+
// Oracle MERGE does not support returning entities
200+
Skip.If(_context.IsProvider(ProviderType.Oracle));
197201

198202
// Arrange
199203
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
@@ -237,6 +241,8 @@ public async Task InsertEntities_WithConflict_RawCondition(InsertStrategy strate
237241
public async Task InsertEntities_WithConflict_ExpressionCondition(InsertStrategy strategy)
238242
{
239243
Skip.If(_context.IsProvider(ProviderType.MySql));
244+
// Oracle MERGE does not support returning entities
245+
Skip.If(_context.IsProvider(ProviderType.Oracle));
240246

241247
// Arrange
242248
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
@@ -280,6 +286,8 @@ public async Task InsertEntities_WithConflict_ExpressionCondition(InsertStrategy
280286
public async Task InsertEntities_WithConflict_ComplexExpressionCondition(InsertStrategy strategy)
281287
{
282288
Skip.If(_context.IsProvider(ProviderType.MySql));
289+
// Oracle MERGE does not support returning entities
290+
Skip.If(_context.IsProvider(ProviderType.Oracle));
283291

284292
// Arrange
285293
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
@@ -312,6 +320,8 @@ public async Task InsertEntities_WithConflict_ComplexExpressionCondition(InsertS
312320
public async Task InsertEntities_WithConflict_MultipleColumns(InsertStrategy strategy)
313321
{
314322
Skip.If(_context.IsProvider(ProviderType.MySql));
323+
// Oracle MERGE does not support returning entities
324+
Skip.If(_context.IsProvider(ProviderType.Oracle));
315325

316326
// Arrange
317327
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
2+
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
3+
4+
using Xunit;
5+
6+
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Merge;
7+
8+
[Trait("Category", "Oracle")]
9+
[Collection(TestDbContainerOracleCollection.Name)]
10+
public class MergeTestsOracle(TestDbContainerOracle dbContainer) : MergeTestsBase<TestDbContextOracle>(dbContainer)
11+
{
12+
}

0 commit comments

Comments
 (0)