Skip to content

Commit 0548cf5

Browse files
CopilotPhenX
andcommitted
Fix Oracle MERGE: validate match columns, exclude match columns from updates
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
1 parent 302f90e commit 0548cf5

2 files changed

Lines changed: 30 additions & 5 deletions

File tree

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,33 @@ public override string BuildMoveDataSql<T>(
4242
}
4343

4444
IEnumerable<string> matchColumns;
45+
IReadOnlyList<string> matchColumnsList;
4546
if (onConflictTyped.Match != null)
4647
{
47-
matchColumns = GetColumns(target, onConflictTyped.Match);
48+
matchColumnsList = GetColumns(target, onConflictTyped.Match).ToList();
4849
}
4950
else if (target.PrimaryKey.Length > 0)
5051
{
51-
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
52+
matchColumnsList = target.PrimaryKey.Select(x => x.QuotedColumName).ToList();
5253
}
5354
else
5455
{
5556
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
5657
}
58+
matchColumns = matchColumnsList;
59+
60+
// Validate that all match columns are available in the source subquery
61+
var insertedColumnNames = insertedColumns.Select(c => c.QuotedColumName).ToHashSet();
62+
var missingMatchColumns = matchColumnsList.Where(c => !insertedColumnNames.Contains(c)).ToList();
63+
if (missingMatchColumns.Count != 0)
64+
{
65+
throw new InvalidOperationException(
66+
$"Oracle MERGE requires match columns to be present in the source data. " +
67+
$"The following match columns are not available: {string.Join(", ", missingMatchColumns)}. " +
68+
$"This can happen when using auto-generated primary key columns for conflict detection. " +
69+
$"Use the 'Match' option to specify non-generated columns for conflict detection, " +
70+
$"or set 'CopyGeneratedColumns = true' if the generated column values are provided.");
71+
}
5772

5873
// Oracle MERGE syntax does NOT use AS for table aliases
5974
q.AppendLine($"MERGE INTO {target.QuotedTableName} {PseudoTableInserted}");
@@ -80,8 +95,6 @@ public override string BuildMoveDataSql<T>(
8095

8196
if (onConflictTyped.Update != null)
8297
{
83-
var columns = target.GetColumns(false);
84-
8598
q.Append("WHEN MATCHED ");
8699

87100
if (onConflictTyped.RawWhere != null || onConflictTyped.Where != null)
@@ -96,7 +109,17 @@ public override string BuildMoveDataSql<T>(
96109
}
97110

98111
q.AppendLine("THEN UPDATE SET ");
99-
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
112+
// Oracle MERGE: columns in ON clause cannot be updated, so exclude match columns
113+
// Use insertedColumns instead of all columns because the USING subquery only contains insertedColumns
114+
var matchColumnSet = matchColumnsList.ToHashSet();
115+
var updateableColumns = insertedColumns.Where(c => !matchColumnSet.Contains(c.QuotedColumName)).ToList();
116+
if (updateableColumns.Count == 0)
117+
{
118+
throw new InvalidOperationException(
119+
"Oracle MERGE cannot update any columns because all available columns are used in the ON clause for conflict detection. " +
120+
"Specify different columns in the 'Match' option or use specific columns in the 'Update' expression.");
121+
}
122+
q.AppendJoin(", ", GetUpdates(context, target, updateableColumns, onConflictTyped.Update));
100123
q.AppendLine();
101124
}
102125
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public async Task InsertEntities_MultipleTimes(InsertStrategy strategy)
3333
{
3434
Skip.If(_context.IsProvider(ProviderType.PostgreSql));
3535
Skip.If(_context.IsProvider(ProviderType.SqlServer));
36+
// Oracle MERGE requires match columns to be in the source data; auto-generated Id is not available
37+
Skip.If(_context.IsProvider(ProviderType.Oracle));
3638

3739
// Arrange
3840
var entities = new List<TestEntity>

0 commit comments

Comments
 (0)