From 82e069c265c0ae483fdc0899b0d89b680238218b Mon Sep 17 00:00:00 2001
From: MohitGuptaC <155074110+MohitGuptaC@users.noreply.github.com>
Date: Wed, 24 Jun 2026 00:13:51 +0530
Subject: [PATCH] fix: correct Oracle identifier quoting and NVARCHAR2 cast in
GenerateOracleUpsert
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Unquoted aliases in SELECT … FROM DUAL caused ORA-00904 because Oracle
uppercases bare identifiers. All aliases, ON condition, UPDATE SET, and
INSERT/VALUES column references are now double-quoted to match the
case-sensitive names EF Core migrations produce.
NVARCHAR2 columns additionally required an explicit CAST because ODP.NET
cannot infer bind parameter types from a FROM DUAL subquery and defaults
to VARCHAR2. CAST(:p AS NVARCHAR2(n)) with length extracted from the EF
column type string resolves the datatype mismatch.
Both fixes are required — neither alone produces working Oracle persistence.
All other providers are unchanged.
fixes Fixes #7755
---
.../Extensions/BulkUpsertExtensions.cs | 213 +++++++++---------
1 file changed, 103 insertions(+), 110 deletions(-)
diff --git a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs
index fadf509a7..2749790bd 100644
--- a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs
+++ b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs
@@ -16,12 +16,6 @@ public static class BulkUpsertExtensions
///
/// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector.
///
- /// The type of the database context.
- /// The type of the entity being upserted.
- /// The database context where the bulk upsert operation will be executed.
- /// The list of entities to be upserted.
- /// An expression used to determine the key for upsert operations.
- /// A token to observe while waiting for the operation to complete.
public static async Task BulkUpsertAsync(
this TDbContext dbContext,
IList entities,
@@ -36,13 +30,6 @@ public static class BulkUpsertExtensions
///
/// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector and optional batch size.
///
- /// The type of the database context.
- /// The type of the entity being upserted.
- /// The database context where the bulk upsert operation will be executed.
- /// The list of entities to be upserted.
- /// An expression used to determine the key for upsert operations.
- /// The size of each batch for processing the upsert operation. Defaults to 50.
- /// A token to observe while waiting for the operation to complete.
/// Thrown if the database provider for the context is not supported.
public static async Task BulkUpsertAsync(
this TDbContext dbContext,
@@ -56,30 +43,29 @@ public static class BulkUpsertExtensions
if (entities.Count == 0)
return;
- // Identify the current provider (e.g., "Microsoft.EntityFrameworkCore.SqlServer")
var providerName = dbContext.Database.ProviderName?.ToLowerInvariant() ?? string.Empty;
- // Determine the method for generating SQL based on the provider
Func, Expression>, (string, object[])> generateSql = providerName switch
{
var pn when pn.Contains("sqlserver") => GenerateSqlServerUpsert,
- var pn when pn.Contains("sqlite") => GenerateSqliteUpsert,
- var pn when pn.Contains("postgres") => GeneratePostgresUpsert,
- var pn when pn.Contains("mysql") => GenerateMySqlUpsert,
- var pn when pn.Contains("oracle") => GenerateOracleUpsert,
+ var pn when pn.Contains("sqlite") => GenerateSqliteUpsert,
+ var pn when pn.Contains("postgres") => GeneratePostgresUpsert,
+ var pn when pn.Contains("mysql") => GenerateMySqlUpsert,
+ var pn when pn.Contains("oracle") => GenerateOracleUpsert,
_ => throw new NotSupportedException($"Provider '{providerName}' is not supported.")
};
- // Loop through batched entities
foreach (var batch in entities.Chunk(batchSize))
{
- // Generate SQL and parameters
var (sql, parameters) = generateSql(dbContext, batch, keySelector);
-
await dbContext.Database.ExecuteSqlRawAsync(sql, parameters, cancellationToken);
}
}
+ // -------------------------------------------------------------------------
+ // SQL Server
+ // -------------------------------------------------------------------------
+
private static (string, object[]) GenerateSqlServerUpsert(
DbContext dbContext,
IList entities,
@@ -92,9 +78,7 @@ public static class BulkUpsertExtensions
var props = entityType.GetProperties().ToList();
var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!;
var keyColumnName = $"[{keyProp.GetColumnName(storeObject)}]";
- var columnNames = props
- .Select(p => $"[{p.GetColumnName(storeObject)}]")
- .ToList();
+ var columnNames = props.Select(p => $"[{p.GetColumnName(storeObject)}]").ToList();
var mergeSql = new StringBuilder();
mergeSql.AppendLine($"MERGE {tableName} AS Target");
@@ -111,27 +95,21 @@ public static class BulkUpsertExtensions
foreach (var property in props)
{
var paramName = $"{{{parameterCount++}}}";
-
- // If it's a shadow property, retrieve value via Entry(..).Property(..)
var value = property.IsShadowProperty()
? dbContext.Entry(entity).Property(property.Name).CurrentValue
: property.PropertyInfo?.GetValue(entity);
-
var converter = property.GetTypeMapping().Converter;
- if (converter != null)
- value = converter.ConvertToProvider(value)!;
+ if (converter != null) value = converter.ConvertToProvider(value)!;
- // Explicitly cast null values for varbinary columns
if (property.GetColumnType().StartsWith("varbinary", StringComparison.OrdinalIgnoreCase) && value is null)
- values.Add("CAST(NULL AS varbinary(max))"); // Explicitly cast null
+ values.Add("CAST(NULL AS varbinary(max))");
else
values.Add(paramName);
-
+
parameters.Add(value!);
}
- var line = $"({string.Join(", ", values)}){(i < entities.Count - 1 ? "," : string.Empty)}";
- mergeSql.AppendLine(line);
+ mergeSql.AppendLine($"({string.Join(", ", values)}){(i < entities.Count - 1 ? "," : string.Empty)}");
}
mergeSql.AppendLine($") AS Source ({string.Join(", ", columnNames)})");
@@ -145,6 +123,10 @@ public static class BulkUpsertExtensions
return (mergeSql.ToString(), parameters.ToArray());
}
+ // -------------------------------------------------------------------------
+ // SQLite
+ // -------------------------------------------------------------------------
+
private static (string, object[]) GenerateSqliteUpsert(
DbContext dbContext,
IList entities,
@@ -157,9 +139,7 @@ public static class BulkUpsertExtensions
var props = entityType.GetProperties().ToList();
var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!;
var keyColumnName = keyProp.GetColumnName(storeObject);
- var columnNames = props
- .Select(p => p.GetColumnName(storeObject)!)
- .ToList();
+ var columnNames = props.Select(p => p.GetColumnName(storeObject)!).ToList();
var sb = new StringBuilder();
var parameters = new List