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 "; + } +}