-
-
Notifications
You must be signed in to change notification settings - Fork 10
Add oracle support #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
9241bd2
795e03b
6e2b8d7
df2e124
8606f69
4ce4dfa
479c41f
259c75a
b7b0503
0b30e4f
5058480
773658f
f6c9803
e43d86c
42aec43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| using Oracle.ManagedDataAccess.Client; | ||
|
|
||
| using PhenX.EntityFrameworkCore.BulkInsert.Options; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle; | ||
|
|
||
| /// <summary> | ||
| /// Options specific to Oracle bulk insert. | ||
| /// </summary> | ||
| public class OracleBulkInsertOptions : BulkInsertOptions | ||
| { | ||
| /// <inheritdoc cref="OracleBulkCopyOptions"/> | ||
| public OracleBulkCopyOptions CopyOptions { get; set; } = OracleBulkCopyOptions.Default; | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| using JetBrains.Annotations; | ||
|
|
||
| using Microsoft.EntityFrameworkCore; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| using Oracle.ManagedDataAccess.Client; | ||
|
|
||
| using PhenX.EntityFrameworkCore.BulkInsert.Metadata; | ||
| using PhenX.EntityFrameworkCore.BulkInsert.Options; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle; | ||
|
|
||
| [UsedImplicitly] | ||
| internal class OracleBulkInsertProvider(ILogger<OracleBulkInsertProvider>? logger) : BulkInsertProviderBase<OracleDialectBuilder, OracleBulkInsertOptions>(logger) | ||
| { | ||
| /// <inheritdoc /> | ||
| protected override string BulkInsertId => "ROWID"; | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override string AddTableCopyBulkInsertId => ""; // No need to add an ID column in Oracle | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert_{Guid.NewGuid().ToString("N")[..8]}"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you not take the whole guid?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oracle under a certain version (12 I think) does not allow indentifiers longer than 30, yay
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would probably comment that, because I would have forgotten that next week.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh I will remember 😅 but yeah I'll add a comment
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ahve said this very often to myself :D |
||
|
|
||
| protected override OracleBulkInsertOptions CreateDefaultOptions() => new() | ||
| { | ||
| BatchSize = 50_000, | ||
| }; | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override IAsyncEnumerable<T> BulkInsertReturnEntities<T>( | ||
| bool sync, | ||
| DbContext context, | ||
| TableMetadata tableInfo, | ||
| IEnumerable<T> entities, | ||
| OracleBulkInsertOptions options, | ||
| OnConflictOptions<T>? onConflict, | ||
| CancellationToken ctk) | ||
| { | ||
| throw new NotSupportedException("Provider does not support returning entities."); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override Task BulkInsert<T>( | ||
| bool sync, | ||
| DbContext context, | ||
| TableMetadata tableInfo, | ||
| IEnumerable<T> entities, | ||
| string tableName, | ||
| IReadOnlyList<ColumnMetadata> columns, | ||
| OracleBulkInsertOptions options, | ||
| CancellationToken ctk) | ||
| { | ||
| var connection = (OracleConnection) context.Database.GetDbConnection(); | ||
|
|
||
| using var bulkCopy = new OracleBulkCopy(connection, options.CopyOptions); | ||
|
|
||
| bulkCopy.DestinationTableName = tableInfo.QuotedTableName; | ||
| bulkCopy.BatchSize = options.BatchSize; | ||
| bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds(); | ||
|
|
||
| foreach (var column in columns) | ||
| { | ||
| bulkCopy.ColumnMappings.Add(column.PropertyName, column.QuotedColumName); | ||
| } | ||
|
|
||
| var dataReader = new EnumerableDataReader<T>(entities, columns, options); | ||
|
|
||
| bulkCopy.WriteToServer(dataReader); | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override async Task DropTempTableAsync(bool sync, DbContext dbContext, string tableName) | ||
| { | ||
| var commandText = $""" | ||
| BEGIN | ||
| EXECUTE IMMEDIATE 'DROP TABLE {tableName}'; | ||
| EXCEPTION | ||
| WHEN OTHERS THEN | ||
| IF SQLCODE != -942 THEN -- ORA-00942: table or view does not exist | ||
| RAISE; | ||
| END IF; | ||
| END; | ||
| """; | ||
|
|
||
| await ExecuteAsync(sync, dbContext, commandText, CancellationToken.None); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| using Microsoft.EntityFrameworkCore; | ||
|
|
||
| using PhenX.EntityFrameworkCore.BulkInsert.Extensions; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle; | ||
|
|
||
| /// <summary> | ||
| /// DbContext options extension for Oracle. | ||
| /// </summary> | ||
| public static class OracleDbContextOptionsExtensions | ||
| { | ||
| /// <summary> | ||
| /// Configures the DbContext to use the Oracle bulk insert provider. | ||
| /// </summary> | ||
| public static DbContextOptionsBuilder UseBulkInsertOracle(this DbContextOptionsBuilder optionsBuilder) | ||
| { | ||
| return optionsBuilder.UseProvider<OracleBulkInsertProvider>(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| using System.Text; | ||
|
|
||
| using Microsoft.EntityFrameworkCore; | ||
|
|
||
| using PhenX.EntityFrameworkCore.BulkInsert.Dialect; | ||
| using PhenX.EntityFrameworkCore.BulkInsert.Metadata; | ||
| using PhenX.EntityFrameworkCore.BulkInsert.Options; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle; | ||
|
|
||
| internal class OracleDialectBuilder : SqlDialectBuilder | ||
| { | ||
| protected override string OpenDelimiter => "\""; | ||
| protected override string CloseDelimiter => "\""; | ||
| protected override string ConcatOperator => "||"; | ||
|
|
||
| protected override bool SupportsMoveRows => false; | ||
|
|
||
| public override string CreateTableCopySql(string tempTableName, TableMetadata tableInfo, IReadOnlyList<ColumnMetadata> columns) | ||
| { | ||
| var q = new StringBuilder(); | ||
| q.Append($"CREATE TABLE {tempTableName} ("); | ||
|
|
||
| foreach (var column in columns) | ||
| { | ||
| q.Append($"{column.QuotedColumName} {column.StoreDefinition}"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I have made an extension method for this case AppenJoined(",\n", column, (c, sb) => sb.Append("...")); or something like that |
||
| if (column != columns[^1]) | ||
| { | ||
| q.Append(','); | ||
| } | ||
| q.AppendLine(); | ||
| } | ||
|
|
||
| q.AppendLine(")"); | ||
|
|
||
| return q.ToString(); | ||
| } | ||
|
|
||
| public override string BuildMoveDataSql<T>( | ||
| DbContext context, | ||
| TableMetadata target, | ||
| string source, | ||
| IReadOnlyList<ColumnMetadata> insertedColumns, | ||
| IReadOnlyList<ColumnMetadata> returnedColumns, | ||
| BulkInsertOptions options, | ||
| OnConflictOptions? onConflict = null) | ||
| { | ||
| var q = new StringBuilder(); | ||
|
|
||
| // Merge handling | ||
| if (onConflict is OnConflictOptions<T> onConflictTyped) | ||
| { | ||
| IEnumerable<string> matchColumns; | ||
| if (onConflictTyped.Match != null) | ||
| { | ||
| matchColumns = GetColumns(target, onConflictTyped.Match); | ||
| } | ||
| else if (target.PrimaryKey.Count > 0) | ||
| { | ||
| matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName); | ||
| } | ||
| else | ||
| { | ||
| throw new InvalidOperationException("Table has no primary key that can be used for conflict detection."); | ||
| } | ||
|
|
||
| q.AppendLine($"MERGE INTO {target.QuotedTableName} AS TARGET"); | ||
|
|
||
| q.Append("USING (SELECT "); | ||
| q.AppendColumns(insertedColumns); | ||
| q.Append($" FROM {source}) AS SOURCE ("); | ||
| q.AppendColumns(insertedColumns); | ||
| q.AppendLine(")"); | ||
|
|
||
| q.Append("ON "); | ||
| q.AppendJoin(" AND ", matchColumns, (b, col) => b.Append($"TARGET.{col} = SOURCE.{col}")); | ||
| q.AppendLine(); | ||
|
|
||
| if (onConflictTyped.Update != null) | ||
| { | ||
| var columns = target.GetColumns(false); | ||
|
|
||
| q.AppendLine("WHEN MATCHED THEN UPDATE SET "); | ||
| q.AppendJoin(", ", GetUpdates(context, target, columns, onConflictTyped.Update)); | ||
| q.AppendLine(); | ||
| } | ||
|
|
||
| q.Append("WHEN NOT MATCHED THEN INSERT ("); | ||
| q.AppendColumns(insertedColumns); | ||
| q.AppendLine(")"); | ||
|
|
||
| q.Append("VALUES ("); | ||
| q.AppendJoin(", ", insertedColumns, (b, col) => b.Append($"SOURCE.{col.QuotedColumName}")); | ||
| q.AppendLine(")"); | ||
|
|
||
| if (returnedColumns.Count != 0) | ||
| { | ||
| q.Append("OUTPUT "); | ||
| q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($"INSERTED.{col.QuotedColumName} AS {col.QuotedColumName}")); | ||
| q.AppendLine(); | ||
| } | ||
| } | ||
|
|
||
| // No conflict handling | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps two methods so that the comments are not necessary? |
||
| else | ||
| { | ||
| q.Append($"INSERT INTO {target.QuotedTableName} ("); | ||
| q.AppendColumns(insertedColumns); | ||
| q.AppendLine(")"); | ||
| q.Append("SELECT "); | ||
| q.AppendColumns(insertedColumns); | ||
| q.AppendLine(); | ||
| q.Append($"FROM {source}"); | ||
| q.AppendLine(); | ||
|
|
||
| if (returnedColumns.Count != 0) | ||
| { | ||
| q.Append("RETURNING "); | ||
| q.AppendJoin(", ", returnedColumns, (b, col) => b.Append(col.QuotedColumName)); | ||
| q.Append(" INTO "); | ||
| q.AppendJoin(", ", returnedColumns, (b, col) => b.Append($":{col.ColumnName}")); | ||
| q.AppendLine(); | ||
| } | ||
| } | ||
|
|
||
| q.AppendLine(";"); | ||
|
|
||
| var result = q.ToString(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you not got rid of this syntax in another PR aka return q.ToString(); |
||
| return result; | ||
| } | ||
|
|
||
| protected override string GetExcludedColumnName(string columnName) | ||
| { | ||
| return $"SOURCE.{columnName}"; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Oracle.EntityFrameworkCore" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.23.80" /> | ||
| <PackageReference Include="Oracle.EntityFrameworkCore" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.23.80" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty line