test: cover Oracle bulk upsert SQL generation
Exercise quoted Oracle identifiers, NVARCHAR2 casts, multi-row SQL, update/insert clauses, and parameter ordering. Reuse the provider SQL generation helper so identifier delimiting stays aligned with Oracle EF Core.
This commit is contained in:
parent
6b7296fa29
commit
33181b2c9d
15
Elsa.sln
15
Elsa.sln
|
|
@ -413,6 +413,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Runt
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Hosting.Management.UnitTests", "test\unit\Elsa.Hosting.Management.UnitTests\Elsa.Hosting.Management.UnitTests.csproj", "{39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EFCore.UnitTests", "test\unit\Elsa.Persistence.EFCore.UnitTests\Elsa.Persistence.EFCore.UnitTests.csproj", "{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -1837,6 +1839,18 @@ Global
|
|||
{39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -1993,6 +2007,7 @@ Global
|
|||
{E1607923-038B-41D6-9D23-F540FC9E6CCE} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{6E3B6948-B16D-480C-879E-B805F732649F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Elsa.Persistence.EFCore")]
|
||||
[assembly: InternalsVisibleTo("Elsa.Persistence.EFCore.UnitTests")]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Persistence.EFCore.Extensions;
|
||||
|
|
@ -337,7 +338,7 @@ public static class BulkUpsertExtensions
|
|||
return (sb.ToString(), parameters.ToArray());
|
||||
}
|
||||
|
||||
private static (string, object[]) GenerateOracleUpsert<TEntity>(
|
||||
internal static (string, object[]) GenerateOracleUpsert<TEntity>(
|
||||
DbContext dbContext,
|
||||
IList<TEntity> entities,
|
||||
Expression<Func<TEntity, string>> keySelector)
|
||||
|
|
@ -347,12 +348,8 @@ public static class BulkUpsertExtensions
|
|||
var schema = entityType.GetSchema();
|
||||
var tableName = entityType.GetTableName()!;
|
||||
var storeObject = StoreObjectIdentifier.Table(tableName, schema);
|
||||
|
||||
// Both schema and table must be quoted so Oracle treats them as case-sensitive
|
||||
// identifiers, matching what EF Core migrations create.
|
||||
var fullName = !string.IsNullOrEmpty(schema)
|
||||
? $"\"{schema}\".\"{tableName}\""
|
||||
: $"\"{tableName}\"";
|
||||
var sqlGenerationHelper = dbContext.GetService<ISqlGenerationHelper>();
|
||||
var fullName = sqlGenerationHelper.DelimitIdentifier(tableName, schema);
|
||||
|
||||
var props = entityType.GetProperties().ToList();
|
||||
|
||||
|
|
@ -361,9 +358,9 @@ public static class BulkUpsertExtensions
|
|||
|
||||
// Pre-build quoted column names once and reuse throughout all clauses.
|
||||
var quotedColumnNames = props
|
||||
.Select(p => $"\"{p.GetColumnName(storeObject)}\"")
|
||||
.Select(p => sqlGenerationHelper.DelimitIdentifier(p.GetColumnName(storeObject)!))
|
||||
.ToList();
|
||||
var quotedKeyColumnName = $"\"{keyColumnName}\"";
|
||||
var quotedKeyColumnName = sqlGenerationHelper.DelimitIdentifier(keyColumnName);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var parameters = new List<object>();
|
||||
|
|
@ -393,13 +390,13 @@ public static class BulkUpsertExtensions
|
|||
|
||||
// Aliases must be quoted so Oracle preserves their case, matching
|
||||
// the quoted references in ON, UPDATE SET, and INSERT/VALUES below.
|
||||
var quotedAlias = $"\"{property.GetColumnName(storeObject)}\"";
|
||||
var quotedAlias = sqlGenerationHelper.DelimitIdentifier(property.GetColumnName(storeObject)!);
|
||||
|
||||
// In a SELECT … FROM DUAL subquery, ODP.NET has no target column to
|
||||
// derive bind parameter types from and defaults to VARCHAR2 for .NET
|
||||
// strings. Elsa's Oracle migrations define string columns as NVARCHAR2,
|
||||
// so an explicit CAST is required to avoid a datatype mismatch error.
|
||||
// The full EF Core column type string (e.g. "NVARCHAR2(450 CHAR)") is
|
||||
// The full EF Core column type string (e.g. "NVARCHAR2(450)") is
|
||||
// used directly in the CAST so all Oracle type variants are handled
|
||||
// correctly without any string parsing.
|
||||
var columnType = property.GetColumnType() ?? string.Empty;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
using Elsa.Persistence.EFCore.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Elsa.Persistence.EFCore.UnitTests;
|
||||
|
||||
public class BulkUpsertExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateOracleUpsert_ProducesQuotedMergeWithNvarcharCasts()
|
||||
{
|
||||
using var dbContext = CreateDbContext();
|
||||
var entities = new List<TestEntity>
|
||||
{
|
||||
new() { Id = "first", Name = "First", Count = 1 },
|
||||
new() { Id = "second", Name = null, Count = 2 }
|
||||
};
|
||||
|
||||
var (sql, parameters) = BulkUpsertExtensions.GenerateOracleUpsert(dbContext, entities, x => x.Id);
|
||||
|
||||
Assert.Contains("MERGE INTO \"Elsa\".\"ActivityExecutionRecords\" Target", sql);
|
||||
Assert.Contains("CAST({0} AS NVARCHAR2(450)) AS \"RecordId\"", sql);
|
||||
Assert.Contains("CAST({2} AS NVARCHAR2(2000)) AS \"DisplayName\"", sql);
|
||||
Assert.Contains("CAST({5} AS NVARCHAR2(2000)) AS \"DisplayName\"", sql);
|
||||
Assert.Contains("FROM DUAL UNION ALL SELECT", sql);
|
||||
Assert.Contains("Target.\"RecordId\" = Source.\"RecordId\"", sql);
|
||||
Assert.Contains("INSERT (\"RecordId\", \"Count\", \"DisplayName\")", sql);
|
||||
Assert.Contains("VALUES (Source.\"RecordId\", Source.\"Count\", Source.\"DisplayName\")", sql);
|
||||
Assert.DoesNotContain("CAST({1} AS NUMBER", sql);
|
||||
|
||||
var updateClause = sql[sql.IndexOf("WHEN MATCHED", StringComparison.Ordinal)..sql.IndexOf("WHEN NOT MATCHED", StringComparison.Ordinal)];
|
||||
Assert.DoesNotContain("Target.\"RecordId\" = Source.\"RecordId\"", updateClause);
|
||||
Assert.Contains("Target.\"DisplayName\" = Source.\"DisplayName\"", updateClause);
|
||||
Assert.Equal(new object?[] { "first", 1, "First", "second", 2, null }, parameters);
|
||||
}
|
||||
|
||||
private static TestDbContext CreateDbContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<TestDbContext>()
|
||||
.UseOracle("Data Source=unused")
|
||||
.Options;
|
||||
return new TestDbContext(options);
|
||||
}
|
||||
|
||||
private sealed class TestDbContext(DbContextOptions<TestDbContext> options) : DbContext(options)
|
||||
{
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var entity = modelBuilder.Entity<TestEntity>();
|
||||
entity.ToTable("ActivityExecutionRecords", "Elsa");
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.Id).HasColumnName("RecordId").HasColumnType("NVARCHAR2(450)");
|
||||
entity.Property(x => x.Count).HasColumnType("NUMBER(10)");
|
||||
entity.Property(x => x.Name).HasColumnName("DisplayName").HasColumnType("NVARCHAR2(2000)");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestEntity
|
||||
{
|
||||
public string Id { get; set; } = null!;
|
||||
public int Count { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Include>[Elsa.Persistence.EFCore]*</Include>
|
||||
<Threshold>0</Threshold>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Oracle.EntityFrameworkCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Persistence.EFCore.Common\Elsa.Persistence.EFCore.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in a new issue