Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ protected override Task BulkInsert<T>(

using var bulkCopy = new OracleBulkCopy(connection, options.CopyOptions);

bulkCopy.DestinationTableName = tableInfo.QuotedTableName;
bulkCopy.DestinationTableName = tableName;
bulkCopy.BatchSize = options.BatchSize;
bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,41 +35,54 @@ public override string BuildMoveDataSql<T>(
// Merge handling
if (onConflict is OnConflictOptions<T> onConflictTyped)
{
IEnumerable<string> matchColumns;
// Oracle MERGE doesn't support returning entities
if (returnedColumns.Count != 0)
{
throw new NotSupportedException("Oracle MERGE does not support returning entities. Use ExecuteBulkInsertAsync without returning results when using conflict resolution.");
}

IReadOnlyList<string> matchColumns;
if (onConflictTyped.Match != null)
{
matchColumns = GetColumns(target, onConflictTyped.Match);
matchColumns = GetColumns(target, onConflictTyped.Match).ToList();
}
else if (target.PrimaryKey.Length > 0)
{
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName).ToList();
}
else
{
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
}

q.AppendLine($"MERGE INTO {target.QuotedTableName} AS {PseudoTableInserted}");
// Validate that all match columns are available in the source subquery
var insertedColumnNames = insertedColumns.Select(c => c.QuotedColumName).ToHashSet();
var missingMatchColumns = matchColumns.Where(c => !insertedColumnNames.Contains(c)).ToList();
if (missingMatchColumns.Count != 0)
{
throw new InvalidOperationException(
$"Oracle MERGE requires match columns to be present in the source data. " +
$"The following match columns are not available: {string.Join(", ", missingMatchColumns)}. " +
$"This can happen when using auto-generated primary key columns for conflict detection. " +
$"Use the 'Match' option to specify non-generated columns for conflict detection, " +
$"or set 'CopyGeneratedColumns = true' if the generated column values are provided.");
}

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

q.Append("USING (SELECT ");
q.AppendColumns(insertedColumns);
q.Append($" FROM {source}) AS {PseudoTableExcluded} (");
q.AppendColumns(insertedColumns);
q.AppendLine(")");

q.Append("ON ");
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
// Oracle MERGE syntax does NOT use AS for subquery aliases
q.Append($" FROM {source}) {PseudoTableExcluded}");
q.AppendLine();

if (onConflictTyped.Update != null)
{
var columns = target.GetColumns(false);

q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
q.AppendLine();
}
// Oracle requires ON clause conditions to be wrapped in parentheses
q.Append("ON (");
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
q.AppendLine(")");

// Oracle MERGE syntax: WHEN NOT MATCHED clause for inserts, followed by WHEN MATCHED clause for updates
q.Append("WHEN NOT MATCHED THEN INSERT (");
q.AppendColumns(insertedColumns);
q.AppendLine(")");
Expand All @@ -78,10 +91,33 @@ public override string BuildMoveDataSql<T>(
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"{PseudoTableExcluded}.{col.QuotedColumName}"));
q.AppendLine(")");

if (returnedColumns.Count != 0)
if (onConflictTyped.Update != null)
{
q.Append("OUTPUT ");
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
q.Append("WHEN MATCHED ");

if (onConflictTyped.RawWhere != null || onConflictTyped.Where != null)
{
if (onConflictTyped is { RawWhere: not null, Where: not null })
{
throw new ArgumentException("Cannot specify both RawWhere and Where in OnConflictOptions.");
}

q.Append("AND ");
AppendConflictCondition(q, target, context, onConflictTyped);
}

q.AppendLine("THEN UPDATE SET ");
// Oracle MERGE: columns in ON clause cannot be updated, so exclude match columns
// Use insertedColumns instead of all columns because the USING subquery only contains insertedColumns
var matchColumnSet = matchColumns.ToHashSet();
var updateableColumns = insertedColumns.Where(c => !matchColumnSet.Contains(c.QuotedColumName)).ToList();
if (updateableColumns.Count == 0)
{
throw new InvalidOperationException(
"Oracle MERGE cannot update any columns because all available columns are used in the ON clause for conflict detection. " +
"Specify different columns in the 'Match' option or use specific columns in the 'Update' expression.");
}
q.AppendJoin(", ", GetUpdates(context, target, updateableColumns, onConflictTyped.Update));
q.AppendLine();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public async Task InsertEntities_MultipleTimes(InsertStrategy strategy)
{
Skip.If(_context.IsProvider(ProviderType.PostgreSql));
Skip.If(_context.IsProvider(ProviderType.SqlServer));
// Oracle MERGE requires match columns to be in the source data; auto-generated Id is not available
Skip.If(_context.IsProvider(ProviderType.Oracle));

// Arrange
var entities = new List<TestEntity>
Expand Down Expand Up @@ -126,6 +128,8 @@ public async Task InsertEntities_MultipleTimes_With_Conflict_On_Id(InsertStrateg
public async Task InsertEntities_WithConflict_SingleColumn(InsertStrategy strategy)
{
Skip.If(_context.IsProvider(ProviderType.MySql));
// Oracle MERGE does not support returning entities
Skip.If(_context.IsProvider(ProviderType.Oracle));

// Arrange
_context.TestEntities.Add(new TestEntity { TestRun = _run,Name = $"{_run}_Entity1" });
Expand Down Expand Up @@ -194,6 +198,8 @@ public async Task InsertEntities_WithConflict_DoNothing(InsertStrategy strategy)
public async Task InsertEntities_WithConflict_RawCondition(InsertStrategy strategy)
{
Skip.If(_context.IsProvider(ProviderType.MySql));
// Oracle MERGE does not support returning entities
Skip.If(_context.IsProvider(ProviderType.Oracle));

// Arrange
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
Expand Down Expand Up @@ -237,6 +243,8 @@ public async Task InsertEntities_WithConflict_RawCondition(InsertStrategy strate
public async Task InsertEntities_WithConflict_ExpressionCondition(InsertStrategy strategy)
{
Skip.If(_context.IsProvider(ProviderType.MySql));
// Oracle MERGE does not support returning entities
Skip.If(_context.IsProvider(ProviderType.Oracle));

// Arrange
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
Expand Down Expand Up @@ -280,6 +288,8 @@ public async Task InsertEntities_WithConflict_ExpressionCondition(InsertStrategy
public async Task InsertEntities_WithConflict_ComplexExpressionCondition(InsertStrategy strategy)
{
Skip.If(_context.IsProvider(ProviderType.MySql));
// Oracle MERGE does not support returning entities
Skip.If(_context.IsProvider(ProviderType.Oracle));

// Arrange
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
Expand Down Expand Up @@ -312,6 +322,8 @@ public async Task InsertEntities_WithConflict_ComplexExpressionCondition(InsertS
public async Task InsertEntities_WithConflict_MultipleColumns(InsertStrategy strategy)
{
Skip.If(_context.IsProvider(ProviderType.MySql));
// Oracle MERGE does not support returning entities
Skip.If(_context.IsProvider(ProviderType.Oracle));

// Arrange
_context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;

using Xunit;

namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Merge;

[Trait("Category", "Oracle")]
[Collection(TestDbContainerOracleCollection.Name)]
public class MergeTestsOracle(TestDbContainerOracle dbContainer) : MergeTestsBase<TestDbContextOracle>(dbContainer)
{
}
Loading