Skip to content

Commit b79b2b5

Browse files
PhenXCopilot
andauthored
Add oracle support (#52)
* Begin support for Oracle * Fix backmerge * Add basic support for Oracle * Fix back merge * Mark test as unstable on oracle * Little fixes * Update src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix drop table method * Put strin enum column back in string * Add comments and fix obsolete code --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c2d32a4 commit b79b2b5

27 files changed

Lines changed: 505 additions & 26 deletions

PhenX.EntityFrameworkCore.BulkInsert.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{45366E91-4
3535
EndProject
3636
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.MySql", "src\PhenX.EntityFrameworkCore.BulkInsert.MySql\PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj", "{17649766-EA68-4333-8DA8-47B014A8B2CC}"
3737
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.Oracle", "src\PhenX.EntityFrameworkCore.BulkInsert.Oracle\PhenX.EntityFrameworkCore.BulkInsert.Oracle.csproj", "{98CC5F0A-5739-4570-A384-A3A067D09755}"
39+
EndProject
3840
Global
3941
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4042
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +71,10 @@ Global
6971
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
7072
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
7173
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.Build.0 = Release|Any CPU
74+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Debug|Any CPU.Build.0 = Debug|Any CPU
76+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Release|Any CPU.ActiveCfg = Release|Any CPU
77+
{98CC5F0A-5739-4570-A384-A3A067D09755}.Release|Any CPU.Build.0 = Release|Any CPU
7278
EndGlobalSection
7379
GlobalSection(SolutionProperties) = preSolution
7480
HideSolutionNode = FALSE
@@ -81,5 +87,6 @@ Global
8187
{E4EB1C53-575C-45F8-924A-93DC42E8ACCA} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
8288
{450E859C-411F-4D67-A0B4-4E02C3D30E14} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
8389
{17649766-EA68-4333-8DA8-47B014A8B2CC} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
90+
{98CC5F0A-5739-4570-A384-A3A067D09755} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
8491
EndGlobalSection
8592
EndGlobal

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PhenX.EntityFrameworkCore.BulkInsert
22

3-
A high-performance, provider-agnostic bulk insert extension for Entity Framework Core 8+. Supports SQL Server, PostgreSQL, SQLite and MySQL.
3+
A high-performance, provider-agnostic bulk insert extension for Entity Framework Core 8+. Supports SQL Server, PostgreSQL, SQLite, MySQL and Oracle.
44

55
Its main purpose is to provide a fast way to perform simple bulk inserts in Entity Framework Core applications.
66

@@ -21,6 +21,7 @@ but they are in [the roadmap](#roadmap).
2121
| `PhenX.EntityFrameworkCore.BulkInsert.PostgreSql` | For PostgreSQL | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql) |
2222
| `PhenX.EntityFrameworkCore.BulkInsert.Sqlite` | For SQLite | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Sqlite.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.Sqlite) |
2323
| `PhenX.EntityFrameworkCore.BulkInsert.MySql` | For MySql | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Sqlite.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.MySql) |
24+
| `PhenX.EntityFrameworkCore.BulkInsert.Oracle` | For Oracle | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Oracle.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.Oracle) |
2425
| `PhenX.EntityFrameworkCore.BulkInsert` | Common library | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert) |
2526

2627
## Installation
@@ -39,6 +40,9 @@ Install-Package PhenX.EntityFrameworkCore.BulkInsert.Sqlite
3940

4041
# For MySql
4142
Install-Package PhenX.EntityFrameworkCore.BulkInsert.MySql
43+
44+
# For Oracle
45+
Install-Package PhenX.EntityFrameworkCore.BulkInsert.Oracle
4246
```
4347

4448
## Usage
@@ -58,6 +62,8 @@ services.AddDbContext<MyDbContext>(options =>
5862
.UseBulkInsertSqlite()
5963
// OR
6064
.UseBulkInsertMySql()
65+
// OR
66+
.UseBulkInsertOracle()
6167
;
6268
});
6369
```
@@ -183,6 +189,10 @@ MySQL results with 500 000 rows :
183189

184190
![bench-mysql.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/main/images/bench-mysql.png)
185191

192+
Oracle results with 500 000 rows :
193+
194+
![bench-oracle.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/main/images/bench-oracle.png)
195+
186196
## Contributing
187197

188198
Contributions are welcome! Please open issues or submit pull requests for bug fixes, features, or documentation improvements.

images/bench-oracle.png

23.8 KB
Loading
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Oracle.ManagedDataAccess.Client;
2+
3+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
4+
5+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
6+
7+
/// <summary>
8+
/// Options specific to Oracle bulk insert.
9+
/// </summary>
10+
public class OracleBulkInsertOptions : BulkInsertOptions
11+
{
12+
/// <inheritdoc cref="OracleBulkCopyOptions"/>
13+
public OracleBulkCopyOptions CopyOptions { get; set; } = OracleBulkCopyOptions.Default;
14+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using JetBrains.Annotations;
2+
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Logging;
5+
6+
using Oracle.ManagedDataAccess.Client;
7+
8+
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
9+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
10+
11+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
12+
13+
[UsedImplicitly]
14+
internal class OracleBulkInsertProvider(ILogger<OracleBulkInsertProvider>? logger) : BulkInsertProviderBase<OracleDialectBuilder, OracleBulkInsertOptions>(logger)
15+
{
16+
/// <inheritdoc />
17+
protected override string BulkInsertId => "ROWID";
18+
19+
/// <inheritdoc />
20+
protected override string AddTableCopyBulkInsertId => ""; // No need to add an ID column in Oracle
21+
22+
/// <inheritdoc />
23+
/// <summary>
24+
/// The temporary table name is generated with a GUID to ensure uniqueness, but limited to less than 30 characters,
25+
/// because Oracle prior 12.2 has a limit of 30 characters for identifiers.
26+
/// </summary>
27+
protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert_{Guid.NewGuid().ToString("N")[..8]}";
28+
29+
protected override OracleBulkInsertOptions CreateDefaultOptions() => new()
30+
{
31+
BatchSize = 50_000,
32+
};
33+
34+
/// <inheritdoc />
35+
protected override IAsyncEnumerable<T> BulkInsertReturnEntities<T>(
36+
bool sync,
37+
DbContext context,
38+
TableMetadata tableInfo,
39+
IEnumerable<T> entities,
40+
OracleBulkInsertOptions options,
41+
OnConflictOptions<T>? onConflict,
42+
CancellationToken ctk)
43+
{
44+
throw new NotSupportedException("Provider does not support returning entities.");
45+
}
46+
47+
/// <inheritdoc />
48+
protected override Task BulkInsert<T>(
49+
bool sync,
50+
DbContext context,
51+
TableMetadata tableInfo,
52+
IEnumerable<T> entities,
53+
string tableName,
54+
IReadOnlyList<ColumnMetadata> columns,
55+
OracleBulkInsertOptions options,
56+
CancellationToken ctk)
57+
{
58+
var connection = (OracleConnection) context.Database.GetDbConnection();
59+
60+
using var bulkCopy = new OracleBulkCopy(connection, options.CopyOptions);
61+
62+
bulkCopy.DestinationTableName = tableInfo.QuotedTableName;
63+
bulkCopy.BatchSize = options.BatchSize;
64+
bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds();
65+
66+
foreach (var column in columns)
67+
{
68+
bulkCopy.ColumnMappings.Add(column.PropertyName, column.QuotedColumName);
69+
}
70+
71+
var dataReader = new EnumerableDataReader<T>(entities, columns, options);
72+
73+
bulkCopy.WriteToServer(dataReader);
74+
75+
return Task.CompletedTask;
76+
}
77+
78+
/// <inheritdoc />
79+
protected override async Task DropTempTableAsync(bool sync, DbContext dbContext, string tableName)
80+
{
81+
var commandText = $"""
82+
BEGIN
83+
EXECUTE IMMEDIATE 'DROP TABLE {tableName}';
84+
EXCEPTION
85+
WHEN OTHERS THEN
86+
IF SQLCODE != -942 THEN -- ORA-00942: table or view does not exist
87+
RAISE;
88+
END IF;
89+
END;
90+
""";
91+
92+
await ExecuteAsync(sync, dbContext, commandText, CancellationToken.None);
93+
}
94+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
4+
5+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
6+
7+
/// <summary>
8+
/// DbContext options extension for Oracle.
9+
/// </summary>
10+
public static class OracleDbContextOptionsExtensions
11+
{
12+
/// <summary>
13+
/// Configures the DbContext to use the Oracle bulk insert provider.
14+
/// </summary>
15+
public static DbContextOptionsBuilder UseBulkInsertOracle(this DbContextOptionsBuilder optionsBuilder)
16+
{
17+
return optionsBuilder.UseProvider<OracleBulkInsertProvider>();
18+
}
19+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Text;
2+
3+
using Microsoft.EntityFrameworkCore;
4+
5+
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
6+
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
7+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
8+
9+
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
10+
11+
internal class OracleDialectBuilder : SqlDialectBuilder
12+
{
13+
protected override string OpenDelimiter => "\"";
14+
protected override string CloseDelimiter => "\"";
15+
protected override string ConcatOperator => "||";
16+
17+
protected override bool SupportsMoveRows => false;
18+
19+
public override string CreateTableCopySql(string tempTableName, TableMetadata tableInfo, IReadOnlyList<ColumnMetadata> columns)
20+
{
21+
return CreateTableCopySqlBase(tempTableName, columns);
22+
}
23+
24+
public override string BuildMoveDataSql<T>(
25+
DbContext context,
26+
TableMetadata target,
27+
string source,
28+
IReadOnlyList<ColumnMetadata> insertedColumns,
29+
IReadOnlyList<ColumnMetadata> returnedColumns,
30+
BulkInsertOptions options,
31+
OnConflictOptions? onConflict = null)
32+
{
33+
var q = new StringBuilder();
34+
35+
// Merge handling
36+
if (onConflict is OnConflictOptions<T> onConflictTyped)
37+
{
38+
IEnumerable<string> matchColumns;
39+
if (onConflictTyped.Match != null)
40+
{
41+
matchColumns = GetColumns(target, onConflictTyped.Match);
42+
}
43+
else if (target.PrimaryKey.Count > 0)
44+
{
45+
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
46+
}
47+
else
48+
{
49+
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
50+
}
51+
52+
q.AppendLine($"MERGE INTO {target.QuotedTableName} AS {PseudoTableInserted}");
53+
54+
q.Append("USING (SELECT ");
55+
q.AppendColumns(insertedColumns);
56+
q.Append($" FROM {source}) AS {PseudoTableExcluded} (");
57+
q.AppendColumns(insertedColumns);
58+
q.AppendLine(")");
59+
60+
q.Append("ON ");
61+
q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col} = {PseudoTableExcluded}.{col}"));
62+
q.AppendLine();
63+
64+
if (onConflictTyped.Update != null)
65+
{
66+
var columns = target.GetColumns(false);
67+
68+
q.AppendLine("WHEN MATCHED THEN UPDATE SET ");
69+
q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update));
70+
q.AppendLine();
71+
}
72+
73+
q.Append("WHEN NOT MATCHED THEN INSERT (");
74+
q.AppendColumns(insertedColumns);
75+
q.AppendLine(")");
76+
77+
q.Append("VALUES (");
78+
q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"{PseudoTableExcluded}.{col.QuotedColumName}"));
79+
q.AppendLine(")");
80+
81+
if (returnedColumns.Count != 0)
82+
{
83+
q.Append("OUTPUT ");
84+
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"{PseudoTableInserted}.{col.QuotedColumName} AS {col.QuotedColumName}"));
85+
q.AppendLine();
86+
}
87+
}
88+
89+
// No conflict handling
90+
else
91+
{
92+
q.Append($"INSERT INTO {target.QuotedTableName} (");
93+
q.AppendColumns(insertedColumns);
94+
q.AppendLine(")");
95+
q.Append("SELECT ");
96+
q.AppendColumns(insertedColumns);
97+
q.AppendLine();
98+
q.Append($"FROM {source}");
99+
q.AppendLine();
100+
101+
if (returnedColumns.Count != 0)
102+
{
103+
q.Append("RETURNING ");
104+
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append(col.QuotedColumName));
105+
q.Append(" INTO ");
106+
q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($":{col.ColumnName}"));
107+
q.AppendLine();
108+
}
109+
}
110+
111+
q.AppendLine(";");
112+
113+
return q.ToString();
114+
}
115+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj" />
5+
</ItemGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Oracle.EntityFrameworkCore" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.23.80" />
9+
<PackageReference Include="Oracle.EntityFrameworkCore" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.23.80" />
10+
</ItemGroup>
11+
12+
</Project>

src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerDialectBuilder.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,7 @@ internal class SqlServerDialectBuilder : SqlDialectBuilder
1818

1919
public override string CreateTableCopySql(string tempTableName, TableMetadata tableInfo, IReadOnlyList<ColumnMetadata> columns)
2020
{
21-
var q = new StringBuilder();
22-
q.Append($"CREATE TABLE {tempTableName} (");
23-
24-
foreach (var column in columns)
25-
{
26-
q.Append($"{column.QuotedColumName} {column.StoreDefinition}");
27-
if (column != columns[^1])
28-
{
29-
q.Append(',');
30-
}
31-
q.AppendLine();
32-
}
33-
34-
q.AppendLine(")");
35-
36-
return q.ToString();
21+
return CreateTableCopySqlBase(tempTableName, columns);
3722
}
3823

3924
protected override string Trim(string lhs) => $"TRIM({lhs})";

src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal class SqliteBulkInsertProvider(ILogger<SqliteBulkInsertProvider>? logge
2424
protected override string AddTableCopyBulkInsertId => "--"; // No need to add an ID column in SQLite
2525

2626
/// <inheritdoc />
27-
protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_test_entity_{Guid.NewGuid():N}";
27+
protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_{Guid.NewGuid():N}";
2828

2929
/// <inheritdoc />
3030
protected override BulkInsertOptions CreateDefaultOptions() => new()

0 commit comments

Comments
 (0)