From c21966d2023194c2e7b260644c6cfee29040eba2 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 29 Oct 2025 22:52:49 +0100 Subject: [PATCH 1/3] Enhance temporary table name generation with random suffix for uniqueness --- .../MySqlBulkInsertProvider.cs | 2 +- .../OracleBulkInsertProvider.cs | 2 +- .../SqlServerBulkInsertProvider.cs | 2 +- .../SqliteBulkInsertProvider.cs | 2 +- .../BulkInsertProviderBase.cs | 3 ++- .../Helpers.cs | 19 ++++++++++++++ .../Tests/Basic/BasicTestsBase.cs | 25 +++++++++++++++++++ 7 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs index 42be248..fc14d5f 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs @@ -19,7 +19,7 @@ internal class MySqlBulkInsertProvider(ILogger logger) protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT AUTO_INCREMENT PRIMARY KEY;"; /// - protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}"; + protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}_{Helpers.RandomString(6)}"; /// protected override MySqlBulkInsertOptions CreateDefaultOptions() => new() diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs index 6f035ef..8ff7aab 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs @@ -24,7 +24,7 @@ internal class OracleBulkInsertProvider(ILogger? logge /// The temporary table name is generated with a GUID to ensure uniqueness, but limited to less than 30 characters, /// because Oracle prior 12.2 has a limit of 30 characters for identifiers. /// - protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert_{Guid.NewGuid().ToString("N")[..8]}"; + protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert__{Helpers.RandomString(6)}"; protected override OracleBulkInsertOptions CreateDefaultOptions() => new() { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs index 1465530..bedff35 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs @@ -17,7 +17,7 @@ internal class SqlServerBulkInsertProvider(ILogger? protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT IDENTITY PRIMARY KEY;"; /// - protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}"; + protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}_{Helpers.RandomString(6)}"; protected override SqlServerBulkInsertOptions CreateDefaultOptions() => new() { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs index 547a72c..7edca6a 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs @@ -24,7 +24,7 @@ internal class SqliteBulkInsertProvider(ILogger? logge protected override string AddTableCopyBulkInsertId => "--"; // No need to add an ID column in SQLite /// - protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_{Guid.NewGuid():N}"; + protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_{Helpers.RandomString(6)}"; /// protected override BulkInsertOptions CreateDefaultOptions() => new() diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs index c03e156..26d2610 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs @@ -19,7 +19,7 @@ internal abstract class BulkInsertProviderBase(ILogger? logg protected abstract string AddTableCopyBulkInsertId { get; } - protected virtual string GetTempTableName(string tableName) => $"_temp_bulk_insert_{tableName}"; + protected virtual string GetTempTableName(string tableName) => $"_temp_bulk_insert_{tableName}_{Helpers.RandomString(6)}"; protected override async IAsyncEnumerable BulkInsertReturnEntities( bool sync, @@ -124,6 +124,7 @@ protected override async Task BulkInsert( await connection.Close(sync, ctk); } } + private async Task PerformBulkInsertAsync( bool sync, DbContext context, diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs index f9ff635..2226ec1 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs @@ -6,6 +6,8 @@ namespace PhenX.EntityFrameworkCore.BulkInsert; internal static class Helpers { + private static readonly Random Random = new(); + public static StringBuilder AppendJoin(this StringBuilder sb, string separator, IEnumerable items, Action formatter) { var first = true; @@ -27,4 +29,21 @@ public static StringBuilder AppendColumns(this StringBuilder sb, IReadOnlyList c.QuotedColumName)); } + + /// + /// Generates a random alphanumeric string of the specified length. + /// + public static string RandomString(int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + + var sb = new StringBuilder(length); + + for (var i = 0; i < length; i++) + { + sb.Append(chars[Random.Next(chars.Length)]); + } + + return sb.ToString(); + } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index 6697dac..b815dfb 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -235,6 +235,31 @@ public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure(InsertSt Assert.Empty(insertedEntities); } + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithOpenTransaction_MultipleInserts(InsertStrategy strategy) + { + // Arrange + var entities = new List + { + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx1" }, + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx2" }, + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx3" }, + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx4" }, + }; + + // Act + await using var transaction = await _context.Database.BeginTransactionAsync(); + + var batches = entities.Chunk(2); + foreach (var batch in batches) + { + await _context.InsertWithStrategyAsync(strategy, batch.ToList()); + } + + await transaction.CommitAsync(); + } + [SkippableFact] public async Task ThrowsWhenUsingWrongConfigurationType() { From b4c3de45ee22fab32a1c2fe017d2db12547af23d Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 29 Oct 2025 23:01:59 +0100 Subject: [PATCH 2/3] Skip tests for Oracle provider due to direct path operation restriction --- .../Tests/Basic/BasicTestsBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index b815dfb..b09493c 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -239,6 +239,9 @@ public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure(InsertSt [CombinatorialData] public async Task InsertEntities_WithOpenTransaction_MultipleInserts(InsertStrategy strategy) { + // Oracle: ORA-39822: A new direct path operation is not allowed in the current transaction. + Skip.If(_context.IsProvider(ProviderType.Oracle)); + // Arrange var entities = new List { From ab0f088906852c346523b37d983db8004ee39d5a Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 29 Oct 2025 23:28:43 +0100 Subject: [PATCH 3/3] Refactor temporary table name generation for Oracle to use an 8-character random suffix for uniqueness and use Shared random generator --- .../OracleBulkInsertProvider.cs | 6 +++--- src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs index 8ff7aab..24333d6 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs @@ -21,10 +21,10 @@ internal class OracleBulkInsertProvider(ILogger? logge /// /// - /// The temporary table name is generated with a GUID to ensure uniqueness, but limited to less than 30 characters, - /// because Oracle prior 12.2 has a limit of 30 characters for identifiers. + /// The temporary table name is generated with a random 8-character suffix to ensure uniqueness, and is limited to less than 30 characters, + /// because Oracle prior to 12.2 has a limit of 30 characters for identifiers. /// - protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert__{Helpers.RandomString(6)}"; + protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert_{Helpers.RandomString(8)}"; protected override OracleBulkInsertOptions CreateDefaultOptions() => new() { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs index 2226ec1..e3e5429 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs @@ -6,8 +6,6 @@ namespace PhenX.EntityFrameworkCore.BulkInsert; internal static class Helpers { - private static readonly Random Random = new(); - public static StringBuilder AppendJoin(this StringBuilder sb, string separator, IEnumerable items, Action formatter) { var first = true; @@ -41,7 +39,7 @@ public static string RandomString(int length) for (var i = 0; i < length; i++) { - sb.Append(chars[Random.Next(chars.Length)]); + sb.Append(chars[Random.Shared.Next(chars.Length)]); } return sb.ToString();