diff --git a/README.md b/README.md
index 8062c60..bf6659a 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ CI build: [![FlexLabs.EntityFrameworkCore.Upsert on MyGet](https://img.shields.i
This library adds basic support for "Upsert" operations to EF Core.
-Uses `INSERT … ON CONFLICT DO UPDATE` in PostgreSQL/Sqlite, `MERGE` in SqlServer and `INSERT INTO … ON DUPLICATE KEY UPDATE` in MySQL.
+Uses `INSERT … ON CONFLICT DO UPDATE` in PostgreSQL/Sqlite, `MERGE` in SqlServer & Oracle and `INSERT INTO … ON DUPLICATE KEY UPDATE` in MySQL.
Also supports injecting sql command runners to add support for other providers
diff --git a/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/DefaultRunners.cs b/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/DefaultRunners.cs
index ddaefa8..84bc0ee 100644
--- a/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/DefaultRunners.cs
+++ b/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/DefaultRunners.cs
@@ -19,6 +19,7 @@ public static IUpsertCommandRunner[] GetRunners()
new PostgreSqlUpsertCommandRunner(),
new SqlServerUpsertCommandRunner(),
new SqliteUpsertCommandRunner(),
+ new OracleUpsertCommandRunner(),
};
return Runners;
}
diff --git a/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/OracleUpsertCommandRunner.cs b/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/OracleUpsertCommandRunner.cs
new file mode 100644
index 0000000..ca4bcf9
--- /dev/null
+++ b/src/FlexLabs.EntityFrameworkCore.Upsert/Runners/OracleUpsertCommandRunner.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using FlexLabs.EntityFrameworkCore.Upsert.Internal;
+
+namespace FlexLabs.EntityFrameworkCore.Upsert.Runners
+{
+ ///
+ /// Upsert command runner for the Oracle.EntityFrameworkCore provider
+ ///
+ public class OracleUpsertCommandRunner : RelationalUpsertCommandRunner
+ {
+ ///
+ public override bool Supports(string providerName) => providerName == "Oracle.EntityFrameworkCore";
+ ///
+ protected override string EscapeName([NotNull] string name) => $"\"{name}\"";
+ ///
+ protected override string? SourcePrefix => "s.";
+ ///
+ protected override string? TargetPrefix => "t.";
+ ///
+ protected override string Parameter(int index) => $":p{index}";
+ ///
+ protected override int? MaxQueryParams => 1000;
+
+ ///
+ public override string GenerateCommand(
+ string tableName,
+ ICollection> entities,
+ ICollection<(string ColumnName, bool IsNullable)> joinColumns,
+ ICollection<(string ColumnName, IKnownValue Value)>? updateExpressions,
+ KnownExpression? updateCondition)
+ {
+ ArgumentNullException.ThrowIfNull(entities);
+ var result = new StringBuilder();
+
+ result.Append(CultureInfo.InvariantCulture, $"MERGE INTO {tableName} t USING (");
+ foreach (var item in entities.Select((e, ind) => new {e, ind}))
+ {
+ result.Append(" SELECT ");
+ result.Append(string.Join(", ", item.e.Select(ec => string.Join(" AS ", ExpandValue(ec.Value), EscapeName(ec.ColumnName)))));
+ result.Append(" FROM dual");
+ if (entities.Count > 1 && item.ind != entities.Count - 1)
+ {
+ result.Append(" UNION ALL ");
+ }
+ }
+ result.Append(") s ON (");
+ result.Append(string.Join(" AND ", joinColumns.Select(j => $"t.{EscapeName(j.ColumnName)} = s.{EscapeName(j.ColumnName)}")));
+ result.Append(") ");
+ result.Append(" WHEN NOT MATCHED THEN INSERT (");
+ result.Append(string.Join(", ", entities.First().Where(e => e.AllowInserts).Select(e => EscapeName(e.ColumnName))));
+ result.Append(") VALUES (");
+ result.Append(string.Join(", ", entities.First().Where(e => e.AllowInserts).Select(e => $"s.{EscapeName(e.ColumnName)}")));
+ result.Append(") ");
+ if (updateExpressions is not null)
+ {
+ result.Append("WHEN MATCHED ");
+
+ result.Append("THEN UPDATE SET ");
+ result.Append(string.Join(", ", updateExpressions.Select(e => $"t.{EscapeName(e.ColumnName)} = {ExpandValue(e.Value)}")));
+ if (updateCondition is not null)
+ {
+ result.Append(CultureInfo.InvariantCulture, $" WHERE {ExpandExpression(updateCondition)} ");
+ }
+ }
+
+ return result.ToString();
+ }
+
+ ///
+ protected override string ExpandExpression(KnownExpression expression, Func? expandLeftColumn = null)
+ {
+ ArgumentNullException.ThrowIfNull(expression);
+
+ switch (expression.ExpressionType)
+ {
+ case ExpressionType.And:
+ {
+ var left = ExpandValue(expression.Value1, expandLeftColumn);
+ var right = ExpandValue(expression.Value2!, expandLeftColumn);
+ return $"BITAND({left}, {right})";
+ }
+ case ExpressionType.Or:
+ {
+ var left = ExpandValue(expression.Value1, expandLeftColumn);
+ var right = ExpandValue(expression.Value2!, expandLeftColumn);
+ return $"BITOR({left}, {right})";
+ }
+ case ExpressionType.Modulo:
+ {
+ var left = ExpandValue(expression.Value1, expandLeftColumn);
+ var right = ExpandValue(expression.Value2!, expandLeftColumn);
+ return $"MOD({left}, {right})";
+ }
+
+ default:
+ return base.ExpandExpression(expression, expandLeftColumn);
+ }
+ }
+ }
+}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/DbDriver.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/DbDriver.cs
index 86ab160..ac5e5f0 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/DbDriver.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/DbDriver.cs
@@ -7,5 +7,6 @@ public enum DbDriver
MySQL,
InMemory,
Sqlite,
+ Oracle,
}
}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/Tables.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/Tables.cs
index 34e2e84..728262d 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/Tables.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/Tables.cs
@@ -66,8 +66,6 @@ public static readonly Expression> MatchKey
public DateTime FirstVisit { get; set; }
public DateTime LastVisit { get; set; }
}
-
- [Table("SchemaTable", Schema = "testsch")]
public class SchemaTable
{
public int ID { get; set; }
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/TestDbContext.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/TestDbContext.cs
index bb6f707..233e7ae 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/TestDbContext.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/Base/TestDbContext.cs
@@ -37,7 +37,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity().HasIndex(b => b.Num1).IsUnique();
modelBuilder.Entity().Property(e => e.Num2).UseIdentityAlwaysColumn();
modelBuilder.Entity().HasIndex(b => b.Num1).IsUnique();
- modelBuilder.Entity().Property(e => e.Num3).HasComputedColumnSql($"{EscapeColumn(dbProvider, nameof(ComputedColumn.Num2))} + 1", stored: true);
+ modelBuilder.Entity().Property(e => e.Num3)
+ .HasComputedColumnSql($"{EscapeColumn(dbProvider, nameof(ComputedColumn.Num2))} + 1", stored: true);
if (dbProvider.Name == "Npgsql.EntityFrameworkCore.PostgreSQL")
{
@@ -48,10 +49,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().Ignore(j => j.Child);
}
+
if (dbProvider.Name != "Pomelo.EntityFrameworkCore.MySql") // Can't have a default value on TEXT columns in MySql
+ {
modelBuilder.Entity().Property(e => e.Text).HasDefaultValue("B");
- if (dbProvider.Name == "Pomelo.EntityFrameworkCore.MySql") // Can't have table schemas in MySql
- modelBuilder.Entity().Metadata.SetSchema(null);
+ }
+
+ if (dbProvider.Name != "Pomelo.EntityFrameworkCore.MySql" &&
+ dbProvider.Name != "Oracle.EntityFrameworkCore") // Can't have table schemas in MySql and Oracle
+ {
+ modelBuilder.Entity().Metadata.SetSchema("testsch");
+ }
}
private string EscapeColumn(IDatabaseProvider dbProvider, string columnName)
@@ -60,6 +68,7 @@ private string EscapeColumn(IDatabaseProvider dbProvider, string columnName)
"Pomelo.EntityFrameworkCore.MySql" => $"`{columnName}`",
"Npgsql.EntityFrameworkCore.PostgreSQL" => $"\"{columnName}\"",
"Microsoft.EntityFrameworkCore.Sqlite" => $"\"{columnName}\"",
+ "Oracle.EntityFrameworkCore" => columnName.ToUpper(),
_ => $"[{columnName}]"
};
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/ContainerisedDatabaseInitializerFixture.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/ContainerisedDatabaseInitializerFixture.cs
new file mode 100644
index 0000000..cb90ac5
--- /dev/null
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/ContainerisedDatabaseInitializerFixture.cs
@@ -0,0 +1,39 @@
+using System.Threading.Tasks;
+using DotNet.Testcontainers.Containers;
+
+namespace FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests
+{
+ public abstract class ContainerisedDatabaseInitializerFixture : DatabaseInitializerFixture
+ where TContainer : IContainer, IDatabaseContainer
+ {
+ public TContainer TestContainer { get; }
+
+ public ContainerisedDatabaseInitializerFixture()
+ {
+ if (!BuildEnvironment.UseLocalService)
+ {
+ TestContainer = BuildContainer();
+ }
+ }
+
+ protected abstract TContainer BuildContainer();
+
+ public override async Task InitializeAsync()
+ {
+ if (TestContainer is not null)
+ {
+ await TestContainer.StartAsync();
+ }
+
+ await base.InitializeAsync();
+ }
+
+ public override async Task DisposeAsync()
+ {
+ if (TestContainer is not null)
+ {
+ await TestContainer.StopAsync();
+ }
+ }
+ }
+}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DatabaseInitializerFixture.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DatabaseInitializerFixture.cs
index 30e5b6f..5124851 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DatabaseInitializerFixture.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DatabaseInitializerFixture.cs
@@ -1,5 +1,4 @@
using System.Threading.Tasks;
-using DotNet.Testcontainers.Containers;
using FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.Base;
using Microsoft.EntityFrameworkCore;
using Xunit;
@@ -8,29 +7,14 @@ namespace FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests
{
public abstract class DatabaseInitializerFixture : IAsyncLifetime
{
- public IContainer TestContainer { get; }
public DbContextOptions DataContextOptions { get; private set; }
- public DatabaseInitializerFixture()
- {
- if (!BuildEnvironment.UseLocalService)
- {
- TestContainer = BuildContainer();
- }
- }
-
public abstract DbDriver DbDriver { get; }
- protected virtual IContainer BuildContainer() => null;
protected abstract void ConfigureContextOptions(DbContextOptionsBuilder builder);
- public async Task InitializeAsync()
+ public virtual async Task InitializeAsync()
{
- if (TestContainer is not null)
- {
- await TestContainer.StartAsync();
- }
-
var builder = new DbContextOptionsBuilder();
ConfigureContextOptions(builder);
DataContextOptions = builder.Options;
@@ -39,12 +23,6 @@ public async Task InitializeAsync()
await context.Database.EnsureCreatedAsync();
}
- public async Task DisposeAsync()
- {
- if (TestContainer is not null)
- {
- await TestContainer.StopAsync();
- }
- }
+ public virtual Task DisposeAsync() => Task.CompletedTask;
}
}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTestsBase.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTestsBase.cs
index 35fa5b3..ae8a4a1 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTestsBase.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTestsBase.cs
@@ -25,6 +25,7 @@ public DbTestsBase(DatabaseInitializerFixture fixture)
ISO = "AU",
Created = NewDateTime(1970, 1, 1),
};
+
readonly PageVisit _dbVisitOld = new()
{
UserID = 1,
@@ -33,6 +34,7 @@ public DbTestsBase(DatabaseInitializerFixture fixture)
FirstVisit = NewDateTime(1970, 1, 1),
LastVisit = NewDateTime(1970, 1, 1),
};
+
readonly PageVisit _dbVisit = new()
{
UserID = 1,
@@ -41,35 +43,43 @@ public DbTestsBase(DatabaseInitializerFixture fixture)
FirstVisit = NewDateTime(1970, 1, 1),
LastVisit = NewDateTime(1970, 1, 1),
};
+
readonly Status _dbStatus = new()
{
ID = 1,
Name = "Created",
LastChecked = NewDateTime(1970, 1, 1),
};
+
readonly Book _dbBook = new()
{
Name = "The Fellowship of the Ring",
Genres = new[] { "Fantasy" },
};
+
readonly NullableCompositeKey _nullableKey1 = new()
{
ID1 = 1,
ID2 = 2,
Value = "First",
};
+
readonly NullableCompositeKey _nullableKey2 = new()
{
ID1 = 1,
ID2 = null,
Value = "Second",
};
+
readonly ComputedColumn _computedColumn = new()
{
Num1 = 1,
Num2 = 7,
};
- readonly static DateTime _now = NewDateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second);
+
+ readonly static DateTime _now = NewDateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
+ DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second);
+
readonly static DateTime _today = NewDateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
readonly int _increment = 8;
@@ -1247,7 +1257,8 @@ public void Upsert_KeyOnly()
[Fact]
public void Upsert_NullableKeys()
{
- if (_fixture.DbDriver == DbDriver.MySQL || _fixture.DbDriver == DbDriver.Postgres || _fixture.DbDriver == DbDriver.Sqlite)
+ if (_fixture.DbDriver == DbDriver.MySQL || _fixture.DbDriver == DbDriver.Postgres ||
+ _fixture.DbDriver == DbDriver.Sqlite || _fixture.DbDriver == DbDriver.Oracle)
return;
ResetDb();
@@ -1681,7 +1692,8 @@ public void Upsert_UpdateCondition_ValueCheck()
[Fact]
public void Upsert_UpdateCondition_ValueCheck_UpdateColumnFromCondition()
{
- if (BuildEnvironment.IsGitHub && _fixture.DbDriver == DbDriver.MySQL && Environment.OSVersion.Platform == PlatformID.Unix)
+ if (BuildEnvironment.IsGitHub && _fixture.DbDriver == DbDriver.MySQL &&
+ Environment.OSVersion.Platform == PlatformID.Unix)
{
// Disabling this test on GitHub Ubuntu images - they're cursed?
return;
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_MySql.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_MySql.cs
index 4e86104..12da175 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_MySql.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_MySql.cs
@@ -1,4 +1,4 @@
-using DotNet.Testcontainers.Containers;
+using System;
using FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.Base;
using FlexLabs.EntityFrameworkCore.Upsert.Tests.EF;
using Microsoft.EntityFrameworkCore;
@@ -10,16 +10,17 @@ namespace FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests
#if !NOMYSQL
public class DbTests_MySql : DbTestsBase, IClassFixture
{
- public sealed class DatabaseInitializer : DatabaseInitializerFixture
+ public sealed class DatabaseInitializer : ContainerisedDatabaseInitializerFixture
{
public override DbDriver DbDriver => DbDriver.MySQL;
- protected override IContainer BuildContainer()
+ protected override MySqlContainer BuildContainer()
=> new MySqlBuilder().Build();
protected override void ConfigureContextOptions(DbContextOptionsBuilder builder)
{
- var connectionString = (TestContainer as IDatabaseContainer).GetConnectionString();
+ var connectionString = TestContainer?.GetConnectionString()
+ ?? throw new InvalidOperationException("Connection string was not initialised");
builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
}
}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_Oracle.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_Oracle.cs
new file mode 100644
index 0000000..225af92
--- /dev/null
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_Oracle.cs
@@ -0,0 +1,36 @@
+using System;
+using FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.Base;
+using FlexLabs.EntityFrameworkCore.Upsert.Tests.EF;
+using Microsoft.EntityFrameworkCore;
+using Testcontainers.Oracle;
+using Xunit;
+
+namespace FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests
+{
+#if !NOORACLE
+ public class DbTests_Oracle : DbTestsBase, IClassFixture
+ {
+ public sealed class DatabaseInitializer : ContainerisedDatabaseInitializerFixture
+ {
+ public override DbDriver DbDriver => DbDriver.Oracle;
+
+ protected override OracleContainer BuildContainer()
+ => new OracleBuilder().Build();
+
+ protected override void ConfigureContextOptions(DbContextOptionsBuilder builder)
+ {
+ var connectionString = TestContainer?.GetConnectionString()
+ ?? throw new InvalidOperationException("Connection string was not initialised");
+ builder
+ .UseOracle(connectionString)
+ .UseUpperSnakeCaseNamingConvention();
+ }
+ }
+
+ public DbTests_Oracle(DatabaseInitializer contexts)
+ : base(contexts)
+ {
+ }
+ }
+#endif
+}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_Postgres.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_Postgres.cs
index ee2f852..7a7270b 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_Postgres.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_Postgres.cs
@@ -1,5 +1,4 @@
using System.Linq;
-using DotNet.Testcontainers.Containers;
using FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.Base;
using FlexLabs.EntityFrameworkCore.Upsert.Tests.EF;
using FluentAssertions;
@@ -13,19 +12,19 @@ namespace FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests
#if !NOPOSTGRES
public class DbTests_Postgres : DbTestsBase, IClassFixture
{
- public sealed class DatabaseInitializer : DatabaseInitializerFixture
+ public sealed class DatabaseInitializer : ContainerisedDatabaseInitializerFixture
{
public override DbDriver DbDriver => DbDriver.Postgres;
- protected override IContainer BuildContainer()
+ protected override PostgreSqlContainer BuildContainer()
=> new PostgreSqlBuilder().Build();
protected override void ConfigureContextOptions(DbContextOptionsBuilder builder)
{
- var connectionString = (TestContainer as IDatabaseContainer)?.GetConnectionString()
+ var connectionString = TestContainer?.GetConnectionString()
?? (BuildEnvironment.IsGitHub ? "Server=localhost;Port=5432;Database=testuser;Username=postgres;Password=root" : null);
builder.UseNpgsql(new NpgsqlDataSourceBuilder(connectionString)
- .EnableDynamicJsonMappings()
+ .EnableDynamicJson()
.Build());
}
}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_SqlServer.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_SqlServer.cs
index 3f52b45..95b3c3f 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_SqlServer.cs
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/DbTests_SqlServer.cs
@@ -1,5 +1,4 @@
-using DotNet.Testcontainers.Containers;
-using FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.Base;
+using FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.Base;
using FlexLabs.EntityFrameworkCore.Upsert.Tests.EF;
using Microsoft.EntityFrameworkCore;
using Testcontainers.MsSql;
@@ -10,16 +9,16 @@ namespace FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests
#if !NOMSSQL
public class DbTests_SqlServer : DbTestsBase, IClassFixture
{
- public sealed class DatabaseInitializer : DatabaseInitializerFixture
+ public sealed class DatabaseInitializer : ContainerisedDatabaseInitializerFixture
{
public override DbDriver DbDriver => DbDriver.MSSQL;
- protected override IContainer BuildContainer()
+ protected override MsSqlContainer BuildContainer()
=> new MsSqlBuilder().Build();
protected override void ConfigureContextOptions(DbContextOptionsBuilder builder)
{
- var connectionString = (TestContainer as IDatabaseContainer)?.GetConnectionString()
+ var connectionString = TestContainer?.GetConnectionString()
?? "Server=(localdb)\\MSSqlLocalDB;Integrated Security=SSPI;Initial Catalog=FlexLabsUpsertTests;";
builder.UseSqlServer(connectionString);
}
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.csproj b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.csproj
index 9088ecd..44a0f45 100644
--- a/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.csproj
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests/FlexLabs.EntityFrameworkCore.Upsert.IntegrationTests.csproj
@@ -1,4 +1,4 @@
-
+
net8.0
@@ -8,26 +8,29 @@
- $(DefineConstants);NOMSSQL;NOMYSQL;POSTGRES_ONLY
+ $(DefineConstants);NOMSSQL;NOMYSQL;NOORACLE;POSTGRES_ONLY
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
all
diff --git a/test/FlexLabs.EntityFrameworkCore.Upsert.Tests/Runners/OracleUpsertCommandRunnerTests.cs b/test/FlexLabs.EntityFrameworkCore.Upsert.Tests/Runners/OracleUpsertCommandRunnerTests.cs
new file mode 100644
index 0000000..e559c77
--- /dev/null
+++ b/test/FlexLabs.EntityFrameworkCore.Upsert.Tests/Runners/OracleUpsertCommandRunnerTests.cs
@@ -0,0 +1,54 @@
+using FlexLabs.EntityFrameworkCore.Upsert.Runners;
+
+namespace FlexLabs.EntityFrameworkCore.Upsert.Tests.Runners
+{
+ public class OracleUpsertCommandRunnerTests : RelationalCommandRunnerTestsBase
+ {
+ public OracleUpsertCommandRunnerTests()
+ : base("Oracle.EntityFrameworkCore")
+ {
+ }
+
+ protected override string NoUpdate_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") ";
+
+ protected override string NoUpdate_Multiple_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual UNION ALL SELECT :p4 AS \"ID\", :p5 AS \"Name\", :p6 AS \"Status\", :p7 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") ";
+
+ protected override string NoUpdate_WithNullable_Sql =>
+ "MERGE INTO \"TestEntityWithNullableKey\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"ID1\", :p2 AS \"ID2\", :p3 AS \"Name\", :p4 AS \"Status\", :p5 AS \"Total\" FROM dual) s ON (t.\"ID1\" = s.\"ID1\" AND t.\"ID2\" = s.\"ID2\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"ID1\", \"ID2\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"ID1\", s.\"ID2\", s.\"Name\", s.\"Status\", s.\"Total\") ";
+
+ protected override string Update_Constant_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Name\" = :p4";
+
+ protected override string Update_Constant_Multiple_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual UNION ALL SELECT :p4 AS \"ID\", :p5 AS \"Name\", :p6 AS \"Status\", :p7 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Name\" = :p8";
+
+ protected override string Update_Source_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Name\" = s.\"Name\"";
+
+ protected override string Update_BinaryAdd_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Total\" = ( t.\"Total\" + :p4 )";
+
+ protected override string Update_Coalesce_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Status\" = ( COALESCE(t.\"Status\", :p4) )";
+
+ protected override string Update_BinaryAddMultiply_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Total\" = ( ( t.\"Total\" + :p4 ) * s.\"Total\" )";
+
+ protected override string Update_BinaryAddMultiplyGroup_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Total\" = ( t.\"Total\" + ( :p4 * s.\"Total\" ) )";
+
+ protected override string Update_Condition_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Name\" = :p4 WHERE t.\"Total\" > :p5 ";
+
+ protected override string Update_Condition_UpdateConditionColumn_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Name\" = :p4, t.\"Total\" = ( t.\"Total\" + :p5 ) WHERE t.\"Total\" > :p6 ";
+
+ protected override string Update_Condition_AndCondition_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Name\" = :p4 WHERE ( t.\"Total\" > :p5 ) AND ( t.\"Status\" != s.\"Status\" ) ";
+
+ protected override string Update_Condition_NullCheck_Sql =>
+ "MERGE INTO \"TestEntity\" t USING ( SELECT :p0 AS \"ID\", :p1 AS \"Name\", :p2 AS \"Status\", :p3 AS \"Total\" FROM dual) s ON (t.\"ID\" = s.\"ID\") WHEN NOT MATCHED THEN INSERT (\"ID\", \"Name\", \"Status\", \"Total\") VALUES (s.\"ID\", s.\"Name\", s.\"Status\", s.\"Total\") WHEN MATCHED THEN UPDATE SET t.\"Name\" = :p4 WHERE t.\"Status\" IS NOT NULL ";
+ }
+}